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, Rgba, SemanticVersion, TestAppContext, UpdateGlobal,
19 VisualTestContext, WindowBounds, WindowOptions, div,
20};
21use indoc::indoc;
22use language::{
23 BracketPairConfig,
24 Capability::ReadWrite,
25 DiagnosticSourceKind, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher,
26 LanguageName, Override, Point,
27 language_settings::{
28 AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings, FormatterList,
29 LanguageSettingsContent, LspInsertMode, PrettierSettings, SelectedFormatter,
30 },
31 tree_sitter_python,
32};
33use language_settings::{Formatter, 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, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
59 OpenOptions, ViewId,
60 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
61};
62
63#[gpui::test]
64fn test_edit_events(cx: &mut TestAppContext) {
65 init_test(cx, |_| {});
66
67 let buffer = cx.new(|cx| {
68 let mut buffer = language::Buffer::local("123456", cx);
69 buffer.set_group_interval(Duration::from_secs(1));
70 buffer
71 });
72
73 let events = Rc::new(RefCell::new(Vec::new()));
74 let editor1 = cx.add_window({
75 let events = events.clone();
76 |window, cx| {
77 let entity = cx.entity().clone();
78 cx.subscribe_in(
79 &entity,
80 window,
81 move |_, _, event: &EditorEvent, _, _| match event {
82 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
83 EditorEvent::BufferEdited => {
84 events.borrow_mut().push(("editor1", "buffer edited"))
85 }
86 _ => {}
87 },
88 )
89 .detach();
90 Editor::for_buffer(buffer.clone(), None, window, cx)
91 }
92 });
93
94 let editor2 = cx.add_window({
95 let events = events.clone();
96 |window, cx| {
97 cx.subscribe_in(
98 &cx.entity().clone(),
99 window,
100 move |_, _, event: &EditorEvent, _, _| match event {
101 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
102 EditorEvent::BufferEdited => {
103 events.borrow_mut().push(("editor2", "buffer edited"))
104 }
105 _ => {}
106 },
107 )
108 .detach();
109 Editor::for_buffer(buffer.clone(), None, window, cx)
110 }
111 });
112
113 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
114
115 // Mutating editor 1 will emit an `Edited` event only for that editor.
116 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
117 assert_eq!(
118 mem::take(&mut *events.borrow_mut()),
119 [
120 ("editor1", "edited"),
121 ("editor1", "buffer edited"),
122 ("editor2", "buffer edited"),
123 ]
124 );
125
126 // Mutating editor 2 will emit an `Edited` event only for that editor.
127 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
128 assert_eq!(
129 mem::take(&mut *events.borrow_mut()),
130 [
131 ("editor2", "edited"),
132 ("editor1", "buffer edited"),
133 ("editor2", "buffer edited"),
134 ]
135 );
136
137 // Undoing on editor 1 will emit an `Edited` event only for that editor.
138 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
139 assert_eq!(
140 mem::take(&mut *events.borrow_mut()),
141 [
142 ("editor1", "edited"),
143 ("editor1", "buffer edited"),
144 ("editor2", "buffer edited"),
145 ]
146 );
147
148 // Redoing on editor 1 will emit an `Edited` event only for that editor.
149 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
150 assert_eq!(
151 mem::take(&mut *events.borrow_mut()),
152 [
153 ("editor1", "edited"),
154 ("editor1", "buffer edited"),
155 ("editor2", "buffer edited"),
156 ]
157 );
158
159 // Undoing on editor 2 will emit an `Edited` event only for that editor.
160 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
161 assert_eq!(
162 mem::take(&mut *events.borrow_mut()),
163 [
164 ("editor2", "edited"),
165 ("editor1", "buffer edited"),
166 ("editor2", "buffer edited"),
167 ]
168 );
169
170 // Redoing on editor 2 will emit an `Edited` event only for that editor.
171 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
172 assert_eq!(
173 mem::take(&mut *events.borrow_mut()),
174 [
175 ("editor2", "edited"),
176 ("editor1", "buffer edited"),
177 ("editor2", "buffer edited"),
178 ]
179 );
180
181 // No event is emitted when the mutation is a no-op.
182 _ = editor2.update(cx, |editor, window, cx| {
183 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
184 s.select_ranges([0..0])
185 });
186
187 editor.backspace(&Backspace, window, cx);
188 });
189 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
190}
191
192#[gpui::test]
193fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
194 init_test(cx, |_| {});
195
196 let mut now = Instant::now();
197 let group_interval = Duration::from_millis(1);
198 let buffer = cx.new(|cx| {
199 let mut buf = language::Buffer::local("123456", cx);
200 buf.set_group_interval(group_interval);
201 buf
202 });
203 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
204 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
205
206 _ = editor.update(cx, |editor, window, cx| {
207 editor.start_transaction_at(now, window, cx);
208 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
209 s.select_ranges([2..4])
210 });
211
212 editor.insert("cd", window, cx);
213 editor.end_transaction_at(now, cx);
214 assert_eq!(editor.text(cx), "12cd56");
215 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
216
217 editor.start_transaction_at(now, window, cx);
218 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
219 s.select_ranges([4..5])
220 });
221 editor.insert("e", window, cx);
222 editor.end_transaction_at(now, cx);
223 assert_eq!(editor.text(cx), "12cde6");
224 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
225
226 now += group_interval + Duration::from_millis(1);
227 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
228 s.select_ranges([2..2])
229 });
230
231 // Simulate an edit in another editor
232 buffer.update(cx, |buffer, cx| {
233 buffer.start_transaction_at(now, cx);
234 buffer.edit([(0..1, "a")], None, cx);
235 buffer.edit([(1..1, "b")], None, cx);
236 buffer.end_transaction_at(now, cx);
237 });
238
239 assert_eq!(editor.text(cx), "ab2cde6");
240 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
241
242 // Last transaction happened past the group interval in a different editor.
243 // Undo it individually and don't restore selections.
244 editor.undo(&Undo, window, cx);
245 assert_eq!(editor.text(cx), "12cde6");
246 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
247
248 // First two transactions happened within the group interval in this editor.
249 // Undo them together and restore selections.
250 editor.undo(&Undo, window, cx);
251 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
252 assert_eq!(editor.text(cx), "123456");
253 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
254
255 // Redo the first two transactions together.
256 editor.redo(&Redo, window, cx);
257 assert_eq!(editor.text(cx), "12cde6");
258 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
259
260 // Redo the last transaction on its own.
261 editor.redo(&Redo, window, cx);
262 assert_eq!(editor.text(cx), "ab2cde6");
263 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
264
265 // Test empty transactions.
266 editor.start_transaction_at(now, window, cx);
267 editor.end_transaction_at(now, cx);
268 editor.undo(&Undo, window, cx);
269 assert_eq!(editor.text(cx), "12cde6");
270 });
271}
272
273#[gpui::test]
274fn test_ime_composition(cx: &mut TestAppContext) {
275 init_test(cx, |_| {});
276
277 let buffer = cx.new(|cx| {
278 let mut buffer = language::Buffer::local("abcde", cx);
279 // Ensure automatic grouping doesn't occur.
280 buffer.set_group_interval(Duration::ZERO);
281 buffer
282 });
283
284 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
285 cx.add_window(|window, cx| {
286 let mut editor = build_editor(buffer.clone(), window, cx);
287
288 // Start a new IME composition.
289 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
290 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
291 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
292 assert_eq!(editor.text(cx), "äbcde");
293 assert_eq!(
294 editor.marked_text_ranges(cx),
295 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
296 );
297
298 // Finalize IME composition.
299 editor.replace_text_in_range(None, "ā", window, cx);
300 assert_eq!(editor.text(cx), "ābcde");
301 assert_eq!(editor.marked_text_ranges(cx), None);
302
303 // IME composition edits are grouped and are undone/redone at once.
304 editor.undo(&Default::default(), window, cx);
305 assert_eq!(editor.text(cx), "abcde");
306 assert_eq!(editor.marked_text_ranges(cx), None);
307 editor.redo(&Default::default(), window, cx);
308 assert_eq!(editor.text(cx), "ābcde");
309 assert_eq!(editor.marked_text_ranges(cx), None);
310
311 // Start a new IME composition.
312 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
313 assert_eq!(
314 editor.marked_text_ranges(cx),
315 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
316 );
317
318 // Undoing during an IME composition cancels it.
319 editor.undo(&Default::default(), window, cx);
320 assert_eq!(editor.text(cx), "ābcde");
321 assert_eq!(editor.marked_text_ranges(cx), None);
322
323 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
324 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
325 assert_eq!(editor.text(cx), "ābcdè");
326 assert_eq!(
327 editor.marked_text_ranges(cx),
328 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
329 );
330
331 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
332 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
333 assert_eq!(editor.text(cx), "ābcdę");
334 assert_eq!(editor.marked_text_ranges(cx), None);
335
336 // Start a new IME composition with multiple cursors.
337 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
338 s.select_ranges([
339 OffsetUtf16(1)..OffsetUtf16(1),
340 OffsetUtf16(3)..OffsetUtf16(3),
341 OffsetUtf16(5)..OffsetUtf16(5),
342 ])
343 });
344 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
345 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
346 assert_eq!(
347 editor.marked_text_ranges(cx),
348 Some(vec![
349 OffsetUtf16(0)..OffsetUtf16(3),
350 OffsetUtf16(4)..OffsetUtf16(7),
351 OffsetUtf16(8)..OffsetUtf16(11)
352 ])
353 );
354
355 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
356 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
357 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
358 assert_eq!(
359 editor.marked_text_ranges(cx),
360 Some(vec![
361 OffsetUtf16(1)..OffsetUtf16(2),
362 OffsetUtf16(5)..OffsetUtf16(6),
363 OffsetUtf16(9)..OffsetUtf16(10)
364 ])
365 );
366
367 // Finalize IME composition with multiple cursors.
368 editor.replace_text_in_range(Some(9..10), "2", window, cx);
369 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
370 assert_eq!(editor.marked_text_ranges(cx), None);
371
372 editor
373 });
374}
375
376#[gpui::test]
377fn test_selection_with_mouse(cx: &mut TestAppContext) {
378 init_test(cx, |_| {});
379
380 let editor = cx.add_window(|window, cx| {
381 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
382 build_editor(buffer, window, cx)
383 });
384
385 _ = editor.update(cx, |editor, window, cx| {
386 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
387 });
388 assert_eq!(
389 editor
390 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
391 .unwrap(),
392 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
393 );
394
395 _ = editor.update(cx, |editor, window, cx| {
396 editor.update_selection(
397 DisplayPoint::new(DisplayRow(3), 3),
398 0,
399 gpui::Point::<f32>::default(),
400 window,
401 cx,
402 );
403 });
404
405 assert_eq!(
406 editor
407 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
408 .unwrap(),
409 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
410 );
411
412 _ = editor.update(cx, |editor, window, cx| {
413 editor.update_selection(
414 DisplayPoint::new(DisplayRow(1), 1),
415 0,
416 gpui::Point::<f32>::default(),
417 window,
418 cx,
419 );
420 });
421
422 assert_eq!(
423 editor
424 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
425 .unwrap(),
426 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
427 );
428
429 _ = editor.update(cx, |editor, window, cx| {
430 editor.end_selection(window, cx);
431 editor.update_selection(
432 DisplayPoint::new(DisplayRow(3), 3),
433 0,
434 gpui::Point::<f32>::default(),
435 window,
436 cx,
437 );
438 });
439
440 assert_eq!(
441 editor
442 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
443 .unwrap(),
444 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
445 );
446
447 _ = editor.update(cx, |editor, window, cx| {
448 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
449 editor.update_selection(
450 DisplayPoint::new(DisplayRow(0), 0),
451 0,
452 gpui::Point::<f32>::default(),
453 window,
454 cx,
455 );
456 });
457
458 assert_eq!(
459 editor
460 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
461 .unwrap(),
462 [
463 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
464 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
465 ]
466 );
467
468 _ = editor.update(cx, |editor, window, cx| {
469 editor.end_selection(window, cx);
470 });
471
472 assert_eq!(
473 editor
474 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
475 .unwrap(),
476 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
477 );
478}
479
480#[gpui::test]
481fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
482 init_test(cx, |_| {});
483
484 let editor = cx.add_window(|window, cx| {
485 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
486 build_editor(buffer, window, cx)
487 });
488
489 _ = editor.update(cx, |editor, window, cx| {
490 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
491 });
492
493 _ = editor.update(cx, |editor, window, cx| {
494 editor.end_selection(window, cx);
495 });
496
497 _ = editor.update(cx, |editor, window, cx| {
498 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
499 });
500
501 _ = editor.update(cx, |editor, window, cx| {
502 editor.end_selection(window, cx);
503 });
504
505 assert_eq!(
506 editor
507 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
508 .unwrap(),
509 [
510 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
511 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
512 ]
513 );
514
515 _ = editor.update(cx, |editor, window, cx| {
516 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
517 });
518
519 _ = editor.update(cx, |editor, window, cx| {
520 editor.end_selection(window, cx);
521 });
522
523 assert_eq!(
524 editor
525 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
526 .unwrap(),
527 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
528 );
529}
530
531#[gpui::test]
532fn test_canceling_pending_selection(cx: &mut TestAppContext) {
533 init_test(cx, |_| {});
534
535 let editor = cx.add_window(|window, cx| {
536 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
537 build_editor(buffer, window, cx)
538 });
539
540 _ = editor.update(cx, |editor, window, cx| {
541 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
542 assert_eq!(
543 editor.selections.display_ranges(cx),
544 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
545 );
546 });
547
548 _ = editor.update(cx, |editor, window, cx| {
549 editor.update_selection(
550 DisplayPoint::new(DisplayRow(3), 3),
551 0,
552 gpui::Point::<f32>::default(),
553 window,
554 cx,
555 );
556 assert_eq!(
557 editor.selections.display_ranges(cx),
558 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
559 );
560 });
561
562 _ = editor.update(cx, |editor, window, cx| {
563 editor.cancel(&Cancel, window, cx);
564 editor.update_selection(
565 DisplayPoint::new(DisplayRow(1), 1),
566 0,
567 gpui::Point::<f32>::default(),
568 window,
569 cx,
570 );
571 assert_eq!(
572 editor.selections.display_ranges(cx),
573 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
574 );
575 });
576}
577
578#[gpui::test]
579fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
580 init_test(cx, |_| {});
581
582 let editor = cx.add_window(|window, cx| {
583 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
584 build_editor(buffer, window, cx)
585 });
586
587 _ = editor.update(cx, |editor, window, cx| {
588 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
589 assert_eq!(
590 editor.selections.display_ranges(cx),
591 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
592 );
593
594 editor.move_down(&Default::default(), window, cx);
595 assert_eq!(
596 editor.selections.display_ranges(cx),
597 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
598 );
599
600 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
601 assert_eq!(
602 editor.selections.display_ranges(cx),
603 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
604 );
605
606 editor.move_up(&Default::default(), window, cx);
607 assert_eq!(
608 editor.selections.display_ranges(cx),
609 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
610 );
611 });
612}
613
614#[gpui::test]
615fn test_clone(cx: &mut TestAppContext) {
616 init_test(cx, |_| {});
617
618 let (text, selection_ranges) = marked_text_ranges(
619 indoc! {"
620 one
621 two
622 threeˇ
623 four
624 fiveˇ
625 "},
626 true,
627 );
628
629 let editor = cx.add_window(|window, cx| {
630 let buffer = MultiBuffer::build_simple(&text, cx);
631 build_editor(buffer, window, cx)
632 });
633
634 _ = editor.update(cx, |editor, window, cx| {
635 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
636 s.select_ranges(selection_ranges.clone())
637 });
638 editor.fold_creases(
639 vec![
640 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
641 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
642 ],
643 true,
644 window,
645 cx,
646 );
647 });
648
649 let cloned_editor = editor
650 .update(cx, |editor, _, cx| {
651 cx.open_window(Default::default(), |window, cx| {
652 cx.new(|cx| editor.clone(window, cx))
653 })
654 })
655 .unwrap()
656 .unwrap();
657
658 let snapshot = editor
659 .update(cx, |e, window, cx| e.snapshot(window, cx))
660 .unwrap();
661 let cloned_snapshot = cloned_editor
662 .update(cx, |e, window, cx| e.snapshot(window, cx))
663 .unwrap();
664
665 assert_eq!(
666 cloned_editor
667 .update(cx, |e, _, cx| e.display_text(cx))
668 .unwrap(),
669 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
670 );
671 assert_eq!(
672 cloned_snapshot
673 .folds_in_range(0..text.len())
674 .collect::<Vec<_>>(),
675 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
676 );
677 assert_set_eq!(
678 cloned_editor
679 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
680 .unwrap(),
681 editor
682 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
683 .unwrap()
684 );
685 assert_set_eq!(
686 cloned_editor
687 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
688 .unwrap(),
689 editor
690 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
691 .unwrap()
692 );
693}
694
695#[gpui::test]
696async fn test_navigation_history(cx: &mut TestAppContext) {
697 init_test(cx, |_| {});
698
699 use workspace::item::Item;
700
701 let fs = FakeFs::new(cx.executor());
702 let project = Project::test(fs, [], cx).await;
703 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
704 let pane = workspace
705 .update(cx, |workspace, _, _| workspace.active_pane().clone())
706 .unwrap();
707
708 _ = workspace.update(cx, |_v, window, cx| {
709 cx.new(|cx| {
710 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
711 let mut editor = build_editor(buffer.clone(), window, cx);
712 let handle = cx.entity();
713 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
714
715 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
716 editor.nav_history.as_mut().unwrap().pop_backward(cx)
717 }
718
719 // Move the cursor a small distance.
720 // Nothing is added to the navigation history.
721 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
722 s.select_display_ranges([
723 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
724 ])
725 });
726 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
727 s.select_display_ranges([
728 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
729 ])
730 });
731 assert!(pop_history(&mut editor, cx).is_none());
732
733 // Move the cursor a large distance.
734 // The history can jump back to the previous position.
735 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
736 s.select_display_ranges([
737 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
738 ])
739 });
740 let nav_entry = pop_history(&mut editor, cx).unwrap();
741 editor.navigate(nav_entry.data.unwrap(), window, cx);
742 assert_eq!(nav_entry.item.id(), cx.entity_id());
743 assert_eq!(
744 editor.selections.display_ranges(cx),
745 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
746 );
747 assert!(pop_history(&mut editor, cx).is_none());
748
749 // Move the cursor a small distance via the mouse.
750 // Nothing is added to the navigation history.
751 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
752 editor.end_selection(window, cx);
753 assert_eq!(
754 editor.selections.display_ranges(cx),
755 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
756 );
757 assert!(pop_history(&mut editor, cx).is_none());
758
759 // Move the cursor a large distance via the mouse.
760 // The history can jump back to the previous position.
761 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
762 editor.end_selection(window, cx);
763 assert_eq!(
764 editor.selections.display_ranges(cx),
765 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
766 );
767 let nav_entry = pop_history(&mut editor, cx).unwrap();
768 editor.navigate(nav_entry.data.unwrap(), window, cx);
769 assert_eq!(nav_entry.item.id(), cx.entity_id());
770 assert_eq!(
771 editor.selections.display_ranges(cx),
772 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
773 );
774 assert!(pop_history(&mut editor, cx).is_none());
775
776 // Set scroll position to check later
777 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
778 let original_scroll_position = editor.scroll_manager.anchor();
779
780 // Jump to the end of the document and adjust scroll
781 editor.move_to_end(&MoveToEnd, window, cx);
782 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
783 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
784
785 let nav_entry = pop_history(&mut editor, cx).unwrap();
786 editor.navigate(nav_entry.data.unwrap(), window, cx);
787 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
788
789 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
790 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
791 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
792 let invalid_point = Point::new(9999, 0);
793 editor.navigate(
794 Box::new(NavigationData {
795 cursor_anchor: invalid_anchor,
796 cursor_position: invalid_point,
797 scroll_anchor: ScrollAnchor {
798 anchor: invalid_anchor,
799 offset: Default::default(),
800 },
801 scroll_top_row: invalid_point.row,
802 }),
803 window,
804 cx,
805 );
806 assert_eq!(
807 editor.selections.display_ranges(cx),
808 &[editor.max_point(cx)..editor.max_point(cx)]
809 );
810 assert_eq!(
811 editor.scroll_position(cx),
812 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
813 );
814
815 editor
816 })
817 });
818}
819
820#[gpui::test]
821fn test_cancel(cx: &mut TestAppContext) {
822 init_test(cx, |_| {});
823
824 let editor = cx.add_window(|window, cx| {
825 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
826 build_editor(buffer, window, cx)
827 });
828
829 _ = editor.update(cx, |editor, window, cx| {
830 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
831 editor.update_selection(
832 DisplayPoint::new(DisplayRow(1), 1),
833 0,
834 gpui::Point::<f32>::default(),
835 window,
836 cx,
837 );
838 editor.end_selection(window, cx);
839
840 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
841 editor.update_selection(
842 DisplayPoint::new(DisplayRow(0), 3),
843 0,
844 gpui::Point::<f32>::default(),
845 window,
846 cx,
847 );
848 editor.end_selection(window, cx);
849 assert_eq!(
850 editor.selections.display_ranges(cx),
851 [
852 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
853 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
854 ]
855 );
856 });
857
858 _ = editor.update(cx, |editor, window, cx| {
859 editor.cancel(&Cancel, window, cx);
860 assert_eq!(
861 editor.selections.display_ranges(cx),
862 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
863 );
864 });
865
866 _ = editor.update(cx, |editor, window, cx| {
867 editor.cancel(&Cancel, window, cx);
868 assert_eq!(
869 editor.selections.display_ranges(cx),
870 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
871 );
872 });
873}
874
875#[gpui::test]
876fn test_fold_action(cx: &mut TestAppContext) {
877 init_test(cx, |_| {});
878
879 let editor = cx.add_window(|window, cx| {
880 let buffer = MultiBuffer::build_simple(
881 &"
882 impl Foo {
883 // Hello!
884
885 fn a() {
886 1
887 }
888
889 fn b() {
890 2
891 }
892
893 fn c() {
894 3
895 }
896 }
897 "
898 .unindent(),
899 cx,
900 );
901 build_editor(buffer.clone(), window, cx)
902 });
903
904 _ = editor.update(cx, |editor, window, cx| {
905 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
906 s.select_display_ranges([
907 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
908 ]);
909 });
910 editor.fold(&Fold, window, cx);
911 assert_eq!(
912 editor.display_text(cx),
913 "
914 impl Foo {
915 // Hello!
916
917 fn a() {
918 1
919 }
920
921 fn b() {⋯
922 }
923
924 fn c() {⋯
925 }
926 }
927 "
928 .unindent(),
929 );
930
931 editor.fold(&Fold, window, cx);
932 assert_eq!(
933 editor.display_text(cx),
934 "
935 impl Foo {⋯
936 }
937 "
938 .unindent(),
939 );
940
941 editor.unfold_lines(&UnfoldLines, window, cx);
942 assert_eq!(
943 editor.display_text(cx),
944 "
945 impl Foo {
946 // Hello!
947
948 fn a() {
949 1
950 }
951
952 fn b() {⋯
953 }
954
955 fn c() {⋯
956 }
957 }
958 "
959 .unindent(),
960 );
961
962 editor.unfold_lines(&UnfoldLines, window, cx);
963 assert_eq!(
964 editor.display_text(cx),
965 editor.buffer.read(cx).read(cx).text()
966 );
967 });
968}
969
970#[gpui::test]
971fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
972 init_test(cx, |_| {});
973
974 let editor = cx.add_window(|window, cx| {
975 let buffer = MultiBuffer::build_simple(
976 &"
977 class Foo:
978 # Hello!
979
980 def a():
981 print(1)
982
983 def b():
984 print(2)
985
986 def c():
987 print(3)
988 "
989 .unindent(),
990 cx,
991 );
992 build_editor(buffer.clone(), window, cx)
993 });
994
995 _ = editor.update(cx, |editor, window, cx| {
996 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
997 s.select_display_ranges([
998 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
999 ]);
1000 });
1001 editor.fold(&Fold, window, cx);
1002 assert_eq!(
1003 editor.display_text(cx),
1004 "
1005 class Foo:
1006 # Hello!
1007
1008 def a():
1009 print(1)
1010
1011 def b():⋯
1012
1013 def c():⋯
1014 "
1015 .unindent(),
1016 );
1017
1018 editor.fold(&Fold, window, cx);
1019 assert_eq!(
1020 editor.display_text(cx),
1021 "
1022 class Foo:⋯
1023 "
1024 .unindent(),
1025 );
1026
1027 editor.unfold_lines(&UnfoldLines, window, cx);
1028 assert_eq!(
1029 editor.display_text(cx),
1030 "
1031 class Foo:
1032 # Hello!
1033
1034 def a():
1035 print(1)
1036
1037 def b():⋯
1038
1039 def c():⋯
1040 "
1041 .unindent(),
1042 );
1043
1044 editor.unfold_lines(&UnfoldLines, window, cx);
1045 assert_eq!(
1046 editor.display_text(cx),
1047 editor.buffer.read(cx).read(cx).text()
1048 );
1049 });
1050}
1051
1052#[gpui::test]
1053fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1054 init_test(cx, |_| {});
1055
1056 let editor = cx.add_window(|window, cx| {
1057 let buffer = MultiBuffer::build_simple(
1058 &"
1059 class Foo:
1060 # Hello!
1061
1062 def a():
1063 print(1)
1064
1065 def b():
1066 print(2)
1067
1068
1069 def c():
1070 print(3)
1071
1072
1073 "
1074 .unindent(),
1075 cx,
1076 );
1077 build_editor(buffer.clone(), window, cx)
1078 });
1079
1080 _ = editor.update(cx, |editor, window, cx| {
1081 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1082 s.select_display_ranges([
1083 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1084 ]);
1085 });
1086 editor.fold(&Fold, window, cx);
1087 assert_eq!(
1088 editor.display_text(cx),
1089 "
1090 class Foo:
1091 # Hello!
1092
1093 def a():
1094 print(1)
1095
1096 def b():⋯
1097
1098
1099 def c():⋯
1100
1101
1102 "
1103 .unindent(),
1104 );
1105
1106 editor.fold(&Fold, window, cx);
1107 assert_eq!(
1108 editor.display_text(cx),
1109 "
1110 class Foo:⋯
1111
1112
1113 "
1114 .unindent(),
1115 );
1116
1117 editor.unfold_lines(&UnfoldLines, window, cx);
1118 assert_eq!(
1119 editor.display_text(cx),
1120 "
1121 class Foo:
1122 # Hello!
1123
1124 def a():
1125 print(1)
1126
1127 def b():⋯
1128
1129
1130 def c():⋯
1131
1132
1133 "
1134 .unindent(),
1135 );
1136
1137 editor.unfold_lines(&UnfoldLines, window, cx);
1138 assert_eq!(
1139 editor.display_text(cx),
1140 editor.buffer.read(cx).read(cx).text()
1141 );
1142 });
1143}
1144
1145#[gpui::test]
1146fn test_fold_at_level(cx: &mut TestAppContext) {
1147 init_test(cx, |_| {});
1148
1149 let editor = cx.add_window(|window, cx| {
1150 let buffer = MultiBuffer::build_simple(
1151 &"
1152 class Foo:
1153 # Hello!
1154
1155 def a():
1156 print(1)
1157
1158 def b():
1159 print(2)
1160
1161
1162 class Bar:
1163 # World!
1164
1165 def a():
1166 print(1)
1167
1168 def b():
1169 print(2)
1170
1171
1172 "
1173 .unindent(),
1174 cx,
1175 );
1176 build_editor(buffer.clone(), window, cx)
1177 });
1178
1179 _ = editor.update(cx, |editor, window, cx| {
1180 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1181 assert_eq!(
1182 editor.display_text(cx),
1183 "
1184 class Foo:
1185 # Hello!
1186
1187 def a():⋯
1188
1189 def b():⋯
1190
1191
1192 class Bar:
1193 # World!
1194
1195 def a():⋯
1196
1197 def b():⋯
1198
1199
1200 "
1201 .unindent(),
1202 );
1203
1204 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1205 assert_eq!(
1206 editor.display_text(cx),
1207 "
1208 class Foo:⋯
1209
1210
1211 class Bar:⋯
1212
1213
1214 "
1215 .unindent(),
1216 );
1217
1218 editor.unfold_all(&UnfoldAll, window, cx);
1219 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1220 assert_eq!(
1221 editor.display_text(cx),
1222 "
1223 class Foo:
1224 # Hello!
1225
1226 def a():
1227 print(1)
1228
1229 def b():
1230 print(2)
1231
1232
1233 class Bar:
1234 # World!
1235
1236 def a():
1237 print(1)
1238
1239 def b():
1240 print(2)
1241
1242
1243 "
1244 .unindent(),
1245 );
1246
1247 assert_eq!(
1248 editor.display_text(cx),
1249 editor.buffer.read(cx).read(cx).text()
1250 );
1251 });
1252}
1253
1254#[gpui::test]
1255fn test_move_cursor(cx: &mut TestAppContext) {
1256 init_test(cx, |_| {});
1257
1258 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1259 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1260
1261 buffer.update(cx, |buffer, cx| {
1262 buffer.edit(
1263 vec![
1264 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1265 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1266 ],
1267 None,
1268 cx,
1269 );
1270 });
1271 _ = editor.update(cx, |editor, window, cx| {
1272 assert_eq!(
1273 editor.selections.display_ranges(cx),
1274 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1275 );
1276
1277 editor.move_down(&MoveDown, window, cx);
1278 assert_eq!(
1279 editor.selections.display_ranges(cx),
1280 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1281 );
1282
1283 editor.move_right(&MoveRight, window, cx);
1284 assert_eq!(
1285 editor.selections.display_ranges(cx),
1286 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1287 );
1288
1289 editor.move_left(&MoveLeft, window, cx);
1290 assert_eq!(
1291 editor.selections.display_ranges(cx),
1292 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1293 );
1294
1295 editor.move_up(&MoveUp, window, cx);
1296 assert_eq!(
1297 editor.selections.display_ranges(cx),
1298 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1299 );
1300
1301 editor.move_to_end(&MoveToEnd, window, cx);
1302 assert_eq!(
1303 editor.selections.display_ranges(cx),
1304 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1305 );
1306
1307 editor.move_to_beginning(&MoveToBeginning, window, cx);
1308 assert_eq!(
1309 editor.selections.display_ranges(cx),
1310 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1311 );
1312
1313 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1314 s.select_display_ranges([
1315 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1316 ]);
1317 });
1318 editor.select_to_beginning(&SelectToBeginning, window, cx);
1319 assert_eq!(
1320 editor.selections.display_ranges(cx),
1321 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1322 );
1323
1324 editor.select_to_end(&SelectToEnd, window, cx);
1325 assert_eq!(
1326 editor.selections.display_ranges(cx),
1327 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1328 );
1329 });
1330}
1331
1332#[gpui::test]
1333fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1334 init_test(cx, |_| {});
1335
1336 let editor = cx.add_window(|window, cx| {
1337 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1338 build_editor(buffer.clone(), window, cx)
1339 });
1340
1341 assert_eq!('🟥'.len_utf8(), 4);
1342 assert_eq!('α'.len_utf8(), 2);
1343
1344 _ = editor.update(cx, |editor, window, cx| {
1345 editor.fold_creases(
1346 vec![
1347 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1348 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1349 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1350 ],
1351 true,
1352 window,
1353 cx,
1354 );
1355 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1356
1357 editor.move_right(&MoveRight, window, cx);
1358 assert_eq!(
1359 editor.selections.display_ranges(cx),
1360 &[empty_range(0, "🟥".len())]
1361 );
1362 editor.move_right(&MoveRight, window, cx);
1363 assert_eq!(
1364 editor.selections.display_ranges(cx),
1365 &[empty_range(0, "🟥🟧".len())]
1366 );
1367 editor.move_right(&MoveRight, window, cx);
1368 assert_eq!(
1369 editor.selections.display_ranges(cx),
1370 &[empty_range(0, "🟥🟧⋯".len())]
1371 );
1372
1373 editor.move_down(&MoveDown, window, cx);
1374 assert_eq!(
1375 editor.selections.display_ranges(cx),
1376 &[empty_range(1, "ab⋯e".len())]
1377 );
1378 editor.move_left(&MoveLeft, window, cx);
1379 assert_eq!(
1380 editor.selections.display_ranges(cx),
1381 &[empty_range(1, "ab⋯".len())]
1382 );
1383 editor.move_left(&MoveLeft, window, cx);
1384 assert_eq!(
1385 editor.selections.display_ranges(cx),
1386 &[empty_range(1, "ab".len())]
1387 );
1388 editor.move_left(&MoveLeft, window, cx);
1389 assert_eq!(
1390 editor.selections.display_ranges(cx),
1391 &[empty_range(1, "a".len())]
1392 );
1393
1394 editor.move_down(&MoveDown, window, cx);
1395 assert_eq!(
1396 editor.selections.display_ranges(cx),
1397 &[empty_range(2, "α".len())]
1398 );
1399 editor.move_right(&MoveRight, window, cx);
1400 assert_eq!(
1401 editor.selections.display_ranges(cx),
1402 &[empty_range(2, "αβ".len())]
1403 );
1404 editor.move_right(&MoveRight, window, cx);
1405 assert_eq!(
1406 editor.selections.display_ranges(cx),
1407 &[empty_range(2, "αβ⋯".len())]
1408 );
1409 editor.move_right(&MoveRight, window, cx);
1410 assert_eq!(
1411 editor.selections.display_ranges(cx),
1412 &[empty_range(2, "αβ⋯ε".len())]
1413 );
1414
1415 editor.move_up(&MoveUp, window, cx);
1416 assert_eq!(
1417 editor.selections.display_ranges(cx),
1418 &[empty_range(1, "ab⋯e".len())]
1419 );
1420 editor.move_down(&MoveDown, window, cx);
1421 assert_eq!(
1422 editor.selections.display_ranges(cx),
1423 &[empty_range(2, "αβ⋯ε".len())]
1424 );
1425 editor.move_up(&MoveUp, window, cx);
1426 assert_eq!(
1427 editor.selections.display_ranges(cx),
1428 &[empty_range(1, "ab⋯e".len())]
1429 );
1430
1431 editor.move_up(&MoveUp, window, cx);
1432 assert_eq!(
1433 editor.selections.display_ranges(cx),
1434 &[empty_range(0, "🟥🟧".len())]
1435 );
1436 editor.move_left(&MoveLeft, window, cx);
1437 assert_eq!(
1438 editor.selections.display_ranges(cx),
1439 &[empty_range(0, "🟥".len())]
1440 );
1441 editor.move_left(&MoveLeft, window, cx);
1442 assert_eq!(
1443 editor.selections.display_ranges(cx),
1444 &[empty_range(0, "".len())]
1445 );
1446 });
1447}
1448
1449#[gpui::test]
1450fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1451 init_test(cx, |_| {});
1452
1453 let editor = cx.add_window(|window, cx| {
1454 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1455 build_editor(buffer.clone(), window, cx)
1456 });
1457 _ = editor.update(cx, |editor, window, cx| {
1458 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1459 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1460 });
1461
1462 // moving above start of document should move selection to start of document,
1463 // but the next move down should still be at the original goal_x
1464 editor.move_up(&MoveUp, window, cx);
1465 assert_eq!(
1466 editor.selections.display_ranges(cx),
1467 &[empty_range(0, "".len())]
1468 );
1469
1470 editor.move_down(&MoveDown, window, cx);
1471 assert_eq!(
1472 editor.selections.display_ranges(cx),
1473 &[empty_range(1, "abcd".len())]
1474 );
1475
1476 editor.move_down(&MoveDown, window, cx);
1477 assert_eq!(
1478 editor.selections.display_ranges(cx),
1479 &[empty_range(2, "αβγ".len())]
1480 );
1481
1482 editor.move_down(&MoveDown, window, cx);
1483 assert_eq!(
1484 editor.selections.display_ranges(cx),
1485 &[empty_range(3, "abcd".len())]
1486 );
1487
1488 editor.move_down(&MoveDown, window, cx);
1489 assert_eq!(
1490 editor.selections.display_ranges(cx),
1491 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1492 );
1493
1494 // moving past end of document should not change goal_x
1495 editor.move_down(&MoveDown, window, cx);
1496 assert_eq!(
1497 editor.selections.display_ranges(cx),
1498 &[empty_range(5, "".len())]
1499 );
1500
1501 editor.move_down(&MoveDown, window, cx);
1502 assert_eq!(
1503 editor.selections.display_ranges(cx),
1504 &[empty_range(5, "".len())]
1505 );
1506
1507 editor.move_up(&MoveUp, window, cx);
1508 assert_eq!(
1509 editor.selections.display_ranges(cx),
1510 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1511 );
1512
1513 editor.move_up(&MoveUp, window, cx);
1514 assert_eq!(
1515 editor.selections.display_ranges(cx),
1516 &[empty_range(3, "abcd".len())]
1517 );
1518
1519 editor.move_up(&MoveUp, window, cx);
1520 assert_eq!(
1521 editor.selections.display_ranges(cx),
1522 &[empty_range(2, "αβγ".len())]
1523 );
1524 });
1525}
1526
1527#[gpui::test]
1528fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1529 init_test(cx, |_| {});
1530 let move_to_beg = MoveToBeginningOfLine {
1531 stop_at_soft_wraps: true,
1532 stop_at_indent: true,
1533 };
1534
1535 let delete_to_beg = DeleteToBeginningOfLine {
1536 stop_at_indent: false,
1537 };
1538
1539 let move_to_end = MoveToEndOfLine {
1540 stop_at_soft_wraps: true,
1541 };
1542
1543 let editor = cx.add_window(|window, cx| {
1544 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1545 build_editor(buffer, window, cx)
1546 });
1547 _ = editor.update(cx, |editor, window, cx| {
1548 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1549 s.select_display_ranges([
1550 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1551 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1552 ]);
1553 });
1554 });
1555
1556 _ = editor.update(cx, |editor, window, cx| {
1557 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1558 assert_eq!(
1559 editor.selections.display_ranges(cx),
1560 &[
1561 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1562 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1563 ]
1564 );
1565 });
1566
1567 _ = editor.update(cx, |editor, window, cx| {
1568 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1569 assert_eq!(
1570 editor.selections.display_ranges(cx),
1571 &[
1572 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1573 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1574 ]
1575 );
1576 });
1577
1578 _ = editor.update(cx, |editor, window, cx| {
1579 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1580 assert_eq!(
1581 editor.selections.display_ranges(cx),
1582 &[
1583 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1584 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1585 ]
1586 );
1587 });
1588
1589 _ = editor.update(cx, |editor, window, cx| {
1590 editor.move_to_end_of_line(&move_to_end, window, cx);
1591 assert_eq!(
1592 editor.selections.display_ranges(cx),
1593 &[
1594 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1595 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1596 ]
1597 );
1598 });
1599
1600 // Moving to the end of line again is a no-op.
1601 _ = editor.update(cx, |editor, window, cx| {
1602 editor.move_to_end_of_line(&move_to_end, window, cx);
1603 assert_eq!(
1604 editor.selections.display_ranges(cx),
1605 &[
1606 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1607 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1608 ]
1609 );
1610 });
1611
1612 _ = editor.update(cx, |editor, window, cx| {
1613 editor.move_left(&MoveLeft, window, cx);
1614 editor.select_to_beginning_of_line(
1615 &SelectToBeginningOfLine {
1616 stop_at_soft_wraps: true,
1617 stop_at_indent: true,
1618 },
1619 window,
1620 cx,
1621 );
1622 assert_eq!(
1623 editor.selections.display_ranges(cx),
1624 &[
1625 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1626 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1627 ]
1628 );
1629 });
1630
1631 _ = editor.update(cx, |editor, window, cx| {
1632 editor.select_to_beginning_of_line(
1633 &SelectToBeginningOfLine {
1634 stop_at_soft_wraps: true,
1635 stop_at_indent: true,
1636 },
1637 window,
1638 cx,
1639 );
1640 assert_eq!(
1641 editor.selections.display_ranges(cx),
1642 &[
1643 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1644 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1645 ]
1646 );
1647 });
1648
1649 _ = editor.update(cx, |editor, window, cx| {
1650 editor.select_to_beginning_of_line(
1651 &SelectToBeginningOfLine {
1652 stop_at_soft_wraps: true,
1653 stop_at_indent: true,
1654 },
1655 window,
1656 cx,
1657 );
1658 assert_eq!(
1659 editor.selections.display_ranges(cx),
1660 &[
1661 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1662 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1663 ]
1664 );
1665 });
1666
1667 _ = editor.update(cx, |editor, window, cx| {
1668 editor.select_to_end_of_line(
1669 &SelectToEndOfLine {
1670 stop_at_soft_wraps: true,
1671 },
1672 window,
1673 cx,
1674 );
1675 assert_eq!(
1676 editor.selections.display_ranges(cx),
1677 &[
1678 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1679 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1680 ]
1681 );
1682 });
1683
1684 _ = editor.update(cx, |editor, window, cx| {
1685 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1686 assert_eq!(editor.display_text(cx), "ab\n de");
1687 assert_eq!(
1688 editor.selections.display_ranges(cx),
1689 &[
1690 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1691 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1692 ]
1693 );
1694 });
1695
1696 _ = editor.update(cx, |editor, window, cx| {
1697 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1698 assert_eq!(editor.display_text(cx), "\n");
1699 assert_eq!(
1700 editor.selections.display_ranges(cx),
1701 &[
1702 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1703 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1704 ]
1705 );
1706 });
1707}
1708
1709#[gpui::test]
1710fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1711 init_test(cx, |_| {});
1712 let move_to_beg = MoveToBeginningOfLine {
1713 stop_at_soft_wraps: false,
1714 stop_at_indent: false,
1715 };
1716
1717 let move_to_end = MoveToEndOfLine {
1718 stop_at_soft_wraps: false,
1719 };
1720
1721 let editor = cx.add_window(|window, cx| {
1722 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1723 build_editor(buffer, window, cx)
1724 });
1725
1726 _ = editor.update(cx, |editor, window, cx| {
1727 editor.set_wrap_width(Some(140.0.into()), cx);
1728
1729 // We expect the following lines after wrapping
1730 // ```
1731 // thequickbrownfox
1732 // jumpedoverthelazydo
1733 // gs
1734 // ```
1735 // The final `gs` was soft-wrapped onto a new line.
1736 assert_eq!(
1737 "thequickbrownfox\njumpedoverthelaz\nydogs",
1738 editor.display_text(cx),
1739 );
1740
1741 // First, let's assert behavior on the first line, that was not soft-wrapped.
1742 // Start the cursor at the `k` on the first line
1743 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1744 s.select_display_ranges([
1745 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1746 ]);
1747 });
1748
1749 // Moving to the beginning of the line should put us at the beginning of the line.
1750 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1751 assert_eq!(
1752 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1753 editor.selections.display_ranges(cx)
1754 );
1755
1756 // Moving to the end of the line should put us at the end of the line.
1757 editor.move_to_end_of_line(&move_to_end, window, cx);
1758 assert_eq!(
1759 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1760 editor.selections.display_ranges(cx)
1761 );
1762
1763 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1764 // Start the cursor at the last line (`y` that was wrapped to a new line)
1765 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1766 s.select_display_ranges([
1767 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1768 ]);
1769 });
1770
1771 // Moving to the beginning of the line should put us at the start of the second line of
1772 // display text, i.e., the `j`.
1773 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1774 assert_eq!(
1775 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1776 editor.selections.display_ranges(cx)
1777 );
1778
1779 // Moving to the beginning of the line again should be a no-op.
1780 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1781 assert_eq!(
1782 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1783 editor.selections.display_ranges(cx)
1784 );
1785
1786 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1787 // next display line.
1788 editor.move_to_end_of_line(&move_to_end, window, cx);
1789 assert_eq!(
1790 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1791 editor.selections.display_ranges(cx)
1792 );
1793
1794 // Moving to the end of the line again should be a no-op.
1795 editor.move_to_end_of_line(&move_to_end, window, cx);
1796 assert_eq!(
1797 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1798 editor.selections.display_ranges(cx)
1799 );
1800 });
1801}
1802
1803#[gpui::test]
1804fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1805 init_test(cx, |_| {});
1806
1807 let move_to_beg = MoveToBeginningOfLine {
1808 stop_at_soft_wraps: true,
1809 stop_at_indent: true,
1810 };
1811
1812 let select_to_beg = SelectToBeginningOfLine {
1813 stop_at_soft_wraps: true,
1814 stop_at_indent: true,
1815 };
1816
1817 let delete_to_beg = DeleteToBeginningOfLine {
1818 stop_at_indent: true,
1819 };
1820
1821 let move_to_end = MoveToEndOfLine {
1822 stop_at_soft_wraps: false,
1823 };
1824
1825 let editor = cx.add_window(|window, cx| {
1826 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1827 build_editor(buffer, window, cx)
1828 });
1829
1830 _ = editor.update(cx, |editor, window, cx| {
1831 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1832 s.select_display_ranges([
1833 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1834 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1835 ]);
1836 });
1837
1838 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1839 // and the second cursor at the first non-whitespace character in the line.
1840 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1841 assert_eq!(
1842 editor.selections.display_ranges(cx),
1843 &[
1844 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1845 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1846 ]
1847 );
1848
1849 // Moving to the beginning of the line again should be a no-op for the first cursor,
1850 // and should move the second cursor to the beginning of the line.
1851 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1852 assert_eq!(
1853 editor.selections.display_ranges(cx),
1854 &[
1855 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1856 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1857 ]
1858 );
1859
1860 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1861 // and should move the second cursor back to the first non-whitespace character in the line.
1862 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1863 assert_eq!(
1864 editor.selections.display_ranges(cx),
1865 &[
1866 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1867 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1868 ]
1869 );
1870
1871 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1872 // and to the first non-whitespace character in the line for the second cursor.
1873 editor.move_to_end_of_line(&move_to_end, window, cx);
1874 editor.move_left(&MoveLeft, window, cx);
1875 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1876 assert_eq!(
1877 editor.selections.display_ranges(cx),
1878 &[
1879 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1880 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1881 ]
1882 );
1883
1884 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1885 // and should select to the beginning of the line for the second cursor.
1886 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1887 assert_eq!(
1888 editor.selections.display_ranges(cx),
1889 &[
1890 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1891 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1892 ]
1893 );
1894
1895 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1896 // and should delete to the first non-whitespace character in the line for the second cursor.
1897 editor.move_to_end_of_line(&move_to_end, window, cx);
1898 editor.move_left(&MoveLeft, window, cx);
1899 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1900 assert_eq!(editor.text(cx), "c\n f");
1901 });
1902}
1903
1904#[gpui::test]
1905fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1906 init_test(cx, |_| {});
1907
1908 let editor = cx.add_window(|window, cx| {
1909 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1910 build_editor(buffer, window, cx)
1911 });
1912 _ = editor.update(cx, |editor, window, cx| {
1913 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1914 s.select_display_ranges([
1915 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1916 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1917 ])
1918 });
1919 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1920 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1921
1922 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1923 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1924
1925 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1926 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1927
1928 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1929 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1930
1931 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1932 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
1933
1934 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1935 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1936
1937 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1938 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1939
1940 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1941 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1942
1943 editor.move_right(&MoveRight, window, cx);
1944 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1945 assert_selection_ranges(
1946 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
1947 editor,
1948 cx,
1949 );
1950
1951 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1952 assert_selection_ranges(
1953 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
1954 editor,
1955 cx,
1956 );
1957
1958 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1959 assert_selection_ranges(
1960 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
1961 editor,
1962 cx,
1963 );
1964 });
1965}
1966
1967#[gpui::test]
1968fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1969 init_test(cx, |_| {});
1970
1971 let editor = cx.add_window(|window, cx| {
1972 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1973 build_editor(buffer, window, cx)
1974 });
1975
1976 _ = editor.update(cx, |editor, window, cx| {
1977 editor.set_wrap_width(Some(140.0.into()), cx);
1978 assert_eq!(
1979 editor.display_text(cx),
1980 "use one::{\n two::three::\n four::five\n};"
1981 );
1982
1983 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1984 s.select_display_ranges([
1985 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1986 ]);
1987 });
1988
1989 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1990 assert_eq!(
1991 editor.selections.display_ranges(cx),
1992 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1993 );
1994
1995 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1996 assert_eq!(
1997 editor.selections.display_ranges(cx),
1998 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1999 );
2000
2001 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2002 assert_eq!(
2003 editor.selections.display_ranges(cx),
2004 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2005 );
2006
2007 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2008 assert_eq!(
2009 editor.selections.display_ranges(cx),
2010 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2011 );
2012
2013 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2014 assert_eq!(
2015 editor.selections.display_ranges(cx),
2016 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2017 );
2018
2019 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2020 assert_eq!(
2021 editor.selections.display_ranges(cx),
2022 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2023 );
2024 });
2025}
2026
2027#[gpui::test]
2028async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2029 init_test(cx, |_| {});
2030 let mut cx = EditorTestContext::new(cx).await;
2031
2032 let line_height = cx.editor(|editor, window, _| {
2033 editor
2034 .style()
2035 .unwrap()
2036 .text
2037 .line_height_in_pixels(window.rem_size())
2038 });
2039 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2040
2041 cx.set_state(
2042 &r#"ˇone
2043 two
2044
2045 three
2046 fourˇ
2047 five
2048
2049 six"#
2050 .unindent(),
2051 );
2052
2053 cx.update_editor(|editor, window, cx| {
2054 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2055 });
2056 cx.assert_editor_state(
2057 &r#"one
2058 two
2059 ˇ
2060 three
2061 four
2062 five
2063 ˇ
2064 six"#
2065 .unindent(),
2066 );
2067
2068 cx.update_editor(|editor, window, cx| {
2069 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2070 });
2071 cx.assert_editor_state(
2072 &r#"one
2073 two
2074
2075 three
2076 four
2077 five
2078 ˇ
2079 sixˇ"#
2080 .unindent(),
2081 );
2082
2083 cx.update_editor(|editor, window, cx| {
2084 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2085 });
2086 cx.assert_editor_state(
2087 &r#"one
2088 two
2089
2090 three
2091 four
2092 five
2093
2094 sixˇ"#
2095 .unindent(),
2096 );
2097
2098 cx.update_editor(|editor, window, cx| {
2099 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2100 });
2101 cx.assert_editor_state(
2102 &r#"one
2103 two
2104
2105 three
2106 four
2107 five
2108 ˇ
2109 six"#
2110 .unindent(),
2111 );
2112
2113 cx.update_editor(|editor, window, cx| {
2114 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2115 });
2116 cx.assert_editor_state(
2117 &r#"one
2118 two
2119 ˇ
2120 three
2121 four
2122 five
2123
2124 six"#
2125 .unindent(),
2126 );
2127
2128 cx.update_editor(|editor, window, cx| {
2129 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2130 });
2131 cx.assert_editor_state(
2132 &r#"ˇone
2133 two
2134
2135 three
2136 four
2137 five
2138
2139 six"#
2140 .unindent(),
2141 );
2142}
2143
2144#[gpui::test]
2145async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2146 init_test(cx, |_| {});
2147 let mut cx = EditorTestContext::new(cx).await;
2148 let line_height = cx.editor(|editor, window, _| {
2149 editor
2150 .style()
2151 .unwrap()
2152 .text
2153 .line_height_in_pixels(window.rem_size())
2154 });
2155 let window = cx.window;
2156 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2157
2158 cx.set_state(
2159 r#"ˇone
2160 two
2161 three
2162 four
2163 five
2164 six
2165 seven
2166 eight
2167 nine
2168 ten
2169 "#,
2170 );
2171
2172 cx.update_editor(|editor, window, cx| {
2173 assert_eq!(
2174 editor.snapshot(window, cx).scroll_position(),
2175 gpui::Point::new(0., 0.)
2176 );
2177 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2178 assert_eq!(
2179 editor.snapshot(window, cx).scroll_position(),
2180 gpui::Point::new(0., 3.)
2181 );
2182 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2183 assert_eq!(
2184 editor.snapshot(window, cx).scroll_position(),
2185 gpui::Point::new(0., 6.)
2186 );
2187 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2188 assert_eq!(
2189 editor.snapshot(window, cx).scroll_position(),
2190 gpui::Point::new(0., 3.)
2191 );
2192
2193 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2194 assert_eq!(
2195 editor.snapshot(window, cx).scroll_position(),
2196 gpui::Point::new(0., 1.)
2197 );
2198 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2199 assert_eq!(
2200 editor.snapshot(window, cx).scroll_position(),
2201 gpui::Point::new(0., 3.)
2202 );
2203 });
2204}
2205
2206#[gpui::test]
2207async fn test_autoscroll(cx: &mut TestAppContext) {
2208 init_test(cx, |_| {});
2209 let mut cx = EditorTestContext::new(cx).await;
2210
2211 let line_height = cx.update_editor(|editor, window, cx| {
2212 editor.set_vertical_scroll_margin(2, cx);
2213 editor
2214 .style()
2215 .unwrap()
2216 .text
2217 .line_height_in_pixels(window.rem_size())
2218 });
2219 let window = cx.window;
2220 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2221
2222 cx.set_state(
2223 r#"ˇone
2224 two
2225 three
2226 four
2227 five
2228 six
2229 seven
2230 eight
2231 nine
2232 ten
2233 "#,
2234 );
2235 cx.update_editor(|editor, window, cx| {
2236 assert_eq!(
2237 editor.snapshot(window, cx).scroll_position(),
2238 gpui::Point::new(0., 0.0)
2239 );
2240 });
2241
2242 // Add a cursor below the visible area. Since both cursors cannot fit
2243 // on screen, the editor autoscrolls to reveal the newest cursor, and
2244 // allows the vertical scroll margin below that cursor.
2245 cx.update_editor(|editor, window, cx| {
2246 editor.change_selections(Default::default(), window, cx, |selections| {
2247 selections.select_ranges([
2248 Point::new(0, 0)..Point::new(0, 0),
2249 Point::new(6, 0)..Point::new(6, 0),
2250 ]);
2251 })
2252 });
2253 cx.update_editor(|editor, window, cx| {
2254 assert_eq!(
2255 editor.snapshot(window, cx).scroll_position(),
2256 gpui::Point::new(0., 3.0)
2257 );
2258 });
2259
2260 // Move down. The editor cursor scrolls down to track the newest cursor.
2261 cx.update_editor(|editor, window, cx| {
2262 editor.move_down(&Default::default(), window, cx);
2263 });
2264 cx.update_editor(|editor, window, cx| {
2265 assert_eq!(
2266 editor.snapshot(window, cx).scroll_position(),
2267 gpui::Point::new(0., 4.0)
2268 );
2269 });
2270
2271 // Add a cursor above the visible area. Since both cursors fit on screen,
2272 // the editor scrolls to show both.
2273 cx.update_editor(|editor, window, cx| {
2274 editor.change_selections(Default::default(), window, cx, |selections| {
2275 selections.select_ranges([
2276 Point::new(1, 0)..Point::new(1, 0),
2277 Point::new(6, 0)..Point::new(6, 0),
2278 ]);
2279 })
2280 });
2281 cx.update_editor(|editor, window, cx| {
2282 assert_eq!(
2283 editor.snapshot(window, cx).scroll_position(),
2284 gpui::Point::new(0., 1.0)
2285 );
2286 });
2287}
2288
2289#[gpui::test]
2290async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2291 init_test(cx, |_| {});
2292 let mut cx = EditorTestContext::new(cx).await;
2293
2294 let line_height = cx.editor(|editor, window, _cx| {
2295 editor
2296 .style()
2297 .unwrap()
2298 .text
2299 .line_height_in_pixels(window.rem_size())
2300 });
2301 let window = cx.window;
2302 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2303 cx.set_state(
2304 &r#"
2305 ˇone
2306 two
2307 threeˇ
2308 four
2309 five
2310 six
2311 seven
2312 eight
2313 nine
2314 ten
2315 "#
2316 .unindent(),
2317 );
2318
2319 cx.update_editor(|editor, window, cx| {
2320 editor.move_page_down(&MovePageDown::default(), window, cx)
2321 });
2322 cx.assert_editor_state(
2323 &r#"
2324 one
2325 two
2326 three
2327 ˇfour
2328 five
2329 sixˇ
2330 seven
2331 eight
2332 nine
2333 ten
2334 "#
2335 .unindent(),
2336 );
2337
2338 cx.update_editor(|editor, window, cx| {
2339 editor.move_page_down(&MovePageDown::default(), window, cx)
2340 });
2341 cx.assert_editor_state(
2342 &r#"
2343 one
2344 two
2345 three
2346 four
2347 five
2348 six
2349 ˇseven
2350 eight
2351 nineˇ
2352 ten
2353 "#
2354 .unindent(),
2355 );
2356
2357 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2358 cx.assert_editor_state(
2359 &r#"
2360 one
2361 two
2362 three
2363 ˇfour
2364 five
2365 sixˇ
2366 seven
2367 eight
2368 nine
2369 ten
2370 "#
2371 .unindent(),
2372 );
2373
2374 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2375 cx.assert_editor_state(
2376 &r#"
2377 ˇone
2378 two
2379 threeˇ
2380 four
2381 five
2382 six
2383 seven
2384 eight
2385 nine
2386 ten
2387 "#
2388 .unindent(),
2389 );
2390
2391 // Test select collapsing
2392 cx.update_editor(|editor, window, cx| {
2393 editor.move_page_down(&MovePageDown::default(), window, cx);
2394 editor.move_page_down(&MovePageDown::default(), window, cx);
2395 editor.move_page_down(&MovePageDown::default(), window, cx);
2396 });
2397 cx.assert_editor_state(
2398 &r#"
2399 one
2400 two
2401 three
2402 four
2403 five
2404 six
2405 seven
2406 eight
2407 nine
2408 ˇten
2409 ˇ"#
2410 .unindent(),
2411 );
2412}
2413
2414#[gpui::test]
2415async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2416 init_test(cx, |_| {});
2417 let mut cx = EditorTestContext::new(cx).await;
2418 cx.set_state("one «two threeˇ» four");
2419 cx.update_editor(|editor, window, cx| {
2420 editor.delete_to_beginning_of_line(
2421 &DeleteToBeginningOfLine {
2422 stop_at_indent: false,
2423 },
2424 window,
2425 cx,
2426 );
2427 assert_eq!(editor.text(cx), " four");
2428 });
2429}
2430
2431#[gpui::test]
2432fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2433 init_test(cx, |_| {});
2434
2435 let editor = cx.add_window(|window, cx| {
2436 let buffer = MultiBuffer::build_simple("one two three four", cx);
2437 build_editor(buffer.clone(), window, cx)
2438 });
2439
2440 _ = editor.update(cx, |editor, window, cx| {
2441 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2442 s.select_display_ranges([
2443 // an empty selection - the preceding word fragment is deleted
2444 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2445 // characters selected - they are deleted
2446 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2447 ])
2448 });
2449 editor.delete_to_previous_word_start(
2450 &DeleteToPreviousWordStart {
2451 ignore_newlines: false,
2452 },
2453 window,
2454 cx,
2455 );
2456 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2457 });
2458
2459 _ = editor.update(cx, |editor, window, cx| {
2460 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2461 s.select_display_ranges([
2462 // an empty selection - the following word fragment is deleted
2463 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2464 // characters selected - they are deleted
2465 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2466 ])
2467 });
2468 editor.delete_to_next_word_end(
2469 &DeleteToNextWordEnd {
2470 ignore_newlines: false,
2471 },
2472 window,
2473 cx,
2474 );
2475 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2476 });
2477}
2478
2479#[gpui::test]
2480fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2481 init_test(cx, |_| {});
2482
2483 let editor = cx.add_window(|window, cx| {
2484 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2485 build_editor(buffer.clone(), window, cx)
2486 });
2487 let del_to_prev_word_start = DeleteToPreviousWordStart {
2488 ignore_newlines: false,
2489 };
2490 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2491 ignore_newlines: true,
2492 };
2493
2494 _ = editor.update(cx, |editor, window, cx| {
2495 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2496 s.select_display_ranges([
2497 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2498 ])
2499 });
2500 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2501 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2502 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2503 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2504 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2505 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2506 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2507 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2508 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2509 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2510 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2511 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2512 });
2513}
2514
2515#[gpui::test]
2516fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2517 init_test(cx, |_| {});
2518
2519 let editor = cx.add_window(|window, cx| {
2520 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2521 build_editor(buffer.clone(), window, cx)
2522 });
2523 let del_to_next_word_end = DeleteToNextWordEnd {
2524 ignore_newlines: false,
2525 };
2526 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2527 ignore_newlines: true,
2528 };
2529
2530 _ = editor.update(cx, |editor, window, cx| {
2531 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2532 s.select_display_ranges([
2533 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2534 ])
2535 });
2536 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2537 assert_eq!(
2538 editor.buffer.read(cx).read(cx).text(),
2539 "one\n two\nthree\n four"
2540 );
2541 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2542 assert_eq!(
2543 editor.buffer.read(cx).read(cx).text(),
2544 "\n two\nthree\n four"
2545 );
2546 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2547 assert_eq!(
2548 editor.buffer.read(cx).read(cx).text(),
2549 "two\nthree\n four"
2550 );
2551 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2552 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2553 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2554 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2555 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2556 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2557 });
2558}
2559
2560#[gpui::test]
2561fn test_newline(cx: &mut TestAppContext) {
2562 init_test(cx, |_| {});
2563
2564 let editor = cx.add_window(|window, cx| {
2565 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2566 build_editor(buffer.clone(), window, cx)
2567 });
2568
2569 _ = editor.update(cx, |editor, window, cx| {
2570 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2571 s.select_display_ranges([
2572 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2573 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2574 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2575 ])
2576 });
2577
2578 editor.newline(&Newline, window, cx);
2579 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2580 });
2581}
2582
2583#[gpui::test]
2584fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2585 init_test(cx, |_| {});
2586
2587 let editor = cx.add_window(|window, cx| {
2588 let buffer = MultiBuffer::build_simple(
2589 "
2590 a
2591 b(
2592 X
2593 )
2594 c(
2595 X
2596 )
2597 "
2598 .unindent()
2599 .as_str(),
2600 cx,
2601 );
2602 let mut editor = build_editor(buffer.clone(), window, cx);
2603 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2604 s.select_ranges([
2605 Point::new(2, 4)..Point::new(2, 5),
2606 Point::new(5, 4)..Point::new(5, 5),
2607 ])
2608 });
2609 editor
2610 });
2611
2612 _ = editor.update(cx, |editor, window, cx| {
2613 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2614 editor.buffer.update(cx, |buffer, cx| {
2615 buffer.edit(
2616 [
2617 (Point::new(1, 2)..Point::new(3, 0), ""),
2618 (Point::new(4, 2)..Point::new(6, 0), ""),
2619 ],
2620 None,
2621 cx,
2622 );
2623 assert_eq!(
2624 buffer.read(cx).text(),
2625 "
2626 a
2627 b()
2628 c()
2629 "
2630 .unindent()
2631 );
2632 });
2633 assert_eq!(
2634 editor.selections.ranges(cx),
2635 &[
2636 Point::new(1, 2)..Point::new(1, 2),
2637 Point::new(2, 2)..Point::new(2, 2),
2638 ],
2639 );
2640
2641 editor.newline(&Newline, window, cx);
2642 assert_eq!(
2643 editor.text(cx),
2644 "
2645 a
2646 b(
2647 )
2648 c(
2649 )
2650 "
2651 .unindent()
2652 );
2653
2654 // The selections are moved after the inserted newlines
2655 assert_eq!(
2656 editor.selections.ranges(cx),
2657 &[
2658 Point::new(2, 0)..Point::new(2, 0),
2659 Point::new(4, 0)..Point::new(4, 0),
2660 ],
2661 );
2662 });
2663}
2664
2665#[gpui::test]
2666async fn test_newline_above(cx: &mut TestAppContext) {
2667 init_test(cx, |settings| {
2668 settings.defaults.tab_size = NonZeroU32::new(4)
2669 });
2670
2671 let language = Arc::new(
2672 Language::new(
2673 LanguageConfig::default(),
2674 Some(tree_sitter_rust::LANGUAGE.into()),
2675 )
2676 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2677 .unwrap(),
2678 );
2679
2680 let mut cx = EditorTestContext::new(cx).await;
2681 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2682 cx.set_state(indoc! {"
2683 const a: ˇA = (
2684 (ˇ
2685 «const_functionˇ»(ˇ),
2686 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2687 )ˇ
2688 ˇ);ˇ
2689 "});
2690
2691 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2692 cx.assert_editor_state(indoc! {"
2693 ˇ
2694 const a: A = (
2695 ˇ
2696 (
2697 ˇ
2698 ˇ
2699 const_function(),
2700 ˇ
2701 ˇ
2702 ˇ
2703 ˇ
2704 something_else,
2705 ˇ
2706 )
2707 ˇ
2708 ˇ
2709 );
2710 "});
2711}
2712
2713#[gpui::test]
2714async fn test_newline_below(cx: &mut TestAppContext) {
2715 init_test(cx, |settings| {
2716 settings.defaults.tab_size = NonZeroU32::new(4)
2717 });
2718
2719 let language = Arc::new(
2720 Language::new(
2721 LanguageConfig::default(),
2722 Some(tree_sitter_rust::LANGUAGE.into()),
2723 )
2724 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2725 .unwrap(),
2726 );
2727
2728 let mut cx = EditorTestContext::new(cx).await;
2729 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2730 cx.set_state(indoc! {"
2731 const a: ˇA = (
2732 (ˇ
2733 «const_functionˇ»(ˇ),
2734 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2735 )ˇ
2736 ˇ);ˇ
2737 "});
2738
2739 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2740 cx.assert_editor_state(indoc! {"
2741 const a: A = (
2742 ˇ
2743 (
2744 ˇ
2745 const_function(),
2746 ˇ
2747 ˇ
2748 something_else,
2749 ˇ
2750 ˇ
2751 ˇ
2752 ˇ
2753 )
2754 ˇ
2755 );
2756 ˇ
2757 ˇ
2758 "});
2759}
2760
2761#[gpui::test]
2762async fn test_newline_comments(cx: &mut TestAppContext) {
2763 init_test(cx, |settings| {
2764 settings.defaults.tab_size = NonZeroU32::new(4)
2765 });
2766
2767 let language = Arc::new(Language::new(
2768 LanguageConfig {
2769 line_comments: vec!["// ".into()],
2770 ..LanguageConfig::default()
2771 },
2772 None,
2773 ));
2774 {
2775 let mut cx = EditorTestContext::new(cx).await;
2776 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2777 cx.set_state(indoc! {"
2778 // Fooˇ
2779 "});
2780
2781 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2782 cx.assert_editor_state(indoc! {"
2783 // Foo
2784 // ˇ
2785 "});
2786 // Ensure that we add comment prefix when existing line contains space
2787 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2788 cx.assert_editor_state(
2789 indoc! {"
2790 // Foo
2791 //s
2792 // ˇ
2793 "}
2794 .replace("s", " ") // s is used as space placeholder to prevent format on save
2795 .as_str(),
2796 );
2797 // Ensure that we add comment prefix when existing line does not contain space
2798 cx.set_state(indoc! {"
2799 // Foo
2800 //ˇ
2801 "});
2802 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2803 cx.assert_editor_state(indoc! {"
2804 // Foo
2805 //
2806 // ˇ
2807 "});
2808 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2809 cx.set_state(indoc! {"
2810 ˇ// Foo
2811 "});
2812 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2813 cx.assert_editor_state(indoc! {"
2814
2815 ˇ// Foo
2816 "});
2817 }
2818 // Ensure that comment continuations can be disabled.
2819 update_test_language_settings(cx, |settings| {
2820 settings.defaults.extend_comment_on_newline = Some(false);
2821 });
2822 let mut cx = EditorTestContext::new(cx).await;
2823 cx.set_state(indoc! {"
2824 // Fooˇ
2825 "});
2826 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2827 cx.assert_editor_state(indoc! {"
2828 // Foo
2829 ˇ
2830 "});
2831}
2832
2833#[gpui::test]
2834async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
2835 init_test(cx, |settings| {
2836 settings.defaults.tab_size = NonZeroU32::new(4)
2837 });
2838
2839 let language = Arc::new(Language::new(
2840 LanguageConfig {
2841 line_comments: vec!["// ".into(), "/// ".into()],
2842 ..LanguageConfig::default()
2843 },
2844 None,
2845 ));
2846 {
2847 let mut cx = EditorTestContext::new(cx).await;
2848 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2849 cx.set_state(indoc! {"
2850 //ˇ
2851 "});
2852 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2853 cx.assert_editor_state(indoc! {"
2854 //
2855 // ˇ
2856 "});
2857
2858 cx.set_state(indoc! {"
2859 ///ˇ
2860 "});
2861 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2862 cx.assert_editor_state(indoc! {"
2863 ///
2864 /// ˇ
2865 "});
2866 }
2867}
2868
2869#[gpui::test]
2870async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
2871 init_test(cx, |settings| {
2872 settings.defaults.tab_size = NonZeroU32::new(4)
2873 });
2874
2875 let language = Arc::new(
2876 Language::new(
2877 LanguageConfig {
2878 documentation_comment: Some(language::BlockCommentConfig {
2879 start: "/**".into(),
2880 end: "*/".into(),
2881 prefix: "* ".into(),
2882 tab_size: 1,
2883 }),
2884
2885 ..LanguageConfig::default()
2886 },
2887 Some(tree_sitter_rust::LANGUAGE.into()),
2888 )
2889 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
2890 .unwrap(),
2891 );
2892
2893 {
2894 let mut cx = EditorTestContext::new(cx).await;
2895 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2896 cx.set_state(indoc! {"
2897 /**ˇ
2898 "});
2899
2900 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2901 cx.assert_editor_state(indoc! {"
2902 /**
2903 * ˇ
2904 "});
2905 // Ensure that if cursor is before the comment start,
2906 // we do not actually insert a comment prefix.
2907 cx.set_state(indoc! {"
2908 ˇ/**
2909 "});
2910 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2911 cx.assert_editor_state(indoc! {"
2912
2913 ˇ/**
2914 "});
2915 // Ensure that if cursor is between it doesn't add comment prefix.
2916 cx.set_state(indoc! {"
2917 /*ˇ*
2918 "});
2919 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2920 cx.assert_editor_state(indoc! {"
2921 /*
2922 ˇ*
2923 "});
2924 // Ensure that if suffix exists on same line after cursor it adds new line.
2925 cx.set_state(indoc! {"
2926 /**ˇ*/
2927 "});
2928 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2929 cx.assert_editor_state(indoc! {"
2930 /**
2931 * ˇ
2932 */
2933 "});
2934 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2935 cx.set_state(indoc! {"
2936 /**ˇ */
2937 "});
2938 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2939 cx.assert_editor_state(indoc! {"
2940 /**
2941 * ˇ
2942 */
2943 "});
2944 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2945 cx.set_state(indoc! {"
2946 /** ˇ*/
2947 "});
2948 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2949 cx.assert_editor_state(
2950 indoc! {"
2951 /**s
2952 * ˇ
2953 */
2954 "}
2955 .replace("s", " ") // s is used as space placeholder to prevent format on save
2956 .as_str(),
2957 );
2958 // Ensure that delimiter space is preserved when newline on already
2959 // spaced delimiter.
2960 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2961 cx.assert_editor_state(
2962 indoc! {"
2963 /**s
2964 *s
2965 * ˇ
2966 */
2967 "}
2968 .replace("s", " ") // s is used as space placeholder to prevent format on save
2969 .as_str(),
2970 );
2971 // Ensure that delimiter space is preserved when space is not
2972 // on existing delimiter.
2973 cx.set_state(indoc! {"
2974 /**
2975 *ˇ
2976 */
2977 "});
2978 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2979 cx.assert_editor_state(indoc! {"
2980 /**
2981 *
2982 * ˇ
2983 */
2984 "});
2985 // Ensure that if suffix exists on same line after cursor it
2986 // doesn't add extra new line if prefix is not on same line.
2987 cx.set_state(indoc! {"
2988 /**
2989 ˇ*/
2990 "});
2991 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2992 cx.assert_editor_state(indoc! {"
2993 /**
2994
2995 ˇ*/
2996 "});
2997 // Ensure that it detects suffix after existing prefix.
2998 cx.set_state(indoc! {"
2999 /**ˇ/
3000 "});
3001 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3002 cx.assert_editor_state(indoc! {"
3003 /**
3004 ˇ/
3005 "});
3006 // Ensure that if suffix exists on same line before
3007 // cursor it does not add comment prefix.
3008 cx.set_state(indoc! {"
3009 /** */ˇ
3010 "});
3011 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3012 cx.assert_editor_state(indoc! {"
3013 /** */
3014 ˇ
3015 "});
3016 // Ensure that if suffix exists on same line before
3017 // cursor it does not add comment prefix.
3018 cx.set_state(indoc! {"
3019 /**
3020 *
3021 */ˇ
3022 "});
3023 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3024 cx.assert_editor_state(indoc! {"
3025 /**
3026 *
3027 */
3028 ˇ
3029 "});
3030
3031 // Ensure that inline comment followed by code
3032 // doesn't add comment prefix on newline
3033 cx.set_state(indoc! {"
3034 /** */ textˇ
3035 "});
3036 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3037 cx.assert_editor_state(indoc! {"
3038 /** */ text
3039 ˇ
3040 "});
3041
3042 // Ensure that text after comment end tag
3043 // doesn't add comment prefix on newline
3044 cx.set_state(indoc! {"
3045 /**
3046 *
3047 */ˇtext
3048 "});
3049 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3050 cx.assert_editor_state(indoc! {"
3051 /**
3052 *
3053 */
3054 ˇtext
3055 "});
3056
3057 // Ensure if not comment block it doesn't
3058 // add comment prefix on newline
3059 cx.set_state(indoc! {"
3060 * textˇ
3061 "});
3062 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3063 cx.assert_editor_state(indoc! {"
3064 * text
3065 ˇ
3066 "});
3067 }
3068 // Ensure that comment continuations can be disabled.
3069 update_test_language_settings(cx, |settings| {
3070 settings.defaults.extend_comment_on_newline = Some(false);
3071 });
3072 let mut cx = EditorTestContext::new(cx).await;
3073 cx.set_state(indoc! {"
3074 /**ˇ
3075 "});
3076 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3077 cx.assert_editor_state(indoc! {"
3078 /**
3079 ˇ
3080 "});
3081}
3082
3083#[gpui::test]
3084async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3085 init_test(cx, |settings| {
3086 settings.defaults.tab_size = NonZeroU32::new(4)
3087 });
3088
3089 let lua_language = Arc::new(Language::new(
3090 LanguageConfig {
3091 line_comments: vec!["--".into()],
3092 block_comment: Some(language::BlockCommentConfig {
3093 start: "--[[".into(),
3094 prefix: "".into(),
3095 end: "]]".into(),
3096 tab_size: 0,
3097 }),
3098 ..LanguageConfig::default()
3099 },
3100 None,
3101 ));
3102
3103 let mut cx = EditorTestContext::new(cx).await;
3104 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3105
3106 // Line with line comment should extend
3107 cx.set_state(indoc! {"
3108 --ˇ
3109 "});
3110 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3111 cx.assert_editor_state(indoc! {"
3112 --
3113 --ˇ
3114 "});
3115
3116 // Line with block comment that matches line comment should not extend
3117 cx.set_state(indoc! {"
3118 --[[ˇ
3119 "});
3120 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3121 cx.assert_editor_state(indoc! {"
3122 --[[
3123 ˇ
3124 "});
3125}
3126
3127#[gpui::test]
3128fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3129 init_test(cx, |_| {});
3130
3131 let editor = cx.add_window(|window, cx| {
3132 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3133 let mut editor = build_editor(buffer.clone(), window, cx);
3134 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3135 s.select_ranges([3..4, 11..12, 19..20])
3136 });
3137 editor
3138 });
3139
3140 _ = editor.update(cx, |editor, window, cx| {
3141 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3142 editor.buffer.update(cx, |buffer, cx| {
3143 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3144 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3145 });
3146 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3147
3148 editor.insert("Z", window, cx);
3149 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3150
3151 // The selections are moved after the inserted characters
3152 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3153 });
3154}
3155
3156#[gpui::test]
3157async fn test_tab(cx: &mut TestAppContext) {
3158 init_test(cx, |settings| {
3159 settings.defaults.tab_size = NonZeroU32::new(3)
3160 });
3161
3162 let mut cx = EditorTestContext::new(cx).await;
3163 cx.set_state(indoc! {"
3164 ˇabˇc
3165 ˇ🏀ˇ🏀ˇefg
3166 dˇ
3167 "});
3168 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3169 cx.assert_editor_state(indoc! {"
3170 ˇab ˇc
3171 ˇ🏀 ˇ🏀 ˇefg
3172 d ˇ
3173 "});
3174
3175 cx.set_state(indoc! {"
3176 a
3177 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3178 "});
3179 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3180 cx.assert_editor_state(indoc! {"
3181 a
3182 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3183 "});
3184}
3185
3186#[gpui::test]
3187async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3188 init_test(cx, |_| {});
3189
3190 let mut cx = EditorTestContext::new(cx).await;
3191 let language = Arc::new(
3192 Language::new(
3193 LanguageConfig::default(),
3194 Some(tree_sitter_rust::LANGUAGE.into()),
3195 )
3196 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3197 .unwrap(),
3198 );
3199 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3200
3201 // test when all cursors are not at suggested indent
3202 // then simply move to their suggested indent location
3203 cx.set_state(indoc! {"
3204 const a: B = (
3205 c(
3206 ˇ
3207 ˇ )
3208 );
3209 "});
3210 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3211 cx.assert_editor_state(indoc! {"
3212 const a: B = (
3213 c(
3214 ˇ
3215 ˇ)
3216 );
3217 "});
3218
3219 // test cursor already at suggested indent not moving when
3220 // other cursors are yet to reach their suggested indents
3221 cx.set_state(indoc! {"
3222 ˇ
3223 const a: B = (
3224 c(
3225 d(
3226 ˇ
3227 )
3228 ˇ
3229 ˇ )
3230 );
3231 "});
3232 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3233 cx.assert_editor_state(indoc! {"
3234 ˇ
3235 const a: B = (
3236 c(
3237 d(
3238 ˇ
3239 )
3240 ˇ
3241 ˇ)
3242 );
3243 "});
3244 // test when all cursors are at suggested indent then tab is inserted
3245 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3246 cx.assert_editor_state(indoc! {"
3247 ˇ
3248 const a: B = (
3249 c(
3250 d(
3251 ˇ
3252 )
3253 ˇ
3254 ˇ)
3255 );
3256 "});
3257
3258 // test when current indent is less than suggested indent,
3259 // we adjust line to match suggested indent and move cursor to it
3260 //
3261 // when no other cursor is at word boundary, all of them should move
3262 cx.set_state(indoc! {"
3263 const a: B = (
3264 c(
3265 d(
3266 ˇ
3267 ˇ )
3268 ˇ )
3269 );
3270 "});
3271 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3272 cx.assert_editor_state(indoc! {"
3273 const a: B = (
3274 c(
3275 d(
3276 ˇ
3277 ˇ)
3278 ˇ)
3279 );
3280 "});
3281
3282 // test when current indent is less than suggested indent,
3283 // we adjust line to match suggested indent and move cursor to it
3284 //
3285 // when some other cursor is at word boundary, it should not move
3286 cx.set_state(indoc! {"
3287 const a: B = (
3288 c(
3289 d(
3290 ˇ
3291 ˇ )
3292 ˇ)
3293 );
3294 "});
3295 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3296 cx.assert_editor_state(indoc! {"
3297 const a: B = (
3298 c(
3299 d(
3300 ˇ
3301 ˇ)
3302 ˇ)
3303 );
3304 "});
3305
3306 // test when current indent is more than suggested indent,
3307 // we just move cursor to current indent instead of suggested indent
3308 //
3309 // when no other cursor is at word boundary, all of them should move
3310 cx.set_state(indoc! {"
3311 const a: B = (
3312 c(
3313 d(
3314 ˇ
3315 ˇ )
3316 ˇ )
3317 );
3318 "});
3319 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3320 cx.assert_editor_state(indoc! {"
3321 const a: B = (
3322 c(
3323 d(
3324 ˇ
3325 ˇ)
3326 ˇ)
3327 );
3328 "});
3329 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3330 cx.assert_editor_state(indoc! {"
3331 const a: B = (
3332 c(
3333 d(
3334 ˇ
3335 ˇ)
3336 ˇ)
3337 );
3338 "});
3339
3340 // test when current indent is more than suggested indent,
3341 // we just move cursor to current indent instead of suggested indent
3342 //
3343 // when some other cursor is at word boundary, it doesn't move
3344 cx.set_state(indoc! {"
3345 const a: B = (
3346 c(
3347 d(
3348 ˇ
3349 ˇ )
3350 ˇ)
3351 );
3352 "});
3353 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3354 cx.assert_editor_state(indoc! {"
3355 const a: B = (
3356 c(
3357 d(
3358 ˇ
3359 ˇ)
3360 ˇ)
3361 );
3362 "});
3363
3364 // handle auto-indent when there are multiple cursors on the same line
3365 cx.set_state(indoc! {"
3366 const a: B = (
3367 c(
3368 ˇ ˇ
3369 ˇ )
3370 );
3371 "});
3372 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3373 cx.assert_editor_state(indoc! {"
3374 const a: B = (
3375 c(
3376 ˇ
3377 ˇ)
3378 );
3379 "});
3380}
3381
3382#[gpui::test]
3383async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3384 init_test(cx, |settings| {
3385 settings.defaults.tab_size = NonZeroU32::new(3)
3386 });
3387
3388 let mut cx = EditorTestContext::new(cx).await;
3389 cx.set_state(indoc! {"
3390 ˇ
3391 \t ˇ
3392 \t ˇ
3393 \t ˇ
3394 \t \t\t \t \t\t \t\t \t \t ˇ
3395 "});
3396
3397 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3398 cx.assert_editor_state(indoc! {"
3399 ˇ
3400 \t ˇ
3401 \t ˇ
3402 \t ˇ
3403 \t \t\t \t \t\t \t\t \t \t ˇ
3404 "});
3405}
3406
3407#[gpui::test]
3408async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3409 init_test(cx, |settings| {
3410 settings.defaults.tab_size = NonZeroU32::new(4)
3411 });
3412
3413 let language = Arc::new(
3414 Language::new(
3415 LanguageConfig::default(),
3416 Some(tree_sitter_rust::LANGUAGE.into()),
3417 )
3418 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3419 .unwrap(),
3420 );
3421
3422 let mut cx = EditorTestContext::new(cx).await;
3423 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3424 cx.set_state(indoc! {"
3425 fn a() {
3426 if b {
3427 \t ˇc
3428 }
3429 }
3430 "});
3431
3432 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3433 cx.assert_editor_state(indoc! {"
3434 fn a() {
3435 if b {
3436 ˇc
3437 }
3438 }
3439 "});
3440}
3441
3442#[gpui::test]
3443async fn test_indent_outdent(cx: &mut TestAppContext) {
3444 init_test(cx, |settings| {
3445 settings.defaults.tab_size = NonZeroU32::new(4);
3446 });
3447
3448 let mut cx = EditorTestContext::new(cx).await;
3449
3450 cx.set_state(indoc! {"
3451 «oneˇ» «twoˇ»
3452 three
3453 four
3454 "});
3455 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3456 cx.assert_editor_state(indoc! {"
3457 «oneˇ» «twoˇ»
3458 three
3459 four
3460 "});
3461
3462 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3463 cx.assert_editor_state(indoc! {"
3464 «oneˇ» «twoˇ»
3465 three
3466 four
3467 "});
3468
3469 // select across line ending
3470 cx.set_state(indoc! {"
3471 one two
3472 t«hree
3473 ˇ» four
3474 "});
3475 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3476 cx.assert_editor_state(indoc! {"
3477 one two
3478 t«hree
3479 ˇ» four
3480 "});
3481
3482 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3483 cx.assert_editor_state(indoc! {"
3484 one two
3485 t«hree
3486 ˇ» four
3487 "});
3488
3489 // Ensure that indenting/outdenting works when the cursor is at column 0.
3490 cx.set_state(indoc! {"
3491 one two
3492 ˇthree
3493 four
3494 "});
3495 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3496 cx.assert_editor_state(indoc! {"
3497 one two
3498 ˇthree
3499 four
3500 "});
3501
3502 cx.set_state(indoc! {"
3503 one two
3504 ˇ three
3505 four
3506 "});
3507 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3508 cx.assert_editor_state(indoc! {"
3509 one two
3510 ˇthree
3511 four
3512 "});
3513}
3514
3515#[gpui::test]
3516async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3517 // This is a regression test for issue #33761
3518 init_test(cx, |_| {});
3519
3520 let mut cx = EditorTestContext::new(cx).await;
3521 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3522 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3523
3524 cx.set_state(
3525 r#"ˇ# ingress:
3526ˇ# api:
3527ˇ# enabled: false
3528ˇ# pathType: Prefix
3529ˇ# console:
3530ˇ# enabled: false
3531ˇ# pathType: Prefix
3532"#,
3533 );
3534
3535 // Press tab to indent all lines
3536 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3537
3538 cx.assert_editor_state(
3539 r#" ˇ# ingress:
3540 ˇ# api:
3541 ˇ# enabled: false
3542 ˇ# pathType: Prefix
3543 ˇ# console:
3544 ˇ# enabled: false
3545 ˇ# pathType: Prefix
3546"#,
3547 );
3548}
3549
3550#[gpui::test]
3551async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3552 // This is a test to make sure our fix for issue #33761 didn't break anything
3553 init_test(cx, |_| {});
3554
3555 let mut cx = EditorTestContext::new(cx).await;
3556 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3557 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3558
3559 cx.set_state(
3560 r#"ˇingress:
3561ˇ api:
3562ˇ enabled: false
3563ˇ pathType: Prefix
3564"#,
3565 );
3566
3567 // Press tab to indent all lines
3568 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3569
3570 cx.assert_editor_state(
3571 r#"ˇingress:
3572 ˇapi:
3573 ˇenabled: false
3574 ˇpathType: Prefix
3575"#,
3576 );
3577}
3578
3579#[gpui::test]
3580async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3581 init_test(cx, |settings| {
3582 settings.defaults.hard_tabs = Some(true);
3583 });
3584
3585 let mut cx = EditorTestContext::new(cx).await;
3586
3587 // select two ranges on one line
3588 cx.set_state(indoc! {"
3589 «oneˇ» «twoˇ»
3590 three
3591 four
3592 "});
3593 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3594 cx.assert_editor_state(indoc! {"
3595 \t«oneˇ» «twoˇ»
3596 three
3597 four
3598 "});
3599 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3600 cx.assert_editor_state(indoc! {"
3601 \t\t«oneˇ» «twoˇ»
3602 three
3603 four
3604 "});
3605 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3606 cx.assert_editor_state(indoc! {"
3607 \t«oneˇ» «twoˇ»
3608 three
3609 four
3610 "});
3611 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3612 cx.assert_editor_state(indoc! {"
3613 «oneˇ» «twoˇ»
3614 three
3615 four
3616 "});
3617
3618 // select across a line ending
3619 cx.set_state(indoc! {"
3620 one two
3621 t«hree
3622 ˇ»four
3623 "});
3624 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3625 cx.assert_editor_state(indoc! {"
3626 one two
3627 \tt«hree
3628 ˇ»four
3629 "});
3630 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3631 cx.assert_editor_state(indoc! {"
3632 one two
3633 \t\tt«hree
3634 ˇ»four
3635 "});
3636 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3637 cx.assert_editor_state(indoc! {"
3638 one two
3639 \tt«hree
3640 ˇ»four
3641 "});
3642 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3643 cx.assert_editor_state(indoc! {"
3644 one two
3645 t«hree
3646 ˇ»four
3647 "});
3648
3649 // Ensure that indenting/outdenting works when the cursor is at column 0.
3650 cx.set_state(indoc! {"
3651 one two
3652 ˇthree
3653 four
3654 "});
3655 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3656 cx.assert_editor_state(indoc! {"
3657 one two
3658 ˇthree
3659 four
3660 "});
3661 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3662 cx.assert_editor_state(indoc! {"
3663 one two
3664 \tˇthree
3665 four
3666 "});
3667 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3668 cx.assert_editor_state(indoc! {"
3669 one two
3670 ˇthree
3671 four
3672 "});
3673}
3674
3675#[gpui::test]
3676fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3677 init_test(cx, |settings| {
3678 settings.languages.0.extend([
3679 (
3680 "TOML".into(),
3681 LanguageSettingsContent {
3682 tab_size: NonZeroU32::new(2),
3683 ..Default::default()
3684 },
3685 ),
3686 (
3687 "Rust".into(),
3688 LanguageSettingsContent {
3689 tab_size: NonZeroU32::new(4),
3690 ..Default::default()
3691 },
3692 ),
3693 ]);
3694 });
3695
3696 let toml_language = Arc::new(Language::new(
3697 LanguageConfig {
3698 name: "TOML".into(),
3699 ..Default::default()
3700 },
3701 None,
3702 ));
3703 let rust_language = Arc::new(Language::new(
3704 LanguageConfig {
3705 name: "Rust".into(),
3706 ..Default::default()
3707 },
3708 None,
3709 ));
3710
3711 let toml_buffer =
3712 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3713 let rust_buffer =
3714 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3715 let multibuffer = cx.new(|cx| {
3716 let mut multibuffer = MultiBuffer::new(ReadWrite);
3717 multibuffer.push_excerpts(
3718 toml_buffer.clone(),
3719 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3720 cx,
3721 );
3722 multibuffer.push_excerpts(
3723 rust_buffer.clone(),
3724 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3725 cx,
3726 );
3727 multibuffer
3728 });
3729
3730 cx.add_window(|window, cx| {
3731 let mut editor = build_editor(multibuffer, window, cx);
3732
3733 assert_eq!(
3734 editor.text(cx),
3735 indoc! {"
3736 a = 1
3737 b = 2
3738
3739 const c: usize = 3;
3740 "}
3741 );
3742
3743 select_ranges(
3744 &mut editor,
3745 indoc! {"
3746 «aˇ» = 1
3747 b = 2
3748
3749 «const c:ˇ» usize = 3;
3750 "},
3751 window,
3752 cx,
3753 );
3754
3755 editor.tab(&Tab, window, cx);
3756 assert_text_with_selections(
3757 &mut editor,
3758 indoc! {"
3759 «aˇ» = 1
3760 b = 2
3761
3762 «const c:ˇ» usize = 3;
3763 "},
3764 cx,
3765 );
3766 editor.backtab(&Backtab, window, cx);
3767 assert_text_with_selections(
3768 &mut editor,
3769 indoc! {"
3770 «aˇ» = 1
3771 b = 2
3772
3773 «const c:ˇ» usize = 3;
3774 "},
3775 cx,
3776 );
3777
3778 editor
3779 });
3780}
3781
3782#[gpui::test]
3783async fn test_backspace(cx: &mut TestAppContext) {
3784 init_test(cx, |_| {});
3785
3786 let mut cx = EditorTestContext::new(cx).await;
3787
3788 // Basic backspace
3789 cx.set_state(indoc! {"
3790 onˇe two three
3791 fou«rˇ» five six
3792 seven «ˇeight nine
3793 »ten
3794 "});
3795 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3796 cx.assert_editor_state(indoc! {"
3797 oˇe two three
3798 fouˇ five six
3799 seven ˇten
3800 "});
3801
3802 // Test backspace inside and around indents
3803 cx.set_state(indoc! {"
3804 zero
3805 ˇone
3806 ˇtwo
3807 ˇ ˇ ˇ three
3808 ˇ ˇ four
3809 "});
3810 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3811 cx.assert_editor_state(indoc! {"
3812 zero
3813 ˇone
3814 ˇtwo
3815 ˇ threeˇ four
3816 "});
3817}
3818
3819#[gpui::test]
3820async fn test_delete(cx: &mut TestAppContext) {
3821 init_test(cx, |_| {});
3822
3823 let mut cx = EditorTestContext::new(cx).await;
3824 cx.set_state(indoc! {"
3825 onˇe two three
3826 fou«rˇ» five six
3827 seven «ˇeight nine
3828 »ten
3829 "});
3830 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3831 cx.assert_editor_state(indoc! {"
3832 onˇ two three
3833 fouˇ five six
3834 seven ˇten
3835 "});
3836}
3837
3838#[gpui::test]
3839fn test_delete_line(cx: &mut TestAppContext) {
3840 init_test(cx, |_| {});
3841
3842 let editor = cx.add_window(|window, cx| {
3843 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3844 build_editor(buffer, window, cx)
3845 });
3846 _ = editor.update(cx, |editor, window, cx| {
3847 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3848 s.select_display_ranges([
3849 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3850 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3851 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3852 ])
3853 });
3854 editor.delete_line(&DeleteLine, window, cx);
3855 assert_eq!(editor.display_text(cx), "ghi");
3856 assert_eq!(
3857 editor.selections.display_ranges(cx),
3858 vec![
3859 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3860 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3861 ]
3862 );
3863 });
3864
3865 let editor = cx.add_window(|window, cx| {
3866 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3867 build_editor(buffer, window, cx)
3868 });
3869 _ = editor.update(cx, |editor, window, cx| {
3870 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3871 s.select_display_ranges([
3872 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3873 ])
3874 });
3875 editor.delete_line(&DeleteLine, window, cx);
3876 assert_eq!(editor.display_text(cx), "ghi\n");
3877 assert_eq!(
3878 editor.selections.display_ranges(cx),
3879 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3880 );
3881 });
3882}
3883
3884#[gpui::test]
3885fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3886 init_test(cx, |_| {});
3887
3888 cx.add_window(|window, cx| {
3889 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3890 let mut editor = build_editor(buffer.clone(), window, cx);
3891 let buffer = buffer.read(cx).as_singleton().unwrap();
3892
3893 assert_eq!(
3894 editor.selections.ranges::<Point>(cx),
3895 &[Point::new(0, 0)..Point::new(0, 0)]
3896 );
3897
3898 // When on single line, replace newline at end by space
3899 editor.join_lines(&JoinLines, window, cx);
3900 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3901 assert_eq!(
3902 editor.selections.ranges::<Point>(cx),
3903 &[Point::new(0, 3)..Point::new(0, 3)]
3904 );
3905
3906 // When multiple lines are selected, remove newlines that are spanned by the selection
3907 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3908 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3909 });
3910 editor.join_lines(&JoinLines, window, cx);
3911 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3912 assert_eq!(
3913 editor.selections.ranges::<Point>(cx),
3914 &[Point::new(0, 11)..Point::new(0, 11)]
3915 );
3916
3917 // Undo should be transactional
3918 editor.undo(&Undo, window, cx);
3919 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3920 assert_eq!(
3921 editor.selections.ranges::<Point>(cx),
3922 &[Point::new(0, 5)..Point::new(2, 2)]
3923 );
3924
3925 // When joining an empty line don't insert a space
3926 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3927 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3928 });
3929 editor.join_lines(&JoinLines, window, cx);
3930 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3931 assert_eq!(
3932 editor.selections.ranges::<Point>(cx),
3933 [Point::new(2, 3)..Point::new(2, 3)]
3934 );
3935
3936 // We can remove trailing newlines
3937 editor.join_lines(&JoinLines, window, cx);
3938 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3939 assert_eq!(
3940 editor.selections.ranges::<Point>(cx),
3941 [Point::new(2, 3)..Point::new(2, 3)]
3942 );
3943
3944 // We don't blow up on the last line
3945 editor.join_lines(&JoinLines, window, cx);
3946 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3947 assert_eq!(
3948 editor.selections.ranges::<Point>(cx),
3949 [Point::new(2, 3)..Point::new(2, 3)]
3950 );
3951
3952 // reset to test indentation
3953 editor.buffer.update(cx, |buffer, cx| {
3954 buffer.edit(
3955 [
3956 (Point::new(1, 0)..Point::new(1, 2), " "),
3957 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3958 ],
3959 None,
3960 cx,
3961 )
3962 });
3963
3964 // We remove any leading spaces
3965 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3966 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3967 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3968 });
3969 editor.join_lines(&JoinLines, window, cx);
3970 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3971
3972 // We don't insert a space for a line containing only spaces
3973 editor.join_lines(&JoinLines, window, cx);
3974 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3975
3976 // We ignore any leading tabs
3977 editor.join_lines(&JoinLines, window, cx);
3978 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3979
3980 editor
3981 });
3982}
3983
3984#[gpui::test]
3985fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3986 init_test(cx, |_| {});
3987
3988 cx.add_window(|window, cx| {
3989 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3990 let mut editor = build_editor(buffer.clone(), window, cx);
3991 let buffer = buffer.read(cx).as_singleton().unwrap();
3992
3993 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3994 s.select_ranges([
3995 Point::new(0, 2)..Point::new(1, 1),
3996 Point::new(1, 2)..Point::new(1, 2),
3997 Point::new(3, 1)..Point::new(3, 2),
3998 ])
3999 });
4000
4001 editor.join_lines(&JoinLines, window, cx);
4002 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4003
4004 assert_eq!(
4005 editor.selections.ranges::<Point>(cx),
4006 [
4007 Point::new(0, 7)..Point::new(0, 7),
4008 Point::new(1, 3)..Point::new(1, 3)
4009 ]
4010 );
4011 editor
4012 });
4013}
4014
4015#[gpui::test]
4016async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4017 init_test(cx, |_| {});
4018
4019 let mut cx = EditorTestContext::new(cx).await;
4020
4021 let diff_base = r#"
4022 Line 0
4023 Line 1
4024 Line 2
4025 Line 3
4026 "#
4027 .unindent();
4028
4029 cx.set_state(
4030 &r#"
4031 ˇLine 0
4032 Line 1
4033 Line 2
4034 Line 3
4035 "#
4036 .unindent(),
4037 );
4038
4039 cx.set_head_text(&diff_base);
4040 executor.run_until_parked();
4041
4042 // Join lines
4043 cx.update_editor(|editor, window, cx| {
4044 editor.join_lines(&JoinLines, window, cx);
4045 });
4046 executor.run_until_parked();
4047
4048 cx.assert_editor_state(
4049 &r#"
4050 Line 0ˇ Line 1
4051 Line 2
4052 Line 3
4053 "#
4054 .unindent(),
4055 );
4056 // Join again
4057 cx.update_editor(|editor, window, cx| {
4058 editor.join_lines(&JoinLines, window, cx);
4059 });
4060 executor.run_until_parked();
4061
4062 cx.assert_editor_state(
4063 &r#"
4064 Line 0 Line 1ˇ Line 2
4065 Line 3
4066 "#
4067 .unindent(),
4068 );
4069}
4070
4071#[gpui::test]
4072async fn test_custom_newlines_cause_no_false_positive_diffs(
4073 executor: BackgroundExecutor,
4074 cx: &mut TestAppContext,
4075) {
4076 init_test(cx, |_| {});
4077 let mut cx = EditorTestContext::new(cx).await;
4078 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4079 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4080 executor.run_until_parked();
4081
4082 cx.update_editor(|editor, window, cx| {
4083 let snapshot = editor.snapshot(window, cx);
4084 assert_eq!(
4085 snapshot
4086 .buffer_snapshot
4087 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
4088 .collect::<Vec<_>>(),
4089 Vec::new(),
4090 "Should not have any diffs for files with custom newlines"
4091 );
4092 });
4093}
4094
4095#[gpui::test]
4096async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4097 init_test(cx, |_| {});
4098
4099 let mut cx = EditorTestContext::new(cx).await;
4100
4101 // Test sort_lines_case_insensitive()
4102 cx.set_state(indoc! {"
4103 «z
4104 y
4105 x
4106 Z
4107 Y
4108 Xˇ»
4109 "});
4110 cx.update_editor(|e, window, cx| {
4111 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4112 });
4113 cx.assert_editor_state(indoc! {"
4114 «x
4115 X
4116 y
4117 Y
4118 z
4119 Zˇ»
4120 "});
4121
4122 // Test sort_lines_by_length()
4123 //
4124 // Demonstrates:
4125 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4126 // - sort is stable
4127 cx.set_state(indoc! {"
4128 «123
4129 æ
4130 12
4131 ∞
4132 1
4133 æˇ»
4134 "});
4135 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4136 cx.assert_editor_state(indoc! {"
4137 «æ
4138 ∞
4139 1
4140 æ
4141 12
4142 123ˇ»
4143 "});
4144
4145 // Test reverse_lines()
4146 cx.set_state(indoc! {"
4147 «5
4148 4
4149 3
4150 2
4151 1ˇ»
4152 "});
4153 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4154 cx.assert_editor_state(indoc! {"
4155 «1
4156 2
4157 3
4158 4
4159 5ˇ»
4160 "});
4161
4162 // Skip testing shuffle_line()
4163
4164 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4165 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4166
4167 // Don't manipulate when cursor is on single line, but expand the selection
4168 cx.set_state(indoc! {"
4169 ddˇdd
4170 ccc
4171 bb
4172 a
4173 "});
4174 cx.update_editor(|e, window, cx| {
4175 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4176 });
4177 cx.assert_editor_state(indoc! {"
4178 «ddddˇ»
4179 ccc
4180 bb
4181 a
4182 "});
4183
4184 // Basic manipulate case
4185 // Start selection moves to column 0
4186 // End of selection shrinks to fit shorter line
4187 cx.set_state(indoc! {"
4188 dd«d
4189 ccc
4190 bb
4191 aaaaaˇ»
4192 "});
4193 cx.update_editor(|e, window, cx| {
4194 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4195 });
4196 cx.assert_editor_state(indoc! {"
4197 «aaaaa
4198 bb
4199 ccc
4200 dddˇ»
4201 "});
4202
4203 // Manipulate case with newlines
4204 cx.set_state(indoc! {"
4205 dd«d
4206 ccc
4207
4208 bb
4209 aaaaa
4210
4211 ˇ»
4212 "});
4213 cx.update_editor(|e, window, cx| {
4214 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4215 });
4216 cx.assert_editor_state(indoc! {"
4217 «
4218
4219 aaaaa
4220 bb
4221 ccc
4222 dddˇ»
4223
4224 "});
4225
4226 // Adding new line
4227 cx.set_state(indoc! {"
4228 aa«a
4229 bbˇ»b
4230 "});
4231 cx.update_editor(|e, window, cx| {
4232 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4233 });
4234 cx.assert_editor_state(indoc! {"
4235 «aaa
4236 bbb
4237 added_lineˇ»
4238 "});
4239
4240 // Removing line
4241 cx.set_state(indoc! {"
4242 aa«a
4243 bbbˇ»
4244 "});
4245 cx.update_editor(|e, window, cx| {
4246 e.manipulate_immutable_lines(window, cx, |lines| {
4247 lines.pop();
4248 })
4249 });
4250 cx.assert_editor_state(indoc! {"
4251 «aaaˇ»
4252 "});
4253
4254 // Removing all lines
4255 cx.set_state(indoc! {"
4256 aa«a
4257 bbbˇ»
4258 "});
4259 cx.update_editor(|e, window, cx| {
4260 e.manipulate_immutable_lines(window, cx, |lines| {
4261 lines.drain(..);
4262 })
4263 });
4264 cx.assert_editor_state(indoc! {"
4265 ˇ
4266 "});
4267}
4268
4269#[gpui::test]
4270async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4271 init_test(cx, |_| {});
4272
4273 let mut cx = EditorTestContext::new(cx).await;
4274
4275 // Consider continuous selection as single selection
4276 cx.set_state(indoc! {"
4277 Aaa«aa
4278 cˇ»c«c
4279 bb
4280 aaaˇ»aa
4281 "});
4282 cx.update_editor(|e, window, cx| {
4283 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4284 });
4285 cx.assert_editor_state(indoc! {"
4286 «Aaaaa
4287 ccc
4288 bb
4289 aaaaaˇ»
4290 "});
4291
4292 cx.set_state(indoc! {"
4293 Aaa«aa
4294 cˇ»c«c
4295 bb
4296 aaaˇ»aa
4297 "});
4298 cx.update_editor(|e, window, cx| {
4299 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4300 });
4301 cx.assert_editor_state(indoc! {"
4302 «Aaaaa
4303 ccc
4304 bbˇ»
4305 "});
4306
4307 // Consider non continuous selection as distinct dedup operations
4308 cx.set_state(indoc! {"
4309 «aaaaa
4310 bb
4311 aaaaa
4312 aaaaaˇ»
4313
4314 aaa«aaˇ»
4315 "});
4316 cx.update_editor(|e, window, cx| {
4317 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4318 });
4319 cx.assert_editor_state(indoc! {"
4320 «aaaaa
4321 bbˇ»
4322
4323 «aaaaaˇ»
4324 "});
4325}
4326
4327#[gpui::test]
4328async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4329 init_test(cx, |_| {});
4330
4331 let mut cx = EditorTestContext::new(cx).await;
4332
4333 cx.set_state(indoc! {"
4334 «Aaa
4335 aAa
4336 Aaaˇ»
4337 "});
4338 cx.update_editor(|e, window, cx| {
4339 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4340 });
4341 cx.assert_editor_state(indoc! {"
4342 «Aaa
4343 aAaˇ»
4344 "});
4345
4346 cx.set_state(indoc! {"
4347 «Aaa
4348 aAa
4349 aaAˇ»
4350 "});
4351 cx.update_editor(|e, window, cx| {
4352 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4353 });
4354 cx.assert_editor_state(indoc! {"
4355 «Aaaˇ»
4356 "});
4357}
4358
4359#[gpui::test]
4360async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
4361 init_test(cx, |_| {});
4362
4363 let mut cx = EditorTestContext::new(cx).await;
4364
4365 // Manipulate with multiple selections on a single line
4366 cx.set_state(indoc! {"
4367 dd«dd
4368 cˇ»c«c
4369 bb
4370 aaaˇ»aa
4371 "});
4372 cx.update_editor(|e, window, cx| {
4373 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4374 });
4375 cx.assert_editor_state(indoc! {"
4376 «aaaaa
4377 bb
4378 ccc
4379 ddddˇ»
4380 "});
4381
4382 // Manipulate with multiple disjoin selections
4383 cx.set_state(indoc! {"
4384 5«
4385 4
4386 3
4387 2
4388 1ˇ»
4389
4390 dd«dd
4391 ccc
4392 bb
4393 aaaˇ»aa
4394 "});
4395 cx.update_editor(|e, window, cx| {
4396 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4397 });
4398 cx.assert_editor_state(indoc! {"
4399 «1
4400 2
4401 3
4402 4
4403 5ˇ»
4404
4405 «aaaaa
4406 bb
4407 ccc
4408 ddddˇ»
4409 "});
4410
4411 // Adding lines on each selection
4412 cx.set_state(indoc! {"
4413 2«
4414 1ˇ»
4415
4416 bb«bb
4417 aaaˇ»aa
4418 "});
4419 cx.update_editor(|e, window, cx| {
4420 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
4421 });
4422 cx.assert_editor_state(indoc! {"
4423 «2
4424 1
4425 added lineˇ»
4426
4427 «bbbb
4428 aaaaa
4429 added lineˇ»
4430 "});
4431
4432 // Removing lines on each selection
4433 cx.set_state(indoc! {"
4434 2«
4435 1ˇ»
4436
4437 bb«bb
4438 aaaˇ»aa
4439 "});
4440 cx.update_editor(|e, window, cx| {
4441 e.manipulate_immutable_lines(window, cx, |lines| {
4442 lines.pop();
4443 })
4444 });
4445 cx.assert_editor_state(indoc! {"
4446 «2ˇ»
4447
4448 «bbbbˇ»
4449 "});
4450}
4451
4452#[gpui::test]
4453async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
4454 init_test(cx, |settings| {
4455 settings.defaults.tab_size = NonZeroU32::new(3)
4456 });
4457
4458 let mut cx = EditorTestContext::new(cx).await;
4459
4460 // MULTI SELECTION
4461 // Ln.1 "«" tests empty lines
4462 // Ln.9 tests just leading whitespace
4463 cx.set_state(indoc! {"
4464 «
4465 abc // No indentationˇ»
4466 «\tabc // 1 tabˇ»
4467 \t\tabc « ˇ» // 2 tabs
4468 \t ab«c // Tab followed by space
4469 \tabc // Space followed by tab (3 spaces should be the result)
4470 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4471 abˇ»ˇc ˇ ˇ // Already space indented«
4472 \t
4473 \tabc\tdef // Only the leading tab is manipulatedˇ»
4474 "});
4475 cx.update_editor(|e, window, cx| {
4476 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4477 });
4478 cx.assert_editor_state(
4479 indoc! {"
4480 «
4481 abc // No indentation
4482 abc // 1 tab
4483 abc // 2 tabs
4484 abc // Tab followed by space
4485 abc // Space followed by tab (3 spaces should be the result)
4486 abc // Mixed indentation (tab conversion depends on the column)
4487 abc // Already space indented
4488 ·
4489 abc\tdef // Only the leading tab is manipulatedˇ»
4490 "}
4491 .replace("·", "")
4492 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4493 );
4494
4495 // Test on just a few lines, the others should remain unchanged
4496 // Only lines (3, 5, 10, 11) should change
4497 cx.set_state(
4498 indoc! {"
4499 ·
4500 abc // No indentation
4501 \tabcˇ // 1 tab
4502 \t\tabc // 2 tabs
4503 \t abcˇ // Tab followed by space
4504 \tabc // Space followed by tab (3 spaces should be the result)
4505 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4506 abc // Already space indented
4507 «\t
4508 \tabc\tdef // Only the leading tab is manipulatedˇ»
4509 "}
4510 .replace("·", "")
4511 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4512 );
4513 cx.update_editor(|e, window, cx| {
4514 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4515 });
4516 cx.assert_editor_state(
4517 indoc! {"
4518 ·
4519 abc // No indentation
4520 « abc // 1 tabˇ»
4521 \t\tabc // 2 tabs
4522 « abc // Tab followed by spaceˇ»
4523 \tabc // Space followed by tab (3 spaces should be the result)
4524 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4525 abc // Already space indented
4526 « ·
4527 abc\tdef // Only the leading tab is manipulatedˇ»
4528 "}
4529 .replace("·", "")
4530 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4531 );
4532
4533 // SINGLE SELECTION
4534 // Ln.1 "«" tests empty lines
4535 // Ln.9 tests just leading whitespace
4536 cx.set_state(indoc! {"
4537 «
4538 abc // No indentation
4539 \tabc // 1 tab
4540 \t\tabc // 2 tabs
4541 \t abc // Tab followed by space
4542 \tabc // Space followed by tab (3 spaces should be the result)
4543 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4544 abc // Already space indented
4545 \t
4546 \tabc\tdef // Only the leading tab is manipulatedˇ»
4547 "});
4548 cx.update_editor(|e, window, cx| {
4549 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4550 });
4551 cx.assert_editor_state(
4552 indoc! {"
4553 «
4554 abc // No indentation
4555 abc // 1 tab
4556 abc // 2 tabs
4557 abc // Tab followed by space
4558 abc // Space followed by tab (3 spaces should be the result)
4559 abc // Mixed indentation (tab conversion depends on the column)
4560 abc // Already space indented
4561 ·
4562 abc\tdef // Only the leading tab is manipulatedˇ»
4563 "}
4564 .replace("·", "")
4565 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4566 );
4567}
4568
4569#[gpui::test]
4570async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
4571 init_test(cx, |settings| {
4572 settings.defaults.tab_size = NonZeroU32::new(3)
4573 });
4574
4575 let mut cx = EditorTestContext::new(cx).await;
4576
4577 // MULTI SELECTION
4578 // Ln.1 "«" tests empty lines
4579 // Ln.11 tests just leading whitespace
4580 cx.set_state(indoc! {"
4581 «
4582 abˇ»ˇc // No indentation
4583 abc ˇ ˇ // 1 space (< 3 so dont convert)
4584 abc « // 2 spaces (< 3 so dont convert)
4585 abc // 3 spaces (convert)
4586 abc ˇ» // 5 spaces (1 tab + 2 spaces)
4587 «\tˇ»\t«\tˇ»abc // Already tab indented
4588 «\t abc // Tab followed by space
4589 \tabc // Space followed by tab (should be consumed due to tab)
4590 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4591 \tˇ» «\t
4592 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
4593 "});
4594 cx.update_editor(|e, window, cx| {
4595 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4596 });
4597 cx.assert_editor_state(indoc! {"
4598 «
4599 abc // No indentation
4600 abc // 1 space (< 3 so dont convert)
4601 abc // 2 spaces (< 3 so dont convert)
4602 \tabc // 3 spaces (convert)
4603 \t abc // 5 spaces (1 tab + 2 spaces)
4604 \t\t\tabc // Already tab indented
4605 \t abc // Tab followed by space
4606 \tabc // Space followed by tab (should be consumed due to tab)
4607 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4608 \t\t\t
4609 \tabc \t // Only the leading spaces should be convertedˇ»
4610 "});
4611
4612 // Test on just a few lines, the other should remain unchanged
4613 // Only lines (4, 8, 11, 12) should change
4614 cx.set_state(
4615 indoc! {"
4616 ·
4617 abc // No indentation
4618 abc // 1 space (< 3 so dont convert)
4619 abc // 2 spaces (< 3 so dont convert)
4620 « abc // 3 spaces (convert)ˇ»
4621 abc // 5 spaces (1 tab + 2 spaces)
4622 \t\t\tabc // Already tab indented
4623 \t abc // Tab followed by space
4624 \tabc ˇ // Space followed by tab (should be consumed due to tab)
4625 \t\t \tabc // Mixed indentation
4626 \t \t \t \tabc // Mixed indentation
4627 \t \tˇ
4628 « abc \t // Only the leading spaces should be convertedˇ»
4629 "}
4630 .replace("·", "")
4631 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4632 );
4633 cx.update_editor(|e, window, cx| {
4634 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4635 });
4636 cx.assert_editor_state(
4637 indoc! {"
4638 ·
4639 abc // No indentation
4640 abc // 1 space (< 3 so dont convert)
4641 abc // 2 spaces (< 3 so dont convert)
4642 «\tabc // 3 spaces (convert)ˇ»
4643 abc // 5 spaces (1 tab + 2 spaces)
4644 \t\t\tabc // Already tab indented
4645 \t abc // Tab followed by space
4646 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
4647 \t\t \tabc // Mixed indentation
4648 \t \t \t \tabc // Mixed indentation
4649 «\t\t\t
4650 \tabc \t // Only the leading spaces should be convertedˇ»
4651 "}
4652 .replace("·", "")
4653 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4654 );
4655
4656 // SINGLE SELECTION
4657 // Ln.1 "«" tests empty lines
4658 // Ln.11 tests just leading whitespace
4659 cx.set_state(indoc! {"
4660 «
4661 abc // No indentation
4662 abc // 1 space (< 3 so dont convert)
4663 abc // 2 spaces (< 3 so dont convert)
4664 abc // 3 spaces (convert)
4665 abc // 5 spaces (1 tab + 2 spaces)
4666 \t\t\tabc // Already tab indented
4667 \t abc // Tab followed by space
4668 \tabc // Space followed by tab (should be consumed due to tab)
4669 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4670 \t \t
4671 abc \t // Only the leading spaces should be convertedˇ»
4672 "});
4673 cx.update_editor(|e, window, cx| {
4674 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4675 });
4676 cx.assert_editor_state(indoc! {"
4677 «
4678 abc // No indentation
4679 abc // 1 space (< 3 so dont convert)
4680 abc // 2 spaces (< 3 so dont convert)
4681 \tabc // 3 spaces (convert)
4682 \t abc // 5 spaces (1 tab + 2 spaces)
4683 \t\t\tabc // Already tab indented
4684 \t abc // Tab followed by space
4685 \tabc // Space followed by tab (should be consumed due to tab)
4686 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4687 \t\t\t
4688 \tabc \t // Only the leading spaces should be convertedˇ»
4689 "});
4690}
4691
4692#[gpui::test]
4693async fn test_toggle_case(cx: &mut TestAppContext) {
4694 init_test(cx, |_| {});
4695
4696 let mut cx = EditorTestContext::new(cx).await;
4697
4698 // If all lower case -> upper case
4699 cx.set_state(indoc! {"
4700 «hello worldˇ»
4701 "});
4702 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4703 cx.assert_editor_state(indoc! {"
4704 «HELLO WORLDˇ»
4705 "});
4706
4707 // If all upper case -> lower case
4708 cx.set_state(indoc! {"
4709 «HELLO WORLDˇ»
4710 "});
4711 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4712 cx.assert_editor_state(indoc! {"
4713 «hello worldˇ»
4714 "});
4715
4716 // If any upper case characters are identified -> lower case
4717 // This matches JetBrains IDEs
4718 cx.set_state(indoc! {"
4719 «hEllo worldˇ»
4720 "});
4721 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4722 cx.assert_editor_state(indoc! {"
4723 «hello worldˇ»
4724 "});
4725}
4726
4727#[gpui::test]
4728async fn test_manipulate_text(cx: &mut TestAppContext) {
4729 init_test(cx, |_| {});
4730
4731 let mut cx = EditorTestContext::new(cx).await;
4732
4733 // Test convert_to_upper_case()
4734 cx.set_state(indoc! {"
4735 «hello worldˇ»
4736 "});
4737 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4738 cx.assert_editor_state(indoc! {"
4739 «HELLO WORLDˇ»
4740 "});
4741
4742 // Test convert_to_lower_case()
4743 cx.set_state(indoc! {"
4744 «HELLO WORLDˇ»
4745 "});
4746 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4747 cx.assert_editor_state(indoc! {"
4748 «hello worldˇ»
4749 "});
4750
4751 // Test multiple line, single selection case
4752 cx.set_state(indoc! {"
4753 «The quick brown
4754 fox jumps over
4755 the lazy dogˇ»
4756 "});
4757 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4758 cx.assert_editor_state(indoc! {"
4759 «The Quick Brown
4760 Fox Jumps Over
4761 The Lazy Dogˇ»
4762 "});
4763
4764 // Test multiple line, single selection case
4765 cx.set_state(indoc! {"
4766 «The quick brown
4767 fox jumps over
4768 the lazy dogˇ»
4769 "});
4770 cx.update_editor(|e, window, cx| {
4771 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4772 });
4773 cx.assert_editor_state(indoc! {"
4774 «TheQuickBrown
4775 FoxJumpsOver
4776 TheLazyDogˇ»
4777 "});
4778
4779 // From here on out, test more complex cases of manipulate_text()
4780
4781 // Test no selection case - should affect words cursors are in
4782 // Cursor at beginning, middle, and end of word
4783 cx.set_state(indoc! {"
4784 ˇhello big beauˇtiful worldˇ
4785 "});
4786 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4787 cx.assert_editor_state(indoc! {"
4788 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4789 "});
4790
4791 // Test multiple selections on a single line and across multiple lines
4792 cx.set_state(indoc! {"
4793 «Theˇ» quick «brown
4794 foxˇ» jumps «overˇ»
4795 the «lazyˇ» dog
4796 "});
4797 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4798 cx.assert_editor_state(indoc! {"
4799 «THEˇ» quick «BROWN
4800 FOXˇ» jumps «OVERˇ»
4801 the «LAZYˇ» dog
4802 "});
4803
4804 // Test case where text length grows
4805 cx.set_state(indoc! {"
4806 «tschüߡ»
4807 "});
4808 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4809 cx.assert_editor_state(indoc! {"
4810 «TSCHÜSSˇ»
4811 "});
4812
4813 // Test to make sure we don't crash when text shrinks
4814 cx.set_state(indoc! {"
4815 aaa_bbbˇ
4816 "});
4817 cx.update_editor(|e, window, cx| {
4818 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4819 });
4820 cx.assert_editor_state(indoc! {"
4821 «aaaBbbˇ»
4822 "});
4823
4824 // Test to make sure we all aware of the fact that each word can grow and shrink
4825 // Final selections should be aware of this fact
4826 cx.set_state(indoc! {"
4827 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4828 "});
4829 cx.update_editor(|e, window, cx| {
4830 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4831 });
4832 cx.assert_editor_state(indoc! {"
4833 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4834 "});
4835
4836 cx.set_state(indoc! {"
4837 «hElLo, WoRld!ˇ»
4838 "});
4839 cx.update_editor(|e, window, cx| {
4840 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4841 });
4842 cx.assert_editor_state(indoc! {"
4843 «HeLlO, wOrLD!ˇ»
4844 "});
4845}
4846
4847#[gpui::test]
4848fn test_duplicate_line(cx: &mut TestAppContext) {
4849 init_test(cx, |_| {});
4850
4851 let editor = cx.add_window(|window, cx| {
4852 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4853 build_editor(buffer, window, cx)
4854 });
4855 _ = editor.update(cx, |editor, window, cx| {
4856 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4857 s.select_display_ranges([
4858 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4859 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4860 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4861 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4862 ])
4863 });
4864 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4865 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4866 assert_eq!(
4867 editor.selections.display_ranges(cx),
4868 vec![
4869 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4870 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4871 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4872 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4873 ]
4874 );
4875 });
4876
4877 let editor = cx.add_window(|window, cx| {
4878 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4879 build_editor(buffer, window, cx)
4880 });
4881 _ = editor.update(cx, |editor, window, cx| {
4882 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4883 s.select_display_ranges([
4884 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4885 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4886 ])
4887 });
4888 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4889 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4890 assert_eq!(
4891 editor.selections.display_ranges(cx),
4892 vec![
4893 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4894 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4895 ]
4896 );
4897 });
4898
4899 // With `move_upwards` the selections stay in place, except for
4900 // the lines inserted above them
4901 let editor = cx.add_window(|window, cx| {
4902 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4903 build_editor(buffer, window, cx)
4904 });
4905 _ = editor.update(cx, |editor, window, cx| {
4906 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4907 s.select_display_ranges([
4908 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4909 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4910 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4911 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4912 ])
4913 });
4914 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4915 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4916 assert_eq!(
4917 editor.selections.display_ranges(cx),
4918 vec![
4919 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4920 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4921 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4922 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4923 ]
4924 );
4925 });
4926
4927 let editor = cx.add_window(|window, cx| {
4928 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4929 build_editor(buffer, window, cx)
4930 });
4931 _ = editor.update(cx, |editor, window, cx| {
4932 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4933 s.select_display_ranges([
4934 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4935 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4936 ])
4937 });
4938 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4939 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4940 assert_eq!(
4941 editor.selections.display_ranges(cx),
4942 vec![
4943 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4944 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4945 ]
4946 );
4947 });
4948
4949 let editor = cx.add_window(|window, cx| {
4950 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4951 build_editor(buffer, window, cx)
4952 });
4953 _ = editor.update(cx, |editor, window, cx| {
4954 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4955 s.select_display_ranges([
4956 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4957 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4958 ])
4959 });
4960 editor.duplicate_selection(&DuplicateSelection, window, cx);
4961 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4962 assert_eq!(
4963 editor.selections.display_ranges(cx),
4964 vec![
4965 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4966 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4967 ]
4968 );
4969 });
4970}
4971
4972#[gpui::test]
4973fn test_move_line_up_down(cx: &mut TestAppContext) {
4974 init_test(cx, |_| {});
4975
4976 let editor = cx.add_window(|window, cx| {
4977 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4978 build_editor(buffer, window, cx)
4979 });
4980 _ = editor.update(cx, |editor, window, cx| {
4981 editor.fold_creases(
4982 vec![
4983 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4984 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4985 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4986 ],
4987 true,
4988 window,
4989 cx,
4990 );
4991 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4992 s.select_display_ranges([
4993 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4994 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4995 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4996 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4997 ])
4998 });
4999 assert_eq!(
5000 editor.display_text(cx),
5001 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5002 );
5003
5004 editor.move_line_up(&MoveLineUp, window, cx);
5005 assert_eq!(
5006 editor.display_text(cx),
5007 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5008 );
5009 assert_eq!(
5010 editor.selections.display_ranges(cx),
5011 vec![
5012 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5013 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5014 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5015 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5016 ]
5017 );
5018 });
5019
5020 _ = editor.update(cx, |editor, window, cx| {
5021 editor.move_line_down(&MoveLineDown, window, cx);
5022 assert_eq!(
5023 editor.display_text(cx),
5024 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5025 );
5026 assert_eq!(
5027 editor.selections.display_ranges(cx),
5028 vec![
5029 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5030 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5031 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5032 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5033 ]
5034 );
5035 });
5036
5037 _ = editor.update(cx, |editor, window, cx| {
5038 editor.move_line_down(&MoveLineDown, window, cx);
5039 assert_eq!(
5040 editor.display_text(cx),
5041 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5042 );
5043 assert_eq!(
5044 editor.selections.display_ranges(cx),
5045 vec![
5046 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5047 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5048 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5049 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5050 ]
5051 );
5052 });
5053
5054 _ = editor.update(cx, |editor, window, cx| {
5055 editor.move_line_up(&MoveLineUp, window, cx);
5056 assert_eq!(
5057 editor.display_text(cx),
5058 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5059 );
5060 assert_eq!(
5061 editor.selections.display_ranges(cx),
5062 vec![
5063 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5064 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5065 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5066 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5067 ]
5068 );
5069 });
5070}
5071
5072#[gpui::test]
5073fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5074 init_test(cx, |_| {});
5075
5076 let editor = cx.add_window(|window, cx| {
5077 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5078 build_editor(buffer, window, cx)
5079 });
5080 _ = editor.update(cx, |editor, window, cx| {
5081 let snapshot = editor.buffer.read(cx).snapshot(cx);
5082 editor.insert_blocks(
5083 [BlockProperties {
5084 style: BlockStyle::Fixed,
5085 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5086 height: Some(1),
5087 render: Arc::new(|_| div().into_any()),
5088 priority: 0,
5089 }],
5090 Some(Autoscroll::fit()),
5091 cx,
5092 );
5093 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5094 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5095 });
5096 editor.move_line_down(&MoveLineDown, window, cx);
5097 });
5098}
5099
5100#[gpui::test]
5101async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5102 init_test(cx, |_| {});
5103
5104 let mut cx = EditorTestContext::new(cx).await;
5105 cx.set_state(
5106 &"
5107 ˇzero
5108 one
5109 two
5110 three
5111 four
5112 five
5113 "
5114 .unindent(),
5115 );
5116
5117 // Create a four-line block that replaces three lines of text.
5118 cx.update_editor(|editor, window, cx| {
5119 let snapshot = editor.snapshot(window, cx);
5120 let snapshot = &snapshot.buffer_snapshot;
5121 let placement = BlockPlacement::Replace(
5122 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5123 );
5124 editor.insert_blocks(
5125 [BlockProperties {
5126 placement,
5127 height: Some(4),
5128 style: BlockStyle::Sticky,
5129 render: Arc::new(|_| gpui::div().into_any_element()),
5130 priority: 0,
5131 }],
5132 None,
5133 cx,
5134 );
5135 });
5136
5137 // Move down so that the cursor touches the block.
5138 cx.update_editor(|editor, window, cx| {
5139 editor.move_down(&Default::default(), window, cx);
5140 });
5141 cx.assert_editor_state(
5142 &"
5143 zero
5144 «one
5145 two
5146 threeˇ»
5147 four
5148 five
5149 "
5150 .unindent(),
5151 );
5152
5153 // Move down past the block.
5154 cx.update_editor(|editor, window, cx| {
5155 editor.move_down(&Default::default(), window, cx);
5156 });
5157 cx.assert_editor_state(
5158 &"
5159 zero
5160 one
5161 two
5162 three
5163 ˇfour
5164 five
5165 "
5166 .unindent(),
5167 );
5168}
5169
5170#[gpui::test]
5171fn test_transpose(cx: &mut TestAppContext) {
5172 init_test(cx, |_| {});
5173
5174 _ = cx.add_window(|window, cx| {
5175 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5176 editor.set_style(EditorStyle::default(), window, cx);
5177 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5178 s.select_ranges([1..1])
5179 });
5180 editor.transpose(&Default::default(), window, cx);
5181 assert_eq!(editor.text(cx), "bac");
5182 assert_eq!(editor.selections.ranges(cx), [2..2]);
5183
5184 editor.transpose(&Default::default(), window, cx);
5185 assert_eq!(editor.text(cx), "bca");
5186 assert_eq!(editor.selections.ranges(cx), [3..3]);
5187
5188 editor.transpose(&Default::default(), window, cx);
5189 assert_eq!(editor.text(cx), "bac");
5190 assert_eq!(editor.selections.ranges(cx), [3..3]);
5191
5192 editor
5193 });
5194
5195 _ = cx.add_window(|window, cx| {
5196 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5197 editor.set_style(EditorStyle::default(), window, cx);
5198 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5199 s.select_ranges([3..3])
5200 });
5201 editor.transpose(&Default::default(), window, cx);
5202 assert_eq!(editor.text(cx), "acb\nde");
5203 assert_eq!(editor.selections.ranges(cx), [3..3]);
5204
5205 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5206 s.select_ranges([4..4])
5207 });
5208 editor.transpose(&Default::default(), window, cx);
5209 assert_eq!(editor.text(cx), "acbd\ne");
5210 assert_eq!(editor.selections.ranges(cx), [5..5]);
5211
5212 editor.transpose(&Default::default(), window, cx);
5213 assert_eq!(editor.text(cx), "acbde\n");
5214 assert_eq!(editor.selections.ranges(cx), [6..6]);
5215
5216 editor.transpose(&Default::default(), window, cx);
5217 assert_eq!(editor.text(cx), "acbd\ne");
5218 assert_eq!(editor.selections.ranges(cx), [6..6]);
5219
5220 editor
5221 });
5222
5223 _ = cx.add_window(|window, cx| {
5224 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5225 editor.set_style(EditorStyle::default(), window, cx);
5226 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5227 s.select_ranges([1..1, 2..2, 4..4])
5228 });
5229 editor.transpose(&Default::default(), window, cx);
5230 assert_eq!(editor.text(cx), "bacd\ne");
5231 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5232
5233 editor.transpose(&Default::default(), window, cx);
5234 assert_eq!(editor.text(cx), "bcade\n");
5235 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5236
5237 editor.transpose(&Default::default(), window, cx);
5238 assert_eq!(editor.text(cx), "bcda\ne");
5239 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5240
5241 editor.transpose(&Default::default(), window, cx);
5242 assert_eq!(editor.text(cx), "bcade\n");
5243 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5244
5245 editor.transpose(&Default::default(), window, cx);
5246 assert_eq!(editor.text(cx), "bcaed\n");
5247 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5248
5249 editor
5250 });
5251
5252 _ = cx.add_window(|window, cx| {
5253 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5254 editor.set_style(EditorStyle::default(), window, cx);
5255 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5256 s.select_ranges([4..4])
5257 });
5258 editor.transpose(&Default::default(), window, cx);
5259 assert_eq!(editor.text(cx), "🏀🍐✋");
5260 assert_eq!(editor.selections.ranges(cx), [8..8]);
5261
5262 editor.transpose(&Default::default(), window, cx);
5263 assert_eq!(editor.text(cx), "🏀✋🍐");
5264 assert_eq!(editor.selections.ranges(cx), [11..11]);
5265
5266 editor.transpose(&Default::default(), window, cx);
5267 assert_eq!(editor.text(cx), "🏀🍐✋");
5268 assert_eq!(editor.selections.ranges(cx), [11..11]);
5269
5270 editor
5271 });
5272}
5273
5274#[gpui::test]
5275async fn test_rewrap(cx: &mut TestAppContext) {
5276 init_test(cx, |settings| {
5277 settings.languages.0.extend([
5278 (
5279 "Markdown".into(),
5280 LanguageSettingsContent {
5281 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5282 preferred_line_length: Some(40),
5283 ..Default::default()
5284 },
5285 ),
5286 (
5287 "Plain Text".into(),
5288 LanguageSettingsContent {
5289 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5290 preferred_line_length: Some(40),
5291 ..Default::default()
5292 },
5293 ),
5294 (
5295 "C++".into(),
5296 LanguageSettingsContent {
5297 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5298 preferred_line_length: Some(40),
5299 ..Default::default()
5300 },
5301 ),
5302 (
5303 "Python".into(),
5304 LanguageSettingsContent {
5305 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5306 preferred_line_length: Some(40),
5307 ..Default::default()
5308 },
5309 ),
5310 (
5311 "Rust".into(),
5312 LanguageSettingsContent {
5313 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5314 preferred_line_length: Some(40),
5315 ..Default::default()
5316 },
5317 ),
5318 ])
5319 });
5320
5321 let mut cx = EditorTestContext::new(cx).await;
5322
5323 let cpp_language = Arc::new(Language::new(
5324 LanguageConfig {
5325 name: "C++".into(),
5326 line_comments: vec!["// ".into()],
5327 ..LanguageConfig::default()
5328 },
5329 None,
5330 ));
5331 let python_language = Arc::new(Language::new(
5332 LanguageConfig {
5333 name: "Python".into(),
5334 line_comments: vec!["# ".into()],
5335 ..LanguageConfig::default()
5336 },
5337 None,
5338 ));
5339 let markdown_language = Arc::new(Language::new(
5340 LanguageConfig {
5341 name: "Markdown".into(),
5342 rewrap_prefixes: vec![
5343 regex::Regex::new("\\d+\\.\\s+").unwrap(),
5344 regex::Regex::new("[-*+]\\s+").unwrap(),
5345 ],
5346 ..LanguageConfig::default()
5347 },
5348 None,
5349 ));
5350 let rust_language = Arc::new(Language::new(
5351 LanguageConfig {
5352 name: "Rust".into(),
5353 line_comments: vec!["// ".into(), "/// ".into()],
5354 ..LanguageConfig::default()
5355 },
5356 Some(tree_sitter_rust::LANGUAGE.into()),
5357 ));
5358
5359 let plaintext_language = Arc::new(Language::new(
5360 LanguageConfig {
5361 name: "Plain Text".into(),
5362 ..LanguageConfig::default()
5363 },
5364 None,
5365 ));
5366
5367 // Test basic rewrapping of a long line with a cursor
5368 assert_rewrap(
5369 indoc! {"
5370 // ˇThis is a long comment that needs to be wrapped.
5371 "},
5372 indoc! {"
5373 // ˇThis is a long comment that needs to
5374 // be wrapped.
5375 "},
5376 cpp_language.clone(),
5377 &mut cx,
5378 );
5379
5380 // Test rewrapping a full selection
5381 assert_rewrap(
5382 indoc! {"
5383 «// This selected long comment needs to be wrapped.ˇ»"
5384 },
5385 indoc! {"
5386 «// This selected long comment needs to
5387 // be wrapped.ˇ»"
5388 },
5389 cpp_language.clone(),
5390 &mut cx,
5391 );
5392
5393 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
5394 assert_rewrap(
5395 indoc! {"
5396 // ˇThis is the first line.
5397 // Thisˇ is the second line.
5398 // This is the thirdˇ line, all part of one paragraph.
5399 "},
5400 indoc! {"
5401 // ˇThis is the first line. Thisˇ is the
5402 // second line. This is the thirdˇ line,
5403 // all part of one paragraph.
5404 "},
5405 cpp_language.clone(),
5406 &mut cx,
5407 );
5408
5409 // Test multiple cursors in different paragraphs trigger separate rewraps
5410 assert_rewrap(
5411 indoc! {"
5412 // ˇThis is the first paragraph, first line.
5413 // ˇThis is the first paragraph, second line.
5414
5415 // ˇThis is the second paragraph, first line.
5416 // ˇThis is the second paragraph, second line.
5417 "},
5418 indoc! {"
5419 // ˇThis is the first paragraph, first
5420 // line. ˇThis is the first paragraph,
5421 // second line.
5422
5423 // ˇThis is the second paragraph, first
5424 // line. ˇThis is the second paragraph,
5425 // second line.
5426 "},
5427 cpp_language.clone(),
5428 &mut cx,
5429 );
5430
5431 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
5432 assert_rewrap(
5433 indoc! {"
5434 «// A regular long long comment to be wrapped.
5435 /// A documentation long comment to be wrapped.ˇ»
5436 "},
5437 indoc! {"
5438 «// A regular long long comment to be
5439 // wrapped.
5440 /// A documentation long comment to be
5441 /// wrapped.ˇ»
5442 "},
5443 rust_language.clone(),
5444 &mut cx,
5445 );
5446
5447 // Test that change in indentation level trigger seperate rewraps
5448 assert_rewrap(
5449 indoc! {"
5450 fn foo() {
5451 «// This is a long comment at the base indent.
5452 // This is a long comment at the next indent.ˇ»
5453 }
5454 "},
5455 indoc! {"
5456 fn foo() {
5457 «// This is a long comment at the
5458 // base indent.
5459 // This is a long comment at the
5460 // next indent.ˇ»
5461 }
5462 "},
5463 rust_language.clone(),
5464 &mut cx,
5465 );
5466
5467 // Test that different comment prefix characters (e.g., '#') are handled correctly
5468 assert_rewrap(
5469 indoc! {"
5470 # ˇThis is a long comment using a pound sign.
5471 "},
5472 indoc! {"
5473 # ˇThis is a long comment using a pound
5474 # sign.
5475 "},
5476 python_language.clone(),
5477 &mut cx,
5478 );
5479
5480 // Test rewrapping only affects comments, not code even when selected
5481 assert_rewrap(
5482 indoc! {"
5483 «/// This doc comment is long and should be wrapped.
5484 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5485 "},
5486 indoc! {"
5487 «/// This doc comment is long and should
5488 /// be wrapped.
5489 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5490 "},
5491 rust_language.clone(),
5492 &mut cx,
5493 );
5494
5495 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
5496 assert_rewrap(
5497 indoc! {"
5498 # Header
5499
5500 A long long long line of markdown text to wrap.ˇ
5501 "},
5502 indoc! {"
5503 # Header
5504
5505 A long long long line of markdown text
5506 to wrap.ˇ
5507 "},
5508 markdown_language.clone(),
5509 &mut cx,
5510 );
5511
5512 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
5513 assert_rewrap(
5514 indoc! {"
5515 «1. This is a numbered list item that is very long and needs to be wrapped properly.
5516 2. This is a numbered list item that is very long and needs to be wrapped properly.
5517 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
5518 "},
5519 indoc! {"
5520 «1. This is a numbered list item that is
5521 very long and needs to be wrapped
5522 properly.
5523 2. This is a numbered list item that is
5524 very long and needs to be wrapped
5525 properly.
5526 - This is an unordered list item that is
5527 also very long and should not merge
5528 with the numbered item.ˇ»
5529 "},
5530 markdown_language.clone(),
5531 &mut cx,
5532 );
5533
5534 // Test that rewrapping add indents for rewrapping boundary if not exists already.
5535 assert_rewrap(
5536 indoc! {"
5537 «1. This is a numbered list item that is
5538 very long and needs to be wrapped
5539 properly.
5540 2. This is a numbered list item that is
5541 very long and needs to be wrapped
5542 properly.
5543 - This is an unordered list item that is
5544 also very long and should not merge with
5545 the numbered item.ˇ»
5546 "},
5547 indoc! {"
5548 «1. This is a numbered list item that is
5549 very long and needs to be wrapped
5550 properly.
5551 2. This is a numbered list item that is
5552 very long and needs to be wrapped
5553 properly.
5554 - This is an unordered list item that is
5555 also very long and should not merge
5556 with the numbered item.ˇ»
5557 "},
5558 markdown_language.clone(),
5559 &mut cx,
5560 );
5561
5562 // Test that rewrapping maintain indents even when they already exists.
5563 assert_rewrap(
5564 indoc! {"
5565 «1. This is a numbered list
5566 item that is very long and needs to be wrapped properly.
5567 2. This is a numbered list
5568 item that is very long and needs to be wrapped properly.
5569 - This is an unordered list item that is also very long and
5570 should not merge with the numbered item.ˇ»
5571 "},
5572 indoc! {"
5573 «1. This is a numbered list item that is
5574 very long and needs to be wrapped
5575 properly.
5576 2. This is a numbered list item that is
5577 very long and needs to be wrapped
5578 properly.
5579 - This is an unordered list item that is
5580 also very long and should not merge
5581 with the numbered item.ˇ»
5582 "},
5583 markdown_language.clone(),
5584 &mut cx,
5585 );
5586
5587 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
5588 assert_rewrap(
5589 indoc! {"
5590 ˇThis is a very long line of plain text that will be wrapped.
5591 "},
5592 indoc! {"
5593 ˇThis is a very long line of plain text
5594 that will be wrapped.
5595 "},
5596 plaintext_language.clone(),
5597 &mut cx,
5598 );
5599
5600 // Test that non-commented code acts as a paragraph boundary within a selection
5601 assert_rewrap(
5602 indoc! {"
5603 «// This is the first long comment block to be wrapped.
5604 fn my_func(a: u32);
5605 // This is the second long comment block to be wrapped.ˇ»
5606 "},
5607 indoc! {"
5608 «// This is the first long comment block
5609 // to be wrapped.
5610 fn my_func(a: u32);
5611 // This is the second long comment block
5612 // to be wrapped.ˇ»
5613 "},
5614 rust_language.clone(),
5615 &mut cx,
5616 );
5617
5618 // Test rewrapping multiple selections, including ones with blank lines or tabs
5619 assert_rewrap(
5620 indoc! {"
5621 «ˇThis is a very long line that will be wrapped.
5622
5623 This is another paragraph in the same selection.»
5624
5625 «\tThis is a very long indented line that will be wrapped.ˇ»
5626 "},
5627 indoc! {"
5628 «ˇThis is a very long line that will be
5629 wrapped.
5630
5631 This is another paragraph in the same
5632 selection.»
5633
5634 «\tThis is a very long indented line
5635 \tthat will be wrapped.ˇ»
5636 "},
5637 plaintext_language.clone(),
5638 &mut cx,
5639 );
5640
5641 // Test that an empty comment line acts as a paragraph boundary
5642 assert_rewrap(
5643 indoc! {"
5644 // ˇThis is a long comment that will be wrapped.
5645 //
5646 // And this is another long comment that will also be wrapped.ˇ
5647 "},
5648 indoc! {"
5649 // ˇThis is a long comment that will be
5650 // wrapped.
5651 //
5652 // And this is another long comment that
5653 // will also be wrapped.ˇ
5654 "},
5655 cpp_language,
5656 &mut cx,
5657 );
5658
5659 #[track_caller]
5660 fn assert_rewrap(
5661 unwrapped_text: &str,
5662 wrapped_text: &str,
5663 language: Arc<Language>,
5664 cx: &mut EditorTestContext,
5665 ) {
5666 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5667 cx.set_state(unwrapped_text);
5668 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5669 cx.assert_editor_state(wrapped_text);
5670 }
5671}
5672
5673#[gpui::test]
5674async fn test_hard_wrap(cx: &mut TestAppContext) {
5675 init_test(cx, |_| {});
5676 let mut cx = EditorTestContext::new(cx).await;
5677
5678 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5679 cx.update_editor(|editor, _, cx| {
5680 editor.set_hard_wrap(Some(14), cx);
5681 });
5682
5683 cx.set_state(indoc!(
5684 "
5685 one two three ˇ
5686 "
5687 ));
5688 cx.simulate_input("four");
5689 cx.run_until_parked();
5690
5691 cx.assert_editor_state(indoc!(
5692 "
5693 one two three
5694 fourˇ
5695 "
5696 ));
5697
5698 cx.update_editor(|editor, window, cx| {
5699 editor.newline(&Default::default(), window, cx);
5700 });
5701 cx.run_until_parked();
5702 cx.assert_editor_state(indoc!(
5703 "
5704 one two three
5705 four
5706 ˇ
5707 "
5708 ));
5709
5710 cx.simulate_input("five");
5711 cx.run_until_parked();
5712 cx.assert_editor_state(indoc!(
5713 "
5714 one two three
5715 four
5716 fiveˇ
5717 "
5718 ));
5719
5720 cx.update_editor(|editor, window, cx| {
5721 editor.newline(&Default::default(), window, cx);
5722 });
5723 cx.run_until_parked();
5724 cx.simulate_input("# ");
5725 cx.run_until_parked();
5726 cx.assert_editor_state(indoc!(
5727 "
5728 one two three
5729 four
5730 five
5731 # ˇ
5732 "
5733 ));
5734
5735 cx.update_editor(|editor, window, cx| {
5736 editor.newline(&Default::default(), window, cx);
5737 });
5738 cx.run_until_parked();
5739 cx.assert_editor_state(indoc!(
5740 "
5741 one two three
5742 four
5743 five
5744 #\x20
5745 #ˇ
5746 "
5747 ));
5748
5749 cx.simulate_input(" 6");
5750 cx.run_until_parked();
5751 cx.assert_editor_state(indoc!(
5752 "
5753 one two three
5754 four
5755 five
5756 #
5757 # 6ˇ
5758 "
5759 ));
5760}
5761
5762#[gpui::test]
5763async fn test_clipboard(cx: &mut TestAppContext) {
5764 init_test(cx, |_| {});
5765
5766 let mut cx = EditorTestContext::new(cx).await;
5767
5768 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5769 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5770 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5771
5772 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5773 cx.set_state("two ˇfour ˇsix ˇ");
5774 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5775 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5776
5777 // Paste again but with only two cursors. Since the number of cursors doesn't
5778 // match the number of slices in the clipboard, the entire clipboard text
5779 // is pasted at each cursor.
5780 cx.set_state("ˇtwo one✅ four three six five ˇ");
5781 cx.update_editor(|e, window, cx| {
5782 e.handle_input("( ", window, cx);
5783 e.paste(&Paste, window, cx);
5784 e.handle_input(") ", window, cx);
5785 });
5786 cx.assert_editor_state(
5787 &([
5788 "( one✅ ",
5789 "three ",
5790 "five ) ˇtwo one✅ four three six five ( one✅ ",
5791 "three ",
5792 "five ) ˇ",
5793 ]
5794 .join("\n")),
5795 );
5796
5797 // Cut with three selections, one of which is full-line.
5798 cx.set_state(indoc! {"
5799 1«2ˇ»3
5800 4ˇ567
5801 «8ˇ»9"});
5802 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5803 cx.assert_editor_state(indoc! {"
5804 1ˇ3
5805 ˇ9"});
5806
5807 // Paste with three selections, noticing how the copied selection that was full-line
5808 // gets inserted before the second cursor.
5809 cx.set_state(indoc! {"
5810 1ˇ3
5811 9ˇ
5812 «oˇ»ne"});
5813 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5814 cx.assert_editor_state(indoc! {"
5815 12ˇ3
5816 4567
5817 9ˇ
5818 8ˇne"});
5819
5820 // Copy with a single cursor only, which writes the whole line into the clipboard.
5821 cx.set_state(indoc! {"
5822 The quick brown
5823 fox juˇmps over
5824 the lazy dog"});
5825 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5826 assert_eq!(
5827 cx.read_from_clipboard()
5828 .and_then(|item| item.text().as_deref().map(str::to_string)),
5829 Some("fox jumps over\n".to_string())
5830 );
5831
5832 // Paste with three selections, noticing how the copied full-line selection is inserted
5833 // before the empty selections but replaces the selection that is non-empty.
5834 cx.set_state(indoc! {"
5835 Tˇhe quick brown
5836 «foˇ»x jumps over
5837 tˇhe lazy dog"});
5838 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5839 cx.assert_editor_state(indoc! {"
5840 fox jumps over
5841 Tˇhe quick brown
5842 fox jumps over
5843 ˇx jumps over
5844 fox jumps over
5845 tˇhe lazy dog"});
5846}
5847
5848#[gpui::test]
5849async fn test_copy_trim(cx: &mut TestAppContext) {
5850 init_test(cx, |_| {});
5851
5852 let mut cx = EditorTestContext::new(cx).await;
5853 cx.set_state(
5854 r#" «for selection in selections.iter() {
5855 let mut start = selection.start;
5856 let mut end = selection.end;
5857 let is_entire_line = selection.is_empty();
5858 if is_entire_line {
5859 start = Point::new(start.row, 0);ˇ»
5860 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5861 }
5862 "#,
5863 );
5864 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5865 assert_eq!(
5866 cx.read_from_clipboard()
5867 .and_then(|item| item.text().as_deref().map(str::to_string)),
5868 Some(
5869 "for selection in selections.iter() {
5870 let mut start = selection.start;
5871 let mut end = selection.end;
5872 let is_entire_line = selection.is_empty();
5873 if is_entire_line {
5874 start = Point::new(start.row, 0);"
5875 .to_string()
5876 ),
5877 "Regular copying preserves all indentation selected",
5878 );
5879 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5880 assert_eq!(
5881 cx.read_from_clipboard()
5882 .and_then(|item| item.text().as_deref().map(str::to_string)),
5883 Some(
5884 "for selection in selections.iter() {
5885let mut start = selection.start;
5886let mut end = selection.end;
5887let is_entire_line = selection.is_empty();
5888if is_entire_line {
5889 start = Point::new(start.row, 0);"
5890 .to_string()
5891 ),
5892 "Copying with stripping should strip all leading whitespaces"
5893 );
5894
5895 cx.set_state(
5896 r#" « for selection in selections.iter() {
5897 let mut start = selection.start;
5898 let mut end = selection.end;
5899 let is_entire_line = selection.is_empty();
5900 if is_entire_line {
5901 start = Point::new(start.row, 0);ˇ»
5902 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5903 }
5904 "#,
5905 );
5906 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5907 assert_eq!(
5908 cx.read_from_clipboard()
5909 .and_then(|item| item.text().as_deref().map(str::to_string)),
5910 Some(
5911 " for selection in selections.iter() {
5912 let mut start = selection.start;
5913 let mut end = selection.end;
5914 let is_entire_line = selection.is_empty();
5915 if is_entire_line {
5916 start = Point::new(start.row, 0);"
5917 .to_string()
5918 ),
5919 "Regular copying preserves all indentation selected",
5920 );
5921 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5922 assert_eq!(
5923 cx.read_from_clipboard()
5924 .and_then(|item| item.text().as_deref().map(str::to_string)),
5925 Some(
5926 "for selection in selections.iter() {
5927let mut start = selection.start;
5928let mut end = selection.end;
5929let is_entire_line = selection.is_empty();
5930if is_entire_line {
5931 start = Point::new(start.row, 0);"
5932 .to_string()
5933 ),
5934 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5935 );
5936
5937 cx.set_state(
5938 r#" «ˇ for selection in selections.iter() {
5939 let mut start = selection.start;
5940 let mut end = selection.end;
5941 let is_entire_line = selection.is_empty();
5942 if is_entire_line {
5943 start = Point::new(start.row, 0);»
5944 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5945 }
5946 "#,
5947 );
5948 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5949 assert_eq!(
5950 cx.read_from_clipboard()
5951 .and_then(|item| item.text().as_deref().map(str::to_string)),
5952 Some(
5953 " for selection in selections.iter() {
5954 let mut start = selection.start;
5955 let mut end = selection.end;
5956 let is_entire_line = selection.is_empty();
5957 if is_entire_line {
5958 start = Point::new(start.row, 0);"
5959 .to_string()
5960 ),
5961 "Regular copying for reverse selection works the same",
5962 );
5963 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5964 assert_eq!(
5965 cx.read_from_clipboard()
5966 .and_then(|item| item.text().as_deref().map(str::to_string)),
5967 Some(
5968 "for selection in selections.iter() {
5969let mut start = selection.start;
5970let mut end = selection.end;
5971let is_entire_line = selection.is_empty();
5972if is_entire_line {
5973 start = Point::new(start.row, 0);"
5974 .to_string()
5975 ),
5976 "Copying with stripping for reverse selection works the same"
5977 );
5978
5979 cx.set_state(
5980 r#" for selection «in selections.iter() {
5981 let mut start = selection.start;
5982 let mut end = selection.end;
5983 let is_entire_line = selection.is_empty();
5984 if is_entire_line {
5985 start = Point::new(start.row, 0);ˇ»
5986 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5987 }
5988 "#,
5989 );
5990 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5991 assert_eq!(
5992 cx.read_from_clipboard()
5993 .and_then(|item| item.text().as_deref().map(str::to_string)),
5994 Some(
5995 "in selections.iter() {
5996 let mut start = selection.start;
5997 let mut end = selection.end;
5998 let is_entire_line = selection.is_empty();
5999 if is_entire_line {
6000 start = Point::new(start.row, 0);"
6001 .to_string()
6002 ),
6003 "When selecting past the indent, the copying works as usual",
6004 );
6005 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6006 assert_eq!(
6007 cx.read_from_clipboard()
6008 .and_then(|item| item.text().as_deref().map(str::to_string)),
6009 Some(
6010 "in selections.iter() {
6011 let mut start = selection.start;
6012 let mut end = selection.end;
6013 let is_entire_line = selection.is_empty();
6014 if is_entire_line {
6015 start = Point::new(start.row, 0);"
6016 .to_string()
6017 ),
6018 "When selecting past the indent, nothing is trimmed"
6019 );
6020
6021 cx.set_state(
6022 r#" «for selection in selections.iter() {
6023 let mut start = selection.start;
6024
6025 let mut end = selection.end;
6026 let is_entire_line = selection.is_empty();
6027 if is_entire_line {
6028 start = Point::new(start.row, 0);
6029ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
6030 }
6031 "#,
6032 );
6033 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6034 assert_eq!(
6035 cx.read_from_clipboard()
6036 .and_then(|item| item.text().as_deref().map(str::to_string)),
6037 Some(
6038 "for selection in selections.iter() {
6039let mut start = selection.start;
6040
6041let mut end = selection.end;
6042let is_entire_line = selection.is_empty();
6043if is_entire_line {
6044 start = Point::new(start.row, 0);
6045"
6046 .to_string()
6047 ),
6048 "Copying with stripping should ignore empty lines"
6049 );
6050}
6051
6052#[gpui::test]
6053async fn test_paste_multiline(cx: &mut TestAppContext) {
6054 init_test(cx, |_| {});
6055
6056 let mut cx = EditorTestContext::new(cx).await;
6057 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6058
6059 // Cut an indented block, without the leading whitespace.
6060 cx.set_state(indoc! {"
6061 const a: B = (
6062 c(),
6063 «d(
6064 e,
6065 f
6066 )ˇ»
6067 );
6068 "});
6069 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6070 cx.assert_editor_state(indoc! {"
6071 const a: B = (
6072 c(),
6073 ˇ
6074 );
6075 "});
6076
6077 // Paste it at the same position.
6078 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6079 cx.assert_editor_state(indoc! {"
6080 const a: B = (
6081 c(),
6082 d(
6083 e,
6084 f
6085 )ˇ
6086 );
6087 "});
6088
6089 // Paste it at a line with a lower indent level.
6090 cx.set_state(indoc! {"
6091 ˇ
6092 const a: B = (
6093 c(),
6094 );
6095 "});
6096 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6097 cx.assert_editor_state(indoc! {"
6098 d(
6099 e,
6100 f
6101 )ˇ
6102 const a: B = (
6103 c(),
6104 );
6105 "});
6106
6107 // Cut an indented block, with the leading whitespace.
6108 cx.set_state(indoc! {"
6109 const a: B = (
6110 c(),
6111 « d(
6112 e,
6113 f
6114 )
6115 ˇ»);
6116 "});
6117 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6118 cx.assert_editor_state(indoc! {"
6119 const a: B = (
6120 c(),
6121 ˇ);
6122 "});
6123
6124 // Paste it at the same position.
6125 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6126 cx.assert_editor_state(indoc! {"
6127 const a: B = (
6128 c(),
6129 d(
6130 e,
6131 f
6132 )
6133 ˇ);
6134 "});
6135
6136 // Paste it at a line with a higher indent level.
6137 cx.set_state(indoc! {"
6138 const a: B = (
6139 c(),
6140 d(
6141 e,
6142 fˇ
6143 )
6144 );
6145 "});
6146 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6147 cx.assert_editor_state(indoc! {"
6148 const a: B = (
6149 c(),
6150 d(
6151 e,
6152 f d(
6153 e,
6154 f
6155 )
6156 ˇ
6157 )
6158 );
6159 "});
6160
6161 // Copy an indented block, starting mid-line
6162 cx.set_state(indoc! {"
6163 const a: B = (
6164 c(),
6165 somethin«g(
6166 e,
6167 f
6168 )ˇ»
6169 );
6170 "});
6171 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6172
6173 // Paste it on a line with a lower indent level
6174 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
6175 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6176 cx.assert_editor_state(indoc! {"
6177 const a: B = (
6178 c(),
6179 something(
6180 e,
6181 f
6182 )
6183 );
6184 g(
6185 e,
6186 f
6187 )ˇ"});
6188}
6189
6190#[gpui::test]
6191async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
6192 init_test(cx, |_| {});
6193
6194 cx.write_to_clipboard(ClipboardItem::new_string(
6195 " d(\n e\n );\n".into(),
6196 ));
6197
6198 let mut cx = EditorTestContext::new(cx).await;
6199 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6200
6201 cx.set_state(indoc! {"
6202 fn a() {
6203 b();
6204 if c() {
6205 ˇ
6206 }
6207 }
6208 "});
6209
6210 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6211 cx.assert_editor_state(indoc! {"
6212 fn a() {
6213 b();
6214 if c() {
6215 d(
6216 e
6217 );
6218 ˇ
6219 }
6220 }
6221 "});
6222
6223 cx.set_state(indoc! {"
6224 fn a() {
6225 b();
6226 ˇ
6227 }
6228 "});
6229
6230 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6231 cx.assert_editor_state(indoc! {"
6232 fn a() {
6233 b();
6234 d(
6235 e
6236 );
6237 ˇ
6238 }
6239 "});
6240}
6241
6242#[gpui::test]
6243fn test_select_all(cx: &mut TestAppContext) {
6244 init_test(cx, |_| {});
6245
6246 let editor = cx.add_window(|window, cx| {
6247 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
6248 build_editor(buffer, window, cx)
6249 });
6250 _ = editor.update(cx, |editor, window, cx| {
6251 editor.select_all(&SelectAll, window, cx);
6252 assert_eq!(
6253 editor.selections.display_ranges(cx),
6254 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
6255 );
6256 });
6257}
6258
6259#[gpui::test]
6260fn test_select_line(cx: &mut TestAppContext) {
6261 init_test(cx, |_| {});
6262
6263 let editor = cx.add_window(|window, cx| {
6264 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
6265 build_editor(buffer, window, cx)
6266 });
6267 _ = editor.update(cx, |editor, window, cx| {
6268 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6269 s.select_display_ranges([
6270 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6271 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6272 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6273 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
6274 ])
6275 });
6276 editor.select_line(&SelectLine, window, cx);
6277 assert_eq!(
6278 editor.selections.display_ranges(cx),
6279 vec![
6280 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
6281 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
6282 ]
6283 );
6284 });
6285
6286 _ = editor.update(cx, |editor, window, cx| {
6287 editor.select_line(&SelectLine, window, cx);
6288 assert_eq!(
6289 editor.selections.display_ranges(cx),
6290 vec![
6291 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
6292 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
6293 ]
6294 );
6295 });
6296
6297 _ = editor.update(cx, |editor, window, cx| {
6298 editor.select_line(&SelectLine, window, cx);
6299 assert_eq!(
6300 editor.selections.display_ranges(cx),
6301 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
6302 );
6303 });
6304}
6305
6306#[gpui::test]
6307async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
6308 init_test(cx, |_| {});
6309 let mut cx = EditorTestContext::new(cx).await;
6310
6311 #[track_caller]
6312 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
6313 cx.set_state(initial_state);
6314 cx.update_editor(|e, window, cx| {
6315 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
6316 });
6317 cx.assert_editor_state(expected_state);
6318 }
6319
6320 // Selection starts and ends at the middle of lines, left-to-right
6321 test(
6322 &mut cx,
6323 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
6324 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6325 );
6326 // Same thing, right-to-left
6327 test(
6328 &mut cx,
6329 "aa\nb«b\ncc\ndd\neˇ»e\nff",
6330 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6331 );
6332
6333 // Whole buffer, left-to-right, last line *doesn't* end with newline
6334 test(
6335 &mut cx,
6336 "«ˇaa\nbb\ncc\ndd\nee\nff»",
6337 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6338 );
6339 // Same thing, right-to-left
6340 test(
6341 &mut cx,
6342 "«aa\nbb\ncc\ndd\nee\nffˇ»",
6343 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6344 );
6345
6346 // Whole buffer, left-to-right, last line ends with newline
6347 test(
6348 &mut cx,
6349 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
6350 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6351 );
6352 // Same thing, right-to-left
6353 test(
6354 &mut cx,
6355 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
6356 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6357 );
6358
6359 // Starts at the end of a line, ends at the start of another
6360 test(
6361 &mut cx,
6362 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
6363 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
6364 );
6365}
6366
6367#[gpui::test]
6368async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
6369 init_test(cx, |_| {});
6370
6371 let editor = cx.add_window(|window, cx| {
6372 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
6373 build_editor(buffer, window, cx)
6374 });
6375
6376 // setup
6377 _ = editor.update(cx, |editor, window, cx| {
6378 editor.fold_creases(
6379 vec![
6380 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
6381 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
6382 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
6383 ],
6384 true,
6385 window,
6386 cx,
6387 );
6388 assert_eq!(
6389 editor.display_text(cx),
6390 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6391 );
6392 });
6393
6394 _ = editor.update(cx, |editor, window, cx| {
6395 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6396 s.select_display_ranges([
6397 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6398 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6399 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6400 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
6401 ])
6402 });
6403 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6404 assert_eq!(
6405 editor.display_text(cx),
6406 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6407 );
6408 });
6409 EditorTestContext::for_editor(editor, cx)
6410 .await
6411 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
6412
6413 _ = editor.update(cx, |editor, window, cx| {
6414 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6415 s.select_display_ranges([
6416 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
6417 ])
6418 });
6419 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6420 assert_eq!(
6421 editor.display_text(cx),
6422 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
6423 );
6424 assert_eq!(
6425 editor.selections.display_ranges(cx),
6426 [
6427 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
6428 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
6429 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
6430 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
6431 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
6432 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
6433 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
6434 ]
6435 );
6436 });
6437 EditorTestContext::for_editor(editor, cx)
6438 .await
6439 .assert_editor_state(
6440 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
6441 );
6442}
6443
6444#[gpui::test]
6445async fn test_add_selection_above_below(cx: &mut TestAppContext) {
6446 init_test(cx, |_| {});
6447
6448 let mut cx = EditorTestContext::new(cx).await;
6449
6450 cx.set_state(indoc!(
6451 r#"abc
6452 defˇghi
6453
6454 jk
6455 nlmo
6456 "#
6457 ));
6458
6459 cx.update_editor(|editor, window, cx| {
6460 editor.add_selection_above(&Default::default(), window, cx);
6461 });
6462
6463 cx.assert_editor_state(indoc!(
6464 r#"abcˇ
6465 defˇghi
6466
6467 jk
6468 nlmo
6469 "#
6470 ));
6471
6472 cx.update_editor(|editor, window, cx| {
6473 editor.add_selection_above(&Default::default(), window, cx);
6474 });
6475
6476 cx.assert_editor_state(indoc!(
6477 r#"abcˇ
6478 defˇghi
6479
6480 jk
6481 nlmo
6482 "#
6483 ));
6484
6485 cx.update_editor(|editor, window, cx| {
6486 editor.add_selection_below(&Default::default(), window, cx);
6487 });
6488
6489 cx.assert_editor_state(indoc!(
6490 r#"abc
6491 defˇghi
6492
6493 jk
6494 nlmo
6495 "#
6496 ));
6497
6498 cx.update_editor(|editor, window, cx| {
6499 editor.undo_selection(&Default::default(), window, cx);
6500 });
6501
6502 cx.assert_editor_state(indoc!(
6503 r#"abcˇ
6504 defˇghi
6505
6506 jk
6507 nlmo
6508 "#
6509 ));
6510
6511 cx.update_editor(|editor, window, cx| {
6512 editor.redo_selection(&Default::default(), window, cx);
6513 });
6514
6515 cx.assert_editor_state(indoc!(
6516 r#"abc
6517 defˇghi
6518
6519 jk
6520 nlmo
6521 "#
6522 ));
6523
6524 cx.update_editor(|editor, window, cx| {
6525 editor.add_selection_below(&Default::default(), window, cx);
6526 });
6527
6528 cx.assert_editor_state(indoc!(
6529 r#"abc
6530 defˇghi
6531 ˇ
6532 jk
6533 nlmo
6534 "#
6535 ));
6536
6537 cx.update_editor(|editor, window, cx| {
6538 editor.add_selection_below(&Default::default(), window, cx);
6539 });
6540
6541 cx.assert_editor_state(indoc!(
6542 r#"abc
6543 defˇghi
6544 ˇ
6545 jkˇ
6546 nlmo
6547 "#
6548 ));
6549
6550 cx.update_editor(|editor, window, cx| {
6551 editor.add_selection_below(&Default::default(), window, cx);
6552 });
6553
6554 cx.assert_editor_state(indoc!(
6555 r#"abc
6556 defˇghi
6557 ˇ
6558 jkˇ
6559 nlmˇo
6560 "#
6561 ));
6562
6563 cx.update_editor(|editor, window, cx| {
6564 editor.add_selection_below(&Default::default(), window, cx);
6565 });
6566
6567 cx.assert_editor_state(indoc!(
6568 r#"abc
6569 defˇghi
6570 ˇ
6571 jkˇ
6572 nlmˇo
6573 ˇ"#
6574 ));
6575
6576 // change selections
6577 cx.set_state(indoc!(
6578 r#"abc
6579 def«ˇg»hi
6580
6581 jk
6582 nlmo
6583 "#
6584 ));
6585
6586 cx.update_editor(|editor, window, cx| {
6587 editor.add_selection_below(&Default::default(), window, cx);
6588 });
6589
6590 cx.assert_editor_state(indoc!(
6591 r#"abc
6592 def«ˇg»hi
6593
6594 jk
6595 nlm«ˇo»
6596 "#
6597 ));
6598
6599 cx.update_editor(|editor, window, cx| {
6600 editor.add_selection_below(&Default::default(), window, cx);
6601 });
6602
6603 cx.assert_editor_state(indoc!(
6604 r#"abc
6605 def«ˇg»hi
6606
6607 jk
6608 nlm«ˇo»
6609 "#
6610 ));
6611
6612 cx.update_editor(|editor, window, cx| {
6613 editor.add_selection_above(&Default::default(), window, cx);
6614 });
6615
6616 cx.assert_editor_state(indoc!(
6617 r#"abc
6618 def«ˇg»hi
6619
6620 jk
6621 nlmo
6622 "#
6623 ));
6624
6625 cx.update_editor(|editor, window, cx| {
6626 editor.add_selection_above(&Default::default(), window, cx);
6627 });
6628
6629 cx.assert_editor_state(indoc!(
6630 r#"abc
6631 def«ˇg»hi
6632
6633 jk
6634 nlmo
6635 "#
6636 ));
6637
6638 // Change selections again
6639 cx.set_state(indoc!(
6640 r#"a«bc
6641 defgˇ»hi
6642
6643 jk
6644 nlmo
6645 "#
6646 ));
6647
6648 cx.update_editor(|editor, window, cx| {
6649 editor.add_selection_below(&Default::default(), window, cx);
6650 });
6651
6652 cx.assert_editor_state(indoc!(
6653 r#"a«bcˇ»
6654 d«efgˇ»hi
6655
6656 j«kˇ»
6657 nlmo
6658 "#
6659 ));
6660
6661 cx.update_editor(|editor, window, cx| {
6662 editor.add_selection_below(&Default::default(), window, cx);
6663 });
6664 cx.assert_editor_state(indoc!(
6665 r#"a«bcˇ»
6666 d«efgˇ»hi
6667
6668 j«kˇ»
6669 n«lmoˇ»
6670 "#
6671 ));
6672 cx.update_editor(|editor, window, cx| {
6673 editor.add_selection_above(&Default::default(), window, cx);
6674 });
6675
6676 cx.assert_editor_state(indoc!(
6677 r#"a«bcˇ»
6678 d«efgˇ»hi
6679
6680 j«kˇ»
6681 nlmo
6682 "#
6683 ));
6684
6685 // Change selections again
6686 cx.set_state(indoc!(
6687 r#"abc
6688 d«ˇefghi
6689
6690 jk
6691 nlm»o
6692 "#
6693 ));
6694
6695 cx.update_editor(|editor, window, cx| {
6696 editor.add_selection_above(&Default::default(), window, cx);
6697 });
6698
6699 cx.assert_editor_state(indoc!(
6700 r#"a«ˇbc»
6701 d«ˇef»ghi
6702
6703 j«ˇk»
6704 n«ˇlm»o
6705 "#
6706 ));
6707
6708 cx.update_editor(|editor, window, cx| {
6709 editor.add_selection_below(&Default::default(), window, cx);
6710 });
6711
6712 cx.assert_editor_state(indoc!(
6713 r#"abc
6714 d«ˇef»ghi
6715
6716 j«ˇk»
6717 n«ˇlm»o
6718 "#
6719 ));
6720}
6721
6722#[gpui::test]
6723async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
6724 init_test(cx, |_| {});
6725 let mut cx = EditorTestContext::new(cx).await;
6726
6727 cx.set_state(indoc!(
6728 r#"line onˇe
6729 liˇne two
6730 line three
6731 line four"#
6732 ));
6733
6734 cx.update_editor(|editor, window, cx| {
6735 editor.add_selection_below(&Default::default(), window, cx);
6736 });
6737
6738 // test multiple cursors expand in the same direction
6739 cx.assert_editor_state(indoc!(
6740 r#"line onˇe
6741 liˇne twˇo
6742 liˇne three
6743 line four"#
6744 ));
6745
6746 cx.update_editor(|editor, window, cx| {
6747 editor.add_selection_below(&Default::default(), window, cx);
6748 });
6749
6750 cx.update_editor(|editor, window, cx| {
6751 editor.add_selection_below(&Default::default(), window, cx);
6752 });
6753
6754 // test multiple cursors expand below overflow
6755 cx.assert_editor_state(indoc!(
6756 r#"line onˇe
6757 liˇne twˇo
6758 liˇne thˇree
6759 liˇne foˇur"#
6760 ));
6761
6762 cx.update_editor(|editor, window, cx| {
6763 editor.add_selection_above(&Default::default(), window, cx);
6764 });
6765
6766 // test multiple cursors retrieves back correctly
6767 cx.assert_editor_state(indoc!(
6768 r#"line onˇe
6769 liˇne twˇo
6770 liˇne thˇree
6771 line four"#
6772 ));
6773
6774 cx.update_editor(|editor, window, cx| {
6775 editor.add_selection_above(&Default::default(), window, cx);
6776 });
6777
6778 cx.update_editor(|editor, window, cx| {
6779 editor.add_selection_above(&Default::default(), window, cx);
6780 });
6781
6782 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
6783 cx.assert_editor_state(indoc!(
6784 r#"liˇne onˇe
6785 liˇne two
6786 line three
6787 line four"#
6788 ));
6789
6790 cx.update_editor(|editor, window, cx| {
6791 editor.undo_selection(&Default::default(), window, cx);
6792 });
6793
6794 // test undo
6795 cx.assert_editor_state(indoc!(
6796 r#"line onˇe
6797 liˇne twˇo
6798 line three
6799 line four"#
6800 ));
6801
6802 cx.update_editor(|editor, window, cx| {
6803 editor.redo_selection(&Default::default(), window, cx);
6804 });
6805
6806 // test redo
6807 cx.assert_editor_state(indoc!(
6808 r#"liˇne onˇe
6809 liˇne two
6810 line three
6811 line four"#
6812 ));
6813
6814 cx.set_state(indoc!(
6815 r#"abcd
6816 ef«ghˇ»
6817 ijkl
6818 «mˇ»nop"#
6819 ));
6820
6821 cx.update_editor(|editor, window, cx| {
6822 editor.add_selection_above(&Default::default(), window, cx);
6823 });
6824
6825 // test multiple selections expand in the same direction
6826 cx.assert_editor_state(indoc!(
6827 r#"ab«cdˇ»
6828 ef«ghˇ»
6829 «iˇ»jkl
6830 «mˇ»nop"#
6831 ));
6832
6833 cx.update_editor(|editor, window, cx| {
6834 editor.add_selection_above(&Default::default(), window, cx);
6835 });
6836
6837 // test multiple selection upward overflow
6838 cx.assert_editor_state(indoc!(
6839 r#"ab«cdˇ»
6840 «eˇ»f«ghˇ»
6841 «iˇ»jkl
6842 «mˇ»nop"#
6843 ));
6844
6845 cx.update_editor(|editor, window, cx| {
6846 editor.add_selection_below(&Default::default(), window, cx);
6847 });
6848
6849 // test multiple selection retrieves back correctly
6850 cx.assert_editor_state(indoc!(
6851 r#"abcd
6852 ef«ghˇ»
6853 «iˇ»jkl
6854 «mˇ»nop"#
6855 ));
6856
6857 cx.update_editor(|editor, window, cx| {
6858 editor.add_selection_below(&Default::default(), window, cx);
6859 });
6860
6861 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
6862 cx.assert_editor_state(indoc!(
6863 r#"abcd
6864 ef«ghˇ»
6865 ij«klˇ»
6866 «mˇ»nop"#
6867 ));
6868
6869 cx.update_editor(|editor, window, cx| {
6870 editor.undo_selection(&Default::default(), window, cx);
6871 });
6872
6873 // test undo
6874 cx.assert_editor_state(indoc!(
6875 r#"abcd
6876 ef«ghˇ»
6877 «iˇ»jkl
6878 «mˇ»nop"#
6879 ));
6880
6881 cx.update_editor(|editor, window, cx| {
6882 editor.redo_selection(&Default::default(), window, cx);
6883 });
6884
6885 // test redo
6886 cx.assert_editor_state(indoc!(
6887 r#"abcd
6888 ef«ghˇ»
6889 ij«klˇ»
6890 «mˇ»nop"#
6891 ));
6892}
6893
6894#[gpui::test]
6895async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
6896 init_test(cx, |_| {});
6897 let mut cx = EditorTestContext::new(cx).await;
6898
6899 cx.set_state(indoc!(
6900 r#"line onˇe
6901 liˇne two
6902 line three
6903 line four"#
6904 ));
6905
6906 cx.update_editor(|editor, window, cx| {
6907 editor.add_selection_below(&Default::default(), window, cx);
6908 editor.add_selection_below(&Default::default(), window, cx);
6909 editor.add_selection_below(&Default::default(), window, cx);
6910 });
6911
6912 // initial state with two multi cursor groups
6913 cx.assert_editor_state(indoc!(
6914 r#"line onˇe
6915 liˇne twˇo
6916 liˇne thˇree
6917 liˇne foˇur"#
6918 ));
6919
6920 // add single cursor in middle - simulate opt click
6921 cx.update_editor(|editor, window, cx| {
6922 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
6923 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6924 editor.end_selection(window, cx);
6925 });
6926
6927 cx.assert_editor_state(indoc!(
6928 r#"line onˇe
6929 liˇne twˇo
6930 liˇneˇ thˇree
6931 liˇne foˇur"#
6932 ));
6933
6934 cx.update_editor(|editor, window, cx| {
6935 editor.add_selection_above(&Default::default(), window, cx);
6936 });
6937
6938 // test new added selection expands above and existing selection shrinks
6939 cx.assert_editor_state(indoc!(
6940 r#"line onˇe
6941 liˇneˇ twˇo
6942 liˇneˇ thˇree
6943 line four"#
6944 ));
6945
6946 cx.update_editor(|editor, window, cx| {
6947 editor.add_selection_above(&Default::default(), window, cx);
6948 });
6949
6950 // test new added selection expands above and existing selection shrinks
6951 cx.assert_editor_state(indoc!(
6952 r#"lineˇ onˇe
6953 liˇneˇ twˇo
6954 lineˇ three
6955 line four"#
6956 ));
6957
6958 // intial state with two selection groups
6959 cx.set_state(indoc!(
6960 r#"abcd
6961 ef«ghˇ»
6962 ijkl
6963 «mˇ»nop"#
6964 ));
6965
6966 cx.update_editor(|editor, window, cx| {
6967 editor.add_selection_above(&Default::default(), window, cx);
6968 editor.add_selection_above(&Default::default(), window, cx);
6969 });
6970
6971 cx.assert_editor_state(indoc!(
6972 r#"ab«cdˇ»
6973 «eˇ»f«ghˇ»
6974 «iˇ»jkl
6975 «mˇ»nop"#
6976 ));
6977
6978 // add single selection in middle - simulate opt drag
6979 cx.update_editor(|editor, window, cx| {
6980 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
6981 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6982 editor.update_selection(
6983 DisplayPoint::new(DisplayRow(2), 4),
6984 0,
6985 gpui::Point::<f32>::default(),
6986 window,
6987 cx,
6988 );
6989 editor.end_selection(window, cx);
6990 });
6991
6992 cx.assert_editor_state(indoc!(
6993 r#"ab«cdˇ»
6994 «eˇ»f«ghˇ»
6995 «iˇ»jk«lˇ»
6996 «mˇ»nop"#
6997 ));
6998
6999 cx.update_editor(|editor, window, cx| {
7000 editor.add_selection_below(&Default::default(), window, cx);
7001 });
7002
7003 // test new added selection expands below, others shrinks from above
7004 cx.assert_editor_state(indoc!(
7005 r#"abcd
7006 ef«ghˇ»
7007 «iˇ»jk«lˇ»
7008 «mˇ»no«pˇ»"#
7009 ));
7010}
7011
7012#[gpui::test]
7013async fn test_select_next(cx: &mut TestAppContext) {
7014 init_test(cx, |_| {});
7015
7016 let mut cx = EditorTestContext::new(cx).await;
7017 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7018
7019 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7020 .unwrap();
7021 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7022
7023 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7024 .unwrap();
7025 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
7026
7027 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7028 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7029
7030 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7031 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
7032
7033 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7034 .unwrap();
7035 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7036
7037 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7038 .unwrap();
7039 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7040
7041 // Test selection direction should be preserved
7042 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7043
7044 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7045 .unwrap();
7046 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
7047}
7048
7049#[gpui::test]
7050async fn test_select_all_matches(cx: &mut TestAppContext) {
7051 init_test(cx, |_| {});
7052
7053 let mut cx = EditorTestContext::new(cx).await;
7054
7055 // Test caret-only selections
7056 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7057 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7058 .unwrap();
7059 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7060
7061 // Test left-to-right selections
7062 cx.set_state("abc\n«abcˇ»\nabc");
7063 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7064 .unwrap();
7065 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
7066
7067 // Test right-to-left selections
7068 cx.set_state("abc\n«ˇabc»\nabc");
7069 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7070 .unwrap();
7071 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
7072
7073 // Test selecting whitespace with caret selection
7074 cx.set_state("abc\nˇ abc\nabc");
7075 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7076 .unwrap();
7077 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
7078
7079 // Test selecting whitespace with left-to-right selection
7080 cx.set_state("abc\n«ˇ »abc\nabc");
7081 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7082 .unwrap();
7083 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
7084
7085 // Test no matches with right-to-left selection
7086 cx.set_state("abc\n« ˇ»abc\nabc");
7087 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7088 .unwrap();
7089 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
7090
7091 // Test with a single word and clip_at_line_ends=true (#29823)
7092 cx.set_state("aˇbc");
7093 cx.update_editor(|e, window, cx| {
7094 e.set_clip_at_line_ends(true, cx);
7095 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
7096 e.set_clip_at_line_ends(false, cx);
7097 });
7098 cx.assert_editor_state("«abcˇ»");
7099}
7100
7101#[gpui::test]
7102async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
7103 init_test(cx, |_| {});
7104
7105 let mut cx = EditorTestContext::new(cx).await;
7106
7107 let large_body_1 = "\nd".repeat(200);
7108 let large_body_2 = "\ne".repeat(200);
7109
7110 cx.set_state(&format!(
7111 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
7112 ));
7113 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
7114 let scroll_position = editor.scroll_position(cx);
7115 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
7116 scroll_position
7117 });
7118
7119 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7120 .unwrap();
7121 cx.assert_editor_state(&format!(
7122 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
7123 ));
7124 let scroll_position_after_selection =
7125 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
7126 assert_eq!(
7127 initial_scroll_position, scroll_position_after_selection,
7128 "Scroll position should not change after selecting all matches"
7129 );
7130}
7131
7132#[gpui::test]
7133async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
7134 init_test(cx, |_| {});
7135
7136 let mut cx = EditorLspTestContext::new_rust(
7137 lsp::ServerCapabilities {
7138 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7139 ..Default::default()
7140 },
7141 cx,
7142 )
7143 .await;
7144
7145 cx.set_state(indoc! {"
7146 line 1
7147 line 2
7148 linˇe 3
7149 line 4
7150 line 5
7151 "});
7152
7153 // Make an edit
7154 cx.update_editor(|editor, window, cx| {
7155 editor.handle_input("X", window, cx);
7156 });
7157
7158 // Move cursor to a different position
7159 cx.update_editor(|editor, window, cx| {
7160 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7161 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
7162 });
7163 });
7164
7165 cx.assert_editor_state(indoc! {"
7166 line 1
7167 line 2
7168 linXe 3
7169 line 4
7170 liˇne 5
7171 "});
7172
7173 cx.lsp
7174 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
7175 Ok(Some(vec![lsp::TextEdit::new(
7176 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
7177 "PREFIX ".to_string(),
7178 )]))
7179 });
7180
7181 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
7182 .unwrap()
7183 .await
7184 .unwrap();
7185
7186 cx.assert_editor_state(indoc! {"
7187 PREFIX line 1
7188 line 2
7189 linXe 3
7190 line 4
7191 liˇne 5
7192 "});
7193
7194 // Undo formatting
7195 cx.update_editor(|editor, window, cx| {
7196 editor.undo(&Default::default(), window, cx);
7197 });
7198
7199 // Verify cursor moved back to position after edit
7200 cx.assert_editor_state(indoc! {"
7201 line 1
7202 line 2
7203 linXˇe 3
7204 line 4
7205 line 5
7206 "});
7207}
7208
7209#[gpui::test]
7210async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext) {
7211 init_test(cx, |_| {});
7212
7213 let mut cx = EditorTestContext::new(cx).await;
7214
7215 let provider = cx.new(|_| FakeInlineCompletionProvider::default());
7216 cx.update_editor(|editor, window, cx| {
7217 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
7218 });
7219
7220 cx.set_state(indoc! {"
7221 line 1
7222 line 2
7223 linˇe 3
7224 line 4
7225 line 5
7226 line 6
7227 line 7
7228 line 8
7229 line 9
7230 line 10
7231 "});
7232
7233 let snapshot = cx.buffer_snapshot();
7234 let edit_position = snapshot.anchor_after(Point::new(2, 4));
7235
7236 cx.update(|_, cx| {
7237 provider.update(cx, |provider, _| {
7238 provider.set_inline_completion(Some(inline_completion::InlineCompletion {
7239 id: None,
7240 edits: vec![(edit_position..edit_position, "X".into())],
7241 edit_preview: None,
7242 }))
7243 })
7244 });
7245
7246 cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx));
7247 cx.update_editor(|editor, window, cx| {
7248 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
7249 });
7250
7251 cx.assert_editor_state(indoc! {"
7252 line 1
7253 line 2
7254 lineXˇ 3
7255 line 4
7256 line 5
7257 line 6
7258 line 7
7259 line 8
7260 line 9
7261 line 10
7262 "});
7263
7264 cx.update_editor(|editor, window, cx| {
7265 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7266 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
7267 });
7268 });
7269
7270 cx.assert_editor_state(indoc! {"
7271 line 1
7272 line 2
7273 lineX 3
7274 line 4
7275 line 5
7276 line 6
7277 line 7
7278 line 8
7279 line 9
7280 liˇne 10
7281 "});
7282
7283 cx.update_editor(|editor, window, cx| {
7284 editor.undo(&Default::default(), window, cx);
7285 });
7286
7287 cx.assert_editor_state(indoc! {"
7288 line 1
7289 line 2
7290 lineˇ 3
7291 line 4
7292 line 5
7293 line 6
7294 line 7
7295 line 8
7296 line 9
7297 line 10
7298 "});
7299}
7300
7301#[gpui::test]
7302async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
7303 init_test(cx, |_| {});
7304
7305 let mut cx = EditorTestContext::new(cx).await;
7306 cx.set_state(
7307 r#"let foo = 2;
7308lˇet foo = 2;
7309let fooˇ = 2;
7310let foo = 2;
7311let foo = ˇ2;"#,
7312 );
7313
7314 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7315 .unwrap();
7316 cx.assert_editor_state(
7317 r#"let foo = 2;
7318«letˇ» foo = 2;
7319let «fooˇ» = 2;
7320let foo = 2;
7321let foo = «2ˇ»;"#,
7322 );
7323
7324 // noop for multiple selections with different contents
7325 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7326 .unwrap();
7327 cx.assert_editor_state(
7328 r#"let foo = 2;
7329«letˇ» foo = 2;
7330let «fooˇ» = 2;
7331let foo = 2;
7332let foo = «2ˇ»;"#,
7333 );
7334
7335 // Test last selection direction should be preserved
7336 cx.set_state(
7337 r#"let foo = 2;
7338let foo = 2;
7339let «fooˇ» = 2;
7340let «ˇfoo» = 2;
7341let foo = 2;"#,
7342 );
7343
7344 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7345 .unwrap();
7346 cx.assert_editor_state(
7347 r#"let foo = 2;
7348let foo = 2;
7349let «fooˇ» = 2;
7350let «ˇfoo» = 2;
7351let «ˇfoo» = 2;"#,
7352 );
7353}
7354
7355#[gpui::test]
7356async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
7357 init_test(cx, |_| {});
7358
7359 let mut cx =
7360 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
7361
7362 cx.assert_editor_state(indoc! {"
7363 ˇbbb
7364 ccc
7365
7366 bbb
7367 ccc
7368 "});
7369 cx.dispatch_action(SelectPrevious::default());
7370 cx.assert_editor_state(indoc! {"
7371 «bbbˇ»
7372 ccc
7373
7374 bbb
7375 ccc
7376 "});
7377 cx.dispatch_action(SelectPrevious::default());
7378 cx.assert_editor_state(indoc! {"
7379 «bbbˇ»
7380 ccc
7381
7382 «bbbˇ»
7383 ccc
7384 "});
7385}
7386
7387#[gpui::test]
7388async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
7389 init_test(cx, |_| {});
7390
7391 let mut cx = EditorTestContext::new(cx).await;
7392 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7393
7394 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7395 .unwrap();
7396 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7397
7398 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7399 .unwrap();
7400 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7401
7402 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7403 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7404
7405 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7406 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7407
7408 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7409 .unwrap();
7410 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
7411
7412 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7413 .unwrap();
7414 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7415}
7416
7417#[gpui::test]
7418async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
7419 init_test(cx, |_| {});
7420
7421 let mut cx = EditorTestContext::new(cx).await;
7422 cx.set_state("aˇ");
7423
7424 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7425 .unwrap();
7426 cx.assert_editor_state("«aˇ»");
7427 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7428 .unwrap();
7429 cx.assert_editor_state("«aˇ»");
7430}
7431
7432#[gpui::test]
7433async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
7434 init_test(cx, |_| {});
7435
7436 let mut cx = EditorTestContext::new(cx).await;
7437 cx.set_state(
7438 r#"let foo = 2;
7439lˇet foo = 2;
7440let fooˇ = 2;
7441let foo = 2;
7442let foo = ˇ2;"#,
7443 );
7444
7445 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7446 .unwrap();
7447 cx.assert_editor_state(
7448 r#"let foo = 2;
7449«letˇ» foo = 2;
7450let «fooˇ» = 2;
7451let foo = 2;
7452let foo = «2ˇ»;"#,
7453 );
7454
7455 // noop for multiple selections with different contents
7456 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7457 .unwrap();
7458 cx.assert_editor_state(
7459 r#"let foo = 2;
7460«letˇ» foo = 2;
7461let «fooˇ» = 2;
7462let foo = 2;
7463let foo = «2ˇ»;"#,
7464 );
7465}
7466
7467#[gpui::test]
7468async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
7469 init_test(cx, |_| {});
7470
7471 let mut cx = EditorTestContext::new(cx).await;
7472 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7473
7474 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7475 .unwrap();
7476 // selection direction is preserved
7477 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7478
7479 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7480 .unwrap();
7481 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7482
7483 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7484 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7485
7486 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7487 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7488
7489 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7490 .unwrap();
7491 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
7492
7493 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7494 .unwrap();
7495 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
7496}
7497
7498#[gpui::test]
7499async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
7500 init_test(cx, |_| {});
7501
7502 let language = Arc::new(Language::new(
7503 LanguageConfig::default(),
7504 Some(tree_sitter_rust::LANGUAGE.into()),
7505 ));
7506
7507 let text = r#"
7508 use mod1::mod2::{mod3, mod4};
7509
7510 fn fn_1(param1: bool, param2: &str) {
7511 let var1 = "text";
7512 }
7513 "#
7514 .unindent();
7515
7516 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7517 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7518 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7519
7520 editor
7521 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7522 .await;
7523
7524 editor.update_in(cx, |editor, window, cx| {
7525 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7526 s.select_display_ranges([
7527 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
7528 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
7529 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
7530 ]);
7531 });
7532 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7533 });
7534 editor.update(cx, |editor, cx| {
7535 assert_text_with_selections(
7536 editor,
7537 indoc! {r#"
7538 use mod1::mod2::{mod3, «mod4ˇ»};
7539
7540 fn fn_1«ˇ(param1: bool, param2: &str)» {
7541 let var1 = "«ˇtext»";
7542 }
7543 "#},
7544 cx,
7545 );
7546 });
7547
7548 editor.update_in(cx, |editor, window, cx| {
7549 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7550 });
7551 editor.update(cx, |editor, cx| {
7552 assert_text_with_selections(
7553 editor,
7554 indoc! {r#"
7555 use mod1::mod2::«{mod3, mod4}ˇ»;
7556
7557 «ˇfn fn_1(param1: bool, param2: &str) {
7558 let var1 = "text";
7559 }»
7560 "#},
7561 cx,
7562 );
7563 });
7564
7565 editor.update_in(cx, |editor, window, cx| {
7566 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7567 });
7568 assert_eq!(
7569 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7570 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7571 );
7572
7573 // Trying to expand the selected syntax node one more time has no effect.
7574 editor.update_in(cx, |editor, window, cx| {
7575 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7576 });
7577 assert_eq!(
7578 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7579 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7580 );
7581
7582 editor.update_in(cx, |editor, window, cx| {
7583 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7584 });
7585 editor.update(cx, |editor, cx| {
7586 assert_text_with_selections(
7587 editor,
7588 indoc! {r#"
7589 use mod1::mod2::«{mod3, mod4}ˇ»;
7590
7591 «ˇfn fn_1(param1: bool, param2: &str) {
7592 let var1 = "text";
7593 }»
7594 "#},
7595 cx,
7596 );
7597 });
7598
7599 editor.update_in(cx, |editor, window, cx| {
7600 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7601 });
7602 editor.update(cx, |editor, cx| {
7603 assert_text_with_selections(
7604 editor,
7605 indoc! {r#"
7606 use mod1::mod2::{mod3, «mod4ˇ»};
7607
7608 fn fn_1«ˇ(param1: bool, param2: &str)» {
7609 let var1 = "«ˇtext»";
7610 }
7611 "#},
7612 cx,
7613 );
7614 });
7615
7616 editor.update_in(cx, |editor, window, cx| {
7617 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7618 });
7619 editor.update(cx, |editor, cx| {
7620 assert_text_with_selections(
7621 editor,
7622 indoc! {r#"
7623 use mod1::mod2::{mod3, mo«ˇ»d4};
7624
7625 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7626 let var1 = "te«ˇ»xt";
7627 }
7628 "#},
7629 cx,
7630 );
7631 });
7632
7633 // Trying to shrink the selected syntax node one more time has no effect.
7634 editor.update_in(cx, |editor, window, cx| {
7635 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7636 });
7637 editor.update_in(cx, |editor, _, cx| {
7638 assert_text_with_selections(
7639 editor,
7640 indoc! {r#"
7641 use mod1::mod2::{mod3, mo«ˇ»d4};
7642
7643 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7644 let var1 = "te«ˇ»xt";
7645 }
7646 "#},
7647 cx,
7648 );
7649 });
7650
7651 // Ensure that we keep expanding the selection if the larger selection starts or ends within
7652 // a fold.
7653 editor.update_in(cx, |editor, window, cx| {
7654 editor.fold_creases(
7655 vec![
7656 Crease::simple(
7657 Point::new(0, 21)..Point::new(0, 24),
7658 FoldPlaceholder::test(),
7659 ),
7660 Crease::simple(
7661 Point::new(3, 20)..Point::new(3, 22),
7662 FoldPlaceholder::test(),
7663 ),
7664 ],
7665 true,
7666 window,
7667 cx,
7668 );
7669 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7670 });
7671 editor.update(cx, |editor, cx| {
7672 assert_text_with_selections(
7673 editor,
7674 indoc! {r#"
7675 use mod1::mod2::«{mod3, mod4}ˇ»;
7676
7677 fn fn_1«ˇ(param1: bool, param2: &str)» {
7678 let var1 = "«ˇtext»";
7679 }
7680 "#},
7681 cx,
7682 );
7683 });
7684}
7685
7686#[gpui::test]
7687async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
7688 init_test(cx, |_| {});
7689
7690 let language = Arc::new(Language::new(
7691 LanguageConfig::default(),
7692 Some(tree_sitter_rust::LANGUAGE.into()),
7693 ));
7694
7695 let text = "let a = 2;";
7696
7697 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7698 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7699 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7700
7701 editor
7702 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7703 .await;
7704
7705 // Test case 1: Cursor at end of word
7706 editor.update_in(cx, |editor, window, cx| {
7707 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7708 s.select_display_ranges([
7709 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
7710 ]);
7711 });
7712 });
7713 editor.update(cx, |editor, cx| {
7714 assert_text_with_selections(editor, "let aˇ = 2;", cx);
7715 });
7716 editor.update_in(cx, |editor, window, cx| {
7717 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7718 });
7719 editor.update(cx, |editor, cx| {
7720 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
7721 });
7722 editor.update_in(cx, |editor, window, cx| {
7723 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7724 });
7725 editor.update(cx, |editor, cx| {
7726 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7727 });
7728
7729 // Test case 2: Cursor at end of statement
7730 editor.update_in(cx, |editor, window, cx| {
7731 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7732 s.select_display_ranges([
7733 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
7734 ]);
7735 });
7736 });
7737 editor.update(cx, |editor, cx| {
7738 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
7739 });
7740 editor.update_in(cx, |editor, window, cx| {
7741 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7742 });
7743 editor.update(cx, |editor, cx| {
7744 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7745 });
7746}
7747
7748#[gpui::test]
7749async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
7750 init_test(cx, |_| {});
7751
7752 let language = Arc::new(Language::new(
7753 LanguageConfig::default(),
7754 Some(tree_sitter_rust::LANGUAGE.into()),
7755 ));
7756
7757 let text = r#"
7758 use mod1::mod2::{mod3, mod4};
7759
7760 fn fn_1(param1: bool, param2: &str) {
7761 let var1 = "hello world";
7762 }
7763 "#
7764 .unindent();
7765
7766 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7767 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7768 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7769
7770 editor
7771 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7772 .await;
7773
7774 // Test 1: Cursor on a letter of a string word
7775 editor.update_in(cx, |editor, window, cx| {
7776 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7777 s.select_display_ranges([
7778 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
7779 ]);
7780 });
7781 });
7782 editor.update_in(cx, |editor, window, cx| {
7783 assert_text_with_selections(
7784 editor,
7785 indoc! {r#"
7786 use mod1::mod2::{mod3, mod4};
7787
7788 fn fn_1(param1: bool, param2: &str) {
7789 let var1 = "hˇello world";
7790 }
7791 "#},
7792 cx,
7793 );
7794 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7795 assert_text_with_selections(
7796 editor,
7797 indoc! {r#"
7798 use mod1::mod2::{mod3, mod4};
7799
7800 fn fn_1(param1: bool, param2: &str) {
7801 let var1 = "«ˇhello» world";
7802 }
7803 "#},
7804 cx,
7805 );
7806 });
7807
7808 // Test 2: Partial selection within a word
7809 editor.update_in(cx, |editor, window, cx| {
7810 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7811 s.select_display_ranges([
7812 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
7813 ]);
7814 });
7815 });
7816 editor.update_in(cx, |editor, window, cx| {
7817 assert_text_with_selections(
7818 editor,
7819 indoc! {r#"
7820 use mod1::mod2::{mod3, mod4};
7821
7822 fn fn_1(param1: bool, param2: &str) {
7823 let var1 = "h«elˇ»lo world";
7824 }
7825 "#},
7826 cx,
7827 );
7828 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7829 assert_text_with_selections(
7830 editor,
7831 indoc! {r#"
7832 use mod1::mod2::{mod3, mod4};
7833
7834 fn fn_1(param1: bool, param2: &str) {
7835 let var1 = "«ˇhello» world";
7836 }
7837 "#},
7838 cx,
7839 );
7840 });
7841
7842 // Test 3: Complete word already selected
7843 editor.update_in(cx, |editor, window, cx| {
7844 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7845 s.select_display_ranges([
7846 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
7847 ]);
7848 });
7849 });
7850 editor.update_in(cx, |editor, window, cx| {
7851 assert_text_with_selections(
7852 editor,
7853 indoc! {r#"
7854 use mod1::mod2::{mod3, mod4};
7855
7856 fn fn_1(param1: bool, param2: &str) {
7857 let var1 = "«helloˇ» world";
7858 }
7859 "#},
7860 cx,
7861 );
7862 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7863 assert_text_with_selections(
7864 editor,
7865 indoc! {r#"
7866 use mod1::mod2::{mod3, mod4};
7867
7868 fn fn_1(param1: bool, param2: &str) {
7869 let var1 = "«hello worldˇ»";
7870 }
7871 "#},
7872 cx,
7873 );
7874 });
7875
7876 // Test 4: Selection spanning across words
7877 editor.update_in(cx, |editor, window, cx| {
7878 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7879 s.select_display_ranges([
7880 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
7881 ]);
7882 });
7883 });
7884 editor.update_in(cx, |editor, window, cx| {
7885 assert_text_with_selections(
7886 editor,
7887 indoc! {r#"
7888 use mod1::mod2::{mod3, mod4};
7889
7890 fn fn_1(param1: bool, param2: &str) {
7891 let var1 = "hel«lo woˇ»rld";
7892 }
7893 "#},
7894 cx,
7895 );
7896 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7897 assert_text_with_selections(
7898 editor,
7899 indoc! {r#"
7900 use mod1::mod2::{mod3, mod4};
7901
7902 fn fn_1(param1: bool, param2: &str) {
7903 let var1 = "«ˇhello world»";
7904 }
7905 "#},
7906 cx,
7907 );
7908 });
7909
7910 // Test 5: Expansion beyond string
7911 editor.update_in(cx, |editor, window, cx| {
7912 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7913 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7914 assert_text_with_selections(
7915 editor,
7916 indoc! {r#"
7917 use mod1::mod2::{mod3, mod4};
7918
7919 fn fn_1(param1: bool, param2: &str) {
7920 «ˇlet var1 = "hello world";»
7921 }
7922 "#},
7923 cx,
7924 );
7925 });
7926}
7927
7928#[gpui::test]
7929async fn test_fold_function_bodies(cx: &mut TestAppContext) {
7930 init_test(cx, |_| {});
7931
7932 let base_text = r#"
7933 impl A {
7934 // this is an uncommitted comment
7935
7936 fn b() {
7937 c();
7938 }
7939
7940 // this is another uncommitted comment
7941
7942 fn d() {
7943 // e
7944 // f
7945 }
7946 }
7947
7948 fn g() {
7949 // h
7950 }
7951 "#
7952 .unindent();
7953
7954 let text = r#"
7955 ˇimpl A {
7956
7957 fn b() {
7958 c();
7959 }
7960
7961 fn d() {
7962 // e
7963 // f
7964 }
7965 }
7966
7967 fn g() {
7968 // h
7969 }
7970 "#
7971 .unindent();
7972
7973 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7974 cx.set_state(&text);
7975 cx.set_head_text(&base_text);
7976 cx.update_editor(|editor, window, cx| {
7977 editor.expand_all_diff_hunks(&Default::default(), window, cx);
7978 });
7979
7980 cx.assert_state_with_diff(
7981 "
7982 ˇimpl A {
7983 - // this is an uncommitted comment
7984
7985 fn b() {
7986 c();
7987 }
7988
7989 - // this is another uncommitted comment
7990 -
7991 fn d() {
7992 // e
7993 // f
7994 }
7995 }
7996
7997 fn g() {
7998 // h
7999 }
8000 "
8001 .unindent(),
8002 );
8003
8004 let expected_display_text = "
8005 impl A {
8006 // this is an uncommitted comment
8007
8008 fn b() {
8009 ⋯
8010 }
8011
8012 // this is another uncommitted comment
8013
8014 fn d() {
8015 ⋯
8016 }
8017 }
8018
8019 fn g() {
8020 ⋯
8021 }
8022 "
8023 .unindent();
8024
8025 cx.update_editor(|editor, window, cx| {
8026 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
8027 assert_eq!(editor.display_text(cx), expected_display_text);
8028 });
8029}
8030
8031#[gpui::test]
8032async fn test_autoindent(cx: &mut TestAppContext) {
8033 init_test(cx, |_| {});
8034
8035 let language = Arc::new(
8036 Language::new(
8037 LanguageConfig {
8038 brackets: BracketPairConfig {
8039 pairs: vec![
8040 BracketPair {
8041 start: "{".to_string(),
8042 end: "}".to_string(),
8043 close: false,
8044 surround: false,
8045 newline: true,
8046 },
8047 BracketPair {
8048 start: "(".to_string(),
8049 end: ")".to_string(),
8050 close: false,
8051 surround: false,
8052 newline: true,
8053 },
8054 ],
8055 ..Default::default()
8056 },
8057 ..Default::default()
8058 },
8059 Some(tree_sitter_rust::LANGUAGE.into()),
8060 )
8061 .with_indents_query(
8062 r#"
8063 (_ "(" ")" @end) @indent
8064 (_ "{" "}" @end) @indent
8065 "#,
8066 )
8067 .unwrap(),
8068 );
8069
8070 let text = "fn a() {}";
8071
8072 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8073 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8074 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8075 editor
8076 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8077 .await;
8078
8079 editor.update_in(cx, |editor, window, cx| {
8080 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8081 s.select_ranges([5..5, 8..8, 9..9])
8082 });
8083 editor.newline(&Newline, window, cx);
8084 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
8085 assert_eq!(
8086 editor.selections.ranges(cx),
8087 &[
8088 Point::new(1, 4)..Point::new(1, 4),
8089 Point::new(3, 4)..Point::new(3, 4),
8090 Point::new(5, 0)..Point::new(5, 0)
8091 ]
8092 );
8093 });
8094}
8095
8096#[gpui::test]
8097async fn test_autoindent_selections(cx: &mut TestAppContext) {
8098 init_test(cx, |_| {});
8099
8100 {
8101 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
8102 cx.set_state(indoc! {"
8103 impl A {
8104
8105 fn b() {}
8106
8107 «fn c() {
8108
8109 }ˇ»
8110 }
8111 "});
8112
8113 cx.update_editor(|editor, window, cx| {
8114 editor.autoindent(&Default::default(), window, cx);
8115 });
8116
8117 cx.assert_editor_state(indoc! {"
8118 impl A {
8119
8120 fn b() {}
8121
8122 «fn c() {
8123
8124 }ˇ»
8125 }
8126 "});
8127 }
8128
8129 {
8130 let mut cx = EditorTestContext::new_multibuffer(
8131 cx,
8132 [indoc! { "
8133 impl A {
8134 «
8135 // a
8136 fn b(){}
8137 »
8138 «
8139 }
8140 fn c(){}
8141 »
8142 "}],
8143 );
8144
8145 let buffer = cx.update_editor(|editor, _, cx| {
8146 let buffer = editor.buffer().update(cx, |buffer, _| {
8147 buffer.all_buffers().iter().next().unwrap().clone()
8148 });
8149 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
8150 buffer
8151 });
8152
8153 cx.run_until_parked();
8154 cx.update_editor(|editor, window, cx| {
8155 editor.select_all(&Default::default(), window, cx);
8156 editor.autoindent(&Default::default(), window, cx)
8157 });
8158 cx.run_until_parked();
8159
8160 cx.update(|_, cx| {
8161 assert_eq!(
8162 buffer.read(cx).text(),
8163 indoc! { "
8164 impl A {
8165
8166 // a
8167 fn b(){}
8168
8169
8170 }
8171 fn c(){}
8172
8173 " }
8174 )
8175 });
8176 }
8177}
8178
8179#[gpui::test]
8180async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
8181 init_test(cx, |_| {});
8182
8183 let mut cx = EditorTestContext::new(cx).await;
8184
8185 let language = Arc::new(Language::new(
8186 LanguageConfig {
8187 brackets: BracketPairConfig {
8188 pairs: vec![
8189 BracketPair {
8190 start: "{".to_string(),
8191 end: "}".to_string(),
8192 close: true,
8193 surround: true,
8194 newline: true,
8195 },
8196 BracketPair {
8197 start: "(".to_string(),
8198 end: ")".to_string(),
8199 close: true,
8200 surround: true,
8201 newline: true,
8202 },
8203 BracketPair {
8204 start: "/*".to_string(),
8205 end: " */".to_string(),
8206 close: true,
8207 surround: true,
8208 newline: true,
8209 },
8210 BracketPair {
8211 start: "[".to_string(),
8212 end: "]".to_string(),
8213 close: false,
8214 surround: false,
8215 newline: true,
8216 },
8217 BracketPair {
8218 start: "\"".to_string(),
8219 end: "\"".to_string(),
8220 close: true,
8221 surround: true,
8222 newline: false,
8223 },
8224 BracketPair {
8225 start: "<".to_string(),
8226 end: ">".to_string(),
8227 close: false,
8228 surround: true,
8229 newline: true,
8230 },
8231 ],
8232 ..Default::default()
8233 },
8234 autoclose_before: "})]".to_string(),
8235 ..Default::default()
8236 },
8237 Some(tree_sitter_rust::LANGUAGE.into()),
8238 ));
8239
8240 cx.language_registry().add(language.clone());
8241 cx.update_buffer(|buffer, cx| {
8242 buffer.set_language(Some(language), cx);
8243 });
8244
8245 cx.set_state(
8246 &r#"
8247 🏀ˇ
8248 εˇ
8249 ❤️ˇ
8250 "#
8251 .unindent(),
8252 );
8253
8254 // autoclose multiple nested brackets at multiple cursors
8255 cx.update_editor(|editor, window, cx| {
8256 editor.handle_input("{", window, cx);
8257 editor.handle_input("{", window, cx);
8258 editor.handle_input("{", window, cx);
8259 });
8260 cx.assert_editor_state(
8261 &"
8262 🏀{{{ˇ}}}
8263 ε{{{ˇ}}}
8264 ❤️{{{ˇ}}}
8265 "
8266 .unindent(),
8267 );
8268
8269 // insert a different closing bracket
8270 cx.update_editor(|editor, window, cx| {
8271 editor.handle_input(")", window, cx);
8272 });
8273 cx.assert_editor_state(
8274 &"
8275 🏀{{{)ˇ}}}
8276 ε{{{)ˇ}}}
8277 ❤️{{{)ˇ}}}
8278 "
8279 .unindent(),
8280 );
8281
8282 // skip over the auto-closed brackets when typing a closing bracket
8283 cx.update_editor(|editor, window, cx| {
8284 editor.move_right(&MoveRight, window, cx);
8285 editor.handle_input("}", window, cx);
8286 editor.handle_input("}", window, cx);
8287 editor.handle_input("}", window, cx);
8288 });
8289 cx.assert_editor_state(
8290 &"
8291 🏀{{{)}}}}ˇ
8292 ε{{{)}}}}ˇ
8293 ❤️{{{)}}}}ˇ
8294 "
8295 .unindent(),
8296 );
8297
8298 // autoclose multi-character pairs
8299 cx.set_state(
8300 &"
8301 ˇ
8302 ˇ
8303 "
8304 .unindent(),
8305 );
8306 cx.update_editor(|editor, window, cx| {
8307 editor.handle_input("/", window, cx);
8308 editor.handle_input("*", window, cx);
8309 });
8310 cx.assert_editor_state(
8311 &"
8312 /*ˇ */
8313 /*ˇ */
8314 "
8315 .unindent(),
8316 );
8317
8318 // one cursor autocloses a multi-character pair, one cursor
8319 // does not autoclose.
8320 cx.set_state(
8321 &"
8322 /ˇ
8323 ˇ
8324 "
8325 .unindent(),
8326 );
8327 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
8328 cx.assert_editor_state(
8329 &"
8330 /*ˇ */
8331 *ˇ
8332 "
8333 .unindent(),
8334 );
8335
8336 // Don't autoclose if the next character isn't whitespace and isn't
8337 // listed in the language's "autoclose_before" section.
8338 cx.set_state("ˇa b");
8339 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8340 cx.assert_editor_state("{ˇa b");
8341
8342 // Don't autoclose if `close` is false for the bracket pair
8343 cx.set_state("ˇ");
8344 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
8345 cx.assert_editor_state("[ˇ");
8346
8347 // Surround with brackets if text is selected
8348 cx.set_state("«aˇ» b");
8349 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8350 cx.assert_editor_state("{«aˇ»} b");
8351
8352 // Autoclose when not immediately after a word character
8353 cx.set_state("a ˇ");
8354 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8355 cx.assert_editor_state("a \"ˇ\"");
8356
8357 // Autoclose pair where the start and end characters are the same
8358 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8359 cx.assert_editor_state("a \"\"ˇ");
8360
8361 // Don't autoclose when immediately after a word character
8362 cx.set_state("aˇ");
8363 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8364 cx.assert_editor_state("a\"ˇ");
8365
8366 // Do autoclose when after a non-word character
8367 cx.set_state("{ˇ");
8368 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8369 cx.assert_editor_state("{\"ˇ\"");
8370
8371 // Non identical pairs autoclose regardless of preceding character
8372 cx.set_state("aˇ");
8373 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8374 cx.assert_editor_state("a{ˇ}");
8375
8376 // Don't autoclose pair if autoclose is disabled
8377 cx.set_state("ˇ");
8378 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8379 cx.assert_editor_state("<ˇ");
8380
8381 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
8382 cx.set_state("«aˇ» b");
8383 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8384 cx.assert_editor_state("<«aˇ»> b");
8385}
8386
8387#[gpui::test]
8388async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
8389 init_test(cx, |settings| {
8390 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8391 });
8392
8393 let mut cx = EditorTestContext::new(cx).await;
8394
8395 let language = Arc::new(Language::new(
8396 LanguageConfig {
8397 brackets: BracketPairConfig {
8398 pairs: vec![
8399 BracketPair {
8400 start: "{".to_string(),
8401 end: "}".to_string(),
8402 close: true,
8403 surround: true,
8404 newline: true,
8405 },
8406 BracketPair {
8407 start: "(".to_string(),
8408 end: ")".to_string(),
8409 close: true,
8410 surround: true,
8411 newline: true,
8412 },
8413 BracketPair {
8414 start: "[".to_string(),
8415 end: "]".to_string(),
8416 close: false,
8417 surround: false,
8418 newline: true,
8419 },
8420 ],
8421 ..Default::default()
8422 },
8423 autoclose_before: "})]".to_string(),
8424 ..Default::default()
8425 },
8426 Some(tree_sitter_rust::LANGUAGE.into()),
8427 ));
8428
8429 cx.language_registry().add(language.clone());
8430 cx.update_buffer(|buffer, cx| {
8431 buffer.set_language(Some(language), cx);
8432 });
8433
8434 cx.set_state(
8435 &"
8436 ˇ
8437 ˇ
8438 ˇ
8439 "
8440 .unindent(),
8441 );
8442
8443 // ensure only matching closing brackets are skipped over
8444 cx.update_editor(|editor, window, cx| {
8445 editor.handle_input("}", window, cx);
8446 editor.move_left(&MoveLeft, window, cx);
8447 editor.handle_input(")", window, cx);
8448 editor.move_left(&MoveLeft, window, cx);
8449 });
8450 cx.assert_editor_state(
8451 &"
8452 ˇ)}
8453 ˇ)}
8454 ˇ)}
8455 "
8456 .unindent(),
8457 );
8458
8459 // skip-over closing brackets at multiple cursors
8460 cx.update_editor(|editor, window, cx| {
8461 editor.handle_input(")", window, cx);
8462 editor.handle_input("}", window, cx);
8463 });
8464 cx.assert_editor_state(
8465 &"
8466 )}ˇ
8467 )}ˇ
8468 )}ˇ
8469 "
8470 .unindent(),
8471 );
8472
8473 // ignore non-close brackets
8474 cx.update_editor(|editor, window, cx| {
8475 editor.handle_input("]", window, cx);
8476 editor.move_left(&MoveLeft, window, cx);
8477 editor.handle_input("]", window, cx);
8478 });
8479 cx.assert_editor_state(
8480 &"
8481 )}]ˇ]
8482 )}]ˇ]
8483 )}]ˇ]
8484 "
8485 .unindent(),
8486 );
8487}
8488
8489#[gpui::test]
8490async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
8491 init_test(cx, |_| {});
8492
8493 let mut cx = EditorTestContext::new(cx).await;
8494
8495 let html_language = Arc::new(
8496 Language::new(
8497 LanguageConfig {
8498 name: "HTML".into(),
8499 brackets: BracketPairConfig {
8500 pairs: vec![
8501 BracketPair {
8502 start: "<".into(),
8503 end: ">".into(),
8504 close: true,
8505 ..Default::default()
8506 },
8507 BracketPair {
8508 start: "{".into(),
8509 end: "}".into(),
8510 close: true,
8511 ..Default::default()
8512 },
8513 BracketPair {
8514 start: "(".into(),
8515 end: ")".into(),
8516 close: true,
8517 ..Default::default()
8518 },
8519 ],
8520 ..Default::default()
8521 },
8522 autoclose_before: "})]>".into(),
8523 ..Default::default()
8524 },
8525 Some(tree_sitter_html::LANGUAGE.into()),
8526 )
8527 .with_injection_query(
8528 r#"
8529 (script_element
8530 (raw_text) @injection.content
8531 (#set! injection.language "javascript"))
8532 "#,
8533 )
8534 .unwrap(),
8535 );
8536
8537 let javascript_language = Arc::new(Language::new(
8538 LanguageConfig {
8539 name: "JavaScript".into(),
8540 brackets: BracketPairConfig {
8541 pairs: vec![
8542 BracketPair {
8543 start: "/*".into(),
8544 end: " */".into(),
8545 close: true,
8546 ..Default::default()
8547 },
8548 BracketPair {
8549 start: "{".into(),
8550 end: "}".into(),
8551 close: true,
8552 ..Default::default()
8553 },
8554 BracketPair {
8555 start: "(".into(),
8556 end: ")".into(),
8557 close: true,
8558 ..Default::default()
8559 },
8560 ],
8561 ..Default::default()
8562 },
8563 autoclose_before: "})]>".into(),
8564 ..Default::default()
8565 },
8566 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8567 ));
8568
8569 cx.language_registry().add(html_language.clone());
8570 cx.language_registry().add(javascript_language.clone());
8571
8572 cx.update_buffer(|buffer, cx| {
8573 buffer.set_language(Some(html_language), cx);
8574 });
8575
8576 cx.set_state(
8577 &r#"
8578 <body>ˇ
8579 <script>
8580 var x = 1;ˇ
8581 </script>
8582 </body>ˇ
8583 "#
8584 .unindent(),
8585 );
8586
8587 // Precondition: different languages are active at different locations.
8588 cx.update_editor(|editor, window, cx| {
8589 let snapshot = editor.snapshot(window, cx);
8590 let cursors = editor.selections.ranges::<usize>(cx);
8591 let languages = cursors
8592 .iter()
8593 .map(|c| snapshot.language_at(c.start).unwrap().name())
8594 .collect::<Vec<_>>();
8595 assert_eq!(
8596 languages,
8597 &["HTML".into(), "JavaScript".into(), "HTML".into()]
8598 );
8599 });
8600
8601 // Angle brackets autoclose in HTML, but not JavaScript.
8602 cx.update_editor(|editor, window, cx| {
8603 editor.handle_input("<", window, cx);
8604 editor.handle_input("a", window, cx);
8605 });
8606 cx.assert_editor_state(
8607 &r#"
8608 <body><aˇ>
8609 <script>
8610 var x = 1;<aˇ
8611 </script>
8612 </body><aˇ>
8613 "#
8614 .unindent(),
8615 );
8616
8617 // Curly braces and parens autoclose in both HTML and JavaScript.
8618 cx.update_editor(|editor, window, cx| {
8619 editor.handle_input(" b=", window, cx);
8620 editor.handle_input("{", window, cx);
8621 editor.handle_input("c", window, cx);
8622 editor.handle_input("(", window, cx);
8623 });
8624 cx.assert_editor_state(
8625 &r#"
8626 <body><a b={c(ˇ)}>
8627 <script>
8628 var x = 1;<a b={c(ˇ)}
8629 </script>
8630 </body><a b={c(ˇ)}>
8631 "#
8632 .unindent(),
8633 );
8634
8635 // Brackets that were already autoclosed are skipped.
8636 cx.update_editor(|editor, window, cx| {
8637 editor.handle_input(")", window, cx);
8638 editor.handle_input("d", window, cx);
8639 editor.handle_input("}", window, cx);
8640 });
8641 cx.assert_editor_state(
8642 &r#"
8643 <body><a b={c()d}ˇ>
8644 <script>
8645 var x = 1;<a b={c()d}ˇ
8646 </script>
8647 </body><a b={c()d}ˇ>
8648 "#
8649 .unindent(),
8650 );
8651 cx.update_editor(|editor, window, cx| {
8652 editor.handle_input(">", window, cx);
8653 });
8654 cx.assert_editor_state(
8655 &r#"
8656 <body><a b={c()d}>ˇ
8657 <script>
8658 var x = 1;<a b={c()d}>ˇ
8659 </script>
8660 </body><a b={c()d}>ˇ
8661 "#
8662 .unindent(),
8663 );
8664
8665 // Reset
8666 cx.set_state(
8667 &r#"
8668 <body>ˇ
8669 <script>
8670 var x = 1;ˇ
8671 </script>
8672 </body>ˇ
8673 "#
8674 .unindent(),
8675 );
8676
8677 cx.update_editor(|editor, window, cx| {
8678 editor.handle_input("<", window, cx);
8679 });
8680 cx.assert_editor_state(
8681 &r#"
8682 <body><ˇ>
8683 <script>
8684 var x = 1;<ˇ
8685 </script>
8686 </body><ˇ>
8687 "#
8688 .unindent(),
8689 );
8690
8691 // When backspacing, the closing angle brackets are removed.
8692 cx.update_editor(|editor, window, cx| {
8693 editor.backspace(&Backspace, window, cx);
8694 });
8695 cx.assert_editor_state(
8696 &r#"
8697 <body>ˇ
8698 <script>
8699 var x = 1;ˇ
8700 </script>
8701 </body>ˇ
8702 "#
8703 .unindent(),
8704 );
8705
8706 // Block comments autoclose in JavaScript, but not HTML.
8707 cx.update_editor(|editor, window, cx| {
8708 editor.handle_input("/", window, cx);
8709 editor.handle_input("*", window, cx);
8710 });
8711 cx.assert_editor_state(
8712 &r#"
8713 <body>/*ˇ
8714 <script>
8715 var x = 1;/*ˇ */
8716 </script>
8717 </body>/*ˇ
8718 "#
8719 .unindent(),
8720 );
8721}
8722
8723#[gpui::test]
8724async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
8725 init_test(cx, |_| {});
8726
8727 let mut cx = EditorTestContext::new(cx).await;
8728
8729 let rust_language = Arc::new(
8730 Language::new(
8731 LanguageConfig {
8732 name: "Rust".into(),
8733 brackets: serde_json::from_value(json!([
8734 { "start": "{", "end": "}", "close": true, "newline": true },
8735 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
8736 ]))
8737 .unwrap(),
8738 autoclose_before: "})]>".into(),
8739 ..Default::default()
8740 },
8741 Some(tree_sitter_rust::LANGUAGE.into()),
8742 )
8743 .with_override_query("(string_literal) @string")
8744 .unwrap(),
8745 );
8746
8747 cx.language_registry().add(rust_language.clone());
8748 cx.update_buffer(|buffer, cx| {
8749 buffer.set_language(Some(rust_language), cx);
8750 });
8751
8752 cx.set_state(
8753 &r#"
8754 let x = ˇ
8755 "#
8756 .unindent(),
8757 );
8758
8759 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
8760 cx.update_editor(|editor, window, cx| {
8761 editor.handle_input("\"", window, cx);
8762 });
8763 cx.assert_editor_state(
8764 &r#"
8765 let x = "ˇ"
8766 "#
8767 .unindent(),
8768 );
8769
8770 // Inserting another quotation mark. The cursor moves across the existing
8771 // automatically-inserted quotation mark.
8772 cx.update_editor(|editor, window, cx| {
8773 editor.handle_input("\"", window, cx);
8774 });
8775 cx.assert_editor_state(
8776 &r#"
8777 let x = ""ˇ
8778 "#
8779 .unindent(),
8780 );
8781
8782 // Reset
8783 cx.set_state(
8784 &r#"
8785 let x = ˇ
8786 "#
8787 .unindent(),
8788 );
8789
8790 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
8791 cx.update_editor(|editor, window, cx| {
8792 editor.handle_input("\"", window, cx);
8793 editor.handle_input(" ", window, cx);
8794 editor.move_left(&Default::default(), window, cx);
8795 editor.handle_input("\\", window, cx);
8796 editor.handle_input("\"", window, cx);
8797 });
8798 cx.assert_editor_state(
8799 &r#"
8800 let x = "\"ˇ "
8801 "#
8802 .unindent(),
8803 );
8804
8805 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
8806 // mark. Nothing is inserted.
8807 cx.update_editor(|editor, window, cx| {
8808 editor.move_right(&Default::default(), window, cx);
8809 editor.handle_input("\"", window, cx);
8810 });
8811 cx.assert_editor_state(
8812 &r#"
8813 let x = "\" "ˇ
8814 "#
8815 .unindent(),
8816 );
8817}
8818
8819#[gpui::test]
8820async fn test_surround_with_pair(cx: &mut TestAppContext) {
8821 init_test(cx, |_| {});
8822
8823 let language = Arc::new(Language::new(
8824 LanguageConfig {
8825 brackets: BracketPairConfig {
8826 pairs: vec![
8827 BracketPair {
8828 start: "{".to_string(),
8829 end: "}".to_string(),
8830 close: true,
8831 surround: true,
8832 newline: true,
8833 },
8834 BracketPair {
8835 start: "/* ".to_string(),
8836 end: "*/".to_string(),
8837 close: true,
8838 surround: true,
8839 ..Default::default()
8840 },
8841 ],
8842 ..Default::default()
8843 },
8844 ..Default::default()
8845 },
8846 Some(tree_sitter_rust::LANGUAGE.into()),
8847 ));
8848
8849 let text = r#"
8850 a
8851 b
8852 c
8853 "#
8854 .unindent();
8855
8856 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8857 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8858 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8859 editor
8860 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8861 .await;
8862
8863 editor.update_in(cx, |editor, window, cx| {
8864 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8865 s.select_display_ranges([
8866 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8867 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8868 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
8869 ])
8870 });
8871
8872 editor.handle_input("{", window, cx);
8873 editor.handle_input("{", window, cx);
8874 editor.handle_input("{", window, cx);
8875 assert_eq!(
8876 editor.text(cx),
8877 "
8878 {{{a}}}
8879 {{{b}}}
8880 {{{c}}}
8881 "
8882 .unindent()
8883 );
8884 assert_eq!(
8885 editor.selections.display_ranges(cx),
8886 [
8887 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
8888 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
8889 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
8890 ]
8891 );
8892
8893 editor.undo(&Undo, window, cx);
8894 editor.undo(&Undo, window, cx);
8895 editor.undo(&Undo, window, cx);
8896 assert_eq!(
8897 editor.text(cx),
8898 "
8899 a
8900 b
8901 c
8902 "
8903 .unindent()
8904 );
8905 assert_eq!(
8906 editor.selections.display_ranges(cx),
8907 [
8908 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8909 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8910 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8911 ]
8912 );
8913
8914 // Ensure inserting the first character of a multi-byte bracket pair
8915 // doesn't surround the selections with the bracket.
8916 editor.handle_input("/", window, cx);
8917 assert_eq!(
8918 editor.text(cx),
8919 "
8920 /
8921 /
8922 /
8923 "
8924 .unindent()
8925 );
8926 assert_eq!(
8927 editor.selections.display_ranges(cx),
8928 [
8929 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8930 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8931 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8932 ]
8933 );
8934
8935 editor.undo(&Undo, window, cx);
8936 assert_eq!(
8937 editor.text(cx),
8938 "
8939 a
8940 b
8941 c
8942 "
8943 .unindent()
8944 );
8945 assert_eq!(
8946 editor.selections.display_ranges(cx),
8947 [
8948 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8949 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8950 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8951 ]
8952 );
8953
8954 // Ensure inserting the last character of a multi-byte bracket pair
8955 // doesn't surround the selections with the bracket.
8956 editor.handle_input("*", window, cx);
8957 assert_eq!(
8958 editor.text(cx),
8959 "
8960 *
8961 *
8962 *
8963 "
8964 .unindent()
8965 );
8966 assert_eq!(
8967 editor.selections.display_ranges(cx),
8968 [
8969 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8970 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8971 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8972 ]
8973 );
8974 });
8975}
8976
8977#[gpui::test]
8978async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
8979 init_test(cx, |_| {});
8980
8981 let language = Arc::new(Language::new(
8982 LanguageConfig {
8983 brackets: BracketPairConfig {
8984 pairs: vec![BracketPair {
8985 start: "{".to_string(),
8986 end: "}".to_string(),
8987 close: true,
8988 surround: true,
8989 newline: true,
8990 }],
8991 ..Default::default()
8992 },
8993 autoclose_before: "}".to_string(),
8994 ..Default::default()
8995 },
8996 Some(tree_sitter_rust::LANGUAGE.into()),
8997 ));
8998
8999 let text = r#"
9000 a
9001 b
9002 c
9003 "#
9004 .unindent();
9005
9006 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9007 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9008 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9009 editor
9010 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9011 .await;
9012
9013 editor.update_in(cx, |editor, window, cx| {
9014 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9015 s.select_ranges([
9016 Point::new(0, 1)..Point::new(0, 1),
9017 Point::new(1, 1)..Point::new(1, 1),
9018 Point::new(2, 1)..Point::new(2, 1),
9019 ])
9020 });
9021
9022 editor.handle_input("{", window, cx);
9023 editor.handle_input("{", window, cx);
9024 editor.handle_input("_", window, cx);
9025 assert_eq!(
9026 editor.text(cx),
9027 "
9028 a{{_}}
9029 b{{_}}
9030 c{{_}}
9031 "
9032 .unindent()
9033 );
9034 assert_eq!(
9035 editor.selections.ranges::<Point>(cx),
9036 [
9037 Point::new(0, 4)..Point::new(0, 4),
9038 Point::new(1, 4)..Point::new(1, 4),
9039 Point::new(2, 4)..Point::new(2, 4)
9040 ]
9041 );
9042
9043 editor.backspace(&Default::default(), window, cx);
9044 editor.backspace(&Default::default(), window, cx);
9045 assert_eq!(
9046 editor.text(cx),
9047 "
9048 a{}
9049 b{}
9050 c{}
9051 "
9052 .unindent()
9053 );
9054 assert_eq!(
9055 editor.selections.ranges::<Point>(cx),
9056 [
9057 Point::new(0, 2)..Point::new(0, 2),
9058 Point::new(1, 2)..Point::new(1, 2),
9059 Point::new(2, 2)..Point::new(2, 2)
9060 ]
9061 );
9062
9063 editor.delete_to_previous_word_start(&Default::default(), window, cx);
9064 assert_eq!(
9065 editor.text(cx),
9066 "
9067 a
9068 b
9069 c
9070 "
9071 .unindent()
9072 );
9073 assert_eq!(
9074 editor.selections.ranges::<Point>(cx),
9075 [
9076 Point::new(0, 1)..Point::new(0, 1),
9077 Point::new(1, 1)..Point::new(1, 1),
9078 Point::new(2, 1)..Point::new(2, 1)
9079 ]
9080 );
9081 });
9082}
9083
9084#[gpui::test]
9085async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
9086 init_test(cx, |settings| {
9087 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
9088 });
9089
9090 let mut cx = EditorTestContext::new(cx).await;
9091
9092 let language = Arc::new(Language::new(
9093 LanguageConfig {
9094 brackets: BracketPairConfig {
9095 pairs: vec![
9096 BracketPair {
9097 start: "{".to_string(),
9098 end: "}".to_string(),
9099 close: true,
9100 surround: true,
9101 newline: true,
9102 },
9103 BracketPair {
9104 start: "(".to_string(),
9105 end: ")".to_string(),
9106 close: true,
9107 surround: true,
9108 newline: true,
9109 },
9110 BracketPair {
9111 start: "[".to_string(),
9112 end: "]".to_string(),
9113 close: false,
9114 surround: true,
9115 newline: true,
9116 },
9117 ],
9118 ..Default::default()
9119 },
9120 autoclose_before: "})]".to_string(),
9121 ..Default::default()
9122 },
9123 Some(tree_sitter_rust::LANGUAGE.into()),
9124 ));
9125
9126 cx.language_registry().add(language.clone());
9127 cx.update_buffer(|buffer, cx| {
9128 buffer.set_language(Some(language), cx);
9129 });
9130
9131 cx.set_state(
9132 &"
9133 {(ˇ)}
9134 [[ˇ]]
9135 {(ˇ)}
9136 "
9137 .unindent(),
9138 );
9139
9140 cx.update_editor(|editor, window, cx| {
9141 editor.backspace(&Default::default(), window, cx);
9142 editor.backspace(&Default::default(), window, cx);
9143 });
9144
9145 cx.assert_editor_state(
9146 &"
9147 ˇ
9148 ˇ]]
9149 ˇ
9150 "
9151 .unindent(),
9152 );
9153
9154 cx.update_editor(|editor, window, cx| {
9155 editor.handle_input("{", window, cx);
9156 editor.handle_input("{", window, cx);
9157 editor.move_right(&MoveRight, window, cx);
9158 editor.move_right(&MoveRight, window, cx);
9159 editor.move_left(&MoveLeft, window, cx);
9160 editor.move_left(&MoveLeft, window, cx);
9161 editor.backspace(&Default::default(), window, cx);
9162 });
9163
9164 cx.assert_editor_state(
9165 &"
9166 {ˇ}
9167 {ˇ}]]
9168 {ˇ}
9169 "
9170 .unindent(),
9171 );
9172
9173 cx.update_editor(|editor, window, cx| {
9174 editor.backspace(&Default::default(), window, cx);
9175 });
9176
9177 cx.assert_editor_state(
9178 &"
9179 ˇ
9180 ˇ]]
9181 ˇ
9182 "
9183 .unindent(),
9184 );
9185}
9186
9187#[gpui::test]
9188async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
9189 init_test(cx, |_| {});
9190
9191 let language = Arc::new(Language::new(
9192 LanguageConfig::default(),
9193 Some(tree_sitter_rust::LANGUAGE.into()),
9194 ));
9195
9196 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
9197 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9198 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9199 editor
9200 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9201 .await;
9202
9203 editor.update_in(cx, |editor, window, cx| {
9204 editor.set_auto_replace_emoji_shortcode(true);
9205
9206 editor.handle_input("Hello ", window, cx);
9207 editor.handle_input(":wave", window, cx);
9208 assert_eq!(editor.text(cx), "Hello :wave".unindent());
9209
9210 editor.handle_input(":", window, cx);
9211 assert_eq!(editor.text(cx), "Hello 👋".unindent());
9212
9213 editor.handle_input(" :smile", window, cx);
9214 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
9215
9216 editor.handle_input(":", window, cx);
9217 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
9218
9219 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
9220 editor.handle_input(":wave", window, cx);
9221 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
9222
9223 editor.handle_input(":", window, cx);
9224 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
9225
9226 editor.handle_input(":1", window, cx);
9227 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
9228
9229 editor.handle_input(":", window, cx);
9230 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
9231
9232 // Ensure shortcode does not get replaced when it is part of a word
9233 editor.handle_input(" Test:wave", window, cx);
9234 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
9235
9236 editor.handle_input(":", window, cx);
9237 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
9238
9239 editor.set_auto_replace_emoji_shortcode(false);
9240
9241 // Ensure shortcode does not get replaced when auto replace is off
9242 editor.handle_input(" :wave", window, cx);
9243 assert_eq!(
9244 editor.text(cx),
9245 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
9246 );
9247
9248 editor.handle_input(":", window, cx);
9249 assert_eq!(
9250 editor.text(cx),
9251 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
9252 );
9253 });
9254}
9255
9256#[gpui::test]
9257async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
9258 init_test(cx, |_| {});
9259
9260 let (text, insertion_ranges) = marked_text_ranges(
9261 indoc! {"
9262 ˇ
9263 "},
9264 false,
9265 );
9266
9267 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
9268 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9269
9270 _ = editor.update_in(cx, |editor, window, cx| {
9271 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
9272
9273 editor
9274 .insert_snippet(&insertion_ranges, snippet, window, cx)
9275 .unwrap();
9276
9277 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
9278 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
9279 assert_eq!(editor.text(cx), expected_text);
9280 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
9281 }
9282
9283 assert(
9284 editor,
9285 cx,
9286 indoc! {"
9287 type «» =•
9288 "},
9289 );
9290
9291 assert!(editor.context_menu_visible(), "There should be a matches");
9292 });
9293}
9294
9295#[gpui::test]
9296async fn test_snippets(cx: &mut TestAppContext) {
9297 init_test(cx, |_| {});
9298
9299 let mut cx = EditorTestContext::new(cx).await;
9300
9301 cx.set_state(indoc! {"
9302 a.ˇ b
9303 a.ˇ b
9304 a.ˇ b
9305 "});
9306
9307 cx.update_editor(|editor, window, cx| {
9308 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
9309 let insertion_ranges = editor
9310 .selections
9311 .all(cx)
9312 .iter()
9313 .map(|s| s.range().clone())
9314 .collect::<Vec<_>>();
9315 editor
9316 .insert_snippet(&insertion_ranges, snippet, window, cx)
9317 .unwrap();
9318 });
9319
9320 cx.assert_editor_state(indoc! {"
9321 a.f(«oneˇ», two, «threeˇ») b
9322 a.f(«oneˇ», two, «threeˇ») b
9323 a.f(«oneˇ», two, «threeˇ») b
9324 "});
9325
9326 // Can't move earlier than the first tab stop
9327 cx.update_editor(|editor, window, cx| {
9328 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9329 });
9330 cx.assert_editor_state(indoc! {"
9331 a.f(«oneˇ», two, «threeˇ») b
9332 a.f(«oneˇ», two, «threeˇ») b
9333 a.f(«oneˇ», two, «threeˇ») b
9334 "});
9335
9336 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9337 cx.assert_editor_state(indoc! {"
9338 a.f(one, «twoˇ», three) b
9339 a.f(one, «twoˇ», three) b
9340 a.f(one, «twoˇ», three) b
9341 "});
9342
9343 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
9344 cx.assert_editor_state(indoc! {"
9345 a.f(«oneˇ», two, «threeˇ») b
9346 a.f(«oneˇ», two, «threeˇ») b
9347 a.f(«oneˇ», two, «threeˇ») b
9348 "});
9349
9350 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9351 cx.assert_editor_state(indoc! {"
9352 a.f(one, «twoˇ», three) b
9353 a.f(one, «twoˇ», three) b
9354 a.f(one, «twoˇ», three) b
9355 "});
9356 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9357 cx.assert_editor_state(indoc! {"
9358 a.f(one, two, three)ˇ b
9359 a.f(one, two, three)ˇ b
9360 a.f(one, two, three)ˇ b
9361 "});
9362
9363 // As soon as the last tab stop is reached, snippet state is gone
9364 cx.update_editor(|editor, window, cx| {
9365 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9366 });
9367 cx.assert_editor_state(indoc! {"
9368 a.f(one, two, three)ˇ b
9369 a.f(one, two, three)ˇ b
9370 a.f(one, two, three)ˇ b
9371 "});
9372}
9373
9374#[gpui::test]
9375async fn test_snippet_indentation(cx: &mut TestAppContext) {
9376 init_test(cx, |_| {});
9377
9378 let mut cx = EditorTestContext::new(cx).await;
9379
9380 cx.update_editor(|editor, window, cx| {
9381 let snippet = Snippet::parse(indoc! {"
9382 /*
9383 * Multiline comment with leading indentation
9384 *
9385 * $1
9386 */
9387 $0"})
9388 .unwrap();
9389 let insertion_ranges = editor
9390 .selections
9391 .all(cx)
9392 .iter()
9393 .map(|s| s.range().clone())
9394 .collect::<Vec<_>>();
9395 editor
9396 .insert_snippet(&insertion_ranges, snippet, window, cx)
9397 .unwrap();
9398 });
9399
9400 cx.assert_editor_state(indoc! {"
9401 /*
9402 * Multiline comment with leading indentation
9403 *
9404 * ˇ
9405 */
9406 "});
9407
9408 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9409 cx.assert_editor_state(indoc! {"
9410 /*
9411 * Multiline comment with leading indentation
9412 *
9413 *•
9414 */
9415 ˇ"});
9416}
9417
9418#[gpui::test]
9419async fn test_document_format_during_save(cx: &mut TestAppContext) {
9420 init_test(cx, |_| {});
9421
9422 let fs = FakeFs::new(cx.executor());
9423 fs.insert_file(path!("/file.rs"), Default::default()).await;
9424
9425 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
9426
9427 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9428 language_registry.add(rust_lang());
9429 let mut fake_servers = language_registry.register_fake_lsp(
9430 "Rust",
9431 FakeLspAdapter {
9432 capabilities: lsp::ServerCapabilities {
9433 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9434 ..Default::default()
9435 },
9436 ..Default::default()
9437 },
9438 );
9439
9440 let buffer = project
9441 .update(cx, |project, cx| {
9442 project.open_local_buffer(path!("/file.rs"), cx)
9443 })
9444 .await
9445 .unwrap();
9446
9447 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9448 let (editor, cx) = cx.add_window_view(|window, cx| {
9449 build_editor_with_project(project.clone(), buffer, window, cx)
9450 });
9451 editor.update_in(cx, |editor, window, cx| {
9452 editor.set_text("one\ntwo\nthree\n", window, cx)
9453 });
9454 assert!(cx.read(|cx| editor.is_dirty(cx)));
9455
9456 cx.executor().start_waiting();
9457 let fake_server = fake_servers.next().await.unwrap();
9458
9459 {
9460 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9461 move |params, _| async move {
9462 assert_eq!(
9463 params.text_document.uri,
9464 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9465 );
9466 assert_eq!(params.options.tab_size, 4);
9467 Ok(Some(vec![lsp::TextEdit::new(
9468 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9469 ", ".to_string(),
9470 )]))
9471 },
9472 );
9473 let save = editor
9474 .update_in(cx, |editor, window, cx| {
9475 editor.save(
9476 SaveOptions {
9477 format: true,
9478 autosave: false,
9479 },
9480 project.clone(),
9481 window,
9482 cx,
9483 )
9484 })
9485 .unwrap();
9486 cx.executor().start_waiting();
9487 save.await;
9488
9489 assert_eq!(
9490 editor.update(cx, |editor, cx| editor.text(cx)),
9491 "one, two\nthree\n"
9492 );
9493 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9494 }
9495
9496 {
9497 editor.update_in(cx, |editor, window, cx| {
9498 editor.set_text("one\ntwo\nthree\n", window, cx)
9499 });
9500 assert!(cx.read(|cx| editor.is_dirty(cx)));
9501
9502 // Ensure we can still save even if formatting hangs.
9503 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9504 move |params, _| async move {
9505 assert_eq!(
9506 params.text_document.uri,
9507 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9508 );
9509 futures::future::pending::<()>().await;
9510 unreachable!()
9511 },
9512 );
9513 let save = editor
9514 .update_in(cx, |editor, window, cx| {
9515 editor.save(
9516 SaveOptions {
9517 format: true,
9518 autosave: false,
9519 },
9520 project.clone(),
9521 window,
9522 cx,
9523 )
9524 })
9525 .unwrap();
9526 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9527 cx.executor().start_waiting();
9528 save.await;
9529 assert_eq!(
9530 editor.update(cx, |editor, cx| editor.text(cx)),
9531 "one\ntwo\nthree\n"
9532 );
9533 }
9534
9535 // Set rust language override and assert overridden tabsize is sent to language server
9536 update_test_language_settings(cx, |settings| {
9537 settings.languages.0.insert(
9538 "Rust".into(),
9539 LanguageSettingsContent {
9540 tab_size: NonZeroU32::new(8),
9541 ..Default::default()
9542 },
9543 );
9544 });
9545
9546 {
9547 editor.update_in(cx, |editor, window, cx| {
9548 editor.set_text("somehting_new\n", window, cx)
9549 });
9550 assert!(cx.read(|cx| editor.is_dirty(cx)));
9551 let _formatting_request_signal = fake_server
9552 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9553 assert_eq!(
9554 params.text_document.uri,
9555 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9556 );
9557 assert_eq!(params.options.tab_size, 8);
9558 Ok(Some(vec![]))
9559 });
9560 let save = editor
9561 .update_in(cx, |editor, window, cx| {
9562 editor.save(
9563 SaveOptions {
9564 format: true,
9565 autosave: false,
9566 },
9567 project.clone(),
9568 window,
9569 cx,
9570 )
9571 })
9572 .unwrap();
9573 cx.executor().start_waiting();
9574 save.await;
9575 }
9576}
9577
9578#[gpui::test]
9579async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
9580 init_test(cx, |settings| {
9581 settings.defaults.ensure_final_newline_on_save = Some(false);
9582 });
9583
9584 let fs = FakeFs::new(cx.executor());
9585 fs.insert_file(path!("/file.txt"), "foo".into()).await;
9586
9587 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
9588
9589 let buffer = project
9590 .update(cx, |project, cx| {
9591 project.open_local_buffer(path!("/file.txt"), cx)
9592 })
9593 .await
9594 .unwrap();
9595
9596 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9597 let (editor, cx) = cx.add_window_view(|window, cx| {
9598 build_editor_with_project(project.clone(), buffer, window, cx)
9599 });
9600 editor.update_in(cx, |editor, window, cx| {
9601 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9602 s.select_ranges([0..0])
9603 });
9604 });
9605 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9606
9607 editor.update_in(cx, |editor, window, cx| {
9608 editor.handle_input("\n", window, cx)
9609 });
9610 cx.run_until_parked();
9611 save(&editor, &project, cx).await;
9612 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9613
9614 editor.update_in(cx, |editor, window, cx| {
9615 editor.undo(&Default::default(), window, cx);
9616 });
9617 save(&editor, &project, cx).await;
9618 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9619
9620 editor.update_in(cx, |editor, window, cx| {
9621 editor.redo(&Default::default(), window, cx);
9622 });
9623 cx.run_until_parked();
9624 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9625
9626 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
9627 let save = editor
9628 .update_in(cx, |editor, window, cx| {
9629 editor.save(
9630 SaveOptions {
9631 format: true,
9632 autosave: false,
9633 },
9634 project.clone(),
9635 window,
9636 cx,
9637 )
9638 })
9639 .unwrap();
9640 cx.executor().start_waiting();
9641 save.await;
9642 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9643 }
9644}
9645
9646#[gpui::test]
9647async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
9648 init_test(cx, |_| {});
9649
9650 let cols = 4;
9651 let rows = 10;
9652 let sample_text_1 = sample_text(rows, cols, 'a');
9653 assert_eq!(
9654 sample_text_1,
9655 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
9656 );
9657 let sample_text_2 = sample_text(rows, cols, 'l');
9658 assert_eq!(
9659 sample_text_2,
9660 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
9661 );
9662 let sample_text_3 = sample_text(rows, cols, 'v');
9663 assert_eq!(
9664 sample_text_3,
9665 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
9666 );
9667
9668 let fs = FakeFs::new(cx.executor());
9669 fs.insert_tree(
9670 path!("/a"),
9671 json!({
9672 "main.rs": sample_text_1,
9673 "other.rs": sample_text_2,
9674 "lib.rs": sample_text_3,
9675 }),
9676 )
9677 .await;
9678
9679 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
9680 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9681 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9682
9683 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9684 language_registry.add(rust_lang());
9685 let mut fake_servers = language_registry.register_fake_lsp(
9686 "Rust",
9687 FakeLspAdapter {
9688 capabilities: lsp::ServerCapabilities {
9689 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9690 ..Default::default()
9691 },
9692 ..Default::default()
9693 },
9694 );
9695
9696 let worktree = project.update(cx, |project, cx| {
9697 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
9698 assert_eq!(worktrees.len(), 1);
9699 worktrees.pop().unwrap()
9700 });
9701 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9702
9703 let buffer_1 = project
9704 .update(cx, |project, cx| {
9705 project.open_buffer((worktree_id, "main.rs"), cx)
9706 })
9707 .await
9708 .unwrap();
9709 let buffer_2 = project
9710 .update(cx, |project, cx| {
9711 project.open_buffer((worktree_id, "other.rs"), cx)
9712 })
9713 .await
9714 .unwrap();
9715 let buffer_3 = project
9716 .update(cx, |project, cx| {
9717 project.open_buffer((worktree_id, "lib.rs"), cx)
9718 })
9719 .await
9720 .unwrap();
9721
9722 let multi_buffer = cx.new(|cx| {
9723 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9724 multi_buffer.push_excerpts(
9725 buffer_1.clone(),
9726 [
9727 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9728 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9729 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9730 ],
9731 cx,
9732 );
9733 multi_buffer.push_excerpts(
9734 buffer_2.clone(),
9735 [
9736 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9737 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9738 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9739 ],
9740 cx,
9741 );
9742 multi_buffer.push_excerpts(
9743 buffer_3.clone(),
9744 [
9745 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9746 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9747 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9748 ],
9749 cx,
9750 );
9751 multi_buffer
9752 });
9753 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
9754 Editor::new(
9755 EditorMode::full(),
9756 multi_buffer,
9757 Some(project.clone()),
9758 window,
9759 cx,
9760 )
9761 });
9762
9763 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9764 editor.change_selections(
9765 SelectionEffects::scroll(Autoscroll::Next),
9766 window,
9767 cx,
9768 |s| s.select_ranges(Some(1..2)),
9769 );
9770 editor.insert("|one|two|three|", window, cx);
9771 });
9772 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9773 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9774 editor.change_selections(
9775 SelectionEffects::scroll(Autoscroll::Next),
9776 window,
9777 cx,
9778 |s| s.select_ranges(Some(60..70)),
9779 );
9780 editor.insert("|four|five|six|", window, cx);
9781 });
9782 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9783
9784 // First two buffers should be edited, but not the third one.
9785 assert_eq!(
9786 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9787 "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}",
9788 );
9789 buffer_1.update(cx, |buffer, _| {
9790 assert!(buffer.is_dirty());
9791 assert_eq!(
9792 buffer.text(),
9793 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
9794 )
9795 });
9796 buffer_2.update(cx, |buffer, _| {
9797 assert!(buffer.is_dirty());
9798 assert_eq!(
9799 buffer.text(),
9800 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
9801 )
9802 });
9803 buffer_3.update(cx, |buffer, _| {
9804 assert!(!buffer.is_dirty());
9805 assert_eq!(buffer.text(), sample_text_3,)
9806 });
9807 cx.executor().run_until_parked();
9808
9809 cx.executor().start_waiting();
9810 let save = multi_buffer_editor
9811 .update_in(cx, |editor, window, cx| {
9812 editor.save(
9813 SaveOptions {
9814 format: true,
9815 autosave: false,
9816 },
9817 project.clone(),
9818 window,
9819 cx,
9820 )
9821 })
9822 .unwrap();
9823
9824 let fake_server = fake_servers.next().await.unwrap();
9825 fake_server
9826 .server
9827 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
9828 Ok(Some(vec![lsp::TextEdit::new(
9829 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9830 format!("[{} formatted]", params.text_document.uri),
9831 )]))
9832 })
9833 .detach();
9834 save.await;
9835
9836 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
9837 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
9838 assert_eq!(
9839 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9840 uri!(
9841 "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}"
9842 ),
9843 );
9844 buffer_1.update(cx, |buffer, _| {
9845 assert!(!buffer.is_dirty());
9846 assert_eq!(
9847 buffer.text(),
9848 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
9849 )
9850 });
9851 buffer_2.update(cx, |buffer, _| {
9852 assert!(!buffer.is_dirty());
9853 assert_eq!(
9854 buffer.text(),
9855 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
9856 )
9857 });
9858 buffer_3.update(cx, |buffer, _| {
9859 assert!(!buffer.is_dirty());
9860 assert_eq!(buffer.text(), sample_text_3,)
9861 });
9862}
9863
9864#[gpui::test]
9865async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
9866 init_test(cx, |_| {});
9867
9868 let fs = FakeFs::new(cx.executor());
9869 fs.insert_tree(
9870 path!("/dir"),
9871 json!({
9872 "file1.rs": "fn main() { println!(\"hello\"); }",
9873 "file2.rs": "fn test() { println!(\"test\"); }",
9874 "file3.rs": "fn other() { println!(\"other\"); }\n",
9875 }),
9876 )
9877 .await;
9878
9879 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
9880 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9881 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9882
9883 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9884 language_registry.add(rust_lang());
9885
9886 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
9887 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9888
9889 // Open three buffers
9890 let buffer_1 = project
9891 .update(cx, |project, cx| {
9892 project.open_buffer((worktree_id, "file1.rs"), cx)
9893 })
9894 .await
9895 .unwrap();
9896 let buffer_2 = project
9897 .update(cx, |project, cx| {
9898 project.open_buffer((worktree_id, "file2.rs"), cx)
9899 })
9900 .await
9901 .unwrap();
9902 let buffer_3 = project
9903 .update(cx, |project, cx| {
9904 project.open_buffer((worktree_id, "file3.rs"), cx)
9905 })
9906 .await
9907 .unwrap();
9908
9909 // Create a multi-buffer with all three buffers
9910 let multi_buffer = cx.new(|cx| {
9911 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9912 multi_buffer.push_excerpts(
9913 buffer_1.clone(),
9914 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9915 cx,
9916 );
9917 multi_buffer.push_excerpts(
9918 buffer_2.clone(),
9919 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9920 cx,
9921 );
9922 multi_buffer.push_excerpts(
9923 buffer_3.clone(),
9924 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9925 cx,
9926 );
9927 multi_buffer
9928 });
9929
9930 let editor = cx.new_window_entity(|window, cx| {
9931 Editor::new(
9932 EditorMode::full(),
9933 multi_buffer,
9934 Some(project.clone()),
9935 window,
9936 cx,
9937 )
9938 });
9939
9940 // Edit only the first buffer
9941 editor.update_in(cx, |editor, window, cx| {
9942 editor.change_selections(
9943 SelectionEffects::scroll(Autoscroll::Next),
9944 window,
9945 cx,
9946 |s| s.select_ranges(Some(10..10)),
9947 );
9948 editor.insert("// edited", window, cx);
9949 });
9950
9951 // Verify that only buffer 1 is dirty
9952 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
9953 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9954 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9955
9956 // Get write counts after file creation (files were created with initial content)
9957 // We expect each file to have been written once during creation
9958 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
9959 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
9960 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
9961
9962 // Perform autosave
9963 let save_task = editor.update_in(cx, |editor, window, cx| {
9964 editor.save(
9965 SaveOptions {
9966 format: true,
9967 autosave: true,
9968 },
9969 project.clone(),
9970 window,
9971 cx,
9972 )
9973 });
9974 save_task.await.unwrap();
9975
9976 // Only the dirty buffer should have been saved
9977 assert_eq!(
9978 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
9979 1,
9980 "Buffer 1 was dirty, so it should have been written once during autosave"
9981 );
9982 assert_eq!(
9983 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
9984 0,
9985 "Buffer 2 was clean, so it should not have been written during autosave"
9986 );
9987 assert_eq!(
9988 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
9989 0,
9990 "Buffer 3 was clean, so it should not have been written during autosave"
9991 );
9992
9993 // Verify buffer states after autosave
9994 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9995 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9996 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9997
9998 // Now perform a manual save (format = true)
9999 let save_task = editor.update_in(cx, |editor, window, cx| {
10000 editor.save(
10001 SaveOptions {
10002 format: true,
10003 autosave: false,
10004 },
10005 project.clone(),
10006 window,
10007 cx,
10008 )
10009 });
10010 save_task.await.unwrap();
10011
10012 // During manual save, clean buffers don't get written to disk
10013 // They just get did_save called for language server notifications
10014 assert_eq!(
10015 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
10016 1,
10017 "Buffer 1 should only have been written once total (during autosave, not manual save)"
10018 );
10019 assert_eq!(
10020 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
10021 0,
10022 "Buffer 2 should not have been written at all"
10023 );
10024 assert_eq!(
10025 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
10026 0,
10027 "Buffer 3 should not have been written at all"
10028 );
10029}
10030
10031async fn setup_range_format_test(
10032 cx: &mut TestAppContext,
10033) -> (
10034 Entity<Project>,
10035 Entity<Editor>,
10036 &mut gpui::VisualTestContext,
10037 lsp::FakeLanguageServer,
10038) {
10039 init_test(cx, |_| {});
10040
10041 let fs = FakeFs::new(cx.executor());
10042 fs.insert_file(path!("/file.rs"), Default::default()).await;
10043
10044 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10045
10046 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10047 language_registry.add(rust_lang());
10048 let mut fake_servers = language_registry.register_fake_lsp(
10049 "Rust",
10050 FakeLspAdapter {
10051 capabilities: lsp::ServerCapabilities {
10052 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
10053 ..lsp::ServerCapabilities::default()
10054 },
10055 ..FakeLspAdapter::default()
10056 },
10057 );
10058
10059 let buffer = project
10060 .update(cx, |project, cx| {
10061 project.open_local_buffer(path!("/file.rs"), cx)
10062 })
10063 .await
10064 .unwrap();
10065
10066 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10067 let (editor, cx) = cx.add_window_view(|window, cx| {
10068 build_editor_with_project(project.clone(), buffer, window, cx)
10069 });
10070
10071 cx.executor().start_waiting();
10072 let fake_server = fake_servers.next().await.unwrap();
10073
10074 (project, editor, cx, fake_server)
10075}
10076
10077#[gpui::test]
10078async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
10079 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10080
10081 editor.update_in(cx, |editor, window, cx| {
10082 editor.set_text("one\ntwo\nthree\n", window, cx)
10083 });
10084 assert!(cx.read(|cx| editor.is_dirty(cx)));
10085
10086 let save = editor
10087 .update_in(cx, |editor, window, cx| {
10088 editor.save(
10089 SaveOptions {
10090 format: true,
10091 autosave: false,
10092 },
10093 project.clone(),
10094 window,
10095 cx,
10096 )
10097 })
10098 .unwrap();
10099 fake_server
10100 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10101 assert_eq!(
10102 params.text_document.uri,
10103 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10104 );
10105 assert_eq!(params.options.tab_size, 4);
10106 Ok(Some(vec![lsp::TextEdit::new(
10107 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10108 ", ".to_string(),
10109 )]))
10110 })
10111 .next()
10112 .await;
10113 cx.executor().start_waiting();
10114 save.await;
10115 assert_eq!(
10116 editor.update(cx, |editor, cx| editor.text(cx)),
10117 "one, two\nthree\n"
10118 );
10119 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10120}
10121
10122#[gpui::test]
10123async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
10124 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10125
10126 editor.update_in(cx, |editor, window, cx| {
10127 editor.set_text("one\ntwo\nthree\n", window, cx)
10128 });
10129 assert!(cx.read(|cx| editor.is_dirty(cx)));
10130
10131 // Test that save still works when formatting hangs
10132 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
10133 move |params, _| async move {
10134 assert_eq!(
10135 params.text_document.uri,
10136 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10137 );
10138 futures::future::pending::<()>().await;
10139 unreachable!()
10140 },
10141 );
10142 let save = editor
10143 .update_in(cx, |editor, window, cx| {
10144 editor.save(
10145 SaveOptions {
10146 format: true,
10147 autosave: false,
10148 },
10149 project.clone(),
10150 window,
10151 cx,
10152 )
10153 })
10154 .unwrap();
10155 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10156 cx.executor().start_waiting();
10157 save.await;
10158 assert_eq!(
10159 editor.update(cx, |editor, cx| editor.text(cx)),
10160 "one\ntwo\nthree\n"
10161 );
10162 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10163}
10164
10165#[gpui::test]
10166async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
10167 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10168
10169 // Buffer starts clean, no formatting should be requested
10170 let save = editor
10171 .update_in(cx, |editor, window, cx| {
10172 editor.save(
10173 SaveOptions {
10174 format: false,
10175 autosave: false,
10176 },
10177 project.clone(),
10178 window,
10179 cx,
10180 )
10181 })
10182 .unwrap();
10183 let _pending_format_request = fake_server
10184 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
10185 panic!("Should not be invoked");
10186 })
10187 .next();
10188 cx.executor().start_waiting();
10189 save.await;
10190 cx.run_until_parked();
10191}
10192
10193#[gpui::test]
10194async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
10195 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10196
10197 // Set Rust language override and assert overridden tabsize is sent to language server
10198 update_test_language_settings(cx, |settings| {
10199 settings.languages.0.insert(
10200 "Rust".into(),
10201 LanguageSettingsContent {
10202 tab_size: NonZeroU32::new(8),
10203 ..Default::default()
10204 },
10205 );
10206 });
10207
10208 editor.update_in(cx, |editor, window, cx| {
10209 editor.set_text("something_new\n", window, cx)
10210 });
10211 assert!(cx.read(|cx| editor.is_dirty(cx)));
10212 let save = editor
10213 .update_in(cx, |editor, window, cx| {
10214 editor.save(
10215 SaveOptions {
10216 format: true,
10217 autosave: false,
10218 },
10219 project.clone(),
10220 window,
10221 cx,
10222 )
10223 })
10224 .unwrap();
10225 fake_server
10226 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10227 assert_eq!(
10228 params.text_document.uri,
10229 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10230 );
10231 assert_eq!(params.options.tab_size, 8);
10232 Ok(Some(Vec::new()))
10233 })
10234 .next()
10235 .await;
10236 save.await;
10237}
10238
10239#[gpui::test]
10240async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
10241 init_test(cx, |settings| {
10242 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
10243 Formatter::LanguageServer { name: None },
10244 )))
10245 });
10246
10247 let fs = FakeFs::new(cx.executor());
10248 fs.insert_file(path!("/file.rs"), Default::default()).await;
10249
10250 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10251
10252 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10253 language_registry.add(Arc::new(Language::new(
10254 LanguageConfig {
10255 name: "Rust".into(),
10256 matcher: LanguageMatcher {
10257 path_suffixes: vec!["rs".to_string()],
10258 ..Default::default()
10259 },
10260 ..LanguageConfig::default()
10261 },
10262 Some(tree_sitter_rust::LANGUAGE.into()),
10263 )));
10264 update_test_language_settings(cx, |settings| {
10265 // Enable Prettier formatting for the same buffer, and ensure
10266 // LSP is called instead of Prettier.
10267 settings.defaults.prettier = Some(PrettierSettings {
10268 allowed: true,
10269 ..PrettierSettings::default()
10270 });
10271 });
10272 let mut fake_servers = language_registry.register_fake_lsp(
10273 "Rust",
10274 FakeLspAdapter {
10275 capabilities: lsp::ServerCapabilities {
10276 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10277 ..Default::default()
10278 },
10279 ..Default::default()
10280 },
10281 );
10282
10283 let buffer = project
10284 .update(cx, |project, cx| {
10285 project.open_local_buffer(path!("/file.rs"), cx)
10286 })
10287 .await
10288 .unwrap();
10289
10290 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10291 let (editor, cx) = cx.add_window_view(|window, cx| {
10292 build_editor_with_project(project.clone(), buffer, window, cx)
10293 });
10294 editor.update_in(cx, |editor, window, cx| {
10295 editor.set_text("one\ntwo\nthree\n", window, cx)
10296 });
10297
10298 cx.executor().start_waiting();
10299 let fake_server = fake_servers.next().await.unwrap();
10300
10301 let format = editor
10302 .update_in(cx, |editor, window, cx| {
10303 editor.perform_format(
10304 project.clone(),
10305 FormatTrigger::Manual,
10306 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10307 window,
10308 cx,
10309 )
10310 })
10311 .unwrap();
10312 fake_server
10313 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
10314 assert_eq!(
10315 params.text_document.uri,
10316 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10317 );
10318 assert_eq!(params.options.tab_size, 4);
10319 Ok(Some(vec![lsp::TextEdit::new(
10320 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10321 ", ".to_string(),
10322 )]))
10323 })
10324 .next()
10325 .await;
10326 cx.executor().start_waiting();
10327 format.await;
10328 assert_eq!(
10329 editor.update(cx, |editor, cx| editor.text(cx)),
10330 "one, two\nthree\n"
10331 );
10332
10333 editor.update_in(cx, |editor, window, cx| {
10334 editor.set_text("one\ntwo\nthree\n", window, cx)
10335 });
10336 // Ensure we don't lock if formatting hangs.
10337 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10338 move |params, _| async move {
10339 assert_eq!(
10340 params.text_document.uri,
10341 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10342 );
10343 futures::future::pending::<()>().await;
10344 unreachable!()
10345 },
10346 );
10347 let format = editor
10348 .update_in(cx, |editor, window, cx| {
10349 editor.perform_format(
10350 project,
10351 FormatTrigger::Manual,
10352 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10353 window,
10354 cx,
10355 )
10356 })
10357 .unwrap();
10358 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10359 cx.executor().start_waiting();
10360 format.await;
10361 assert_eq!(
10362 editor.update(cx, |editor, cx| editor.text(cx)),
10363 "one\ntwo\nthree\n"
10364 );
10365}
10366
10367#[gpui::test]
10368async fn test_multiple_formatters(cx: &mut TestAppContext) {
10369 init_test(cx, |settings| {
10370 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
10371 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10372 Formatter::LanguageServer { name: None },
10373 Formatter::CodeActions(
10374 [
10375 ("code-action-1".into(), true),
10376 ("code-action-2".into(), true),
10377 ]
10378 .into_iter()
10379 .collect(),
10380 ),
10381 ])))
10382 });
10383
10384 let fs = FakeFs::new(cx.executor());
10385 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
10386 .await;
10387
10388 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10389 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10390 language_registry.add(rust_lang());
10391
10392 let mut fake_servers = language_registry.register_fake_lsp(
10393 "Rust",
10394 FakeLspAdapter {
10395 capabilities: lsp::ServerCapabilities {
10396 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10397 execute_command_provider: Some(lsp::ExecuteCommandOptions {
10398 commands: vec!["the-command-for-code-action-1".into()],
10399 ..Default::default()
10400 }),
10401 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10402 ..Default::default()
10403 },
10404 ..Default::default()
10405 },
10406 );
10407
10408 let buffer = project
10409 .update(cx, |project, cx| {
10410 project.open_local_buffer(path!("/file.rs"), cx)
10411 })
10412 .await
10413 .unwrap();
10414
10415 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10416 let (editor, cx) = cx.add_window_view(|window, cx| {
10417 build_editor_with_project(project.clone(), buffer, window, cx)
10418 });
10419
10420 cx.executor().start_waiting();
10421
10422 let fake_server = fake_servers.next().await.unwrap();
10423 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10424 move |_params, _| async move {
10425 Ok(Some(vec![lsp::TextEdit::new(
10426 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10427 "applied-formatting\n".to_string(),
10428 )]))
10429 },
10430 );
10431 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10432 move |params, _| async move {
10433 assert_eq!(
10434 params.context.only,
10435 Some(vec!["code-action-1".into(), "code-action-2".into()])
10436 );
10437 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
10438 Ok(Some(vec![
10439 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10440 kind: Some("code-action-1".into()),
10441 edit: Some(lsp::WorkspaceEdit::new(
10442 [(
10443 uri.clone(),
10444 vec![lsp::TextEdit::new(
10445 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10446 "applied-code-action-1-edit\n".to_string(),
10447 )],
10448 )]
10449 .into_iter()
10450 .collect(),
10451 )),
10452 command: Some(lsp::Command {
10453 command: "the-command-for-code-action-1".into(),
10454 ..Default::default()
10455 }),
10456 ..Default::default()
10457 }),
10458 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10459 kind: Some("code-action-2".into()),
10460 edit: Some(lsp::WorkspaceEdit::new(
10461 [(
10462 uri.clone(),
10463 vec![lsp::TextEdit::new(
10464 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10465 "applied-code-action-2-edit\n".to_string(),
10466 )],
10467 )]
10468 .into_iter()
10469 .collect(),
10470 )),
10471 ..Default::default()
10472 }),
10473 ]))
10474 },
10475 );
10476
10477 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
10478 move |params, _| async move { Ok(params) }
10479 });
10480
10481 let command_lock = Arc::new(futures::lock::Mutex::new(()));
10482 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
10483 let fake = fake_server.clone();
10484 let lock = command_lock.clone();
10485 move |params, _| {
10486 assert_eq!(params.command, "the-command-for-code-action-1");
10487 let fake = fake.clone();
10488 let lock = lock.clone();
10489 async move {
10490 lock.lock().await;
10491 fake.server
10492 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
10493 label: None,
10494 edit: lsp::WorkspaceEdit {
10495 changes: Some(
10496 [(
10497 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
10498 vec![lsp::TextEdit {
10499 range: lsp::Range::new(
10500 lsp::Position::new(0, 0),
10501 lsp::Position::new(0, 0),
10502 ),
10503 new_text: "applied-code-action-1-command\n".into(),
10504 }],
10505 )]
10506 .into_iter()
10507 .collect(),
10508 ),
10509 ..Default::default()
10510 },
10511 })
10512 .await
10513 .into_response()
10514 .unwrap();
10515 Ok(Some(json!(null)))
10516 }
10517 }
10518 });
10519
10520 cx.executor().start_waiting();
10521 editor
10522 .update_in(cx, |editor, window, cx| {
10523 editor.perform_format(
10524 project.clone(),
10525 FormatTrigger::Manual,
10526 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10527 window,
10528 cx,
10529 )
10530 })
10531 .unwrap()
10532 .await;
10533 editor.update(cx, |editor, cx| {
10534 assert_eq!(
10535 editor.text(cx),
10536 r#"
10537 applied-code-action-2-edit
10538 applied-code-action-1-command
10539 applied-code-action-1-edit
10540 applied-formatting
10541 one
10542 two
10543 three
10544 "#
10545 .unindent()
10546 );
10547 });
10548
10549 editor.update_in(cx, |editor, window, cx| {
10550 editor.undo(&Default::default(), window, cx);
10551 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10552 });
10553
10554 // Perform a manual edit while waiting for an LSP command
10555 // that's being run as part of a formatting code action.
10556 let lock_guard = command_lock.lock().await;
10557 let format = editor
10558 .update_in(cx, |editor, window, cx| {
10559 editor.perform_format(
10560 project.clone(),
10561 FormatTrigger::Manual,
10562 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10563 window,
10564 cx,
10565 )
10566 })
10567 .unwrap();
10568 cx.run_until_parked();
10569 editor.update(cx, |editor, cx| {
10570 assert_eq!(
10571 editor.text(cx),
10572 r#"
10573 applied-code-action-1-edit
10574 applied-formatting
10575 one
10576 two
10577 three
10578 "#
10579 .unindent()
10580 );
10581
10582 editor.buffer.update(cx, |buffer, cx| {
10583 let ix = buffer.len(cx);
10584 buffer.edit([(ix..ix, "edited\n")], None, cx);
10585 });
10586 });
10587
10588 // Allow the LSP command to proceed. Because the buffer was edited,
10589 // the second code action will not be run.
10590 drop(lock_guard);
10591 format.await;
10592 editor.update_in(cx, |editor, window, cx| {
10593 assert_eq!(
10594 editor.text(cx),
10595 r#"
10596 applied-code-action-1-command
10597 applied-code-action-1-edit
10598 applied-formatting
10599 one
10600 two
10601 three
10602 edited
10603 "#
10604 .unindent()
10605 );
10606
10607 // The manual edit is undone first, because it is the last thing the user did
10608 // (even though the command completed afterwards).
10609 editor.undo(&Default::default(), window, cx);
10610 assert_eq!(
10611 editor.text(cx),
10612 r#"
10613 applied-code-action-1-command
10614 applied-code-action-1-edit
10615 applied-formatting
10616 one
10617 two
10618 three
10619 "#
10620 .unindent()
10621 );
10622
10623 // All the formatting (including the command, which completed after the manual edit)
10624 // is undone together.
10625 editor.undo(&Default::default(), window, cx);
10626 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10627 });
10628}
10629
10630#[gpui::test]
10631async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
10632 init_test(cx, |settings| {
10633 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10634 Formatter::LanguageServer { name: None },
10635 ])))
10636 });
10637
10638 let fs = FakeFs::new(cx.executor());
10639 fs.insert_file(path!("/file.ts"), Default::default()).await;
10640
10641 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10642
10643 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10644 language_registry.add(Arc::new(Language::new(
10645 LanguageConfig {
10646 name: "TypeScript".into(),
10647 matcher: LanguageMatcher {
10648 path_suffixes: vec!["ts".to_string()],
10649 ..Default::default()
10650 },
10651 ..LanguageConfig::default()
10652 },
10653 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
10654 )));
10655 update_test_language_settings(cx, |settings| {
10656 settings.defaults.prettier = Some(PrettierSettings {
10657 allowed: true,
10658 ..PrettierSettings::default()
10659 });
10660 });
10661 let mut fake_servers = language_registry.register_fake_lsp(
10662 "TypeScript",
10663 FakeLspAdapter {
10664 capabilities: lsp::ServerCapabilities {
10665 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10666 ..Default::default()
10667 },
10668 ..Default::default()
10669 },
10670 );
10671
10672 let buffer = project
10673 .update(cx, |project, cx| {
10674 project.open_local_buffer(path!("/file.ts"), cx)
10675 })
10676 .await
10677 .unwrap();
10678
10679 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10680 let (editor, cx) = cx.add_window_view(|window, cx| {
10681 build_editor_with_project(project.clone(), buffer, window, cx)
10682 });
10683 editor.update_in(cx, |editor, window, cx| {
10684 editor.set_text(
10685 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10686 window,
10687 cx,
10688 )
10689 });
10690
10691 cx.executor().start_waiting();
10692 let fake_server = fake_servers.next().await.unwrap();
10693
10694 let format = editor
10695 .update_in(cx, |editor, window, cx| {
10696 editor.perform_code_action_kind(
10697 project.clone(),
10698 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10699 window,
10700 cx,
10701 )
10702 })
10703 .unwrap();
10704 fake_server
10705 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
10706 assert_eq!(
10707 params.text_document.uri,
10708 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10709 );
10710 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
10711 lsp::CodeAction {
10712 title: "Organize Imports".to_string(),
10713 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
10714 edit: Some(lsp::WorkspaceEdit {
10715 changes: Some(
10716 [(
10717 params.text_document.uri.clone(),
10718 vec![lsp::TextEdit::new(
10719 lsp::Range::new(
10720 lsp::Position::new(1, 0),
10721 lsp::Position::new(2, 0),
10722 ),
10723 "".to_string(),
10724 )],
10725 )]
10726 .into_iter()
10727 .collect(),
10728 ),
10729 ..Default::default()
10730 }),
10731 ..Default::default()
10732 },
10733 )]))
10734 })
10735 .next()
10736 .await;
10737 cx.executor().start_waiting();
10738 format.await;
10739 assert_eq!(
10740 editor.update(cx, |editor, cx| editor.text(cx)),
10741 "import { a } from 'module';\n\nconst x = a;\n"
10742 );
10743
10744 editor.update_in(cx, |editor, window, cx| {
10745 editor.set_text(
10746 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10747 window,
10748 cx,
10749 )
10750 });
10751 // Ensure we don't lock if code action hangs.
10752 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10753 move |params, _| async move {
10754 assert_eq!(
10755 params.text_document.uri,
10756 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10757 );
10758 futures::future::pending::<()>().await;
10759 unreachable!()
10760 },
10761 );
10762 let format = editor
10763 .update_in(cx, |editor, window, cx| {
10764 editor.perform_code_action_kind(
10765 project,
10766 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10767 window,
10768 cx,
10769 )
10770 })
10771 .unwrap();
10772 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
10773 cx.executor().start_waiting();
10774 format.await;
10775 assert_eq!(
10776 editor.update(cx, |editor, cx| editor.text(cx)),
10777 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
10778 );
10779}
10780
10781#[gpui::test]
10782async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
10783 init_test(cx, |_| {});
10784
10785 let mut cx = EditorLspTestContext::new_rust(
10786 lsp::ServerCapabilities {
10787 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10788 ..Default::default()
10789 },
10790 cx,
10791 )
10792 .await;
10793
10794 cx.set_state(indoc! {"
10795 one.twoˇ
10796 "});
10797
10798 // The format request takes a long time. When it completes, it inserts
10799 // a newline and an indent before the `.`
10800 cx.lsp
10801 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
10802 let executor = cx.background_executor().clone();
10803 async move {
10804 executor.timer(Duration::from_millis(100)).await;
10805 Ok(Some(vec![lsp::TextEdit {
10806 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
10807 new_text: "\n ".into(),
10808 }]))
10809 }
10810 });
10811
10812 // Submit a format request.
10813 let format_1 = cx
10814 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10815 .unwrap();
10816 cx.executor().run_until_parked();
10817
10818 // Submit a second format request.
10819 let format_2 = cx
10820 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10821 .unwrap();
10822 cx.executor().run_until_parked();
10823
10824 // Wait for both format requests to complete
10825 cx.executor().advance_clock(Duration::from_millis(200));
10826 cx.executor().start_waiting();
10827 format_1.await.unwrap();
10828 cx.executor().start_waiting();
10829 format_2.await.unwrap();
10830
10831 // The formatting edits only happens once.
10832 cx.assert_editor_state(indoc! {"
10833 one
10834 .twoˇ
10835 "});
10836}
10837
10838#[gpui::test]
10839async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
10840 init_test(cx, |settings| {
10841 settings.defaults.formatter = Some(SelectedFormatter::Auto)
10842 });
10843
10844 let mut cx = EditorLspTestContext::new_rust(
10845 lsp::ServerCapabilities {
10846 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10847 ..Default::default()
10848 },
10849 cx,
10850 )
10851 .await;
10852
10853 // Set up a buffer white some trailing whitespace and no trailing newline.
10854 cx.set_state(
10855 &[
10856 "one ", //
10857 "twoˇ", //
10858 "three ", //
10859 "four", //
10860 ]
10861 .join("\n"),
10862 );
10863
10864 // Submit a format request.
10865 let format = cx
10866 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10867 .unwrap();
10868
10869 // Record which buffer changes have been sent to the language server
10870 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
10871 cx.lsp
10872 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
10873 let buffer_changes = buffer_changes.clone();
10874 move |params, _| {
10875 buffer_changes.lock().extend(
10876 params
10877 .content_changes
10878 .into_iter()
10879 .map(|e| (e.range.unwrap(), e.text)),
10880 );
10881 }
10882 });
10883
10884 // Handle formatting requests to the language server.
10885 cx.lsp
10886 .set_request_handler::<lsp::request::Formatting, _, _>({
10887 let buffer_changes = buffer_changes.clone();
10888 move |_, _| {
10889 // When formatting is requested, trailing whitespace has already been stripped,
10890 // and the trailing newline has already been added.
10891 assert_eq!(
10892 &buffer_changes.lock()[1..],
10893 &[
10894 (
10895 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
10896 "".into()
10897 ),
10898 (
10899 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
10900 "".into()
10901 ),
10902 (
10903 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
10904 "\n".into()
10905 ),
10906 ]
10907 );
10908
10909 // Insert blank lines between each line of the buffer.
10910 async move {
10911 Ok(Some(vec![
10912 lsp::TextEdit {
10913 range: lsp::Range::new(
10914 lsp::Position::new(1, 0),
10915 lsp::Position::new(1, 0),
10916 ),
10917 new_text: "\n".into(),
10918 },
10919 lsp::TextEdit {
10920 range: lsp::Range::new(
10921 lsp::Position::new(2, 0),
10922 lsp::Position::new(2, 0),
10923 ),
10924 new_text: "\n".into(),
10925 },
10926 ]))
10927 }
10928 }
10929 });
10930
10931 // After formatting the buffer, the trailing whitespace is stripped,
10932 // a newline is appended, and the edits provided by the language server
10933 // have been applied.
10934 format.await.unwrap();
10935 cx.assert_editor_state(
10936 &[
10937 "one", //
10938 "", //
10939 "twoˇ", //
10940 "", //
10941 "three", //
10942 "four", //
10943 "", //
10944 ]
10945 .join("\n"),
10946 );
10947
10948 // Undoing the formatting undoes the trailing whitespace removal, the
10949 // trailing newline, and the LSP edits.
10950 cx.update_buffer(|buffer, cx| buffer.undo(cx));
10951 cx.assert_editor_state(
10952 &[
10953 "one ", //
10954 "twoˇ", //
10955 "three ", //
10956 "four", //
10957 ]
10958 .join("\n"),
10959 );
10960}
10961
10962#[gpui::test]
10963async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
10964 cx: &mut TestAppContext,
10965) {
10966 init_test(cx, |_| {});
10967
10968 cx.update(|cx| {
10969 cx.update_global::<SettingsStore, _>(|settings, cx| {
10970 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10971 settings.auto_signature_help = Some(true);
10972 });
10973 });
10974 });
10975
10976 let mut cx = EditorLspTestContext::new_rust(
10977 lsp::ServerCapabilities {
10978 signature_help_provider: Some(lsp::SignatureHelpOptions {
10979 ..Default::default()
10980 }),
10981 ..Default::default()
10982 },
10983 cx,
10984 )
10985 .await;
10986
10987 let language = Language::new(
10988 LanguageConfig {
10989 name: "Rust".into(),
10990 brackets: BracketPairConfig {
10991 pairs: vec![
10992 BracketPair {
10993 start: "{".to_string(),
10994 end: "}".to_string(),
10995 close: true,
10996 surround: true,
10997 newline: true,
10998 },
10999 BracketPair {
11000 start: "(".to_string(),
11001 end: ")".to_string(),
11002 close: true,
11003 surround: true,
11004 newline: true,
11005 },
11006 BracketPair {
11007 start: "/*".to_string(),
11008 end: " */".to_string(),
11009 close: true,
11010 surround: true,
11011 newline: true,
11012 },
11013 BracketPair {
11014 start: "[".to_string(),
11015 end: "]".to_string(),
11016 close: false,
11017 surround: false,
11018 newline: true,
11019 },
11020 BracketPair {
11021 start: "\"".to_string(),
11022 end: "\"".to_string(),
11023 close: true,
11024 surround: true,
11025 newline: false,
11026 },
11027 BracketPair {
11028 start: "<".to_string(),
11029 end: ">".to_string(),
11030 close: false,
11031 surround: true,
11032 newline: true,
11033 },
11034 ],
11035 ..Default::default()
11036 },
11037 autoclose_before: "})]".to_string(),
11038 ..Default::default()
11039 },
11040 Some(tree_sitter_rust::LANGUAGE.into()),
11041 );
11042 let language = Arc::new(language);
11043
11044 cx.language_registry().add(language.clone());
11045 cx.update_buffer(|buffer, cx| {
11046 buffer.set_language(Some(language), cx);
11047 });
11048
11049 cx.set_state(
11050 &r#"
11051 fn main() {
11052 sampleˇ
11053 }
11054 "#
11055 .unindent(),
11056 );
11057
11058 cx.update_editor(|editor, window, cx| {
11059 editor.handle_input("(", window, cx);
11060 });
11061 cx.assert_editor_state(
11062 &"
11063 fn main() {
11064 sample(ˇ)
11065 }
11066 "
11067 .unindent(),
11068 );
11069
11070 let mocked_response = lsp::SignatureHelp {
11071 signatures: vec![lsp::SignatureInformation {
11072 label: "fn sample(param1: u8, param2: u8)".to_string(),
11073 documentation: None,
11074 parameters: Some(vec![
11075 lsp::ParameterInformation {
11076 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11077 documentation: None,
11078 },
11079 lsp::ParameterInformation {
11080 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11081 documentation: None,
11082 },
11083 ]),
11084 active_parameter: None,
11085 }],
11086 active_signature: Some(0),
11087 active_parameter: Some(0),
11088 };
11089 handle_signature_help_request(&mut cx, mocked_response).await;
11090
11091 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11092 .await;
11093
11094 cx.editor(|editor, _, _| {
11095 let signature_help_state = editor.signature_help_state.popover().cloned();
11096 let signature = signature_help_state.unwrap();
11097 assert_eq!(
11098 signature.signatures[signature.current_signature].label,
11099 "fn sample(param1: u8, param2: u8)"
11100 );
11101 });
11102}
11103
11104#[gpui::test]
11105async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
11106 init_test(cx, |_| {});
11107
11108 cx.update(|cx| {
11109 cx.update_global::<SettingsStore, _>(|settings, cx| {
11110 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11111 settings.auto_signature_help = Some(false);
11112 settings.show_signature_help_after_edits = Some(false);
11113 });
11114 });
11115 });
11116
11117 let mut cx = EditorLspTestContext::new_rust(
11118 lsp::ServerCapabilities {
11119 signature_help_provider: Some(lsp::SignatureHelpOptions {
11120 ..Default::default()
11121 }),
11122 ..Default::default()
11123 },
11124 cx,
11125 )
11126 .await;
11127
11128 let language = Language::new(
11129 LanguageConfig {
11130 name: "Rust".into(),
11131 brackets: BracketPairConfig {
11132 pairs: vec![
11133 BracketPair {
11134 start: "{".to_string(),
11135 end: "}".to_string(),
11136 close: true,
11137 surround: true,
11138 newline: true,
11139 },
11140 BracketPair {
11141 start: "(".to_string(),
11142 end: ")".to_string(),
11143 close: true,
11144 surround: true,
11145 newline: true,
11146 },
11147 BracketPair {
11148 start: "/*".to_string(),
11149 end: " */".to_string(),
11150 close: true,
11151 surround: true,
11152 newline: true,
11153 },
11154 BracketPair {
11155 start: "[".to_string(),
11156 end: "]".to_string(),
11157 close: false,
11158 surround: false,
11159 newline: true,
11160 },
11161 BracketPair {
11162 start: "\"".to_string(),
11163 end: "\"".to_string(),
11164 close: true,
11165 surround: true,
11166 newline: false,
11167 },
11168 BracketPair {
11169 start: "<".to_string(),
11170 end: ">".to_string(),
11171 close: false,
11172 surround: true,
11173 newline: true,
11174 },
11175 ],
11176 ..Default::default()
11177 },
11178 autoclose_before: "})]".to_string(),
11179 ..Default::default()
11180 },
11181 Some(tree_sitter_rust::LANGUAGE.into()),
11182 );
11183 let language = Arc::new(language);
11184
11185 cx.language_registry().add(language.clone());
11186 cx.update_buffer(|buffer, cx| {
11187 buffer.set_language(Some(language), cx);
11188 });
11189
11190 // Ensure that signature_help is not called when no signature help is enabled.
11191 cx.set_state(
11192 &r#"
11193 fn main() {
11194 sampleˇ
11195 }
11196 "#
11197 .unindent(),
11198 );
11199 cx.update_editor(|editor, window, cx| {
11200 editor.handle_input("(", window, cx);
11201 });
11202 cx.assert_editor_state(
11203 &"
11204 fn main() {
11205 sample(ˇ)
11206 }
11207 "
11208 .unindent(),
11209 );
11210 cx.editor(|editor, _, _| {
11211 assert!(editor.signature_help_state.task().is_none());
11212 });
11213
11214 let mocked_response = lsp::SignatureHelp {
11215 signatures: vec![lsp::SignatureInformation {
11216 label: "fn sample(param1: u8, param2: u8)".to_string(),
11217 documentation: None,
11218 parameters: Some(vec![
11219 lsp::ParameterInformation {
11220 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11221 documentation: None,
11222 },
11223 lsp::ParameterInformation {
11224 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11225 documentation: None,
11226 },
11227 ]),
11228 active_parameter: None,
11229 }],
11230 active_signature: Some(0),
11231 active_parameter: Some(0),
11232 };
11233
11234 // Ensure that signature_help is called when enabled afte edits
11235 cx.update(|_, cx| {
11236 cx.update_global::<SettingsStore, _>(|settings, cx| {
11237 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11238 settings.auto_signature_help = Some(false);
11239 settings.show_signature_help_after_edits = Some(true);
11240 });
11241 });
11242 });
11243 cx.set_state(
11244 &r#"
11245 fn main() {
11246 sampleˇ
11247 }
11248 "#
11249 .unindent(),
11250 );
11251 cx.update_editor(|editor, window, cx| {
11252 editor.handle_input("(", window, cx);
11253 });
11254 cx.assert_editor_state(
11255 &"
11256 fn main() {
11257 sample(ˇ)
11258 }
11259 "
11260 .unindent(),
11261 );
11262 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11263 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11264 .await;
11265 cx.update_editor(|editor, _, _| {
11266 let signature_help_state = editor.signature_help_state.popover().cloned();
11267 assert!(signature_help_state.is_some());
11268 let signature = signature_help_state.unwrap();
11269 assert_eq!(
11270 signature.signatures[signature.current_signature].label,
11271 "fn sample(param1: u8, param2: u8)"
11272 );
11273 editor.signature_help_state = SignatureHelpState::default();
11274 });
11275
11276 // Ensure that signature_help is called when auto signature help override is enabled
11277 cx.update(|_, cx| {
11278 cx.update_global::<SettingsStore, _>(|settings, cx| {
11279 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11280 settings.auto_signature_help = Some(true);
11281 settings.show_signature_help_after_edits = Some(false);
11282 });
11283 });
11284 });
11285 cx.set_state(
11286 &r#"
11287 fn main() {
11288 sampleˇ
11289 }
11290 "#
11291 .unindent(),
11292 );
11293 cx.update_editor(|editor, window, cx| {
11294 editor.handle_input("(", window, cx);
11295 });
11296 cx.assert_editor_state(
11297 &"
11298 fn main() {
11299 sample(ˇ)
11300 }
11301 "
11302 .unindent(),
11303 );
11304 handle_signature_help_request(&mut cx, mocked_response).await;
11305 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11306 .await;
11307 cx.editor(|editor, _, _| {
11308 let signature_help_state = editor.signature_help_state.popover().cloned();
11309 assert!(signature_help_state.is_some());
11310 let signature = signature_help_state.unwrap();
11311 assert_eq!(
11312 signature.signatures[signature.current_signature].label,
11313 "fn sample(param1: u8, param2: u8)"
11314 );
11315 });
11316}
11317
11318#[gpui::test]
11319async fn test_signature_help(cx: &mut TestAppContext) {
11320 init_test(cx, |_| {});
11321 cx.update(|cx| {
11322 cx.update_global::<SettingsStore, _>(|settings, cx| {
11323 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11324 settings.auto_signature_help = Some(true);
11325 });
11326 });
11327 });
11328
11329 let mut cx = EditorLspTestContext::new_rust(
11330 lsp::ServerCapabilities {
11331 signature_help_provider: Some(lsp::SignatureHelpOptions {
11332 ..Default::default()
11333 }),
11334 ..Default::default()
11335 },
11336 cx,
11337 )
11338 .await;
11339
11340 // A test that directly calls `show_signature_help`
11341 cx.update_editor(|editor, window, cx| {
11342 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11343 });
11344
11345 let mocked_response = lsp::SignatureHelp {
11346 signatures: vec![lsp::SignatureInformation {
11347 label: "fn sample(param1: u8, param2: u8)".to_string(),
11348 documentation: None,
11349 parameters: Some(vec![
11350 lsp::ParameterInformation {
11351 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11352 documentation: None,
11353 },
11354 lsp::ParameterInformation {
11355 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11356 documentation: None,
11357 },
11358 ]),
11359 active_parameter: None,
11360 }],
11361 active_signature: Some(0),
11362 active_parameter: Some(0),
11363 };
11364 handle_signature_help_request(&mut cx, mocked_response).await;
11365
11366 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11367 .await;
11368
11369 cx.editor(|editor, _, _| {
11370 let signature_help_state = editor.signature_help_state.popover().cloned();
11371 assert!(signature_help_state.is_some());
11372 let signature = signature_help_state.unwrap();
11373 assert_eq!(
11374 signature.signatures[signature.current_signature].label,
11375 "fn sample(param1: u8, param2: u8)"
11376 );
11377 });
11378
11379 // When exiting outside from inside the brackets, `signature_help` is closed.
11380 cx.set_state(indoc! {"
11381 fn main() {
11382 sample(ˇ);
11383 }
11384
11385 fn sample(param1: u8, param2: u8) {}
11386 "});
11387
11388 cx.update_editor(|editor, window, cx| {
11389 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11390 s.select_ranges([0..0])
11391 });
11392 });
11393
11394 let mocked_response = lsp::SignatureHelp {
11395 signatures: Vec::new(),
11396 active_signature: None,
11397 active_parameter: None,
11398 };
11399 handle_signature_help_request(&mut cx, mocked_response).await;
11400
11401 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11402 .await;
11403
11404 cx.editor(|editor, _, _| {
11405 assert!(!editor.signature_help_state.is_shown());
11406 });
11407
11408 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
11409 cx.set_state(indoc! {"
11410 fn main() {
11411 sample(ˇ);
11412 }
11413
11414 fn sample(param1: u8, param2: u8) {}
11415 "});
11416
11417 let mocked_response = lsp::SignatureHelp {
11418 signatures: vec![lsp::SignatureInformation {
11419 label: "fn sample(param1: u8, param2: u8)".to_string(),
11420 documentation: None,
11421 parameters: Some(vec![
11422 lsp::ParameterInformation {
11423 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11424 documentation: None,
11425 },
11426 lsp::ParameterInformation {
11427 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11428 documentation: None,
11429 },
11430 ]),
11431 active_parameter: None,
11432 }],
11433 active_signature: Some(0),
11434 active_parameter: Some(0),
11435 };
11436 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11437 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11438 .await;
11439 cx.editor(|editor, _, _| {
11440 assert!(editor.signature_help_state.is_shown());
11441 });
11442
11443 // Restore the popover with more parameter input
11444 cx.set_state(indoc! {"
11445 fn main() {
11446 sample(param1, param2ˇ);
11447 }
11448
11449 fn sample(param1: u8, param2: u8) {}
11450 "});
11451
11452 let mocked_response = lsp::SignatureHelp {
11453 signatures: vec![lsp::SignatureInformation {
11454 label: "fn sample(param1: u8, param2: u8)".to_string(),
11455 documentation: None,
11456 parameters: Some(vec![
11457 lsp::ParameterInformation {
11458 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11459 documentation: None,
11460 },
11461 lsp::ParameterInformation {
11462 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11463 documentation: None,
11464 },
11465 ]),
11466 active_parameter: None,
11467 }],
11468 active_signature: Some(0),
11469 active_parameter: Some(1),
11470 };
11471 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11472 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11473 .await;
11474
11475 // When selecting a range, the popover is gone.
11476 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
11477 cx.update_editor(|editor, window, cx| {
11478 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11479 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11480 })
11481 });
11482 cx.assert_editor_state(indoc! {"
11483 fn main() {
11484 sample(param1, «ˇparam2»);
11485 }
11486
11487 fn sample(param1: u8, param2: u8) {}
11488 "});
11489 cx.editor(|editor, _, _| {
11490 assert!(!editor.signature_help_state.is_shown());
11491 });
11492
11493 // When unselecting again, the popover is back if within the brackets.
11494 cx.update_editor(|editor, window, cx| {
11495 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11496 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11497 })
11498 });
11499 cx.assert_editor_state(indoc! {"
11500 fn main() {
11501 sample(param1, ˇparam2);
11502 }
11503
11504 fn sample(param1: u8, param2: u8) {}
11505 "});
11506 handle_signature_help_request(&mut cx, mocked_response).await;
11507 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11508 .await;
11509 cx.editor(|editor, _, _| {
11510 assert!(editor.signature_help_state.is_shown());
11511 });
11512
11513 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
11514 cx.update_editor(|editor, window, cx| {
11515 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11516 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
11517 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11518 })
11519 });
11520 cx.assert_editor_state(indoc! {"
11521 fn main() {
11522 sample(param1, ˇparam2);
11523 }
11524
11525 fn sample(param1: u8, param2: u8) {}
11526 "});
11527
11528 let mocked_response = lsp::SignatureHelp {
11529 signatures: vec![lsp::SignatureInformation {
11530 label: "fn sample(param1: u8, param2: u8)".to_string(),
11531 documentation: None,
11532 parameters: Some(vec![
11533 lsp::ParameterInformation {
11534 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11535 documentation: None,
11536 },
11537 lsp::ParameterInformation {
11538 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11539 documentation: None,
11540 },
11541 ]),
11542 active_parameter: None,
11543 }],
11544 active_signature: Some(0),
11545 active_parameter: Some(1),
11546 };
11547 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11548 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11549 .await;
11550 cx.update_editor(|editor, _, cx| {
11551 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
11552 });
11553 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11554 .await;
11555 cx.update_editor(|editor, window, cx| {
11556 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11557 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11558 })
11559 });
11560 cx.assert_editor_state(indoc! {"
11561 fn main() {
11562 sample(param1, «ˇparam2»);
11563 }
11564
11565 fn sample(param1: u8, param2: u8) {}
11566 "});
11567 cx.update_editor(|editor, window, cx| {
11568 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11569 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11570 })
11571 });
11572 cx.assert_editor_state(indoc! {"
11573 fn main() {
11574 sample(param1, ˇparam2);
11575 }
11576
11577 fn sample(param1: u8, param2: u8) {}
11578 "});
11579 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
11580 .await;
11581}
11582
11583#[gpui::test]
11584async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
11585 init_test(cx, |_| {});
11586
11587 let mut cx = EditorLspTestContext::new_rust(
11588 lsp::ServerCapabilities {
11589 signature_help_provider: Some(lsp::SignatureHelpOptions {
11590 ..Default::default()
11591 }),
11592 ..Default::default()
11593 },
11594 cx,
11595 )
11596 .await;
11597
11598 cx.set_state(indoc! {"
11599 fn main() {
11600 overloadedˇ
11601 }
11602 "});
11603
11604 cx.update_editor(|editor, window, cx| {
11605 editor.handle_input("(", window, cx);
11606 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11607 });
11608
11609 // Mock response with 3 signatures
11610 let mocked_response = lsp::SignatureHelp {
11611 signatures: vec![
11612 lsp::SignatureInformation {
11613 label: "fn overloaded(x: i32)".to_string(),
11614 documentation: None,
11615 parameters: Some(vec![lsp::ParameterInformation {
11616 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11617 documentation: None,
11618 }]),
11619 active_parameter: None,
11620 },
11621 lsp::SignatureInformation {
11622 label: "fn overloaded(x: i32, y: i32)".to_string(),
11623 documentation: None,
11624 parameters: Some(vec![
11625 lsp::ParameterInformation {
11626 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11627 documentation: None,
11628 },
11629 lsp::ParameterInformation {
11630 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
11631 documentation: None,
11632 },
11633 ]),
11634 active_parameter: None,
11635 },
11636 lsp::SignatureInformation {
11637 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
11638 documentation: None,
11639 parameters: Some(vec![
11640 lsp::ParameterInformation {
11641 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11642 documentation: None,
11643 },
11644 lsp::ParameterInformation {
11645 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
11646 documentation: None,
11647 },
11648 lsp::ParameterInformation {
11649 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
11650 documentation: None,
11651 },
11652 ]),
11653 active_parameter: None,
11654 },
11655 ],
11656 active_signature: Some(1),
11657 active_parameter: Some(0),
11658 };
11659 handle_signature_help_request(&mut cx, mocked_response).await;
11660
11661 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11662 .await;
11663
11664 // Verify we have multiple signatures and the right one is selected
11665 cx.editor(|editor, _, _| {
11666 let popover = editor.signature_help_state.popover().cloned().unwrap();
11667 assert_eq!(popover.signatures.len(), 3);
11668 // active_signature was 1, so that should be the current
11669 assert_eq!(popover.current_signature, 1);
11670 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
11671 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
11672 assert_eq!(
11673 popover.signatures[2].label,
11674 "fn overloaded(x: i32, y: i32, z: i32)"
11675 );
11676 });
11677
11678 // Test navigation functionality
11679 cx.update_editor(|editor, window, cx| {
11680 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
11681 });
11682
11683 cx.editor(|editor, _, _| {
11684 let popover = editor.signature_help_state.popover().cloned().unwrap();
11685 assert_eq!(popover.current_signature, 2);
11686 });
11687
11688 // Test wrap around
11689 cx.update_editor(|editor, window, cx| {
11690 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
11691 });
11692
11693 cx.editor(|editor, _, _| {
11694 let popover = editor.signature_help_state.popover().cloned().unwrap();
11695 assert_eq!(popover.current_signature, 0);
11696 });
11697
11698 // Test previous navigation
11699 cx.update_editor(|editor, window, cx| {
11700 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
11701 });
11702
11703 cx.editor(|editor, _, _| {
11704 let popover = editor.signature_help_state.popover().cloned().unwrap();
11705 assert_eq!(popover.current_signature, 2);
11706 });
11707}
11708
11709#[gpui::test]
11710async fn test_completion_mode(cx: &mut TestAppContext) {
11711 init_test(cx, |_| {});
11712 let mut cx = EditorLspTestContext::new_rust(
11713 lsp::ServerCapabilities {
11714 completion_provider: Some(lsp::CompletionOptions {
11715 resolve_provider: Some(true),
11716 ..Default::default()
11717 }),
11718 ..Default::default()
11719 },
11720 cx,
11721 )
11722 .await;
11723
11724 struct Run {
11725 run_description: &'static str,
11726 initial_state: String,
11727 buffer_marked_text: String,
11728 completion_label: &'static str,
11729 completion_text: &'static str,
11730 expected_with_insert_mode: String,
11731 expected_with_replace_mode: String,
11732 expected_with_replace_subsequence_mode: String,
11733 expected_with_replace_suffix_mode: String,
11734 }
11735
11736 let runs = [
11737 Run {
11738 run_description: "Start of word matches completion text",
11739 initial_state: "before ediˇ after".into(),
11740 buffer_marked_text: "before <edi|> after".into(),
11741 completion_label: "editor",
11742 completion_text: "editor",
11743 expected_with_insert_mode: "before editorˇ after".into(),
11744 expected_with_replace_mode: "before editorˇ after".into(),
11745 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11746 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11747 },
11748 Run {
11749 run_description: "Accept same text at the middle of the word",
11750 initial_state: "before ediˇtor after".into(),
11751 buffer_marked_text: "before <edi|tor> after".into(),
11752 completion_label: "editor",
11753 completion_text: "editor",
11754 expected_with_insert_mode: "before editorˇtor after".into(),
11755 expected_with_replace_mode: "before editorˇ after".into(),
11756 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11757 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11758 },
11759 Run {
11760 run_description: "End of word matches completion text -- cursor at end",
11761 initial_state: "before torˇ after".into(),
11762 buffer_marked_text: "before <tor|> after".into(),
11763 completion_label: "editor",
11764 completion_text: "editor",
11765 expected_with_insert_mode: "before editorˇ after".into(),
11766 expected_with_replace_mode: "before editorˇ after".into(),
11767 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11768 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11769 },
11770 Run {
11771 run_description: "End of word matches completion text -- cursor at start",
11772 initial_state: "before ˇtor after".into(),
11773 buffer_marked_text: "before <|tor> after".into(),
11774 completion_label: "editor",
11775 completion_text: "editor",
11776 expected_with_insert_mode: "before editorˇtor after".into(),
11777 expected_with_replace_mode: "before editorˇ after".into(),
11778 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11779 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11780 },
11781 Run {
11782 run_description: "Prepend text containing whitespace",
11783 initial_state: "pˇfield: bool".into(),
11784 buffer_marked_text: "<p|field>: bool".into(),
11785 completion_label: "pub ",
11786 completion_text: "pub ",
11787 expected_with_insert_mode: "pub ˇfield: bool".into(),
11788 expected_with_replace_mode: "pub ˇ: bool".into(),
11789 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
11790 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
11791 },
11792 Run {
11793 run_description: "Add element to start of list",
11794 initial_state: "[element_ˇelement_2]".into(),
11795 buffer_marked_text: "[<element_|element_2>]".into(),
11796 completion_label: "element_1",
11797 completion_text: "element_1",
11798 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
11799 expected_with_replace_mode: "[element_1ˇ]".into(),
11800 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
11801 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
11802 },
11803 Run {
11804 run_description: "Add element to start of list -- first and second elements are equal",
11805 initial_state: "[elˇelement]".into(),
11806 buffer_marked_text: "[<el|element>]".into(),
11807 completion_label: "element",
11808 completion_text: "element",
11809 expected_with_insert_mode: "[elementˇelement]".into(),
11810 expected_with_replace_mode: "[elementˇ]".into(),
11811 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
11812 expected_with_replace_suffix_mode: "[elementˇ]".into(),
11813 },
11814 Run {
11815 run_description: "Ends with matching suffix",
11816 initial_state: "SubˇError".into(),
11817 buffer_marked_text: "<Sub|Error>".into(),
11818 completion_label: "SubscriptionError",
11819 completion_text: "SubscriptionError",
11820 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
11821 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11822 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11823 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
11824 },
11825 Run {
11826 run_description: "Suffix is a subsequence -- contiguous",
11827 initial_state: "SubˇErr".into(),
11828 buffer_marked_text: "<Sub|Err>".into(),
11829 completion_label: "SubscriptionError",
11830 completion_text: "SubscriptionError",
11831 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
11832 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11833 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11834 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
11835 },
11836 Run {
11837 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
11838 initial_state: "Suˇscrirr".into(),
11839 buffer_marked_text: "<Su|scrirr>".into(),
11840 completion_label: "SubscriptionError",
11841 completion_text: "SubscriptionError",
11842 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
11843 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11844 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11845 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
11846 },
11847 Run {
11848 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
11849 initial_state: "foo(indˇix)".into(),
11850 buffer_marked_text: "foo(<ind|ix>)".into(),
11851 completion_label: "node_index",
11852 completion_text: "node_index",
11853 expected_with_insert_mode: "foo(node_indexˇix)".into(),
11854 expected_with_replace_mode: "foo(node_indexˇ)".into(),
11855 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
11856 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
11857 },
11858 Run {
11859 run_description: "Replace range ends before cursor - should extend to cursor",
11860 initial_state: "before editˇo after".into(),
11861 buffer_marked_text: "before <{ed}>it|o after".into(),
11862 completion_label: "editor",
11863 completion_text: "editor",
11864 expected_with_insert_mode: "before editorˇo after".into(),
11865 expected_with_replace_mode: "before editorˇo after".into(),
11866 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
11867 expected_with_replace_suffix_mode: "before editorˇo after".into(),
11868 },
11869 Run {
11870 run_description: "Uses label for suffix matching",
11871 initial_state: "before ediˇtor after".into(),
11872 buffer_marked_text: "before <edi|tor> after".into(),
11873 completion_label: "editor",
11874 completion_text: "editor()",
11875 expected_with_insert_mode: "before editor()ˇtor after".into(),
11876 expected_with_replace_mode: "before editor()ˇ after".into(),
11877 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
11878 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
11879 },
11880 Run {
11881 run_description: "Case insensitive subsequence and suffix matching",
11882 initial_state: "before EDiˇtoR after".into(),
11883 buffer_marked_text: "before <EDi|toR> after".into(),
11884 completion_label: "editor",
11885 completion_text: "editor",
11886 expected_with_insert_mode: "before editorˇtoR after".into(),
11887 expected_with_replace_mode: "before editorˇ after".into(),
11888 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11889 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11890 },
11891 ];
11892
11893 for run in runs {
11894 let run_variations = [
11895 (LspInsertMode::Insert, run.expected_with_insert_mode),
11896 (LspInsertMode::Replace, run.expected_with_replace_mode),
11897 (
11898 LspInsertMode::ReplaceSubsequence,
11899 run.expected_with_replace_subsequence_mode,
11900 ),
11901 (
11902 LspInsertMode::ReplaceSuffix,
11903 run.expected_with_replace_suffix_mode,
11904 ),
11905 ];
11906
11907 for (lsp_insert_mode, expected_text) in run_variations {
11908 eprintln!(
11909 "run = {:?}, mode = {lsp_insert_mode:.?}",
11910 run.run_description,
11911 );
11912
11913 update_test_language_settings(&mut cx, |settings| {
11914 settings.defaults.completions = Some(CompletionSettings {
11915 lsp_insert_mode,
11916 words: WordsCompletionMode::Disabled,
11917 lsp: true,
11918 lsp_fetch_timeout_ms: 0,
11919 });
11920 });
11921
11922 cx.set_state(&run.initial_state);
11923 cx.update_editor(|editor, window, cx| {
11924 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11925 });
11926
11927 let counter = Arc::new(AtomicUsize::new(0));
11928 handle_completion_request_with_insert_and_replace(
11929 &mut cx,
11930 &run.buffer_marked_text,
11931 vec![(run.completion_label, run.completion_text)],
11932 counter.clone(),
11933 )
11934 .await;
11935 cx.condition(|editor, _| editor.context_menu_visible())
11936 .await;
11937 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11938
11939 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11940 editor
11941 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11942 .unwrap()
11943 });
11944 cx.assert_editor_state(&expected_text);
11945 handle_resolve_completion_request(&mut cx, None).await;
11946 apply_additional_edits.await.unwrap();
11947 }
11948 }
11949}
11950
11951#[gpui::test]
11952async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
11953 init_test(cx, |_| {});
11954 let mut cx = EditorLspTestContext::new_rust(
11955 lsp::ServerCapabilities {
11956 completion_provider: Some(lsp::CompletionOptions {
11957 resolve_provider: Some(true),
11958 ..Default::default()
11959 }),
11960 ..Default::default()
11961 },
11962 cx,
11963 )
11964 .await;
11965
11966 let initial_state = "SubˇError";
11967 let buffer_marked_text = "<Sub|Error>";
11968 let completion_text = "SubscriptionError";
11969 let expected_with_insert_mode = "SubscriptionErrorˇError";
11970 let expected_with_replace_mode = "SubscriptionErrorˇ";
11971
11972 update_test_language_settings(&mut cx, |settings| {
11973 settings.defaults.completions = Some(CompletionSettings {
11974 words: WordsCompletionMode::Disabled,
11975 // set the opposite here to ensure that the action is overriding the default behavior
11976 lsp_insert_mode: LspInsertMode::Insert,
11977 lsp: true,
11978 lsp_fetch_timeout_ms: 0,
11979 });
11980 });
11981
11982 cx.set_state(initial_state);
11983 cx.update_editor(|editor, window, cx| {
11984 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11985 });
11986
11987 let counter = Arc::new(AtomicUsize::new(0));
11988 handle_completion_request_with_insert_and_replace(
11989 &mut cx,
11990 &buffer_marked_text,
11991 vec![(completion_text, completion_text)],
11992 counter.clone(),
11993 )
11994 .await;
11995 cx.condition(|editor, _| editor.context_menu_visible())
11996 .await;
11997 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11998
11999 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12000 editor
12001 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12002 .unwrap()
12003 });
12004 cx.assert_editor_state(&expected_with_replace_mode);
12005 handle_resolve_completion_request(&mut cx, None).await;
12006 apply_additional_edits.await.unwrap();
12007
12008 update_test_language_settings(&mut cx, |settings| {
12009 settings.defaults.completions = Some(CompletionSettings {
12010 words: WordsCompletionMode::Disabled,
12011 // set the opposite here to ensure that the action is overriding the default behavior
12012 lsp_insert_mode: LspInsertMode::Replace,
12013 lsp: true,
12014 lsp_fetch_timeout_ms: 0,
12015 });
12016 });
12017
12018 cx.set_state(initial_state);
12019 cx.update_editor(|editor, window, cx| {
12020 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12021 });
12022 handle_completion_request_with_insert_and_replace(
12023 &mut cx,
12024 &buffer_marked_text,
12025 vec![(completion_text, completion_text)],
12026 counter.clone(),
12027 )
12028 .await;
12029 cx.condition(|editor, _| editor.context_menu_visible())
12030 .await;
12031 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12032
12033 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12034 editor
12035 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
12036 .unwrap()
12037 });
12038 cx.assert_editor_state(&expected_with_insert_mode);
12039 handle_resolve_completion_request(&mut cx, None).await;
12040 apply_additional_edits.await.unwrap();
12041}
12042
12043#[gpui::test]
12044async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
12045 init_test(cx, |_| {});
12046 let mut cx = EditorLspTestContext::new_rust(
12047 lsp::ServerCapabilities {
12048 completion_provider: Some(lsp::CompletionOptions {
12049 resolve_provider: Some(true),
12050 ..Default::default()
12051 }),
12052 ..Default::default()
12053 },
12054 cx,
12055 )
12056 .await;
12057
12058 // scenario: surrounding text matches completion text
12059 let completion_text = "to_offset";
12060 let initial_state = indoc! {"
12061 1. buf.to_offˇsuffix
12062 2. buf.to_offˇsuf
12063 3. buf.to_offˇfix
12064 4. buf.to_offˇ
12065 5. into_offˇensive
12066 6. ˇsuffix
12067 7. let ˇ //
12068 8. aaˇzz
12069 9. buf.to_off«zzzzzˇ»suffix
12070 10. buf.«ˇzzzzz»suffix
12071 11. to_off«ˇzzzzz»
12072
12073 buf.to_offˇsuffix // newest cursor
12074 "};
12075 let completion_marked_buffer = indoc! {"
12076 1. buf.to_offsuffix
12077 2. buf.to_offsuf
12078 3. buf.to_offfix
12079 4. buf.to_off
12080 5. into_offensive
12081 6. suffix
12082 7. let //
12083 8. aazz
12084 9. buf.to_offzzzzzsuffix
12085 10. buf.zzzzzsuffix
12086 11. to_offzzzzz
12087
12088 buf.<to_off|suffix> // newest cursor
12089 "};
12090 let expected = indoc! {"
12091 1. buf.to_offsetˇ
12092 2. buf.to_offsetˇsuf
12093 3. buf.to_offsetˇfix
12094 4. buf.to_offsetˇ
12095 5. into_offsetˇensive
12096 6. to_offsetˇsuffix
12097 7. let to_offsetˇ //
12098 8. aato_offsetˇzz
12099 9. buf.to_offsetˇ
12100 10. buf.to_offsetˇsuffix
12101 11. to_offsetˇ
12102
12103 buf.to_offsetˇ // newest cursor
12104 "};
12105 cx.set_state(initial_state);
12106 cx.update_editor(|editor, window, cx| {
12107 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12108 });
12109 handle_completion_request_with_insert_and_replace(
12110 &mut cx,
12111 completion_marked_buffer,
12112 vec![(completion_text, completion_text)],
12113 Arc::new(AtomicUsize::new(0)),
12114 )
12115 .await;
12116 cx.condition(|editor, _| editor.context_menu_visible())
12117 .await;
12118 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12119 editor
12120 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12121 .unwrap()
12122 });
12123 cx.assert_editor_state(expected);
12124 handle_resolve_completion_request(&mut cx, None).await;
12125 apply_additional_edits.await.unwrap();
12126
12127 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
12128 let completion_text = "foo_and_bar";
12129 let initial_state = indoc! {"
12130 1. ooanbˇ
12131 2. zooanbˇ
12132 3. ooanbˇz
12133 4. zooanbˇz
12134 5. ooanˇ
12135 6. oanbˇ
12136
12137 ooanbˇ
12138 "};
12139 let completion_marked_buffer = indoc! {"
12140 1. ooanb
12141 2. zooanb
12142 3. ooanbz
12143 4. zooanbz
12144 5. ooan
12145 6. oanb
12146
12147 <ooanb|>
12148 "};
12149 let expected = indoc! {"
12150 1. foo_and_barˇ
12151 2. zfoo_and_barˇ
12152 3. foo_and_barˇz
12153 4. zfoo_and_barˇz
12154 5. ooanfoo_and_barˇ
12155 6. oanbfoo_and_barˇ
12156
12157 foo_and_barˇ
12158 "};
12159 cx.set_state(initial_state);
12160 cx.update_editor(|editor, window, cx| {
12161 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12162 });
12163 handle_completion_request_with_insert_and_replace(
12164 &mut cx,
12165 completion_marked_buffer,
12166 vec![(completion_text, completion_text)],
12167 Arc::new(AtomicUsize::new(0)),
12168 )
12169 .await;
12170 cx.condition(|editor, _| editor.context_menu_visible())
12171 .await;
12172 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12173 editor
12174 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12175 .unwrap()
12176 });
12177 cx.assert_editor_state(expected);
12178 handle_resolve_completion_request(&mut cx, None).await;
12179 apply_additional_edits.await.unwrap();
12180
12181 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
12182 // (expects the same as if it was inserted at the end)
12183 let completion_text = "foo_and_bar";
12184 let initial_state = indoc! {"
12185 1. ooˇanb
12186 2. zooˇanb
12187 3. ooˇanbz
12188 4. zooˇanbz
12189
12190 ooˇanb
12191 "};
12192 let completion_marked_buffer = indoc! {"
12193 1. ooanb
12194 2. zooanb
12195 3. ooanbz
12196 4. zooanbz
12197
12198 <oo|anb>
12199 "};
12200 let expected = indoc! {"
12201 1. foo_and_barˇ
12202 2. zfoo_and_barˇ
12203 3. foo_and_barˇz
12204 4. zfoo_and_barˇz
12205
12206 foo_and_barˇ
12207 "};
12208 cx.set_state(initial_state);
12209 cx.update_editor(|editor, window, cx| {
12210 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12211 });
12212 handle_completion_request_with_insert_and_replace(
12213 &mut cx,
12214 completion_marked_buffer,
12215 vec![(completion_text, completion_text)],
12216 Arc::new(AtomicUsize::new(0)),
12217 )
12218 .await;
12219 cx.condition(|editor, _| editor.context_menu_visible())
12220 .await;
12221 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12222 editor
12223 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12224 .unwrap()
12225 });
12226 cx.assert_editor_state(expected);
12227 handle_resolve_completion_request(&mut cx, None).await;
12228 apply_additional_edits.await.unwrap();
12229}
12230
12231// This used to crash
12232#[gpui::test]
12233async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
12234 init_test(cx, |_| {});
12235
12236 let buffer_text = indoc! {"
12237 fn main() {
12238 10.satu;
12239
12240 //
12241 // separate cursors so they open in different excerpts (manually reproducible)
12242 //
12243
12244 10.satu20;
12245 }
12246 "};
12247 let multibuffer_text_with_selections = indoc! {"
12248 fn main() {
12249 10.satuˇ;
12250
12251 //
12252
12253 //
12254
12255 10.satuˇ20;
12256 }
12257 "};
12258 let expected_multibuffer = indoc! {"
12259 fn main() {
12260 10.saturating_sub()ˇ;
12261
12262 //
12263
12264 //
12265
12266 10.saturating_sub()ˇ;
12267 }
12268 "};
12269
12270 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
12271 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
12272
12273 let fs = FakeFs::new(cx.executor());
12274 fs.insert_tree(
12275 path!("/a"),
12276 json!({
12277 "main.rs": buffer_text,
12278 }),
12279 )
12280 .await;
12281
12282 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12283 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12284 language_registry.add(rust_lang());
12285 let mut fake_servers = language_registry.register_fake_lsp(
12286 "Rust",
12287 FakeLspAdapter {
12288 capabilities: lsp::ServerCapabilities {
12289 completion_provider: Some(lsp::CompletionOptions {
12290 resolve_provider: None,
12291 ..lsp::CompletionOptions::default()
12292 }),
12293 ..lsp::ServerCapabilities::default()
12294 },
12295 ..FakeLspAdapter::default()
12296 },
12297 );
12298 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12299 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12300 let buffer = project
12301 .update(cx, |project, cx| {
12302 project.open_local_buffer(path!("/a/main.rs"), cx)
12303 })
12304 .await
12305 .unwrap();
12306
12307 let multi_buffer = cx.new(|cx| {
12308 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
12309 multi_buffer.push_excerpts(
12310 buffer.clone(),
12311 [ExcerptRange::new(0..first_excerpt_end)],
12312 cx,
12313 );
12314 multi_buffer.push_excerpts(
12315 buffer.clone(),
12316 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
12317 cx,
12318 );
12319 multi_buffer
12320 });
12321
12322 let editor = workspace
12323 .update(cx, |_, window, cx| {
12324 cx.new(|cx| {
12325 Editor::new(
12326 EditorMode::Full {
12327 scale_ui_elements_with_buffer_font_size: false,
12328 show_active_line_background: false,
12329 sized_by_content: false,
12330 },
12331 multi_buffer.clone(),
12332 Some(project.clone()),
12333 window,
12334 cx,
12335 )
12336 })
12337 })
12338 .unwrap();
12339
12340 let pane = workspace
12341 .update(cx, |workspace, _, _| workspace.active_pane().clone())
12342 .unwrap();
12343 pane.update_in(cx, |pane, window, cx| {
12344 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
12345 });
12346
12347 let fake_server = fake_servers.next().await.unwrap();
12348
12349 editor.update_in(cx, |editor, window, cx| {
12350 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12351 s.select_ranges([
12352 Point::new(1, 11)..Point::new(1, 11),
12353 Point::new(7, 11)..Point::new(7, 11),
12354 ])
12355 });
12356
12357 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
12358 });
12359
12360 editor.update_in(cx, |editor, window, cx| {
12361 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12362 });
12363
12364 fake_server
12365 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12366 let completion_item = lsp::CompletionItem {
12367 label: "saturating_sub()".into(),
12368 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
12369 lsp::InsertReplaceEdit {
12370 new_text: "saturating_sub()".to_owned(),
12371 insert: lsp::Range::new(
12372 lsp::Position::new(7, 7),
12373 lsp::Position::new(7, 11),
12374 ),
12375 replace: lsp::Range::new(
12376 lsp::Position::new(7, 7),
12377 lsp::Position::new(7, 13),
12378 ),
12379 },
12380 )),
12381 ..lsp::CompletionItem::default()
12382 };
12383
12384 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
12385 })
12386 .next()
12387 .await
12388 .unwrap();
12389
12390 cx.condition(&editor, |editor, _| editor.context_menu_visible())
12391 .await;
12392
12393 editor
12394 .update_in(cx, |editor, window, cx| {
12395 editor
12396 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12397 .unwrap()
12398 })
12399 .await
12400 .unwrap();
12401
12402 editor.update(cx, |editor, cx| {
12403 assert_text_with_selections(editor, expected_multibuffer, cx);
12404 })
12405}
12406
12407#[gpui::test]
12408async fn test_completion(cx: &mut TestAppContext) {
12409 init_test(cx, |_| {});
12410
12411 let mut cx = EditorLspTestContext::new_rust(
12412 lsp::ServerCapabilities {
12413 completion_provider: Some(lsp::CompletionOptions {
12414 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12415 resolve_provider: Some(true),
12416 ..Default::default()
12417 }),
12418 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12419 ..Default::default()
12420 },
12421 cx,
12422 )
12423 .await;
12424 let counter = Arc::new(AtomicUsize::new(0));
12425
12426 cx.set_state(indoc! {"
12427 oneˇ
12428 two
12429 three
12430 "});
12431 cx.simulate_keystroke(".");
12432 handle_completion_request(
12433 indoc! {"
12434 one.|<>
12435 two
12436 three
12437 "},
12438 vec!["first_completion", "second_completion"],
12439 true,
12440 counter.clone(),
12441 &mut cx,
12442 )
12443 .await;
12444 cx.condition(|editor, _| editor.context_menu_visible())
12445 .await;
12446 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12447
12448 let _handler = handle_signature_help_request(
12449 &mut cx,
12450 lsp::SignatureHelp {
12451 signatures: vec![lsp::SignatureInformation {
12452 label: "test signature".to_string(),
12453 documentation: None,
12454 parameters: Some(vec![lsp::ParameterInformation {
12455 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
12456 documentation: None,
12457 }]),
12458 active_parameter: None,
12459 }],
12460 active_signature: None,
12461 active_parameter: None,
12462 },
12463 );
12464 cx.update_editor(|editor, window, cx| {
12465 assert!(
12466 !editor.signature_help_state.is_shown(),
12467 "No signature help was called for"
12468 );
12469 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12470 });
12471 cx.run_until_parked();
12472 cx.update_editor(|editor, _, _| {
12473 assert!(
12474 !editor.signature_help_state.is_shown(),
12475 "No signature help should be shown when completions menu is open"
12476 );
12477 });
12478
12479 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12480 editor.context_menu_next(&Default::default(), window, cx);
12481 editor
12482 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12483 .unwrap()
12484 });
12485 cx.assert_editor_state(indoc! {"
12486 one.second_completionˇ
12487 two
12488 three
12489 "});
12490
12491 handle_resolve_completion_request(
12492 &mut cx,
12493 Some(vec![
12494 (
12495 //This overlaps with the primary completion edit which is
12496 //misbehavior from the LSP spec, test that we filter it out
12497 indoc! {"
12498 one.second_ˇcompletion
12499 two
12500 threeˇ
12501 "},
12502 "overlapping additional edit",
12503 ),
12504 (
12505 indoc! {"
12506 one.second_completion
12507 two
12508 threeˇ
12509 "},
12510 "\nadditional edit",
12511 ),
12512 ]),
12513 )
12514 .await;
12515 apply_additional_edits.await.unwrap();
12516 cx.assert_editor_state(indoc! {"
12517 one.second_completionˇ
12518 two
12519 three
12520 additional edit
12521 "});
12522
12523 cx.set_state(indoc! {"
12524 one.second_completion
12525 twoˇ
12526 threeˇ
12527 additional edit
12528 "});
12529 cx.simulate_keystroke(" ");
12530 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12531 cx.simulate_keystroke("s");
12532 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12533
12534 cx.assert_editor_state(indoc! {"
12535 one.second_completion
12536 two sˇ
12537 three sˇ
12538 additional edit
12539 "});
12540 handle_completion_request(
12541 indoc! {"
12542 one.second_completion
12543 two s
12544 three <s|>
12545 additional edit
12546 "},
12547 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12548 true,
12549 counter.clone(),
12550 &mut cx,
12551 )
12552 .await;
12553 cx.condition(|editor, _| editor.context_menu_visible())
12554 .await;
12555 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12556
12557 cx.simulate_keystroke("i");
12558
12559 handle_completion_request(
12560 indoc! {"
12561 one.second_completion
12562 two si
12563 three <si|>
12564 additional edit
12565 "},
12566 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12567 true,
12568 counter.clone(),
12569 &mut cx,
12570 )
12571 .await;
12572 cx.condition(|editor, _| editor.context_menu_visible())
12573 .await;
12574 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12575
12576 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12577 editor
12578 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12579 .unwrap()
12580 });
12581 cx.assert_editor_state(indoc! {"
12582 one.second_completion
12583 two sixth_completionˇ
12584 three sixth_completionˇ
12585 additional edit
12586 "});
12587
12588 apply_additional_edits.await.unwrap();
12589
12590 update_test_language_settings(&mut cx, |settings| {
12591 settings.defaults.show_completions_on_input = Some(false);
12592 });
12593 cx.set_state("editorˇ");
12594 cx.simulate_keystroke(".");
12595 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12596 cx.simulate_keystrokes("c l o");
12597 cx.assert_editor_state("editor.cloˇ");
12598 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12599 cx.update_editor(|editor, window, cx| {
12600 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12601 });
12602 handle_completion_request(
12603 "editor.<clo|>",
12604 vec!["close", "clobber"],
12605 true,
12606 counter.clone(),
12607 &mut cx,
12608 )
12609 .await;
12610 cx.condition(|editor, _| editor.context_menu_visible())
12611 .await;
12612 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
12613
12614 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12615 editor
12616 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12617 .unwrap()
12618 });
12619 cx.assert_editor_state("editor.clobberˇ");
12620 handle_resolve_completion_request(&mut cx, None).await;
12621 apply_additional_edits.await.unwrap();
12622}
12623
12624#[gpui::test]
12625async fn test_completion_reuse(cx: &mut TestAppContext) {
12626 init_test(cx, |_| {});
12627
12628 let mut cx = EditorLspTestContext::new_rust(
12629 lsp::ServerCapabilities {
12630 completion_provider: Some(lsp::CompletionOptions {
12631 trigger_characters: Some(vec![".".to_string()]),
12632 ..Default::default()
12633 }),
12634 ..Default::default()
12635 },
12636 cx,
12637 )
12638 .await;
12639
12640 let counter = Arc::new(AtomicUsize::new(0));
12641 cx.set_state("objˇ");
12642 cx.simulate_keystroke(".");
12643
12644 // Initial completion request returns complete results
12645 let is_incomplete = false;
12646 handle_completion_request(
12647 "obj.|<>",
12648 vec!["a", "ab", "abc"],
12649 is_incomplete,
12650 counter.clone(),
12651 &mut cx,
12652 )
12653 .await;
12654 cx.run_until_parked();
12655 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12656 cx.assert_editor_state("obj.ˇ");
12657 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12658
12659 // Type "a" - filters existing completions
12660 cx.simulate_keystroke("a");
12661 cx.run_until_parked();
12662 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12663 cx.assert_editor_state("obj.aˇ");
12664 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12665
12666 // Type "b" - filters existing completions
12667 cx.simulate_keystroke("b");
12668 cx.run_until_parked();
12669 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12670 cx.assert_editor_state("obj.abˇ");
12671 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12672
12673 // Type "c" - filters existing completions
12674 cx.simulate_keystroke("c");
12675 cx.run_until_parked();
12676 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12677 cx.assert_editor_state("obj.abcˇ");
12678 check_displayed_completions(vec!["abc"], &mut cx);
12679
12680 // Backspace to delete "c" - filters existing completions
12681 cx.update_editor(|editor, window, cx| {
12682 editor.backspace(&Backspace, window, cx);
12683 });
12684 cx.run_until_parked();
12685 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12686 cx.assert_editor_state("obj.abˇ");
12687 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12688
12689 // Moving cursor to the left dismisses menu.
12690 cx.update_editor(|editor, window, cx| {
12691 editor.move_left(&MoveLeft, window, cx);
12692 });
12693 cx.run_until_parked();
12694 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12695 cx.assert_editor_state("obj.aˇb");
12696 cx.update_editor(|editor, _, _| {
12697 assert_eq!(editor.context_menu_visible(), false);
12698 });
12699
12700 // Type "b" - new request
12701 cx.simulate_keystroke("b");
12702 let is_incomplete = false;
12703 handle_completion_request(
12704 "obj.<ab|>a",
12705 vec!["ab", "abc"],
12706 is_incomplete,
12707 counter.clone(),
12708 &mut cx,
12709 )
12710 .await;
12711 cx.run_until_parked();
12712 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12713 cx.assert_editor_state("obj.abˇb");
12714 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12715
12716 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
12717 cx.update_editor(|editor, window, cx| {
12718 editor.backspace(&Backspace, window, cx);
12719 });
12720 let is_incomplete = false;
12721 handle_completion_request(
12722 "obj.<a|>b",
12723 vec!["a", "ab", "abc"],
12724 is_incomplete,
12725 counter.clone(),
12726 &mut cx,
12727 )
12728 .await;
12729 cx.run_until_parked();
12730 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12731 cx.assert_editor_state("obj.aˇb");
12732 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12733
12734 // Backspace to delete "a" - dismisses menu.
12735 cx.update_editor(|editor, window, cx| {
12736 editor.backspace(&Backspace, window, cx);
12737 });
12738 cx.run_until_parked();
12739 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12740 cx.assert_editor_state("obj.ˇb");
12741 cx.update_editor(|editor, _, _| {
12742 assert_eq!(editor.context_menu_visible(), false);
12743 });
12744}
12745
12746#[gpui::test]
12747async fn test_word_completion(cx: &mut TestAppContext) {
12748 let lsp_fetch_timeout_ms = 10;
12749 init_test(cx, |language_settings| {
12750 language_settings.defaults.completions = Some(CompletionSettings {
12751 words: WordsCompletionMode::Fallback,
12752 lsp: true,
12753 lsp_fetch_timeout_ms: 10,
12754 lsp_insert_mode: LspInsertMode::Insert,
12755 });
12756 });
12757
12758 let mut cx = EditorLspTestContext::new_rust(
12759 lsp::ServerCapabilities {
12760 completion_provider: Some(lsp::CompletionOptions {
12761 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12762 ..lsp::CompletionOptions::default()
12763 }),
12764 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12765 ..lsp::ServerCapabilities::default()
12766 },
12767 cx,
12768 )
12769 .await;
12770
12771 let throttle_completions = Arc::new(AtomicBool::new(false));
12772
12773 let lsp_throttle_completions = throttle_completions.clone();
12774 let _completion_requests_handler =
12775 cx.lsp
12776 .server
12777 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
12778 let lsp_throttle_completions = lsp_throttle_completions.clone();
12779 let cx = cx.clone();
12780 async move {
12781 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
12782 cx.background_executor()
12783 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
12784 .await;
12785 }
12786 Ok(Some(lsp::CompletionResponse::Array(vec![
12787 lsp::CompletionItem {
12788 label: "first".into(),
12789 ..lsp::CompletionItem::default()
12790 },
12791 lsp::CompletionItem {
12792 label: "last".into(),
12793 ..lsp::CompletionItem::default()
12794 },
12795 ])))
12796 }
12797 });
12798
12799 cx.set_state(indoc! {"
12800 oneˇ
12801 two
12802 three
12803 "});
12804 cx.simulate_keystroke(".");
12805 cx.executor().run_until_parked();
12806 cx.condition(|editor, _| editor.context_menu_visible())
12807 .await;
12808 cx.update_editor(|editor, window, cx| {
12809 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12810 {
12811 assert_eq!(
12812 completion_menu_entries(&menu),
12813 &["first", "last"],
12814 "When LSP server is fast to reply, no fallback word completions are used"
12815 );
12816 } else {
12817 panic!("expected completion menu to be open");
12818 }
12819 editor.cancel(&Cancel, window, cx);
12820 });
12821 cx.executor().run_until_parked();
12822 cx.condition(|editor, _| !editor.context_menu_visible())
12823 .await;
12824
12825 throttle_completions.store(true, atomic::Ordering::Release);
12826 cx.simulate_keystroke(".");
12827 cx.executor()
12828 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
12829 cx.executor().run_until_parked();
12830 cx.condition(|editor, _| editor.context_menu_visible())
12831 .await;
12832 cx.update_editor(|editor, _, _| {
12833 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12834 {
12835 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
12836 "When LSP server is slow, document words can be shown instead, if configured accordingly");
12837 } else {
12838 panic!("expected completion menu to be open");
12839 }
12840 });
12841}
12842
12843#[gpui::test]
12844async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
12845 init_test(cx, |language_settings| {
12846 language_settings.defaults.completions = Some(CompletionSettings {
12847 words: WordsCompletionMode::Enabled,
12848 lsp: true,
12849 lsp_fetch_timeout_ms: 0,
12850 lsp_insert_mode: LspInsertMode::Insert,
12851 });
12852 });
12853
12854 let mut cx = EditorLspTestContext::new_rust(
12855 lsp::ServerCapabilities {
12856 completion_provider: Some(lsp::CompletionOptions {
12857 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12858 ..lsp::CompletionOptions::default()
12859 }),
12860 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12861 ..lsp::ServerCapabilities::default()
12862 },
12863 cx,
12864 )
12865 .await;
12866
12867 let _completion_requests_handler =
12868 cx.lsp
12869 .server
12870 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12871 Ok(Some(lsp::CompletionResponse::Array(vec![
12872 lsp::CompletionItem {
12873 label: "first".into(),
12874 ..lsp::CompletionItem::default()
12875 },
12876 lsp::CompletionItem {
12877 label: "last".into(),
12878 ..lsp::CompletionItem::default()
12879 },
12880 ])))
12881 });
12882
12883 cx.set_state(indoc! {"ˇ
12884 first
12885 last
12886 second
12887 "});
12888 cx.simulate_keystroke(".");
12889 cx.executor().run_until_parked();
12890 cx.condition(|editor, _| editor.context_menu_visible())
12891 .await;
12892 cx.update_editor(|editor, _, _| {
12893 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12894 {
12895 assert_eq!(
12896 completion_menu_entries(&menu),
12897 &["first", "last", "second"],
12898 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
12899 );
12900 } else {
12901 panic!("expected completion menu to be open");
12902 }
12903 });
12904}
12905
12906#[gpui::test]
12907async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
12908 init_test(cx, |language_settings| {
12909 language_settings.defaults.completions = Some(CompletionSettings {
12910 words: WordsCompletionMode::Disabled,
12911 lsp: true,
12912 lsp_fetch_timeout_ms: 0,
12913 lsp_insert_mode: LspInsertMode::Insert,
12914 });
12915 });
12916
12917 let mut cx = EditorLspTestContext::new_rust(
12918 lsp::ServerCapabilities {
12919 completion_provider: Some(lsp::CompletionOptions {
12920 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12921 ..lsp::CompletionOptions::default()
12922 }),
12923 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12924 ..lsp::ServerCapabilities::default()
12925 },
12926 cx,
12927 )
12928 .await;
12929
12930 let _completion_requests_handler =
12931 cx.lsp
12932 .server
12933 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12934 panic!("LSP completions should not be queried when dealing with word completions")
12935 });
12936
12937 cx.set_state(indoc! {"ˇ
12938 first
12939 last
12940 second
12941 "});
12942 cx.update_editor(|editor, window, cx| {
12943 editor.show_word_completions(&ShowWordCompletions, window, cx);
12944 });
12945 cx.executor().run_until_parked();
12946 cx.condition(|editor, _| editor.context_menu_visible())
12947 .await;
12948 cx.update_editor(|editor, _, _| {
12949 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12950 {
12951 assert_eq!(
12952 completion_menu_entries(&menu),
12953 &["first", "last", "second"],
12954 "`ShowWordCompletions` action should show word completions"
12955 );
12956 } else {
12957 panic!("expected completion menu to be open");
12958 }
12959 });
12960
12961 cx.simulate_keystroke("l");
12962 cx.executor().run_until_parked();
12963 cx.condition(|editor, _| editor.context_menu_visible())
12964 .await;
12965 cx.update_editor(|editor, _, _| {
12966 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12967 {
12968 assert_eq!(
12969 completion_menu_entries(&menu),
12970 &["last"],
12971 "After showing word completions, further editing should filter them and not query the LSP"
12972 );
12973 } else {
12974 panic!("expected completion menu to be open");
12975 }
12976 });
12977}
12978
12979#[gpui::test]
12980async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
12981 init_test(cx, |language_settings| {
12982 language_settings.defaults.completions = Some(CompletionSettings {
12983 words: WordsCompletionMode::Fallback,
12984 lsp: false,
12985 lsp_fetch_timeout_ms: 0,
12986 lsp_insert_mode: LspInsertMode::Insert,
12987 });
12988 });
12989
12990 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12991
12992 cx.set_state(indoc! {"ˇ
12993 0_usize
12994 let
12995 33
12996 4.5f32
12997 "});
12998 cx.update_editor(|editor, window, cx| {
12999 editor.show_completions(&ShowCompletions::default(), window, cx);
13000 });
13001 cx.executor().run_until_parked();
13002 cx.condition(|editor, _| editor.context_menu_visible())
13003 .await;
13004 cx.update_editor(|editor, window, cx| {
13005 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13006 {
13007 assert_eq!(
13008 completion_menu_entries(&menu),
13009 &["let"],
13010 "With no digits in the completion query, no digits should be in the word completions"
13011 );
13012 } else {
13013 panic!("expected completion menu to be open");
13014 }
13015 editor.cancel(&Cancel, window, cx);
13016 });
13017
13018 cx.set_state(indoc! {"3ˇ
13019 0_usize
13020 let
13021 3
13022 33.35f32
13023 "});
13024 cx.update_editor(|editor, window, cx| {
13025 editor.show_completions(&ShowCompletions::default(), window, cx);
13026 });
13027 cx.executor().run_until_parked();
13028 cx.condition(|editor, _| editor.context_menu_visible())
13029 .await;
13030 cx.update_editor(|editor, _, _| {
13031 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13032 {
13033 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
13034 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
13035 } else {
13036 panic!("expected completion menu to be open");
13037 }
13038 });
13039}
13040
13041fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
13042 let position = || lsp::Position {
13043 line: params.text_document_position.position.line,
13044 character: params.text_document_position.position.character,
13045 };
13046 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13047 range: lsp::Range {
13048 start: position(),
13049 end: position(),
13050 },
13051 new_text: text.to_string(),
13052 }))
13053}
13054
13055#[gpui::test]
13056async fn test_multiline_completion(cx: &mut TestAppContext) {
13057 init_test(cx, |_| {});
13058
13059 let fs = FakeFs::new(cx.executor());
13060 fs.insert_tree(
13061 path!("/a"),
13062 json!({
13063 "main.ts": "a",
13064 }),
13065 )
13066 .await;
13067
13068 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13069 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13070 let typescript_language = Arc::new(Language::new(
13071 LanguageConfig {
13072 name: "TypeScript".into(),
13073 matcher: LanguageMatcher {
13074 path_suffixes: vec!["ts".to_string()],
13075 ..LanguageMatcher::default()
13076 },
13077 line_comments: vec!["// ".into()],
13078 ..LanguageConfig::default()
13079 },
13080 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
13081 ));
13082 language_registry.add(typescript_language.clone());
13083 let mut fake_servers = language_registry.register_fake_lsp(
13084 "TypeScript",
13085 FakeLspAdapter {
13086 capabilities: lsp::ServerCapabilities {
13087 completion_provider: Some(lsp::CompletionOptions {
13088 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13089 ..lsp::CompletionOptions::default()
13090 }),
13091 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13092 ..lsp::ServerCapabilities::default()
13093 },
13094 // Emulate vtsls label generation
13095 label_for_completion: Some(Box::new(|item, _| {
13096 let text = if let Some(description) = item
13097 .label_details
13098 .as_ref()
13099 .and_then(|label_details| label_details.description.as_ref())
13100 {
13101 format!("{} {}", item.label, description)
13102 } else if let Some(detail) = &item.detail {
13103 format!("{} {}", item.label, detail)
13104 } else {
13105 item.label.clone()
13106 };
13107 let len = text.len();
13108 Some(language::CodeLabel {
13109 text,
13110 runs: Vec::new(),
13111 filter_range: 0..len,
13112 })
13113 })),
13114 ..FakeLspAdapter::default()
13115 },
13116 );
13117 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13118 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13119 let worktree_id = workspace
13120 .update(cx, |workspace, _window, cx| {
13121 workspace.project().update(cx, |project, cx| {
13122 project.worktrees(cx).next().unwrap().read(cx).id()
13123 })
13124 })
13125 .unwrap();
13126 let _buffer = project
13127 .update(cx, |project, cx| {
13128 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
13129 })
13130 .await
13131 .unwrap();
13132 let editor = workspace
13133 .update(cx, |workspace, window, cx| {
13134 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
13135 })
13136 .unwrap()
13137 .await
13138 .unwrap()
13139 .downcast::<Editor>()
13140 .unwrap();
13141 let fake_server = fake_servers.next().await.unwrap();
13142
13143 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
13144 let multiline_label_2 = "a\nb\nc\n";
13145 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
13146 let multiline_description = "d\ne\nf\n";
13147 let multiline_detail_2 = "g\nh\ni\n";
13148
13149 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
13150 move |params, _| async move {
13151 Ok(Some(lsp::CompletionResponse::Array(vec![
13152 lsp::CompletionItem {
13153 label: multiline_label.to_string(),
13154 text_edit: gen_text_edit(¶ms, "new_text_1"),
13155 ..lsp::CompletionItem::default()
13156 },
13157 lsp::CompletionItem {
13158 label: "single line label 1".to_string(),
13159 detail: Some(multiline_detail.to_string()),
13160 text_edit: gen_text_edit(¶ms, "new_text_2"),
13161 ..lsp::CompletionItem::default()
13162 },
13163 lsp::CompletionItem {
13164 label: "single line label 2".to_string(),
13165 label_details: Some(lsp::CompletionItemLabelDetails {
13166 description: Some(multiline_description.to_string()),
13167 detail: None,
13168 }),
13169 text_edit: gen_text_edit(¶ms, "new_text_2"),
13170 ..lsp::CompletionItem::default()
13171 },
13172 lsp::CompletionItem {
13173 label: multiline_label_2.to_string(),
13174 detail: Some(multiline_detail_2.to_string()),
13175 text_edit: gen_text_edit(¶ms, "new_text_3"),
13176 ..lsp::CompletionItem::default()
13177 },
13178 lsp::CompletionItem {
13179 label: "Label with many spaces and \t but without newlines".to_string(),
13180 detail: Some(
13181 "Details with many spaces and \t but without newlines".to_string(),
13182 ),
13183 text_edit: gen_text_edit(¶ms, "new_text_4"),
13184 ..lsp::CompletionItem::default()
13185 },
13186 ])))
13187 },
13188 );
13189
13190 editor.update_in(cx, |editor, window, cx| {
13191 cx.focus_self(window);
13192 editor.move_to_end(&MoveToEnd, window, cx);
13193 editor.handle_input(".", window, cx);
13194 });
13195 cx.run_until_parked();
13196 completion_handle.next().await.unwrap();
13197
13198 editor.update(cx, |editor, _| {
13199 assert!(editor.context_menu_visible());
13200 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13201 {
13202 let completion_labels = menu
13203 .completions
13204 .borrow()
13205 .iter()
13206 .map(|c| c.label.text.clone())
13207 .collect::<Vec<_>>();
13208 assert_eq!(
13209 completion_labels,
13210 &[
13211 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
13212 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
13213 "single line label 2 d e f ",
13214 "a b c g h i ",
13215 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
13216 ],
13217 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
13218 );
13219
13220 for completion in menu
13221 .completions
13222 .borrow()
13223 .iter() {
13224 assert_eq!(
13225 completion.label.filter_range,
13226 0..completion.label.text.len(),
13227 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
13228 );
13229 }
13230 } else {
13231 panic!("expected completion menu to be open");
13232 }
13233 });
13234}
13235
13236#[gpui::test]
13237async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
13238 init_test(cx, |_| {});
13239 let mut cx = EditorLspTestContext::new_rust(
13240 lsp::ServerCapabilities {
13241 completion_provider: Some(lsp::CompletionOptions {
13242 trigger_characters: Some(vec![".".to_string()]),
13243 ..Default::default()
13244 }),
13245 ..Default::default()
13246 },
13247 cx,
13248 )
13249 .await;
13250 cx.lsp
13251 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13252 Ok(Some(lsp::CompletionResponse::Array(vec![
13253 lsp::CompletionItem {
13254 label: "first".into(),
13255 ..Default::default()
13256 },
13257 lsp::CompletionItem {
13258 label: "last".into(),
13259 ..Default::default()
13260 },
13261 ])))
13262 });
13263 cx.set_state("variableˇ");
13264 cx.simulate_keystroke(".");
13265 cx.executor().run_until_parked();
13266
13267 cx.update_editor(|editor, _, _| {
13268 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13269 {
13270 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
13271 } else {
13272 panic!("expected completion menu to be open");
13273 }
13274 });
13275
13276 cx.update_editor(|editor, window, cx| {
13277 editor.move_page_down(&MovePageDown::default(), window, cx);
13278 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13279 {
13280 assert!(
13281 menu.selected_item == 1,
13282 "expected PageDown to select the last item from the context menu"
13283 );
13284 } else {
13285 panic!("expected completion menu to stay open after PageDown");
13286 }
13287 });
13288
13289 cx.update_editor(|editor, window, cx| {
13290 editor.move_page_up(&MovePageUp::default(), window, cx);
13291 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13292 {
13293 assert!(
13294 menu.selected_item == 0,
13295 "expected PageUp to select the first item from the context menu"
13296 );
13297 } else {
13298 panic!("expected completion menu to stay open after PageUp");
13299 }
13300 });
13301}
13302
13303#[gpui::test]
13304async fn test_as_is_completions(cx: &mut TestAppContext) {
13305 init_test(cx, |_| {});
13306 let mut cx = EditorLspTestContext::new_rust(
13307 lsp::ServerCapabilities {
13308 completion_provider: Some(lsp::CompletionOptions {
13309 ..Default::default()
13310 }),
13311 ..Default::default()
13312 },
13313 cx,
13314 )
13315 .await;
13316 cx.lsp
13317 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13318 Ok(Some(lsp::CompletionResponse::Array(vec![
13319 lsp::CompletionItem {
13320 label: "unsafe".into(),
13321 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13322 range: lsp::Range {
13323 start: lsp::Position {
13324 line: 1,
13325 character: 2,
13326 },
13327 end: lsp::Position {
13328 line: 1,
13329 character: 3,
13330 },
13331 },
13332 new_text: "unsafe".to_string(),
13333 })),
13334 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
13335 ..Default::default()
13336 },
13337 ])))
13338 });
13339 cx.set_state("fn a() {}\n nˇ");
13340 cx.executor().run_until_parked();
13341 cx.update_editor(|editor, window, cx| {
13342 editor.show_completions(
13343 &ShowCompletions {
13344 trigger: Some("\n".into()),
13345 },
13346 window,
13347 cx,
13348 );
13349 });
13350 cx.executor().run_until_parked();
13351
13352 cx.update_editor(|editor, window, cx| {
13353 editor.confirm_completion(&Default::default(), window, cx)
13354 });
13355 cx.executor().run_until_parked();
13356 cx.assert_editor_state("fn a() {}\n unsafeˇ");
13357}
13358
13359#[gpui::test]
13360async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
13361 init_test(cx, |_| {});
13362 let language =
13363 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
13364 let mut cx = EditorLspTestContext::new(
13365 language,
13366 lsp::ServerCapabilities {
13367 completion_provider: Some(lsp::CompletionOptions {
13368 ..lsp::CompletionOptions::default()
13369 }),
13370 ..lsp::ServerCapabilities::default()
13371 },
13372 cx,
13373 )
13374 .await;
13375
13376 cx.set_state(
13377 "#ifndef BAR_H
13378#define BAR_H
13379
13380#include <stdbool.h>
13381
13382int fn_branch(bool do_branch1, bool do_branch2);
13383
13384#endif // BAR_H
13385ˇ",
13386 );
13387 cx.executor().run_until_parked();
13388 cx.update_editor(|editor, window, cx| {
13389 editor.handle_input("#", window, cx);
13390 });
13391 cx.executor().run_until_parked();
13392 cx.update_editor(|editor, window, cx| {
13393 editor.handle_input("i", window, cx);
13394 });
13395 cx.executor().run_until_parked();
13396 cx.update_editor(|editor, window, cx| {
13397 editor.handle_input("n", window, cx);
13398 });
13399 cx.executor().run_until_parked();
13400 cx.assert_editor_state(
13401 "#ifndef BAR_H
13402#define BAR_H
13403
13404#include <stdbool.h>
13405
13406int fn_branch(bool do_branch1, bool do_branch2);
13407
13408#endif // BAR_H
13409#inˇ",
13410 );
13411
13412 cx.lsp
13413 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13414 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13415 is_incomplete: false,
13416 item_defaults: None,
13417 items: vec![lsp::CompletionItem {
13418 kind: Some(lsp::CompletionItemKind::SNIPPET),
13419 label_details: Some(lsp::CompletionItemLabelDetails {
13420 detail: Some("header".to_string()),
13421 description: None,
13422 }),
13423 label: " include".to_string(),
13424 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13425 range: lsp::Range {
13426 start: lsp::Position {
13427 line: 8,
13428 character: 1,
13429 },
13430 end: lsp::Position {
13431 line: 8,
13432 character: 1,
13433 },
13434 },
13435 new_text: "include \"$0\"".to_string(),
13436 })),
13437 sort_text: Some("40b67681include".to_string()),
13438 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13439 filter_text: Some("include".to_string()),
13440 insert_text: Some("include \"$0\"".to_string()),
13441 ..lsp::CompletionItem::default()
13442 }],
13443 })))
13444 });
13445 cx.update_editor(|editor, window, cx| {
13446 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13447 });
13448 cx.executor().run_until_parked();
13449 cx.update_editor(|editor, window, cx| {
13450 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
13451 });
13452 cx.executor().run_until_parked();
13453 cx.assert_editor_state(
13454 "#ifndef BAR_H
13455#define BAR_H
13456
13457#include <stdbool.h>
13458
13459int fn_branch(bool do_branch1, bool do_branch2);
13460
13461#endif // BAR_H
13462#include \"ˇ\"",
13463 );
13464
13465 cx.lsp
13466 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13467 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13468 is_incomplete: true,
13469 item_defaults: None,
13470 items: vec![lsp::CompletionItem {
13471 kind: Some(lsp::CompletionItemKind::FILE),
13472 label: "AGL/".to_string(),
13473 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13474 range: lsp::Range {
13475 start: lsp::Position {
13476 line: 8,
13477 character: 10,
13478 },
13479 end: lsp::Position {
13480 line: 8,
13481 character: 11,
13482 },
13483 },
13484 new_text: "AGL/".to_string(),
13485 })),
13486 sort_text: Some("40b67681AGL/".to_string()),
13487 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
13488 filter_text: Some("AGL/".to_string()),
13489 insert_text: Some("AGL/".to_string()),
13490 ..lsp::CompletionItem::default()
13491 }],
13492 })))
13493 });
13494 cx.update_editor(|editor, window, cx| {
13495 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13496 });
13497 cx.executor().run_until_parked();
13498 cx.update_editor(|editor, window, cx| {
13499 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
13500 });
13501 cx.executor().run_until_parked();
13502 cx.assert_editor_state(
13503 r##"#ifndef BAR_H
13504#define BAR_H
13505
13506#include <stdbool.h>
13507
13508int fn_branch(bool do_branch1, bool do_branch2);
13509
13510#endif // BAR_H
13511#include "AGL/ˇ"##,
13512 );
13513
13514 cx.update_editor(|editor, window, cx| {
13515 editor.handle_input("\"", window, cx);
13516 });
13517 cx.executor().run_until_parked();
13518 cx.assert_editor_state(
13519 r##"#ifndef BAR_H
13520#define BAR_H
13521
13522#include <stdbool.h>
13523
13524int fn_branch(bool do_branch1, bool do_branch2);
13525
13526#endif // BAR_H
13527#include "AGL/"ˇ"##,
13528 );
13529}
13530
13531#[gpui::test]
13532async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
13533 init_test(cx, |_| {});
13534
13535 let mut cx = EditorLspTestContext::new_rust(
13536 lsp::ServerCapabilities {
13537 completion_provider: Some(lsp::CompletionOptions {
13538 trigger_characters: Some(vec![".".to_string()]),
13539 resolve_provider: Some(true),
13540 ..Default::default()
13541 }),
13542 ..Default::default()
13543 },
13544 cx,
13545 )
13546 .await;
13547
13548 cx.set_state("fn main() { let a = 2ˇ; }");
13549 cx.simulate_keystroke(".");
13550 let completion_item = lsp::CompletionItem {
13551 label: "Some".into(),
13552 kind: Some(lsp::CompletionItemKind::SNIPPET),
13553 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13554 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13555 kind: lsp::MarkupKind::Markdown,
13556 value: "```rust\nSome(2)\n```".to_string(),
13557 })),
13558 deprecated: Some(false),
13559 sort_text: Some("Some".to_string()),
13560 filter_text: Some("Some".to_string()),
13561 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13562 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13563 range: lsp::Range {
13564 start: lsp::Position {
13565 line: 0,
13566 character: 22,
13567 },
13568 end: lsp::Position {
13569 line: 0,
13570 character: 22,
13571 },
13572 },
13573 new_text: "Some(2)".to_string(),
13574 })),
13575 additional_text_edits: Some(vec![lsp::TextEdit {
13576 range: lsp::Range {
13577 start: lsp::Position {
13578 line: 0,
13579 character: 20,
13580 },
13581 end: lsp::Position {
13582 line: 0,
13583 character: 22,
13584 },
13585 },
13586 new_text: "".to_string(),
13587 }]),
13588 ..Default::default()
13589 };
13590
13591 let closure_completion_item = completion_item.clone();
13592 let counter = Arc::new(AtomicUsize::new(0));
13593 let counter_clone = counter.clone();
13594 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13595 let task_completion_item = closure_completion_item.clone();
13596 counter_clone.fetch_add(1, atomic::Ordering::Release);
13597 async move {
13598 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13599 is_incomplete: true,
13600 item_defaults: None,
13601 items: vec![task_completion_item],
13602 })))
13603 }
13604 });
13605
13606 cx.condition(|editor, _| editor.context_menu_visible())
13607 .await;
13608 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
13609 assert!(request.next().await.is_some());
13610 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13611
13612 cx.simulate_keystrokes("S o m");
13613 cx.condition(|editor, _| editor.context_menu_visible())
13614 .await;
13615 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
13616 assert!(request.next().await.is_some());
13617 assert!(request.next().await.is_some());
13618 assert!(request.next().await.is_some());
13619 request.close();
13620 assert!(request.next().await.is_none());
13621 assert_eq!(
13622 counter.load(atomic::Ordering::Acquire),
13623 4,
13624 "With the completions menu open, only one LSP request should happen per input"
13625 );
13626}
13627
13628#[gpui::test]
13629async fn test_toggle_comment(cx: &mut TestAppContext) {
13630 init_test(cx, |_| {});
13631 let mut cx = EditorTestContext::new(cx).await;
13632 let language = Arc::new(Language::new(
13633 LanguageConfig {
13634 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13635 ..Default::default()
13636 },
13637 Some(tree_sitter_rust::LANGUAGE.into()),
13638 ));
13639 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13640
13641 // If multiple selections intersect a line, the line is only toggled once.
13642 cx.set_state(indoc! {"
13643 fn a() {
13644 «//b();
13645 ˇ»// «c();
13646 //ˇ» d();
13647 }
13648 "});
13649
13650 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13651
13652 cx.assert_editor_state(indoc! {"
13653 fn a() {
13654 «b();
13655 c();
13656 ˇ» d();
13657 }
13658 "});
13659
13660 // The comment prefix is inserted at the same column for every line in a
13661 // selection.
13662 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13663
13664 cx.assert_editor_state(indoc! {"
13665 fn a() {
13666 // «b();
13667 // c();
13668 ˇ»// d();
13669 }
13670 "});
13671
13672 // If a selection ends at the beginning of a line, that line is not toggled.
13673 cx.set_selections_state(indoc! {"
13674 fn a() {
13675 // b();
13676 «// c();
13677 ˇ» // d();
13678 }
13679 "});
13680
13681 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13682
13683 cx.assert_editor_state(indoc! {"
13684 fn a() {
13685 // b();
13686 «c();
13687 ˇ» // d();
13688 }
13689 "});
13690
13691 // If a selection span a single line and is empty, the line is toggled.
13692 cx.set_state(indoc! {"
13693 fn a() {
13694 a();
13695 b();
13696 ˇ
13697 }
13698 "});
13699
13700 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13701
13702 cx.assert_editor_state(indoc! {"
13703 fn a() {
13704 a();
13705 b();
13706 //•ˇ
13707 }
13708 "});
13709
13710 // If a selection span multiple lines, empty lines are not toggled.
13711 cx.set_state(indoc! {"
13712 fn a() {
13713 «a();
13714
13715 c();ˇ»
13716 }
13717 "});
13718
13719 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13720
13721 cx.assert_editor_state(indoc! {"
13722 fn a() {
13723 // «a();
13724
13725 // c();ˇ»
13726 }
13727 "});
13728
13729 // If a selection includes multiple comment prefixes, all lines are uncommented.
13730 cx.set_state(indoc! {"
13731 fn a() {
13732 «// a();
13733 /// b();
13734 //! c();ˇ»
13735 }
13736 "});
13737
13738 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13739
13740 cx.assert_editor_state(indoc! {"
13741 fn a() {
13742 «a();
13743 b();
13744 c();ˇ»
13745 }
13746 "});
13747}
13748
13749#[gpui::test]
13750async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
13751 init_test(cx, |_| {});
13752 let mut cx = EditorTestContext::new(cx).await;
13753 let language = Arc::new(Language::new(
13754 LanguageConfig {
13755 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13756 ..Default::default()
13757 },
13758 Some(tree_sitter_rust::LANGUAGE.into()),
13759 ));
13760 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13761
13762 let toggle_comments = &ToggleComments {
13763 advance_downwards: false,
13764 ignore_indent: true,
13765 };
13766
13767 // If multiple selections intersect a line, the line is only toggled once.
13768 cx.set_state(indoc! {"
13769 fn a() {
13770 // «b();
13771 // c();
13772 // ˇ» d();
13773 }
13774 "});
13775
13776 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13777
13778 cx.assert_editor_state(indoc! {"
13779 fn a() {
13780 «b();
13781 c();
13782 ˇ» d();
13783 }
13784 "});
13785
13786 // The comment prefix is inserted at the beginning of each line
13787 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13788
13789 cx.assert_editor_state(indoc! {"
13790 fn a() {
13791 // «b();
13792 // c();
13793 // ˇ» d();
13794 }
13795 "});
13796
13797 // If a selection ends at the beginning of a line, that line is not toggled.
13798 cx.set_selections_state(indoc! {"
13799 fn a() {
13800 // b();
13801 // «c();
13802 ˇ»// d();
13803 }
13804 "});
13805
13806 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13807
13808 cx.assert_editor_state(indoc! {"
13809 fn a() {
13810 // b();
13811 «c();
13812 ˇ»// d();
13813 }
13814 "});
13815
13816 // If a selection span a single line and is empty, the line is toggled.
13817 cx.set_state(indoc! {"
13818 fn a() {
13819 a();
13820 b();
13821 ˇ
13822 }
13823 "});
13824
13825 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13826
13827 cx.assert_editor_state(indoc! {"
13828 fn a() {
13829 a();
13830 b();
13831 //ˇ
13832 }
13833 "});
13834
13835 // If a selection span multiple lines, empty lines are not toggled.
13836 cx.set_state(indoc! {"
13837 fn a() {
13838 «a();
13839
13840 c();ˇ»
13841 }
13842 "});
13843
13844 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13845
13846 cx.assert_editor_state(indoc! {"
13847 fn a() {
13848 // «a();
13849
13850 // c();ˇ»
13851 }
13852 "});
13853
13854 // If a selection includes multiple comment prefixes, all lines are uncommented.
13855 cx.set_state(indoc! {"
13856 fn a() {
13857 // «a();
13858 /// b();
13859 //! c();ˇ»
13860 }
13861 "});
13862
13863 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13864
13865 cx.assert_editor_state(indoc! {"
13866 fn a() {
13867 «a();
13868 b();
13869 c();ˇ»
13870 }
13871 "});
13872}
13873
13874#[gpui::test]
13875async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
13876 init_test(cx, |_| {});
13877
13878 let language = Arc::new(Language::new(
13879 LanguageConfig {
13880 line_comments: vec!["// ".into()],
13881 ..Default::default()
13882 },
13883 Some(tree_sitter_rust::LANGUAGE.into()),
13884 ));
13885
13886 let mut cx = EditorTestContext::new(cx).await;
13887
13888 cx.language_registry().add(language.clone());
13889 cx.update_buffer(|buffer, cx| {
13890 buffer.set_language(Some(language), cx);
13891 });
13892
13893 let toggle_comments = &ToggleComments {
13894 advance_downwards: true,
13895 ignore_indent: false,
13896 };
13897
13898 // Single cursor on one line -> advance
13899 // Cursor moves horizontally 3 characters as well on non-blank line
13900 cx.set_state(indoc!(
13901 "fn a() {
13902 ˇdog();
13903 cat();
13904 }"
13905 ));
13906 cx.update_editor(|editor, window, cx| {
13907 editor.toggle_comments(toggle_comments, window, cx);
13908 });
13909 cx.assert_editor_state(indoc!(
13910 "fn a() {
13911 // dog();
13912 catˇ();
13913 }"
13914 ));
13915
13916 // Single selection on one line -> don't advance
13917 cx.set_state(indoc!(
13918 "fn a() {
13919 «dog()ˇ»;
13920 cat();
13921 }"
13922 ));
13923 cx.update_editor(|editor, window, cx| {
13924 editor.toggle_comments(toggle_comments, window, cx);
13925 });
13926 cx.assert_editor_state(indoc!(
13927 "fn a() {
13928 // «dog()ˇ»;
13929 cat();
13930 }"
13931 ));
13932
13933 // Multiple cursors on one line -> advance
13934 cx.set_state(indoc!(
13935 "fn a() {
13936 ˇdˇog();
13937 cat();
13938 }"
13939 ));
13940 cx.update_editor(|editor, window, cx| {
13941 editor.toggle_comments(toggle_comments, window, cx);
13942 });
13943 cx.assert_editor_state(indoc!(
13944 "fn a() {
13945 // dog();
13946 catˇ(ˇ);
13947 }"
13948 ));
13949
13950 // Multiple cursors on one line, with selection -> don't advance
13951 cx.set_state(indoc!(
13952 "fn a() {
13953 ˇdˇog«()ˇ»;
13954 cat();
13955 }"
13956 ));
13957 cx.update_editor(|editor, window, cx| {
13958 editor.toggle_comments(toggle_comments, window, cx);
13959 });
13960 cx.assert_editor_state(indoc!(
13961 "fn a() {
13962 // ˇdˇog«()ˇ»;
13963 cat();
13964 }"
13965 ));
13966
13967 // Single cursor on one line -> advance
13968 // Cursor moves to column 0 on blank line
13969 cx.set_state(indoc!(
13970 "fn a() {
13971 ˇdog();
13972
13973 cat();
13974 }"
13975 ));
13976 cx.update_editor(|editor, window, cx| {
13977 editor.toggle_comments(toggle_comments, window, cx);
13978 });
13979 cx.assert_editor_state(indoc!(
13980 "fn a() {
13981 // dog();
13982 ˇ
13983 cat();
13984 }"
13985 ));
13986
13987 // Single cursor on one line -> advance
13988 // Cursor starts and ends at column 0
13989 cx.set_state(indoc!(
13990 "fn a() {
13991 ˇ dog();
13992 cat();
13993 }"
13994 ));
13995 cx.update_editor(|editor, window, cx| {
13996 editor.toggle_comments(toggle_comments, window, cx);
13997 });
13998 cx.assert_editor_state(indoc!(
13999 "fn a() {
14000 // dog();
14001 ˇ cat();
14002 }"
14003 ));
14004}
14005
14006#[gpui::test]
14007async fn test_toggle_block_comment(cx: &mut TestAppContext) {
14008 init_test(cx, |_| {});
14009
14010 let mut cx = EditorTestContext::new(cx).await;
14011
14012 let html_language = Arc::new(
14013 Language::new(
14014 LanguageConfig {
14015 name: "HTML".into(),
14016 block_comment: Some(BlockCommentConfig {
14017 start: "<!-- ".into(),
14018 prefix: "".into(),
14019 end: " -->".into(),
14020 tab_size: 0,
14021 }),
14022 ..Default::default()
14023 },
14024 Some(tree_sitter_html::LANGUAGE.into()),
14025 )
14026 .with_injection_query(
14027 r#"
14028 (script_element
14029 (raw_text) @injection.content
14030 (#set! injection.language "javascript"))
14031 "#,
14032 )
14033 .unwrap(),
14034 );
14035
14036 let javascript_language = Arc::new(Language::new(
14037 LanguageConfig {
14038 name: "JavaScript".into(),
14039 line_comments: vec!["// ".into()],
14040 ..Default::default()
14041 },
14042 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
14043 ));
14044
14045 cx.language_registry().add(html_language.clone());
14046 cx.language_registry().add(javascript_language.clone());
14047 cx.update_buffer(|buffer, cx| {
14048 buffer.set_language(Some(html_language), cx);
14049 });
14050
14051 // Toggle comments for empty selections
14052 cx.set_state(
14053 &r#"
14054 <p>A</p>ˇ
14055 <p>B</p>ˇ
14056 <p>C</p>ˇ
14057 "#
14058 .unindent(),
14059 );
14060 cx.update_editor(|editor, window, cx| {
14061 editor.toggle_comments(&ToggleComments::default(), window, cx)
14062 });
14063 cx.assert_editor_state(
14064 &r#"
14065 <!-- <p>A</p>ˇ -->
14066 <!-- <p>B</p>ˇ -->
14067 <!-- <p>C</p>ˇ -->
14068 "#
14069 .unindent(),
14070 );
14071 cx.update_editor(|editor, window, cx| {
14072 editor.toggle_comments(&ToggleComments::default(), window, cx)
14073 });
14074 cx.assert_editor_state(
14075 &r#"
14076 <p>A</p>ˇ
14077 <p>B</p>ˇ
14078 <p>C</p>ˇ
14079 "#
14080 .unindent(),
14081 );
14082
14083 // Toggle comments for mixture of empty and non-empty selections, where
14084 // multiple selections occupy a given line.
14085 cx.set_state(
14086 &r#"
14087 <p>A«</p>
14088 <p>ˇ»B</p>ˇ
14089 <p>C«</p>
14090 <p>ˇ»D</p>ˇ
14091 "#
14092 .unindent(),
14093 );
14094
14095 cx.update_editor(|editor, window, cx| {
14096 editor.toggle_comments(&ToggleComments::default(), window, cx)
14097 });
14098 cx.assert_editor_state(
14099 &r#"
14100 <!-- <p>A«</p>
14101 <p>ˇ»B</p>ˇ -->
14102 <!-- <p>C«</p>
14103 <p>ˇ»D</p>ˇ -->
14104 "#
14105 .unindent(),
14106 );
14107 cx.update_editor(|editor, window, cx| {
14108 editor.toggle_comments(&ToggleComments::default(), window, cx)
14109 });
14110 cx.assert_editor_state(
14111 &r#"
14112 <p>A«</p>
14113 <p>ˇ»B</p>ˇ
14114 <p>C«</p>
14115 <p>ˇ»D</p>ˇ
14116 "#
14117 .unindent(),
14118 );
14119
14120 // Toggle comments when different languages are active for different
14121 // selections.
14122 cx.set_state(
14123 &r#"
14124 ˇ<script>
14125 ˇvar x = new Y();
14126 ˇ</script>
14127 "#
14128 .unindent(),
14129 );
14130 cx.executor().run_until_parked();
14131 cx.update_editor(|editor, window, cx| {
14132 editor.toggle_comments(&ToggleComments::default(), window, cx)
14133 });
14134 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
14135 // Uncommenting and commenting from this position brings in even more wrong artifacts.
14136 cx.assert_editor_state(
14137 &r#"
14138 <!-- ˇ<script> -->
14139 // ˇvar x = new Y();
14140 <!-- ˇ</script> -->
14141 "#
14142 .unindent(),
14143 );
14144}
14145
14146#[gpui::test]
14147fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
14148 init_test(cx, |_| {});
14149
14150 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14151 let multibuffer = cx.new(|cx| {
14152 let mut multibuffer = MultiBuffer::new(ReadWrite);
14153 multibuffer.push_excerpts(
14154 buffer.clone(),
14155 [
14156 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
14157 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
14158 ],
14159 cx,
14160 );
14161 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
14162 multibuffer
14163 });
14164
14165 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
14166 editor.update_in(cx, |editor, window, cx| {
14167 assert_eq!(editor.text(cx), "aaaa\nbbbb");
14168 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14169 s.select_ranges([
14170 Point::new(0, 0)..Point::new(0, 0),
14171 Point::new(1, 0)..Point::new(1, 0),
14172 ])
14173 });
14174
14175 editor.handle_input("X", window, cx);
14176 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
14177 assert_eq!(
14178 editor.selections.ranges(cx),
14179 [
14180 Point::new(0, 1)..Point::new(0, 1),
14181 Point::new(1, 1)..Point::new(1, 1),
14182 ]
14183 );
14184
14185 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
14186 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14187 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
14188 });
14189 editor.backspace(&Default::default(), window, cx);
14190 assert_eq!(editor.text(cx), "Xa\nbbb");
14191 assert_eq!(
14192 editor.selections.ranges(cx),
14193 [Point::new(1, 0)..Point::new(1, 0)]
14194 );
14195
14196 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14197 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
14198 });
14199 editor.backspace(&Default::default(), window, cx);
14200 assert_eq!(editor.text(cx), "X\nbb");
14201 assert_eq!(
14202 editor.selections.ranges(cx),
14203 [Point::new(0, 1)..Point::new(0, 1)]
14204 );
14205 });
14206}
14207
14208#[gpui::test]
14209fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
14210 init_test(cx, |_| {});
14211
14212 let markers = vec![('[', ']').into(), ('(', ')').into()];
14213 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
14214 indoc! {"
14215 [aaaa
14216 (bbbb]
14217 cccc)",
14218 },
14219 markers.clone(),
14220 );
14221 let excerpt_ranges = markers.into_iter().map(|marker| {
14222 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
14223 ExcerptRange::new(context.clone())
14224 });
14225 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
14226 let multibuffer = cx.new(|cx| {
14227 let mut multibuffer = MultiBuffer::new(ReadWrite);
14228 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
14229 multibuffer
14230 });
14231
14232 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
14233 editor.update_in(cx, |editor, window, cx| {
14234 let (expected_text, selection_ranges) = marked_text_ranges(
14235 indoc! {"
14236 aaaa
14237 bˇbbb
14238 bˇbbˇb
14239 cccc"
14240 },
14241 true,
14242 );
14243 assert_eq!(editor.text(cx), expected_text);
14244 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14245 s.select_ranges(selection_ranges)
14246 });
14247
14248 editor.handle_input("X", window, cx);
14249
14250 let (expected_text, expected_selections) = marked_text_ranges(
14251 indoc! {"
14252 aaaa
14253 bXˇbbXb
14254 bXˇbbXˇb
14255 cccc"
14256 },
14257 false,
14258 );
14259 assert_eq!(editor.text(cx), expected_text);
14260 assert_eq!(editor.selections.ranges(cx), expected_selections);
14261
14262 editor.newline(&Newline, window, cx);
14263 let (expected_text, expected_selections) = marked_text_ranges(
14264 indoc! {"
14265 aaaa
14266 bX
14267 ˇbbX
14268 b
14269 bX
14270 ˇbbX
14271 ˇb
14272 cccc"
14273 },
14274 false,
14275 );
14276 assert_eq!(editor.text(cx), expected_text);
14277 assert_eq!(editor.selections.ranges(cx), expected_selections);
14278 });
14279}
14280
14281#[gpui::test]
14282fn test_refresh_selections(cx: &mut TestAppContext) {
14283 init_test(cx, |_| {});
14284
14285 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14286 let mut excerpt1_id = None;
14287 let multibuffer = cx.new(|cx| {
14288 let mut multibuffer = MultiBuffer::new(ReadWrite);
14289 excerpt1_id = multibuffer
14290 .push_excerpts(
14291 buffer.clone(),
14292 [
14293 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
14294 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
14295 ],
14296 cx,
14297 )
14298 .into_iter()
14299 .next();
14300 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
14301 multibuffer
14302 });
14303
14304 let editor = cx.add_window(|window, cx| {
14305 let mut editor = build_editor(multibuffer.clone(), window, cx);
14306 let snapshot = editor.snapshot(window, cx);
14307 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14308 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
14309 });
14310 editor.begin_selection(
14311 Point::new(2, 1).to_display_point(&snapshot),
14312 true,
14313 1,
14314 window,
14315 cx,
14316 );
14317 assert_eq!(
14318 editor.selections.ranges(cx),
14319 [
14320 Point::new(1, 3)..Point::new(1, 3),
14321 Point::new(2, 1)..Point::new(2, 1),
14322 ]
14323 );
14324 editor
14325 });
14326
14327 // Refreshing selections is a no-op when excerpts haven't changed.
14328 _ = editor.update(cx, |editor, window, cx| {
14329 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14330 assert_eq!(
14331 editor.selections.ranges(cx),
14332 [
14333 Point::new(1, 3)..Point::new(1, 3),
14334 Point::new(2, 1)..Point::new(2, 1),
14335 ]
14336 );
14337 });
14338
14339 multibuffer.update(cx, |multibuffer, cx| {
14340 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14341 });
14342 _ = editor.update(cx, |editor, window, cx| {
14343 // Removing an excerpt causes the first selection to become degenerate.
14344 assert_eq!(
14345 editor.selections.ranges(cx),
14346 [
14347 Point::new(0, 0)..Point::new(0, 0),
14348 Point::new(0, 1)..Point::new(0, 1)
14349 ]
14350 );
14351
14352 // Refreshing selections will relocate the first selection to the original buffer
14353 // location.
14354 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14355 assert_eq!(
14356 editor.selections.ranges(cx),
14357 [
14358 Point::new(0, 1)..Point::new(0, 1),
14359 Point::new(0, 3)..Point::new(0, 3)
14360 ]
14361 );
14362 assert!(editor.selections.pending_anchor().is_some());
14363 });
14364}
14365
14366#[gpui::test]
14367fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
14368 init_test(cx, |_| {});
14369
14370 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14371 let mut excerpt1_id = None;
14372 let multibuffer = cx.new(|cx| {
14373 let mut multibuffer = MultiBuffer::new(ReadWrite);
14374 excerpt1_id = multibuffer
14375 .push_excerpts(
14376 buffer.clone(),
14377 [
14378 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
14379 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
14380 ],
14381 cx,
14382 )
14383 .into_iter()
14384 .next();
14385 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
14386 multibuffer
14387 });
14388
14389 let editor = cx.add_window(|window, cx| {
14390 let mut editor = build_editor(multibuffer.clone(), window, cx);
14391 let snapshot = editor.snapshot(window, cx);
14392 editor.begin_selection(
14393 Point::new(1, 3).to_display_point(&snapshot),
14394 false,
14395 1,
14396 window,
14397 cx,
14398 );
14399 assert_eq!(
14400 editor.selections.ranges(cx),
14401 [Point::new(1, 3)..Point::new(1, 3)]
14402 );
14403 editor
14404 });
14405
14406 multibuffer.update(cx, |multibuffer, cx| {
14407 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14408 });
14409 _ = editor.update(cx, |editor, window, cx| {
14410 assert_eq!(
14411 editor.selections.ranges(cx),
14412 [Point::new(0, 0)..Point::new(0, 0)]
14413 );
14414
14415 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
14416 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14417 assert_eq!(
14418 editor.selections.ranges(cx),
14419 [Point::new(0, 3)..Point::new(0, 3)]
14420 );
14421 assert!(editor.selections.pending_anchor().is_some());
14422 });
14423}
14424
14425#[gpui::test]
14426async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
14427 init_test(cx, |_| {});
14428
14429 let language = Arc::new(
14430 Language::new(
14431 LanguageConfig {
14432 brackets: BracketPairConfig {
14433 pairs: vec![
14434 BracketPair {
14435 start: "{".to_string(),
14436 end: "}".to_string(),
14437 close: true,
14438 surround: true,
14439 newline: true,
14440 },
14441 BracketPair {
14442 start: "/* ".to_string(),
14443 end: " */".to_string(),
14444 close: true,
14445 surround: true,
14446 newline: true,
14447 },
14448 ],
14449 ..Default::default()
14450 },
14451 ..Default::default()
14452 },
14453 Some(tree_sitter_rust::LANGUAGE.into()),
14454 )
14455 .with_indents_query("")
14456 .unwrap(),
14457 );
14458
14459 let text = concat!(
14460 "{ }\n", //
14461 " x\n", //
14462 " /* */\n", //
14463 "x\n", //
14464 "{{} }\n", //
14465 );
14466
14467 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
14468 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14469 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14470 editor
14471 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
14472 .await;
14473
14474 editor.update_in(cx, |editor, window, cx| {
14475 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14476 s.select_display_ranges([
14477 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
14478 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
14479 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
14480 ])
14481 });
14482 editor.newline(&Newline, window, cx);
14483
14484 assert_eq!(
14485 editor.buffer().read(cx).read(cx).text(),
14486 concat!(
14487 "{ \n", // Suppress rustfmt
14488 "\n", //
14489 "}\n", //
14490 " x\n", //
14491 " /* \n", //
14492 " \n", //
14493 " */\n", //
14494 "x\n", //
14495 "{{} \n", //
14496 "}\n", //
14497 )
14498 );
14499 });
14500}
14501
14502#[gpui::test]
14503fn test_highlighted_ranges(cx: &mut TestAppContext) {
14504 init_test(cx, |_| {});
14505
14506 let editor = cx.add_window(|window, cx| {
14507 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
14508 build_editor(buffer.clone(), window, cx)
14509 });
14510
14511 _ = editor.update(cx, |editor, window, cx| {
14512 struct Type1;
14513 struct Type2;
14514
14515 let buffer = editor.buffer.read(cx).snapshot(cx);
14516
14517 let anchor_range =
14518 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
14519
14520 editor.highlight_background::<Type1>(
14521 &[
14522 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
14523 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
14524 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
14525 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
14526 ],
14527 |_| Hsla::red(),
14528 cx,
14529 );
14530 editor.highlight_background::<Type2>(
14531 &[
14532 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
14533 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
14534 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
14535 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
14536 ],
14537 |_| Hsla::green(),
14538 cx,
14539 );
14540
14541 let snapshot = editor.snapshot(window, cx);
14542 let mut highlighted_ranges = editor.background_highlights_in_range(
14543 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
14544 &snapshot,
14545 cx.theme(),
14546 );
14547 // Enforce a consistent ordering based on color without relying on the ordering of the
14548 // highlight's `TypeId` which is non-executor.
14549 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
14550 assert_eq!(
14551 highlighted_ranges,
14552 &[
14553 (
14554 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
14555 Hsla::red(),
14556 ),
14557 (
14558 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14559 Hsla::red(),
14560 ),
14561 (
14562 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
14563 Hsla::green(),
14564 ),
14565 (
14566 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
14567 Hsla::green(),
14568 ),
14569 ]
14570 );
14571 assert_eq!(
14572 editor.background_highlights_in_range(
14573 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
14574 &snapshot,
14575 cx.theme(),
14576 ),
14577 &[(
14578 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14579 Hsla::red(),
14580 )]
14581 );
14582 });
14583}
14584
14585#[gpui::test]
14586async fn test_following(cx: &mut TestAppContext) {
14587 init_test(cx, |_| {});
14588
14589 let fs = FakeFs::new(cx.executor());
14590 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14591
14592 let buffer = project.update(cx, |project, cx| {
14593 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
14594 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
14595 });
14596 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
14597 let follower = cx.update(|cx| {
14598 cx.open_window(
14599 WindowOptions {
14600 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
14601 gpui::Point::new(px(0.), px(0.)),
14602 gpui::Point::new(px(10.), px(80.)),
14603 ))),
14604 ..Default::default()
14605 },
14606 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
14607 )
14608 .unwrap()
14609 });
14610
14611 let is_still_following = Rc::new(RefCell::new(true));
14612 let follower_edit_event_count = Rc::new(RefCell::new(0));
14613 let pending_update = Rc::new(RefCell::new(None));
14614 let leader_entity = leader.root(cx).unwrap();
14615 let follower_entity = follower.root(cx).unwrap();
14616 _ = follower.update(cx, {
14617 let update = pending_update.clone();
14618 let is_still_following = is_still_following.clone();
14619 let follower_edit_event_count = follower_edit_event_count.clone();
14620 |_, window, cx| {
14621 cx.subscribe_in(
14622 &leader_entity,
14623 window,
14624 move |_, leader, event, window, cx| {
14625 leader.read(cx).add_event_to_update_proto(
14626 event,
14627 &mut update.borrow_mut(),
14628 window,
14629 cx,
14630 );
14631 },
14632 )
14633 .detach();
14634
14635 cx.subscribe_in(
14636 &follower_entity,
14637 window,
14638 move |_, _, event: &EditorEvent, _window, _cx| {
14639 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
14640 *is_still_following.borrow_mut() = false;
14641 }
14642
14643 if let EditorEvent::BufferEdited = event {
14644 *follower_edit_event_count.borrow_mut() += 1;
14645 }
14646 },
14647 )
14648 .detach();
14649 }
14650 });
14651
14652 // Update the selections only
14653 _ = leader.update(cx, |leader, window, cx| {
14654 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14655 s.select_ranges([1..1])
14656 });
14657 });
14658 follower
14659 .update(cx, |follower, window, cx| {
14660 follower.apply_update_proto(
14661 &project,
14662 pending_update.borrow_mut().take().unwrap(),
14663 window,
14664 cx,
14665 )
14666 })
14667 .unwrap()
14668 .await
14669 .unwrap();
14670 _ = follower.update(cx, |follower, _, cx| {
14671 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
14672 });
14673 assert!(*is_still_following.borrow());
14674 assert_eq!(*follower_edit_event_count.borrow(), 0);
14675
14676 // Update the scroll position only
14677 _ = leader.update(cx, |leader, window, cx| {
14678 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14679 });
14680 follower
14681 .update(cx, |follower, window, cx| {
14682 follower.apply_update_proto(
14683 &project,
14684 pending_update.borrow_mut().take().unwrap(),
14685 window,
14686 cx,
14687 )
14688 })
14689 .unwrap()
14690 .await
14691 .unwrap();
14692 assert_eq!(
14693 follower
14694 .update(cx, |follower, _, cx| follower.scroll_position(cx))
14695 .unwrap(),
14696 gpui::Point::new(1.5, 3.5)
14697 );
14698 assert!(*is_still_following.borrow());
14699 assert_eq!(*follower_edit_event_count.borrow(), 0);
14700
14701 // Update the selections and scroll position. The follower's scroll position is updated
14702 // via autoscroll, not via the leader's exact scroll position.
14703 _ = leader.update(cx, |leader, window, cx| {
14704 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14705 s.select_ranges([0..0])
14706 });
14707 leader.request_autoscroll(Autoscroll::newest(), cx);
14708 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14709 });
14710 follower
14711 .update(cx, |follower, window, cx| {
14712 follower.apply_update_proto(
14713 &project,
14714 pending_update.borrow_mut().take().unwrap(),
14715 window,
14716 cx,
14717 )
14718 })
14719 .unwrap()
14720 .await
14721 .unwrap();
14722 _ = follower.update(cx, |follower, _, cx| {
14723 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
14724 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
14725 });
14726 assert!(*is_still_following.borrow());
14727
14728 // Creating a pending selection that precedes another selection
14729 _ = leader.update(cx, |leader, window, cx| {
14730 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14731 s.select_ranges([1..1])
14732 });
14733 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
14734 });
14735 follower
14736 .update(cx, |follower, window, cx| {
14737 follower.apply_update_proto(
14738 &project,
14739 pending_update.borrow_mut().take().unwrap(),
14740 window,
14741 cx,
14742 )
14743 })
14744 .unwrap()
14745 .await
14746 .unwrap();
14747 _ = follower.update(cx, |follower, _, cx| {
14748 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
14749 });
14750 assert!(*is_still_following.borrow());
14751
14752 // Extend the pending selection so that it surrounds another selection
14753 _ = leader.update(cx, |leader, window, cx| {
14754 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
14755 });
14756 follower
14757 .update(cx, |follower, window, cx| {
14758 follower.apply_update_proto(
14759 &project,
14760 pending_update.borrow_mut().take().unwrap(),
14761 window,
14762 cx,
14763 )
14764 })
14765 .unwrap()
14766 .await
14767 .unwrap();
14768 _ = follower.update(cx, |follower, _, cx| {
14769 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
14770 });
14771
14772 // Scrolling locally breaks the follow
14773 _ = follower.update(cx, |follower, window, cx| {
14774 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
14775 follower.set_scroll_anchor(
14776 ScrollAnchor {
14777 anchor: top_anchor,
14778 offset: gpui::Point::new(0.0, 0.5),
14779 },
14780 window,
14781 cx,
14782 );
14783 });
14784 assert!(!(*is_still_following.borrow()));
14785}
14786
14787#[gpui::test]
14788async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
14789 init_test(cx, |_| {});
14790
14791 let fs = FakeFs::new(cx.executor());
14792 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14793 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14794 let pane = workspace
14795 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14796 .unwrap();
14797
14798 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14799
14800 let leader = pane.update_in(cx, |_, window, cx| {
14801 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
14802 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
14803 });
14804
14805 // Start following the editor when it has no excerpts.
14806 let mut state_message =
14807 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14808 let workspace_entity = workspace.root(cx).unwrap();
14809 let follower_1 = cx
14810 .update_window(*workspace.deref(), |_, window, cx| {
14811 Editor::from_state_proto(
14812 workspace_entity,
14813 ViewId {
14814 creator: CollaboratorId::PeerId(PeerId::default()),
14815 id: 0,
14816 },
14817 &mut state_message,
14818 window,
14819 cx,
14820 )
14821 })
14822 .unwrap()
14823 .unwrap()
14824 .await
14825 .unwrap();
14826
14827 let update_message = Rc::new(RefCell::new(None));
14828 follower_1.update_in(cx, {
14829 let update = update_message.clone();
14830 |_, window, cx| {
14831 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
14832 leader.read(cx).add_event_to_update_proto(
14833 event,
14834 &mut update.borrow_mut(),
14835 window,
14836 cx,
14837 );
14838 })
14839 .detach();
14840 }
14841 });
14842
14843 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
14844 (
14845 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
14846 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
14847 )
14848 });
14849
14850 // Insert some excerpts.
14851 leader.update(cx, |leader, cx| {
14852 leader.buffer.update(cx, |multibuffer, cx| {
14853 multibuffer.set_excerpts_for_path(
14854 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
14855 buffer_1.clone(),
14856 vec![
14857 Point::row_range(0..3),
14858 Point::row_range(1..6),
14859 Point::row_range(12..15),
14860 ],
14861 0,
14862 cx,
14863 );
14864 multibuffer.set_excerpts_for_path(
14865 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
14866 buffer_2.clone(),
14867 vec![Point::row_range(0..6), Point::row_range(8..12)],
14868 0,
14869 cx,
14870 );
14871 });
14872 });
14873
14874 // Apply the update of adding the excerpts.
14875 follower_1
14876 .update_in(cx, |follower, window, cx| {
14877 follower.apply_update_proto(
14878 &project,
14879 update_message.borrow().clone().unwrap(),
14880 window,
14881 cx,
14882 )
14883 })
14884 .await
14885 .unwrap();
14886 assert_eq!(
14887 follower_1.update(cx, |editor, cx| editor.text(cx)),
14888 leader.update(cx, |editor, cx| editor.text(cx))
14889 );
14890 update_message.borrow_mut().take();
14891
14892 // Start following separately after it already has excerpts.
14893 let mut state_message =
14894 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14895 let workspace_entity = workspace.root(cx).unwrap();
14896 let follower_2 = cx
14897 .update_window(*workspace.deref(), |_, window, cx| {
14898 Editor::from_state_proto(
14899 workspace_entity,
14900 ViewId {
14901 creator: CollaboratorId::PeerId(PeerId::default()),
14902 id: 0,
14903 },
14904 &mut state_message,
14905 window,
14906 cx,
14907 )
14908 })
14909 .unwrap()
14910 .unwrap()
14911 .await
14912 .unwrap();
14913 assert_eq!(
14914 follower_2.update(cx, |editor, cx| editor.text(cx)),
14915 leader.update(cx, |editor, cx| editor.text(cx))
14916 );
14917
14918 // Remove some excerpts.
14919 leader.update(cx, |leader, cx| {
14920 leader.buffer.update(cx, |multibuffer, cx| {
14921 let excerpt_ids = multibuffer.excerpt_ids();
14922 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
14923 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
14924 });
14925 });
14926
14927 // Apply the update of removing the excerpts.
14928 follower_1
14929 .update_in(cx, |follower, window, cx| {
14930 follower.apply_update_proto(
14931 &project,
14932 update_message.borrow().clone().unwrap(),
14933 window,
14934 cx,
14935 )
14936 })
14937 .await
14938 .unwrap();
14939 follower_2
14940 .update_in(cx, |follower, window, cx| {
14941 follower.apply_update_proto(
14942 &project,
14943 update_message.borrow().clone().unwrap(),
14944 window,
14945 cx,
14946 )
14947 })
14948 .await
14949 .unwrap();
14950 update_message.borrow_mut().take();
14951 assert_eq!(
14952 follower_1.update(cx, |editor, cx| editor.text(cx)),
14953 leader.update(cx, |editor, cx| editor.text(cx))
14954 );
14955}
14956
14957#[gpui::test]
14958async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14959 init_test(cx, |_| {});
14960
14961 let mut cx = EditorTestContext::new(cx).await;
14962 let lsp_store =
14963 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
14964
14965 cx.set_state(indoc! {"
14966 ˇfn func(abc def: i32) -> u32 {
14967 }
14968 "});
14969
14970 cx.update(|_, cx| {
14971 lsp_store.update(cx, |lsp_store, cx| {
14972 lsp_store
14973 .update_diagnostics(
14974 LanguageServerId(0),
14975 lsp::PublishDiagnosticsParams {
14976 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
14977 version: None,
14978 diagnostics: vec![
14979 lsp::Diagnostic {
14980 range: lsp::Range::new(
14981 lsp::Position::new(0, 11),
14982 lsp::Position::new(0, 12),
14983 ),
14984 severity: Some(lsp::DiagnosticSeverity::ERROR),
14985 ..Default::default()
14986 },
14987 lsp::Diagnostic {
14988 range: lsp::Range::new(
14989 lsp::Position::new(0, 12),
14990 lsp::Position::new(0, 15),
14991 ),
14992 severity: Some(lsp::DiagnosticSeverity::ERROR),
14993 ..Default::default()
14994 },
14995 lsp::Diagnostic {
14996 range: lsp::Range::new(
14997 lsp::Position::new(0, 25),
14998 lsp::Position::new(0, 28),
14999 ),
15000 severity: Some(lsp::DiagnosticSeverity::ERROR),
15001 ..Default::default()
15002 },
15003 ],
15004 },
15005 None,
15006 DiagnosticSourceKind::Pushed,
15007 &[],
15008 cx,
15009 )
15010 .unwrap()
15011 });
15012 });
15013
15014 executor.run_until_parked();
15015
15016 cx.update_editor(|editor, window, cx| {
15017 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
15018 });
15019
15020 cx.assert_editor_state(indoc! {"
15021 fn func(abc def: i32) -> ˇu32 {
15022 }
15023 "});
15024
15025 cx.update_editor(|editor, window, cx| {
15026 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
15027 });
15028
15029 cx.assert_editor_state(indoc! {"
15030 fn func(abc ˇdef: i32) -> u32 {
15031 }
15032 "});
15033
15034 cx.update_editor(|editor, window, cx| {
15035 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
15036 });
15037
15038 cx.assert_editor_state(indoc! {"
15039 fn func(abcˇ def: i32) -> u32 {
15040 }
15041 "});
15042
15043 cx.update_editor(|editor, window, cx| {
15044 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
15045 });
15046
15047 cx.assert_editor_state(indoc! {"
15048 fn func(abc def: i32) -> ˇu32 {
15049 }
15050 "});
15051}
15052
15053#[gpui::test]
15054async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15055 init_test(cx, |_| {});
15056
15057 let mut cx = EditorTestContext::new(cx).await;
15058
15059 let diff_base = r#"
15060 use some::mod;
15061
15062 const A: u32 = 42;
15063
15064 fn main() {
15065 println!("hello");
15066
15067 println!("world");
15068 }
15069 "#
15070 .unindent();
15071
15072 // Edits are modified, removed, modified, added
15073 cx.set_state(
15074 &r#"
15075 use some::modified;
15076
15077 ˇ
15078 fn main() {
15079 println!("hello there");
15080
15081 println!("around the");
15082 println!("world");
15083 }
15084 "#
15085 .unindent(),
15086 );
15087
15088 cx.set_head_text(&diff_base);
15089 executor.run_until_parked();
15090
15091 cx.update_editor(|editor, window, cx| {
15092 //Wrap around the bottom of the buffer
15093 for _ in 0..3 {
15094 editor.go_to_next_hunk(&GoToHunk, window, cx);
15095 }
15096 });
15097
15098 cx.assert_editor_state(
15099 &r#"
15100 ˇuse some::modified;
15101
15102
15103 fn main() {
15104 println!("hello there");
15105
15106 println!("around the");
15107 println!("world");
15108 }
15109 "#
15110 .unindent(),
15111 );
15112
15113 cx.update_editor(|editor, window, cx| {
15114 //Wrap around the top of the buffer
15115 for _ in 0..2 {
15116 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15117 }
15118 });
15119
15120 cx.assert_editor_state(
15121 &r#"
15122 use some::modified;
15123
15124
15125 fn main() {
15126 ˇ println!("hello there");
15127
15128 println!("around the");
15129 println!("world");
15130 }
15131 "#
15132 .unindent(),
15133 );
15134
15135 cx.update_editor(|editor, window, cx| {
15136 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15137 });
15138
15139 cx.assert_editor_state(
15140 &r#"
15141 use some::modified;
15142
15143 ˇ
15144 fn main() {
15145 println!("hello there");
15146
15147 println!("around the");
15148 println!("world");
15149 }
15150 "#
15151 .unindent(),
15152 );
15153
15154 cx.update_editor(|editor, window, cx| {
15155 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15156 });
15157
15158 cx.assert_editor_state(
15159 &r#"
15160 ˇuse some::modified;
15161
15162
15163 fn main() {
15164 println!("hello there");
15165
15166 println!("around the");
15167 println!("world");
15168 }
15169 "#
15170 .unindent(),
15171 );
15172
15173 cx.update_editor(|editor, window, cx| {
15174 for _ in 0..2 {
15175 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15176 }
15177 });
15178
15179 cx.assert_editor_state(
15180 &r#"
15181 use some::modified;
15182
15183
15184 fn main() {
15185 ˇ println!("hello there");
15186
15187 println!("around the");
15188 println!("world");
15189 }
15190 "#
15191 .unindent(),
15192 );
15193
15194 cx.update_editor(|editor, window, cx| {
15195 editor.fold(&Fold, window, cx);
15196 });
15197
15198 cx.update_editor(|editor, window, cx| {
15199 editor.go_to_next_hunk(&GoToHunk, window, cx);
15200 });
15201
15202 cx.assert_editor_state(
15203 &r#"
15204 ˇuse some::modified;
15205
15206
15207 fn main() {
15208 println!("hello there");
15209
15210 println!("around the");
15211 println!("world");
15212 }
15213 "#
15214 .unindent(),
15215 );
15216}
15217
15218#[test]
15219fn test_split_words() {
15220 fn split(text: &str) -> Vec<&str> {
15221 split_words(text).collect()
15222 }
15223
15224 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
15225 assert_eq!(split("hello_world"), &["hello_", "world"]);
15226 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
15227 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
15228 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
15229 assert_eq!(split("helloworld"), &["helloworld"]);
15230
15231 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
15232}
15233
15234#[gpui::test]
15235async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
15236 init_test(cx, |_| {});
15237
15238 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
15239 let mut assert = |before, after| {
15240 let _state_context = cx.set_state(before);
15241 cx.run_until_parked();
15242 cx.update_editor(|editor, window, cx| {
15243 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
15244 });
15245 cx.run_until_parked();
15246 cx.assert_editor_state(after);
15247 };
15248
15249 // Outside bracket jumps to outside of matching bracket
15250 assert("console.logˇ(var);", "console.log(var)ˇ;");
15251 assert("console.log(var)ˇ;", "console.logˇ(var);");
15252
15253 // Inside bracket jumps to inside of matching bracket
15254 assert("console.log(ˇvar);", "console.log(varˇ);");
15255 assert("console.log(varˇ);", "console.log(ˇvar);");
15256
15257 // When outside a bracket and inside, favor jumping to the inside bracket
15258 assert(
15259 "console.log('foo', [1, 2, 3]ˇ);",
15260 "console.log(ˇ'foo', [1, 2, 3]);",
15261 );
15262 assert(
15263 "console.log(ˇ'foo', [1, 2, 3]);",
15264 "console.log('foo', [1, 2, 3]ˇ);",
15265 );
15266
15267 // Bias forward if two options are equally likely
15268 assert(
15269 "let result = curried_fun()ˇ();",
15270 "let result = curried_fun()()ˇ;",
15271 );
15272
15273 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
15274 assert(
15275 indoc! {"
15276 function test() {
15277 console.log('test')ˇ
15278 }"},
15279 indoc! {"
15280 function test() {
15281 console.logˇ('test')
15282 }"},
15283 );
15284}
15285
15286#[gpui::test]
15287async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
15288 init_test(cx, |_| {});
15289
15290 let fs = FakeFs::new(cx.executor());
15291 fs.insert_tree(
15292 path!("/a"),
15293 json!({
15294 "main.rs": "fn main() { let a = 5; }",
15295 "other.rs": "// Test file",
15296 }),
15297 )
15298 .await;
15299 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15300
15301 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15302 language_registry.add(Arc::new(Language::new(
15303 LanguageConfig {
15304 name: "Rust".into(),
15305 matcher: LanguageMatcher {
15306 path_suffixes: vec!["rs".to_string()],
15307 ..Default::default()
15308 },
15309 brackets: BracketPairConfig {
15310 pairs: vec![BracketPair {
15311 start: "{".to_string(),
15312 end: "}".to_string(),
15313 close: true,
15314 surround: true,
15315 newline: true,
15316 }],
15317 disabled_scopes_by_bracket_ix: Vec::new(),
15318 },
15319 ..Default::default()
15320 },
15321 Some(tree_sitter_rust::LANGUAGE.into()),
15322 )));
15323 let mut fake_servers = language_registry.register_fake_lsp(
15324 "Rust",
15325 FakeLspAdapter {
15326 capabilities: lsp::ServerCapabilities {
15327 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
15328 first_trigger_character: "{".to_string(),
15329 more_trigger_character: None,
15330 }),
15331 ..Default::default()
15332 },
15333 ..Default::default()
15334 },
15335 );
15336
15337 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15338
15339 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15340
15341 let worktree_id = workspace
15342 .update(cx, |workspace, _, cx| {
15343 workspace.project().update(cx, |project, cx| {
15344 project.worktrees(cx).next().unwrap().read(cx).id()
15345 })
15346 })
15347 .unwrap();
15348
15349 let buffer = project
15350 .update(cx, |project, cx| {
15351 project.open_local_buffer(path!("/a/main.rs"), cx)
15352 })
15353 .await
15354 .unwrap();
15355 let editor_handle = workspace
15356 .update(cx, |workspace, window, cx| {
15357 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
15358 })
15359 .unwrap()
15360 .await
15361 .unwrap()
15362 .downcast::<Editor>()
15363 .unwrap();
15364
15365 cx.executor().start_waiting();
15366 let fake_server = fake_servers.next().await.unwrap();
15367
15368 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
15369 |params, _| async move {
15370 assert_eq!(
15371 params.text_document_position.text_document.uri,
15372 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
15373 );
15374 assert_eq!(
15375 params.text_document_position.position,
15376 lsp::Position::new(0, 21),
15377 );
15378
15379 Ok(Some(vec![lsp::TextEdit {
15380 new_text: "]".to_string(),
15381 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15382 }]))
15383 },
15384 );
15385
15386 editor_handle.update_in(cx, |editor, window, cx| {
15387 window.focus(&editor.focus_handle(cx));
15388 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15389 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
15390 });
15391 editor.handle_input("{", window, cx);
15392 });
15393
15394 cx.executor().run_until_parked();
15395
15396 buffer.update(cx, |buffer, _| {
15397 assert_eq!(
15398 buffer.text(),
15399 "fn main() { let a = {5}; }",
15400 "No extra braces from on type formatting should appear in the buffer"
15401 )
15402 });
15403}
15404
15405#[gpui::test(iterations = 20, seeds(31))]
15406async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
15407 init_test(cx, |_| {});
15408
15409 let mut cx = EditorLspTestContext::new_rust(
15410 lsp::ServerCapabilities {
15411 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
15412 first_trigger_character: ".".to_string(),
15413 more_trigger_character: None,
15414 }),
15415 ..Default::default()
15416 },
15417 cx,
15418 )
15419 .await;
15420
15421 cx.update_buffer(|buffer, _| {
15422 // This causes autoindent to be async.
15423 buffer.set_sync_parse_timeout(Duration::ZERO)
15424 });
15425
15426 cx.set_state("fn c() {\n d()ˇ\n}\n");
15427 cx.simulate_keystroke("\n");
15428 cx.run_until_parked();
15429
15430 let buffer_cloned =
15431 cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap().clone());
15432 let mut request =
15433 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
15434 let buffer_cloned = buffer_cloned.clone();
15435 async move {
15436 buffer_cloned.update(&mut cx, |buffer, _| {
15437 assert_eq!(
15438 buffer.text(),
15439 "fn c() {\n d()\n .\n}\n",
15440 "OnTypeFormatting should triggered after autoindent applied"
15441 )
15442 })?;
15443
15444 Ok(Some(vec![]))
15445 }
15446 });
15447
15448 cx.simulate_keystroke(".");
15449 cx.run_until_parked();
15450
15451 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
15452 assert!(request.next().await.is_some());
15453 request.close();
15454 assert!(request.next().await.is_none());
15455}
15456
15457#[gpui::test]
15458async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
15459 init_test(cx, |_| {});
15460
15461 let fs = FakeFs::new(cx.executor());
15462 fs.insert_tree(
15463 path!("/a"),
15464 json!({
15465 "main.rs": "fn main() { let a = 5; }",
15466 "other.rs": "// Test file",
15467 }),
15468 )
15469 .await;
15470
15471 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15472
15473 let server_restarts = Arc::new(AtomicUsize::new(0));
15474 let closure_restarts = Arc::clone(&server_restarts);
15475 let language_server_name = "test language server";
15476 let language_name: LanguageName = "Rust".into();
15477
15478 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15479 language_registry.add(Arc::new(Language::new(
15480 LanguageConfig {
15481 name: language_name.clone(),
15482 matcher: LanguageMatcher {
15483 path_suffixes: vec!["rs".to_string()],
15484 ..Default::default()
15485 },
15486 ..Default::default()
15487 },
15488 Some(tree_sitter_rust::LANGUAGE.into()),
15489 )));
15490 let mut fake_servers = language_registry.register_fake_lsp(
15491 "Rust",
15492 FakeLspAdapter {
15493 name: language_server_name,
15494 initialization_options: Some(json!({
15495 "testOptionValue": true
15496 })),
15497 initializer: Some(Box::new(move |fake_server| {
15498 let task_restarts = Arc::clone(&closure_restarts);
15499 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
15500 task_restarts.fetch_add(1, atomic::Ordering::Release);
15501 futures::future::ready(Ok(()))
15502 });
15503 })),
15504 ..Default::default()
15505 },
15506 );
15507
15508 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15509 let _buffer = project
15510 .update(cx, |project, cx| {
15511 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
15512 })
15513 .await
15514 .unwrap();
15515 let _fake_server = fake_servers.next().await.unwrap();
15516 update_test_language_settings(cx, |language_settings| {
15517 language_settings.languages.0.insert(
15518 language_name.clone(),
15519 LanguageSettingsContent {
15520 tab_size: NonZeroU32::new(8),
15521 ..Default::default()
15522 },
15523 );
15524 });
15525 cx.executor().run_until_parked();
15526 assert_eq!(
15527 server_restarts.load(atomic::Ordering::Acquire),
15528 0,
15529 "Should not restart LSP server on an unrelated change"
15530 );
15531
15532 update_test_project_settings(cx, |project_settings| {
15533 project_settings.lsp.insert(
15534 "Some other server name".into(),
15535 LspSettings {
15536 binary: None,
15537 settings: None,
15538 initialization_options: Some(json!({
15539 "some other init value": false
15540 })),
15541 enable_lsp_tasks: false,
15542 },
15543 );
15544 });
15545 cx.executor().run_until_parked();
15546 assert_eq!(
15547 server_restarts.load(atomic::Ordering::Acquire),
15548 0,
15549 "Should not restart LSP server on an unrelated LSP settings change"
15550 );
15551
15552 update_test_project_settings(cx, |project_settings| {
15553 project_settings.lsp.insert(
15554 language_server_name.into(),
15555 LspSettings {
15556 binary: None,
15557 settings: None,
15558 initialization_options: Some(json!({
15559 "anotherInitValue": false
15560 })),
15561 enable_lsp_tasks: false,
15562 },
15563 );
15564 });
15565 cx.executor().run_until_parked();
15566 assert_eq!(
15567 server_restarts.load(atomic::Ordering::Acquire),
15568 1,
15569 "Should restart LSP server on a related LSP settings change"
15570 );
15571
15572 update_test_project_settings(cx, |project_settings| {
15573 project_settings.lsp.insert(
15574 language_server_name.into(),
15575 LspSettings {
15576 binary: None,
15577 settings: None,
15578 initialization_options: Some(json!({
15579 "anotherInitValue": false
15580 })),
15581 enable_lsp_tasks: false,
15582 },
15583 );
15584 });
15585 cx.executor().run_until_parked();
15586 assert_eq!(
15587 server_restarts.load(atomic::Ordering::Acquire),
15588 1,
15589 "Should not restart LSP server on a related LSP settings change that is the same"
15590 );
15591
15592 update_test_project_settings(cx, |project_settings| {
15593 project_settings.lsp.insert(
15594 language_server_name.into(),
15595 LspSettings {
15596 binary: None,
15597 settings: None,
15598 initialization_options: None,
15599 enable_lsp_tasks: false,
15600 },
15601 );
15602 });
15603 cx.executor().run_until_parked();
15604 assert_eq!(
15605 server_restarts.load(atomic::Ordering::Acquire),
15606 2,
15607 "Should restart LSP server on another related LSP settings change"
15608 );
15609}
15610
15611#[gpui::test]
15612async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
15613 init_test(cx, |_| {});
15614
15615 let mut cx = EditorLspTestContext::new_rust(
15616 lsp::ServerCapabilities {
15617 completion_provider: Some(lsp::CompletionOptions {
15618 trigger_characters: Some(vec![".".to_string()]),
15619 resolve_provider: Some(true),
15620 ..Default::default()
15621 }),
15622 ..Default::default()
15623 },
15624 cx,
15625 )
15626 .await;
15627
15628 cx.set_state("fn main() { let a = 2ˇ; }");
15629 cx.simulate_keystroke(".");
15630 let completion_item = lsp::CompletionItem {
15631 label: "some".into(),
15632 kind: Some(lsp::CompletionItemKind::SNIPPET),
15633 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15634 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15635 kind: lsp::MarkupKind::Markdown,
15636 value: "```rust\nSome(2)\n```".to_string(),
15637 })),
15638 deprecated: Some(false),
15639 sort_text: Some("fffffff2".to_string()),
15640 filter_text: Some("some".to_string()),
15641 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15642 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15643 range: lsp::Range {
15644 start: lsp::Position {
15645 line: 0,
15646 character: 22,
15647 },
15648 end: lsp::Position {
15649 line: 0,
15650 character: 22,
15651 },
15652 },
15653 new_text: "Some(2)".to_string(),
15654 })),
15655 additional_text_edits: Some(vec![lsp::TextEdit {
15656 range: lsp::Range {
15657 start: lsp::Position {
15658 line: 0,
15659 character: 20,
15660 },
15661 end: lsp::Position {
15662 line: 0,
15663 character: 22,
15664 },
15665 },
15666 new_text: "".to_string(),
15667 }]),
15668 ..Default::default()
15669 };
15670
15671 let closure_completion_item = completion_item.clone();
15672 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15673 let task_completion_item = closure_completion_item.clone();
15674 async move {
15675 Ok(Some(lsp::CompletionResponse::Array(vec![
15676 task_completion_item,
15677 ])))
15678 }
15679 });
15680
15681 request.next().await;
15682
15683 cx.condition(|editor, _| editor.context_menu_visible())
15684 .await;
15685 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15686 editor
15687 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15688 .unwrap()
15689 });
15690 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
15691
15692 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
15693 let task_completion_item = completion_item.clone();
15694 async move { Ok(task_completion_item) }
15695 })
15696 .next()
15697 .await
15698 .unwrap();
15699 apply_additional_edits.await.unwrap();
15700 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
15701}
15702
15703#[gpui::test]
15704async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
15705 init_test(cx, |_| {});
15706
15707 let mut cx = EditorLspTestContext::new_rust(
15708 lsp::ServerCapabilities {
15709 completion_provider: Some(lsp::CompletionOptions {
15710 trigger_characters: Some(vec![".".to_string()]),
15711 resolve_provider: Some(true),
15712 ..Default::default()
15713 }),
15714 ..Default::default()
15715 },
15716 cx,
15717 )
15718 .await;
15719
15720 cx.set_state("fn main() { let a = 2ˇ; }");
15721 cx.simulate_keystroke(".");
15722
15723 let item1 = lsp::CompletionItem {
15724 label: "method id()".to_string(),
15725 filter_text: Some("id".to_string()),
15726 detail: None,
15727 documentation: None,
15728 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15729 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15730 new_text: ".id".to_string(),
15731 })),
15732 ..lsp::CompletionItem::default()
15733 };
15734
15735 let item2 = lsp::CompletionItem {
15736 label: "other".to_string(),
15737 filter_text: Some("other".to_string()),
15738 detail: None,
15739 documentation: None,
15740 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15741 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15742 new_text: ".other".to_string(),
15743 })),
15744 ..lsp::CompletionItem::default()
15745 };
15746
15747 let item1 = item1.clone();
15748 cx.set_request_handler::<lsp::request::Completion, _, _>({
15749 let item1 = item1.clone();
15750 move |_, _, _| {
15751 let item1 = item1.clone();
15752 let item2 = item2.clone();
15753 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
15754 }
15755 })
15756 .next()
15757 .await;
15758
15759 cx.condition(|editor, _| editor.context_menu_visible())
15760 .await;
15761 cx.update_editor(|editor, _, _| {
15762 let context_menu = editor.context_menu.borrow_mut();
15763 let context_menu = context_menu
15764 .as_ref()
15765 .expect("Should have the context menu deployed");
15766 match context_menu {
15767 CodeContextMenu::Completions(completions_menu) => {
15768 let completions = completions_menu.completions.borrow_mut();
15769 assert_eq!(
15770 completions
15771 .iter()
15772 .map(|completion| &completion.label.text)
15773 .collect::<Vec<_>>(),
15774 vec!["method id()", "other"]
15775 )
15776 }
15777 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15778 }
15779 });
15780
15781 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
15782 let item1 = item1.clone();
15783 move |_, item_to_resolve, _| {
15784 let item1 = item1.clone();
15785 async move {
15786 if item1 == item_to_resolve {
15787 Ok(lsp::CompletionItem {
15788 label: "method id()".to_string(),
15789 filter_text: Some("id".to_string()),
15790 detail: Some("Now resolved!".to_string()),
15791 documentation: Some(lsp::Documentation::String("Docs".to_string())),
15792 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15793 range: lsp::Range::new(
15794 lsp::Position::new(0, 22),
15795 lsp::Position::new(0, 22),
15796 ),
15797 new_text: ".id".to_string(),
15798 })),
15799 ..lsp::CompletionItem::default()
15800 })
15801 } else {
15802 Ok(item_to_resolve)
15803 }
15804 }
15805 }
15806 })
15807 .next()
15808 .await
15809 .unwrap();
15810 cx.run_until_parked();
15811
15812 cx.update_editor(|editor, window, cx| {
15813 editor.context_menu_next(&Default::default(), window, cx);
15814 });
15815
15816 cx.update_editor(|editor, _, _| {
15817 let context_menu = editor.context_menu.borrow_mut();
15818 let context_menu = context_menu
15819 .as_ref()
15820 .expect("Should have the context menu deployed");
15821 match context_menu {
15822 CodeContextMenu::Completions(completions_menu) => {
15823 let completions = completions_menu.completions.borrow_mut();
15824 assert_eq!(
15825 completions
15826 .iter()
15827 .map(|completion| &completion.label.text)
15828 .collect::<Vec<_>>(),
15829 vec!["method id() Now resolved!", "other"],
15830 "Should update first completion label, but not second as the filter text did not match."
15831 );
15832 }
15833 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15834 }
15835 });
15836}
15837
15838#[gpui::test]
15839async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
15840 init_test(cx, |_| {});
15841 let mut cx = EditorLspTestContext::new_rust(
15842 lsp::ServerCapabilities {
15843 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
15844 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
15845 completion_provider: Some(lsp::CompletionOptions {
15846 resolve_provider: Some(true),
15847 ..Default::default()
15848 }),
15849 ..Default::default()
15850 },
15851 cx,
15852 )
15853 .await;
15854 cx.set_state(indoc! {"
15855 struct TestStruct {
15856 field: i32
15857 }
15858
15859 fn mainˇ() {
15860 let unused_var = 42;
15861 let test_struct = TestStruct { field: 42 };
15862 }
15863 "});
15864 let symbol_range = cx.lsp_range(indoc! {"
15865 struct TestStruct {
15866 field: i32
15867 }
15868
15869 «fn main»() {
15870 let unused_var = 42;
15871 let test_struct = TestStruct { field: 42 };
15872 }
15873 "});
15874 let mut hover_requests =
15875 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
15876 Ok(Some(lsp::Hover {
15877 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
15878 kind: lsp::MarkupKind::Markdown,
15879 value: "Function documentation".to_string(),
15880 }),
15881 range: Some(symbol_range),
15882 }))
15883 });
15884
15885 // Case 1: Test that code action menu hide hover popover
15886 cx.dispatch_action(Hover);
15887 hover_requests.next().await;
15888 cx.condition(|editor, _| editor.hover_state.visible()).await;
15889 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
15890 move |_, _, _| async move {
15891 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
15892 lsp::CodeAction {
15893 title: "Remove unused variable".to_string(),
15894 kind: Some(CodeActionKind::QUICKFIX),
15895 edit: Some(lsp::WorkspaceEdit {
15896 changes: Some(
15897 [(
15898 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
15899 vec![lsp::TextEdit {
15900 range: lsp::Range::new(
15901 lsp::Position::new(5, 4),
15902 lsp::Position::new(5, 27),
15903 ),
15904 new_text: "".to_string(),
15905 }],
15906 )]
15907 .into_iter()
15908 .collect(),
15909 ),
15910 ..Default::default()
15911 }),
15912 ..Default::default()
15913 },
15914 )]))
15915 },
15916 );
15917 cx.update_editor(|editor, window, cx| {
15918 editor.toggle_code_actions(
15919 &ToggleCodeActions {
15920 deployed_from: None,
15921 quick_launch: false,
15922 },
15923 window,
15924 cx,
15925 );
15926 });
15927 code_action_requests.next().await;
15928 cx.run_until_parked();
15929 cx.condition(|editor, _| editor.context_menu_visible())
15930 .await;
15931 cx.update_editor(|editor, _, _| {
15932 assert!(
15933 !editor.hover_state.visible(),
15934 "Hover popover should be hidden when code action menu is shown"
15935 );
15936 // Hide code actions
15937 editor.context_menu.take();
15938 });
15939
15940 // Case 2: Test that code completions hide hover popover
15941 cx.dispatch_action(Hover);
15942 hover_requests.next().await;
15943 cx.condition(|editor, _| editor.hover_state.visible()).await;
15944 let counter = Arc::new(AtomicUsize::new(0));
15945 let mut completion_requests =
15946 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15947 let counter = counter.clone();
15948 async move {
15949 counter.fetch_add(1, atomic::Ordering::Release);
15950 Ok(Some(lsp::CompletionResponse::Array(vec![
15951 lsp::CompletionItem {
15952 label: "main".into(),
15953 kind: Some(lsp::CompletionItemKind::FUNCTION),
15954 detail: Some("() -> ()".to_string()),
15955 ..Default::default()
15956 },
15957 lsp::CompletionItem {
15958 label: "TestStruct".into(),
15959 kind: Some(lsp::CompletionItemKind::STRUCT),
15960 detail: Some("struct TestStruct".to_string()),
15961 ..Default::default()
15962 },
15963 ])))
15964 }
15965 });
15966 cx.update_editor(|editor, window, cx| {
15967 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15968 });
15969 completion_requests.next().await;
15970 cx.condition(|editor, _| editor.context_menu_visible())
15971 .await;
15972 cx.update_editor(|editor, _, _| {
15973 assert!(
15974 !editor.hover_state.visible(),
15975 "Hover popover should be hidden when completion menu is shown"
15976 );
15977 });
15978}
15979
15980#[gpui::test]
15981async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
15982 init_test(cx, |_| {});
15983
15984 let mut cx = EditorLspTestContext::new_rust(
15985 lsp::ServerCapabilities {
15986 completion_provider: Some(lsp::CompletionOptions {
15987 trigger_characters: Some(vec![".".to_string()]),
15988 resolve_provider: Some(true),
15989 ..Default::default()
15990 }),
15991 ..Default::default()
15992 },
15993 cx,
15994 )
15995 .await;
15996
15997 cx.set_state("fn main() { let a = 2ˇ; }");
15998 cx.simulate_keystroke(".");
15999
16000 let unresolved_item_1 = lsp::CompletionItem {
16001 label: "id".to_string(),
16002 filter_text: Some("id".to_string()),
16003 detail: None,
16004 documentation: None,
16005 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16006 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16007 new_text: ".id".to_string(),
16008 })),
16009 ..lsp::CompletionItem::default()
16010 };
16011 let resolved_item_1 = lsp::CompletionItem {
16012 additional_text_edits: Some(vec![lsp::TextEdit {
16013 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
16014 new_text: "!!".to_string(),
16015 }]),
16016 ..unresolved_item_1.clone()
16017 };
16018 let unresolved_item_2 = lsp::CompletionItem {
16019 label: "other".to_string(),
16020 filter_text: Some("other".to_string()),
16021 detail: None,
16022 documentation: None,
16023 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16024 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16025 new_text: ".other".to_string(),
16026 })),
16027 ..lsp::CompletionItem::default()
16028 };
16029 let resolved_item_2 = lsp::CompletionItem {
16030 additional_text_edits: Some(vec![lsp::TextEdit {
16031 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
16032 new_text: "??".to_string(),
16033 }]),
16034 ..unresolved_item_2.clone()
16035 };
16036
16037 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
16038 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
16039 cx.lsp
16040 .server
16041 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
16042 let unresolved_item_1 = unresolved_item_1.clone();
16043 let resolved_item_1 = resolved_item_1.clone();
16044 let unresolved_item_2 = unresolved_item_2.clone();
16045 let resolved_item_2 = resolved_item_2.clone();
16046 let resolve_requests_1 = resolve_requests_1.clone();
16047 let resolve_requests_2 = resolve_requests_2.clone();
16048 move |unresolved_request, _| {
16049 let unresolved_item_1 = unresolved_item_1.clone();
16050 let resolved_item_1 = resolved_item_1.clone();
16051 let unresolved_item_2 = unresolved_item_2.clone();
16052 let resolved_item_2 = resolved_item_2.clone();
16053 let resolve_requests_1 = resolve_requests_1.clone();
16054 let resolve_requests_2 = resolve_requests_2.clone();
16055 async move {
16056 if unresolved_request == unresolved_item_1 {
16057 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
16058 Ok(resolved_item_1.clone())
16059 } else if unresolved_request == unresolved_item_2 {
16060 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
16061 Ok(resolved_item_2.clone())
16062 } else {
16063 panic!("Unexpected completion item {unresolved_request:?}")
16064 }
16065 }
16066 }
16067 })
16068 .detach();
16069
16070 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16071 let unresolved_item_1 = unresolved_item_1.clone();
16072 let unresolved_item_2 = unresolved_item_2.clone();
16073 async move {
16074 Ok(Some(lsp::CompletionResponse::Array(vec![
16075 unresolved_item_1,
16076 unresolved_item_2,
16077 ])))
16078 }
16079 })
16080 .next()
16081 .await;
16082
16083 cx.condition(|editor, _| editor.context_menu_visible())
16084 .await;
16085 cx.update_editor(|editor, _, _| {
16086 let context_menu = editor.context_menu.borrow_mut();
16087 let context_menu = context_menu
16088 .as_ref()
16089 .expect("Should have the context menu deployed");
16090 match context_menu {
16091 CodeContextMenu::Completions(completions_menu) => {
16092 let completions = completions_menu.completions.borrow_mut();
16093 assert_eq!(
16094 completions
16095 .iter()
16096 .map(|completion| &completion.label.text)
16097 .collect::<Vec<_>>(),
16098 vec!["id", "other"]
16099 )
16100 }
16101 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
16102 }
16103 });
16104 cx.run_until_parked();
16105
16106 cx.update_editor(|editor, window, cx| {
16107 editor.context_menu_next(&ContextMenuNext, window, cx);
16108 });
16109 cx.run_until_parked();
16110 cx.update_editor(|editor, window, cx| {
16111 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
16112 });
16113 cx.run_until_parked();
16114 cx.update_editor(|editor, window, cx| {
16115 editor.context_menu_next(&ContextMenuNext, window, cx);
16116 });
16117 cx.run_until_parked();
16118 cx.update_editor(|editor, window, cx| {
16119 editor
16120 .compose_completion(&ComposeCompletion::default(), window, cx)
16121 .expect("No task returned")
16122 })
16123 .await
16124 .expect("Completion failed");
16125 cx.run_until_parked();
16126
16127 cx.update_editor(|editor, _, cx| {
16128 assert_eq!(
16129 resolve_requests_1.load(atomic::Ordering::Acquire),
16130 1,
16131 "Should always resolve once despite multiple selections"
16132 );
16133 assert_eq!(
16134 resolve_requests_2.load(atomic::Ordering::Acquire),
16135 1,
16136 "Should always resolve once after multiple selections and applying the completion"
16137 );
16138 assert_eq!(
16139 editor.text(cx),
16140 "fn main() { let a = ??.other; }",
16141 "Should use resolved data when applying the completion"
16142 );
16143 });
16144}
16145
16146#[gpui::test]
16147async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
16148 init_test(cx, |_| {});
16149
16150 let item_0 = lsp::CompletionItem {
16151 label: "abs".into(),
16152 insert_text: Some("abs".into()),
16153 data: Some(json!({ "very": "special"})),
16154 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
16155 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
16156 lsp::InsertReplaceEdit {
16157 new_text: "abs".to_string(),
16158 insert: lsp::Range::default(),
16159 replace: lsp::Range::default(),
16160 },
16161 )),
16162 ..lsp::CompletionItem::default()
16163 };
16164 let items = iter::once(item_0.clone())
16165 .chain((11..51).map(|i| lsp::CompletionItem {
16166 label: format!("item_{}", i),
16167 insert_text: Some(format!("item_{}", i)),
16168 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
16169 ..lsp::CompletionItem::default()
16170 }))
16171 .collect::<Vec<_>>();
16172
16173 let default_commit_characters = vec!["?".to_string()];
16174 let default_data = json!({ "default": "data"});
16175 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
16176 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
16177 let default_edit_range = lsp::Range {
16178 start: lsp::Position {
16179 line: 0,
16180 character: 5,
16181 },
16182 end: lsp::Position {
16183 line: 0,
16184 character: 5,
16185 },
16186 };
16187
16188 let mut cx = EditorLspTestContext::new_rust(
16189 lsp::ServerCapabilities {
16190 completion_provider: Some(lsp::CompletionOptions {
16191 trigger_characters: Some(vec![".".to_string()]),
16192 resolve_provider: Some(true),
16193 ..Default::default()
16194 }),
16195 ..Default::default()
16196 },
16197 cx,
16198 )
16199 .await;
16200
16201 cx.set_state("fn main() { let a = 2ˇ; }");
16202 cx.simulate_keystroke(".");
16203
16204 let completion_data = default_data.clone();
16205 let completion_characters = default_commit_characters.clone();
16206 let completion_items = items.clone();
16207 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16208 let default_data = completion_data.clone();
16209 let default_commit_characters = completion_characters.clone();
16210 let items = completion_items.clone();
16211 async move {
16212 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16213 items,
16214 item_defaults: Some(lsp::CompletionListItemDefaults {
16215 data: Some(default_data.clone()),
16216 commit_characters: Some(default_commit_characters.clone()),
16217 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
16218 default_edit_range,
16219 )),
16220 insert_text_format: Some(default_insert_text_format),
16221 insert_text_mode: Some(default_insert_text_mode),
16222 }),
16223 ..lsp::CompletionList::default()
16224 })))
16225 }
16226 })
16227 .next()
16228 .await;
16229
16230 let resolved_items = Arc::new(Mutex::new(Vec::new()));
16231 cx.lsp
16232 .server
16233 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
16234 let closure_resolved_items = resolved_items.clone();
16235 move |item_to_resolve, _| {
16236 let closure_resolved_items = closure_resolved_items.clone();
16237 async move {
16238 closure_resolved_items.lock().push(item_to_resolve.clone());
16239 Ok(item_to_resolve)
16240 }
16241 }
16242 })
16243 .detach();
16244
16245 cx.condition(|editor, _| editor.context_menu_visible())
16246 .await;
16247 cx.run_until_parked();
16248 cx.update_editor(|editor, _, _| {
16249 let menu = editor.context_menu.borrow_mut();
16250 match menu.as_ref().expect("should have the completions menu") {
16251 CodeContextMenu::Completions(completions_menu) => {
16252 assert_eq!(
16253 completions_menu
16254 .entries
16255 .borrow()
16256 .iter()
16257 .map(|mat| mat.string.clone())
16258 .collect::<Vec<String>>(),
16259 items
16260 .iter()
16261 .map(|completion| completion.label.clone())
16262 .collect::<Vec<String>>()
16263 );
16264 }
16265 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
16266 }
16267 });
16268 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
16269 // with 4 from the end.
16270 assert_eq!(
16271 *resolved_items.lock(),
16272 [&items[0..16], &items[items.len() - 4..items.len()]]
16273 .concat()
16274 .iter()
16275 .cloned()
16276 .map(|mut item| {
16277 if item.data.is_none() {
16278 item.data = Some(default_data.clone());
16279 }
16280 item
16281 })
16282 .collect::<Vec<lsp::CompletionItem>>(),
16283 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
16284 );
16285 resolved_items.lock().clear();
16286
16287 cx.update_editor(|editor, window, cx| {
16288 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
16289 });
16290 cx.run_until_parked();
16291 // Completions that have already been resolved are skipped.
16292 assert_eq!(
16293 *resolved_items.lock(),
16294 items[items.len() - 17..items.len() - 4]
16295 .iter()
16296 .cloned()
16297 .map(|mut item| {
16298 if item.data.is_none() {
16299 item.data = Some(default_data.clone());
16300 }
16301 item
16302 })
16303 .collect::<Vec<lsp::CompletionItem>>()
16304 );
16305 resolved_items.lock().clear();
16306}
16307
16308#[gpui::test]
16309async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
16310 init_test(cx, |_| {});
16311
16312 let mut cx = EditorLspTestContext::new(
16313 Language::new(
16314 LanguageConfig {
16315 matcher: LanguageMatcher {
16316 path_suffixes: vec!["jsx".into()],
16317 ..Default::default()
16318 },
16319 overrides: [(
16320 "element".into(),
16321 LanguageConfigOverride {
16322 completion_query_characters: Override::Set(['-'].into_iter().collect()),
16323 ..Default::default()
16324 },
16325 )]
16326 .into_iter()
16327 .collect(),
16328 ..Default::default()
16329 },
16330 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16331 )
16332 .with_override_query("(jsx_self_closing_element) @element")
16333 .unwrap(),
16334 lsp::ServerCapabilities {
16335 completion_provider: Some(lsp::CompletionOptions {
16336 trigger_characters: Some(vec![":".to_string()]),
16337 ..Default::default()
16338 }),
16339 ..Default::default()
16340 },
16341 cx,
16342 )
16343 .await;
16344
16345 cx.lsp
16346 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16347 Ok(Some(lsp::CompletionResponse::Array(vec![
16348 lsp::CompletionItem {
16349 label: "bg-blue".into(),
16350 ..Default::default()
16351 },
16352 lsp::CompletionItem {
16353 label: "bg-red".into(),
16354 ..Default::default()
16355 },
16356 lsp::CompletionItem {
16357 label: "bg-yellow".into(),
16358 ..Default::default()
16359 },
16360 ])))
16361 });
16362
16363 cx.set_state(r#"<p class="bgˇ" />"#);
16364
16365 // Trigger completion when typing a dash, because the dash is an extra
16366 // word character in the 'element' scope, which contains the cursor.
16367 cx.simulate_keystroke("-");
16368 cx.executor().run_until_parked();
16369 cx.update_editor(|editor, _, _| {
16370 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16371 {
16372 assert_eq!(
16373 completion_menu_entries(&menu),
16374 &["bg-blue", "bg-red", "bg-yellow"]
16375 );
16376 } else {
16377 panic!("expected completion menu to be open");
16378 }
16379 });
16380
16381 cx.simulate_keystroke("l");
16382 cx.executor().run_until_parked();
16383 cx.update_editor(|editor, _, _| {
16384 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16385 {
16386 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
16387 } else {
16388 panic!("expected completion menu to be open");
16389 }
16390 });
16391
16392 // When filtering completions, consider the character after the '-' to
16393 // be the start of a subword.
16394 cx.set_state(r#"<p class="yelˇ" />"#);
16395 cx.simulate_keystroke("l");
16396 cx.executor().run_until_parked();
16397 cx.update_editor(|editor, _, _| {
16398 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16399 {
16400 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
16401 } else {
16402 panic!("expected completion menu to be open");
16403 }
16404 });
16405}
16406
16407fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
16408 let entries = menu.entries.borrow();
16409 entries.iter().map(|mat| mat.string.clone()).collect()
16410}
16411
16412#[gpui::test]
16413async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
16414 init_test(cx, |settings| {
16415 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
16416 Formatter::Prettier,
16417 )))
16418 });
16419
16420 let fs = FakeFs::new(cx.executor());
16421 fs.insert_file(path!("/file.ts"), Default::default()).await;
16422
16423 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
16424 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16425
16426 language_registry.add(Arc::new(Language::new(
16427 LanguageConfig {
16428 name: "TypeScript".into(),
16429 matcher: LanguageMatcher {
16430 path_suffixes: vec!["ts".to_string()],
16431 ..Default::default()
16432 },
16433 ..Default::default()
16434 },
16435 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
16436 )));
16437 update_test_language_settings(cx, |settings| {
16438 settings.defaults.prettier = Some(PrettierSettings {
16439 allowed: true,
16440 ..PrettierSettings::default()
16441 });
16442 });
16443
16444 let test_plugin = "test_plugin";
16445 let _ = language_registry.register_fake_lsp(
16446 "TypeScript",
16447 FakeLspAdapter {
16448 prettier_plugins: vec![test_plugin],
16449 ..Default::default()
16450 },
16451 );
16452
16453 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
16454 let buffer = project
16455 .update(cx, |project, cx| {
16456 project.open_local_buffer(path!("/file.ts"), cx)
16457 })
16458 .await
16459 .unwrap();
16460
16461 let buffer_text = "one\ntwo\nthree\n";
16462 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16463 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16464 editor.update_in(cx, |editor, window, cx| {
16465 editor.set_text(buffer_text, window, cx)
16466 });
16467
16468 editor
16469 .update_in(cx, |editor, window, cx| {
16470 editor.perform_format(
16471 project.clone(),
16472 FormatTrigger::Manual,
16473 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16474 window,
16475 cx,
16476 )
16477 })
16478 .unwrap()
16479 .await;
16480 assert_eq!(
16481 editor.update(cx, |editor, cx| editor.text(cx)),
16482 buffer_text.to_string() + prettier_format_suffix,
16483 "Test prettier formatting was not applied to the original buffer text",
16484 );
16485
16486 update_test_language_settings(cx, |settings| {
16487 settings.defaults.formatter = Some(SelectedFormatter::Auto)
16488 });
16489 let format = editor.update_in(cx, |editor, window, cx| {
16490 editor.perform_format(
16491 project.clone(),
16492 FormatTrigger::Manual,
16493 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16494 window,
16495 cx,
16496 )
16497 });
16498 format.await.unwrap();
16499 assert_eq!(
16500 editor.update(cx, |editor, cx| editor.text(cx)),
16501 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
16502 "Autoformatting (via test prettier) was not applied to the original buffer text",
16503 );
16504}
16505
16506#[gpui::test]
16507async fn test_addition_reverts(cx: &mut TestAppContext) {
16508 init_test(cx, |_| {});
16509 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16510 let base_text = indoc! {r#"
16511 struct Row;
16512 struct Row1;
16513 struct Row2;
16514
16515 struct Row4;
16516 struct Row5;
16517 struct Row6;
16518
16519 struct Row8;
16520 struct Row9;
16521 struct Row10;"#};
16522
16523 // When addition hunks are not adjacent to carets, no hunk revert is performed
16524 assert_hunk_revert(
16525 indoc! {r#"struct Row;
16526 struct Row1;
16527 struct Row1.1;
16528 struct Row1.2;
16529 struct Row2;ˇ
16530
16531 struct Row4;
16532 struct Row5;
16533 struct Row6;
16534
16535 struct Row8;
16536 ˇstruct Row9;
16537 struct Row9.1;
16538 struct Row9.2;
16539 struct Row9.3;
16540 struct Row10;"#},
16541 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16542 indoc! {r#"struct Row;
16543 struct Row1;
16544 struct Row1.1;
16545 struct Row1.2;
16546 struct Row2;ˇ
16547
16548 struct Row4;
16549 struct Row5;
16550 struct Row6;
16551
16552 struct Row8;
16553 ˇstruct Row9;
16554 struct Row9.1;
16555 struct Row9.2;
16556 struct Row9.3;
16557 struct Row10;"#},
16558 base_text,
16559 &mut cx,
16560 );
16561 // Same for selections
16562 assert_hunk_revert(
16563 indoc! {r#"struct Row;
16564 struct Row1;
16565 struct Row2;
16566 struct Row2.1;
16567 struct Row2.2;
16568 «ˇ
16569 struct Row4;
16570 struct» Row5;
16571 «struct Row6;
16572 ˇ»
16573 struct Row9.1;
16574 struct Row9.2;
16575 struct Row9.3;
16576 struct Row8;
16577 struct Row9;
16578 struct Row10;"#},
16579 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16580 indoc! {r#"struct Row;
16581 struct Row1;
16582 struct Row2;
16583 struct Row2.1;
16584 struct Row2.2;
16585 «ˇ
16586 struct Row4;
16587 struct» Row5;
16588 «struct Row6;
16589 ˇ»
16590 struct Row9.1;
16591 struct Row9.2;
16592 struct Row9.3;
16593 struct Row8;
16594 struct Row9;
16595 struct Row10;"#},
16596 base_text,
16597 &mut cx,
16598 );
16599
16600 // When carets and selections intersect the addition hunks, those are reverted.
16601 // Adjacent carets got merged.
16602 assert_hunk_revert(
16603 indoc! {r#"struct Row;
16604 ˇ// something on the top
16605 struct Row1;
16606 struct Row2;
16607 struct Roˇw3.1;
16608 struct Row2.2;
16609 struct Row2.3;ˇ
16610
16611 struct Row4;
16612 struct ˇRow5.1;
16613 struct Row5.2;
16614 struct «Rowˇ»5.3;
16615 struct Row5;
16616 struct Row6;
16617 ˇ
16618 struct Row9.1;
16619 struct «Rowˇ»9.2;
16620 struct «ˇRow»9.3;
16621 struct Row8;
16622 struct Row9;
16623 «ˇ// something on bottom»
16624 struct Row10;"#},
16625 vec![
16626 DiffHunkStatusKind::Added,
16627 DiffHunkStatusKind::Added,
16628 DiffHunkStatusKind::Added,
16629 DiffHunkStatusKind::Added,
16630 DiffHunkStatusKind::Added,
16631 ],
16632 indoc! {r#"struct Row;
16633 ˇstruct Row1;
16634 struct Row2;
16635 ˇ
16636 struct Row4;
16637 ˇstruct Row5;
16638 struct Row6;
16639 ˇ
16640 ˇstruct Row8;
16641 struct Row9;
16642 ˇstruct Row10;"#},
16643 base_text,
16644 &mut cx,
16645 );
16646}
16647
16648#[gpui::test]
16649async fn test_modification_reverts(cx: &mut TestAppContext) {
16650 init_test(cx, |_| {});
16651 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16652 let base_text = indoc! {r#"
16653 struct Row;
16654 struct Row1;
16655 struct Row2;
16656
16657 struct Row4;
16658 struct Row5;
16659 struct Row6;
16660
16661 struct Row8;
16662 struct Row9;
16663 struct Row10;"#};
16664
16665 // Modification hunks behave the same as the addition ones.
16666 assert_hunk_revert(
16667 indoc! {r#"struct Row;
16668 struct Row1;
16669 struct Row33;
16670 ˇ
16671 struct Row4;
16672 struct Row5;
16673 struct Row6;
16674 ˇ
16675 struct Row99;
16676 struct Row9;
16677 struct Row10;"#},
16678 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16679 indoc! {r#"struct Row;
16680 struct Row1;
16681 struct Row33;
16682 ˇ
16683 struct Row4;
16684 struct Row5;
16685 struct Row6;
16686 ˇ
16687 struct Row99;
16688 struct Row9;
16689 struct Row10;"#},
16690 base_text,
16691 &mut cx,
16692 );
16693 assert_hunk_revert(
16694 indoc! {r#"struct Row;
16695 struct Row1;
16696 struct Row33;
16697 «ˇ
16698 struct Row4;
16699 struct» Row5;
16700 «struct Row6;
16701 ˇ»
16702 struct Row99;
16703 struct Row9;
16704 struct Row10;"#},
16705 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16706 indoc! {r#"struct Row;
16707 struct Row1;
16708 struct Row33;
16709 «ˇ
16710 struct Row4;
16711 struct» Row5;
16712 «struct Row6;
16713 ˇ»
16714 struct Row99;
16715 struct Row9;
16716 struct Row10;"#},
16717 base_text,
16718 &mut cx,
16719 );
16720
16721 assert_hunk_revert(
16722 indoc! {r#"ˇstruct Row1.1;
16723 struct Row1;
16724 «ˇstr»uct Row22;
16725
16726 struct ˇRow44;
16727 struct Row5;
16728 struct «Rˇ»ow66;ˇ
16729
16730 «struˇ»ct Row88;
16731 struct Row9;
16732 struct Row1011;ˇ"#},
16733 vec![
16734 DiffHunkStatusKind::Modified,
16735 DiffHunkStatusKind::Modified,
16736 DiffHunkStatusKind::Modified,
16737 DiffHunkStatusKind::Modified,
16738 DiffHunkStatusKind::Modified,
16739 DiffHunkStatusKind::Modified,
16740 ],
16741 indoc! {r#"struct Row;
16742 ˇstruct Row1;
16743 struct Row2;
16744 ˇ
16745 struct Row4;
16746 ˇstruct Row5;
16747 struct Row6;
16748 ˇ
16749 struct Row8;
16750 ˇstruct Row9;
16751 struct Row10;ˇ"#},
16752 base_text,
16753 &mut cx,
16754 );
16755}
16756
16757#[gpui::test]
16758async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
16759 init_test(cx, |_| {});
16760 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16761 let base_text = indoc! {r#"
16762 one
16763
16764 two
16765 three
16766 "#};
16767
16768 cx.set_head_text(base_text);
16769 cx.set_state("\nˇ\n");
16770 cx.executor().run_until_parked();
16771 cx.update_editor(|editor, _window, cx| {
16772 editor.expand_selected_diff_hunks(cx);
16773 });
16774 cx.executor().run_until_parked();
16775 cx.update_editor(|editor, window, cx| {
16776 editor.backspace(&Default::default(), window, cx);
16777 });
16778 cx.run_until_parked();
16779 cx.assert_state_with_diff(
16780 indoc! {r#"
16781
16782 - two
16783 - threeˇ
16784 +
16785 "#}
16786 .to_string(),
16787 );
16788}
16789
16790#[gpui::test]
16791async fn test_deletion_reverts(cx: &mut TestAppContext) {
16792 init_test(cx, |_| {});
16793 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16794 let base_text = indoc! {r#"struct Row;
16795struct Row1;
16796struct Row2;
16797
16798struct Row4;
16799struct Row5;
16800struct Row6;
16801
16802struct Row8;
16803struct Row9;
16804struct Row10;"#};
16805
16806 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
16807 assert_hunk_revert(
16808 indoc! {r#"struct Row;
16809 struct Row2;
16810
16811 ˇstruct Row4;
16812 struct Row5;
16813 struct Row6;
16814 ˇ
16815 struct Row8;
16816 struct Row10;"#},
16817 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16818 indoc! {r#"struct Row;
16819 struct Row2;
16820
16821 ˇstruct Row4;
16822 struct Row5;
16823 struct Row6;
16824 ˇ
16825 struct Row8;
16826 struct Row10;"#},
16827 base_text,
16828 &mut cx,
16829 );
16830 assert_hunk_revert(
16831 indoc! {r#"struct Row;
16832 struct Row2;
16833
16834 «ˇstruct Row4;
16835 struct» Row5;
16836 «struct Row6;
16837 ˇ»
16838 struct Row8;
16839 struct Row10;"#},
16840 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16841 indoc! {r#"struct Row;
16842 struct Row2;
16843
16844 «ˇstruct Row4;
16845 struct» Row5;
16846 «struct Row6;
16847 ˇ»
16848 struct Row8;
16849 struct Row10;"#},
16850 base_text,
16851 &mut cx,
16852 );
16853
16854 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
16855 assert_hunk_revert(
16856 indoc! {r#"struct Row;
16857 ˇstruct Row2;
16858
16859 struct Row4;
16860 struct Row5;
16861 struct Row6;
16862
16863 struct Row8;ˇ
16864 struct Row10;"#},
16865 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16866 indoc! {r#"struct Row;
16867 struct Row1;
16868 ˇstruct Row2;
16869
16870 struct Row4;
16871 struct Row5;
16872 struct Row6;
16873
16874 struct Row8;ˇ
16875 struct Row9;
16876 struct Row10;"#},
16877 base_text,
16878 &mut cx,
16879 );
16880 assert_hunk_revert(
16881 indoc! {r#"struct Row;
16882 struct Row2«ˇ;
16883 struct Row4;
16884 struct» Row5;
16885 «struct Row6;
16886
16887 struct Row8;ˇ»
16888 struct Row10;"#},
16889 vec![
16890 DiffHunkStatusKind::Deleted,
16891 DiffHunkStatusKind::Deleted,
16892 DiffHunkStatusKind::Deleted,
16893 ],
16894 indoc! {r#"struct Row;
16895 struct Row1;
16896 struct Row2«ˇ;
16897
16898 struct Row4;
16899 struct» Row5;
16900 «struct Row6;
16901
16902 struct Row8;ˇ»
16903 struct Row9;
16904 struct Row10;"#},
16905 base_text,
16906 &mut cx,
16907 );
16908}
16909
16910#[gpui::test]
16911async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
16912 init_test(cx, |_| {});
16913
16914 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
16915 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
16916 let base_text_3 =
16917 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
16918
16919 let text_1 = edit_first_char_of_every_line(base_text_1);
16920 let text_2 = edit_first_char_of_every_line(base_text_2);
16921 let text_3 = edit_first_char_of_every_line(base_text_3);
16922
16923 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
16924 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
16925 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
16926
16927 let multibuffer = cx.new(|cx| {
16928 let mut multibuffer = MultiBuffer::new(ReadWrite);
16929 multibuffer.push_excerpts(
16930 buffer_1.clone(),
16931 [
16932 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16933 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16934 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16935 ],
16936 cx,
16937 );
16938 multibuffer.push_excerpts(
16939 buffer_2.clone(),
16940 [
16941 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16942 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16943 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16944 ],
16945 cx,
16946 );
16947 multibuffer.push_excerpts(
16948 buffer_3.clone(),
16949 [
16950 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16951 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16952 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16953 ],
16954 cx,
16955 );
16956 multibuffer
16957 });
16958
16959 let fs = FakeFs::new(cx.executor());
16960 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
16961 let (editor, cx) = cx
16962 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
16963 editor.update_in(cx, |editor, _window, cx| {
16964 for (buffer, diff_base) in [
16965 (buffer_1.clone(), base_text_1),
16966 (buffer_2.clone(), base_text_2),
16967 (buffer_3.clone(), base_text_3),
16968 ] {
16969 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
16970 editor
16971 .buffer
16972 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
16973 }
16974 });
16975 cx.executor().run_until_parked();
16976
16977 editor.update_in(cx, |editor, window, cx| {
16978 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}");
16979 editor.select_all(&SelectAll, window, cx);
16980 editor.git_restore(&Default::default(), window, cx);
16981 });
16982 cx.executor().run_until_parked();
16983
16984 // When all ranges are selected, all buffer hunks are reverted.
16985 editor.update(cx, |editor, cx| {
16986 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");
16987 });
16988 buffer_1.update(cx, |buffer, _| {
16989 assert_eq!(buffer.text(), base_text_1);
16990 });
16991 buffer_2.update(cx, |buffer, _| {
16992 assert_eq!(buffer.text(), base_text_2);
16993 });
16994 buffer_3.update(cx, |buffer, _| {
16995 assert_eq!(buffer.text(), base_text_3);
16996 });
16997
16998 editor.update_in(cx, |editor, window, cx| {
16999 editor.undo(&Default::default(), window, cx);
17000 });
17001
17002 editor.update_in(cx, |editor, window, cx| {
17003 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17004 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
17005 });
17006 editor.git_restore(&Default::default(), window, cx);
17007 });
17008
17009 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
17010 // but not affect buffer_2 and its related excerpts.
17011 editor.update(cx, |editor, cx| {
17012 assert_eq!(
17013 editor.text(cx),
17014 "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}"
17015 );
17016 });
17017 buffer_1.update(cx, |buffer, _| {
17018 assert_eq!(buffer.text(), base_text_1);
17019 });
17020 buffer_2.update(cx, |buffer, _| {
17021 assert_eq!(
17022 buffer.text(),
17023 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
17024 );
17025 });
17026 buffer_3.update(cx, |buffer, _| {
17027 assert_eq!(
17028 buffer.text(),
17029 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
17030 );
17031 });
17032
17033 fn edit_first_char_of_every_line(text: &str) -> String {
17034 text.split('\n')
17035 .map(|line| format!("X{}", &line[1..]))
17036 .collect::<Vec<_>>()
17037 .join("\n")
17038 }
17039}
17040
17041#[gpui::test]
17042async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
17043 init_test(cx, |_| {});
17044
17045 let cols = 4;
17046 let rows = 10;
17047 let sample_text_1 = sample_text(rows, cols, 'a');
17048 assert_eq!(
17049 sample_text_1,
17050 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
17051 );
17052 let sample_text_2 = sample_text(rows, cols, 'l');
17053 assert_eq!(
17054 sample_text_2,
17055 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
17056 );
17057 let sample_text_3 = sample_text(rows, cols, 'v');
17058 assert_eq!(
17059 sample_text_3,
17060 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
17061 );
17062
17063 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
17064 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
17065 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
17066
17067 let multi_buffer = cx.new(|cx| {
17068 let mut multibuffer = MultiBuffer::new(ReadWrite);
17069 multibuffer.push_excerpts(
17070 buffer_1.clone(),
17071 [
17072 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17073 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17074 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17075 ],
17076 cx,
17077 );
17078 multibuffer.push_excerpts(
17079 buffer_2.clone(),
17080 [
17081 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17082 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17083 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17084 ],
17085 cx,
17086 );
17087 multibuffer.push_excerpts(
17088 buffer_3.clone(),
17089 [
17090 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17091 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17092 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17093 ],
17094 cx,
17095 );
17096 multibuffer
17097 });
17098
17099 let fs = FakeFs::new(cx.executor());
17100 fs.insert_tree(
17101 "/a",
17102 json!({
17103 "main.rs": sample_text_1,
17104 "other.rs": sample_text_2,
17105 "lib.rs": sample_text_3,
17106 }),
17107 )
17108 .await;
17109 let project = Project::test(fs, ["/a".as_ref()], cx).await;
17110 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17111 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17112 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17113 Editor::new(
17114 EditorMode::full(),
17115 multi_buffer,
17116 Some(project.clone()),
17117 window,
17118 cx,
17119 )
17120 });
17121 let multibuffer_item_id = workspace
17122 .update(cx, |workspace, window, cx| {
17123 assert!(
17124 workspace.active_item(cx).is_none(),
17125 "active item should be None before the first item is added"
17126 );
17127 workspace.add_item_to_active_pane(
17128 Box::new(multi_buffer_editor.clone()),
17129 None,
17130 true,
17131 window,
17132 cx,
17133 );
17134 let active_item = workspace
17135 .active_item(cx)
17136 .expect("should have an active item after adding the multi buffer");
17137 assert!(
17138 !active_item.is_singleton(cx),
17139 "A multi buffer was expected to active after adding"
17140 );
17141 active_item.item_id()
17142 })
17143 .unwrap();
17144 cx.executor().run_until_parked();
17145
17146 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17147 editor.change_selections(
17148 SelectionEffects::scroll(Autoscroll::Next),
17149 window,
17150 cx,
17151 |s| s.select_ranges(Some(1..2)),
17152 );
17153 editor.open_excerpts(&OpenExcerpts, window, cx);
17154 });
17155 cx.executor().run_until_parked();
17156 let first_item_id = workspace
17157 .update(cx, |workspace, window, cx| {
17158 let active_item = workspace
17159 .active_item(cx)
17160 .expect("should have an active item after navigating into the 1st buffer");
17161 let first_item_id = active_item.item_id();
17162 assert_ne!(
17163 first_item_id, multibuffer_item_id,
17164 "Should navigate into the 1st buffer and activate it"
17165 );
17166 assert!(
17167 active_item.is_singleton(cx),
17168 "New active item should be a singleton buffer"
17169 );
17170 assert_eq!(
17171 active_item
17172 .act_as::<Editor>(cx)
17173 .expect("should have navigated into an editor for the 1st buffer")
17174 .read(cx)
17175 .text(cx),
17176 sample_text_1
17177 );
17178
17179 workspace
17180 .go_back(workspace.active_pane().downgrade(), window, cx)
17181 .detach_and_log_err(cx);
17182
17183 first_item_id
17184 })
17185 .unwrap();
17186 cx.executor().run_until_parked();
17187 workspace
17188 .update(cx, |workspace, _, cx| {
17189 let active_item = workspace
17190 .active_item(cx)
17191 .expect("should have an active item after navigating back");
17192 assert_eq!(
17193 active_item.item_id(),
17194 multibuffer_item_id,
17195 "Should navigate back to the multi buffer"
17196 );
17197 assert!(!active_item.is_singleton(cx));
17198 })
17199 .unwrap();
17200
17201 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17202 editor.change_selections(
17203 SelectionEffects::scroll(Autoscroll::Next),
17204 window,
17205 cx,
17206 |s| s.select_ranges(Some(39..40)),
17207 );
17208 editor.open_excerpts(&OpenExcerpts, window, cx);
17209 });
17210 cx.executor().run_until_parked();
17211 let second_item_id = workspace
17212 .update(cx, |workspace, window, cx| {
17213 let active_item = workspace
17214 .active_item(cx)
17215 .expect("should have an active item after navigating into the 2nd buffer");
17216 let second_item_id = active_item.item_id();
17217 assert_ne!(
17218 second_item_id, multibuffer_item_id,
17219 "Should navigate away from the multibuffer"
17220 );
17221 assert_ne!(
17222 second_item_id, first_item_id,
17223 "Should navigate into the 2nd buffer and activate it"
17224 );
17225 assert!(
17226 active_item.is_singleton(cx),
17227 "New active item should be a singleton buffer"
17228 );
17229 assert_eq!(
17230 active_item
17231 .act_as::<Editor>(cx)
17232 .expect("should have navigated into an editor")
17233 .read(cx)
17234 .text(cx),
17235 sample_text_2
17236 );
17237
17238 workspace
17239 .go_back(workspace.active_pane().downgrade(), window, cx)
17240 .detach_and_log_err(cx);
17241
17242 second_item_id
17243 })
17244 .unwrap();
17245 cx.executor().run_until_parked();
17246 workspace
17247 .update(cx, |workspace, _, cx| {
17248 let active_item = workspace
17249 .active_item(cx)
17250 .expect("should have an active item after navigating back from the 2nd buffer");
17251 assert_eq!(
17252 active_item.item_id(),
17253 multibuffer_item_id,
17254 "Should navigate back from the 2nd buffer to the multi buffer"
17255 );
17256 assert!(!active_item.is_singleton(cx));
17257 })
17258 .unwrap();
17259
17260 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17261 editor.change_selections(
17262 SelectionEffects::scroll(Autoscroll::Next),
17263 window,
17264 cx,
17265 |s| s.select_ranges(Some(70..70)),
17266 );
17267 editor.open_excerpts(&OpenExcerpts, window, cx);
17268 });
17269 cx.executor().run_until_parked();
17270 workspace
17271 .update(cx, |workspace, window, cx| {
17272 let active_item = workspace
17273 .active_item(cx)
17274 .expect("should have an active item after navigating into the 3rd buffer");
17275 let third_item_id = active_item.item_id();
17276 assert_ne!(
17277 third_item_id, multibuffer_item_id,
17278 "Should navigate into the 3rd buffer and activate it"
17279 );
17280 assert_ne!(third_item_id, first_item_id);
17281 assert_ne!(third_item_id, second_item_id);
17282 assert!(
17283 active_item.is_singleton(cx),
17284 "New active item should be a singleton buffer"
17285 );
17286 assert_eq!(
17287 active_item
17288 .act_as::<Editor>(cx)
17289 .expect("should have navigated into an editor")
17290 .read(cx)
17291 .text(cx),
17292 sample_text_3
17293 );
17294
17295 workspace
17296 .go_back(workspace.active_pane().downgrade(), window, cx)
17297 .detach_and_log_err(cx);
17298 })
17299 .unwrap();
17300 cx.executor().run_until_parked();
17301 workspace
17302 .update(cx, |workspace, _, cx| {
17303 let active_item = workspace
17304 .active_item(cx)
17305 .expect("should have an active item after navigating back from the 3rd buffer");
17306 assert_eq!(
17307 active_item.item_id(),
17308 multibuffer_item_id,
17309 "Should navigate back from the 3rd buffer to the multi buffer"
17310 );
17311 assert!(!active_item.is_singleton(cx));
17312 })
17313 .unwrap();
17314}
17315
17316#[gpui::test]
17317async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17318 init_test(cx, |_| {});
17319
17320 let mut cx = EditorTestContext::new(cx).await;
17321
17322 let diff_base = r#"
17323 use some::mod;
17324
17325 const A: u32 = 42;
17326
17327 fn main() {
17328 println!("hello");
17329
17330 println!("world");
17331 }
17332 "#
17333 .unindent();
17334
17335 cx.set_state(
17336 &r#"
17337 use some::modified;
17338
17339 ˇ
17340 fn main() {
17341 println!("hello there");
17342
17343 println!("around the");
17344 println!("world");
17345 }
17346 "#
17347 .unindent(),
17348 );
17349
17350 cx.set_head_text(&diff_base);
17351 executor.run_until_parked();
17352
17353 cx.update_editor(|editor, window, cx| {
17354 editor.go_to_next_hunk(&GoToHunk, window, cx);
17355 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17356 });
17357 executor.run_until_parked();
17358 cx.assert_state_with_diff(
17359 r#"
17360 use some::modified;
17361
17362
17363 fn main() {
17364 - println!("hello");
17365 + ˇ println!("hello there");
17366
17367 println!("around the");
17368 println!("world");
17369 }
17370 "#
17371 .unindent(),
17372 );
17373
17374 cx.update_editor(|editor, window, cx| {
17375 for _ in 0..2 {
17376 editor.go_to_next_hunk(&GoToHunk, window, cx);
17377 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17378 }
17379 });
17380 executor.run_until_parked();
17381 cx.assert_state_with_diff(
17382 r#"
17383 - use some::mod;
17384 + ˇuse some::modified;
17385
17386
17387 fn main() {
17388 - println!("hello");
17389 + println!("hello there");
17390
17391 + println!("around the");
17392 println!("world");
17393 }
17394 "#
17395 .unindent(),
17396 );
17397
17398 cx.update_editor(|editor, window, cx| {
17399 editor.go_to_next_hunk(&GoToHunk, window, cx);
17400 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17401 });
17402 executor.run_until_parked();
17403 cx.assert_state_with_diff(
17404 r#"
17405 - use some::mod;
17406 + use some::modified;
17407
17408 - const A: u32 = 42;
17409 ˇ
17410 fn main() {
17411 - println!("hello");
17412 + println!("hello there");
17413
17414 + println!("around the");
17415 println!("world");
17416 }
17417 "#
17418 .unindent(),
17419 );
17420
17421 cx.update_editor(|editor, window, cx| {
17422 editor.cancel(&Cancel, window, cx);
17423 });
17424
17425 cx.assert_state_with_diff(
17426 r#"
17427 use some::modified;
17428
17429 ˇ
17430 fn main() {
17431 println!("hello there");
17432
17433 println!("around the");
17434 println!("world");
17435 }
17436 "#
17437 .unindent(),
17438 );
17439}
17440
17441#[gpui::test]
17442async fn test_diff_base_change_with_expanded_diff_hunks(
17443 executor: BackgroundExecutor,
17444 cx: &mut TestAppContext,
17445) {
17446 init_test(cx, |_| {});
17447
17448 let mut cx = EditorTestContext::new(cx).await;
17449
17450 let diff_base = r#"
17451 use some::mod1;
17452 use some::mod2;
17453
17454 const A: u32 = 42;
17455 const B: u32 = 42;
17456 const C: u32 = 42;
17457
17458 fn main() {
17459 println!("hello");
17460
17461 println!("world");
17462 }
17463 "#
17464 .unindent();
17465
17466 cx.set_state(
17467 &r#"
17468 use some::mod2;
17469
17470 const A: u32 = 42;
17471 const C: u32 = 42;
17472
17473 fn main(ˇ) {
17474 //println!("hello");
17475
17476 println!("world");
17477 //
17478 //
17479 }
17480 "#
17481 .unindent(),
17482 );
17483
17484 cx.set_head_text(&diff_base);
17485 executor.run_until_parked();
17486
17487 cx.update_editor(|editor, window, cx| {
17488 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17489 });
17490 executor.run_until_parked();
17491 cx.assert_state_with_diff(
17492 r#"
17493 - use some::mod1;
17494 use some::mod2;
17495
17496 const A: u32 = 42;
17497 - const B: u32 = 42;
17498 const C: u32 = 42;
17499
17500 fn main(ˇ) {
17501 - println!("hello");
17502 + //println!("hello");
17503
17504 println!("world");
17505 + //
17506 + //
17507 }
17508 "#
17509 .unindent(),
17510 );
17511
17512 cx.set_head_text("new diff base!");
17513 executor.run_until_parked();
17514 cx.assert_state_with_diff(
17515 r#"
17516 - new diff base!
17517 + use some::mod2;
17518 +
17519 + const A: u32 = 42;
17520 + const C: u32 = 42;
17521 +
17522 + fn main(ˇ) {
17523 + //println!("hello");
17524 +
17525 + println!("world");
17526 + //
17527 + //
17528 + }
17529 "#
17530 .unindent(),
17531 );
17532}
17533
17534#[gpui::test]
17535async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
17536 init_test(cx, |_| {});
17537
17538 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17539 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17540 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17541 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17542 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
17543 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
17544
17545 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
17546 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
17547 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
17548
17549 let multi_buffer = cx.new(|cx| {
17550 let mut multibuffer = MultiBuffer::new(ReadWrite);
17551 multibuffer.push_excerpts(
17552 buffer_1.clone(),
17553 [
17554 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17555 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17556 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17557 ],
17558 cx,
17559 );
17560 multibuffer.push_excerpts(
17561 buffer_2.clone(),
17562 [
17563 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17564 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17565 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17566 ],
17567 cx,
17568 );
17569 multibuffer.push_excerpts(
17570 buffer_3.clone(),
17571 [
17572 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17573 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17574 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17575 ],
17576 cx,
17577 );
17578 multibuffer
17579 });
17580
17581 let editor =
17582 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17583 editor
17584 .update(cx, |editor, _window, cx| {
17585 for (buffer, diff_base) in [
17586 (buffer_1.clone(), file_1_old),
17587 (buffer_2.clone(), file_2_old),
17588 (buffer_3.clone(), file_3_old),
17589 ] {
17590 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
17591 editor
17592 .buffer
17593 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
17594 }
17595 })
17596 .unwrap();
17597
17598 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17599 cx.run_until_parked();
17600
17601 cx.assert_editor_state(
17602 &"
17603 ˇaaa
17604 ccc
17605 ddd
17606
17607 ggg
17608 hhh
17609
17610
17611 lll
17612 mmm
17613 NNN
17614
17615 qqq
17616 rrr
17617
17618 uuu
17619 111
17620 222
17621 333
17622
17623 666
17624 777
17625
17626 000
17627 !!!"
17628 .unindent(),
17629 );
17630
17631 cx.update_editor(|editor, window, cx| {
17632 editor.select_all(&SelectAll, window, cx);
17633 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17634 });
17635 cx.executor().run_until_parked();
17636
17637 cx.assert_state_with_diff(
17638 "
17639 «aaa
17640 - bbb
17641 ccc
17642 ddd
17643
17644 ggg
17645 hhh
17646
17647
17648 lll
17649 mmm
17650 - nnn
17651 + NNN
17652
17653 qqq
17654 rrr
17655
17656 uuu
17657 111
17658 222
17659 333
17660
17661 + 666
17662 777
17663
17664 000
17665 !!!ˇ»"
17666 .unindent(),
17667 );
17668}
17669
17670#[gpui::test]
17671async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
17672 init_test(cx, |_| {});
17673
17674 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
17675 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
17676
17677 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
17678 let multi_buffer = cx.new(|cx| {
17679 let mut multibuffer = MultiBuffer::new(ReadWrite);
17680 multibuffer.push_excerpts(
17681 buffer.clone(),
17682 [
17683 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
17684 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
17685 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
17686 ],
17687 cx,
17688 );
17689 multibuffer
17690 });
17691
17692 let editor =
17693 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17694 editor
17695 .update(cx, |editor, _window, cx| {
17696 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
17697 editor
17698 .buffer
17699 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
17700 })
17701 .unwrap();
17702
17703 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17704 cx.run_until_parked();
17705
17706 cx.update_editor(|editor, window, cx| {
17707 editor.expand_all_diff_hunks(&Default::default(), window, cx)
17708 });
17709 cx.executor().run_until_parked();
17710
17711 // When the start of a hunk coincides with the start of its excerpt,
17712 // the hunk is expanded. When the start of a a hunk is earlier than
17713 // the start of its excerpt, the hunk is not expanded.
17714 cx.assert_state_with_diff(
17715 "
17716 ˇaaa
17717 - bbb
17718 + BBB
17719
17720 - ddd
17721 - eee
17722 + DDD
17723 + EEE
17724 fff
17725
17726 iii
17727 "
17728 .unindent(),
17729 );
17730}
17731
17732#[gpui::test]
17733async fn test_edits_around_expanded_insertion_hunks(
17734 executor: BackgroundExecutor,
17735 cx: &mut TestAppContext,
17736) {
17737 init_test(cx, |_| {});
17738
17739 let mut cx = EditorTestContext::new(cx).await;
17740
17741 let diff_base = r#"
17742 use some::mod1;
17743 use some::mod2;
17744
17745 const A: u32 = 42;
17746
17747 fn main() {
17748 println!("hello");
17749
17750 println!("world");
17751 }
17752 "#
17753 .unindent();
17754 executor.run_until_parked();
17755 cx.set_state(
17756 &r#"
17757 use some::mod1;
17758 use some::mod2;
17759
17760 const A: u32 = 42;
17761 const B: u32 = 42;
17762 const C: u32 = 42;
17763 ˇ
17764
17765 fn main() {
17766 println!("hello");
17767
17768 println!("world");
17769 }
17770 "#
17771 .unindent(),
17772 );
17773
17774 cx.set_head_text(&diff_base);
17775 executor.run_until_parked();
17776
17777 cx.update_editor(|editor, window, cx| {
17778 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17779 });
17780 executor.run_until_parked();
17781
17782 cx.assert_state_with_diff(
17783 r#"
17784 use some::mod1;
17785 use some::mod2;
17786
17787 const A: u32 = 42;
17788 + const B: u32 = 42;
17789 + const C: u32 = 42;
17790 + ˇ
17791
17792 fn main() {
17793 println!("hello");
17794
17795 println!("world");
17796 }
17797 "#
17798 .unindent(),
17799 );
17800
17801 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
17802 executor.run_until_parked();
17803
17804 cx.assert_state_with_diff(
17805 r#"
17806 use some::mod1;
17807 use some::mod2;
17808
17809 const A: u32 = 42;
17810 + const B: u32 = 42;
17811 + const C: u32 = 42;
17812 + const D: u32 = 42;
17813 + ˇ
17814
17815 fn main() {
17816 println!("hello");
17817
17818 println!("world");
17819 }
17820 "#
17821 .unindent(),
17822 );
17823
17824 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
17825 executor.run_until_parked();
17826
17827 cx.assert_state_with_diff(
17828 r#"
17829 use some::mod1;
17830 use some::mod2;
17831
17832 const A: u32 = 42;
17833 + const B: u32 = 42;
17834 + const C: u32 = 42;
17835 + const D: u32 = 42;
17836 + const E: u32 = 42;
17837 + ˇ
17838
17839 fn main() {
17840 println!("hello");
17841
17842 println!("world");
17843 }
17844 "#
17845 .unindent(),
17846 );
17847
17848 cx.update_editor(|editor, window, cx| {
17849 editor.delete_line(&DeleteLine, window, cx);
17850 });
17851 executor.run_until_parked();
17852
17853 cx.assert_state_with_diff(
17854 r#"
17855 use some::mod1;
17856 use some::mod2;
17857
17858 const A: u32 = 42;
17859 + const B: u32 = 42;
17860 + const C: u32 = 42;
17861 + const D: u32 = 42;
17862 + const E: u32 = 42;
17863 ˇ
17864 fn main() {
17865 println!("hello");
17866
17867 println!("world");
17868 }
17869 "#
17870 .unindent(),
17871 );
17872
17873 cx.update_editor(|editor, window, cx| {
17874 editor.move_up(&MoveUp, window, cx);
17875 editor.delete_line(&DeleteLine, window, cx);
17876 editor.move_up(&MoveUp, window, cx);
17877 editor.delete_line(&DeleteLine, window, cx);
17878 editor.move_up(&MoveUp, window, cx);
17879 editor.delete_line(&DeleteLine, window, cx);
17880 });
17881 executor.run_until_parked();
17882 cx.assert_state_with_diff(
17883 r#"
17884 use some::mod1;
17885 use some::mod2;
17886
17887 const A: u32 = 42;
17888 + const B: u32 = 42;
17889 ˇ
17890 fn main() {
17891 println!("hello");
17892
17893 println!("world");
17894 }
17895 "#
17896 .unindent(),
17897 );
17898
17899 cx.update_editor(|editor, window, cx| {
17900 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
17901 editor.delete_line(&DeleteLine, window, cx);
17902 });
17903 executor.run_until_parked();
17904 cx.assert_state_with_diff(
17905 r#"
17906 ˇ
17907 fn main() {
17908 println!("hello");
17909
17910 println!("world");
17911 }
17912 "#
17913 .unindent(),
17914 );
17915}
17916
17917#[gpui::test]
17918async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
17919 init_test(cx, |_| {});
17920
17921 let mut cx = EditorTestContext::new(cx).await;
17922 cx.set_head_text(indoc! { "
17923 one
17924 two
17925 three
17926 four
17927 five
17928 "
17929 });
17930 cx.set_state(indoc! { "
17931 one
17932 ˇthree
17933 five
17934 "});
17935 cx.run_until_parked();
17936 cx.update_editor(|editor, window, cx| {
17937 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17938 });
17939 cx.assert_state_with_diff(
17940 indoc! { "
17941 one
17942 - two
17943 ˇthree
17944 - four
17945 five
17946 "}
17947 .to_string(),
17948 );
17949 cx.update_editor(|editor, window, cx| {
17950 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17951 });
17952
17953 cx.assert_state_with_diff(
17954 indoc! { "
17955 one
17956 ˇthree
17957 five
17958 "}
17959 .to_string(),
17960 );
17961
17962 cx.set_state(indoc! { "
17963 one
17964 ˇTWO
17965 three
17966 four
17967 five
17968 "});
17969 cx.run_until_parked();
17970 cx.update_editor(|editor, window, cx| {
17971 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17972 });
17973
17974 cx.assert_state_with_diff(
17975 indoc! { "
17976 one
17977 - two
17978 + ˇTWO
17979 three
17980 four
17981 five
17982 "}
17983 .to_string(),
17984 );
17985 cx.update_editor(|editor, window, cx| {
17986 editor.move_up(&Default::default(), window, cx);
17987 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17988 });
17989 cx.assert_state_with_diff(
17990 indoc! { "
17991 one
17992 ˇTWO
17993 three
17994 four
17995 five
17996 "}
17997 .to_string(),
17998 );
17999}
18000
18001#[gpui::test]
18002async fn test_edits_around_expanded_deletion_hunks(
18003 executor: BackgroundExecutor,
18004 cx: &mut TestAppContext,
18005) {
18006 init_test(cx, |_| {});
18007
18008 let mut cx = EditorTestContext::new(cx).await;
18009
18010 let diff_base = r#"
18011 use some::mod1;
18012 use some::mod2;
18013
18014 const A: u32 = 42;
18015 const B: u32 = 42;
18016 const C: u32 = 42;
18017
18018
18019 fn main() {
18020 println!("hello");
18021
18022 println!("world");
18023 }
18024 "#
18025 .unindent();
18026 executor.run_until_parked();
18027 cx.set_state(
18028 &r#"
18029 use some::mod1;
18030 use some::mod2;
18031
18032 ˇconst B: u32 = 42;
18033 const C: u32 = 42;
18034
18035
18036 fn main() {
18037 println!("hello");
18038
18039 println!("world");
18040 }
18041 "#
18042 .unindent(),
18043 );
18044
18045 cx.set_head_text(&diff_base);
18046 executor.run_until_parked();
18047
18048 cx.update_editor(|editor, window, cx| {
18049 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18050 });
18051 executor.run_until_parked();
18052
18053 cx.assert_state_with_diff(
18054 r#"
18055 use some::mod1;
18056 use some::mod2;
18057
18058 - const A: u32 = 42;
18059 ˇconst B: u32 = 42;
18060 const C: u32 = 42;
18061
18062
18063 fn main() {
18064 println!("hello");
18065
18066 println!("world");
18067 }
18068 "#
18069 .unindent(),
18070 );
18071
18072 cx.update_editor(|editor, window, cx| {
18073 editor.delete_line(&DeleteLine, window, cx);
18074 });
18075 executor.run_until_parked();
18076 cx.assert_state_with_diff(
18077 r#"
18078 use some::mod1;
18079 use some::mod2;
18080
18081 - const A: u32 = 42;
18082 - const B: u32 = 42;
18083 ˇconst C: u32 = 42;
18084
18085
18086 fn main() {
18087 println!("hello");
18088
18089 println!("world");
18090 }
18091 "#
18092 .unindent(),
18093 );
18094
18095 cx.update_editor(|editor, window, cx| {
18096 editor.delete_line(&DeleteLine, window, cx);
18097 });
18098 executor.run_until_parked();
18099 cx.assert_state_with_diff(
18100 r#"
18101 use some::mod1;
18102 use some::mod2;
18103
18104 - const A: u32 = 42;
18105 - const B: u32 = 42;
18106 - const C: u32 = 42;
18107 ˇ
18108
18109 fn main() {
18110 println!("hello");
18111
18112 println!("world");
18113 }
18114 "#
18115 .unindent(),
18116 );
18117
18118 cx.update_editor(|editor, window, cx| {
18119 editor.handle_input("replacement", window, cx);
18120 });
18121 executor.run_until_parked();
18122 cx.assert_state_with_diff(
18123 r#"
18124 use some::mod1;
18125 use some::mod2;
18126
18127 - const A: u32 = 42;
18128 - const B: u32 = 42;
18129 - const C: u32 = 42;
18130 -
18131 + replacementˇ
18132
18133 fn main() {
18134 println!("hello");
18135
18136 println!("world");
18137 }
18138 "#
18139 .unindent(),
18140 );
18141}
18142
18143#[gpui::test]
18144async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18145 init_test(cx, |_| {});
18146
18147 let mut cx = EditorTestContext::new(cx).await;
18148
18149 let base_text = r#"
18150 one
18151 two
18152 three
18153 four
18154 five
18155 "#
18156 .unindent();
18157 executor.run_until_parked();
18158 cx.set_state(
18159 &r#"
18160 one
18161 two
18162 fˇour
18163 five
18164 "#
18165 .unindent(),
18166 );
18167
18168 cx.set_head_text(&base_text);
18169 executor.run_until_parked();
18170
18171 cx.update_editor(|editor, window, cx| {
18172 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18173 });
18174 executor.run_until_parked();
18175
18176 cx.assert_state_with_diff(
18177 r#"
18178 one
18179 two
18180 - three
18181 fˇour
18182 five
18183 "#
18184 .unindent(),
18185 );
18186
18187 cx.update_editor(|editor, window, cx| {
18188 editor.backspace(&Backspace, window, cx);
18189 editor.backspace(&Backspace, window, cx);
18190 });
18191 executor.run_until_parked();
18192 cx.assert_state_with_diff(
18193 r#"
18194 one
18195 two
18196 - threeˇ
18197 - four
18198 + our
18199 five
18200 "#
18201 .unindent(),
18202 );
18203}
18204
18205#[gpui::test]
18206async fn test_edit_after_expanded_modification_hunk(
18207 executor: BackgroundExecutor,
18208 cx: &mut TestAppContext,
18209) {
18210 init_test(cx, |_| {});
18211
18212 let mut cx = EditorTestContext::new(cx).await;
18213
18214 let diff_base = r#"
18215 use some::mod1;
18216 use some::mod2;
18217
18218 const A: u32 = 42;
18219 const B: u32 = 42;
18220 const C: u32 = 42;
18221 const D: u32 = 42;
18222
18223
18224 fn main() {
18225 println!("hello");
18226
18227 println!("world");
18228 }"#
18229 .unindent();
18230
18231 cx.set_state(
18232 &r#"
18233 use some::mod1;
18234 use some::mod2;
18235
18236 const A: u32 = 42;
18237 const B: u32 = 42;
18238 const C: u32 = 43ˇ
18239 const D: u32 = 42;
18240
18241
18242 fn main() {
18243 println!("hello");
18244
18245 println!("world");
18246 }"#
18247 .unindent(),
18248 );
18249
18250 cx.set_head_text(&diff_base);
18251 executor.run_until_parked();
18252 cx.update_editor(|editor, window, cx| {
18253 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18254 });
18255 executor.run_until_parked();
18256
18257 cx.assert_state_with_diff(
18258 r#"
18259 use some::mod1;
18260 use some::mod2;
18261
18262 const A: u32 = 42;
18263 const B: u32 = 42;
18264 - const C: u32 = 42;
18265 + const C: u32 = 43ˇ
18266 const D: u32 = 42;
18267
18268
18269 fn main() {
18270 println!("hello");
18271
18272 println!("world");
18273 }"#
18274 .unindent(),
18275 );
18276
18277 cx.update_editor(|editor, window, cx| {
18278 editor.handle_input("\nnew_line\n", window, cx);
18279 });
18280 executor.run_until_parked();
18281
18282 cx.assert_state_with_diff(
18283 r#"
18284 use some::mod1;
18285 use some::mod2;
18286
18287 const A: u32 = 42;
18288 const B: u32 = 42;
18289 - const C: u32 = 42;
18290 + const C: u32 = 43
18291 + new_line
18292 + ˇ
18293 const D: u32 = 42;
18294
18295
18296 fn main() {
18297 println!("hello");
18298
18299 println!("world");
18300 }"#
18301 .unindent(),
18302 );
18303}
18304
18305#[gpui::test]
18306async fn test_stage_and_unstage_added_file_hunk(
18307 executor: BackgroundExecutor,
18308 cx: &mut TestAppContext,
18309) {
18310 init_test(cx, |_| {});
18311
18312 let mut cx = EditorTestContext::new(cx).await;
18313 cx.update_editor(|editor, _, cx| {
18314 editor.set_expand_all_diff_hunks(cx);
18315 });
18316
18317 let working_copy = r#"
18318 ˇfn main() {
18319 println!("hello, world!");
18320 }
18321 "#
18322 .unindent();
18323
18324 cx.set_state(&working_copy);
18325 executor.run_until_parked();
18326
18327 cx.assert_state_with_diff(
18328 r#"
18329 + ˇfn main() {
18330 + println!("hello, world!");
18331 + }
18332 "#
18333 .unindent(),
18334 );
18335 cx.assert_index_text(None);
18336
18337 cx.update_editor(|editor, window, cx| {
18338 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18339 });
18340 executor.run_until_parked();
18341 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
18342 cx.assert_state_with_diff(
18343 r#"
18344 + ˇfn main() {
18345 + println!("hello, world!");
18346 + }
18347 "#
18348 .unindent(),
18349 );
18350
18351 cx.update_editor(|editor, window, cx| {
18352 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18353 });
18354 executor.run_until_parked();
18355 cx.assert_index_text(None);
18356}
18357
18358async fn setup_indent_guides_editor(
18359 text: &str,
18360 cx: &mut TestAppContext,
18361) -> (BufferId, EditorTestContext) {
18362 init_test(cx, |_| {});
18363
18364 let mut cx = EditorTestContext::new(cx).await;
18365
18366 let buffer_id = cx.update_editor(|editor, window, cx| {
18367 editor.set_text(text, window, cx);
18368 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
18369
18370 buffer_ids[0]
18371 });
18372
18373 (buffer_id, cx)
18374}
18375
18376fn assert_indent_guides(
18377 range: Range<u32>,
18378 expected: Vec<IndentGuide>,
18379 active_indices: Option<Vec<usize>>,
18380 cx: &mut EditorTestContext,
18381) {
18382 let indent_guides = cx.update_editor(|editor, window, cx| {
18383 let snapshot = editor.snapshot(window, cx).display_snapshot;
18384 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
18385 editor,
18386 MultiBufferRow(range.start)..MultiBufferRow(range.end),
18387 true,
18388 &snapshot,
18389 cx,
18390 );
18391
18392 indent_guides.sort_by(|a, b| {
18393 a.depth.cmp(&b.depth).then(
18394 a.start_row
18395 .cmp(&b.start_row)
18396 .then(a.end_row.cmp(&b.end_row)),
18397 )
18398 });
18399 indent_guides
18400 });
18401
18402 if let Some(expected) = active_indices {
18403 let active_indices = cx.update_editor(|editor, window, cx| {
18404 let snapshot = editor.snapshot(window, cx).display_snapshot;
18405 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
18406 });
18407
18408 assert_eq!(
18409 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
18410 expected,
18411 "Active indent guide indices do not match"
18412 );
18413 }
18414
18415 assert_eq!(indent_guides, expected, "Indent guides do not match");
18416}
18417
18418fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
18419 IndentGuide {
18420 buffer_id,
18421 start_row: MultiBufferRow(start_row),
18422 end_row: MultiBufferRow(end_row),
18423 depth,
18424 tab_size: 4,
18425 settings: IndentGuideSettings {
18426 enabled: true,
18427 line_width: 1,
18428 active_line_width: 1,
18429 ..Default::default()
18430 },
18431 }
18432}
18433
18434#[gpui::test]
18435async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
18436 let (buffer_id, mut cx) = setup_indent_guides_editor(
18437 &"
18438 fn main() {
18439 let a = 1;
18440 }"
18441 .unindent(),
18442 cx,
18443 )
18444 .await;
18445
18446 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18447}
18448
18449#[gpui::test]
18450async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
18451 let (buffer_id, mut cx) = setup_indent_guides_editor(
18452 &"
18453 fn main() {
18454 let a = 1;
18455 let b = 2;
18456 }"
18457 .unindent(),
18458 cx,
18459 )
18460 .await;
18461
18462 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
18463}
18464
18465#[gpui::test]
18466async fn test_indent_guide_nested(cx: &mut TestAppContext) {
18467 let (buffer_id, mut cx) = setup_indent_guides_editor(
18468 &"
18469 fn main() {
18470 let a = 1;
18471 if a == 3 {
18472 let b = 2;
18473 } else {
18474 let c = 3;
18475 }
18476 }"
18477 .unindent(),
18478 cx,
18479 )
18480 .await;
18481
18482 assert_indent_guides(
18483 0..8,
18484 vec![
18485 indent_guide(buffer_id, 1, 6, 0),
18486 indent_guide(buffer_id, 3, 3, 1),
18487 indent_guide(buffer_id, 5, 5, 1),
18488 ],
18489 None,
18490 &mut cx,
18491 );
18492}
18493
18494#[gpui::test]
18495async fn test_indent_guide_tab(cx: &mut TestAppContext) {
18496 let (buffer_id, mut cx) = setup_indent_guides_editor(
18497 &"
18498 fn main() {
18499 let a = 1;
18500 let b = 2;
18501 let c = 3;
18502 }"
18503 .unindent(),
18504 cx,
18505 )
18506 .await;
18507
18508 assert_indent_guides(
18509 0..5,
18510 vec![
18511 indent_guide(buffer_id, 1, 3, 0),
18512 indent_guide(buffer_id, 2, 2, 1),
18513 ],
18514 None,
18515 &mut cx,
18516 );
18517}
18518
18519#[gpui::test]
18520async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
18521 let (buffer_id, mut cx) = setup_indent_guides_editor(
18522 &"
18523 fn main() {
18524 let a = 1;
18525
18526 let c = 3;
18527 }"
18528 .unindent(),
18529 cx,
18530 )
18531 .await;
18532
18533 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
18534}
18535
18536#[gpui::test]
18537async fn test_indent_guide_complex(cx: &mut TestAppContext) {
18538 let (buffer_id, mut cx) = setup_indent_guides_editor(
18539 &"
18540 fn main() {
18541 let a = 1;
18542
18543 let c = 3;
18544
18545 if a == 3 {
18546 let b = 2;
18547 } else {
18548 let c = 3;
18549 }
18550 }"
18551 .unindent(),
18552 cx,
18553 )
18554 .await;
18555
18556 assert_indent_guides(
18557 0..11,
18558 vec![
18559 indent_guide(buffer_id, 1, 9, 0),
18560 indent_guide(buffer_id, 6, 6, 1),
18561 indent_guide(buffer_id, 8, 8, 1),
18562 ],
18563 None,
18564 &mut cx,
18565 );
18566}
18567
18568#[gpui::test]
18569async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
18570 let (buffer_id, mut cx) = setup_indent_guides_editor(
18571 &"
18572 fn main() {
18573 let a = 1;
18574
18575 let c = 3;
18576
18577 if a == 3 {
18578 let b = 2;
18579 } else {
18580 let c = 3;
18581 }
18582 }"
18583 .unindent(),
18584 cx,
18585 )
18586 .await;
18587
18588 assert_indent_guides(
18589 1..11,
18590 vec![
18591 indent_guide(buffer_id, 1, 9, 0),
18592 indent_guide(buffer_id, 6, 6, 1),
18593 indent_guide(buffer_id, 8, 8, 1),
18594 ],
18595 None,
18596 &mut cx,
18597 );
18598}
18599
18600#[gpui::test]
18601async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
18602 let (buffer_id, mut cx) = setup_indent_guides_editor(
18603 &"
18604 fn main() {
18605 let a = 1;
18606
18607 let c = 3;
18608
18609 if a == 3 {
18610 let b = 2;
18611 } else {
18612 let c = 3;
18613 }
18614 }"
18615 .unindent(),
18616 cx,
18617 )
18618 .await;
18619
18620 assert_indent_guides(
18621 1..10,
18622 vec![
18623 indent_guide(buffer_id, 1, 9, 0),
18624 indent_guide(buffer_id, 6, 6, 1),
18625 indent_guide(buffer_id, 8, 8, 1),
18626 ],
18627 None,
18628 &mut cx,
18629 );
18630}
18631
18632#[gpui::test]
18633async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
18634 let (buffer_id, mut cx) = setup_indent_guides_editor(
18635 &"
18636 fn main() {
18637 if a {
18638 b(
18639 c,
18640 d,
18641 )
18642 } else {
18643 e(
18644 f
18645 )
18646 }
18647 }"
18648 .unindent(),
18649 cx,
18650 )
18651 .await;
18652
18653 assert_indent_guides(
18654 0..11,
18655 vec![
18656 indent_guide(buffer_id, 1, 10, 0),
18657 indent_guide(buffer_id, 2, 5, 1),
18658 indent_guide(buffer_id, 7, 9, 1),
18659 indent_guide(buffer_id, 3, 4, 2),
18660 indent_guide(buffer_id, 8, 8, 2),
18661 ],
18662 None,
18663 &mut cx,
18664 );
18665
18666 cx.update_editor(|editor, window, cx| {
18667 editor.fold_at(MultiBufferRow(2), window, cx);
18668 assert_eq!(
18669 editor.display_text(cx),
18670 "
18671 fn main() {
18672 if a {
18673 b(⋯
18674 )
18675 } else {
18676 e(
18677 f
18678 )
18679 }
18680 }"
18681 .unindent()
18682 );
18683 });
18684
18685 assert_indent_guides(
18686 0..11,
18687 vec![
18688 indent_guide(buffer_id, 1, 10, 0),
18689 indent_guide(buffer_id, 2, 5, 1),
18690 indent_guide(buffer_id, 7, 9, 1),
18691 indent_guide(buffer_id, 8, 8, 2),
18692 ],
18693 None,
18694 &mut cx,
18695 );
18696}
18697
18698#[gpui::test]
18699async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
18700 let (buffer_id, mut cx) = setup_indent_guides_editor(
18701 &"
18702 block1
18703 block2
18704 block3
18705 block4
18706 block2
18707 block1
18708 block1"
18709 .unindent(),
18710 cx,
18711 )
18712 .await;
18713
18714 assert_indent_guides(
18715 1..10,
18716 vec![
18717 indent_guide(buffer_id, 1, 4, 0),
18718 indent_guide(buffer_id, 2, 3, 1),
18719 indent_guide(buffer_id, 3, 3, 2),
18720 ],
18721 None,
18722 &mut cx,
18723 );
18724}
18725
18726#[gpui::test]
18727async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
18728 let (buffer_id, mut cx) = setup_indent_guides_editor(
18729 &"
18730 block1
18731 block2
18732 block3
18733
18734 block1
18735 block1"
18736 .unindent(),
18737 cx,
18738 )
18739 .await;
18740
18741 assert_indent_guides(
18742 0..6,
18743 vec![
18744 indent_guide(buffer_id, 1, 2, 0),
18745 indent_guide(buffer_id, 2, 2, 1),
18746 ],
18747 None,
18748 &mut cx,
18749 );
18750}
18751
18752#[gpui::test]
18753async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
18754 let (buffer_id, mut cx) = setup_indent_guides_editor(
18755 &"
18756 function component() {
18757 \treturn (
18758 \t\t\t
18759 \t\t<div>
18760 \t\t\t<abc></abc>
18761 \t\t</div>
18762 \t)
18763 }"
18764 .unindent(),
18765 cx,
18766 )
18767 .await;
18768
18769 assert_indent_guides(
18770 0..8,
18771 vec![
18772 indent_guide(buffer_id, 1, 6, 0),
18773 indent_guide(buffer_id, 2, 5, 1),
18774 indent_guide(buffer_id, 4, 4, 2),
18775 ],
18776 None,
18777 &mut cx,
18778 );
18779}
18780
18781#[gpui::test]
18782async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
18783 let (buffer_id, mut cx) = setup_indent_guides_editor(
18784 &"
18785 function component() {
18786 \treturn (
18787 \t
18788 \t\t<div>
18789 \t\t\t<abc></abc>
18790 \t\t</div>
18791 \t)
18792 }"
18793 .unindent(),
18794 cx,
18795 )
18796 .await;
18797
18798 assert_indent_guides(
18799 0..8,
18800 vec![
18801 indent_guide(buffer_id, 1, 6, 0),
18802 indent_guide(buffer_id, 2, 5, 1),
18803 indent_guide(buffer_id, 4, 4, 2),
18804 ],
18805 None,
18806 &mut cx,
18807 );
18808}
18809
18810#[gpui::test]
18811async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
18812 let (buffer_id, mut cx) = setup_indent_guides_editor(
18813 &"
18814 block1
18815
18816
18817
18818 block2
18819 "
18820 .unindent(),
18821 cx,
18822 )
18823 .await;
18824
18825 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18826}
18827
18828#[gpui::test]
18829async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
18830 let (buffer_id, mut cx) = setup_indent_guides_editor(
18831 &"
18832 def a:
18833 \tb = 3
18834 \tif True:
18835 \t\tc = 4
18836 \t\td = 5
18837 \tprint(b)
18838 "
18839 .unindent(),
18840 cx,
18841 )
18842 .await;
18843
18844 assert_indent_guides(
18845 0..6,
18846 vec![
18847 indent_guide(buffer_id, 1, 5, 0),
18848 indent_guide(buffer_id, 3, 4, 1),
18849 ],
18850 None,
18851 &mut cx,
18852 );
18853}
18854
18855#[gpui::test]
18856async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
18857 let (buffer_id, mut cx) = setup_indent_guides_editor(
18858 &"
18859 fn main() {
18860 let a = 1;
18861 }"
18862 .unindent(),
18863 cx,
18864 )
18865 .await;
18866
18867 cx.update_editor(|editor, window, cx| {
18868 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18869 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18870 });
18871 });
18872
18873 assert_indent_guides(
18874 0..3,
18875 vec![indent_guide(buffer_id, 1, 1, 0)],
18876 Some(vec![0]),
18877 &mut cx,
18878 );
18879}
18880
18881#[gpui::test]
18882async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
18883 let (buffer_id, mut cx) = setup_indent_guides_editor(
18884 &"
18885 fn main() {
18886 if 1 == 2 {
18887 let a = 1;
18888 }
18889 }"
18890 .unindent(),
18891 cx,
18892 )
18893 .await;
18894
18895 cx.update_editor(|editor, window, cx| {
18896 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18897 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18898 });
18899 });
18900
18901 assert_indent_guides(
18902 0..4,
18903 vec![
18904 indent_guide(buffer_id, 1, 3, 0),
18905 indent_guide(buffer_id, 2, 2, 1),
18906 ],
18907 Some(vec![1]),
18908 &mut cx,
18909 );
18910
18911 cx.update_editor(|editor, window, cx| {
18912 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18913 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18914 });
18915 });
18916
18917 assert_indent_guides(
18918 0..4,
18919 vec![
18920 indent_guide(buffer_id, 1, 3, 0),
18921 indent_guide(buffer_id, 2, 2, 1),
18922 ],
18923 Some(vec![1]),
18924 &mut cx,
18925 );
18926
18927 cx.update_editor(|editor, window, cx| {
18928 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18929 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
18930 });
18931 });
18932
18933 assert_indent_guides(
18934 0..4,
18935 vec![
18936 indent_guide(buffer_id, 1, 3, 0),
18937 indent_guide(buffer_id, 2, 2, 1),
18938 ],
18939 Some(vec![0]),
18940 &mut cx,
18941 );
18942}
18943
18944#[gpui::test]
18945async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
18946 let (buffer_id, mut cx) = setup_indent_guides_editor(
18947 &"
18948 fn main() {
18949 let a = 1;
18950
18951 let b = 2;
18952 }"
18953 .unindent(),
18954 cx,
18955 )
18956 .await;
18957
18958 cx.update_editor(|editor, window, cx| {
18959 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18960 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18961 });
18962 });
18963
18964 assert_indent_guides(
18965 0..5,
18966 vec![indent_guide(buffer_id, 1, 3, 0)],
18967 Some(vec![0]),
18968 &mut cx,
18969 );
18970}
18971
18972#[gpui::test]
18973async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
18974 let (buffer_id, mut cx) = setup_indent_guides_editor(
18975 &"
18976 def m:
18977 a = 1
18978 pass"
18979 .unindent(),
18980 cx,
18981 )
18982 .await;
18983
18984 cx.update_editor(|editor, window, cx| {
18985 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18986 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18987 });
18988 });
18989
18990 assert_indent_guides(
18991 0..3,
18992 vec![indent_guide(buffer_id, 1, 2, 0)],
18993 Some(vec![0]),
18994 &mut cx,
18995 );
18996}
18997
18998#[gpui::test]
18999async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
19000 init_test(cx, |_| {});
19001 let mut cx = EditorTestContext::new(cx).await;
19002 let text = indoc! {
19003 "
19004 impl A {
19005 fn b() {
19006 0;
19007 3;
19008 5;
19009 6;
19010 7;
19011 }
19012 }
19013 "
19014 };
19015 let base_text = indoc! {
19016 "
19017 impl A {
19018 fn b() {
19019 0;
19020 1;
19021 2;
19022 3;
19023 4;
19024 }
19025 fn c() {
19026 5;
19027 6;
19028 7;
19029 }
19030 }
19031 "
19032 };
19033
19034 cx.update_editor(|editor, window, cx| {
19035 editor.set_text(text, window, cx);
19036
19037 editor.buffer().update(cx, |multibuffer, cx| {
19038 let buffer = multibuffer.as_singleton().unwrap();
19039 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
19040
19041 multibuffer.set_all_diff_hunks_expanded(cx);
19042 multibuffer.add_diff(diff, cx);
19043
19044 buffer.read(cx).remote_id()
19045 })
19046 });
19047 cx.run_until_parked();
19048
19049 cx.assert_state_with_diff(
19050 indoc! { "
19051 impl A {
19052 fn b() {
19053 0;
19054 - 1;
19055 - 2;
19056 3;
19057 - 4;
19058 - }
19059 - fn c() {
19060 5;
19061 6;
19062 7;
19063 }
19064 }
19065 ˇ"
19066 }
19067 .to_string(),
19068 );
19069
19070 let mut actual_guides = cx.update_editor(|editor, window, cx| {
19071 editor
19072 .snapshot(window, cx)
19073 .buffer_snapshot
19074 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
19075 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
19076 .collect::<Vec<_>>()
19077 });
19078 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
19079 assert_eq!(
19080 actual_guides,
19081 vec![
19082 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
19083 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
19084 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
19085 ]
19086 );
19087}
19088
19089#[gpui::test]
19090async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19091 init_test(cx, |_| {});
19092 let mut cx = EditorTestContext::new(cx).await;
19093
19094 let diff_base = r#"
19095 a
19096 b
19097 c
19098 "#
19099 .unindent();
19100
19101 cx.set_state(
19102 &r#"
19103 ˇA
19104 b
19105 C
19106 "#
19107 .unindent(),
19108 );
19109 cx.set_head_text(&diff_base);
19110 cx.update_editor(|editor, window, cx| {
19111 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19112 });
19113 executor.run_until_parked();
19114
19115 let both_hunks_expanded = r#"
19116 - a
19117 + ˇA
19118 b
19119 - c
19120 + C
19121 "#
19122 .unindent();
19123
19124 cx.assert_state_with_diff(both_hunks_expanded.clone());
19125
19126 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19127 let snapshot = editor.snapshot(window, cx);
19128 let hunks = editor
19129 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19130 .collect::<Vec<_>>();
19131 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19132 let buffer_id = hunks[0].buffer_id;
19133 hunks
19134 .into_iter()
19135 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
19136 .collect::<Vec<_>>()
19137 });
19138 assert_eq!(hunk_ranges.len(), 2);
19139
19140 cx.update_editor(|editor, _, cx| {
19141 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19142 });
19143 executor.run_until_parked();
19144
19145 let second_hunk_expanded = r#"
19146 ˇA
19147 b
19148 - c
19149 + C
19150 "#
19151 .unindent();
19152
19153 cx.assert_state_with_diff(second_hunk_expanded);
19154
19155 cx.update_editor(|editor, _, cx| {
19156 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19157 });
19158 executor.run_until_parked();
19159
19160 cx.assert_state_with_diff(both_hunks_expanded.clone());
19161
19162 cx.update_editor(|editor, _, cx| {
19163 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
19164 });
19165 executor.run_until_parked();
19166
19167 let first_hunk_expanded = r#"
19168 - a
19169 + ˇA
19170 b
19171 C
19172 "#
19173 .unindent();
19174
19175 cx.assert_state_with_diff(first_hunk_expanded);
19176
19177 cx.update_editor(|editor, _, cx| {
19178 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
19179 });
19180 executor.run_until_parked();
19181
19182 cx.assert_state_with_diff(both_hunks_expanded);
19183
19184 cx.set_state(
19185 &r#"
19186 ˇA
19187 b
19188 "#
19189 .unindent(),
19190 );
19191 cx.run_until_parked();
19192
19193 // TODO this cursor position seems bad
19194 cx.assert_state_with_diff(
19195 r#"
19196 - ˇa
19197 + A
19198 b
19199 "#
19200 .unindent(),
19201 );
19202
19203 cx.update_editor(|editor, window, cx| {
19204 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19205 });
19206
19207 cx.assert_state_with_diff(
19208 r#"
19209 - ˇa
19210 + A
19211 b
19212 - c
19213 "#
19214 .unindent(),
19215 );
19216
19217 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19218 let snapshot = editor.snapshot(window, cx);
19219 let hunks = editor
19220 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19221 .collect::<Vec<_>>();
19222 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19223 let buffer_id = hunks[0].buffer_id;
19224 hunks
19225 .into_iter()
19226 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
19227 .collect::<Vec<_>>()
19228 });
19229 assert_eq!(hunk_ranges.len(), 2);
19230
19231 cx.update_editor(|editor, _, cx| {
19232 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
19233 });
19234 executor.run_until_parked();
19235
19236 cx.assert_state_with_diff(
19237 r#"
19238 - ˇa
19239 + A
19240 b
19241 "#
19242 .unindent(),
19243 );
19244}
19245
19246#[gpui::test]
19247async fn test_toggle_deletion_hunk_at_start_of_file(
19248 executor: BackgroundExecutor,
19249 cx: &mut TestAppContext,
19250) {
19251 init_test(cx, |_| {});
19252 let mut cx = EditorTestContext::new(cx).await;
19253
19254 let diff_base = r#"
19255 a
19256 b
19257 c
19258 "#
19259 .unindent();
19260
19261 cx.set_state(
19262 &r#"
19263 ˇb
19264 c
19265 "#
19266 .unindent(),
19267 );
19268 cx.set_head_text(&diff_base);
19269 cx.update_editor(|editor, window, cx| {
19270 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19271 });
19272 executor.run_until_parked();
19273
19274 let hunk_expanded = r#"
19275 - a
19276 ˇb
19277 c
19278 "#
19279 .unindent();
19280
19281 cx.assert_state_with_diff(hunk_expanded.clone());
19282
19283 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19284 let snapshot = editor.snapshot(window, cx);
19285 let hunks = editor
19286 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19287 .collect::<Vec<_>>();
19288 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19289 let buffer_id = hunks[0].buffer_id;
19290 hunks
19291 .into_iter()
19292 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
19293 .collect::<Vec<_>>()
19294 });
19295 assert_eq!(hunk_ranges.len(), 1);
19296
19297 cx.update_editor(|editor, _, cx| {
19298 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19299 });
19300 executor.run_until_parked();
19301
19302 let hunk_collapsed = r#"
19303 ˇb
19304 c
19305 "#
19306 .unindent();
19307
19308 cx.assert_state_with_diff(hunk_collapsed);
19309
19310 cx.update_editor(|editor, _, cx| {
19311 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19312 });
19313 executor.run_until_parked();
19314
19315 cx.assert_state_with_diff(hunk_expanded.clone());
19316}
19317
19318#[gpui::test]
19319async fn test_display_diff_hunks(cx: &mut TestAppContext) {
19320 init_test(cx, |_| {});
19321
19322 let fs = FakeFs::new(cx.executor());
19323 fs.insert_tree(
19324 path!("/test"),
19325 json!({
19326 ".git": {},
19327 "file-1": "ONE\n",
19328 "file-2": "TWO\n",
19329 "file-3": "THREE\n",
19330 }),
19331 )
19332 .await;
19333
19334 fs.set_head_for_repo(
19335 path!("/test/.git").as_ref(),
19336 &[
19337 ("file-1".into(), "one\n".into()),
19338 ("file-2".into(), "two\n".into()),
19339 ("file-3".into(), "three\n".into()),
19340 ],
19341 "deadbeef",
19342 );
19343
19344 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
19345 let mut buffers = vec![];
19346 for i in 1..=3 {
19347 let buffer = project
19348 .update(cx, |project, cx| {
19349 let path = format!(path!("/test/file-{}"), i);
19350 project.open_local_buffer(path, cx)
19351 })
19352 .await
19353 .unwrap();
19354 buffers.push(buffer);
19355 }
19356
19357 let multibuffer = cx.new(|cx| {
19358 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
19359 multibuffer.set_all_diff_hunks_expanded(cx);
19360 for buffer in &buffers {
19361 let snapshot = buffer.read(cx).snapshot();
19362 multibuffer.set_excerpts_for_path(
19363 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
19364 buffer.clone(),
19365 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
19366 DEFAULT_MULTIBUFFER_CONTEXT,
19367 cx,
19368 );
19369 }
19370 multibuffer
19371 });
19372
19373 let editor = cx.add_window(|window, cx| {
19374 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
19375 });
19376 cx.run_until_parked();
19377
19378 let snapshot = editor
19379 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19380 .unwrap();
19381 let hunks = snapshot
19382 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
19383 .map(|hunk| match hunk {
19384 DisplayDiffHunk::Unfolded {
19385 display_row_range, ..
19386 } => display_row_range,
19387 DisplayDiffHunk::Folded { .. } => unreachable!(),
19388 })
19389 .collect::<Vec<_>>();
19390 assert_eq!(
19391 hunks,
19392 [
19393 DisplayRow(2)..DisplayRow(4),
19394 DisplayRow(7)..DisplayRow(9),
19395 DisplayRow(12)..DisplayRow(14),
19396 ]
19397 );
19398}
19399
19400#[gpui::test]
19401async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
19402 init_test(cx, |_| {});
19403
19404 let mut cx = EditorTestContext::new(cx).await;
19405 cx.set_head_text(indoc! { "
19406 one
19407 two
19408 three
19409 four
19410 five
19411 "
19412 });
19413 cx.set_index_text(indoc! { "
19414 one
19415 two
19416 three
19417 four
19418 five
19419 "
19420 });
19421 cx.set_state(indoc! {"
19422 one
19423 TWO
19424 ˇTHREE
19425 FOUR
19426 five
19427 "});
19428 cx.run_until_parked();
19429 cx.update_editor(|editor, window, cx| {
19430 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19431 });
19432 cx.run_until_parked();
19433 cx.assert_index_text(Some(indoc! {"
19434 one
19435 TWO
19436 THREE
19437 FOUR
19438 five
19439 "}));
19440 cx.set_state(indoc! { "
19441 one
19442 TWO
19443 ˇTHREE-HUNDRED
19444 FOUR
19445 five
19446 "});
19447 cx.run_until_parked();
19448 cx.update_editor(|editor, window, cx| {
19449 let snapshot = editor.snapshot(window, cx);
19450 let hunks = editor
19451 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19452 .collect::<Vec<_>>();
19453 assert_eq!(hunks.len(), 1);
19454 assert_eq!(
19455 hunks[0].status(),
19456 DiffHunkStatus {
19457 kind: DiffHunkStatusKind::Modified,
19458 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
19459 }
19460 );
19461
19462 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19463 });
19464 cx.run_until_parked();
19465 cx.assert_index_text(Some(indoc! {"
19466 one
19467 TWO
19468 THREE-HUNDRED
19469 FOUR
19470 five
19471 "}));
19472}
19473
19474#[gpui::test]
19475fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
19476 init_test(cx, |_| {});
19477
19478 let editor = cx.add_window(|window, cx| {
19479 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
19480 build_editor(buffer, window, cx)
19481 });
19482
19483 let render_args = Arc::new(Mutex::new(None));
19484 let snapshot = editor
19485 .update(cx, |editor, window, cx| {
19486 let snapshot = editor.buffer().read(cx).snapshot(cx);
19487 let range =
19488 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
19489
19490 struct RenderArgs {
19491 row: MultiBufferRow,
19492 folded: bool,
19493 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
19494 }
19495
19496 let crease = Crease::inline(
19497 range,
19498 FoldPlaceholder::test(),
19499 {
19500 let toggle_callback = render_args.clone();
19501 move |row, folded, callback, _window, _cx| {
19502 *toggle_callback.lock() = Some(RenderArgs {
19503 row,
19504 folded,
19505 callback,
19506 });
19507 div()
19508 }
19509 },
19510 |_row, _folded, _window, _cx| div(),
19511 );
19512
19513 editor.insert_creases(Some(crease), cx);
19514 let snapshot = editor.snapshot(window, cx);
19515 let _div = snapshot.render_crease_toggle(
19516 MultiBufferRow(1),
19517 false,
19518 cx.entity().clone(),
19519 window,
19520 cx,
19521 );
19522 snapshot
19523 })
19524 .unwrap();
19525
19526 let render_args = render_args.lock().take().unwrap();
19527 assert_eq!(render_args.row, MultiBufferRow(1));
19528 assert!(!render_args.folded);
19529 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19530
19531 cx.update_window(*editor, |_, window, cx| {
19532 (render_args.callback)(true, window, cx)
19533 })
19534 .unwrap();
19535 let snapshot = editor
19536 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19537 .unwrap();
19538 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
19539
19540 cx.update_window(*editor, |_, window, cx| {
19541 (render_args.callback)(false, window, cx)
19542 })
19543 .unwrap();
19544 let snapshot = editor
19545 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19546 .unwrap();
19547 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19548}
19549
19550#[gpui::test]
19551async fn test_input_text(cx: &mut TestAppContext) {
19552 init_test(cx, |_| {});
19553 let mut cx = EditorTestContext::new(cx).await;
19554
19555 cx.set_state(
19556 &r#"ˇone
19557 two
19558
19559 three
19560 fourˇ
19561 five
19562
19563 siˇx"#
19564 .unindent(),
19565 );
19566
19567 cx.dispatch_action(HandleInput(String::new()));
19568 cx.assert_editor_state(
19569 &r#"ˇone
19570 two
19571
19572 three
19573 fourˇ
19574 five
19575
19576 siˇx"#
19577 .unindent(),
19578 );
19579
19580 cx.dispatch_action(HandleInput("AAAA".to_string()));
19581 cx.assert_editor_state(
19582 &r#"AAAAˇone
19583 two
19584
19585 three
19586 fourAAAAˇ
19587 five
19588
19589 siAAAAˇx"#
19590 .unindent(),
19591 );
19592}
19593
19594#[gpui::test]
19595async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
19596 init_test(cx, |_| {});
19597
19598 let mut cx = EditorTestContext::new(cx).await;
19599 cx.set_state(
19600 r#"let foo = 1;
19601let foo = 2;
19602let foo = 3;
19603let fooˇ = 4;
19604let foo = 5;
19605let foo = 6;
19606let foo = 7;
19607let foo = 8;
19608let foo = 9;
19609let foo = 10;
19610let foo = 11;
19611let foo = 12;
19612let foo = 13;
19613let foo = 14;
19614let foo = 15;"#,
19615 );
19616
19617 cx.update_editor(|e, window, cx| {
19618 assert_eq!(
19619 e.next_scroll_position,
19620 NextScrollCursorCenterTopBottom::Center,
19621 "Default next scroll direction is center",
19622 );
19623
19624 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19625 assert_eq!(
19626 e.next_scroll_position,
19627 NextScrollCursorCenterTopBottom::Top,
19628 "After center, next scroll direction should be top",
19629 );
19630
19631 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19632 assert_eq!(
19633 e.next_scroll_position,
19634 NextScrollCursorCenterTopBottom::Bottom,
19635 "After top, next scroll direction should be bottom",
19636 );
19637
19638 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19639 assert_eq!(
19640 e.next_scroll_position,
19641 NextScrollCursorCenterTopBottom::Center,
19642 "After bottom, scrolling should start over",
19643 );
19644
19645 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19646 assert_eq!(
19647 e.next_scroll_position,
19648 NextScrollCursorCenterTopBottom::Top,
19649 "Scrolling continues if retriggered fast enough"
19650 );
19651 });
19652
19653 cx.executor()
19654 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
19655 cx.executor().run_until_parked();
19656 cx.update_editor(|e, _, _| {
19657 assert_eq!(
19658 e.next_scroll_position,
19659 NextScrollCursorCenterTopBottom::Center,
19660 "If scrolling is not triggered fast enough, it should reset"
19661 );
19662 });
19663}
19664
19665#[gpui::test]
19666async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
19667 init_test(cx, |_| {});
19668 let mut cx = EditorLspTestContext::new_rust(
19669 lsp::ServerCapabilities {
19670 definition_provider: Some(lsp::OneOf::Left(true)),
19671 references_provider: Some(lsp::OneOf::Left(true)),
19672 ..lsp::ServerCapabilities::default()
19673 },
19674 cx,
19675 )
19676 .await;
19677
19678 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
19679 let go_to_definition = cx
19680 .lsp
19681 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19682 move |params, _| async move {
19683 if empty_go_to_definition {
19684 Ok(None)
19685 } else {
19686 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
19687 uri: params.text_document_position_params.text_document.uri,
19688 range: lsp::Range::new(
19689 lsp::Position::new(4, 3),
19690 lsp::Position::new(4, 6),
19691 ),
19692 })))
19693 }
19694 },
19695 );
19696 let references = cx
19697 .lsp
19698 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
19699 Ok(Some(vec![lsp::Location {
19700 uri: params.text_document_position.text_document.uri,
19701 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
19702 }]))
19703 });
19704 (go_to_definition, references)
19705 };
19706
19707 cx.set_state(
19708 &r#"fn one() {
19709 let mut a = ˇtwo();
19710 }
19711
19712 fn two() {}"#
19713 .unindent(),
19714 );
19715 set_up_lsp_handlers(false, &mut cx);
19716 let navigated = cx
19717 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19718 .await
19719 .expect("Failed to navigate to definition");
19720 assert_eq!(
19721 navigated,
19722 Navigated::Yes,
19723 "Should have navigated to definition from the GetDefinition response"
19724 );
19725 cx.assert_editor_state(
19726 &r#"fn one() {
19727 let mut a = two();
19728 }
19729
19730 fn «twoˇ»() {}"#
19731 .unindent(),
19732 );
19733
19734 let editors = cx.update_workspace(|workspace, _, cx| {
19735 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19736 });
19737 cx.update_editor(|_, _, test_editor_cx| {
19738 assert_eq!(
19739 editors.len(),
19740 1,
19741 "Initially, only one, test, editor should be open in the workspace"
19742 );
19743 assert_eq!(
19744 test_editor_cx.entity(),
19745 editors.last().expect("Asserted len is 1").clone()
19746 );
19747 });
19748
19749 set_up_lsp_handlers(true, &mut cx);
19750 let navigated = cx
19751 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19752 .await
19753 .expect("Failed to navigate to lookup references");
19754 assert_eq!(
19755 navigated,
19756 Navigated::Yes,
19757 "Should have navigated to references as a fallback after empty GoToDefinition response"
19758 );
19759 // We should not change the selections in the existing file,
19760 // if opening another milti buffer with the references
19761 cx.assert_editor_state(
19762 &r#"fn one() {
19763 let mut a = two();
19764 }
19765
19766 fn «twoˇ»() {}"#
19767 .unindent(),
19768 );
19769 let editors = cx.update_workspace(|workspace, _, cx| {
19770 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19771 });
19772 cx.update_editor(|_, _, test_editor_cx| {
19773 assert_eq!(
19774 editors.len(),
19775 2,
19776 "After falling back to references search, we open a new editor with the results"
19777 );
19778 let references_fallback_text = editors
19779 .into_iter()
19780 .find(|new_editor| *new_editor != test_editor_cx.entity())
19781 .expect("Should have one non-test editor now")
19782 .read(test_editor_cx)
19783 .text(test_editor_cx);
19784 assert_eq!(
19785 references_fallback_text, "fn one() {\n let mut a = two();\n}",
19786 "Should use the range from the references response and not the GoToDefinition one"
19787 );
19788 });
19789}
19790
19791#[gpui::test]
19792async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
19793 init_test(cx, |_| {});
19794 cx.update(|cx| {
19795 let mut editor_settings = EditorSettings::get_global(cx).clone();
19796 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
19797 EditorSettings::override_global(editor_settings, cx);
19798 });
19799 let mut cx = EditorLspTestContext::new_rust(
19800 lsp::ServerCapabilities {
19801 definition_provider: Some(lsp::OneOf::Left(true)),
19802 references_provider: Some(lsp::OneOf::Left(true)),
19803 ..lsp::ServerCapabilities::default()
19804 },
19805 cx,
19806 )
19807 .await;
19808 let original_state = r#"fn one() {
19809 let mut a = ˇtwo();
19810 }
19811
19812 fn two() {}"#
19813 .unindent();
19814 cx.set_state(&original_state);
19815
19816 let mut go_to_definition = cx
19817 .lsp
19818 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19819 move |_, _| async move { Ok(None) },
19820 );
19821 let _references = cx
19822 .lsp
19823 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
19824 panic!("Should not call for references with no go to definition fallback")
19825 });
19826
19827 let navigated = cx
19828 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19829 .await
19830 .expect("Failed to navigate to lookup references");
19831 go_to_definition
19832 .next()
19833 .await
19834 .expect("Should have called the go_to_definition handler");
19835
19836 assert_eq!(
19837 navigated,
19838 Navigated::No,
19839 "Should have navigated to references as a fallback after empty GoToDefinition response"
19840 );
19841 cx.assert_editor_state(&original_state);
19842 let editors = cx.update_workspace(|workspace, _, cx| {
19843 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19844 });
19845 cx.update_editor(|_, _, _| {
19846 assert_eq!(
19847 editors.len(),
19848 1,
19849 "After unsuccessful fallback, no other editor should have been opened"
19850 );
19851 });
19852}
19853
19854#[gpui::test]
19855async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
19856 init_test(cx, |_| {});
19857
19858 let language = Arc::new(Language::new(
19859 LanguageConfig::default(),
19860 Some(tree_sitter_rust::LANGUAGE.into()),
19861 ));
19862
19863 let text = r#"
19864 #[cfg(test)]
19865 mod tests() {
19866 #[test]
19867 fn runnable_1() {
19868 let a = 1;
19869 }
19870
19871 #[test]
19872 fn runnable_2() {
19873 let a = 1;
19874 let b = 2;
19875 }
19876 }
19877 "#
19878 .unindent();
19879
19880 let fs = FakeFs::new(cx.executor());
19881 fs.insert_file("/file.rs", Default::default()).await;
19882
19883 let project = Project::test(fs, ["/a".as_ref()], cx).await;
19884 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19885 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19886 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
19887 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
19888
19889 let editor = cx.new_window_entity(|window, cx| {
19890 Editor::new(
19891 EditorMode::full(),
19892 multi_buffer,
19893 Some(project.clone()),
19894 window,
19895 cx,
19896 )
19897 });
19898
19899 editor.update_in(cx, |editor, window, cx| {
19900 let snapshot = editor.buffer().read(cx).snapshot(cx);
19901 editor.tasks.insert(
19902 (buffer.read(cx).remote_id(), 3),
19903 RunnableTasks {
19904 templates: vec![],
19905 offset: snapshot.anchor_before(43),
19906 column: 0,
19907 extra_variables: HashMap::default(),
19908 context_range: BufferOffset(43)..BufferOffset(85),
19909 },
19910 );
19911 editor.tasks.insert(
19912 (buffer.read(cx).remote_id(), 8),
19913 RunnableTasks {
19914 templates: vec![],
19915 offset: snapshot.anchor_before(86),
19916 column: 0,
19917 extra_variables: HashMap::default(),
19918 context_range: BufferOffset(86)..BufferOffset(191),
19919 },
19920 );
19921
19922 // Test finding task when cursor is inside function body
19923 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19924 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
19925 });
19926 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19927 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
19928
19929 // Test finding task when cursor is on function name
19930 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19931 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
19932 });
19933 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19934 assert_eq!(row, 8, "Should find task when cursor is on function name");
19935 });
19936}
19937
19938#[gpui::test]
19939async fn test_folding_buffers(cx: &mut TestAppContext) {
19940 init_test(cx, |_| {});
19941
19942 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
19943 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
19944 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
19945
19946 let fs = FakeFs::new(cx.executor());
19947 fs.insert_tree(
19948 path!("/a"),
19949 json!({
19950 "first.rs": sample_text_1,
19951 "second.rs": sample_text_2,
19952 "third.rs": sample_text_3,
19953 }),
19954 )
19955 .await;
19956 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19957 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19958 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19959 let worktree = project.update(cx, |project, cx| {
19960 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19961 assert_eq!(worktrees.len(), 1);
19962 worktrees.pop().unwrap()
19963 });
19964 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19965
19966 let buffer_1 = project
19967 .update(cx, |project, cx| {
19968 project.open_buffer((worktree_id, "first.rs"), cx)
19969 })
19970 .await
19971 .unwrap();
19972 let buffer_2 = project
19973 .update(cx, |project, cx| {
19974 project.open_buffer((worktree_id, "second.rs"), cx)
19975 })
19976 .await
19977 .unwrap();
19978 let buffer_3 = project
19979 .update(cx, |project, cx| {
19980 project.open_buffer((worktree_id, "third.rs"), cx)
19981 })
19982 .await
19983 .unwrap();
19984
19985 let multi_buffer = cx.new(|cx| {
19986 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19987 multi_buffer.push_excerpts(
19988 buffer_1.clone(),
19989 [
19990 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19991 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19992 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19993 ],
19994 cx,
19995 );
19996 multi_buffer.push_excerpts(
19997 buffer_2.clone(),
19998 [
19999 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20000 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20001 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20002 ],
20003 cx,
20004 );
20005 multi_buffer.push_excerpts(
20006 buffer_3.clone(),
20007 [
20008 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20009 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20010 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20011 ],
20012 cx,
20013 );
20014 multi_buffer
20015 });
20016 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20017 Editor::new(
20018 EditorMode::full(),
20019 multi_buffer.clone(),
20020 Some(project.clone()),
20021 window,
20022 cx,
20023 )
20024 });
20025
20026 assert_eq!(
20027 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20028 "\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",
20029 );
20030
20031 multi_buffer_editor.update(cx, |editor, cx| {
20032 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
20033 });
20034 assert_eq!(
20035 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20036 "\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",
20037 "After folding the first buffer, its text should not be displayed"
20038 );
20039
20040 multi_buffer_editor.update(cx, |editor, cx| {
20041 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
20042 });
20043 assert_eq!(
20044 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20045 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
20046 "After folding the second buffer, its text should not be displayed"
20047 );
20048
20049 multi_buffer_editor.update(cx, |editor, cx| {
20050 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
20051 });
20052 assert_eq!(
20053 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20054 "\n\n\n\n\n",
20055 "After folding the third buffer, its text should not be displayed"
20056 );
20057
20058 // Emulate selection inside the fold logic, that should work
20059 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20060 editor
20061 .snapshot(window, cx)
20062 .next_line_boundary(Point::new(0, 4));
20063 });
20064
20065 multi_buffer_editor.update(cx, |editor, cx| {
20066 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
20067 });
20068 assert_eq!(
20069 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20070 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
20071 "After unfolding the second buffer, its text should be displayed"
20072 );
20073
20074 // Typing inside of buffer 1 causes that buffer to be unfolded.
20075 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20076 assert_eq!(
20077 multi_buffer
20078 .read(cx)
20079 .snapshot(cx)
20080 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
20081 .collect::<String>(),
20082 "bbbb"
20083 );
20084 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
20085 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
20086 });
20087 editor.handle_input("B", window, cx);
20088 });
20089
20090 assert_eq!(
20091 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20092 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
20093 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
20094 );
20095
20096 multi_buffer_editor.update(cx, |editor, cx| {
20097 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
20098 });
20099 assert_eq!(
20100 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20101 "\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",
20102 "After unfolding the all buffers, all original text should be displayed"
20103 );
20104}
20105
20106#[gpui::test]
20107async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
20108 init_test(cx, |_| {});
20109
20110 let sample_text_1 = "1111\n2222\n3333".to_string();
20111 let sample_text_2 = "4444\n5555\n6666".to_string();
20112 let sample_text_3 = "7777\n8888\n9999".to_string();
20113
20114 let fs = FakeFs::new(cx.executor());
20115 fs.insert_tree(
20116 path!("/a"),
20117 json!({
20118 "first.rs": sample_text_1,
20119 "second.rs": sample_text_2,
20120 "third.rs": sample_text_3,
20121 }),
20122 )
20123 .await;
20124 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20125 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20126 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20127 let worktree = project.update(cx, |project, cx| {
20128 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
20129 assert_eq!(worktrees.len(), 1);
20130 worktrees.pop().unwrap()
20131 });
20132 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
20133
20134 let buffer_1 = project
20135 .update(cx, |project, cx| {
20136 project.open_buffer((worktree_id, "first.rs"), cx)
20137 })
20138 .await
20139 .unwrap();
20140 let buffer_2 = project
20141 .update(cx, |project, cx| {
20142 project.open_buffer((worktree_id, "second.rs"), cx)
20143 })
20144 .await
20145 .unwrap();
20146 let buffer_3 = project
20147 .update(cx, |project, cx| {
20148 project.open_buffer((worktree_id, "third.rs"), cx)
20149 })
20150 .await
20151 .unwrap();
20152
20153 let multi_buffer = cx.new(|cx| {
20154 let mut multi_buffer = MultiBuffer::new(ReadWrite);
20155 multi_buffer.push_excerpts(
20156 buffer_1.clone(),
20157 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
20158 cx,
20159 );
20160 multi_buffer.push_excerpts(
20161 buffer_2.clone(),
20162 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
20163 cx,
20164 );
20165 multi_buffer.push_excerpts(
20166 buffer_3.clone(),
20167 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
20168 cx,
20169 );
20170 multi_buffer
20171 });
20172
20173 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20174 Editor::new(
20175 EditorMode::full(),
20176 multi_buffer,
20177 Some(project.clone()),
20178 window,
20179 cx,
20180 )
20181 });
20182
20183 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
20184 assert_eq!(
20185 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20186 full_text,
20187 );
20188
20189 multi_buffer_editor.update(cx, |editor, cx| {
20190 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
20191 });
20192 assert_eq!(
20193 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20194 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
20195 "After folding the first buffer, its text should not be displayed"
20196 );
20197
20198 multi_buffer_editor.update(cx, |editor, cx| {
20199 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
20200 });
20201
20202 assert_eq!(
20203 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20204 "\n\n\n\n\n\n7777\n8888\n9999",
20205 "After folding the second buffer, its text should not be displayed"
20206 );
20207
20208 multi_buffer_editor.update(cx, |editor, cx| {
20209 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
20210 });
20211 assert_eq!(
20212 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20213 "\n\n\n\n\n",
20214 "After folding the third buffer, its text should not be displayed"
20215 );
20216
20217 multi_buffer_editor.update(cx, |editor, cx| {
20218 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
20219 });
20220 assert_eq!(
20221 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20222 "\n\n\n\n4444\n5555\n6666\n\n",
20223 "After unfolding the second buffer, its text should be displayed"
20224 );
20225
20226 multi_buffer_editor.update(cx, |editor, cx| {
20227 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
20228 });
20229 assert_eq!(
20230 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20231 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
20232 "After unfolding the first buffer, its text should be displayed"
20233 );
20234
20235 multi_buffer_editor.update(cx, |editor, cx| {
20236 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
20237 });
20238 assert_eq!(
20239 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20240 full_text,
20241 "After unfolding all buffers, all original text should be displayed"
20242 );
20243}
20244
20245#[gpui::test]
20246async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
20247 init_test(cx, |_| {});
20248
20249 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
20250
20251 let fs = FakeFs::new(cx.executor());
20252 fs.insert_tree(
20253 path!("/a"),
20254 json!({
20255 "main.rs": sample_text,
20256 }),
20257 )
20258 .await;
20259 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20260 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20261 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20262 let worktree = project.update(cx, |project, cx| {
20263 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
20264 assert_eq!(worktrees.len(), 1);
20265 worktrees.pop().unwrap()
20266 });
20267 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
20268
20269 let buffer_1 = project
20270 .update(cx, |project, cx| {
20271 project.open_buffer((worktree_id, "main.rs"), cx)
20272 })
20273 .await
20274 .unwrap();
20275
20276 let multi_buffer = cx.new(|cx| {
20277 let mut multi_buffer = MultiBuffer::new(ReadWrite);
20278 multi_buffer.push_excerpts(
20279 buffer_1.clone(),
20280 [ExcerptRange::new(
20281 Point::new(0, 0)
20282 ..Point::new(
20283 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
20284 0,
20285 ),
20286 )],
20287 cx,
20288 );
20289 multi_buffer
20290 });
20291 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20292 Editor::new(
20293 EditorMode::full(),
20294 multi_buffer,
20295 Some(project.clone()),
20296 window,
20297 cx,
20298 )
20299 });
20300
20301 let selection_range = Point::new(1, 0)..Point::new(2, 0);
20302 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20303 enum TestHighlight {}
20304 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
20305 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
20306 editor.highlight_text::<TestHighlight>(
20307 vec![highlight_range.clone()],
20308 HighlightStyle::color(Hsla::green()),
20309 cx,
20310 );
20311 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20312 s.select_ranges(Some(highlight_range))
20313 });
20314 });
20315
20316 let full_text = format!("\n\n{sample_text}");
20317 assert_eq!(
20318 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20319 full_text,
20320 );
20321}
20322
20323#[gpui::test]
20324async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
20325 init_test(cx, |_| {});
20326 cx.update(|cx| {
20327 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
20328 "keymaps/default-linux.json",
20329 cx,
20330 )
20331 .unwrap();
20332 cx.bind_keys(default_key_bindings);
20333 });
20334
20335 let (editor, cx) = cx.add_window_view(|window, cx| {
20336 let multi_buffer = MultiBuffer::build_multi(
20337 [
20338 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
20339 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
20340 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
20341 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
20342 ],
20343 cx,
20344 );
20345 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
20346
20347 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
20348 // fold all but the second buffer, so that we test navigating between two
20349 // adjacent folded buffers, as well as folded buffers at the start and
20350 // end the multibuffer
20351 editor.fold_buffer(buffer_ids[0], cx);
20352 editor.fold_buffer(buffer_ids[2], cx);
20353 editor.fold_buffer(buffer_ids[3], cx);
20354
20355 editor
20356 });
20357 cx.simulate_resize(size(px(1000.), px(1000.)));
20358
20359 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
20360 cx.assert_excerpts_with_selections(indoc! {"
20361 [EXCERPT]
20362 ˇ[FOLDED]
20363 [EXCERPT]
20364 a1
20365 b1
20366 [EXCERPT]
20367 [FOLDED]
20368 [EXCERPT]
20369 [FOLDED]
20370 "
20371 });
20372 cx.simulate_keystroke("down");
20373 cx.assert_excerpts_with_selections(indoc! {"
20374 [EXCERPT]
20375 [FOLDED]
20376 [EXCERPT]
20377 ˇa1
20378 b1
20379 [EXCERPT]
20380 [FOLDED]
20381 [EXCERPT]
20382 [FOLDED]
20383 "
20384 });
20385 cx.simulate_keystroke("down");
20386 cx.assert_excerpts_with_selections(indoc! {"
20387 [EXCERPT]
20388 [FOLDED]
20389 [EXCERPT]
20390 a1
20391 ˇb1
20392 [EXCERPT]
20393 [FOLDED]
20394 [EXCERPT]
20395 [FOLDED]
20396 "
20397 });
20398 cx.simulate_keystroke("down");
20399 cx.assert_excerpts_with_selections(indoc! {"
20400 [EXCERPT]
20401 [FOLDED]
20402 [EXCERPT]
20403 a1
20404 b1
20405 ˇ[EXCERPT]
20406 [FOLDED]
20407 [EXCERPT]
20408 [FOLDED]
20409 "
20410 });
20411 cx.simulate_keystroke("down");
20412 cx.assert_excerpts_with_selections(indoc! {"
20413 [EXCERPT]
20414 [FOLDED]
20415 [EXCERPT]
20416 a1
20417 b1
20418 [EXCERPT]
20419 ˇ[FOLDED]
20420 [EXCERPT]
20421 [FOLDED]
20422 "
20423 });
20424 for _ in 0..5 {
20425 cx.simulate_keystroke("down");
20426 cx.assert_excerpts_with_selections(indoc! {"
20427 [EXCERPT]
20428 [FOLDED]
20429 [EXCERPT]
20430 a1
20431 b1
20432 [EXCERPT]
20433 [FOLDED]
20434 [EXCERPT]
20435 ˇ[FOLDED]
20436 "
20437 });
20438 }
20439
20440 cx.simulate_keystroke("up");
20441 cx.assert_excerpts_with_selections(indoc! {"
20442 [EXCERPT]
20443 [FOLDED]
20444 [EXCERPT]
20445 a1
20446 b1
20447 [EXCERPT]
20448 ˇ[FOLDED]
20449 [EXCERPT]
20450 [FOLDED]
20451 "
20452 });
20453 cx.simulate_keystroke("up");
20454 cx.assert_excerpts_with_selections(indoc! {"
20455 [EXCERPT]
20456 [FOLDED]
20457 [EXCERPT]
20458 a1
20459 b1
20460 ˇ[EXCERPT]
20461 [FOLDED]
20462 [EXCERPT]
20463 [FOLDED]
20464 "
20465 });
20466 cx.simulate_keystroke("up");
20467 cx.assert_excerpts_with_selections(indoc! {"
20468 [EXCERPT]
20469 [FOLDED]
20470 [EXCERPT]
20471 a1
20472 ˇb1
20473 [EXCERPT]
20474 [FOLDED]
20475 [EXCERPT]
20476 [FOLDED]
20477 "
20478 });
20479 cx.simulate_keystroke("up");
20480 cx.assert_excerpts_with_selections(indoc! {"
20481 [EXCERPT]
20482 [FOLDED]
20483 [EXCERPT]
20484 ˇa1
20485 b1
20486 [EXCERPT]
20487 [FOLDED]
20488 [EXCERPT]
20489 [FOLDED]
20490 "
20491 });
20492 for _ in 0..5 {
20493 cx.simulate_keystroke("up");
20494 cx.assert_excerpts_with_selections(indoc! {"
20495 [EXCERPT]
20496 ˇ[FOLDED]
20497 [EXCERPT]
20498 a1
20499 b1
20500 [EXCERPT]
20501 [FOLDED]
20502 [EXCERPT]
20503 [FOLDED]
20504 "
20505 });
20506 }
20507}
20508
20509#[gpui::test]
20510async fn test_inline_completion_text(cx: &mut TestAppContext) {
20511 init_test(cx, |_| {});
20512
20513 // Simple insertion
20514 assert_highlighted_edits(
20515 "Hello, world!",
20516 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
20517 true,
20518 cx,
20519 |highlighted_edits, cx| {
20520 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
20521 assert_eq!(highlighted_edits.highlights.len(), 1);
20522 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
20523 assert_eq!(
20524 highlighted_edits.highlights[0].1.background_color,
20525 Some(cx.theme().status().created_background)
20526 );
20527 },
20528 )
20529 .await;
20530
20531 // Replacement
20532 assert_highlighted_edits(
20533 "This is a test.",
20534 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
20535 false,
20536 cx,
20537 |highlighted_edits, cx| {
20538 assert_eq!(highlighted_edits.text, "That is a test.");
20539 assert_eq!(highlighted_edits.highlights.len(), 1);
20540 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
20541 assert_eq!(
20542 highlighted_edits.highlights[0].1.background_color,
20543 Some(cx.theme().status().created_background)
20544 );
20545 },
20546 )
20547 .await;
20548
20549 // Multiple edits
20550 assert_highlighted_edits(
20551 "Hello, world!",
20552 vec![
20553 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
20554 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
20555 ],
20556 false,
20557 cx,
20558 |highlighted_edits, cx| {
20559 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
20560 assert_eq!(highlighted_edits.highlights.len(), 2);
20561 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
20562 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
20563 assert_eq!(
20564 highlighted_edits.highlights[0].1.background_color,
20565 Some(cx.theme().status().created_background)
20566 );
20567 assert_eq!(
20568 highlighted_edits.highlights[1].1.background_color,
20569 Some(cx.theme().status().created_background)
20570 );
20571 },
20572 )
20573 .await;
20574
20575 // Multiple lines with edits
20576 assert_highlighted_edits(
20577 "First line\nSecond line\nThird line\nFourth line",
20578 vec![
20579 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
20580 (
20581 Point::new(2, 0)..Point::new(2, 10),
20582 "New third line".to_string(),
20583 ),
20584 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
20585 ],
20586 false,
20587 cx,
20588 |highlighted_edits, cx| {
20589 assert_eq!(
20590 highlighted_edits.text,
20591 "Second modified\nNew third line\nFourth updated line"
20592 );
20593 assert_eq!(highlighted_edits.highlights.len(), 3);
20594 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
20595 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
20596 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
20597 for highlight in &highlighted_edits.highlights {
20598 assert_eq!(
20599 highlight.1.background_color,
20600 Some(cx.theme().status().created_background)
20601 );
20602 }
20603 },
20604 )
20605 .await;
20606}
20607
20608#[gpui::test]
20609async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
20610 init_test(cx, |_| {});
20611
20612 // Deletion
20613 assert_highlighted_edits(
20614 "Hello, world!",
20615 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
20616 true,
20617 cx,
20618 |highlighted_edits, cx| {
20619 assert_eq!(highlighted_edits.text, "Hello, world!");
20620 assert_eq!(highlighted_edits.highlights.len(), 1);
20621 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
20622 assert_eq!(
20623 highlighted_edits.highlights[0].1.background_color,
20624 Some(cx.theme().status().deleted_background)
20625 );
20626 },
20627 )
20628 .await;
20629
20630 // Insertion
20631 assert_highlighted_edits(
20632 "Hello, world!",
20633 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
20634 true,
20635 cx,
20636 |highlighted_edits, cx| {
20637 assert_eq!(highlighted_edits.highlights.len(), 1);
20638 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
20639 assert_eq!(
20640 highlighted_edits.highlights[0].1.background_color,
20641 Some(cx.theme().status().created_background)
20642 );
20643 },
20644 )
20645 .await;
20646}
20647
20648async fn assert_highlighted_edits(
20649 text: &str,
20650 edits: Vec<(Range<Point>, String)>,
20651 include_deletions: bool,
20652 cx: &mut TestAppContext,
20653 assertion_fn: impl Fn(HighlightedText, &App),
20654) {
20655 let window = cx.add_window(|window, cx| {
20656 let buffer = MultiBuffer::build_simple(text, cx);
20657 Editor::new(EditorMode::full(), buffer, None, window, cx)
20658 });
20659 let cx = &mut VisualTestContext::from_window(*window, cx);
20660
20661 let (buffer, snapshot) = window
20662 .update(cx, |editor, _window, cx| {
20663 (
20664 editor.buffer().clone(),
20665 editor.buffer().read(cx).snapshot(cx),
20666 )
20667 })
20668 .unwrap();
20669
20670 let edits = edits
20671 .into_iter()
20672 .map(|(range, edit)| {
20673 (
20674 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
20675 edit,
20676 )
20677 })
20678 .collect::<Vec<_>>();
20679
20680 let text_anchor_edits = edits
20681 .clone()
20682 .into_iter()
20683 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
20684 .collect::<Vec<_>>();
20685
20686 let edit_preview = window
20687 .update(cx, |_, _window, cx| {
20688 buffer
20689 .read(cx)
20690 .as_singleton()
20691 .unwrap()
20692 .read(cx)
20693 .preview_edits(text_anchor_edits.into(), cx)
20694 })
20695 .unwrap()
20696 .await;
20697
20698 cx.update(|_window, cx| {
20699 let highlighted_edits = inline_completion_edit_text(
20700 &snapshot.as_singleton().unwrap().2,
20701 &edits,
20702 &edit_preview,
20703 include_deletions,
20704 cx,
20705 );
20706 assertion_fn(highlighted_edits, cx)
20707 });
20708}
20709
20710#[track_caller]
20711fn assert_breakpoint(
20712 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
20713 path: &Arc<Path>,
20714 expected: Vec<(u32, Breakpoint)>,
20715) {
20716 if expected.len() == 0usize {
20717 assert!(!breakpoints.contains_key(path), "{}", path.display());
20718 } else {
20719 let mut breakpoint = breakpoints
20720 .get(path)
20721 .unwrap()
20722 .into_iter()
20723 .map(|breakpoint| {
20724 (
20725 breakpoint.row,
20726 Breakpoint {
20727 message: breakpoint.message.clone(),
20728 state: breakpoint.state,
20729 condition: breakpoint.condition.clone(),
20730 hit_condition: breakpoint.hit_condition.clone(),
20731 },
20732 )
20733 })
20734 .collect::<Vec<_>>();
20735
20736 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
20737
20738 assert_eq!(expected, breakpoint);
20739 }
20740}
20741
20742fn add_log_breakpoint_at_cursor(
20743 editor: &mut Editor,
20744 log_message: &str,
20745 window: &mut Window,
20746 cx: &mut Context<Editor>,
20747) {
20748 let (anchor, bp) = editor
20749 .breakpoints_at_cursors(window, cx)
20750 .first()
20751 .and_then(|(anchor, bp)| {
20752 if let Some(bp) = bp {
20753 Some((*anchor, bp.clone()))
20754 } else {
20755 None
20756 }
20757 })
20758 .unwrap_or_else(|| {
20759 let cursor_position: Point = editor.selections.newest(cx).head();
20760
20761 let breakpoint_position = editor
20762 .snapshot(window, cx)
20763 .display_snapshot
20764 .buffer_snapshot
20765 .anchor_before(Point::new(cursor_position.row, 0));
20766
20767 (breakpoint_position, Breakpoint::new_log(&log_message))
20768 });
20769
20770 editor.edit_breakpoint_at_anchor(
20771 anchor,
20772 bp,
20773 BreakpointEditAction::EditLogMessage(log_message.into()),
20774 cx,
20775 );
20776}
20777
20778#[gpui::test]
20779async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
20780 init_test(cx, |_| {});
20781
20782 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20783 let fs = FakeFs::new(cx.executor());
20784 fs.insert_tree(
20785 path!("/a"),
20786 json!({
20787 "main.rs": sample_text,
20788 }),
20789 )
20790 .await;
20791 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20792 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20793 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20794
20795 let fs = FakeFs::new(cx.executor());
20796 fs.insert_tree(
20797 path!("/a"),
20798 json!({
20799 "main.rs": sample_text,
20800 }),
20801 )
20802 .await;
20803 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20804 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20805 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20806 let worktree_id = workspace
20807 .update(cx, |workspace, _window, cx| {
20808 workspace.project().update(cx, |project, cx| {
20809 project.worktrees(cx).next().unwrap().read(cx).id()
20810 })
20811 })
20812 .unwrap();
20813
20814 let buffer = project
20815 .update(cx, |project, cx| {
20816 project.open_buffer((worktree_id, "main.rs"), cx)
20817 })
20818 .await
20819 .unwrap();
20820
20821 let (editor, cx) = cx.add_window_view(|window, cx| {
20822 Editor::new(
20823 EditorMode::full(),
20824 MultiBuffer::build_from_buffer(buffer, cx),
20825 Some(project.clone()),
20826 window,
20827 cx,
20828 )
20829 });
20830
20831 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20832 let abs_path = project.read_with(cx, |project, cx| {
20833 project
20834 .absolute_path(&project_path, cx)
20835 .map(|path_buf| Arc::from(path_buf.to_owned()))
20836 .unwrap()
20837 });
20838
20839 // assert we can add breakpoint on the first line
20840 editor.update_in(cx, |editor, window, cx| {
20841 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20842 editor.move_to_end(&MoveToEnd, window, cx);
20843 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20844 });
20845
20846 let breakpoints = editor.update(cx, |editor, cx| {
20847 editor
20848 .breakpoint_store()
20849 .as_ref()
20850 .unwrap()
20851 .read(cx)
20852 .all_source_breakpoints(cx)
20853 .clone()
20854 });
20855
20856 assert_eq!(1, breakpoints.len());
20857 assert_breakpoint(
20858 &breakpoints,
20859 &abs_path,
20860 vec![
20861 (0, Breakpoint::new_standard()),
20862 (3, Breakpoint::new_standard()),
20863 ],
20864 );
20865
20866 editor.update_in(cx, |editor, window, cx| {
20867 editor.move_to_beginning(&MoveToBeginning, window, cx);
20868 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20869 });
20870
20871 let breakpoints = editor.update(cx, |editor, cx| {
20872 editor
20873 .breakpoint_store()
20874 .as_ref()
20875 .unwrap()
20876 .read(cx)
20877 .all_source_breakpoints(cx)
20878 .clone()
20879 });
20880
20881 assert_eq!(1, breakpoints.len());
20882 assert_breakpoint(
20883 &breakpoints,
20884 &abs_path,
20885 vec![(3, Breakpoint::new_standard())],
20886 );
20887
20888 editor.update_in(cx, |editor, window, cx| {
20889 editor.move_to_end(&MoveToEnd, window, cx);
20890 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20891 });
20892
20893 let breakpoints = editor.update(cx, |editor, cx| {
20894 editor
20895 .breakpoint_store()
20896 .as_ref()
20897 .unwrap()
20898 .read(cx)
20899 .all_source_breakpoints(cx)
20900 .clone()
20901 });
20902
20903 assert_eq!(0, breakpoints.len());
20904 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20905}
20906
20907#[gpui::test]
20908async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
20909 init_test(cx, |_| {});
20910
20911 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20912
20913 let fs = FakeFs::new(cx.executor());
20914 fs.insert_tree(
20915 path!("/a"),
20916 json!({
20917 "main.rs": sample_text,
20918 }),
20919 )
20920 .await;
20921 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20922 let (workspace, cx) =
20923 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20924
20925 let worktree_id = workspace.update(cx, |workspace, cx| {
20926 workspace.project().update(cx, |project, cx| {
20927 project.worktrees(cx).next().unwrap().read(cx).id()
20928 })
20929 });
20930
20931 let buffer = project
20932 .update(cx, |project, cx| {
20933 project.open_buffer((worktree_id, "main.rs"), cx)
20934 })
20935 .await
20936 .unwrap();
20937
20938 let (editor, cx) = cx.add_window_view(|window, cx| {
20939 Editor::new(
20940 EditorMode::full(),
20941 MultiBuffer::build_from_buffer(buffer, cx),
20942 Some(project.clone()),
20943 window,
20944 cx,
20945 )
20946 });
20947
20948 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20949 let abs_path = project.read_with(cx, |project, cx| {
20950 project
20951 .absolute_path(&project_path, cx)
20952 .map(|path_buf| Arc::from(path_buf.to_owned()))
20953 .unwrap()
20954 });
20955
20956 editor.update_in(cx, |editor, window, cx| {
20957 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20958 });
20959
20960 let breakpoints = editor.update(cx, |editor, cx| {
20961 editor
20962 .breakpoint_store()
20963 .as_ref()
20964 .unwrap()
20965 .read(cx)
20966 .all_source_breakpoints(cx)
20967 .clone()
20968 });
20969
20970 assert_breakpoint(
20971 &breakpoints,
20972 &abs_path,
20973 vec![(0, Breakpoint::new_log("hello world"))],
20974 );
20975
20976 // Removing a log message from a log breakpoint should remove it
20977 editor.update_in(cx, |editor, window, cx| {
20978 add_log_breakpoint_at_cursor(editor, "", window, cx);
20979 });
20980
20981 let breakpoints = editor.update(cx, |editor, cx| {
20982 editor
20983 .breakpoint_store()
20984 .as_ref()
20985 .unwrap()
20986 .read(cx)
20987 .all_source_breakpoints(cx)
20988 .clone()
20989 });
20990
20991 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20992
20993 editor.update_in(cx, |editor, window, cx| {
20994 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20995 editor.move_to_end(&MoveToEnd, window, cx);
20996 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20997 // Not adding a log message to a standard breakpoint shouldn't remove it
20998 add_log_breakpoint_at_cursor(editor, "", window, cx);
20999 });
21000
21001 let breakpoints = editor.update(cx, |editor, cx| {
21002 editor
21003 .breakpoint_store()
21004 .as_ref()
21005 .unwrap()
21006 .read(cx)
21007 .all_source_breakpoints(cx)
21008 .clone()
21009 });
21010
21011 assert_breakpoint(
21012 &breakpoints,
21013 &abs_path,
21014 vec![
21015 (0, Breakpoint::new_standard()),
21016 (3, Breakpoint::new_standard()),
21017 ],
21018 );
21019
21020 editor.update_in(cx, |editor, window, cx| {
21021 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
21022 });
21023
21024 let breakpoints = editor.update(cx, |editor, cx| {
21025 editor
21026 .breakpoint_store()
21027 .as_ref()
21028 .unwrap()
21029 .read(cx)
21030 .all_source_breakpoints(cx)
21031 .clone()
21032 });
21033
21034 assert_breakpoint(
21035 &breakpoints,
21036 &abs_path,
21037 vec![
21038 (0, Breakpoint::new_standard()),
21039 (3, Breakpoint::new_log("hello world")),
21040 ],
21041 );
21042
21043 editor.update_in(cx, |editor, window, cx| {
21044 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
21045 });
21046
21047 let breakpoints = editor.update(cx, |editor, cx| {
21048 editor
21049 .breakpoint_store()
21050 .as_ref()
21051 .unwrap()
21052 .read(cx)
21053 .all_source_breakpoints(cx)
21054 .clone()
21055 });
21056
21057 assert_breakpoint(
21058 &breakpoints,
21059 &abs_path,
21060 vec![
21061 (0, Breakpoint::new_standard()),
21062 (3, Breakpoint::new_log("hello Earth!!")),
21063 ],
21064 );
21065}
21066
21067/// This also tests that Editor::breakpoint_at_cursor_head is working properly
21068/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
21069/// or when breakpoints were placed out of order. This tests for a regression too
21070#[gpui::test]
21071async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
21072 init_test(cx, |_| {});
21073
21074 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
21075 let fs = FakeFs::new(cx.executor());
21076 fs.insert_tree(
21077 path!("/a"),
21078 json!({
21079 "main.rs": sample_text,
21080 }),
21081 )
21082 .await;
21083 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21084 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21085 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21086
21087 let fs = FakeFs::new(cx.executor());
21088 fs.insert_tree(
21089 path!("/a"),
21090 json!({
21091 "main.rs": sample_text,
21092 }),
21093 )
21094 .await;
21095 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21096 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21097 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21098 let worktree_id = workspace
21099 .update(cx, |workspace, _window, cx| {
21100 workspace.project().update(cx, |project, cx| {
21101 project.worktrees(cx).next().unwrap().read(cx).id()
21102 })
21103 })
21104 .unwrap();
21105
21106 let buffer = project
21107 .update(cx, |project, cx| {
21108 project.open_buffer((worktree_id, "main.rs"), cx)
21109 })
21110 .await
21111 .unwrap();
21112
21113 let (editor, cx) = cx.add_window_view(|window, cx| {
21114 Editor::new(
21115 EditorMode::full(),
21116 MultiBuffer::build_from_buffer(buffer, cx),
21117 Some(project.clone()),
21118 window,
21119 cx,
21120 )
21121 });
21122
21123 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
21124 let abs_path = project.read_with(cx, |project, cx| {
21125 project
21126 .absolute_path(&project_path, cx)
21127 .map(|path_buf| Arc::from(path_buf.to_owned()))
21128 .unwrap()
21129 });
21130
21131 // assert we can add breakpoint on the first line
21132 editor.update_in(cx, |editor, window, cx| {
21133 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21134 editor.move_to_end(&MoveToEnd, window, cx);
21135 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21136 editor.move_up(&MoveUp, window, cx);
21137 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21138 });
21139
21140 let breakpoints = editor.update(cx, |editor, cx| {
21141 editor
21142 .breakpoint_store()
21143 .as_ref()
21144 .unwrap()
21145 .read(cx)
21146 .all_source_breakpoints(cx)
21147 .clone()
21148 });
21149
21150 assert_eq!(1, breakpoints.len());
21151 assert_breakpoint(
21152 &breakpoints,
21153 &abs_path,
21154 vec![
21155 (0, Breakpoint::new_standard()),
21156 (2, Breakpoint::new_standard()),
21157 (3, Breakpoint::new_standard()),
21158 ],
21159 );
21160
21161 editor.update_in(cx, |editor, window, cx| {
21162 editor.move_to_beginning(&MoveToBeginning, window, cx);
21163 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21164 editor.move_to_end(&MoveToEnd, window, cx);
21165 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21166 // Disabling a breakpoint that doesn't exist should do nothing
21167 editor.move_up(&MoveUp, window, cx);
21168 editor.move_up(&MoveUp, window, cx);
21169 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21170 });
21171
21172 let breakpoints = editor.update(cx, |editor, cx| {
21173 editor
21174 .breakpoint_store()
21175 .as_ref()
21176 .unwrap()
21177 .read(cx)
21178 .all_source_breakpoints(cx)
21179 .clone()
21180 });
21181
21182 let disable_breakpoint = {
21183 let mut bp = Breakpoint::new_standard();
21184 bp.state = BreakpointState::Disabled;
21185 bp
21186 };
21187
21188 assert_eq!(1, breakpoints.len());
21189 assert_breakpoint(
21190 &breakpoints,
21191 &abs_path,
21192 vec![
21193 (0, disable_breakpoint.clone()),
21194 (2, Breakpoint::new_standard()),
21195 (3, disable_breakpoint.clone()),
21196 ],
21197 );
21198
21199 editor.update_in(cx, |editor, window, cx| {
21200 editor.move_to_beginning(&MoveToBeginning, window, cx);
21201 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
21202 editor.move_to_end(&MoveToEnd, window, cx);
21203 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
21204 editor.move_up(&MoveUp, window, cx);
21205 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21206 });
21207
21208 let breakpoints = editor.update(cx, |editor, cx| {
21209 editor
21210 .breakpoint_store()
21211 .as_ref()
21212 .unwrap()
21213 .read(cx)
21214 .all_source_breakpoints(cx)
21215 .clone()
21216 });
21217
21218 assert_eq!(1, breakpoints.len());
21219 assert_breakpoint(
21220 &breakpoints,
21221 &abs_path,
21222 vec![
21223 (0, Breakpoint::new_standard()),
21224 (2, disable_breakpoint),
21225 (3, Breakpoint::new_standard()),
21226 ],
21227 );
21228}
21229
21230#[gpui::test]
21231async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
21232 init_test(cx, |_| {});
21233 let capabilities = lsp::ServerCapabilities {
21234 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
21235 prepare_provider: Some(true),
21236 work_done_progress_options: Default::default(),
21237 })),
21238 ..Default::default()
21239 };
21240 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
21241
21242 cx.set_state(indoc! {"
21243 struct Fˇoo {}
21244 "});
21245
21246 cx.update_editor(|editor, _, cx| {
21247 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
21248 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
21249 editor.highlight_background::<DocumentHighlightRead>(
21250 &[highlight_range],
21251 |theme| theme.colors().editor_document_highlight_read_background,
21252 cx,
21253 );
21254 });
21255
21256 let mut prepare_rename_handler = cx
21257 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
21258 move |_, _, _| async move {
21259 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
21260 start: lsp::Position {
21261 line: 0,
21262 character: 7,
21263 },
21264 end: lsp::Position {
21265 line: 0,
21266 character: 10,
21267 },
21268 })))
21269 },
21270 );
21271 let prepare_rename_task = cx
21272 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
21273 .expect("Prepare rename was not started");
21274 prepare_rename_handler.next().await.unwrap();
21275 prepare_rename_task.await.expect("Prepare rename failed");
21276
21277 let mut rename_handler =
21278 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
21279 let edit = lsp::TextEdit {
21280 range: lsp::Range {
21281 start: lsp::Position {
21282 line: 0,
21283 character: 7,
21284 },
21285 end: lsp::Position {
21286 line: 0,
21287 character: 10,
21288 },
21289 },
21290 new_text: "FooRenamed".to_string(),
21291 };
21292 Ok(Some(lsp::WorkspaceEdit::new(
21293 // Specify the same edit twice
21294 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
21295 )))
21296 });
21297 let rename_task = cx
21298 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
21299 .expect("Confirm rename was not started");
21300 rename_handler.next().await.unwrap();
21301 rename_task.await.expect("Confirm rename failed");
21302 cx.run_until_parked();
21303
21304 // Despite two edits, only one is actually applied as those are identical
21305 cx.assert_editor_state(indoc! {"
21306 struct FooRenamedˇ {}
21307 "});
21308}
21309
21310#[gpui::test]
21311async fn test_rename_without_prepare(cx: &mut TestAppContext) {
21312 init_test(cx, |_| {});
21313 // These capabilities indicate that the server does not support prepare rename.
21314 let capabilities = lsp::ServerCapabilities {
21315 rename_provider: Some(lsp::OneOf::Left(true)),
21316 ..Default::default()
21317 };
21318 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
21319
21320 cx.set_state(indoc! {"
21321 struct Fˇoo {}
21322 "});
21323
21324 cx.update_editor(|editor, _window, cx| {
21325 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
21326 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
21327 editor.highlight_background::<DocumentHighlightRead>(
21328 &[highlight_range],
21329 |theme| theme.colors().editor_document_highlight_read_background,
21330 cx,
21331 );
21332 });
21333
21334 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
21335 .expect("Prepare rename was not started")
21336 .await
21337 .expect("Prepare rename failed");
21338
21339 let mut rename_handler =
21340 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
21341 let edit = lsp::TextEdit {
21342 range: lsp::Range {
21343 start: lsp::Position {
21344 line: 0,
21345 character: 7,
21346 },
21347 end: lsp::Position {
21348 line: 0,
21349 character: 10,
21350 },
21351 },
21352 new_text: "FooRenamed".to_string(),
21353 };
21354 Ok(Some(lsp::WorkspaceEdit::new(
21355 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
21356 )))
21357 });
21358 let rename_task = cx
21359 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
21360 .expect("Confirm rename was not started");
21361 rename_handler.next().await.unwrap();
21362 rename_task.await.expect("Confirm rename failed");
21363 cx.run_until_parked();
21364
21365 // Correct range is renamed, as `surrounding_word` is used to find it.
21366 cx.assert_editor_state(indoc! {"
21367 struct FooRenamedˇ {}
21368 "});
21369}
21370
21371#[gpui::test]
21372async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
21373 init_test(cx, |_| {});
21374 let mut cx = EditorTestContext::new(cx).await;
21375
21376 let language = Arc::new(
21377 Language::new(
21378 LanguageConfig::default(),
21379 Some(tree_sitter_html::LANGUAGE.into()),
21380 )
21381 .with_brackets_query(
21382 r#"
21383 ("<" @open "/>" @close)
21384 ("</" @open ">" @close)
21385 ("<" @open ">" @close)
21386 ("\"" @open "\"" @close)
21387 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
21388 "#,
21389 )
21390 .unwrap(),
21391 );
21392 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21393
21394 cx.set_state(indoc! {"
21395 <span>ˇ</span>
21396 "});
21397 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21398 cx.assert_editor_state(indoc! {"
21399 <span>
21400 ˇ
21401 </span>
21402 "});
21403
21404 cx.set_state(indoc! {"
21405 <span><span></span>ˇ</span>
21406 "});
21407 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21408 cx.assert_editor_state(indoc! {"
21409 <span><span></span>
21410 ˇ</span>
21411 "});
21412
21413 cx.set_state(indoc! {"
21414 <span>ˇ
21415 </span>
21416 "});
21417 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21418 cx.assert_editor_state(indoc! {"
21419 <span>
21420 ˇ
21421 </span>
21422 "});
21423}
21424
21425#[gpui::test(iterations = 10)]
21426async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
21427 init_test(cx, |_| {});
21428
21429 let fs = FakeFs::new(cx.executor());
21430 fs.insert_tree(
21431 path!("/dir"),
21432 json!({
21433 "a.ts": "a",
21434 }),
21435 )
21436 .await;
21437
21438 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
21439 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21440 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21441
21442 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21443 language_registry.add(Arc::new(Language::new(
21444 LanguageConfig {
21445 name: "TypeScript".into(),
21446 matcher: LanguageMatcher {
21447 path_suffixes: vec!["ts".to_string()],
21448 ..Default::default()
21449 },
21450 ..Default::default()
21451 },
21452 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
21453 )));
21454 let mut fake_language_servers = language_registry.register_fake_lsp(
21455 "TypeScript",
21456 FakeLspAdapter {
21457 capabilities: lsp::ServerCapabilities {
21458 code_lens_provider: Some(lsp::CodeLensOptions {
21459 resolve_provider: Some(true),
21460 }),
21461 execute_command_provider: Some(lsp::ExecuteCommandOptions {
21462 commands: vec!["_the/command".to_string()],
21463 ..lsp::ExecuteCommandOptions::default()
21464 }),
21465 ..lsp::ServerCapabilities::default()
21466 },
21467 ..FakeLspAdapter::default()
21468 },
21469 );
21470
21471 let editor = workspace
21472 .update(cx, |workspace, window, cx| {
21473 workspace.open_abs_path(
21474 PathBuf::from(path!("/dir/a.ts")),
21475 OpenOptions::default(),
21476 window,
21477 cx,
21478 )
21479 })
21480 .unwrap()
21481 .await
21482 .unwrap()
21483 .downcast::<Editor>()
21484 .unwrap();
21485 cx.executor().run_until_parked();
21486
21487 let fake_server = fake_language_servers.next().await.unwrap();
21488
21489 let buffer = editor.update(cx, |editor, cx| {
21490 editor
21491 .buffer()
21492 .read(cx)
21493 .as_singleton()
21494 .expect("have opened a single file by path")
21495 });
21496
21497 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
21498 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
21499 drop(buffer_snapshot);
21500 let actions = cx
21501 .update_window(*workspace, |_, window, cx| {
21502 project.code_actions(&buffer, anchor..anchor, window, cx)
21503 })
21504 .unwrap();
21505
21506 fake_server
21507 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
21508 Ok(Some(vec![
21509 lsp::CodeLens {
21510 range: lsp::Range::default(),
21511 command: Some(lsp::Command {
21512 title: "Code lens command".to_owned(),
21513 command: "_the/command".to_owned(),
21514 arguments: None,
21515 }),
21516 data: None,
21517 },
21518 lsp::CodeLens {
21519 range: lsp::Range::default(),
21520 command: Some(lsp::Command {
21521 title: "Command not in capabilities".to_owned(),
21522 command: "not in capabilities".to_owned(),
21523 arguments: None,
21524 }),
21525 data: None,
21526 },
21527 lsp::CodeLens {
21528 range: lsp::Range {
21529 start: lsp::Position {
21530 line: 1,
21531 character: 1,
21532 },
21533 end: lsp::Position {
21534 line: 1,
21535 character: 1,
21536 },
21537 },
21538 command: Some(lsp::Command {
21539 title: "Command not in range".to_owned(),
21540 command: "_the/command".to_owned(),
21541 arguments: None,
21542 }),
21543 data: None,
21544 },
21545 ]))
21546 })
21547 .next()
21548 .await;
21549
21550 let actions = actions.await.unwrap();
21551 assert_eq!(
21552 actions.len(),
21553 1,
21554 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
21555 );
21556 let action = actions[0].clone();
21557 let apply = project.update(cx, |project, cx| {
21558 project.apply_code_action(buffer.clone(), action, true, cx)
21559 });
21560
21561 // Resolving the code action does not populate its edits. In absence of
21562 // edits, we must execute the given command.
21563 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
21564 |mut lens, _| async move {
21565 let lens_command = lens.command.as_mut().expect("should have a command");
21566 assert_eq!(lens_command.title, "Code lens command");
21567 lens_command.arguments = Some(vec![json!("the-argument")]);
21568 Ok(lens)
21569 },
21570 );
21571
21572 // While executing the command, the language server sends the editor
21573 // a `workspaceEdit` request.
21574 fake_server
21575 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
21576 let fake = fake_server.clone();
21577 move |params, _| {
21578 assert_eq!(params.command, "_the/command");
21579 let fake = fake.clone();
21580 async move {
21581 fake.server
21582 .request::<lsp::request::ApplyWorkspaceEdit>(
21583 lsp::ApplyWorkspaceEditParams {
21584 label: None,
21585 edit: lsp::WorkspaceEdit {
21586 changes: Some(
21587 [(
21588 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
21589 vec![lsp::TextEdit {
21590 range: lsp::Range::new(
21591 lsp::Position::new(0, 0),
21592 lsp::Position::new(0, 0),
21593 ),
21594 new_text: "X".into(),
21595 }],
21596 )]
21597 .into_iter()
21598 .collect(),
21599 ),
21600 ..lsp::WorkspaceEdit::default()
21601 },
21602 },
21603 )
21604 .await
21605 .into_response()
21606 .unwrap();
21607 Ok(Some(json!(null)))
21608 }
21609 }
21610 })
21611 .next()
21612 .await;
21613
21614 // Applying the code lens command returns a project transaction containing the edits
21615 // sent by the language server in its `workspaceEdit` request.
21616 let transaction = apply.await.unwrap();
21617 assert!(transaction.0.contains_key(&buffer));
21618 buffer.update(cx, |buffer, cx| {
21619 assert_eq!(buffer.text(), "Xa");
21620 buffer.undo(cx);
21621 assert_eq!(buffer.text(), "a");
21622 });
21623
21624 let actions_after_edits = cx
21625 .update_window(*workspace, |_, window, cx| {
21626 project.code_actions(&buffer, anchor..anchor, window, cx)
21627 })
21628 .unwrap()
21629 .await
21630 .unwrap();
21631 assert_eq!(
21632 actions, actions_after_edits,
21633 "For the same selection, same code lens actions should be returned"
21634 );
21635
21636 let _responses =
21637 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
21638 panic!("No more code lens requests are expected");
21639 });
21640 editor.update_in(cx, |editor, window, cx| {
21641 editor.select_all(&SelectAll, window, cx);
21642 });
21643 cx.executor().run_until_parked();
21644 let new_actions = cx
21645 .update_window(*workspace, |_, window, cx| {
21646 project.code_actions(&buffer, anchor..anchor, window, cx)
21647 })
21648 .unwrap()
21649 .await
21650 .unwrap();
21651 assert_eq!(
21652 actions, new_actions,
21653 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
21654 );
21655}
21656
21657#[gpui::test]
21658async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
21659 init_test(cx, |_| {});
21660
21661 let fs = FakeFs::new(cx.executor());
21662 let main_text = r#"fn main() {
21663println!("1");
21664println!("2");
21665println!("3");
21666println!("4");
21667println!("5");
21668}"#;
21669 let lib_text = "mod foo {}";
21670 fs.insert_tree(
21671 path!("/a"),
21672 json!({
21673 "lib.rs": lib_text,
21674 "main.rs": main_text,
21675 }),
21676 )
21677 .await;
21678
21679 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21680 let (workspace, cx) =
21681 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21682 let worktree_id = workspace.update(cx, |workspace, cx| {
21683 workspace.project().update(cx, |project, cx| {
21684 project.worktrees(cx).next().unwrap().read(cx).id()
21685 })
21686 });
21687
21688 let expected_ranges = vec![
21689 Point::new(0, 0)..Point::new(0, 0),
21690 Point::new(1, 0)..Point::new(1, 1),
21691 Point::new(2, 0)..Point::new(2, 2),
21692 Point::new(3, 0)..Point::new(3, 3),
21693 ];
21694
21695 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21696 let editor_1 = workspace
21697 .update_in(cx, |workspace, window, cx| {
21698 workspace.open_path(
21699 (worktree_id, "main.rs"),
21700 Some(pane_1.downgrade()),
21701 true,
21702 window,
21703 cx,
21704 )
21705 })
21706 .unwrap()
21707 .await
21708 .downcast::<Editor>()
21709 .unwrap();
21710 pane_1.update(cx, |pane, cx| {
21711 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21712 open_editor.update(cx, |editor, cx| {
21713 assert_eq!(
21714 editor.display_text(cx),
21715 main_text,
21716 "Original main.rs text on initial open",
21717 );
21718 assert_eq!(
21719 editor
21720 .selections
21721 .all::<Point>(cx)
21722 .into_iter()
21723 .map(|s| s.range())
21724 .collect::<Vec<_>>(),
21725 vec![Point::zero()..Point::zero()],
21726 "Default selections on initial open",
21727 );
21728 })
21729 });
21730 editor_1.update_in(cx, |editor, window, cx| {
21731 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21732 s.select_ranges(expected_ranges.clone());
21733 });
21734 });
21735
21736 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
21737 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
21738 });
21739 let editor_2 = workspace
21740 .update_in(cx, |workspace, window, cx| {
21741 workspace.open_path(
21742 (worktree_id, "main.rs"),
21743 Some(pane_2.downgrade()),
21744 true,
21745 window,
21746 cx,
21747 )
21748 })
21749 .unwrap()
21750 .await
21751 .downcast::<Editor>()
21752 .unwrap();
21753 pane_2.update(cx, |pane, cx| {
21754 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21755 open_editor.update(cx, |editor, cx| {
21756 assert_eq!(
21757 editor.display_text(cx),
21758 main_text,
21759 "Original main.rs text on initial open in another panel",
21760 );
21761 assert_eq!(
21762 editor
21763 .selections
21764 .all::<Point>(cx)
21765 .into_iter()
21766 .map(|s| s.range())
21767 .collect::<Vec<_>>(),
21768 vec![Point::zero()..Point::zero()],
21769 "Default selections on initial open in another panel",
21770 );
21771 })
21772 });
21773
21774 editor_2.update_in(cx, |editor, window, cx| {
21775 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
21776 });
21777
21778 let _other_editor_1 = workspace
21779 .update_in(cx, |workspace, window, cx| {
21780 workspace.open_path(
21781 (worktree_id, "lib.rs"),
21782 Some(pane_1.downgrade()),
21783 true,
21784 window,
21785 cx,
21786 )
21787 })
21788 .unwrap()
21789 .await
21790 .downcast::<Editor>()
21791 .unwrap();
21792 pane_1
21793 .update_in(cx, |pane, window, cx| {
21794 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
21795 })
21796 .await
21797 .unwrap();
21798 drop(editor_1);
21799 pane_1.update(cx, |pane, cx| {
21800 pane.active_item()
21801 .unwrap()
21802 .downcast::<Editor>()
21803 .unwrap()
21804 .update(cx, |editor, cx| {
21805 assert_eq!(
21806 editor.display_text(cx),
21807 lib_text,
21808 "Other file should be open and active",
21809 );
21810 });
21811 assert_eq!(pane.items().count(), 1, "No other editors should be open");
21812 });
21813
21814 let _other_editor_2 = workspace
21815 .update_in(cx, |workspace, window, cx| {
21816 workspace.open_path(
21817 (worktree_id, "lib.rs"),
21818 Some(pane_2.downgrade()),
21819 true,
21820 window,
21821 cx,
21822 )
21823 })
21824 .unwrap()
21825 .await
21826 .downcast::<Editor>()
21827 .unwrap();
21828 pane_2
21829 .update_in(cx, |pane, window, cx| {
21830 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
21831 })
21832 .await
21833 .unwrap();
21834 drop(editor_2);
21835 pane_2.update(cx, |pane, cx| {
21836 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21837 open_editor.update(cx, |editor, cx| {
21838 assert_eq!(
21839 editor.display_text(cx),
21840 lib_text,
21841 "Other file should be open and active in another panel too",
21842 );
21843 });
21844 assert_eq!(
21845 pane.items().count(),
21846 1,
21847 "No other editors should be open in another pane",
21848 );
21849 });
21850
21851 let _editor_1_reopened = workspace
21852 .update_in(cx, |workspace, window, cx| {
21853 workspace.open_path(
21854 (worktree_id, "main.rs"),
21855 Some(pane_1.downgrade()),
21856 true,
21857 window,
21858 cx,
21859 )
21860 })
21861 .unwrap()
21862 .await
21863 .downcast::<Editor>()
21864 .unwrap();
21865 let _editor_2_reopened = workspace
21866 .update_in(cx, |workspace, window, cx| {
21867 workspace.open_path(
21868 (worktree_id, "main.rs"),
21869 Some(pane_2.downgrade()),
21870 true,
21871 window,
21872 cx,
21873 )
21874 })
21875 .unwrap()
21876 .await
21877 .downcast::<Editor>()
21878 .unwrap();
21879 pane_1.update(cx, |pane, cx| {
21880 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21881 open_editor.update(cx, |editor, cx| {
21882 assert_eq!(
21883 editor.display_text(cx),
21884 main_text,
21885 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
21886 );
21887 assert_eq!(
21888 editor
21889 .selections
21890 .all::<Point>(cx)
21891 .into_iter()
21892 .map(|s| s.range())
21893 .collect::<Vec<_>>(),
21894 expected_ranges,
21895 "Previous editor in the 1st panel had selections and should get them restored on reopen",
21896 );
21897 })
21898 });
21899 pane_2.update(cx, |pane, cx| {
21900 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21901 open_editor.update(cx, |editor, cx| {
21902 assert_eq!(
21903 editor.display_text(cx),
21904 r#"fn main() {
21905⋯rintln!("1");
21906⋯intln!("2");
21907⋯ntln!("3");
21908println!("4");
21909println!("5");
21910}"#,
21911 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
21912 );
21913 assert_eq!(
21914 editor
21915 .selections
21916 .all::<Point>(cx)
21917 .into_iter()
21918 .map(|s| s.range())
21919 .collect::<Vec<_>>(),
21920 vec![Point::zero()..Point::zero()],
21921 "Previous editor in the 2nd pane had no selections changed hence should restore none",
21922 );
21923 })
21924 });
21925}
21926
21927#[gpui::test]
21928async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
21929 init_test(cx, |_| {});
21930
21931 let fs = FakeFs::new(cx.executor());
21932 let main_text = r#"fn main() {
21933println!("1");
21934println!("2");
21935println!("3");
21936println!("4");
21937println!("5");
21938}"#;
21939 let lib_text = "mod foo {}";
21940 fs.insert_tree(
21941 path!("/a"),
21942 json!({
21943 "lib.rs": lib_text,
21944 "main.rs": main_text,
21945 }),
21946 )
21947 .await;
21948
21949 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21950 let (workspace, cx) =
21951 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21952 let worktree_id = workspace.update(cx, |workspace, cx| {
21953 workspace.project().update(cx, |project, cx| {
21954 project.worktrees(cx).next().unwrap().read(cx).id()
21955 })
21956 });
21957
21958 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21959 let editor = workspace
21960 .update_in(cx, |workspace, window, cx| {
21961 workspace.open_path(
21962 (worktree_id, "main.rs"),
21963 Some(pane.downgrade()),
21964 true,
21965 window,
21966 cx,
21967 )
21968 })
21969 .unwrap()
21970 .await
21971 .downcast::<Editor>()
21972 .unwrap();
21973 pane.update(cx, |pane, cx| {
21974 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21975 open_editor.update(cx, |editor, cx| {
21976 assert_eq!(
21977 editor.display_text(cx),
21978 main_text,
21979 "Original main.rs text on initial open",
21980 );
21981 })
21982 });
21983 editor.update_in(cx, |editor, window, cx| {
21984 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
21985 });
21986
21987 cx.update_global(|store: &mut SettingsStore, cx| {
21988 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21989 s.restore_on_file_reopen = Some(false);
21990 });
21991 });
21992 editor.update_in(cx, |editor, window, cx| {
21993 editor.fold_ranges(
21994 vec![
21995 Point::new(1, 0)..Point::new(1, 1),
21996 Point::new(2, 0)..Point::new(2, 2),
21997 Point::new(3, 0)..Point::new(3, 3),
21998 ],
21999 false,
22000 window,
22001 cx,
22002 );
22003 });
22004 pane.update_in(cx, |pane, window, cx| {
22005 pane.close_all_items(&CloseAllItems::default(), window, cx)
22006 })
22007 .await
22008 .unwrap();
22009 pane.update(cx, |pane, _| {
22010 assert!(pane.active_item().is_none());
22011 });
22012 cx.update_global(|store: &mut SettingsStore, cx| {
22013 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
22014 s.restore_on_file_reopen = Some(true);
22015 });
22016 });
22017
22018 let _editor_reopened = workspace
22019 .update_in(cx, |workspace, window, cx| {
22020 workspace.open_path(
22021 (worktree_id, "main.rs"),
22022 Some(pane.downgrade()),
22023 true,
22024 window,
22025 cx,
22026 )
22027 })
22028 .unwrap()
22029 .await
22030 .downcast::<Editor>()
22031 .unwrap();
22032 pane.update(cx, |pane, cx| {
22033 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22034 open_editor.update(cx, |editor, cx| {
22035 assert_eq!(
22036 editor.display_text(cx),
22037 main_text,
22038 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
22039 );
22040 })
22041 });
22042}
22043
22044#[gpui::test]
22045async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
22046 struct EmptyModalView {
22047 focus_handle: gpui::FocusHandle,
22048 }
22049 impl EventEmitter<DismissEvent> for EmptyModalView {}
22050 impl Render for EmptyModalView {
22051 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
22052 div()
22053 }
22054 }
22055 impl Focusable for EmptyModalView {
22056 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
22057 self.focus_handle.clone()
22058 }
22059 }
22060 impl workspace::ModalView for EmptyModalView {}
22061 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
22062 EmptyModalView {
22063 focus_handle: cx.focus_handle(),
22064 }
22065 }
22066
22067 init_test(cx, |_| {});
22068
22069 let fs = FakeFs::new(cx.executor());
22070 let project = Project::test(fs, [], cx).await;
22071 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22072 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
22073 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22074 let editor = cx.new_window_entity(|window, cx| {
22075 Editor::new(
22076 EditorMode::full(),
22077 buffer,
22078 Some(project.clone()),
22079 window,
22080 cx,
22081 )
22082 });
22083 workspace
22084 .update(cx, |workspace, window, cx| {
22085 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
22086 })
22087 .unwrap();
22088 editor.update_in(cx, |editor, window, cx| {
22089 editor.open_context_menu(&OpenContextMenu, window, cx);
22090 assert!(editor.mouse_context_menu.is_some());
22091 });
22092 workspace
22093 .update(cx, |workspace, window, cx| {
22094 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
22095 })
22096 .unwrap();
22097 cx.read(|cx| {
22098 assert!(editor.read(cx).mouse_context_menu.is_none());
22099 });
22100}
22101
22102#[gpui::test]
22103async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
22104 init_test(cx, |_| {});
22105
22106 let fs = FakeFs::new(cx.executor());
22107 fs.insert_file(path!("/file.html"), Default::default())
22108 .await;
22109
22110 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
22111
22112 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22113 let html_language = Arc::new(Language::new(
22114 LanguageConfig {
22115 name: "HTML".into(),
22116 matcher: LanguageMatcher {
22117 path_suffixes: vec!["html".to_string()],
22118 ..LanguageMatcher::default()
22119 },
22120 brackets: BracketPairConfig {
22121 pairs: vec![BracketPair {
22122 start: "<".into(),
22123 end: ">".into(),
22124 close: true,
22125 ..Default::default()
22126 }],
22127 ..Default::default()
22128 },
22129 ..Default::default()
22130 },
22131 Some(tree_sitter_html::LANGUAGE.into()),
22132 ));
22133 language_registry.add(html_language);
22134 let mut fake_servers = language_registry.register_fake_lsp(
22135 "HTML",
22136 FakeLspAdapter {
22137 capabilities: lsp::ServerCapabilities {
22138 completion_provider: Some(lsp::CompletionOptions {
22139 resolve_provider: Some(true),
22140 ..Default::default()
22141 }),
22142 ..Default::default()
22143 },
22144 ..Default::default()
22145 },
22146 );
22147
22148 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22149 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22150
22151 let worktree_id = workspace
22152 .update(cx, |workspace, _window, cx| {
22153 workspace.project().update(cx, |project, cx| {
22154 project.worktrees(cx).next().unwrap().read(cx).id()
22155 })
22156 })
22157 .unwrap();
22158 project
22159 .update(cx, |project, cx| {
22160 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
22161 })
22162 .await
22163 .unwrap();
22164 let editor = workspace
22165 .update(cx, |workspace, window, cx| {
22166 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
22167 })
22168 .unwrap()
22169 .await
22170 .unwrap()
22171 .downcast::<Editor>()
22172 .unwrap();
22173
22174 let fake_server = fake_servers.next().await.unwrap();
22175 editor.update_in(cx, |editor, window, cx| {
22176 editor.set_text("<ad></ad>", window, cx);
22177 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
22178 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
22179 });
22180 let Some((buffer, _)) = editor
22181 .buffer
22182 .read(cx)
22183 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
22184 else {
22185 panic!("Failed to get buffer for selection position");
22186 };
22187 let buffer = buffer.read(cx);
22188 let buffer_id = buffer.remote_id();
22189 let opening_range =
22190 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
22191 let closing_range =
22192 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
22193 let mut linked_ranges = HashMap::default();
22194 linked_ranges.insert(
22195 buffer_id,
22196 vec![(opening_range.clone(), vec![closing_range.clone()])],
22197 );
22198 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
22199 });
22200 let mut completion_handle =
22201 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
22202 Ok(Some(lsp::CompletionResponse::Array(vec![
22203 lsp::CompletionItem {
22204 label: "head".to_string(),
22205 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
22206 lsp::InsertReplaceEdit {
22207 new_text: "head".to_string(),
22208 insert: lsp::Range::new(
22209 lsp::Position::new(0, 1),
22210 lsp::Position::new(0, 3),
22211 ),
22212 replace: lsp::Range::new(
22213 lsp::Position::new(0, 1),
22214 lsp::Position::new(0, 3),
22215 ),
22216 },
22217 )),
22218 ..Default::default()
22219 },
22220 ])))
22221 });
22222 editor.update_in(cx, |editor, window, cx| {
22223 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
22224 });
22225 cx.run_until_parked();
22226 completion_handle.next().await.unwrap();
22227 editor.update(cx, |editor, _| {
22228 assert!(
22229 editor.context_menu_visible(),
22230 "Completion menu should be visible"
22231 );
22232 });
22233 editor.update_in(cx, |editor, window, cx| {
22234 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
22235 });
22236 cx.executor().run_until_parked();
22237 editor.update(cx, |editor, cx| {
22238 assert_eq!(editor.text(cx), "<head></head>");
22239 });
22240}
22241
22242#[gpui::test]
22243async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
22244 init_test(cx, |_| {});
22245
22246 let fs = FakeFs::new(cx.executor());
22247 fs.insert_tree(
22248 path!("/root"),
22249 json!({
22250 "a": {
22251 "main.rs": "fn main() {}",
22252 },
22253 "foo": {
22254 "bar": {
22255 "external_file.rs": "pub mod external {}",
22256 }
22257 }
22258 }),
22259 )
22260 .await;
22261
22262 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
22263 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22264 language_registry.add(rust_lang());
22265 let _fake_servers = language_registry.register_fake_lsp(
22266 "Rust",
22267 FakeLspAdapter {
22268 ..FakeLspAdapter::default()
22269 },
22270 );
22271 let (workspace, cx) =
22272 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22273 let worktree_id = workspace.update(cx, |workspace, cx| {
22274 workspace.project().update(cx, |project, cx| {
22275 project.worktrees(cx).next().unwrap().read(cx).id()
22276 })
22277 });
22278
22279 let assert_language_servers_count =
22280 |expected: usize, context: &str, cx: &mut VisualTestContext| {
22281 project.update(cx, |project, cx| {
22282 let current = project
22283 .lsp_store()
22284 .read(cx)
22285 .as_local()
22286 .unwrap()
22287 .language_servers
22288 .len();
22289 assert_eq!(expected, current, "{context}");
22290 });
22291 };
22292
22293 assert_language_servers_count(
22294 0,
22295 "No servers should be running before any file is open",
22296 cx,
22297 );
22298 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
22299 let main_editor = workspace
22300 .update_in(cx, |workspace, window, cx| {
22301 workspace.open_path(
22302 (worktree_id, "main.rs"),
22303 Some(pane.downgrade()),
22304 true,
22305 window,
22306 cx,
22307 )
22308 })
22309 .unwrap()
22310 .await
22311 .downcast::<Editor>()
22312 .unwrap();
22313 pane.update(cx, |pane, cx| {
22314 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22315 open_editor.update(cx, |editor, cx| {
22316 assert_eq!(
22317 editor.display_text(cx),
22318 "fn main() {}",
22319 "Original main.rs text on initial open",
22320 );
22321 });
22322 assert_eq!(open_editor, main_editor);
22323 });
22324 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
22325
22326 let external_editor = workspace
22327 .update_in(cx, |workspace, window, cx| {
22328 workspace.open_abs_path(
22329 PathBuf::from("/root/foo/bar/external_file.rs"),
22330 OpenOptions::default(),
22331 window,
22332 cx,
22333 )
22334 })
22335 .await
22336 .expect("opening external file")
22337 .downcast::<Editor>()
22338 .expect("downcasted external file's open element to editor");
22339 pane.update(cx, |pane, cx| {
22340 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22341 open_editor.update(cx, |editor, cx| {
22342 assert_eq!(
22343 editor.display_text(cx),
22344 "pub mod external {}",
22345 "External file is open now",
22346 );
22347 });
22348 assert_eq!(open_editor, external_editor);
22349 });
22350 assert_language_servers_count(
22351 1,
22352 "Second, external, *.rs file should join the existing server",
22353 cx,
22354 );
22355
22356 pane.update_in(cx, |pane, window, cx| {
22357 pane.close_active_item(&CloseActiveItem::default(), window, cx)
22358 })
22359 .await
22360 .unwrap();
22361 pane.update_in(cx, |pane, window, cx| {
22362 pane.navigate_backward(window, cx);
22363 });
22364 cx.run_until_parked();
22365 pane.update(cx, |pane, cx| {
22366 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22367 open_editor.update(cx, |editor, cx| {
22368 assert_eq!(
22369 editor.display_text(cx),
22370 "pub mod external {}",
22371 "External file is open now",
22372 );
22373 });
22374 });
22375 assert_language_servers_count(
22376 1,
22377 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
22378 cx,
22379 );
22380
22381 cx.update(|_, cx| {
22382 workspace::reload(&workspace::Reload::default(), cx);
22383 });
22384 assert_language_servers_count(
22385 1,
22386 "After reloading the worktree with local and external files opened, only one project should be started",
22387 cx,
22388 );
22389}
22390
22391#[gpui::test]
22392async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
22393 init_test(cx, |_| {});
22394
22395 let mut cx = EditorTestContext::new(cx).await;
22396 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22397 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22398
22399 // test cursor move to start of each line on tab
22400 // for `if`, `elif`, `else`, `while`, `with` and `for`
22401 cx.set_state(indoc! {"
22402 def main():
22403 ˇ for item in items:
22404 ˇ while item.active:
22405 ˇ if item.value > 10:
22406 ˇ continue
22407 ˇ elif item.value < 0:
22408 ˇ break
22409 ˇ else:
22410 ˇ with item.context() as ctx:
22411 ˇ yield count
22412 ˇ else:
22413 ˇ log('while else')
22414 ˇ else:
22415 ˇ log('for else')
22416 "});
22417 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22418 cx.assert_editor_state(indoc! {"
22419 def main():
22420 ˇfor item in items:
22421 ˇwhile item.active:
22422 ˇif item.value > 10:
22423 ˇcontinue
22424 ˇelif item.value < 0:
22425 ˇbreak
22426 ˇelse:
22427 ˇwith item.context() as ctx:
22428 ˇyield count
22429 ˇelse:
22430 ˇlog('while else')
22431 ˇelse:
22432 ˇlog('for else')
22433 "});
22434 // test relative indent is preserved when tab
22435 // for `if`, `elif`, `else`, `while`, `with` and `for`
22436 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22437 cx.assert_editor_state(indoc! {"
22438 def main():
22439 ˇfor item in items:
22440 ˇwhile item.active:
22441 ˇif item.value > 10:
22442 ˇcontinue
22443 ˇelif item.value < 0:
22444 ˇbreak
22445 ˇelse:
22446 ˇwith item.context() as ctx:
22447 ˇyield count
22448 ˇelse:
22449 ˇlog('while else')
22450 ˇelse:
22451 ˇlog('for else')
22452 "});
22453
22454 // test cursor move to start of each line on tab
22455 // for `try`, `except`, `else`, `finally`, `match` and `def`
22456 cx.set_state(indoc! {"
22457 def main():
22458 ˇ try:
22459 ˇ fetch()
22460 ˇ except ValueError:
22461 ˇ handle_error()
22462 ˇ else:
22463 ˇ match value:
22464 ˇ case _:
22465 ˇ finally:
22466 ˇ def status():
22467 ˇ return 0
22468 "});
22469 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22470 cx.assert_editor_state(indoc! {"
22471 def main():
22472 ˇtry:
22473 ˇfetch()
22474 ˇexcept ValueError:
22475 ˇhandle_error()
22476 ˇelse:
22477 ˇmatch value:
22478 ˇcase _:
22479 ˇfinally:
22480 ˇdef status():
22481 ˇreturn 0
22482 "});
22483 // test relative indent is preserved when tab
22484 // for `try`, `except`, `else`, `finally`, `match` and `def`
22485 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22486 cx.assert_editor_state(indoc! {"
22487 def main():
22488 ˇtry:
22489 ˇfetch()
22490 ˇexcept ValueError:
22491 ˇhandle_error()
22492 ˇelse:
22493 ˇmatch value:
22494 ˇcase _:
22495 ˇfinally:
22496 ˇdef status():
22497 ˇreturn 0
22498 "});
22499}
22500
22501#[gpui::test]
22502async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
22503 init_test(cx, |_| {});
22504
22505 let mut cx = EditorTestContext::new(cx).await;
22506 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22507 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22508
22509 // test `else` auto outdents when typed inside `if` block
22510 cx.set_state(indoc! {"
22511 def main():
22512 if i == 2:
22513 return
22514 ˇ
22515 "});
22516 cx.update_editor(|editor, window, cx| {
22517 editor.handle_input("else:", window, cx);
22518 });
22519 cx.assert_editor_state(indoc! {"
22520 def main():
22521 if i == 2:
22522 return
22523 else:ˇ
22524 "});
22525
22526 // test `except` auto outdents when typed inside `try` block
22527 cx.set_state(indoc! {"
22528 def main():
22529 try:
22530 i = 2
22531 ˇ
22532 "});
22533 cx.update_editor(|editor, window, cx| {
22534 editor.handle_input("except:", window, cx);
22535 });
22536 cx.assert_editor_state(indoc! {"
22537 def main():
22538 try:
22539 i = 2
22540 except:ˇ
22541 "});
22542
22543 // test `else` auto outdents when typed inside `except` block
22544 cx.set_state(indoc! {"
22545 def main():
22546 try:
22547 i = 2
22548 except:
22549 j = 2
22550 ˇ
22551 "});
22552 cx.update_editor(|editor, window, cx| {
22553 editor.handle_input("else:", window, cx);
22554 });
22555 cx.assert_editor_state(indoc! {"
22556 def main():
22557 try:
22558 i = 2
22559 except:
22560 j = 2
22561 else:ˇ
22562 "});
22563
22564 // test `finally` auto outdents when typed inside `else` block
22565 cx.set_state(indoc! {"
22566 def main():
22567 try:
22568 i = 2
22569 except:
22570 j = 2
22571 else:
22572 k = 2
22573 ˇ
22574 "});
22575 cx.update_editor(|editor, window, cx| {
22576 editor.handle_input("finally:", window, cx);
22577 });
22578 cx.assert_editor_state(indoc! {"
22579 def main():
22580 try:
22581 i = 2
22582 except:
22583 j = 2
22584 else:
22585 k = 2
22586 finally:ˇ
22587 "});
22588
22589 // test `else` does not outdents when typed inside `except` block right after for block
22590 cx.set_state(indoc! {"
22591 def main():
22592 try:
22593 i = 2
22594 except:
22595 for i in range(n):
22596 pass
22597 ˇ
22598 "});
22599 cx.update_editor(|editor, window, cx| {
22600 editor.handle_input("else:", window, cx);
22601 });
22602 cx.assert_editor_state(indoc! {"
22603 def main():
22604 try:
22605 i = 2
22606 except:
22607 for i in range(n):
22608 pass
22609 else:ˇ
22610 "});
22611
22612 // test `finally` auto outdents when typed inside `else` block right after for block
22613 cx.set_state(indoc! {"
22614 def main():
22615 try:
22616 i = 2
22617 except:
22618 j = 2
22619 else:
22620 for i in range(n):
22621 pass
22622 ˇ
22623 "});
22624 cx.update_editor(|editor, window, cx| {
22625 editor.handle_input("finally:", window, cx);
22626 });
22627 cx.assert_editor_state(indoc! {"
22628 def main():
22629 try:
22630 i = 2
22631 except:
22632 j = 2
22633 else:
22634 for i in range(n):
22635 pass
22636 finally:ˇ
22637 "});
22638
22639 // test `except` outdents to inner "try" block
22640 cx.set_state(indoc! {"
22641 def main():
22642 try:
22643 i = 2
22644 if i == 2:
22645 try:
22646 i = 3
22647 ˇ
22648 "});
22649 cx.update_editor(|editor, window, cx| {
22650 editor.handle_input("except:", window, cx);
22651 });
22652 cx.assert_editor_state(indoc! {"
22653 def main():
22654 try:
22655 i = 2
22656 if i == 2:
22657 try:
22658 i = 3
22659 except:ˇ
22660 "});
22661
22662 // test `except` outdents to outer "try" block
22663 cx.set_state(indoc! {"
22664 def main():
22665 try:
22666 i = 2
22667 if i == 2:
22668 try:
22669 i = 3
22670 ˇ
22671 "});
22672 cx.update_editor(|editor, window, cx| {
22673 editor.handle_input("except:", window, cx);
22674 });
22675 cx.assert_editor_state(indoc! {"
22676 def main():
22677 try:
22678 i = 2
22679 if i == 2:
22680 try:
22681 i = 3
22682 except:ˇ
22683 "});
22684
22685 // test `else` stays at correct indent when typed after `for` block
22686 cx.set_state(indoc! {"
22687 def main():
22688 for i in range(10):
22689 if i == 3:
22690 break
22691 ˇ
22692 "});
22693 cx.update_editor(|editor, window, cx| {
22694 editor.handle_input("else:", window, cx);
22695 });
22696 cx.assert_editor_state(indoc! {"
22697 def main():
22698 for i in range(10):
22699 if i == 3:
22700 break
22701 else:ˇ
22702 "});
22703
22704 // test does not outdent on typing after line with square brackets
22705 cx.set_state(indoc! {"
22706 def f() -> list[str]:
22707 ˇ
22708 "});
22709 cx.update_editor(|editor, window, cx| {
22710 editor.handle_input("a", window, cx);
22711 });
22712 cx.assert_editor_state(indoc! {"
22713 def f() -> list[str]:
22714 aˇ
22715 "});
22716
22717 // test does not outdent on typing : after case keyword
22718 cx.set_state(indoc! {"
22719 match 1:
22720 caseˇ
22721 "});
22722 cx.update_editor(|editor, window, cx| {
22723 editor.handle_input(":", window, cx);
22724 });
22725 cx.assert_editor_state(indoc! {"
22726 match 1:
22727 case:ˇ
22728 "});
22729}
22730
22731#[gpui::test]
22732async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
22733 init_test(cx, |_| {});
22734 update_test_language_settings(cx, |settings| {
22735 settings.defaults.extend_comment_on_newline = Some(false);
22736 });
22737 let mut cx = EditorTestContext::new(cx).await;
22738 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22739 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22740
22741 // test correct indent after newline on comment
22742 cx.set_state(indoc! {"
22743 # COMMENT:ˇ
22744 "});
22745 cx.update_editor(|editor, window, cx| {
22746 editor.newline(&Newline, window, cx);
22747 });
22748 cx.assert_editor_state(indoc! {"
22749 # COMMENT:
22750 ˇ
22751 "});
22752
22753 // test correct indent after newline in brackets
22754 cx.set_state(indoc! {"
22755 {ˇ}
22756 "});
22757 cx.update_editor(|editor, window, cx| {
22758 editor.newline(&Newline, window, cx);
22759 });
22760 cx.run_until_parked();
22761 cx.assert_editor_state(indoc! {"
22762 {
22763 ˇ
22764 }
22765 "});
22766
22767 cx.set_state(indoc! {"
22768 (ˇ)
22769 "});
22770 cx.update_editor(|editor, window, cx| {
22771 editor.newline(&Newline, window, cx);
22772 });
22773 cx.run_until_parked();
22774 cx.assert_editor_state(indoc! {"
22775 (
22776 ˇ
22777 )
22778 "});
22779
22780 // do not indent after empty lists or dictionaries
22781 cx.set_state(indoc! {"
22782 a = []ˇ
22783 "});
22784 cx.update_editor(|editor, window, cx| {
22785 editor.newline(&Newline, window, cx);
22786 });
22787 cx.run_until_parked();
22788 cx.assert_editor_state(indoc! {"
22789 a = []
22790 ˇ
22791 "});
22792}
22793
22794fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
22795 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
22796 point..point
22797}
22798
22799#[track_caller]
22800fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
22801 let (text, ranges) = marked_text_ranges(marked_text, true);
22802 assert_eq!(editor.text(cx), text);
22803 assert_eq!(
22804 editor.selections.ranges(cx),
22805 ranges,
22806 "Assert selections are {}",
22807 marked_text
22808 );
22809}
22810
22811pub fn handle_signature_help_request(
22812 cx: &mut EditorLspTestContext,
22813 mocked_response: lsp::SignatureHelp,
22814) -> impl Future<Output = ()> + use<> {
22815 let mut request =
22816 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
22817 let mocked_response = mocked_response.clone();
22818 async move { Ok(Some(mocked_response)) }
22819 });
22820
22821 async move {
22822 request.next().await;
22823 }
22824}
22825
22826#[track_caller]
22827pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
22828 cx.update_editor(|editor, _, _| {
22829 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
22830 let entries = menu.entries.borrow();
22831 let entries = entries
22832 .iter()
22833 .map(|entry| entry.string.as_str())
22834 .collect::<Vec<_>>();
22835 assert_eq!(entries, expected);
22836 } else {
22837 panic!("Expected completions menu");
22838 }
22839 });
22840}
22841
22842/// Handle completion request passing a marked string specifying where the completion
22843/// should be triggered from using '|' character, what range should be replaced, and what completions
22844/// should be returned using '<' and '>' to delimit the range.
22845///
22846/// Also see `handle_completion_request_with_insert_and_replace`.
22847#[track_caller]
22848pub fn handle_completion_request(
22849 marked_string: &str,
22850 completions: Vec<&'static str>,
22851 is_incomplete: bool,
22852 counter: Arc<AtomicUsize>,
22853 cx: &mut EditorLspTestContext,
22854) -> impl Future<Output = ()> {
22855 let complete_from_marker: TextRangeMarker = '|'.into();
22856 let replace_range_marker: TextRangeMarker = ('<', '>').into();
22857 let (_, mut marked_ranges) = marked_text_ranges_by(
22858 marked_string,
22859 vec![complete_from_marker.clone(), replace_range_marker.clone()],
22860 );
22861
22862 let complete_from_position =
22863 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
22864 let replace_range =
22865 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
22866
22867 let mut request =
22868 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
22869 let completions = completions.clone();
22870 counter.fetch_add(1, atomic::Ordering::Release);
22871 async move {
22872 assert_eq!(params.text_document_position.text_document.uri, url.clone());
22873 assert_eq!(
22874 params.text_document_position.position,
22875 complete_from_position
22876 );
22877 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
22878 is_incomplete: is_incomplete,
22879 item_defaults: None,
22880 items: completions
22881 .iter()
22882 .map(|completion_text| lsp::CompletionItem {
22883 label: completion_text.to_string(),
22884 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
22885 range: replace_range,
22886 new_text: completion_text.to_string(),
22887 })),
22888 ..Default::default()
22889 })
22890 .collect(),
22891 })))
22892 }
22893 });
22894
22895 async move {
22896 request.next().await;
22897 }
22898}
22899
22900/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
22901/// given instead, which also contains an `insert` range.
22902///
22903/// This function uses markers to define ranges:
22904/// - `|` marks the cursor position
22905/// - `<>` marks the replace range
22906/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
22907pub fn handle_completion_request_with_insert_and_replace(
22908 cx: &mut EditorLspTestContext,
22909 marked_string: &str,
22910 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
22911 counter: Arc<AtomicUsize>,
22912) -> impl Future<Output = ()> {
22913 let complete_from_marker: TextRangeMarker = '|'.into();
22914 let replace_range_marker: TextRangeMarker = ('<', '>').into();
22915 let insert_range_marker: TextRangeMarker = ('{', '}').into();
22916
22917 let (_, mut marked_ranges) = marked_text_ranges_by(
22918 marked_string,
22919 vec![
22920 complete_from_marker.clone(),
22921 replace_range_marker.clone(),
22922 insert_range_marker.clone(),
22923 ],
22924 );
22925
22926 let complete_from_position =
22927 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
22928 let replace_range =
22929 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
22930
22931 let insert_range = match marked_ranges.remove(&insert_range_marker) {
22932 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
22933 _ => lsp::Range {
22934 start: replace_range.start,
22935 end: complete_from_position,
22936 },
22937 };
22938
22939 let mut request =
22940 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
22941 let completions = completions.clone();
22942 counter.fetch_add(1, atomic::Ordering::Release);
22943 async move {
22944 assert_eq!(params.text_document_position.text_document.uri, url.clone());
22945 assert_eq!(
22946 params.text_document_position.position, complete_from_position,
22947 "marker `|` position doesn't match",
22948 );
22949 Ok(Some(lsp::CompletionResponse::Array(
22950 completions
22951 .iter()
22952 .map(|(label, new_text)| lsp::CompletionItem {
22953 label: label.to_string(),
22954 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
22955 lsp::InsertReplaceEdit {
22956 insert: insert_range,
22957 replace: replace_range,
22958 new_text: new_text.to_string(),
22959 },
22960 )),
22961 ..Default::default()
22962 })
22963 .collect(),
22964 )))
22965 }
22966 });
22967
22968 async move {
22969 request.next().await;
22970 }
22971}
22972
22973fn handle_resolve_completion_request(
22974 cx: &mut EditorLspTestContext,
22975 edits: Option<Vec<(&'static str, &'static str)>>,
22976) -> impl Future<Output = ()> {
22977 let edits = edits.map(|edits| {
22978 edits
22979 .iter()
22980 .map(|(marked_string, new_text)| {
22981 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
22982 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
22983 lsp::TextEdit::new(replace_range, new_text.to_string())
22984 })
22985 .collect::<Vec<_>>()
22986 });
22987
22988 let mut request =
22989 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
22990 let edits = edits.clone();
22991 async move {
22992 Ok(lsp::CompletionItem {
22993 additional_text_edits: edits,
22994 ..Default::default()
22995 })
22996 }
22997 });
22998
22999 async move {
23000 request.next().await;
23001 }
23002}
23003
23004pub(crate) fn update_test_language_settings(
23005 cx: &mut TestAppContext,
23006 f: impl Fn(&mut AllLanguageSettingsContent),
23007) {
23008 cx.update(|cx| {
23009 SettingsStore::update_global(cx, |store, cx| {
23010 store.update_user_settings::<AllLanguageSettings>(cx, f);
23011 });
23012 });
23013}
23014
23015pub(crate) fn update_test_project_settings(
23016 cx: &mut TestAppContext,
23017 f: impl Fn(&mut ProjectSettings),
23018) {
23019 cx.update(|cx| {
23020 SettingsStore::update_global(cx, |store, cx| {
23021 store.update_user_settings::<ProjectSettings>(cx, f);
23022 });
23023 });
23024}
23025
23026pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
23027 cx.update(|cx| {
23028 assets::Assets.load_test_fonts(cx);
23029 let store = SettingsStore::test(cx);
23030 cx.set_global(store);
23031 theme::init(theme::LoadThemes::JustBase, cx);
23032 release_channel::init(SemanticVersion::default(), cx);
23033 client::init_settings(cx);
23034 language::init(cx);
23035 Project::init_settings(cx);
23036 workspace::init_settings(cx);
23037 crate::init(cx);
23038 });
23039 zlog::init_test();
23040 update_test_language_settings(cx, f);
23041}
23042
23043#[track_caller]
23044fn assert_hunk_revert(
23045 not_reverted_text_with_selections: &str,
23046 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
23047 expected_reverted_text_with_selections: &str,
23048 base_text: &str,
23049 cx: &mut EditorLspTestContext,
23050) {
23051 cx.set_state(not_reverted_text_with_selections);
23052 cx.set_head_text(base_text);
23053 cx.executor().run_until_parked();
23054
23055 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
23056 let snapshot = editor.snapshot(window, cx);
23057 let reverted_hunk_statuses = snapshot
23058 .buffer_snapshot
23059 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
23060 .map(|hunk| hunk.status().kind)
23061 .collect::<Vec<_>>();
23062
23063 editor.git_restore(&Default::default(), window, cx);
23064 reverted_hunk_statuses
23065 });
23066 cx.executor().run_until_parked();
23067 cx.assert_editor_state(expected_reverted_text_with_selections);
23068 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
23069}
23070
23071#[gpui::test(iterations = 10)]
23072async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
23073 init_test(cx, |_| {});
23074
23075 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
23076 let counter = diagnostic_requests.clone();
23077
23078 let fs = FakeFs::new(cx.executor());
23079 fs.insert_tree(
23080 path!("/a"),
23081 json!({
23082 "first.rs": "fn main() { let a = 5; }",
23083 "second.rs": "// Test file",
23084 }),
23085 )
23086 .await;
23087
23088 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23089 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23090 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23091
23092 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23093 language_registry.add(rust_lang());
23094 let mut fake_servers = language_registry.register_fake_lsp(
23095 "Rust",
23096 FakeLspAdapter {
23097 capabilities: lsp::ServerCapabilities {
23098 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
23099 lsp::DiagnosticOptions {
23100 identifier: None,
23101 inter_file_dependencies: true,
23102 workspace_diagnostics: true,
23103 work_done_progress_options: Default::default(),
23104 },
23105 )),
23106 ..Default::default()
23107 },
23108 ..Default::default()
23109 },
23110 );
23111
23112 let editor = workspace
23113 .update(cx, |workspace, window, cx| {
23114 workspace.open_abs_path(
23115 PathBuf::from(path!("/a/first.rs")),
23116 OpenOptions::default(),
23117 window,
23118 cx,
23119 )
23120 })
23121 .unwrap()
23122 .await
23123 .unwrap()
23124 .downcast::<Editor>()
23125 .unwrap();
23126 let fake_server = fake_servers.next().await.unwrap();
23127 let server_id = fake_server.server.server_id();
23128 let mut first_request = fake_server
23129 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
23130 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
23131 let result_id = Some(new_result_id.to_string());
23132 assert_eq!(
23133 params.text_document.uri,
23134 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
23135 );
23136 async move {
23137 Ok(lsp::DocumentDiagnosticReportResult::Report(
23138 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
23139 related_documents: None,
23140 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
23141 items: Vec::new(),
23142 result_id,
23143 },
23144 }),
23145 ))
23146 }
23147 });
23148
23149 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
23150 project.update(cx, |project, cx| {
23151 let buffer_id = editor
23152 .read(cx)
23153 .buffer()
23154 .read(cx)
23155 .as_singleton()
23156 .expect("created a singleton buffer")
23157 .read(cx)
23158 .remote_id();
23159 let buffer_result_id = project
23160 .lsp_store()
23161 .read(cx)
23162 .result_id(server_id, buffer_id, cx);
23163 assert_eq!(expected, buffer_result_id);
23164 });
23165 };
23166
23167 ensure_result_id(None, cx);
23168 cx.executor().advance_clock(Duration::from_millis(60));
23169 cx.executor().run_until_parked();
23170 assert_eq!(
23171 diagnostic_requests.load(atomic::Ordering::Acquire),
23172 1,
23173 "Opening file should trigger diagnostic request"
23174 );
23175 first_request
23176 .next()
23177 .await
23178 .expect("should have sent the first diagnostics pull request");
23179 ensure_result_id(Some("1".to_string()), cx);
23180
23181 // Editing should trigger diagnostics
23182 editor.update_in(cx, |editor, window, cx| {
23183 editor.handle_input("2", window, cx)
23184 });
23185 cx.executor().advance_clock(Duration::from_millis(60));
23186 cx.executor().run_until_parked();
23187 assert_eq!(
23188 diagnostic_requests.load(atomic::Ordering::Acquire),
23189 2,
23190 "Editing should trigger diagnostic request"
23191 );
23192 ensure_result_id(Some("2".to_string()), cx);
23193
23194 // Moving cursor should not trigger diagnostic request
23195 editor.update_in(cx, |editor, window, cx| {
23196 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23197 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
23198 });
23199 });
23200 cx.executor().advance_clock(Duration::from_millis(60));
23201 cx.executor().run_until_parked();
23202 assert_eq!(
23203 diagnostic_requests.load(atomic::Ordering::Acquire),
23204 2,
23205 "Cursor movement should not trigger diagnostic request"
23206 );
23207 ensure_result_id(Some("2".to_string()), cx);
23208 // Multiple rapid edits should be debounced
23209 for _ in 0..5 {
23210 editor.update_in(cx, |editor, window, cx| {
23211 editor.handle_input("x", window, cx)
23212 });
23213 }
23214 cx.executor().advance_clock(Duration::from_millis(60));
23215 cx.executor().run_until_parked();
23216
23217 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
23218 assert!(
23219 final_requests <= 4,
23220 "Multiple rapid edits should be debounced (got {final_requests} requests)",
23221 );
23222 ensure_result_id(Some(final_requests.to_string()), cx);
23223}
23224
23225#[gpui::test]
23226async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
23227 // Regression test for issue #11671
23228 // Previously, adding a cursor after moving multiple cursors would reset
23229 // the cursor count instead of adding to the existing cursors.
23230 init_test(cx, |_| {});
23231 let mut cx = EditorTestContext::new(cx).await;
23232
23233 // Create a simple buffer with cursor at start
23234 cx.set_state(indoc! {"
23235 ˇaaaa
23236 bbbb
23237 cccc
23238 dddd
23239 eeee
23240 ffff
23241 gggg
23242 hhhh"});
23243
23244 // Add 2 cursors below (so we have 3 total)
23245 cx.update_editor(|editor, window, cx| {
23246 editor.add_selection_below(&Default::default(), window, cx);
23247 editor.add_selection_below(&Default::default(), window, cx);
23248 });
23249
23250 // Verify we have 3 cursors
23251 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
23252 assert_eq!(
23253 initial_count, 3,
23254 "Should have 3 cursors after adding 2 below"
23255 );
23256
23257 // Move down one line
23258 cx.update_editor(|editor, window, cx| {
23259 editor.move_down(&MoveDown, window, cx);
23260 });
23261
23262 // Add another cursor below
23263 cx.update_editor(|editor, window, cx| {
23264 editor.add_selection_below(&Default::default(), window, cx);
23265 });
23266
23267 // Should now have 4 cursors (3 original + 1 new)
23268 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
23269 assert_eq!(
23270 final_count, 4,
23271 "Should have 4 cursors after moving and adding another"
23272 );
23273}
23274
23275#[gpui::test(iterations = 10)]
23276async fn test_document_colors(cx: &mut TestAppContext) {
23277 let expected_color = Rgba {
23278 r: 0.33,
23279 g: 0.33,
23280 b: 0.33,
23281 a: 0.33,
23282 };
23283
23284 init_test(cx, |_| {});
23285
23286 let fs = FakeFs::new(cx.executor());
23287 fs.insert_tree(
23288 path!("/a"),
23289 json!({
23290 "first.rs": "fn main() { let a = 5; }",
23291 }),
23292 )
23293 .await;
23294
23295 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23296 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23297 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23298
23299 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23300 language_registry.add(rust_lang());
23301 let mut fake_servers = language_registry.register_fake_lsp(
23302 "Rust",
23303 FakeLspAdapter {
23304 capabilities: lsp::ServerCapabilities {
23305 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
23306 ..lsp::ServerCapabilities::default()
23307 },
23308 name: "rust-analyzer",
23309 ..FakeLspAdapter::default()
23310 },
23311 );
23312 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
23313 "Rust",
23314 FakeLspAdapter {
23315 capabilities: lsp::ServerCapabilities {
23316 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
23317 ..lsp::ServerCapabilities::default()
23318 },
23319 name: "not-rust-analyzer",
23320 ..FakeLspAdapter::default()
23321 },
23322 );
23323
23324 let editor = workspace
23325 .update(cx, |workspace, window, cx| {
23326 workspace.open_abs_path(
23327 PathBuf::from(path!("/a/first.rs")),
23328 OpenOptions::default(),
23329 window,
23330 cx,
23331 )
23332 })
23333 .unwrap()
23334 .await
23335 .unwrap()
23336 .downcast::<Editor>()
23337 .unwrap();
23338 let fake_language_server = fake_servers.next().await.unwrap();
23339 let fake_language_server_without_capabilities =
23340 fake_servers_without_capabilities.next().await.unwrap();
23341 let requests_made = Arc::new(AtomicUsize::new(0));
23342 let closure_requests_made = Arc::clone(&requests_made);
23343 let mut color_request_handle = fake_language_server
23344 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
23345 let requests_made = Arc::clone(&closure_requests_made);
23346 async move {
23347 assert_eq!(
23348 params.text_document.uri,
23349 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
23350 );
23351 requests_made.fetch_add(1, atomic::Ordering::Release);
23352 Ok(vec![
23353 lsp::ColorInformation {
23354 range: lsp::Range {
23355 start: lsp::Position {
23356 line: 0,
23357 character: 0,
23358 },
23359 end: lsp::Position {
23360 line: 0,
23361 character: 1,
23362 },
23363 },
23364 color: lsp::Color {
23365 red: 0.33,
23366 green: 0.33,
23367 blue: 0.33,
23368 alpha: 0.33,
23369 },
23370 },
23371 lsp::ColorInformation {
23372 range: lsp::Range {
23373 start: lsp::Position {
23374 line: 0,
23375 character: 0,
23376 },
23377 end: lsp::Position {
23378 line: 0,
23379 character: 1,
23380 },
23381 },
23382 color: lsp::Color {
23383 red: 0.33,
23384 green: 0.33,
23385 blue: 0.33,
23386 alpha: 0.33,
23387 },
23388 },
23389 ])
23390 }
23391 });
23392
23393 let _handle = fake_language_server_without_capabilities
23394 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
23395 panic!("Should not be called");
23396 });
23397 cx.executor().advance_clock(Duration::from_millis(100));
23398 color_request_handle.next().await.unwrap();
23399 cx.run_until_parked();
23400 assert_eq!(
23401 1,
23402 requests_made.load(atomic::Ordering::Acquire),
23403 "Should query for colors once per editor open"
23404 );
23405 editor.update_in(cx, |editor, _, cx| {
23406 assert_eq!(
23407 vec![expected_color],
23408 extract_color_inlays(editor, cx),
23409 "Should have an initial inlay"
23410 );
23411 });
23412
23413 // opening another file in a split should not influence the LSP query counter
23414 workspace
23415 .update(cx, |workspace, window, cx| {
23416 assert_eq!(
23417 workspace.panes().len(),
23418 1,
23419 "Should have one pane with one editor"
23420 );
23421 workspace.move_item_to_pane_in_direction(
23422 &MoveItemToPaneInDirection {
23423 direction: SplitDirection::Right,
23424 focus: false,
23425 clone: true,
23426 },
23427 window,
23428 cx,
23429 );
23430 })
23431 .unwrap();
23432 cx.run_until_parked();
23433 workspace
23434 .update(cx, |workspace, _, cx| {
23435 let panes = workspace.panes();
23436 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
23437 for pane in panes {
23438 let editor = pane
23439 .read(cx)
23440 .active_item()
23441 .and_then(|item| item.downcast::<Editor>())
23442 .expect("Should have opened an editor in each split");
23443 let editor_file = editor
23444 .read(cx)
23445 .buffer()
23446 .read(cx)
23447 .as_singleton()
23448 .expect("test deals with singleton buffers")
23449 .read(cx)
23450 .file()
23451 .expect("test buffese should have a file")
23452 .path();
23453 assert_eq!(
23454 editor_file.as_ref(),
23455 Path::new("first.rs"),
23456 "Both editors should be opened for the same file"
23457 )
23458 }
23459 })
23460 .unwrap();
23461
23462 cx.executor().advance_clock(Duration::from_millis(500));
23463 let save = editor.update_in(cx, |editor, window, cx| {
23464 editor.move_to_end(&MoveToEnd, window, cx);
23465 editor.handle_input("dirty", window, cx);
23466 editor.save(
23467 SaveOptions {
23468 format: true,
23469 autosave: true,
23470 },
23471 project.clone(),
23472 window,
23473 cx,
23474 )
23475 });
23476 save.await.unwrap();
23477
23478 color_request_handle.next().await.unwrap();
23479 cx.run_until_parked();
23480 assert_eq!(
23481 3,
23482 requests_made.load(atomic::Ordering::Acquire),
23483 "Should query for colors once per save and once per formatting after save"
23484 );
23485
23486 drop(editor);
23487 let close = workspace
23488 .update(cx, |workspace, window, cx| {
23489 workspace.active_pane().update(cx, |pane, cx| {
23490 pane.close_active_item(&CloseActiveItem::default(), window, cx)
23491 })
23492 })
23493 .unwrap();
23494 close.await.unwrap();
23495 let close = workspace
23496 .update(cx, |workspace, window, cx| {
23497 workspace.active_pane().update(cx, |pane, cx| {
23498 pane.close_active_item(&CloseActiveItem::default(), window, cx)
23499 })
23500 })
23501 .unwrap();
23502 close.await.unwrap();
23503 assert_eq!(
23504 3,
23505 requests_made.load(atomic::Ordering::Acquire),
23506 "After saving and closing all editors, no extra requests should be made"
23507 );
23508 workspace
23509 .update(cx, |workspace, _, cx| {
23510 assert!(
23511 workspace.active_item(cx).is_none(),
23512 "Should close all editors"
23513 )
23514 })
23515 .unwrap();
23516
23517 workspace
23518 .update(cx, |workspace, window, cx| {
23519 workspace.active_pane().update(cx, |pane, cx| {
23520 pane.navigate_backward(window, cx);
23521 })
23522 })
23523 .unwrap();
23524 cx.executor().advance_clock(Duration::from_millis(100));
23525 cx.run_until_parked();
23526 let editor = workspace
23527 .update(cx, |workspace, _, cx| {
23528 workspace
23529 .active_item(cx)
23530 .expect("Should have reopened the editor again after navigating back")
23531 .downcast::<Editor>()
23532 .expect("Should be an editor")
23533 })
23534 .unwrap();
23535 color_request_handle.next().await.unwrap();
23536 assert_eq!(
23537 3,
23538 requests_made.load(atomic::Ordering::Acquire),
23539 "Cache should be reused on buffer close and reopen"
23540 );
23541 editor.update(cx, |editor, cx| {
23542 assert_eq!(
23543 vec![expected_color],
23544 extract_color_inlays(editor, cx),
23545 "Should have an initial inlay"
23546 );
23547 });
23548}
23549
23550#[gpui::test]
23551async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
23552 init_test(cx, |_| {});
23553 let (editor, cx) = cx.add_window_view(Editor::single_line);
23554 editor.update_in(cx, |editor, window, cx| {
23555 editor.set_text("oops\n\nwow\n", window, cx)
23556 });
23557 cx.run_until_parked();
23558 editor.update(cx, |editor, cx| {
23559 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
23560 });
23561 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
23562 cx.run_until_parked();
23563 editor.update(cx, |editor, cx| {
23564 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
23565 });
23566}
23567
23568#[track_caller]
23569fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
23570 editor
23571 .all_inlays(cx)
23572 .into_iter()
23573 .filter_map(|inlay| inlay.get_color())
23574 .map(Rgba::from)
23575 .collect()
23576}