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, CloseInactiveItems, 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: Some(language::DocumentationConfig {
2879 start: "/**".into(),
2880 end: "*/".into(),
2881 prefix: "* ".into(),
2882 tab_size: NonZeroU32::new(1).unwrap(),
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(("--[[".into(), "]]".into())),
3093 ..LanguageConfig::default()
3094 },
3095 None,
3096 ));
3097
3098 let mut cx = EditorTestContext::new(cx).await;
3099 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3100
3101 // Line with line comment should extend
3102 cx.set_state(indoc! {"
3103 --ˇ
3104 "});
3105 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3106 cx.assert_editor_state(indoc! {"
3107 --
3108 --ˇ
3109 "});
3110
3111 // Line with block comment that matches line comment should not extend
3112 cx.set_state(indoc! {"
3113 --[[ˇ
3114 "});
3115 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3116 cx.assert_editor_state(indoc! {"
3117 --[[
3118 ˇ
3119 "});
3120}
3121
3122#[gpui::test]
3123fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3124 init_test(cx, |_| {});
3125
3126 let editor = cx.add_window(|window, cx| {
3127 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3128 let mut editor = build_editor(buffer.clone(), window, cx);
3129 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3130 s.select_ranges([3..4, 11..12, 19..20])
3131 });
3132 editor
3133 });
3134
3135 _ = editor.update(cx, |editor, window, cx| {
3136 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3137 editor.buffer.update(cx, |buffer, cx| {
3138 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3139 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3140 });
3141 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3142
3143 editor.insert("Z", window, cx);
3144 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3145
3146 // The selections are moved after the inserted characters
3147 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3148 });
3149}
3150
3151#[gpui::test]
3152async fn test_tab(cx: &mut TestAppContext) {
3153 init_test(cx, |settings| {
3154 settings.defaults.tab_size = NonZeroU32::new(3)
3155 });
3156
3157 let mut cx = EditorTestContext::new(cx).await;
3158 cx.set_state(indoc! {"
3159 ˇabˇc
3160 ˇ🏀ˇ🏀ˇefg
3161 dˇ
3162 "});
3163 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3164 cx.assert_editor_state(indoc! {"
3165 ˇab ˇc
3166 ˇ🏀 ˇ🏀 ˇefg
3167 d ˇ
3168 "});
3169
3170 cx.set_state(indoc! {"
3171 a
3172 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3173 "});
3174 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3175 cx.assert_editor_state(indoc! {"
3176 a
3177 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3178 "});
3179}
3180
3181#[gpui::test]
3182async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3183 init_test(cx, |_| {});
3184
3185 let mut cx = EditorTestContext::new(cx).await;
3186 let language = Arc::new(
3187 Language::new(
3188 LanguageConfig::default(),
3189 Some(tree_sitter_rust::LANGUAGE.into()),
3190 )
3191 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3192 .unwrap(),
3193 );
3194 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3195
3196 // test when all cursors are not at suggested indent
3197 // then simply move to their suggested indent location
3198 cx.set_state(indoc! {"
3199 const a: B = (
3200 c(
3201 ˇ
3202 ˇ )
3203 );
3204 "});
3205 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3206 cx.assert_editor_state(indoc! {"
3207 const a: B = (
3208 c(
3209 ˇ
3210 ˇ)
3211 );
3212 "});
3213
3214 // test cursor already at suggested indent not moving when
3215 // other cursors are yet to reach their suggested indents
3216 cx.set_state(indoc! {"
3217 ˇ
3218 const a: B = (
3219 c(
3220 d(
3221 ˇ
3222 )
3223 ˇ
3224 ˇ )
3225 );
3226 "});
3227 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3228 cx.assert_editor_state(indoc! {"
3229 ˇ
3230 const a: B = (
3231 c(
3232 d(
3233 ˇ
3234 )
3235 ˇ
3236 ˇ)
3237 );
3238 "});
3239 // test when all cursors are at suggested indent then tab is inserted
3240 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3241 cx.assert_editor_state(indoc! {"
3242 ˇ
3243 const a: B = (
3244 c(
3245 d(
3246 ˇ
3247 )
3248 ˇ
3249 ˇ)
3250 );
3251 "});
3252
3253 // test when current indent is less than suggested indent,
3254 // we adjust line to match suggested indent and move cursor to it
3255 //
3256 // when no other cursor is at word boundary, all of them should move
3257 cx.set_state(indoc! {"
3258 const a: B = (
3259 c(
3260 d(
3261 ˇ
3262 ˇ )
3263 ˇ )
3264 );
3265 "});
3266 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3267 cx.assert_editor_state(indoc! {"
3268 const a: B = (
3269 c(
3270 d(
3271 ˇ
3272 ˇ)
3273 ˇ)
3274 );
3275 "});
3276
3277 // test when current indent is less than suggested indent,
3278 // we adjust line to match suggested indent and move cursor to it
3279 //
3280 // when some other cursor is at word boundary, it should not move
3281 cx.set_state(indoc! {"
3282 const a: B = (
3283 c(
3284 d(
3285 ˇ
3286 ˇ )
3287 ˇ)
3288 );
3289 "});
3290 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3291 cx.assert_editor_state(indoc! {"
3292 const a: B = (
3293 c(
3294 d(
3295 ˇ
3296 ˇ)
3297 ˇ)
3298 );
3299 "});
3300
3301 // test when current indent is more than suggested indent,
3302 // we just move cursor to current indent instead of suggested indent
3303 //
3304 // when no other cursor is at word boundary, all of them should move
3305 cx.set_state(indoc! {"
3306 const a: B = (
3307 c(
3308 d(
3309 ˇ
3310 ˇ )
3311 ˇ )
3312 );
3313 "});
3314 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3315 cx.assert_editor_state(indoc! {"
3316 const a: B = (
3317 c(
3318 d(
3319 ˇ
3320 ˇ)
3321 ˇ)
3322 );
3323 "});
3324 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3325 cx.assert_editor_state(indoc! {"
3326 const a: B = (
3327 c(
3328 d(
3329 ˇ
3330 ˇ)
3331 ˇ)
3332 );
3333 "});
3334
3335 // test when current indent is more than suggested indent,
3336 // we just move cursor to current indent instead of suggested indent
3337 //
3338 // when some other cursor is at word boundary, it doesn't move
3339 cx.set_state(indoc! {"
3340 const a: B = (
3341 c(
3342 d(
3343 ˇ
3344 ˇ )
3345 ˇ)
3346 );
3347 "});
3348 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3349 cx.assert_editor_state(indoc! {"
3350 const a: B = (
3351 c(
3352 d(
3353 ˇ
3354 ˇ)
3355 ˇ)
3356 );
3357 "});
3358
3359 // handle auto-indent when there are multiple cursors on the same line
3360 cx.set_state(indoc! {"
3361 const a: B = (
3362 c(
3363 ˇ ˇ
3364 ˇ )
3365 );
3366 "});
3367 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3368 cx.assert_editor_state(indoc! {"
3369 const a: B = (
3370 c(
3371 ˇ
3372 ˇ)
3373 );
3374 "});
3375}
3376
3377#[gpui::test]
3378async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3379 init_test(cx, |settings| {
3380 settings.defaults.tab_size = NonZeroU32::new(3)
3381 });
3382
3383 let mut cx = EditorTestContext::new(cx).await;
3384 cx.set_state(indoc! {"
3385 ˇ
3386 \t ˇ
3387 \t ˇ
3388 \t ˇ
3389 \t \t\t \t \t\t \t\t \t \t ˇ
3390 "});
3391
3392 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3393 cx.assert_editor_state(indoc! {"
3394 ˇ
3395 \t ˇ
3396 \t ˇ
3397 \t ˇ
3398 \t \t\t \t \t\t \t\t \t \t ˇ
3399 "});
3400}
3401
3402#[gpui::test]
3403async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3404 init_test(cx, |settings| {
3405 settings.defaults.tab_size = NonZeroU32::new(4)
3406 });
3407
3408 let language = Arc::new(
3409 Language::new(
3410 LanguageConfig::default(),
3411 Some(tree_sitter_rust::LANGUAGE.into()),
3412 )
3413 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3414 .unwrap(),
3415 );
3416
3417 let mut cx = EditorTestContext::new(cx).await;
3418 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3419 cx.set_state(indoc! {"
3420 fn a() {
3421 if b {
3422 \t ˇc
3423 }
3424 }
3425 "});
3426
3427 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3428 cx.assert_editor_state(indoc! {"
3429 fn a() {
3430 if b {
3431 ˇc
3432 }
3433 }
3434 "});
3435}
3436
3437#[gpui::test]
3438async fn test_indent_outdent(cx: &mut TestAppContext) {
3439 init_test(cx, |settings| {
3440 settings.defaults.tab_size = NonZeroU32::new(4);
3441 });
3442
3443 let mut cx = EditorTestContext::new(cx).await;
3444
3445 cx.set_state(indoc! {"
3446 «oneˇ» «twoˇ»
3447 three
3448 four
3449 "});
3450 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3451 cx.assert_editor_state(indoc! {"
3452 «oneˇ» «twoˇ»
3453 three
3454 four
3455 "});
3456
3457 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3458 cx.assert_editor_state(indoc! {"
3459 «oneˇ» «twoˇ»
3460 three
3461 four
3462 "});
3463
3464 // select across line ending
3465 cx.set_state(indoc! {"
3466 one two
3467 t«hree
3468 ˇ» four
3469 "});
3470 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3471 cx.assert_editor_state(indoc! {"
3472 one two
3473 t«hree
3474 ˇ» four
3475 "});
3476
3477 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3478 cx.assert_editor_state(indoc! {"
3479 one two
3480 t«hree
3481 ˇ» four
3482 "});
3483
3484 // Ensure that indenting/outdenting works when the cursor is at column 0.
3485 cx.set_state(indoc! {"
3486 one two
3487 ˇthree
3488 four
3489 "});
3490 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3491 cx.assert_editor_state(indoc! {"
3492 one two
3493 ˇthree
3494 four
3495 "});
3496
3497 cx.set_state(indoc! {"
3498 one two
3499 ˇ three
3500 four
3501 "});
3502 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3503 cx.assert_editor_state(indoc! {"
3504 one two
3505 ˇthree
3506 four
3507 "});
3508}
3509
3510#[gpui::test]
3511async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3512 // This is a regression test for issue #33761
3513 init_test(cx, |_| {});
3514
3515 let mut cx = EditorTestContext::new(cx).await;
3516 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3517 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3518
3519 cx.set_state(
3520 r#"ˇ# ingress:
3521ˇ# api:
3522ˇ# enabled: false
3523ˇ# pathType: Prefix
3524ˇ# console:
3525ˇ# enabled: false
3526ˇ# pathType: Prefix
3527"#,
3528 );
3529
3530 // Press tab to indent all lines
3531 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3532
3533 cx.assert_editor_state(
3534 r#" ˇ# ingress:
3535 ˇ# api:
3536 ˇ# enabled: false
3537 ˇ# pathType: Prefix
3538 ˇ# console:
3539 ˇ# enabled: false
3540 ˇ# pathType: Prefix
3541"#,
3542 );
3543}
3544
3545#[gpui::test]
3546async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3547 // This is a test to make sure our fix for issue #33761 didn't break anything
3548 init_test(cx, |_| {});
3549
3550 let mut cx = EditorTestContext::new(cx).await;
3551 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3552 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3553
3554 cx.set_state(
3555 r#"ˇingress:
3556ˇ api:
3557ˇ enabled: false
3558ˇ pathType: Prefix
3559"#,
3560 );
3561
3562 // Press tab to indent all lines
3563 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3564
3565 cx.assert_editor_state(
3566 r#"ˇingress:
3567 ˇapi:
3568 ˇenabled: false
3569 ˇpathType: Prefix
3570"#,
3571 );
3572}
3573
3574#[gpui::test]
3575async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3576 init_test(cx, |settings| {
3577 settings.defaults.hard_tabs = Some(true);
3578 });
3579
3580 let mut cx = EditorTestContext::new(cx).await;
3581
3582 // select two ranges on one line
3583 cx.set_state(indoc! {"
3584 «oneˇ» «twoˇ»
3585 three
3586 four
3587 "});
3588 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3589 cx.assert_editor_state(indoc! {"
3590 \t«oneˇ» «twoˇ»
3591 three
3592 four
3593 "});
3594 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3595 cx.assert_editor_state(indoc! {"
3596 \t\t«oneˇ» «twoˇ»
3597 three
3598 four
3599 "});
3600 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3601 cx.assert_editor_state(indoc! {"
3602 \t«oneˇ» «twoˇ»
3603 three
3604 four
3605 "});
3606 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3607 cx.assert_editor_state(indoc! {"
3608 «oneˇ» «twoˇ»
3609 three
3610 four
3611 "});
3612
3613 // select across a line ending
3614 cx.set_state(indoc! {"
3615 one two
3616 t«hree
3617 ˇ»four
3618 "});
3619 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3620 cx.assert_editor_state(indoc! {"
3621 one two
3622 \tt«hree
3623 ˇ»four
3624 "});
3625 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3626 cx.assert_editor_state(indoc! {"
3627 one two
3628 \t\tt«hree
3629 ˇ»four
3630 "});
3631 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3632 cx.assert_editor_state(indoc! {"
3633 one two
3634 \tt«hree
3635 ˇ»four
3636 "});
3637 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3638 cx.assert_editor_state(indoc! {"
3639 one two
3640 t«hree
3641 ˇ»four
3642 "});
3643
3644 // Ensure that indenting/outdenting works when the cursor is at column 0.
3645 cx.set_state(indoc! {"
3646 one two
3647 ˇthree
3648 four
3649 "});
3650 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3651 cx.assert_editor_state(indoc! {"
3652 one two
3653 ˇthree
3654 four
3655 "});
3656 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3657 cx.assert_editor_state(indoc! {"
3658 one two
3659 \tˇthree
3660 four
3661 "});
3662 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3663 cx.assert_editor_state(indoc! {"
3664 one two
3665 ˇthree
3666 four
3667 "});
3668}
3669
3670#[gpui::test]
3671fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3672 init_test(cx, |settings| {
3673 settings.languages.0.extend([
3674 (
3675 "TOML".into(),
3676 LanguageSettingsContent {
3677 tab_size: NonZeroU32::new(2),
3678 ..Default::default()
3679 },
3680 ),
3681 (
3682 "Rust".into(),
3683 LanguageSettingsContent {
3684 tab_size: NonZeroU32::new(4),
3685 ..Default::default()
3686 },
3687 ),
3688 ]);
3689 });
3690
3691 let toml_language = Arc::new(Language::new(
3692 LanguageConfig {
3693 name: "TOML".into(),
3694 ..Default::default()
3695 },
3696 None,
3697 ));
3698 let rust_language = Arc::new(Language::new(
3699 LanguageConfig {
3700 name: "Rust".into(),
3701 ..Default::default()
3702 },
3703 None,
3704 ));
3705
3706 let toml_buffer =
3707 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3708 let rust_buffer =
3709 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3710 let multibuffer = cx.new(|cx| {
3711 let mut multibuffer = MultiBuffer::new(ReadWrite);
3712 multibuffer.push_excerpts(
3713 toml_buffer.clone(),
3714 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3715 cx,
3716 );
3717 multibuffer.push_excerpts(
3718 rust_buffer.clone(),
3719 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3720 cx,
3721 );
3722 multibuffer
3723 });
3724
3725 cx.add_window(|window, cx| {
3726 let mut editor = build_editor(multibuffer, window, cx);
3727
3728 assert_eq!(
3729 editor.text(cx),
3730 indoc! {"
3731 a = 1
3732 b = 2
3733
3734 const c: usize = 3;
3735 "}
3736 );
3737
3738 select_ranges(
3739 &mut editor,
3740 indoc! {"
3741 «aˇ» = 1
3742 b = 2
3743
3744 «const c:ˇ» usize = 3;
3745 "},
3746 window,
3747 cx,
3748 );
3749
3750 editor.tab(&Tab, window, cx);
3751 assert_text_with_selections(
3752 &mut editor,
3753 indoc! {"
3754 «aˇ» = 1
3755 b = 2
3756
3757 «const c:ˇ» usize = 3;
3758 "},
3759 cx,
3760 );
3761 editor.backtab(&Backtab, window, cx);
3762 assert_text_with_selections(
3763 &mut editor,
3764 indoc! {"
3765 «aˇ» = 1
3766 b = 2
3767
3768 «const c:ˇ» usize = 3;
3769 "},
3770 cx,
3771 );
3772
3773 editor
3774 });
3775}
3776
3777#[gpui::test]
3778async fn test_backspace(cx: &mut TestAppContext) {
3779 init_test(cx, |_| {});
3780
3781 let mut cx = EditorTestContext::new(cx).await;
3782
3783 // Basic backspace
3784 cx.set_state(indoc! {"
3785 onˇe two three
3786 fou«rˇ» five six
3787 seven «ˇeight nine
3788 »ten
3789 "});
3790 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3791 cx.assert_editor_state(indoc! {"
3792 oˇe two three
3793 fouˇ five six
3794 seven ˇten
3795 "});
3796
3797 // Test backspace inside and around indents
3798 cx.set_state(indoc! {"
3799 zero
3800 ˇone
3801 ˇtwo
3802 ˇ ˇ ˇ three
3803 ˇ ˇ four
3804 "});
3805 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3806 cx.assert_editor_state(indoc! {"
3807 zero
3808 ˇone
3809 ˇtwo
3810 ˇ threeˇ four
3811 "});
3812}
3813
3814#[gpui::test]
3815async fn test_delete(cx: &mut TestAppContext) {
3816 init_test(cx, |_| {});
3817
3818 let mut cx = EditorTestContext::new(cx).await;
3819 cx.set_state(indoc! {"
3820 onˇe two three
3821 fou«rˇ» five six
3822 seven «ˇeight nine
3823 »ten
3824 "});
3825 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3826 cx.assert_editor_state(indoc! {"
3827 onˇ two three
3828 fouˇ five six
3829 seven ˇten
3830 "});
3831}
3832
3833#[gpui::test]
3834fn test_delete_line(cx: &mut TestAppContext) {
3835 init_test(cx, |_| {});
3836
3837 let editor = cx.add_window(|window, cx| {
3838 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3839 build_editor(buffer, window, cx)
3840 });
3841 _ = editor.update(cx, |editor, window, cx| {
3842 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3843 s.select_display_ranges([
3844 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3845 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3846 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3847 ])
3848 });
3849 editor.delete_line(&DeleteLine, window, cx);
3850 assert_eq!(editor.display_text(cx), "ghi");
3851 assert_eq!(
3852 editor.selections.display_ranges(cx),
3853 vec![
3854 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3855 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3856 ]
3857 );
3858 });
3859
3860 let editor = cx.add_window(|window, cx| {
3861 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3862 build_editor(buffer, window, cx)
3863 });
3864 _ = editor.update(cx, |editor, window, cx| {
3865 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3866 s.select_display_ranges([
3867 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3868 ])
3869 });
3870 editor.delete_line(&DeleteLine, window, cx);
3871 assert_eq!(editor.display_text(cx), "ghi\n");
3872 assert_eq!(
3873 editor.selections.display_ranges(cx),
3874 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3875 );
3876 });
3877}
3878
3879#[gpui::test]
3880fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3881 init_test(cx, |_| {});
3882
3883 cx.add_window(|window, cx| {
3884 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3885 let mut editor = build_editor(buffer.clone(), window, cx);
3886 let buffer = buffer.read(cx).as_singleton().unwrap();
3887
3888 assert_eq!(
3889 editor.selections.ranges::<Point>(cx),
3890 &[Point::new(0, 0)..Point::new(0, 0)]
3891 );
3892
3893 // When on single line, replace newline at end by space
3894 editor.join_lines(&JoinLines, window, cx);
3895 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3896 assert_eq!(
3897 editor.selections.ranges::<Point>(cx),
3898 &[Point::new(0, 3)..Point::new(0, 3)]
3899 );
3900
3901 // When multiple lines are selected, remove newlines that are spanned by the selection
3902 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3903 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3904 });
3905 editor.join_lines(&JoinLines, window, cx);
3906 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3907 assert_eq!(
3908 editor.selections.ranges::<Point>(cx),
3909 &[Point::new(0, 11)..Point::new(0, 11)]
3910 );
3911
3912 // Undo should be transactional
3913 editor.undo(&Undo, window, cx);
3914 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3915 assert_eq!(
3916 editor.selections.ranges::<Point>(cx),
3917 &[Point::new(0, 5)..Point::new(2, 2)]
3918 );
3919
3920 // When joining an empty line don't insert a space
3921 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3922 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3923 });
3924 editor.join_lines(&JoinLines, window, cx);
3925 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3926 assert_eq!(
3927 editor.selections.ranges::<Point>(cx),
3928 [Point::new(2, 3)..Point::new(2, 3)]
3929 );
3930
3931 // We can remove trailing newlines
3932 editor.join_lines(&JoinLines, window, cx);
3933 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3934 assert_eq!(
3935 editor.selections.ranges::<Point>(cx),
3936 [Point::new(2, 3)..Point::new(2, 3)]
3937 );
3938
3939 // We don't blow up on the last line
3940 editor.join_lines(&JoinLines, window, cx);
3941 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3942 assert_eq!(
3943 editor.selections.ranges::<Point>(cx),
3944 [Point::new(2, 3)..Point::new(2, 3)]
3945 );
3946
3947 // reset to test indentation
3948 editor.buffer.update(cx, |buffer, cx| {
3949 buffer.edit(
3950 [
3951 (Point::new(1, 0)..Point::new(1, 2), " "),
3952 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3953 ],
3954 None,
3955 cx,
3956 )
3957 });
3958
3959 // We remove any leading spaces
3960 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3961 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3962 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3963 });
3964 editor.join_lines(&JoinLines, window, cx);
3965 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3966
3967 // We don't insert a space for a line containing only spaces
3968 editor.join_lines(&JoinLines, window, cx);
3969 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3970
3971 // We ignore any leading tabs
3972 editor.join_lines(&JoinLines, window, cx);
3973 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3974
3975 editor
3976 });
3977}
3978
3979#[gpui::test]
3980fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3981 init_test(cx, |_| {});
3982
3983 cx.add_window(|window, cx| {
3984 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3985 let mut editor = build_editor(buffer.clone(), window, cx);
3986 let buffer = buffer.read(cx).as_singleton().unwrap();
3987
3988 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3989 s.select_ranges([
3990 Point::new(0, 2)..Point::new(1, 1),
3991 Point::new(1, 2)..Point::new(1, 2),
3992 Point::new(3, 1)..Point::new(3, 2),
3993 ])
3994 });
3995
3996 editor.join_lines(&JoinLines, window, cx);
3997 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3998
3999 assert_eq!(
4000 editor.selections.ranges::<Point>(cx),
4001 [
4002 Point::new(0, 7)..Point::new(0, 7),
4003 Point::new(1, 3)..Point::new(1, 3)
4004 ]
4005 );
4006 editor
4007 });
4008}
4009
4010#[gpui::test]
4011async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4012 init_test(cx, |_| {});
4013
4014 let mut cx = EditorTestContext::new(cx).await;
4015
4016 let diff_base = r#"
4017 Line 0
4018 Line 1
4019 Line 2
4020 Line 3
4021 "#
4022 .unindent();
4023
4024 cx.set_state(
4025 &r#"
4026 ˇLine 0
4027 Line 1
4028 Line 2
4029 Line 3
4030 "#
4031 .unindent(),
4032 );
4033
4034 cx.set_head_text(&diff_base);
4035 executor.run_until_parked();
4036
4037 // Join lines
4038 cx.update_editor(|editor, window, cx| {
4039 editor.join_lines(&JoinLines, window, cx);
4040 });
4041 executor.run_until_parked();
4042
4043 cx.assert_editor_state(
4044 &r#"
4045 Line 0ˇ Line 1
4046 Line 2
4047 Line 3
4048 "#
4049 .unindent(),
4050 );
4051 // Join again
4052 cx.update_editor(|editor, window, cx| {
4053 editor.join_lines(&JoinLines, window, cx);
4054 });
4055 executor.run_until_parked();
4056
4057 cx.assert_editor_state(
4058 &r#"
4059 Line 0 Line 1ˇ Line 2
4060 Line 3
4061 "#
4062 .unindent(),
4063 );
4064}
4065
4066#[gpui::test]
4067async fn test_custom_newlines_cause_no_false_positive_diffs(
4068 executor: BackgroundExecutor,
4069 cx: &mut TestAppContext,
4070) {
4071 init_test(cx, |_| {});
4072 let mut cx = EditorTestContext::new(cx).await;
4073 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4074 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4075 executor.run_until_parked();
4076
4077 cx.update_editor(|editor, window, cx| {
4078 let snapshot = editor.snapshot(window, cx);
4079 assert_eq!(
4080 snapshot
4081 .buffer_snapshot
4082 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
4083 .collect::<Vec<_>>(),
4084 Vec::new(),
4085 "Should not have any diffs for files with custom newlines"
4086 );
4087 });
4088}
4089
4090#[gpui::test]
4091async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4092 init_test(cx, |_| {});
4093
4094 let mut cx = EditorTestContext::new(cx).await;
4095
4096 // Test sort_lines_case_insensitive()
4097 cx.set_state(indoc! {"
4098 «z
4099 y
4100 x
4101 Z
4102 Y
4103 Xˇ»
4104 "});
4105 cx.update_editor(|e, window, cx| {
4106 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4107 });
4108 cx.assert_editor_state(indoc! {"
4109 «x
4110 X
4111 y
4112 Y
4113 z
4114 Zˇ»
4115 "});
4116
4117 // Test sort_lines_by_length()
4118 //
4119 // Demonstrates:
4120 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4121 // - sort is stable
4122 cx.set_state(indoc! {"
4123 «123
4124 æ
4125 12
4126 ∞
4127 1
4128 æˇ»
4129 "});
4130 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4131 cx.assert_editor_state(indoc! {"
4132 «æ
4133 ∞
4134 1
4135 æ
4136 12
4137 123ˇ»
4138 "});
4139
4140 // Test reverse_lines()
4141 cx.set_state(indoc! {"
4142 «5
4143 4
4144 3
4145 2
4146 1ˇ»
4147 "});
4148 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4149 cx.assert_editor_state(indoc! {"
4150 «1
4151 2
4152 3
4153 4
4154 5ˇ»
4155 "});
4156
4157 // Skip testing shuffle_line()
4158
4159 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4160 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4161
4162 // Don't manipulate when cursor is on single line, but expand the selection
4163 cx.set_state(indoc! {"
4164 ddˇdd
4165 ccc
4166 bb
4167 a
4168 "});
4169 cx.update_editor(|e, window, cx| {
4170 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4171 });
4172 cx.assert_editor_state(indoc! {"
4173 «ddddˇ»
4174 ccc
4175 bb
4176 a
4177 "});
4178
4179 // Basic manipulate case
4180 // Start selection moves to column 0
4181 // End of selection shrinks to fit shorter line
4182 cx.set_state(indoc! {"
4183 dd«d
4184 ccc
4185 bb
4186 aaaaaˇ»
4187 "});
4188 cx.update_editor(|e, window, cx| {
4189 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4190 });
4191 cx.assert_editor_state(indoc! {"
4192 «aaaaa
4193 bb
4194 ccc
4195 dddˇ»
4196 "});
4197
4198 // Manipulate case with newlines
4199 cx.set_state(indoc! {"
4200 dd«d
4201 ccc
4202
4203 bb
4204 aaaaa
4205
4206 ˇ»
4207 "});
4208 cx.update_editor(|e, window, cx| {
4209 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4210 });
4211 cx.assert_editor_state(indoc! {"
4212 «
4213
4214 aaaaa
4215 bb
4216 ccc
4217 dddˇ»
4218
4219 "});
4220
4221 // Adding new line
4222 cx.set_state(indoc! {"
4223 aa«a
4224 bbˇ»b
4225 "});
4226 cx.update_editor(|e, window, cx| {
4227 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4228 });
4229 cx.assert_editor_state(indoc! {"
4230 «aaa
4231 bbb
4232 added_lineˇ»
4233 "});
4234
4235 // Removing line
4236 cx.set_state(indoc! {"
4237 aa«a
4238 bbbˇ»
4239 "});
4240 cx.update_editor(|e, window, cx| {
4241 e.manipulate_immutable_lines(window, cx, |lines| {
4242 lines.pop();
4243 })
4244 });
4245 cx.assert_editor_state(indoc! {"
4246 «aaaˇ»
4247 "});
4248
4249 // Removing all lines
4250 cx.set_state(indoc! {"
4251 aa«a
4252 bbbˇ»
4253 "});
4254 cx.update_editor(|e, window, cx| {
4255 e.manipulate_immutable_lines(window, cx, |lines| {
4256 lines.drain(..);
4257 })
4258 });
4259 cx.assert_editor_state(indoc! {"
4260 ˇ
4261 "});
4262}
4263
4264#[gpui::test]
4265async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4266 init_test(cx, |_| {});
4267
4268 let mut cx = EditorTestContext::new(cx).await;
4269
4270 // Consider continuous selection as single selection
4271 cx.set_state(indoc! {"
4272 Aaa«aa
4273 cˇ»c«c
4274 bb
4275 aaaˇ»aa
4276 "});
4277 cx.update_editor(|e, window, cx| {
4278 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4279 });
4280 cx.assert_editor_state(indoc! {"
4281 «Aaaaa
4282 ccc
4283 bb
4284 aaaaaˇ»
4285 "});
4286
4287 cx.set_state(indoc! {"
4288 Aaa«aa
4289 cˇ»c«c
4290 bb
4291 aaaˇ»aa
4292 "});
4293 cx.update_editor(|e, window, cx| {
4294 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4295 });
4296 cx.assert_editor_state(indoc! {"
4297 «Aaaaa
4298 ccc
4299 bbˇ»
4300 "});
4301
4302 // Consider non continuous selection as distinct dedup operations
4303 cx.set_state(indoc! {"
4304 «aaaaa
4305 bb
4306 aaaaa
4307 aaaaaˇ»
4308
4309 aaa«aaˇ»
4310 "});
4311 cx.update_editor(|e, window, cx| {
4312 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4313 });
4314 cx.assert_editor_state(indoc! {"
4315 «aaaaa
4316 bbˇ»
4317
4318 «aaaaaˇ»
4319 "});
4320}
4321
4322#[gpui::test]
4323async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4324 init_test(cx, |_| {});
4325
4326 let mut cx = EditorTestContext::new(cx).await;
4327
4328 cx.set_state(indoc! {"
4329 «Aaa
4330 aAa
4331 Aaaˇ»
4332 "});
4333 cx.update_editor(|e, window, cx| {
4334 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4335 });
4336 cx.assert_editor_state(indoc! {"
4337 «Aaa
4338 aAaˇ»
4339 "});
4340
4341 cx.set_state(indoc! {"
4342 «Aaa
4343 aAa
4344 aaAˇ»
4345 "});
4346 cx.update_editor(|e, window, cx| {
4347 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4348 });
4349 cx.assert_editor_state(indoc! {"
4350 «Aaaˇ»
4351 "});
4352}
4353
4354#[gpui::test]
4355async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
4356 init_test(cx, |_| {});
4357
4358 let mut cx = EditorTestContext::new(cx).await;
4359
4360 // Manipulate with multiple selections on a single line
4361 cx.set_state(indoc! {"
4362 dd«dd
4363 cˇ»c«c
4364 bb
4365 aaaˇ»aa
4366 "});
4367 cx.update_editor(|e, window, cx| {
4368 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4369 });
4370 cx.assert_editor_state(indoc! {"
4371 «aaaaa
4372 bb
4373 ccc
4374 ddddˇ»
4375 "});
4376
4377 // Manipulate with multiple disjoin selections
4378 cx.set_state(indoc! {"
4379 5«
4380 4
4381 3
4382 2
4383 1ˇ»
4384
4385 dd«dd
4386 ccc
4387 bb
4388 aaaˇ»aa
4389 "});
4390 cx.update_editor(|e, window, cx| {
4391 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4392 });
4393 cx.assert_editor_state(indoc! {"
4394 «1
4395 2
4396 3
4397 4
4398 5ˇ»
4399
4400 «aaaaa
4401 bb
4402 ccc
4403 ddddˇ»
4404 "});
4405
4406 // Adding lines on each selection
4407 cx.set_state(indoc! {"
4408 2«
4409 1ˇ»
4410
4411 bb«bb
4412 aaaˇ»aa
4413 "});
4414 cx.update_editor(|e, window, cx| {
4415 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
4416 });
4417 cx.assert_editor_state(indoc! {"
4418 «2
4419 1
4420 added lineˇ»
4421
4422 «bbbb
4423 aaaaa
4424 added lineˇ»
4425 "});
4426
4427 // Removing lines on each selection
4428 cx.set_state(indoc! {"
4429 2«
4430 1ˇ»
4431
4432 bb«bb
4433 aaaˇ»aa
4434 "});
4435 cx.update_editor(|e, window, cx| {
4436 e.manipulate_immutable_lines(window, cx, |lines| {
4437 lines.pop();
4438 })
4439 });
4440 cx.assert_editor_state(indoc! {"
4441 «2ˇ»
4442
4443 «bbbbˇ»
4444 "});
4445}
4446
4447#[gpui::test]
4448async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
4449 init_test(cx, |settings| {
4450 settings.defaults.tab_size = NonZeroU32::new(3)
4451 });
4452
4453 let mut cx = EditorTestContext::new(cx).await;
4454
4455 // MULTI SELECTION
4456 // Ln.1 "«" tests empty lines
4457 // Ln.9 tests just leading whitespace
4458 cx.set_state(indoc! {"
4459 «
4460 abc // No indentationˇ»
4461 «\tabc // 1 tabˇ»
4462 \t\tabc « ˇ» // 2 tabs
4463 \t ab«c // Tab followed by space
4464 \tabc // Space followed by tab (3 spaces should be the result)
4465 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4466 abˇ»ˇc ˇ ˇ // Already space indented«
4467 \t
4468 \tabc\tdef // Only the leading tab is manipulatedˇ»
4469 "});
4470 cx.update_editor(|e, window, cx| {
4471 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4472 });
4473 cx.assert_editor_state(
4474 indoc! {"
4475 «
4476 abc // No indentation
4477 abc // 1 tab
4478 abc // 2 tabs
4479 abc // Tab followed by space
4480 abc // Space followed by tab (3 spaces should be the result)
4481 abc // Mixed indentation (tab conversion depends on the column)
4482 abc // Already space indented
4483 ·
4484 abc\tdef // Only the leading tab is manipulatedˇ»
4485 "}
4486 .replace("·", "")
4487 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4488 );
4489
4490 // Test on just a few lines, the others should remain unchanged
4491 // Only lines (3, 5, 10, 11) should change
4492 cx.set_state(
4493 indoc! {"
4494 ·
4495 abc // No indentation
4496 \tabcˇ // 1 tab
4497 \t\tabc // 2 tabs
4498 \t abcˇ // Tab followed by space
4499 \tabc // Space followed by tab (3 spaces should be the result)
4500 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4501 abc // Already space indented
4502 «\t
4503 \tabc\tdef // Only the leading tab is manipulatedˇ»
4504 "}
4505 .replace("·", "")
4506 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4507 );
4508 cx.update_editor(|e, window, cx| {
4509 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4510 });
4511 cx.assert_editor_state(
4512 indoc! {"
4513 ·
4514 abc // No indentation
4515 « abc // 1 tabˇ»
4516 \t\tabc // 2 tabs
4517 « abc // Tab followed by spaceˇ»
4518 \tabc // Space followed by tab (3 spaces should be the result)
4519 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4520 abc // Already space indented
4521 « ·
4522 abc\tdef // Only the leading tab is manipulatedˇ»
4523 "}
4524 .replace("·", "")
4525 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4526 );
4527
4528 // SINGLE SELECTION
4529 // Ln.1 "«" tests empty lines
4530 // Ln.9 tests just leading whitespace
4531 cx.set_state(indoc! {"
4532 «
4533 abc // No indentation
4534 \tabc // 1 tab
4535 \t\tabc // 2 tabs
4536 \t abc // Tab followed by space
4537 \tabc // Space followed by tab (3 spaces should be the result)
4538 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4539 abc // Already space indented
4540 \t
4541 \tabc\tdef // Only the leading tab is manipulatedˇ»
4542 "});
4543 cx.update_editor(|e, window, cx| {
4544 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4545 });
4546 cx.assert_editor_state(
4547 indoc! {"
4548 «
4549 abc // No indentation
4550 abc // 1 tab
4551 abc // 2 tabs
4552 abc // Tab followed by space
4553 abc // Space followed by tab (3 spaces should be the result)
4554 abc // Mixed indentation (tab conversion depends on the column)
4555 abc // Already space indented
4556 ·
4557 abc\tdef // Only the leading tab is manipulatedˇ»
4558 "}
4559 .replace("·", "")
4560 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4561 );
4562}
4563
4564#[gpui::test]
4565async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
4566 init_test(cx, |settings| {
4567 settings.defaults.tab_size = NonZeroU32::new(3)
4568 });
4569
4570 let mut cx = EditorTestContext::new(cx).await;
4571
4572 // MULTI SELECTION
4573 // Ln.1 "«" tests empty lines
4574 // Ln.11 tests just leading whitespace
4575 cx.set_state(indoc! {"
4576 «
4577 abˇ»ˇc // No indentation
4578 abc ˇ ˇ // 1 space (< 3 so dont convert)
4579 abc « // 2 spaces (< 3 so dont convert)
4580 abc // 3 spaces (convert)
4581 abc ˇ» // 5 spaces (1 tab + 2 spaces)
4582 «\tˇ»\t«\tˇ»abc // Already tab indented
4583 «\t abc // Tab followed by space
4584 \tabc // Space followed by tab (should be consumed due to tab)
4585 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4586 \tˇ» «\t
4587 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
4588 "});
4589 cx.update_editor(|e, window, cx| {
4590 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4591 });
4592 cx.assert_editor_state(indoc! {"
4593 «
4594 abc // No indentation
4595 abc // 1 space (< 3 so dont convert)
4596 abc // 2 spaces (< 3 so dont convert)
4597 \tabc // 3 spaces (convert)
4598 \t abc // 5 spaces (1 tab + 2 spaces)
4599 \t\t\tabc // Already tab indented
4600 \t abc // Tab followed by space
4601 \tabc // Space followed by tab (should be consumed due to tab)
4602 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4603 \t\t\t
4604 \tabc \t // Only the leading spaces should be convertedˇ»
4605 "});
4606
4607 // Test on just a few lines, the other should remain unchanged
4608 // Only lines (4, 8, 11, 12) should change
4609 cx.set_state(
4610 indoc! {"
4611 ·
4612 abc // No indentation
4613 abc // 1 space (< 3 so dont convert)
4614 abc // 2 spaces (< 3 so dont convert)
4615 « abc // 3 spaces (convert)ˇ»
4616 abc // 5 spaces (1 tab + 2 spaces)
4617 \t\t\tabc // Already tab indented
4618 \t abc // Tab followed by space
4619 \tabc ˇ // Space followed by tab (should be consumed due to tab)
4620 \t\t \tabc // Mixed indentation
4621 \t \t \t \tabc // Mixed indentation
4622 \t \tˇ
4623 « abc \t // Only the leading spaces should be convertedˇ»
4624 "}
4625 .replace("·", "")
4626 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4627 );
4628 cx.update_editor(|e, window, cx| {
4629 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4630 });
4631 cx.assert_editor_state(
4632 indoc! {"
4633 ·
4634 abc // No indentation
4635 abc // 1 space (< 3 so dont convert)
4636 abc // 2 spaces (< 3 so dont convert)
4637 «\tabc // 3 spaces (convert)ˇ»
4638 abc // 5 spaces (1 tab + 2 spaces)
4639 \t\t\tabc // Already tab indented
4640 \t abc // Tab followed by space
4641 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
4642 \t\t \tabc // Mixed indentation
4643 \t \t \t \tabc // Mixed indentation
4644 «\t\t\t
4645 \tabc \t // Only the leading spaces should be convertedˇ»
4646 "}
4647 .replace("·", "")
4648 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4649 );
4650
4651 // SINGLE SELECTION
4652 // Ln.1 "«" tests empty lines
4653 // Ln.11 tests just leading whitespace
4654 cx.set_state(indoc! {"
4655 «
4656 abc // No indentation
4657 abc // 1 space (< 3 so dont convert)
4658 abc // 2 spaces (< 3 so dont convert)
4659 abc // 3 spaces (convert)
4660 abc // 5 spaces (1 tab + 2 spaces)
4661 \t\t\tabc // Already tab indented
4662 \t abc // Tab followed by space
4663 \tabc // Space followed by tab (should be consumed due to tab)
4664 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4665 \t \t
4666 abc \t // Only the leading spaces should be convertedˇ»
4667 "});
4668 cx.update_editor(|e, window, cx| {
4669 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4670 });
4671 cx.assert_editor_state(indoc! {"
4672 «
4673 abc // No indentation
4674 abc // 1 space (< 3 so dont convert)
4675 abc // 2 spaces (< 3 so dont convert)
4676 \tabc // 3 spaces (convert)
4677 \t abc // 5 spaces (1 tab + 2 spaces)
4678 \t\t\tabc // Already tab indented
4679 \t abc // Tab followed by space
4680 \tabc // Space followed by tab (should be consumed due to tab)
4681 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4682 \t\t\t
4683 \tabc \t // Only the leading spaces should be convertedˇ»
4684 "});
4685}
4686
4687#[gpui::test]
4688async fn test_toggle_case(cx: &mut TestAppContext) {
4689 init_test(cx, |_| {});
4690
4691 let mut cx = EditorTestContext::new(cx).await;
4692
4693 // If all lower case -> upper case
4694 cx.set_state(indoc! {"
4695 «hello worldˇ»
4696 "});
4697 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4698 cx.assert_editor_state(indoc! {"
4699 «HELLO WORLDˇ»
4700 "});
4701
4702 // If all upper case -> lower case
4703 cx.set_state(indoc! {"
4704 «HELLO WORLDˇ»
4705 "});
4706 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4707 cx.assert_editor_state(indoc! {"
4708 «hello worldˇ»
4709 "});
4710
4711 // If any upper case characters are identified -> lower case
4712 // This matches JetBrains IDEs
4713 cx.set_state(indoc! {"
4714 «hEllo worldˇ»
4715 "});
4716 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4717 cx.assert_editor_state(indoc! {"
4718 «hello worldˇ»
4719 "});
4720}
4721
4722#[gpui::test]
4723async fn test_manipulate_text(cx: &mut TestAppContext) {
4724 init_test(cx, |_| {});
4725
4726 let mut cx = EditorTestContext::new(cx).await;
4727
4728 // Test convert_to_upper_case()
4729 cx.set_state(indoc! {"
4730 «hello worldˇ»
4731 "});
4732 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4733 cx.assert_editor_state(indoc! {"
4734 «HELLO WORLDˇ»
4735 "});
4736
4737 // Test convert_to_lower_case()
4738 cx.set_state(indoc! {"
4739 «HELLO WORLDˇ»
4740 "});
4741 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4742 cx.assert_editor_state(indoc! {"
4743 «hello worldˇ»
4744 "});
4745
4746 // Test multiple line, single selection case
4747 cx.set_state(indoc! {"
4748 «The quick brown
4749 fox jumps over
4750 the lazy dogˇ»
4751 "});
4752 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4753 cx.assert_editor_state(indoc! {"
4754 «The Quick Brown
4755 Fox Jumps Over
4756 The Lazy Dogˇ»
4757 "});
4758
4759 // Test multiple line, single selection case
4760 cx.set_state(indoc! {"
4761 «The quick brown
4762 fox jumps over
4763 the lazy dogˇ»
4764 "});
4765 cx.update_editor(|e, window, cx| {
4766 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4767 });
4768 cx.assert_editor_state(indoc! {"
4769 «TheQuickBrown
4770 FoxJumpsOver
4771 TheLazyDogˇ»
4772 "});
4773
4774 // From here on out, test more complex cases of manipulate_text()
4775
4776 // Test no selection case - should affect words cursors are in
4777 // Cursor at beginning, middle, and end of word
4778 cx.set_state(indoc! {"
4779 ˇhello big beauˇtiful worldˇ
4780 "});
4781 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4782 cx.assert_editor_state(indoc! {"
4783 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4784 "});
4785
4786 // Test multiple selections on a single line and across multiple lines
4787 cx.set_state(indoc! {"
4788 «Theˇ» quick «brown
4789 foxˇ» jumps «overˇ»
4790 the «lazyˇ» dog
4791 "});
4792 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4793 cx.assert_editor_state(indoc! {"
4794 «THEˇ» quick «BROWN
4795 FOXˇ» jumps «OVERˇ»
4796 the «LAZYˇ» dog
4797 "});
4798
4799 // Test case where text length grows
4800 cx.set_state(indoc! {"
4801 «tschüߡ»
4802 "});
4803 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4804 cx.assert_editor_state(indoc! {"
4805 «TSCHÜSSˇ»
4806 "});
4807
4808 // Test to make sure we don't crash when text shrinks
4809 cx.set_state(indoc! {"
4810 aaa_bbbˇ
4811 "});
4812 cx.update_editor(|e, window, cx| {
4813 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4814 });
4815 cx.assert_editor_state(indoc! {"
4816 «aaaBbbˇ»
4817 "});
4818
4819 // Test to make sure we all aware of the fact that each word can grow and shrink
4820 // Final selections should be aware of this fact
4821 cx.set_state(indoc! {"
4822 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4823 "});
4824 cx.update_editor(|e, window, cx| {
4825 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4826 });
4827 cx.assert_editor_state(indoc! {"
4828 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4829 "});
4830
4831 cx.set_state(indoc! {"
4832 «hElLo, WoRld!ˇ»
4833 "});
4834 cx.update_editor(|e, window, cx| {
4835 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4836 });
4837 cx.assert_editor_state(indoc! {"
4838 «HeLlO, wOrLD!ˇ»
4839 "});
4840}
4841
4842#[gpui::test]
4843fn test_duplicate_line(cx: &mut TestAppContext) {
4844 init_test(cx, |_| {});
4845
4846 let editor = cx.add_window(|window, cx| {
4847 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4848 build_editor(buffer, window, cx)
4849 });
4850 _ = editor.update(cx, |editor, window, cx| {
4851 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4852 s.select_display_ranges([
4853 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4854 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4855 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4856 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4857 ])
4858 });
4859 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4860 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4861 assert_eq!(
4862 editor.selections.display_ranges(cx),
4863 vec![
4864 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4865 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4866 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4867 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4868 ]
4869 );
4870 });
4871
4872 let editor = cx.add_window(|window, cx| {
4873 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4874 build_editor(buffer, window, cx)
4875 });
4876 _ = editor.update(cx, |editor, window, cx| {
4877 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4878 s.select_display_ranges([
4879 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4880 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4881 ])
4882 });
4883 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4884 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4885 assert_eq!(
4886 editor.selections.display_ranges(cx),
4887 vec![
4888 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4889 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4890 ]
4891 );
4892 });
4893
4894 // With `move_upwards` the selections stay in place, except for
4895 // the lines inserted above them
4896 let editor = cx.add_window(|window, cx| {
4897 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4898 build_editor(buffer, window, cx)
4899 });
4900 _ = editor.update(cx, |editor, window, cx| {
4901 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4902 s.select_display_ranges([
4903 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4904 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4905 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4906 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4907 ])
4908 });
4909 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4910 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4911 assert_eq!(
4912 editor.selections.display_ranges(cx),
4913 vec![
4914 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4915 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4916 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4917 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4918 ]
4919 );
4920 });
4921
4922 let editor = cx.add_window(|window, cx| {
4923 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4924 build_editor(buffer, window, cx)
4925 });
4926 _ = editor.update(cx, |editor, window, cx| {
4927 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4928 s.select_display_ranges([
4929 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4930 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4931 ])
4932 });
4933 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4934 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4935 assert_eq!(
4936 editor.selections.display_ranges(cx),
4937 vec![
4938 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4939 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4940 ]
4941 );
4942 });
4943
4944 let editor = cx.add_window(|window, cx| {
4945 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4946 build_editor(buffer, window, cx)
4947 });
4948 _ = editor.update(cx, |editor, window, cx| {
4949 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4950 s.select_display_ranges([
4951 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4952 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4953 ])
4954 });
4955 editor.duplicate_selection(&DuplicateSelection, window, cx);
4956 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4957 assert_eq!(
4958 editor.selections.display_ranges(cx),
4959 vec![
4960 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4961 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4962 ]
4963 );
4964 });
4965}
4966
4967#[gpui::test]
4968fn test_move_line_up_down(cx: &mut TestAppContext) {
4969 init_test(cx, |_| {});
4970
4971 let editor = cx.add_window(|window, cx| {
4972 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4973 build_editor(buffer, window, cx)
4974 });
4975 _ = editor.update(cx, |editor, window, cx| {
4976 editor.fold_creases(
4977 vec![
4978 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4979 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4980 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4981 ],
4982 true,
4983 window,
4984 cx,
4985 );
4986 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4987 s.select_display_ranges([
4988 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4989 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4990 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4991 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4992 ])
4993 });
4994 assert_eq!(
4995 editor.display_text(cx),
4996 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4997 );
4998
4999 editor.move_line_up(&MoveLineUp, window, cx);
5000 assert_eq!(
5001 editor.display_text(cx),
5002 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5003 );
5004 assert_eq!(
5005 editor.selections.display_ranges(cx),
5006 vec![
5007 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5008 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5009 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5010 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5011 ]
5012 );
5013 });
5014
5015 _ = editor.update(cx, |editor, window, cx| {
5016 editor.move_line_down(&MoveLineDown, window, cx);
5017 assert_eq!(
5018 editor.display_text(cx),
5019 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5020 );
5021 assert_eq!(
5022 editor.selections.display_ranges(cx),
5023 vec![
5024 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5025 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5026 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5027 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5028 ]
5029 );
5030 });
5031
5032 _ = editor.update(cx, |editor, window, cx| {
5033 editor.move_line_down(&MoveLineDown, window, cx);
5034 assert_eq!(
5035 editor.display_text(cx),
5036 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5037 );
5038 assert_eq!(
5039 editor.selections.display_ranges(cx),
5040 vec![
5041 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5042 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5043 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5044 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5045 ]
5046 );
5047 });
5048
5049 _ = editor.update(cx, |editor, window, cx| {
5050 editor.move_line_up(&MoveLineUp, window, cx);
5051 assert_eq!(
5052 editor.display_text(cx),
5053 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5054 );
5055 assert_eq!(
5056 editor.selections.display_ranges(cx),
5057 vec![
5058 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5059 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5060 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5061 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5062 ]
5063 );
5064 });
5065}
5066
5067#[gpui::test]
5068fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5069 init_test(cx, |_| {});
5070
5071 let editor = cx.add_window(|window, cx| {
5072 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5073 build_editor(buffer, window, cx)
5074 });
5075 _ = editor.update(cx, |editor, window, cx| {
5076 let snapshot = editor.buffer.read(cx).snapshot(cx);
5077 editor.insert_blocks(
5078 [BlockProperties {
5079 style: BlockStyle::Fixed,
5080 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5081 height: Some(1),
5082 render: Arc::new(|_| div().into_any()),
5083 priority: 0,
5084 render_in_minimap: true,
5085 }],
5086 Some(Autoscroll::fit()),
5087 cx,
5088 );
5089 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5090 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5091 });
5092 editor.move_line_down(&MoveLineDown, window, cx);
5093 });
5094}
5095
5096#[gpui::test]
5097async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5098 init_test(cx, |_| {});
5099
5100 let mut cx = EditorTestContext::new(cx).await;
5101 cx.set_state(
5102 &"
5103 ˇzero
5104 one
5105 two
5106 three
5107 four
5108 five
5109 "
5110 .unindent(),
5111 );
5112
5113 // Create a four-line block that replaces three lines of text.
5114 cx.update_editor(|editor, window, cx| {
5115 let snapshot = editor.snapshot(window, cx);
5116 let snapshot = &snapshot.buffer_snapshot;
5117 let placement = BlockPlacement::Replace(
5118 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5119 );
5120 editor.insert_blocks(
5121 [BlockProperties {
5122 placement,
5123 height: Some(4),
5124 style: BlockStyle::Sticky,
5125 render: Arc::new(|_| gpui::div().into_any_element()),
5126 priority: 0,
5127 render_in_minimap: true,
5128 }],
5129 None,
5130 cx,
5131 );
5132 });
5133
5134 // Move down so that the cursor touches the block.
5135 cx.update_editor(|editor, window, cx| {
5136 editor.move_down(&Default::default(), window, cx);
5137 });
5138 cx.assert_editor_state(
5139 &"
5140 zero
5141 «one
5142 two
5143 threeˇ»
5144 four
5145 five
5146 "
5147 .unindent(),
5148 );
5149
5150 // Move down past the block.
5151 cx.update_editor(|editor, window, cx| {
5152 editor.move_down(&Default::default(), window, cx);
5153 });
5154 cx.assert_editor_state(
5155 &"
5156 zero
5157 one
5158 two
5159 three
5160 ˇfour
5161 five
5162 "
5163 .unindent(),
5164 );
5165}
5166
5167#[gpui::test]
5168fn test_transpose(cx: &mut TestAppContext) {
5169 init_test(cx, |_| {});
5170
5171 _ = cx.add_window(|window, cx| {
5172 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5173 editor.set_style(EditorStyle::default(), window, cx);
5174 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5175 s.select_ranges([1..1])
5176 });
5177 editor.transpose(&Default::default(), window, cx);
5178 assert_eq!(editor.text(cx), "bac");
5179 assert_eq!(editor.selections.ranges(cx), [2..2]);
5180
5181 editor.transpose(&Default::default(), window, cx);
5182 assert_eq!(editor.text(cx), "bca");
5183 assert_eq!(editor.selections.ranges(cx), [3..3]);
5184
5185 editor.transpose(&Default::default(), window, cx);
5186 assert_eq!(editor.text(cx), "bac");
5187 assert_eq!(editor.selections.ranges(cx), [3..3]);
5188
5189 editor
5190 });
5191
5192 _ = cx.add_window(|window, cx| {
5193 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5194 editor.set_style(EditorStyle::default(), window, cx);
5195 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5196 s.select_ranges([3..3])
5197 });
5198 editor.transpose(&Default::default(), window, cx);
5199 assert_eq!(editor.text(cx), "acb\nde");
5200 assert_eq!(editor.selections.ranges(cx), [3..3]);
5201
5202 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5203 s.select_ranges([4..4])
5204 });
5205 editor.transpose(&Default::default(), window, cx);
5206 assert_eq!(editor.text(cx), "acbd\ne");
5207 assert_eq!(editor.selections.ranges(cx), [5..5]);
5208
5209 editor.transpose(&Default::default(), window, cx);
5210 assert_eq!(editor.text(cx), "acbde\n");
5211 assert_eq!(editor.selections.ranges(cx), [6..6]);
5212
5213 editor.transpose(&Default::default(), window, cx);
5214 assert_eq!(editor.text(cx), "acbd\ne");
5215 assert_eq!(editor.selections.ranges(cx), [6..6]);
5216
5217 editor
5218 });
5219
5220 _ = cx.add_window(|window, cx| {
5221 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5222 editor.set_style(EditorStyle::default(), window, cx);
5223 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5224 s.select_ranges([1..1, 2..2, 4..4])
5225 });
5226 editor.transpose(&Default::default(), window, cx);
5227 assert_eq!(editor.text(cx), "bacd\ne");
5228 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5229
5230 editor.transpose(&Default::default(), window, cx);
5231 assert_eq!(editor.text(cx), "bcade\n");
5232 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5233
5234 editor.transpose(&Default::default(), window, cx);
5235 assert_eq!(editor.text(cx), "bcda\ne");
5236 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5237
5238 editor.transpose(&Default::default(), window, cx);
5239 assert_eq!(editor.text(cx), "bcade\n");
5240 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5241
5242 editor.transpose(&Default::default(), window, cx);
5243 assert_eq!(editor.text(cx), "bcaed\n");
5244 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5245
5246 editor
5247 });
5248
5249 _ = cx.add_window(|window, cx| {
5250 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5251 editor.set_style(EditorStyle::default(), window, cx);
5252 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5253 s.select_ranges([4..4])
5254 });
5255 editor.transpose(&Default::default(), window, cx);
5256 assert_eq!(editor.text(cx), "🏀🍐✋");
5257 assert_eq!(editor.selections.ranges(cx), [8..8]);
5258
5259 editor.transpose(&Default::default(), window, cx);
5260 assert_eq!(editor.text(cx), "🏀✋🍐");
5261 assert_eq!(editor.selections.ranges(cx), [11..11]);
5262
5263 editor.transpose(&Default::default(), window, cx);
5264 assert_eq!(editor.text(cx), "🏀🍐✋");
5265 assert_eq!(editor.selections.ranges(cx), [11..11]);
5266
5267 editor
5268 });
5269}
5270
5271#[gpui::test]
5272async fn test_rewrap(cx: &mut TestAppContext) {
5273 init_test(cx, |settings| {
5274 settings.languages.0.extend([
5275 (
5276 "Markdown".into(),
5277 LanguageSettingsContent {
5278 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5279 preferred_line_length: Some(40),
5280 ..Default::default()
5281 },
5282 ),
5283 (
5284 "Plain Text".into(),
5285 LanguageSettingsContent {
5286 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5287 preferred_line_length: Some(40),
5288 ..Default::default()
5289 },
5290 ),
5291 (
5292 "C++".into(),
5293 LanguageSettingsContent {
5294 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5295 preferred_line_length: Some(40),
5296 ..Default::default()
5297 },
5298 ),
5299 (
5300 "Python".into(),
5301 LanguageSettingsContent {
5302 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5303 preferred_line_length: Some(40),
5304 ..Default::default()
5305 },
5306 ),
5307 (
5308 "Rust".into(),
5309 LanguageSettingsContent {
5310 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5311 preferred_line_length: Some(40),
5312 ..Default::default()
5313 },
5314 ),
5315 ])
5316 });
5317
5318 let mut cx = EditorTestContext::new(cx).await;
5319
5320 let cpp_language = Arc::new(Language::new(
5321 LanguageConfig {
5322 name: "C++".into(),
5323 line_comments: vec!["// ".into()],
5324 ..LanguageConfig::default()
5325 },
5326 None,
5327 ));
5328 let python_language = Arc::new(Language::new(
5329 LanguageConfig {
5330 name: "Python".into(),
5331 line_comments: vec!["# ".into()],
5332 ..LanguageConfig::default()
5333 },
5334 None,
5335 ));
5336 let markdown_language = Arc::new(Language::new(
5337 LanguageConfig {
5338 name: "Markdown".into(),
5339 rewrap_prefixes: vec![
5340 regex::Regex::new("\\d+\\.\\s+").unwrap(),
5341 regex::Regex::new("[-*+]\\s+").unwrap(),
5342 ],
5343 ..LanguageConfig::default()
5344 },
5345 None,
5346 ));
5347 let rust_language = Arc::new(Language::new(
5348 LanguageConfig {
5349 name: "Rust".into(),
5350 line_comments: vec!["// ".into(), "/// ".into()],
5351 ..LanguageConfig::default()
5352 },
5353 Some(tree_sitter_rust::LANGUAGE.into()),
5354 ));
5355
5356 let plaintext_language = Arc::new(Language::new(
5357 LanguageConfig {
5358 name: "Plain Text".into(),
5359 ..LanguageConfig::default()
5360 },
5361 None,
5362 ));
5363
5364 // Test basic rewrapping of a long line with a cursor
5365 assert_rewrap(
5366 indoc! {"
5367 // ˇThis is a long comment that needs to be wrapped.
5368 "},
5369 indoc! {"
5370 // ˇThis is a long comment that needs to
5371 // be wrapped.
5372 "},
5373 cpp_language.clone(),
5374 &mut cx,
5375 );
5376
5377 // Test rewrapping a full selection
5378 assert_rewrap(
5379 indoc! {"
5380 «// This selected long comment needs to be wrapped.ˇ»"
5381 },
5382 indoc! {"
5383 «// This selected long comment needs to
5384 // be wrapped.ˇ»"
5385 },
5386 cpp_language.clone(),
5387 &mut cx,
5388 );
5389
5390 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
5391 assert_rewrap(
5392 indoc! {"
5393 // ˇThis is the first line.
5394 // Thisˇ is the second line.
5395 // This is the thirdˇ line, all part of one paragraph.
5396 "},
5397 indoc! {"
5398 // ˇThis is the first line. Thisˇ is the
5399 // second line. This is the thirdˇ line,
5400 // all part of one paragraph.
5401 "},
5402 cpp_language.clone(),
5403 &mut cx,
5404 );
5405
5406 // Test multiple cursors in different paragraphs trigger separate rewraps
5407 assert_rewrap(
5408 indoc! {"
5409 // ˇThis is the first paragraph, first line.
5410 // ˇThis is the first paragraph, second line.
5411
5412 // ˇThis is the second paragraph, first line.
5413 // ˇThis is the second paragraph, second line.
5414 "},
5415 indoc! {"
5416 // ˇThis is the first paragraph, first
5417 // line. ˇThis is the first paragraph,
5418 // second line.
5419
5420 // ˇThis is the second paragraph, first
5421 // line. ˇThis is the second paragraph,
5422 // second line.
5423 "},
5424 cpp_language.clone(),
5425 &mut cx,
5426 );
5427
5428 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
5429 assert_rewrap(
5430 indoc! {"
5431 «// A regular long long comment to be wrapped.
5432 /// A documentation long comment to be wrapped.ˇ»
5433 "},
5434 indoc! {"
5435 «// A regular long long comment to be
5436 // wrapped.
5437 /// A documentation long comment to be
5438 /// wrapped.ˇ»
5439 "},
5440 rust_language.clone(),
5441 &mut cx,
5442 );
5443
5444 // Test that change in indentation level trigger seperate rewraps
5445 assert_rewrap(
5446 indoc! {"
5447 fn foo() {
5448 «// This is a long comment at the base indent.
5449 // This is a long comment at the next indent.ˇ»
5450 }
5451 "},
5452 indoc! {"
5453 fn foo() {
5454 «// This is a long comment at the
5455 // base indent.
5456 // This is a long comment at the
5457 // next indent.ˇ»
5458 }
5459 "},
5460 rust_language.clone(),
5461 &mut cx,
5462 );
5463
5464 // Test that different comment prefix characters (e.g., '#') are handled correctly
5465 assert_rewrap(
5466 indoc! {"
5467 # ˇThis is a long comment using a pound sign.
5468 "},
5469 indoc! {"
5470 # ˇThis is a long comment using a pound
5471 # sign.
5472 "},
5473 python_language.clone(),
5474 &mut cx,
5475 );
5476
5477 // Test rewrapping only affects comments, not code even when selected
5478 assert_rewrap(
5479 indoc! {"
5480 «/// This doc comment is long and should be wrapped.
5481 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5482 "},
5483 indoc! {"
5484 «/// This doc comment is long and should
5485 /// be wrapped.
5486 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5487 "},
5488 rust_language.clone(),
5489 &mut cx,
5490 );
5491
5492 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
5493 assert_rewrap(
5494 indoc! {"
5495 # Header
5496
5497 A long long long line of markdown text to wrap.ˇ
5498 "},
5499 indoc! {"
5500 # Header
5501
5502 A long long long line of markdown text
5503 to wrap.ˇ
5504 "},
5505 markdown_language.clone(),
5506 &mut cx,
5507 );
5508
5509 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
5510 assert_rewrap(
5511 indoc! {"
5512 «1. This is a numbered list item that is very long and needs to be wrapped properly.
5513 2. This is a numbered list item that is very long and needs to be wrapped properly.
5514 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
5515 "},
5516 indoc! {"
5517 «1. This is a numbered list item that is
5518 very long and needs to be wrapped
5519 properly.
5520 2. This is a numbered list item that is
5521 very long and needs to be wrapped
5522 properly.
5523 - This is an unordered list item that is
5524 also very long and should not merge
5525 with the numbered item.ˇ»
5526 "},
5527 markdown_language.clone(),
5528 &mut cx,
5529 );
5530
5531 // Test that rewrapping add indents for rewrapping boundary if not exists already.
5532 assert_rewrap(
5533 indoc! {"
5534 «1. This is a numbered list item that is
5535 very long and needs to be wrapped
5536 properly.
5537 2. This is a numbered list item that is
5538 very long and needs to be wrapped
5539 properly.
5540 - This is an unordered list item that is
5541 also very long and should not merge with
5542 the numbered item.ˇ»
5543 "},
5544 indoc! {"
5545 «1. This is a numbered list item that is
5546 very long and needs to be wrapped
5547 properly.
5548 2. This is a numbered list item that is
5549 very long and needs to be wrapped
5550 properly.
5551 - This is an unordered list item that is
5552 also very long and should not merge
5553 with the numbered item.ˇ»
5554 "},
5555 markdown_language.clone(),
5556 &mut cx,
5557 );
5558
5559 // Test that rewrapping maintain indents even when they already exists.
5560 assert_rewrap(
5561 indoc! {"
5562 «1. This is a numbered list
5563 item that is very long and needs to be wrapped properly.
5564 2. This is a numbered list
5565 item that is very long and needs to be wrapped properly.
5566 - This is an unordered list item that is also very long and
5567 should not merge with the numbered item.ˇ»
5568 "},
5569 indoc! {"
5570 «1. This is a numbered list item that is
5571 very long and needs to be wrapped
5572 properly.
5573 2. This is a numbered list item that is
5574 very long and needs to be wrapped
5575 properly.
5576 - This is an unordered list item that is
5577 also very long and should not merge
5578 with the numbered item.ˇ»
5579 "},
5580 markdown_language.clone(),
5581 &mut cx,
5582 );
5583
5584 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
5585 assert_rewrap(
5586 indoc! {"
5587 ˇThis is a very long line of plain text that will be wrapped.
5588 "},
5589 indoc! {"
5590 ˇThis is a very long line of plain text
5591 that will be wrapped.
5592 "},
5593 plaintext_language.clone(),
5594 &mut cx,
5595 );
5596
5597 // Test that non-commented code acts as a paragraph boundary within a selection
5598 assert_rewrap(
5599 indoc! {"
5600 «// This is the first long comment block to be wrapped.
5601 fn my_func(a: u32);
5602 // This is the second long comment block to be wrapped.ˇ»
5603 "},
5604 indoc! {"
5605 «// This is the first long comment block
5606 // to be wrapped.
5607 fn my_func(a: u32);
5608 // This is the second long comment block
5609 // to be wrapped.ˇ»
5610 "},
5611 rust_language.clone(),
5612 &mut cx,
5613 );
5614
5615 // Test rewrapping multiple selections, including ones with blank lines or tabs
5616 assert_rewrap(
5617 indoc! {"
5618 «ˇThis is a very long line that will be wrapped.
5619
5620 This is another paragraph in the same selection.»
5621
5622 «\tThis is a very long indented line that will be wrapped.ˇ»
5623 "},
5624 indoc! {"
5625 «ˇThis is a very long line that will be
5626 wrapped.
5627
5628 This is another paragraph in the same
5629 selection.»
5630
5631 «\tThis is a very long indented line
5632 \tthat will be wrapped.ˇ»
5633 "},
5634 plaintext_language.clone(),
5635 &mut cx,
5636 );
5637
5638 // Test that an empty comment line acts as a paragraph boundary
5639 assert_rewrap(
5640 indoc! {"
5641 // ˇThis is a long comment that will be wrapped.
5642 //
5643 // And this is another long comment that will also be wrapped.ˇ
5644 "},
5645 indoc! {"
5646 // ˇThis is a long comment that will be
5647 // wrapped.
5648 //
5649 // And this is another long comment that
5650 // will also be wrapped.ˇ
5651 "},
5652 cpp_language,
5653 &mut cx,
5654 );
5655
5656 #[track_caller]
5657 fn assert_rewrap(
5658 unwrapped_text: &str,
5659 wrapped_text: &str,
5660 language: Arc<Language>,
5661 cx: &mut EditorTestContext,
5662 ) {
5663 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5664 cx.set_state(unwrapped_text);
5665 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5666 cx.assert_editor_state(wrapped_text);
5667 }
5668}
5669
5670#[gpui::test]
5671async fn test_hard_wrap(cx: &mut TestAppContext) {
5672 init_test(cx, |_| {});
5673 let mut cx = EditorTestContext::new(cx).await;
5674
5675 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5676 cx.update_editor(|editor, _, cx| {
5677 editor.set_hard_wrap(Some(14), cx);
5678 });
5679
5680 cx.set_state(indoc!(
5681 "
5682 one two three ˇ
5683 "
5684 ));
5685 cx.simulate_input("four");
5686 cx.run_until_parked();
5687
5688 cx.assert_editor_state(indoc!(
5689 "
5690 one two three
5691 fourˇ
5692 "
5693 ));
5694
5695 cx.update_editor(|editor, window, cx| {
5696 editor.newline(&Default::default(), window, cx);
5697 });
5698 cx.run_until_parked();
5699 cx.assert_editor_state(indoc!(
5700 "
5701 one two three
5702 four
5703 ˇ
5704 "
5705 ));
5706
5707 cx.simulate_input("five");
5708 cx.run_until_parked();
5709 cx.assert_editor_state(indoc!(
5710 "
5711 one two three
5712 four
5713 fiveˇ
5714 "
5715 ));
5716
5717 cx.update_editor(|editor, window, cx| {
5718 editor.newline(&Default::default(), window, cx);
5719 });
5720 cx.run_until_parked();
5721 cx.simulate_input("# ");
5722 cx.run_until_parked();
5723 cx.assert_editor_state(indoc!(
5724 "
5725 one two three
5726 four
5727 five
5728 # ˇ
5729 "
5730 ));
5731
5732 cx.update_editor(|editor, window, cx| {
5733 editor.newline(&Default::default(), window, cx);
5734 });
5735 cx.run_until_parked();
5736 cx.assert_editor_state(indoc!(
5737 "
5738 one two three
5739 four
5740 five
5741 #\x20
5742 #ˇ
5743 "
5744 ));
5745
5746 cx.simulate_input(" 6");
5747 cx.run_until_parked();
5748 cx.assert_editor_state(indoc!(
5749 "
5750 one two three
5751 four
5752 five
5753 #
5754 # 6ˇ
5755 "
5756 ));
5757}
5758
5759#[gpui::test]
5760async fn test_clipboard(cx: &mut TestAppContext) {
5761 init_test(cx, |_| {});
5762
5763 let mut cx = EditorTestContext::new(cx).await;
5764
5765 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5766 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5767 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5768
5769 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5770 cx.set_state("two ˇfour ˇsix ˇ");
5771 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5772 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5773
5774 // Paste again but with only two cursors. Since the number of cursors doesn't
5775 // match the number of slices in the clipboard, the entire clipboard text
5776 // is pasted at each cursor.
5777 cx.set_state("ˇtwo one✅ four three six five ˇ");
5778 cx.update_editor(|e, window, cx| {
5779 e.handle_input("( ", window, cx);
5780 e.paste(&Paste, window, cx);
5781 e.handle_input(") ", window, cx);
5782 });
5783 cx.assert_editor_state(
5784 &([
5785 "( one✅ ",
5786 "three ",
5787 "five ) ˇtwo one✅ four three six five ( one✅ ",
5788 "three ",
5789 "five ) ˇ",
5790 ]
5791 .join("\n")),
5792 );
5793
5794 // Cut with three selections, one of which is full-line.
5795 cx.set_state(indoc! {"
5796 1«2ˇ»3
5797 4ˇ567
5798 «8ˇ»9"});
5799 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5800 cx.assert_editor_state(indoc! {"
5801 1ˇ3
5802 ˇ9"});
5803
5804 // Paste with three selections, noticing how the copied selection that was full-line
5805 // gets inserted before the second cursor.
5806 cx.set_state(indoc! {"
5807 1ˇ3
5808 9ˇ
5809 «oˇ»ne"});
5810 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5811 cx.assert_editor_state(indoc! {"
5812 12ˇ3
5813 4567
5814 9ˇ
5815 8ˇne"});
5816
5817 // Copy with a single cursor only, which writes the whole line into the clipboard.
5818 cx.set_state(indoc! {"
5819 The quick brown
5820 fox juˇmps over
5821 the lazy dog"});
5822 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5823 assert_eq!(
5824 cx.read_from_clipboard()
5825 .and_then(|item| item.text().as_deref().map(str::to_string)),
5826 Some("fox jumps over\n".to_string())
5827 );
5828
5829 // Paste with three selections, noticing how the copied full-line selection is inserted
5830 // before the empty selections but replaces the selection that is non-empty.
5831 cx.set_state(indoc! {"
5832 Tˇhe quick brown
5833 «foˇ»x jumps over
5834 tˇhe lazy dog"});
5835 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5836 cx.assert_editor_state(indoc! {"
5837 fox jumps over
5838 Tˇhe quick brown
5839 fox jumps over
5840 ˇx jumps over
5841 fox jumps over
5842 tˇhe lazy dog"});
5843}
5844
5845#[gpui::test]
5846async fn test_copy_trim(cx: &mut TestAppContext) {
5847 init_test(cx, |_| {});
5848
5849 let mut cx = EditorTestContext::new(cx).await;
5850 cx.set_state(
5851 r#" «for selection in selections.iter() {
5852 let mut start = selection.start;
5853 let mut end = selection.end;
5854 let is_entire_line = selection.is_empty();
5855 if is_entire_line {
5856 start = Point::new(start.row, 0);ˇ»
5857 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5858 }
5859 "#,
5860 );
5861 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5862 assert_eq!(
5863 cx.read_from_clipboard()
5864 .and_then(|item| item.text().as_deref().map(str::to_string)),
5865 Some(
5866 "for selection in selections.iter() {
5867 let mut start = selection.start;
5868 let mut end = selection.end;
5869 let is_entire_line = selection.is_empty();
5870 if is_entire_line {
5871 start = Point::new(start.row, 0);"
5872 .to_string()
5873 ),
5874 "Regular copying preserves all indentation selected",
5875 );
5876 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5877 assert_eq!(
5878 cx.read_from_clipboard()
5879 .and_then(|item| item.text().as_deref().map(str::to_string)),
5880 Some(
5881 "for selection in selections.iter() {
5882let mut start = selection.start;
5883let mut end = selection.end;
5884let is_entire_line = selection.is_empty();
5885if is_entire_line {
5886 start = Point::new(start.row, 0);"
5887 .to_string()
5888 ),
5889 "Copying with stripping should strip all leading whitespaces"
5890 );
5891
5892 cx.set_state(
5893 r#" « for selection in selections.iter() {
5894 let mut start = selection.start;
5895 let mut end = selection.end;
5896 let is_entire_line = selection.is_empty();
5897 if is_entire_line {
5898 start = Point::new(start.row, 0);ˇ»
5899 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5900 }
5901 "#,
5902 );
5903 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5904 assert_eq!(
5905 cx.read_from_clipboard()
5906 .and_then(|item| item.text().as_deref().map(str::to_string)),
5907 Some(
5908 " for selection in selections.iter() {
5909 let mut start = selection.start;
5910 let mut end = selection.end;
5911 let is_entire_line = selection.is_empty();
5912 if is_entire_line {
5913 start = Point::new(start.row, 0);"
5914 .to_string()
5915 ),
5916 "Regular copying preserves all indentation selected",
5917 );
5918 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5919 assert_eq!(
5920 cx.read_from_clipboard()
5921 .and_then(|item| item.text().as_deref().map(str::to_string)),
5922 Some(
5923 "for selection in selections.iter() {
5924let mut start = selection.start;
5925let mut end = selection.end;
5926let is_entire_line = selection.is_empty();
5927if is_entire_line {
5928 start = Point::new(start.row, 0);"
5929 .to_string()
5930 ),
5931 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5932 );
5933
5934 cx.set_state(
5935 r#" «ˇ for selection in selections.iter() {
5936 let mut start = selection.start;
5937 let mut end = selection.end;
5938 let is_entire_line = selection.is_empty();
5939 if is_entire_line {
5940 start = Point::new(start.row, 0);»
5941 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5942 }
5943 "#,
5944 );
5945 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5946 assert_eq!(
5947 cx.read_from_clipboard()
5948 .and_then(|item| item.text().as_deref().map(str::to_string)),
5949 Some(
5950 " for selection in selections.iter() {
5951 let mut start = selection.start;
5952 let mut end = selection.end;
5953 let is_entire_line = selection.is_empty();
5954 if is_entire_line {
5955 start = Point::new(start.row, 0);"
5956 .to_string()
5957 ),
5958 "Regular copying for reverse selection works the same",
5959 );
5960 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5961 assert_eq!(
5962 cx.read_from_clipboard()
5963 .and_then(|item| item.text().as_deref().map(str::to_string)),
5964 Some(
5965 "for selection in selections.iter() {
5966let mut start = selection.start;
5967let mut end = selection.end;
5968let is_entire_line = selection.is_empty();
5969if is_entire_line {
5970 start = Point::new(start.row, 0);"
5971 .to_string()
5972 ),
5973 "Copying with stripping for reverse selection works the same"
5974 );
5975
5976 cx.set_state(
5977 r#" for selection «in selections.iter() {
5978 let mut start = selection.start;
5979 let mut end = selection.end;
5980 let is_entire_line = selection.is_empty();
5981 if is_entire_line {
5982 start = Point::new(start.row, 0);ˇ»
5983 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5984 }
5985 "#,
5986 );
5987 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5988 assert_eq!(
5989 cx.read_from_clipboard()
5990 .and_then(|item| item.text().as_deref().map(str::to_string)),
5991 Some(
5992 "in selections.iter() {
5993 let mut start = selection.start;
5994 let mut end = selection.end;
5995 let is_entire_line = selection.is_empty();
5996 if is_entire_line {
5997 start = Point::new(start.row, 0);"
5998 .to_string()
5999 ),
6000 "When selecting past the indent, the copying works as usual",
6001 );
6002 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6003 assert_eq!(
6004 cx.read_from_clipboard()
6005 .and_then(|item| item.text().as_deref().map(str::to_string)),
6006 Some(
6007 "in selections.iter() {
6008 let mut start = selection.start;
6009 let mut end = selection.end;
6010 let is_entire_line = selection.is_empty();
6011 if is_entire_line {
6012 start = Point::new(start.row, 0);"
6013 .to_string()
6014 ),
6015 "When selecting past the indent, nothing is trimmed"
6016 );
6017
6018 cx.set_state(
6019 r#" «for selection in selections.iter() {
6020 let mut start = selection.start;
6021
6022 let mut end = selection.end;
6023 let is_entire_line = selection.is_empty();
6024 if is_entire_line {
6025 start = Point::new(start.row, 0);
6026ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
6027 }
6028 "#,
6029 );
6030 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6031 assert_eq!(
6032 cx.read_from_clipboard()
6033 .and_then(|item| item.text().as_deref().map(str::to_string)),
6034 Some(
6035 "for selection in selections.iter() {
6036let mut start = selection.start;
6037
6038let mut end = selection.end;
6039let is_entire_line = selection.is_empty();
6040if is_entire_line {
6041 start = Point::new(start.row, 0);
6042"
6043 .to_string()
6044 ),
6045 "Copying with stripping should ignore empty lines"
6046 );
6047}
6048
6049#[gpui::test]
6050async fn test_paste_multiline(cx: &mut TestAppContext) {
6051 init_test(cx, |_| {});
6052
6053 let mut cx = EditorTestContext::new(cx).await;
6054 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6055
6056 // Cut an indented block, without the leading whitespace.
6057 cx.set_state(indoc! {"
6058 const a: B = (
6059 c(),
6060 «d(
6061 e,
6062 f
6063 )ˇ»
6064 );
6065 "});
6066 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6067 cx.assert_editor_state(indoc! {"
6068 const a: B = (
6069 c(),
6070 ˇ
6071 );
6072 "});
6073
6074 // Paste it at the same position.
6075 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6076 cx.assert_editor_state(indoc! {"
6077 const a: B = (
6078 c(),
6079 d(
6080 e,
6081 f
6082 )ˇ
6083 );
6084 "});
6085
6086 // Paste it at a line with a lower indent level.
6087 cx.set_state(indoc! {"
6088 ˇ
6089 const a: B = (
6090 c(),
6091 );
6092 "});
6093 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6094 cx.assert_editor_state(indoc! {"
6095 d(
6096 e,
6097 f
6098 )ˇ
6099 const a: B = (
6100 c(),
6101 );
6102 "});
6103
6104 // Cut an indented block, with the leading whitespace.
6105 cx.set_state(indoc! {"
6106 const a: B = (
6107 c(),
6108 « d(
6109 e,
6110 f
6111 )
6112 ˇ»);
6113 "});
6114 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6115 cx.assert_editor_state(indoc! {"
6116 const a: B = (
6117 c(),
6118 ˇ);
6119 "});
6120
6121 // Paste it at the same position.
6122 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6123 cx.assert_editor_state(indoc! {"
6124 const a: B = (
6125 c(),
6126 d(
6127 e,
6128 f
6129 )
6130 ˇ);
6131 "});
6132
6133 // Paste it at a line with a higher indent level.
6134 cx.set_state(indoc! {"
6135 const a: B = (
6136 c(),
6137 d(
6138 e,
6139 fˇ
6140 )
6141 );
6142 "});
6143 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6144 cx.assert_editor_state(indoc! {"
6145 const a: B = (
6146 c(),
6147 d(
6148 e,
6149 f d(
6150 e,
6151 f
6152 )
6153 ˇ
6154 )
6155 );
6156 "});
6157
6158 // Copy an indented block, starting mid-line
6159 cx.set_state(indoc! {"
6160 const a: B = (
6161 c(),
6162 somethin«g(
6163 e,
6164 f
6165 )ˇ»
6166 );
6167 "});
6168 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6169
6170 // Paste it on a line with a lower indent level
6171 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
6172 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6173 cx.assert_editor_state(indoc! {"
6174 const a: B = (
6175 c(),
6176 something(
6177 e,
6178 f
6179 )
6180 );
6181 g(
6182 e,
6183 f
6184 )ˇ"});
6185}
6186
6187#[gpui::test]
6188async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
6189 init_test(cx, |_| {});
6190
6191 cx.write_to_clipboard(ClipboardItem::new_string(
6192 " d(\n e\n );\n".into(),
6193 ));
6194
6195 let mut cx = EditorTestContext::new(cx).await;
6196 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6197
6198 cx.set_state(indoc! {"
6199 fn a() {
6200 b();
6201 if c() {
6202 ˇ
6203 }
6204 }
6205 "});
6206
6207 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6208 cx.assert_editor_state(indoc! {"
6209 fn a() {
6210 b();
6211 if c() {
6212 d(
6213 e
6214 );
6215 ˇ
6216 }
6217 }
6218 "});
6219
6220 cx.set_state(indoc! {"
6221 fn a() {
6222 b();
6223 ˇ
6224 }
6225 "});
6226
6227 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6228 cx.assert_editor_state(indoc! {"
6229 fn a() {
6230 b();
6231 d(
6232 e
6233 );
6234 ˇ
6235 }
6236 "});
6237}
6238
6239#[gpui::test]
6240fn test_select_all(cx: &mut TestAppContext) {
6241 init_test(cx, |_| {});
6242
6243 let editor = cx.add_window(|window, cx| {
6244 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
6245 build_editor(buffer, window, cx)
6246 });
6247 _ = editor.update(cx, |editor, window, cx| {
6248 editor.select_all(&SelectAll, window, cx);
6249 assert_eq!(
6250 editor.selections.display_ranges(cx),
6251 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
6252 );
6253 });
6254}
6255
6256#[gpui::test]
6257fn test_select_line(cx: &mut TestAppContext) {
6258 init_test(cx, |_| {});
6259
6260 let editor = cx.add_window(|window, cx| {
6261 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
6262 build_editor(buffer, window, cx)
6263 });
6264 _ = editor.update(cx, |editor, window, cx| {
6265 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6266 s.select_display_ranges([
6267 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6268 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6269 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6270 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
6271 ])
6272 });
6273 editor.select_line(&SelectLine, window, cx);
6274 assert_eq!(
6275 editor.selections.display_ranges(cx),
6276 vec![
6277 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
6278 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
6279 ]
6280 );
6281 });
6282
6283 _ = editor.update(cx, |editor, window, cx| {
6284 editor.select_line(&SelectLine, window, cx);
6285 assert_eq!(
6286 editor.selections.display_ranges(cx),
6287 vec![
6288 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
6289 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
6290 ]
6291 );
6292 });
6293
6294 _ = editor.update(cx, |editor, window, cx| {
6295 editor.select_line(&SelectLine, window, cx);
6296 assert_eq!(
6297 editor.selections.display_ranges(cx),
6298 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
6299 );
6300 });
6301}
6302
6303#[gpui::test]
6304async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
6305 init_test(cx, |_| {});
6306 let mut cx = EditorTestContext::new(cx).await;
6307
6308 #[track_caller]
6309 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
6310 cx.set_state(initial_state);
6311 cx.update_editor(|e, window, cx| {
6312 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
6313 });
6314 cx.assert_editor_state(expected_state);
6315 }
6316
6317 // Selection starts and ends at the middle of lines, left-to-right
6318 test(
6319 &mut cx,
6320 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
6321 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6322 );
6323 // Same thing, right-to-left
6324 test(
6325 &mut cx,
6326 "aa\nb«b\ncc\ndd\neˇ»e\nff",
6327 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6328 );
6329
6330 // Whole buffer, left-to-right, last line *doesn't* end with newline
6331 test(
6332 &mut cx,
6333 "«ˇaa\nbb\ncc\ndd\nee\nff»",
6334 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6335 );
6336 // Same thing, right-to-left
6337 test(
6338 &mut cx,
6339 "«aa\nbb\ncc\ndd\nee\nffˇ»",
6340 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6341 );
6342
6343 // Whole buffer, left-to-right, last line ends with newline
6344 test(
6345 &mut cx,
6346 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
6347 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6348 );
6349 // Same thing, right-to-left
6350 test(
6351 &mut cx,
6352 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
6353 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6354 );
6355
6356 // Starts at the end of a line, ends at the start of another
6357 test(
6358 &mut cx,
6359 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
6360 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
6361 );
6362}
6363
6364#[gpui::test]
6365async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
6366 init_test(cx, |_| {});
6367
6368 let editor = cx.add_window(|window, cx| {
6369 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
6370 build_editor(buffer, window, cx)
6371 });
6372
6373 // setup
6374 _ = editor.update(cx, |editor, window, cx| {
6375 editor.fold_creases(
6376 vec![
6377 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
6378 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
6379 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
6380 ],
6381 true,
6382 window,
6383 cx,
6384 );
6385 assert_eq!(
6386 editor.display_text(cx),
6387 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6388 );
6389 });
6390
6391 _ = editor.update(cx, |editor, window, cx| {
6392 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6393 s.select_display_ranges([
6394 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6395 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6396 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6397 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
6398 ])
6399 });
6400 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6401 assert_eq!(
6402 editor.display_text(cx),
6403 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6404 );
6405 });
6406 EditorTestContext::for_editor(editor, cx)
6407 .await
6408 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
6409
6410 _ = editor.update(cx, |editor, window, cx| {
6411 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6412 s.select_display_ranges([
6413 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
6414 ])
6415 });
6416 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6417 assert_eq!(
6418 editor.display_text(cx),
6419 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
6420 );
6421 assert_eq!(
6422 editor.selections.display_ranges(cx),
6423 [
6424 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
6425 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
6426 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
6427 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
6428 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
6429 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
6430 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
6431 ]
6432 );
6433 });
6434 EditorTestContext::for_editor(editor, cx)
6435 .await
6436 .assert_editor_state(
6437 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
6438 );
6439}
6440
6441#[gpui::test]
6442async fn test_add_selection_above_below(cx: &mut TestAppContext) {
6443 init_test(cx, |_| {});
6444
6445 let mut cx = EditorTestContext::new(cx).await;
6446
6447 cx.set_state(indoc!(
6448 r#"abc
6449 defˇghi
6450
6451 jk
6452 nlmo
6453 "#
6454 ));
6455
6456 cx.update_editor(|editor, window, cx| {
6457 editor.add_selection_above(&Default::default(), window, cx);
6458 });
6459
6460 cx.assert_editor_state(indoc!(
6461 r#"abcˇ
6462 defˇghi
6463
6464 jk
6465 nlmo
6466 "#
6467 ));
6468
6469 cx.update_editor(|editor, window, cx| {
6470 editor.add_selection_above(&Default::default(), window, cx);
6471 });
6472
6473 cx.assert_editor_state(indoc!(
6474 r#"abcˇ
6475 defˇghi
6476
6477 jk
6478 nlmo
6479 "#
6480 ));
6481
6482 cx.update_editor(|editor, window, cx| {
6483 editor.add_selection_below(&Default::default(), window, cx);
6484 });
6485
6486 cx.assert_editor_state(indoc!(
6487 r#"abc
6488 defˇghi
6489
6490 jk
6491 nlmo
6492 "#
6493 ));
6494
6495 cx.update_editor(|editor, window, cx| {
6496 editor.undo_selection(&Default::default(), window, cx);
6497 });
6498
6499 cx.assert_editor_state(indoc!(
6500 r#"abcˇ
6501 defˇghi
6502
6503 jk
6504 nlmo
6505 "#
6506 ));
6507
6508 cx.update_editor(|editor, window, cx| {
6509 editor.redo_selection(&Default::default(), window, cx);
6510 });
6511
6512 cx.assert_editor_state(indoc!(
6513 r#"abc
6514 defˇghi
6515
6516 jk
6517 nlmo
6518 "#
6519 ));
6520
6521 cx.update_editor(|editor, window, cx| {
6522 editor.add_selection_below(&Default::default(), window, cx);
6523 });
6524
6525 cx.assert_editor_state(indoc!(
6526 r#"abc
6527 defˇghi
6528 ˇ
6529 jk
6530 nlmo
6531 "#
6532 ));
6533
6534 cx.update_editor(|editor, window, cx| {
6535 editor.add_selection_below(&Default::default(), window, cx);
6536 });
6537
6538 cx.assert_editor_state(indoc!(
6539 r#"abc
6540 defˇghi
6541 ˇ
6542 jkˇ
6543 nlmo
6544 "#
6545 ));
6546
6547 cx.update_editor(|editor, window, cx| {
6548 editor.add_selection_below(&Default::default(), window, cx);
6549 });
6550
6551 cx.assert_editor_state(indoc!(
6552 r#"abc
6553 defˇghi
6554 ˇ
6555 jkˇ
6556 nlmˇo
6557 "#
6558 ));
6559
6560 cx.update_editor(|editor, window, cx| {
6561 editor.add_selection_below(&Default::default(), window, cx);
6562 });
6563
6564 cx.assert_editor_state(indoc!(
6565 r#"abc
6566 defˇghi
6567 ˇ
6568 jkˇ
6569 nlmˇo
6570 ˇ"#
6571 ));
6572
6573 // change selections
6574 cx.set_state(indoc!(
6575 r#"abc
6576 def«ˇg»hi
6577
6578 jk
6579 nlmo
6580 "#
6581 ));
6582
6583 cx.update_editor(|editor, window, cx| {
6584 editor.add_selection_below(&Default::default(), window, cx);
6585 });
6586
6587 cx.assert_editor_state(indoc!(
6588 r#"abc
6589 def«ˇg»hi
6590
6591 jk
6592 nlm«ˇo»
6593 "#
6594 ));
6595
6596 cx.update_editor(|editor, window, cx| {
6597 editor.add_selection_below(&Default::default(), window, cx);
6598 });
6599
6600 cx.assert_editor_state(indoc!(
6601 r#"abc
6602 def«ˇg»hi
6603
6604 jk
6605 nlm«ˇo»
6606 "#
6607 ));
6608
6609 cx.update_editor(|editor, window, cx| {
6610 editor.add_selection_above(&Default::default(), window, cx);
6611 });
6612
6613 cx.assert_editor_state(indoc!(
6614 r#"abc
6615 def«ˇg»hi
6616
6617 jk
6618 nlmo
6619 "#
6620 ));
6621
6622 cx.update_editor(|editor, window, cx| {
6623 editor.add_selection_above(&Default::default(), window, cx);
6624 });
6625
6626 cx.assert_editor_state(indoc!(
6627 r#"abc
6628 def«ˇg»hi
6629
6630 jk
6631 nlmo
6632 "#
6633 ));
6634
6635 // Change selections again
6636 cx.set_state(indoc!(
6637 r#"a«bc
6638 defgˇ»hi
6639
6640 jk
6641 nlmo
6642 "#
6643 ));
6644
6645 cx.update_editor(|editor, window, cx| {
6646 editor.add_selection_below(&Default::default(), window, cx);
6647 });
6648
6649 cx.assert_editor_state(indoc!(
6650 r#"a«bcˇ»
6651 d«efgˇ»hi
6652
6653 j«kˇ»
6654 nlmo
6655 "#
6656 ));
6657
6658 cx.update_editor(|editor, window, cx| {
6659 editor.add_selection_below(&Default::default(), window, cx);
6660 });
6661 cx.assert_editor_state(indoc!(
6662 r#"a«bcˇ»
6663 d«efgˇ»hi
6664
6665 j«kˇ»
6666 n«lmoˇ»
6667 "#
6668 ));
6669 cx.update_editor(|editor, window, cx| {
6670 editor.add_selection_above(&Default::default(), window, cx);
6671 });
6672
6673 cx.assert_editor_state(indoc!(
6674 r#"a«bcˇ»
6675 d«efgˇ»hi
6676
6677 j«kˇ»
6678 nlmo
6679 "#
6680 ));
6681
6682 // Change selections again
6683 cx.set_state(indoc!(
6684 r#"abc
6685 d«ˇefghi
6686
6687 jk
6688 nlm»o
6689 "#
6690 ));
6691
6692 cx.update_editor(|editor, window, cx| {
6693 editor.add_selection_above(&Default::default(), window, cx);
6694 });
6695
6696 cx.assert_editor_state(indoc!(
6697 r#"a«ˇbc»
6698 d«ˇef»ghi
6699
6700 j«ˇk»
6701 n«ˇlm»o
6702 "#
6703 ));
6704
6705 cx.update_editor(|editor, window, cx| {
6706 editor.add_selection_below(&Default::default(), window, cx);
6707 });
6708
6709 cx.assert_editor_state(indoc!(
6710 r#"abc
6711 d«ˇef»ghi
6712
6713 j«ˇk»
6714 n«ˇlm»o
6715 "#
6716 ));
6717}
6718
6719#[gpui::test]
6720async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
6721 init_test(cx, |_| {});
6722 let mut cx = EditorTestContext::new(cx).await;
6723
6724 cx.set_state(indoc!(
6725 r#"line onˇe
6726 liˇne two
6727 line three
6728 line four"#
6729 ));
6730
6731 cx.update_editor(|editor, window, cx| {
6732 editor.add_selection_below(&Default::default(), window, cx);
6733 });
6734
6735 // test multiple cursors expand in the same direction
6736 cx.assert_editor_state(indoc!(
6737 r#"line onˇe
6738 liˇne twˇo
6739 liˇne three
6740 line four"#
6741 ));
6742
6743 cx.update_editor(|editor, window, cx| {
6744 editor.add_selection_below(&Default::default(), window, cx);
6745 });
6746
6747 cx.update_editor(|editor, window, cx| {
6748 editor.add_selection_below(&Default::default(), window, cx);
6749 });
6750
6751 // test multiple cursors expand below overflow
6752 cx.assert_editor_state(indoc!(
6753 r#"line onˇe
6754 liˇne twˇo
6755 liˇne thˇree
6756 liˇne foˇur"#
6757 ));
6758
6759 cx.update_editor(|editor, window, cx| {
6760 editor.add_selection_above(&Default::default(), window, cx);
6761 });
6762
6763 // test multiple cursors retrieves back correctly
6764 cx.assert_editor_state(indoc!(
6765 r#"line onˇe
6766 liˇne twˇo
6767 liˇne thˇree
6768 line four"#
6769 ));
6770
6771 cx.update_editor(|editor, window, cx| {
6772 editor.add_selection_above(&Default::default(), window, cx);
6773 });
6774
6775 cx.update_editor(|editor, window, cx| {
6776 editor.add_selection_above(&Default::default(), window, cx);
6777 });
6778
6779 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
6780 cx.assert_editor_state(indoc!(
6781 r#"liˇne onˇe
6782 liˇne two
6783 line three
6784 line four"#
6785 ));
6786
6787 cx.update_editor(|editor, window, cx| {
6788 editor.undo_selection(&Default::default(), window, cx);
6789 });
6790
6791 // test undo
6792 cx.assert_editor_state(indoc!(
6793 r#"line onˇe
6794 liˇne twˇo
6795 line three
6796 line four"#
6797 ));
6798
6799 cx.update_editor(|editor, window, cx| {
6800 editor.redo_selection(&Default::default(), window, cx);
6801 });
6802
6803 // test redo
6804 cx.assert_editor_state(indoc!(
6805 r#"liˇne onˇe
6806 liˇne two
6807 line three
6808 line four"#
6809 ));
6810
6811 cx.set_state(indoc!(
6812 r#"abcd
6813 ef«ghˇ»
6814 ijkl
6815 «mˇ»nop"#
6816 ));
6817
6818 cx.update_editor(|editor, window, cx| {
6819 editor.add_selection_above(&Default::default(), window, cx);
6820 });
6821
6822 // test multiple selections expand in the same direction
6823 cx.assert_editor_state(indoc!(
6824 r#"ab«cdˇ»
6825 ef«ghˇ»
6826 «iˇ»jkl
6827 «mˇ»nop"#
6828 ));
6829
6830 cx.update_editor(|editor, window, cx| {
6831 editor.add_selection_above(&Default::default(), window, cx);
6832 });
6833
6834 // test multiple selection upward overflow
6835 cx.assert_editor_state(indoc!(
6836 r#"ab«cdˇ»
6837 «eˇ»f«ghˇ»
6838 «iˇ»jkl
6839 «mˇ»nop"#
6840 ));
6841
6842 cx.update_editor(|editor, window, cx| {
6843 editor.add_selection_below(&Default::default(), window, cx);
6844 });
6845
6846 // test multiple selection retrieves back correctly
6847 cx.assert_editor_state(indoc!(
6848 r#"abcd
6849 ef«ghˇ»
6850 «iˇ»jkl
6851 «mˇ»nop"#
6852 ));
6853
6854 cx.update_editor(|editor, window, cx| {
6855 editor.add_selection_below(&Default::default(), window, cx);
6856 });
6857
6858 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
6859 cx.assert_editor_state(indoc!(
6860 r#"abcd
6861 ef«ghˇ»
6862 ij«klˇ»
6863 «mˇ»nop"#
6864 ));
6865
6866 cx.update_editor(|editor, window, cx| {
6867 editor.undo_selection(&Default::default(), window, cx);
6868 });
6869
6870 // test undo
6871 cx.assert_editor_state(indoc!(
6872 r#"abcd
6873 ef«ghˇ»
6874 «iˇ»jkl
6875 «mˇ»nop"#
6876 ));
6877
6878 cx.update_editor(|editor, window, cx| {
6879 editor.redo_selection(&Default::default(), window, cx);
6880 });
6881
6882 // test redo
6883 cx.assert_editor_state(indoc!(
6884 r#"abcd
6885 ef«ghˇ»
6886 ij«klˇ»
6887 «mˇ»nop"#
6888 ));
6889}
6890
6891#[gpui::test]
6892async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
6893 init_test(cx, |_| {});
6894 let mut cx = EditorTestContext::new(cx).await;
6895
6896 cx.set_state(indoc!(
6897 r#"line onˇe
6898 liˇne two
6899 line three
6900 line four"#
6901 ));
6902
6903 cx.update_editor(|editor, window, cx| {
6904 editor.add_selection_below(&Default::default(), window, cx);
6905 editor.add_selection_below(&Default::default(), window, cx);
6906 editor.add_selection_below(&Default::default(), window, cx);
6907 });
6908
6909 // initial state with two multi cursor groups
6910 cx.assert_editor_state(indoc!(
6911 r#"line onˇe
6912 liˇne twˇo
6913 liˇne thˇree
6914 liˇne foˇur"#
6915 ));
6916
6917 // add single cursor in middle - simulate opt click
6918 cx.update_editor(|editor, window, cx| {
6919 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
6920 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6921 editor.end_selection(window, cx);
6922 });
6923
6924 cx.assert_editor_state(indoc!(
6925 r#"line onˇe
6926 liˇne twˇo
6927 liˇneˇ thˇree
6928 liˇne foˇur"#
6929 ));
6930
6931 cx.update_editor(|editor, window, cx| {
6932 editor.add_selection_above(&Default::default(), window, cx);
6933 });
6934
6935 // test new added selection expands above and existing selection shrinks
6936 cx.assert_editor_state(indoc!(
6937 r#"line onˇe
6938 liˇneˇ twˇo
6939 liˇneˇ thˇree
6940 line four"#
6941 ));
6942
6943 cx.update_editor(|editor, window, cx| {
6944 editor.add_selection_above(&Default::default(), window, cx);
6945 });
6946
6947 // test new added selection expands above and existing selection shrinks
6948 cx.assert_editor_state(indoc!(
6949 r#"lineˇ onˇe
6950 liˇneˇ twˇo
6951 lineˇ three
6952 line four"#
6953 ));
6954
6955 // intial state with two selection groups
6956 cx.set_state(indoc!(
6957 r#"abcd
6958 ef«ghˇ»
6959 ijkl
6960 «mˇ»nop"#
6961 ));
6962
6963 cx.update_editor(|editor, window, cx| {
6964 editor.add_selection_above(&Default::default(), window, cx);
6965 editor.add_selection_above(&Default::default(), window, cx);
6966 });
6967
6968 cx.assert_editor_state(indoc!(
6969 r#"ab«cdˇ»
6970 «eˇ»f«ghˇ»
6971 «iˇ»jkl
6972 «mˇ»nop"#
6973 ));
6974
6975 // add single selection in middle - simulate opt drag
6976 cx.update_editor(|editor, window, cx| {
6977 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
6978 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6979 editor.update_selection(
6980 DisplayPoint::new(DisplayRow(2), 4),
6981 0,
6982 gpui::Point::<f32>::default(),
6983 window,
6984 cx,
6985 );
6986 editor.end_selection(window, cx);
6987 });
6988
6989 cx.assert_editor_state(indoc!(
6990 r#"ab«cdˇ»
6991 «eˇ»f«ghˇ»
6992 «iˇ»jk«lˇ»
6993 «mˇ»nop"#
6994 ));
6995
6996 cx.update_editor(|editor, window, cx| {
6997 editor.add_selection_below(&Default::default(), window, cx);
6998 });
6999
7000 // test new added selection expands below, others shrinks from above
7001 cx.assert_editor_state(indoc!(
7002 r#"abcd
7003 ef«ghˇ»
7004 «iˇ»jk«lˇ»
7005 «mˇ»no«pˇ»"#
7006 ));
7007}
7008
7009#[gpui::test]
7010async fn test_select_next(cx: &mut TestAppContext) {
7011 init_test(cx, |_| {});
7012
7013 let mut cx = EditorTestContext::new(cx).await;
7014 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7015
7016 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7017 .unwrap();
7018 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7019
7020 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7021 .unwrap();
7022 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
7023
7024 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7025 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7026
7027 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7028 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
7029
7030 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7031 .unwrap();
7032 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7033
7034 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7035 .unwrap();
7036 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7037
7038 // Test selection direction should be preserved
7039 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7040
7041 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7042 .unwrap();
7043 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
7044}
7045
7046#[gpui::test]
7047async fn test_select_all_matches(cx: &mut TestAppContext) {
7048 init_test(cx, |_| {});
7049
7050 let mut cx = EditorTestContext::new(cx).await;
7051
7052 // Test caret-only selections
7053 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7054 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7055 .unwrap();
7056 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7057
7058 // Test left-to-right selections
7059 cx.set_state("abc\n«abcˇ»\nabc");
7060 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7061 .unwrap();
7062 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
7063
7064 // Test right-to-left selections
7065 cx.set_state("abc\n«ˇabc»\nabc");
7066 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7067 .unwrap();
7068 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
7069
7070 // Test selecting whitespace with caret selection
7071 cx.set_state("abc\nˇ abc\nabc");
7072 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7073 .unwrap();
7074 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
7075
7076 // Test selecting whitespace with left-to-right selection
7077 cx.set_state("abc\n«ˇ »abc\nabc");
7078 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7079 .unwrap();
7080 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
7081
7082 // Test no matches with right-to-left selection
7083 cx.set_state("abc\n« ˇ»abc\nabc");
7084 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7085 .unwrap();
7086 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
7087
7088 // Test with a single word and clip_at_line_ends=true (#29823)
7089 cx.set_state("aˇbc");
7090 cx.update_editor(|e, window, cx| {
7091 e.set_clip_at_line_ends(true, cx);
7092 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
7093 e.set_clip_at_line_ends(false, cx);
7094 });
7095 cx.assert_editor_state("«abcˇ»");
7096}
7097
7098#[gpui::test]
7099async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
7100 init_test(cx, |_| {});
7101
7102 let mut cx = EditorTestContext::new(cx).await;
7103
7104 let large_body_1 = "\nd".repeat(200);
7105 let large_body_2 = "\ne".repeat(200);
7106
7107 cx.set_state(&format!(
7108 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
7109 ));
7110 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
7111 let scroll_position = editor.scroll_position(cx);
7112 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
7113 scroll_position
7114 });
7115
7116 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7117 .unwrap();
7118 cx.assert_editor_state(&format!(
7119 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
7120 ));
7121 let scroll_position_after_selection =
7122 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
7123 assert_eq!(
7124 initial_scroll_position, scroll_position_after_selection,
7125 "Scroll position should not change after selecting all matches"
7126 );
7127}
7128
7129#[gpui::test]
7130async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
7131 init_test(cx, |_| {});
7132
7133 let mut cx = EditorLspTestContext::new_rust(
7134 lsp::ServerCapabilities {
7135 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7136 ..Default::default()
7137 },
7138 cx,
7139 )
7140 .await;
7141
7142 cx.set_state(indoc! {"
7143 line 1
7144 line 2
7145 linˇe 3
7146 line 4
7147 line 5
7148 "});
7149
7150 // Make an edit
7151 cx.update_editor(|editor, window, cx| {
7152 editor.handle_input("X", window, cx);
7153 });
7154
7155 // Move cursor to a different position
7156 cx.update_editor(|editor, window, cx| {
7157 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7158 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
7159 });
7160 });
7161
7162 cx.assert_editor_state(indoc! {"
7163 line 1
7164 line 2
7165 linXe 3
7166 line 4
7167 liˇne 5
7168 "});
7169
7170 cx.lsp
7171 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
7172 Ok(Some(vec![lsp::TextEdit::new(
7173 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
7174 "PREFIX ".to_string(),
7175 )]))
7176 });
7177
7178 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
7179 .unwrap()
7180 .await
7181 .unwrap();
7182
7183 cx.assert_editor_state(indoc! {"
7184 PREFIX line 1
7185 line 2
7186 linXe 3
7187 line 4
7188 liˇne 5
7189 "});
7190
7191 // Undo formatting
7192 cx.update_editor(|editor, window, cx| {
7193 editor.undo(&Default::default(), window, cx);
7194 });
7195
7196 // Verify cursor moved back to position after edit
7197 cx.assert_editor_state(indoc! {"
7198 line 1
7199 line 2
7200 linXˇe 3
7201 line 4
7202 line 5
7203 "});
7204}
7205
7206#[gpui::test]
7207async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext) {
7208 init_test(cx, |_| {});
7209
7210 let mut cx = EditorTestContext::new(cx).await;
7211
7212 let provider = cx.new(|_| FakeInlineCompletionProvider::default());
7213 cx.update_editor(|editor, window, cx| {
7214 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
7215 });
7216
7217 cx.set_state(indoc! {"
7218 line 1
7219 line 2
7220 linˇe 3
7221 line 4
7222 line 5
7223 line 6
7224 line 7
7225 line 8
7226 line 9
7227 line 10
7228 "});
7229
7230 let snapshot = cx.buffer_snapshot();
7231 let edit_position = snapshot.anchor_after(Point::new(2, 4));
7232
7233 cx.update(|_, cx| {
7234 provider.update(cx, |provider, _| {
7235 provider.set_inline_completion(Some(inline_completion::InlineCompletion {
7236 id: None,
7237 edits: vec![(edit_position..edit_position, "X".into())],
7238 edit_preview: None,
7239 }))
7240 })
7241 });
7242
7243 cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx));
7244 cx.update_editor(|editor, window, cx| {
7245 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
7246 });
7247
7248 cx.assert_editor_state(indoc! {"
7249 line 1
7250 line 2
7251 lineXˇ 3
7252 line 4
7253 line 5
7254 line 6
7255 line 7
7256 line 8
7257 line 9
7258 line 10
7259 "});
7260
7261 cx.update_editor(|editor, window, cx| {
7262 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7263 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
7264 });
7265 });
7266
7267 cx.assert_editor_state(indoc! {"
7268 line 1
7269 line 2
7270 lineX 3
7271 line 4
7272 line 5
7273 line 6
7274 line 7
7275 line 8
7276 line 9
7277 liˇne 10
7278 "});
7279
7280 cx.update_editor(|editor, window, cx| {
7281 editor.undo(&Default::default(), window, cx);
7282 });
7283
7284 cx.assert_editor_state(indoc! {"
7285 line 1
7286 line 2
7287 lineˇ 3
7288 line 4
7289 line 5
7290 line 6
7291 line 7
7292 line 8
7293 line 9
7294 line 10
7295 "});
7296}
7297
7298#[gpui::test]
7299async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
7300 init_test(cx, |_| {});
7301
7302 let mut cx = EditorTestContext::new(cx).await;
7303 cx.set_state(
7304 r#"let foo = 2;
7305lˇet foo = 2;
7306let fooˇ = 2;
7307let foo = 2;
7308let foo = ˇ2;"#,
7309 );
7310
7311 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7312 .unwrap();
7313 cx.assert_editor_state(
7314 r#"let foo = 2;
7315«letˇ» foo = 2;
7316let «fooˇ» = 2;
7317let foo = 2;
7318let foo = «2ˇ»;"#,
7319 );
7320
7321 // noop for multiple selections with different contents
7322 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7323 .unwrap();
7324 cx.assert_editor_state(
7325 r#"let foo = 2;
7326«letˇ» foo = 2;
7327let «fooˇ» = 2;
7328let foo = 2;
7329let foo = «2ˇ»;"#,
7330 );
7331
7332 // Test last selection direction should be preserved
7333 cx.set_state(
7334 r#"let foo = 2;
7335let foo = 2;
7336let «fooˇ» = 2;
7337let «ˇfoo» = 2;
7338let foo = 2;"#,
7339 );
7340
7341 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7342 .unwrap();
7343 cx.assert_editor_state(
7344 r#"let foo = 2;
7345let foo = 2;
7346let «fooˇ» = 2;
7347let «ˇfoo» = 2;
7348let «ˇfoo» = 2;"#,
7349 );
7350}
7351
7352#[gpui::test]
7353async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
7354 init_test(cx, |_| {});
7355
7356 let mut cx =
7357 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
7358
7359 cx.assert_editor_state(indoc! {"
7360 ˇbbb
7361 ccc
7362
7363 bbb
7364 ccc
7365 "});
7366 cx.dispatch_action(SelectPrevious::default());
7367 cx.assert_editor_state(indoc! {"
7368 «bbbˇ»
7369 ccc
7370
7371 bbb
7372 ccc
7373 "});
7374 cx.dispatch_action(SelectPrevious::default());
7375 cx.assert_editor_state(indoc! {"
7376 «bbbˇ»
7377 ccc
7378
7379 «bbbˇ»
7380 ccc
7381 "});
7382}
7383
7384#[gpui::test]
7385async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
7386 init_test(cx, |_| {});
7387
7388 let mut cx = EditorTestContext::new(cx).await;
7389 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7390
7391 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7392 .unwrap();
7393 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7394
7395 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7396 .unwrap();
7397 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7398
7399 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7400 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7401
7402 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7403 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7404
7405 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7406 .unwrap();
7407 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
7408
7409 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7410 .unwrap();
7411 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7412}
7413
7414#[gpui::test]
7415async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
7416 init_test(cx, |_| {});
7417
7418 let mut cx = EditorTestContext::new(cx).await;
7419 cx.set_state("aˇ");
7420
7421 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7422 .unwrap();
7423 cx.assert_editor_state("«aˇ»");
7424 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7425 .unwrap();
7426 cx.assert_editor_state("«aˇ»");
7427}
7428
7429#[gpui::test]
7430async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
7431 init_test(cx, |_| {});
7432
7433 let mut cx = EditorTestContext::new(cx).await;
7434 cx.set_state(
7435 r#"let foo = 2;
7436lˇet foo = 2;
7437let fooˇ = 2;
7438let foo = 2;
7439let foo = ˇ2;"#,
7440 );
7441
7442 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7443 .unwrap();
7444 cx.assert_editor_state(
7445 r#"let foo = 2;
7446«letˇ» foo = 2;
7447let «fooˇ» = 2;
7448let foo = 2;
7449let foo = «2ˇ»;"#,
7450 );
7451
7452 // noop for multiple selections with different contents
7453 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7454 .unwrap();
7455 cx.assert_editor_state(
7456 r#"let foo = 2;
7457«letˇ» foo = 2;
7458let «fooˇ» = 2;
7459let foo = 2;
7460let foo = «2ˇ»;"#,
7461 );
7462}
7463
7464#[gpui::test]
7465async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
7466 init_test(cx, |_| {});
7467
7468 let mut cx = EditorTestContext::new(cx).await;
7469 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7470
7471 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7472 .unwrap();
7473 // selection direction is preserved
7474 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7475
7476 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7477 .unwrap();
7478 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7479
7480 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7481 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7482
7483 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7484 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7485
7486 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7487 .unwrap();
7488 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
7489
7490 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7491 .unwrap();
7492 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
7493}
7494
7495#[gpui::test]
7496async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
7497 init_test(cx, |_| {});
7498
7499 let language = Arc::new(Language::new(
7500 LanguageConfig::default(),
7501 Some(tree_sitter_rust::LANGUAGE.into()),
7502 ));
7503
7504 let text = r#"
7505 use mod1::mod2::{mod3, mod4};
7506
7507 fn fn_1(param1: bool, param2: &str) {
7508 let var1 = "text";
7509 }
7510 "#
7511 .unindent();
7512
7513 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7514 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7515 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7516
7517 editor
7518 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7519 .await;
7520
7521 editor.update_in(cx, |editor, window, cx| {
7522 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7523 s.select_display_ranges([
7524 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
7525 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
7526 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
7527 ]);
7528 });
7529 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7530 });
7531 editor.update(cx, |editor, cx| {
7532 assert_text_with_selections(
7533 editor,
7534 indoc! {r#"
7535 use mod1::mod2::{mod3, «mod4ˇ»};
7536
7537 fn fn_1«ˇ(param1: bool, param2: &str)» {
7538 let var1 = "«ˇtext»";
7539 }
7540 "#},
7541 cx,
7542 );
7543 });
7544
7545 editor.update_in(cx, |editor, window, cx| {
7546 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7547 });
7548 editor.update(cx, |editor, cx| {
7549 assert_text_with_selections(
7550 editor,
7551 indoc! {r#"
7552 use mod1::mod2::«{mod3, mod4}ˇ»;
7553
7554 «ˇfn fn_1(param1: bool, param2: &str) {
7555 let var1 = "text";
7556 }»
7557 "#},
7558 cx,
7559 );
7560 });
7561
7562 editor.update_in(cx, |editor, window, cx| {
7563 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7564 });
7565 assert_eq!(
7566 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7567 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7568 );
7569
7570 // Trying to expand the selected syntax node one more time has no effect.
7571 editor.update_in(cx, |editor, window, cx| {
7572 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7573 });
7574 assert_eq!(
7575 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7576 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7577 );
7578
7579 editor.update_in(cx, |editor, window, cx| {
7580 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7581 });
7582 editor.update(cx, |editor, cx| {
7583 assert_text_with_selections(
7584 editor,
7585 indoc! {r#"
7586 use mod1::mod2::«{mod3, mod4}ˇ»;
7587
7588 «ˇfn fn_1(param1: bool, param2: &str) {
7589 let var1 = "text";
7590 }»
7591 "#},
7592 cx,
7593 );
7594 });
7595
7596 editor.update_in(cx, |editor, window, cx| {
7597 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7598 });
7599 editor.update(cx, |editor, cx| {
7600 assert_text_with_selections(
7601 editor,
7602 indoc! {r#"
7603 use mod1::mod2::{mod3, «mod4ˇ»};
7604
7605 fn fn_1«ˇ(param1: bool, param2: &str)» {
7606 let var1 = "«ˇtext»";
7607 }
7608 "#},
7609 cx,
7610 );
7611 });
7612
7613 editor.update_in(cx, |editor, window, cx| {
7614 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7615 });
7616 editor.update(cx, |editor, cx| {
7617 assert_text_with_selections(
7618 editor,
7619 indoc! {r#"
7620 use mod1::mod2::{mod3, mo«ˇ»d4};
7621
7622 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7623 let var1 = "te«ˇ»xt";
7624 }
7625 "#},
7626 cx,
7627 );
7628 });
7629
7630 // Trying to shrink the selected syntax node one more time has no effect.
7631 editor.update_in(cx, |editor, window, cx| {
7632 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7633 });
7634 editor.update_in(cx, |editor, _, cx| {
7635 assert_text_with_selections(
7636 editor,
7637 indoc! {r#"
7638 use mod1::mod2::{mod3, mo«ˇ»d4};
7639
7640 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7641 let var1 = "te«ˇ»xt";
7642 }
7643 "#},
7644 cx,
7645 );
7646 });
7647
7648 // Ensure that we keep expanding the selection if the larger selection starts or ends within
7649 // a fold.
7650 editor.update_in(cx, |editor, window, cx| {
7651 editor.fold_creases(
7652 vec![
7653 Crease::simple(
7654 Point::new(0, 21)..Point::new(0, 24),
7655 FoldPlaceholder::test(),
7656 ),
7657 Crease::simple(
7658 Point::new(3, 20)..Point::new(3, 22),
7659 FoldPlaceholder::test(),
7660 ),
7661 ],
7662 true,
7663 window,
7664 cx,
7665 );
7666 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7667 });
7668 editor.update(cx, |editor, cx| {
7669 assert_text_with_selections(
7670 editor,
7671 indoc! {r#"
7672 use mod1::mod2::«{mod3, mod4}ˇ»;
7673
7674 fn fn_1«ˇ(param1: bool, param2: &str)» {
7675 let var1 = "«ˇtext»";
7676 }
7677 "#},
7678 cx,
7679 );
7680 });
7681}
7682
7683#[gpui::test]
7684async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
7685 init_test(cx, |_| {});
7686
7687 let language = Arc::new(Language::new(
7688 LanguageConfig::default(),
7689 Some(tree_sitter_rust::LANGUAGE.into()),
7690 ));
7691
7692 let text = "let a = 2;";
7693
7694 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7695 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7696 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7697
7698 editor
7699 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7700 .await;
7701
7702 // Test case 1: Cursor at end of word
7703 editor.update_in(cx, |editor, window, cx| {
7704 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7705 s.select_display_ranges([
7706 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
7707 ]);
7708 });
7709 });
7710 editor.update(cx, |editor, cx| {
7711 assert_text_with_selections(editor, "let aˇ = 2;", cx);
7712 });
7713 editor.update_in(cx, |editor, window, cx| {
7714 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7715 });
7716 editor.update(cx, |editor, cx| {
7717 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
7718 });
7719 editor.update_in(cx, |editor, window, cx| {
7720 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7721 });
7722 editor.update(cx, |editor, cx| {
7723 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7724 });
7725
7726 // Test case 2: Cursor at end of statement
7727 editor.update_in(cx, |editor, window, cx| {
7728 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7729 s.select_display_ranges([
7730 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
7731 ]);
7732 });
7733 });
7734 editor.update(cx, |editor, cx| {
7735 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
7736 });
7737 editor.update_in(cx, |editor, window, cx| {
7738 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7739 });
7740 editor.update(cx, |editor, cx| {
7741 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7742 });
7743}
7744
7745#[gpui::test]
7746async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
7747 init_test(cx, |_| {});
7748
7749 let language = Arc::new(Language::new(
7750 LanguageConfig::default(),
7751 Some(tree_sitter_rust::LANGUAGE.into()),
7752 ));
7753
7754 let text = r#"
7755 use mod1::mod2::{mod3, mod4};
7756
7757 fn fn_1(param1: bool, param2: &str) {
7758 let var1 = "hello world";
7759 }
7760 "#
7761 .unindent();
7762
7763 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7764 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7765 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7766
7767 editor
7768 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7769 .await;
7770
7771 // Test 1: Cursor on a letter of a string word
7772 editor.update_in(cx, |editor, window, cx| {
7773 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7774 s.select_display_ranges([
7775 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
7776 ]);
7777 });
7778 });
7779 editor.update_in(cx, |editor, window, cx| {
7780 assert_text_with_selections(
7781 editor,
7782 indoc! {r#"
7783 use mod1::mod2::{mod3, mod4};
7784
7785 fn fn_1(param1: bool, param2: &str) {
7786 let var1 = "hˇello world";
7787 }
7788 "#},
7789 cx,
7790 );
7791 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7792 assert_text_with_selections(
7793 editor,
7794 indoc! {r#"
7795 use mod1::mod2::{mod3, mod4};
7796
7797 fn fn_1(param1: bool, param2: &str) {
7798 let var1 = "«ˇhello» world";
7799 }
7800 "#},
7801 cx,
7802 );
7803 });
7804
7805 // Test 2: Partial selection within a word
7806 editor.update_in(cx, |editor, window, cx| {
7807 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7808 s.select_display_ranges([
7809 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
7810 ]);
7811 });
7812 });
7813 editor.update_in(cx, |editor, window, cx| {
7814 assert_text_with_selections(
7815 editor,
7816 indoc! {r#"
7817 use mod1::mod2::{mod3, mod4};
7818
7819 fn fn_1(param1: bool, param2: &str) {
7820 let var1 = "h«elˇ»lo world";
7821 }
7822 "#},
7823 cx,
7824 );
7825 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7826 assert_text_with_selections(
7827 editor,
7828 indoc! {r#"
7829 use mod1::mod2::{mod3, mod4};
7830
7831 fn fn_1(param1: bool, param2: &str) {
7832 let var1 = "«ˇhello» world";
7833 }
7834 "#},
7835 cx,
7836 );
7837 });
7838
7839 // Test 3: Complete word already selected
7840 editor.update_in(cx, |editor, window, cx| {
7841 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7842 s.select_display_ranges([
7843 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
7844 ]);
7845 });
7846 });
7847 editor.update_in(cx, |editor, window, cx| {
7848 assert_text_with_selections(
7849 editor,
7850 indoc! {r#"
7851 use mod1::mod2::{mod3, mod4};
7852
7853 fn fn_1(param1: bool, param2: &str) {
7854 let var1 = "«helloˇ» world";
7855 }
7856 "#},
7857 cx,
7858 );
7859 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7860 assert_text_with_selections(
7861 editor,
7862 indoc! {r#"
7863 use mod1::mod2::{mod3, mod4};
7864
7865 fn fn_1(param1: bool, param2: &str) {
7866 let var1 = "«hello worldˇ»";
7867 }
7868 "#},
7869 cx,
7870 );
7871 });
7872
7873 // Test 4: Selection spanning across words
7874 editor.update_in(cx, |editor, window, cx| {
7875 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7876 s.select_display_ranges([
7877 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
7878 ]);
7879 });
7880 });
7881 editor.update_in(cx, |editor, window, cx| {
7882 assert_text_with_selections(
7883 editor,
7884 indoc! {r#"
7885 use mod1::mod2::{mod3, mod4};
7886
7887 fn fn_1(param1: bool, param2: &str) {
7888 let var1 = "hel«lo woˇ»rld";
7889 }
7890 "#},
7891 cx,
7892 );
7893 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7894 assert_text_with_selections(
7895 editor,
7896 indoc! {r#"
7897 use mod1::mod2::{mod3, mod4};
7898
7899 fn fn_1(param1: bool, param2: &str) {
7900 let var1 = "«ˇhello world»";
7901 }
7902 "#},
7903 cx,
7904 );
7905 });
7906
7907 // Test 5: Expansion beyond string
7908 editor.update_in(cx, |editor, window, cx| {
7909 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7910 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7911 assert_text_with_selections(
7912 editor,
7913 indoc! {r#"
7914 use mod1::mod2::{mod3, mod4};
7915
7916 fn fn_1(param1: bool, param2: &str) {
7917 «ˇlet var1 = "hello world";»
7918 }
7919 "#},
7920 cx,
7921 );
7922 });
7923}
7924
7925#[gpui::test]
7926async fn test_fold_function_bodies(cx: &mut TestAppContext) {
7927 init_test(cx, |_| {});
7928
7929 let base_text = r#"
7930 impl A {
7931 // this is an uncommitted comment
7932
7933 fn b() {
7934 c();
7935 }
7936
7937 // this is another uncommitted comment
7938
7939 fn d() {
7940 // e
7941 // f
7942 }
7943 }
7944
7945 fn g() {
7946 // h
7947 }
7948 "#
7949 .unindent();
7950
7951 let text = r#"
7952 ˇimpl A {
7953
7954 fn b() {
7955 c();
7956 }
7957
7958 fn d() {
7959 // e
7960 // f
7961 }
7962 }
7963
7964 fn g() {
7965 // h
7966 }
7967 "#
7968 .unindent();
7969
7970 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7971 cx.set_state(&text);
7972 cx.set_head_text(&base_text);
7973 cx.update_editor(|editor, window, cx| {
7974 editor.expand_all_diff_hunks(&Default::default(), window, cx);
7975 });
7976
7977 cx.assert_state_with_diff(
7978 "
7979 ˇimpl A {
7980 - // this is an uncommitted comment
7981
7982 fn b() {
7983 c();
7984 }
7985
7986 - // this is another uncommitted comment
7987 -
7988 fn d() {
7989 // e
7990 // f
7991 }
7992 }
7993
7994 fn g() {
7995 // h
7996 }
7997 "
7998 .unindent(),
7999 );
8000
8001 let expected_display_text = "
8002 impl A {
8003 // this is an uncommitted comment
8004
8005 fn b() {
8006 ⋯
8007 }
8008
8009 // this is another uncommitted comment
8010
8011 fn d() {
8012 ⋯
8013 }
8014 }
8015
8016 fn g() {
8017 ⋯
8018 }
8019 "
8020 .unindent();
8021
8022 cx.update_editor(|editor, window, cx| {
8023 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
8024 assert_eq!(editor.display_text(cx), expected_display_text);
8025 });
8026}
8027
8028#[gpui::test]
8029async fn test_autoindent(cx: &mut TestAppContext) {
8030 init_test(cx, |_| {});
8031
8032 let language = Arc::new(
8033 Language::new(
8034 LanguageConfig {
8035 brackets: BracketPairConfig {
8036 pairs: vec![
8037 BracketPair {
8038 start: "{".to_string(),
8039 end: "}".to_string(),
8040 close: false,
8041 surround: false,
8042 newline: true,
8043 },
8044 BracketPair {
8045 start: "(".to_string(),
8046 end: ")".to_string(),
8047 close: false,
8048 surround: false,
8049 newline: true,
8050 },
8051 ],
8052 ..Default::default()
8053 },
8054 ..Default::default()
8055 },
8056 Some(tree_sitter_rust::LANGUAGE.into()),
8057 )
8058 .with_indents_query(
8059 r#"
8060 (_ "(" ")" @end) @indent
8061 (_ "{" "}" @end) @indent
8062 "#,
8063 )
8064 .unwrap(),
8065 );
8066
8067 let text = "fn a() {}";
8068
8069 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8070 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8071 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8072 editor
8073 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8074 .await;
8075
8076 editor.update_in(cx, |editor, window, cx| {
8077 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8078 s.select_ranges([5..5, 8..8, 9..9])
8079 });
8080 editor.newline(&Newline, window, cx);
8081 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
8082 assert_eq!(
8083 editor.selections.ranges(cx),
8084 &[
8085 Point::new(1, 4)..Point::new(1, 4),
8086 Point::new(3, 4)..Point::new(3, 4),
8087 Point::new(5, 0)..Point::new(5, 0)
8088 ]
8089 );
8090 });
8091}
8092
8093#[gpui::test]
8094async fn test_autoindent_selections(cx: &mut TestAppContext) {
8095 init_test(cx, |_| {});
8096
8097 {
8098 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
8099 cx.set_state(indoc! {"
8100 impl A {
8101
8102 fn b() {}
8103
8104 «fn c() {
8105
8106 }ˇ»
8107 }
8108 "});
8109
8110 cx.update_editor(|editor, window, cx| {
8111 editor.autoindent(&Default::default(), window, cx);
8112 });
8113
8114 cx.assert_editor_state(indoc! {"
8115 impl A {
8116
8117 fn b() {}
8118
8119 «fn c() {
8120
8121 }ˇ»
8122 }
8123 "});
8124 }
8125
8126 {
8127 let mut cx = EditorTestContext::new_multibuffer(
8128 cx,
8129 [indoc! { "
8130 impl A {
8131 «
8132 // a
8133 fn b(){}
8134 »
8135 «
8136 }
8137 fn c(){}
8138 »
8139 "}],
8140 );
8141
8142 let buffer = cx.update_editor(|editor, _, cx| {
8143 let buffer = editor.buffer().update(cx, |buffer, _| {
8144 buffer.all_buffers().iter().next().unwrap().clone()
8145 });
8146 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
8147 buffer
8148 });
8149
8150 cx.run_until_parked();
8151 cx.update_editor(|editor, window, cx| {
8152 editor.select_all(&Default::default(), window, cx);
8153 editor.autoindent(&Default::default(), window, cx)
8154 });
8155 cx.run_until_parked();
8156
8157 cx.update(|_, cx| {
8158 assert_eq!(
8159 buffer.read(cx).text(),
8160 indoc! { "
8161 impl A {
8162
8163 // a
8164 fn b(){}
8165
8166
8167 }
8168 fn c(){}
8169
8170 " }
8171 )
8172 });
8173 }
8174}
8175
8176#[gpui::test]
8177async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
8178 init_test(cx, |_| {});
8179
8180 let mut cx = EditorTestContext::new(cx).await;
8181
8182 let language = Arc::new(Language::new(
8183 LanguageConfig {
8184 brackets: BracketPairConfig {
8185 pairs: vec![
8186 BracketPair {
8187 start: "{".to_string(),
8188 end: "}".to_string(),
8189 close: true,
8190 surround: true,
8191 newline: true,
8192 },
8193 BracketPair {
8194 start: "(".to_string(),
8195 end: ")".to_string(),
8196 close: true,
8197 surround: true,
8198 newline: true,
8199 },
8200 BracketPair {
8201 start: "/*".to_string(),
8202 end: " */".to_string(),
8203 close: true,
8204 surround: true,
8205 newline: true,
8206 },
8207 BracketPair {
8208 start: "[".to_string(),
8209 end: "]".to_string(),
8210 close: false,
8211 surround: false,
8212 newline: true,
8213 },
8214 BracketPair {
8215 start: "\"".to_string(),
8216 end: "\"".to_string(),
8217 close: true,
8218 surround: true,
8219 newline: false,
8220 },
8221 BracketPair {
8222 start: "<".to_string(),
8223 end: ">".to_string(),
8224 close: false,
8225 surround: true,
8226 newline: true,
8227 },
8228 ],
8229 ..Default::default()
8230 },
8231 autoclose_before: "})]".to_string(),
8232 ..Default::default()
8233 },
8234 Some(tree_sitter_rust::LANGUAGE.into()),
8235 ));
8236
8237 cx.language_registry().add(language.clone());
8238 cx.update_buffer(|buffer, cx| {
8239 buffer.set_language(Some(language), cx);
8240 });
8241
8242 cx.set_state(
8243 &r#"
8244 🏀ˇ
8245 εˇ
8246 ❤️ˇ
8247 "#
8248 .unindent(),
8249 );
8250
8251 // autoclose multiple nested brackets at multiple cursors
8252 cx.update_editor(|editor, window, cx| {
8253 editor.handle_input("{", window, cx);
8254 editor.handle_input("{", window, cx);
8255 editor.handle_input("{", window, cx);
8256 });
8257 cx.assert_editor_state(
8258 &"
8259 🏀{{{ˇ}}}
8260 ε{{{ˇ}}}
8261 ❤️{{{ˇ}}}
8262 "
8263 .unindent(),
8264 );
8265
8266 // insert a different closing bracket
8267 cx.update_editor(|editor, window, cx| {
8268 editor.handle_input(")", window, cx);
8269 });
8270 cx.assert_editor_state(
8271 &"
8272 🏀{{{)ˇ}}}
8273 ε{{{)ˇ}}}
8274 ❤️{{{)ˇ}}}
8275 "
8276 .unindent(),
8277 );
8278
8279 // skip over the auto-closed brackets when typing a closing bracket
8280 cx.update_editor(|editor, window, cx| {
8281 editor.move_right(&MoveRight, window, cx);
8282 editor.handle_input("}", window, cx);
8283 editor.handle_input("}", window, cx);
8284 editor.handle_input("}", window, cx);
8285 });
8286 cx.assert_editor_state(
8287 &"
8288 🏀{{{)}}}}ˇ
8289 ε{{{)}}}}ˇ
8290 ❤️{{{)}}}}ˇ
8291 "
8292 .unindent(),
8293 );
8294
8295 // autoclose multi-character pairs
8296 cx.set_state(
8297 &"
8298 ˇ
8299 ˇ
8300 "
8301 .unindent(),
8302 );
8303 cx.update_editor(|editor, window, cx| {
8304 editor.handle_input("/", window, cx);
8305 editor.handle_input("*", window, cx);
8306 });
8307 cx.assert_editor_state(
8308 &"
8309 /*ˇ */
8310 /*ˇ */
8311 "
8312 .unindent(),
8313 );
8314
8315 // one cursor autocloses a multi-character pair, one cursor
8316 // does not autoclose.
8317 cx.set_state(
8318 &"
8319 /ˇ
8320 ˇ
8321 "
8322 .unindent(),
8323 );
8324 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
8325 cx.assert_editor_state(
8326 &"
8327 /*ˇ */
8328 *ˇ
8329 "
8330 .unindent(),
8331 );
8332
8333 // Don't autoclose if the next character isn't whitespace and isn't
8334 // listed in the language's "autoclose_before" section.
8335 cx.set_state("ˇa b");
8336 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8337 cx.assert_editor_state("{ˇa b");
8338
8339 // Don't autoclose if `close` is false for the bracket pair
8340 cx.set_state("ˇ");
8341 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
8342 cx.assert_editor_state("[ˇ");
8343
8344 // Surround with brackets if text is selected
8345 cx.set_state("«aˇ» b");
8346 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8347 cx.assert_editor_state("{«aˇ»} b");
8348
8349 // Autoclose when not immediately after a word character
8350 cx.set_state("a ˇ");
8351 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8352 cx.assert_editor_state("a \"ˇ\"");
8353
8354 // Autoclose pair where the start and end characters are the same
8355 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8356 cx.assert_editor_state("a \"\"ˇ");
8357
8358 // Don't autoclose when immediately after a word character
8359 cx.set_state("aˇ");
8360 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8361 cx.assert_editor_state("a\"ˇ");
8362
8363 // Do autoclose when after a non-word character
8364 cx.set_state("{ˇ");
8365 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8366 cx.assert_editor_state("{\"ˇ\"");
8367
8368 // Non identical pairs autoclose regardless of preceding character
8369 cx.set_state("aˇ");
8370 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8371 cx.assert_editor_state("a{ˇ}");
8372
8373 // Don't autoclose pair if autoclose is disabled
8374 cx.set_state("ˇ");
8375 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8376 cx.assert_editor_state("<ˇ");
8377
8378 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
8379 cx.set_state("«aˇ» b");
8380 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8381 cx.assert_editor_state("<«aˇ»> b");
8382}
8383
8384#[gpui::test]
8385async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
8386 init_test(cx, |settings| {
8387 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8388 });
8389
8390 let mut cx = EditorTestContext::new(cx).await;
8391
8392 let language = Arc::new(Language::new(
8393 LanguageConfig {
8394 brackets: BracketPairConfig {
8395 pairs: vec![
8396 BracketPair {
8397 start: "{".to_string(),
8398 end: "}".to_string(),
8399 close: true,
8400 surround: true,
8401 newline: true,
8402 },
8403 BracketPair {
8404 start: "(".to_string(),
8405 end: ")".to_string(),
8406 close: true,
8407 surround: true,
8408 newline: true,
8409 },
8410 BracketPair {
8411 start: "[".to_string(),
8412 end: "]".to_string(),
8413 close: false,
8414 surround: false,
8415 newline: true,
8416 },
8417 ],
8418 ..Default::default()
8419 },
8420 autoclose_before: "})]".to_string(),
8421 ..Default::default()
8422 },
8423 Some(tree_sitter_rust::LANGUAGE.into()),
8424 ));
8425
8426 cx.language_registry().add(language.clone());
8427 cx.update_buffer(|buffer, cx| {
8428 buffer.set_language(Some(language), cx);
8429 });
8430
8431 cx.set_state(
8432 &"
8433 ˇ
8434 ˇ
8435 ˇ
8436 "
8437 .unindent(),
8438 );
8439
8440 // ensure only matching closing brackets are skipped over
8441 cx.update_editor(|editor, window, cx| {
8442 editor.handle_input("}", window, cx);
8443 editor.move_left(&MoveLeft, window, cx);
8444 editor.handle_input(")", window, cx);
8445 editor.move_left(&MoveLeft, window, cx);
8446 });
8447 cx.assert_editor_state(
8448 &"
8449 ˇ)}
8450 ˇ)}
8451 ˇ)}
8452 "
8453 .unindent(),
8454 );
8455
8456 // skip-over closing brackets at multiple cursors
8457 cx.update_editor(|editor, window, cx| {
8458 editor.handle_input(")", window, cx);
8459 editor.handle_input("}", window, cx);
8460 });
8461 cx.assert_editor_state(
8462 &"
8463 )}ˇ
8464 )}ˇ
8465 )}ˇ
8466 "
8467 .unindent(),
8468 );
8469
8470 // ignore non-close brackets
8471 cx.update_editor(|editor, window, cx| {
8472 editor.handle_input("]", window, cx);
8473 editor.move_left(&MoveLeft, window, cx);
8474 editor.handle_input("]", window, cx);
8475 });
8476 cx.assert_editor_state(
8477 &"
8478 )}]ˇ]
8479 )}]ˇ]
8480 )}]ˇ]
8481 "
8482 .unindent(),
8483 );
8484}
8485
8486#[gpui::test]
8487async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
8488 init_test(cx, |_| {});
8489
8490 let mut cx = EditorTestContext::new(cx).await;
8491
8492 let html_language = Arc::new(
8493 Language::new(
8494 LanguageConfig {
8495 name: "HTML".into(),
8496 brackets: BracketPairConfig {
8497 pairs: vec![
8498 BracketPair {
8499 start: "<".into(),
8500 end: ">".into(),
8501 close: true,
8502 ..Default::default()
8503 },
8504 BracketPair {
8505 start: "{".into(),
8506 end: "}".into(),
8507 close: true,
8508 ..Default::default()
8509 },
8510 BracketPair {
8511 start: "(".into(),
8512 end: ")".into(),
8513 close: true,
8514 ..Default::default()
8515 },
8516 ],
8517 ..Default::default()
8518 },
8519 autoclose_before: "})]>".into(),
8520 ..Default::default()
8521 },
8522 Some(tree_sitter_html::LANGUAGE.into()),
8523 )
8524 .with_injection_query(
8525 r#"
8526 (script_element
8527 (raw_text) @injection.content
8528 (#set! injection.language "javascript"))
8529 "#,
8530 )
8531 .unwrap(),
8532 );
8533
8534 let javascript_language = Arc::new(Language::new(
8535 LanguageConfig {
8536 name: "JavaScript".into(),
8537 brackets: BracketPairConfig {
8538 pairs: vec![
8539 BracketPair {
8540 start: "/*".into(),
8541 end: " */".into(),
8542 close: true,
8543 ..Default::default()
8544 },
8545 BracketPair {
8546 start: "{".into(),
8547 end: "}".into(),
8548 close: true,
8549 ..Default::default()
8550 },
8551 BracketPair {
8552 start: "(".into(),
8553 end: ")".into(),
8554 close: true,
8555 ..Default::default()
8556 },
8557 ],
8558 ..Default::default()
8559 },
8560 autoclose_before: "})]>".into(),
8561 ..Default::default()
8562 },
8563 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8564 ));
8565
8566 cx.language_registry().add(html_language.clone());
8567 cx.language_registry().add(javascript_language.clone());
8568
8569 cx.update_buffer(|buffer, cx| {
8570 buffer.set_language(Some(html_language), cx);
8571 });
8572
8573 cx.set_state(
8574 &r#"
8575 <body>ˇ
8576 <script>
8577 var x = 1;ˇ
8578 </script>
8579 </body>ˇ
8580 "#
8581 .unindent(),
8582 );
8583
8584 // Precondition: different languages are active at different locations.
8585 cx.update_editor(|editor, window, cx| {
8586 let snapshot = editor.snapshot(window, cx);
8587 let cursors = editor.selections.ranges::<usize>(cx);
8588 let languages = cursors
8589 .iter()
8590 .map(|c| snapshot.language_at(c.start).unwrap().name())
8591 .collect::<Vec<_>>();
8592 assert_eq!(
8593 languages,
8594 &["HTML".into(), "JavaScript".into(), "HTML".into()]
8595 );
8596 });
8597
8598 // Angle brackets autoclose in HTML, but not JavaScript.
8599 cx.update_editor(|editor, window, cx| {
8600 editor.handle_input("<", window, cx);
8601 editor.handle_input("a", window, cx);
8602 });
8603 cx.assert_editor_state(
8604 &r#"
8605 <body><aˇ>
8606 <script>
8607 var x = 1;<aˇ
8608 </script>
8609 </body><aˇ>
8610 "#
8611 .unindent(),
8612 );
8613
8614 // Curly braces and parens autoclose in both HTML and JavaScript.
8615 cx.update_editor(|editor, window, cx| {
8616 editor.handle_input(" b=", window, cx);
8617 editor.handle_input("{", window, cx);
8618 editor.handle_input("c", window, cx);
8619 editor.handle_input("(", window, cx);
8620 });
8621 cx.assert_editor_state(
8622 &r#"
8623 <body><a b={c(ˇ)}>
8624 <script>
8625 var x = 1;<a b={c(ˇ)}
8626 </script>
8627 </body><a b={c(ˇ)}>
8628 "#
8629 .unindent(),
8630 );
8631
8632 // Brackets that were already autoclosed are skipped.
8633 cx.update_editor(|editor, window, cx| {
8634 editor.handle_input(")", window, cx);
8635 editor.handle_input("d", window, cx);
8636 editor.handle_input("}", window, cx);
8637 });
8638 cx.assert_editor_state(
8639 &r#"
8640 <body><a b={c()d}ˇ>
8641 <script>
8642 var x = 1;<a b={c()d}ˇ
8643 </script>
8644 </body><a b={c()d}ˇ>
8645 "#
8646 .unindent(),
8647 );
8648 cx.update_editor(|editor, window, cx| {
8649 editor.handle_input(">", window, cx);
8650 });
8651 cx.assert_editor_state(
8652 &r#"
8653 <body><a b={c()d}>ˇ
8654 <script>
8655 var x = 1;<a b={c()d}>ˇ
8656 </script>
8657 </body><a b={c()d}>ˇ
8658 "#
8659 .unindent(),
8660 );
8661
8662 // Reset
8663 cx.set_state(
8664 &r#"
8665 <body>ˇ
8666 <script>
8667 var x = 1;ˇ
8668 </script>
8669 </body>ˇ
8670 "#
8671 .unindent(),
8672 );
8673
8674 cx.update_editor(|editor, window, cx| {
8675 editor.handle_input("<", window, cx);
8676 });
8677 cx.assert_editor_state(
8678 &r#"
8679 <body><ˇ>
8680 <script>
8681 var x = 1;<ˇ
8682 </script>
8683 </body><ˇ>
8684 "#
8685 .unindent(),
8686 );
8687
8688 // When backspacing, the closing angle brackets are removed.
8689 cx.update_editor(|editor, window, cx| {
8690 editor.backspace(&Backspace, window, cx);
8691 });
8692 cx.assert_editor_state(
8693 &r#"
8694 <body>ˇ
8695 <script>
8696 var x = 1;ˇ
8697 </script>
8698 </body>ˇ
8699 "#
8700 .unindent(),
8701 );
8702
8703 // Block comments autoclose in JavaScript, but not HTML.
8704 cx.update_editor(|editor, window, cx| {
8705 editor.handle_input("/", window, cx);
8706 editor.handle_input("*", window, cx);
8707 });
8708 cx.assert_editor_state(
8709 &r#"
8710 <body>/*ˇ
8711 <script>
8712 var x = 1;/*ˇ */
8713 </script>
8714 </body>/*ˇ
8715 "#
8716 .unindent(),
8717 );
8718}
8719
8720#[gpui::test]
8721async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
8722 init_test(cx, |_| {});
8723
8724 let mut cx = EditorTestContext::new(cx).await;
8725
8726 let rust_language = Arc::new(
8727 Language::new(
8728 LanguageConfig {
8729 name: "Rust".into(),
8730 brackets: serde_json::from_value(json!([
8731 { "start": "{", "end": "}", "close": true, "newline": true },
8732 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
8733 ]))
8734 .unwrap(),
8735 autoclose_before: "})]>".into(),
8736 ..Default::default()
8737 },
8738 Some(tree_sitter_rust::LANGUAGE.into()),
8739 )
8740 .with_override_query("(string_literal) @string")
8741 .unwrap(),
8742 );
8743
8744 cx.language_registry().add(rust_language.clone());
8745 cx.update_buffer(|buffer, cx| {
8746 buffer.set_language(Some(rust_language), cx);
8747 });
8748
8749 cx.set_state(
8750 &r#"
8751 let x = ˇ
8752 "#
8753 .unindent(),
8754 );
8755
8756 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
8757 cx.update_editor(|editor, window, cx| {
8758 editor.handle_input("\"", window, cx);
8759 });
8760 cx.assert_editor_state(
8761 &r#"
8762 let x = "ˇ"
8763 "#
8764 .unindent(),
8765 );
8766
8767 // Inserting another quotation mark. The cursor moves across the existing
8768 // automatically-inserted quotation mark.
8769 cx.update_editor(|editor, window, cx| {
8770 editor.handle_input("\"", window, cx);
8771 });
8772 cx.assert_editor_state(
8773 &r#"
8774 let x = ""ˇ
8775 "#
8776 .unindent(),
8777 );
8778
8779 // Reset
8780 cx.set_state(
8781 &r#"
8782 let x = ˇ
8783 "#
8784 .unindent(),
8785 );
8786
8787 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
8788 cx.update_editor(|editor, window, cx| {
8789 editor.handle_input("\"", window, cx);
8790 editor.handle_input(" ", window, cx);
8791 editor.move_left(&Default::default(), window, cx);
8792 editor.handle_input("\\", window, cx);
8793 editor.handle_input("\"", window, cx);
8794 });
8795 cx.assert_editor_state(
8796 &r#"
8797 let x = "\"ˇ "
8798 "#
8799 .unindent(),
8800 );
8801
8802 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
8803 // mark. Nothing is inserted.
8804 cx.update_editor(|editor, window, cx| {
8805 editor.move_right(&Default::default(), window, cx);
8806 editor.handle_input("\"", window, cx);
8807 });
8808 cx.assert_editor_state(
8809 &r#"
8810 let x = "\" "ˇ
8811 "#
8812 .unindent(),
8813 );
8814}
8815
8816#[gpui::test]
8817async fn test_surround_with_pair(cx: &mut TestAppContext) {
8818 init_test(cx, |_| {});
8819
8820 let language = Arc::new(Language::new(
8821 LanguageConfig {
8822 brackets: BracketPairConfig {
8823 pairs: vec![
8824 BracketPair {
8825 start: "{".to_string(),
8826 end: "}".to_string(),
8827 close: true,
8828 surround: true,
8829 newline: true,
8830 },
8831 BracketPair {
8832 start: "/* ".to_string(),
8833 end: "*/".to_string(),
8834 close: true,
8835 surround: true,
8836 ..Default::default()
8837 },
8838 ],
8839 ..Default::default()
8840 },
8841 ..Default::default()
8842 },
8843 Some(tree_sitter_rust::LANGUAGE.into()),
8844 ));
8845
8846 let text = r#"
8847 a
8848 b
8849 c
8850 "#
8851 .unindent();
8852
8853 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8854 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8855 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8856 editor
8857 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8858 .await;
8859
8860 editor.update_in(cx, |editor, window, cx| {
8861 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8862 s.select_display_ranges([
8863 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8864 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8865 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
8866 ])
8867 });
8868
8869 editor.handle_input("{", window, cx);
8870 editor.handle_input("{", window, cx);
8871 editor.handle_input("{", window, cx);
8872 assert_eq!(
8873 editor.text(cx),
8874 "
8875 {{{a}}}
8876 {{{b}}}
8877 {{{c}}}
8878 "
8879 .unindent()
8880 );
8881 assert_eq!(
8882 editor.selections.display_ranges(cx),
8883 [
8884 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
8885 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
8886 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
8887 ]
8888 );
8889
8890 editor.undo(&Undo, window, cx);
8891 editor.undo(&Undo, window, cx);
8892 editor.undo(&Undo, window, cx);
8893 assert_eq!(
8894 editor.text(cx),
8895 "
8896 a
8897 b
8898 c
8899 "
8900 .unindent()
8901 );
8902 assert_eq!(
8903 editor.selections.display_ranges(cx),
8904 [
8905 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8906 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8907 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8908 ]
8909 );
8910
8911 // Ensure inserting the first character of a multi-byte bracket pair
8912 // doesn't surround the selections with the bracket.
8913 editor.handle_input("/", window, cx);
8914 assert_eq!(
8915 editor.text(cx),
8916 "
8917 /
8918 /
8919 /
8920 "
8921 .unindent()
8922 );
8923 assert_eq!(
8924 editor.selections.display_ranges(cx),
8925 [
8926 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8927 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8928 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8929 ]
8930 );
8931
8932 editor.undo(&Undo, window, cx);
8933 assert_eq!(
8934 editor.text(cx),
8935 "
8936 a
8937 b
8938 c
8939 "
8940 .unindent()
8941 );
8942 assert_eq!(
8943 editor.selections.display_ranges(cx),
8944 [
8945 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8946 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8947 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8948 ]
8949 );
8950
8951 // Ensure inserting the last character of a multi-byte bracket pair
8952 // doesn't surround the selections with the bracket.
8953 editor.handle_input("*", window, cx);
8954 assert_eq!(
8955 editor.text(cx),
8956 "
8957 *
8958 *
8959 *
8960 "
8961 .unindent()
8962 );
8963 assert_eq!(
8964 editor.selections.display_ranges(cx),
8965 [
8966 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8967 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8968 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8969 ]
8970 );
8971 });
8972}
8973
8974#[gpui::test]
8975async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
8976 init_test(cx, |_| {});
8977
8978 let language = Arc::new(Language::new(
8979 LanguageConfig {
8980 brackets: BracketPairConfig {
8981 pairs: vec![BracketPair {
8982 start: "{".to_string(),
8983 end: "}".to_string(),
8984 close: true,
8985 surround: true,
8986 newline: true,
8987 }],
8988 ..Default::default()
8989 },
8990 autoclose_before: "}".to_string(),
8991 ..Default::default()
8992 },
8993 Some(tree_sitter_rust::LANGUAGE.into()),
8994 ));
8995
8996 let text = r#"
8997 a
8998 b
8999 c
9000 "#
9001 .unindent();
9002
9003 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9004 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9005 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9006 editor
9007 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9008 .await;
9009
9010 editor.update_in(cx, |editor, window, cx| {
9011 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9012 s.select_ranges([
9013 Point::new(0, 1)..Point::new(0, 1),
9014 Point::new(1, 1)..Point::new(1, 1),
9015 Point::new(2, 1)..Point::new(2, 1),
9016 ])
9017 });
9018
9019 editor.handle_input("{", window, cx);
9020 editor.handle_input("{", window, cx);
9021 editor.handle_input("_", window, cx);
9022 assert_eq!(
9023 editor.text(cx),
9024 "
9025 a{{_}}
9026 b{{_}}
9027 c{{_}}
9028 "
9029 .unindent()
9030 );
9031 assert_eq!(
9032 editor.selections.ranges::<Point>(cx),
9033 [
9034 Point::new(0, 4)..Point::new(0, 4),
9035 Point::new(1, 4)..Point::new(1, 4),
9036 Point::new(2, 4)..Point::new(2, 4)
9037 ]
9038 );
9039
9040 editor.backspace(&Default::default(), window, cx);
9041 editor.backspace(&Default::default(), window, cx);
9042 assert_eq!(
9043 editor.text(cx),
9044 "
9045 a{}
9046 b{}
9047 c{}
9048 "
9049 .unindent()
9050 );
9051 assert_eq!(
9052 editor.selections.ranges::<Point>(cx),
9053 [
9054 Point::new(0, 2)..Point::new(0, 2),
9055 Point::new(1, 2)..Point::new(1, 2),
9056 Point::new(2, 2)..Point::new(2, 2)
9057 ]
9058 );
9059
9060 editor.delete_to_previous_word_start(&Default::default(), window, cx);
9061 assert_eq!(
9062 editor.text(cx),
9063 "
9064 a
9065 b
9066 c
9067 "
9068 .unindent()
9069 );
9070 assert_eq!(
9071 editor.selections.ranges::<Point>(cx),
9072 [
9073 Point::new(0, 1)..Point::new(0, 1),
9074 Point::new(1, 1)..Point::new(1, 1),
9075 Point::new(2, 1)..Point::new(2, 1)
9076 ]
9077 );
9078 });
9079}
9080
9081#[gpui::test]
9082async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
9083 init_test(cx, |settings| {
9084 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
9085 });
9086
9087 let mut cx = EditorTestContext::new(cx).await;
9088
9089 let language = Arc::new(Language::new(
9090 LanguageConfig {
9091 brackets: BracketPairConfig {
9092 pairs: vec![
9093 BracketPair {
9094 start: "{".to_string(),
9095 end: "}".to_string(),
9096 close: true,
9097 surround: true,
9098 newline: true,
9099 },
9100 BracketPair {
9101 start: "(".to_string(),
9102 end: ")".to_string(),
9103 close: true,
9104 surround: true,
9105 newline: true,
9106 },
9107 BracketPair {
9108 start: "[".to_string(),
9109 end: "]".to_string(),
9110 close: false,
9111 surround: true,
9112 newline: true,
9113 },
9114 ],
9115 ..Default::default()
9116 },
9117 autoclose_before: "})]".to_string(),
9118 ..Default::default()
9119 },
9120 Some(tree_sitter_rust::LANGUAGE.into()),
9121 ));
9122
9123 cx.language_registry().add(language.clone());
9124 cx.update_buffer(|buffer, cx| {
9125 buffer.set_language(Some(language), cx);
9126 });
9127
9128 cx.set_state(
9129 &"
9130 {(ˇ)}
9131 [[ˇ]]
9132 {(ˇ)}
9133 "
9134 .unindent(),
9135 );
9136
9137 cx.update_editor(|editor, window, cx| {
9138 editor.backspace(&Default::default(), window, cx);
9139 editor.backspace(&Default::default(), window, cx);
9140 });
9141
9142 cx.assert_editor_state(
9143 &"
9144 ˇ
9145 ˇ]]
9146 ˇ
9147 "
9148 .unindent(),
9149 );
9150
9151 cx.update_editor(|editor, window, cx| {
9152 editor.handle_input("{", window, cx);
9153 editor.handle_input("{", window, cx);
9154 editor.move_right(&MoveRight, window, cx);
9155 editor.move_right(&MoveRight, window, cx);
9156 editor.move_left(&MoveLeft, window, cx);
9157 editor.move_left(&MoveLeft, window, cx);
9158 editor.backspace(&Default::default(), window, cx);
9159 });
9160
9161 cx.assert_editor_state(
9162 &"
9163 {ˇ}
9164 {ˇ}]]
9165 {ˇ}
9166 "
9167 .unindent(),
9168 );
9169
9170 cx.update_editor(|editor, window, cx| {
9171 editor.backspace(&Default::default(), window, cx);
9172 });
9173
9174 cx.assert_editor_state(
9175 &"
9176 ˇ
9177 ˇ]]
9178 ˇ
9179 "
9180 .unindent(),
9181 );
9182}
9183
9184#[gpui::test]
9185async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
9186 init_test(cx, |_| {});
9187
9188 let language = Arc::new(Language::new(
9189 LanguageConfig::default(),
9190 Some(tree_sitter_rust::LANGUAGE.into()),
9191 ));
9192
9193 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
9194 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9195 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9196 editor
9197 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9198 .await;
9199
9200 editor.update_in(cx, |editor, window, cx| {
9201 editor.set_auto_replace_emoji_shortcode(true);
9202
9203 editor.handle_input("Hello ", window, cx);
9204 editor.handle_input(":wave", window, cx);
9205 assert_eq!(editor.text(cx), "Hello :wave".unindent());
9206
9207 editor.handle_input(":", window, cx);
9208 assert_eq!(editor.text(cx), "Hello 👋".unindent());
9209
9210 editor.handle_input(" :smile", window, cx);
9211 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
9212
9213 editor.handle_input(":", window, cx);
9214 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
9215
9216 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
9217 editor.handle_input(":wave", window, cx);
9218 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
9219
9220 editor.handle_input(":", window, cx);
9221 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
9222
9223 editor.handle_input(":1", window, cx);
9224 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
9225
9226 editor.handle_input(":", window, cx);
9227 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
9228
9229 // Ensure shortcode does not get replaced when it is part of a word
9230 editor.handle_input(" Test:wave", window, cx);
9231 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
9232
9233 editor.handle_input(":", window, cx);
9234 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
9235
9236 editor.set_auto_replace_emoji_shortcode(false);
9237
9238 // Ensure shortcode does not get replaced when auto replace is off
9239 editor.handle_input(" :wave", window, cx);
9240 assert_eq!(
9241 editor.text(cx),
9242 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
9243 );
9244
9245 editor.handle_input(":", window, cx);
9246 assert_eq!(
9247 editor.text(cx),
9248 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
9249 );
9250 });
9251}
9252
9253#[gpui::test]
9254async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
9255 init_test(cx, |_| {});
9256
9257 let (text, insertion_ranges) = marked_text_ranges(
9258 indoc! {"
9259 ˇ
9260 "},
9261 false,
9262 );
9263
9264 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
9265 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9266
9267 _ = editor.update_in(cx, |editor, window, cx| {
9268 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
9269
9270 editor
9271 .insert_snippet(&insertion_ranges, snippet, window, cx)
9272 .unwrap();
9273
9274 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
9275 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
9276 assert_eq!(editor.text(cx), expected_text);
9277 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
9278 }
9279
9280 assert(
9281 editor,
9282 cx,
9283 indoc! {"
9284 type «» =•
9285 "},
9286 );
9287
9288 assert!(editor.context_menu_visible(), "There should be a matches");
9289 });
9290}
9291
9292#[gpui::test]
9293async fn test_snippets(cx: &mut TestAppContext) {
9294 init_test(cx, |_| {});
9295
9296 let mut cx = EditorTestContext::new(cx).await;
9297
9298 cx.set_state(indoc! {"
9299 a.ˇ b
9300 a.ˇ b
9301 a.ˇ b
9302 "});
9303
9304 cx.update_editor(|editor, window, cx| {
9305 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
9306 let insertion_ranges = editor
9307 .selections
9308 .all(cx)
9309 .iter()
9310 .map(|s| s.range().clone())
9311 .collect::<Vec<_>>();
9312 editor
9313 .insert_snippet(&insertion_ranges, snippet, window, cx)
9314 .unwrap();
9315 });
9316
9317 cx.assert_editor_state(indoc! {"
9318 a.f(«oneˇ», two, «threeˇ») b
9319 a.f(«oneˇ», two, «threeˇ») b
9320 a.f(«oneˇ», two, «threeˇ») b
9321 "});
9322
9323 // Can't move earlier than the first tab stop
9324 cx.update_editor(|editor, window, cx| {
9325 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9326 });
9327 cx.assert_editor_state(indoc! {"
9328 a.f(«oneˇ», two, «threeˇ») b
9329 a.f(«oneˇ», two, «threeˇ») b
9330 a.f(«oneˇ», two, «threeˇ») b
9331 "});
9332
9333 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9334 cx.assert_editor_state(indoc! {"
9335 a.f(one, «twoˇ», three) b
9336 a.f(one, «twoˇ», three) b
9337 a.f(one, «twoˇ», three) b
9338 "});
9339
9340 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
9341 cx.assert_editor_state(indoc! {"
9342 a.f(«oneˇ», two, «threeˇ») b
9343 a.f(«oneˇ», two, «threeˇ») b
9344 a.f(«oneˇ», two, «threeˇ») b
9345 "});
9346
9347 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9348 cx.assert_editor_state(indoc! {"
9349 a.f(one, «twoˇ», three) b
9350 a.f(one, «twoˇ», three) b
9351 a.f(one, «twoˇ», three) b
9352 "});
9353 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9354 cx.assert_editor_state(indoc! {"
9355 a.f(one, two, three)ˇ b
9356 a.f(one, two, three)ˇ b
9357 a.f(one, two, three)ˇ b
9358 "});
9359
9360 // As soon as the last tab stop is reached, snippet state is gone
9361 cx.update_editor(|editor, window, cx| {
9362 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9363 });
9364 cx.assert_editor_state(indoc! {"
9365 a.f(one, two, three)ˇ b
9366 a.f(one, two, three)ˇ b
9367 a.f(one, two, three)ˇ b
9368 "});
9369}
9370
9371#[gpui::test]
9372async fn test_snippet_indentation(cx: &mut TestAppContext) {
9373 init_test(cx, |_| {});
9374
9375 let mut cx = EditorTestContext::new(cx).await;
9376
9377 cx.update_editor(|editor, window, cx| {
9378 let snippet = Snippet::parse(indoc! {"
9379 /*
9380 * Multiline comment with leading indentation
9381 *
9382 * $1
9383 */
9384 $0"})
9385 .unwrap();
9386 let insertion_ranges = editor
9387 .selections
9388 .all(cx)
9389 .iter()
9390 .map(|s| s.range().clone())
9391 .collect::<Vec<_>>();
9392 editor
9393 .insert_snippet(&insertion_ranges, snippet, window, cx)
9394 .unwrap();
9395 });
9396
9397 cx.assert_editor_state(indoc! {"
9398 /*
9399 * Multiline comment with leading indentation
9400 *
9401 * ˇ
9402 */
9403 "});
9404
9405 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9406 cx.assert_editor_state(indoc! {"
9407 /*
9408 * Multiline comment with leading indentation
9409 *
9410 *•
9411 */
9412 ˇ"});
9413}
9414
9415#[gpui::test]
9416async fn test_document_format_during_save(cx: &mut TestAppContext) {
9417 init_test(cx, |_| {});
9418
9419 let fs = FakeFs::new(cx.executor());
9420 fs.insert_file(path!("/file.rs"), Default::default()).await;
9421
9422 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
9423
9424 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9425 language_registry.add(rust_lang());
9426 let mut fake_servers = language_registry.register_fake_lsp(
9427 "Rust",
9428 FakeLspAdapter {
9429 capabilities: lsp::ServerCapabilities {
9430 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9431 ..Default::default()
9432 },
9433 ..Default::default()
9434 },
9435 );
9436
9437 let buffer = project
9438 .update(cx, |project, cx| {
9439 project.open_local_buffer(path!("/file.rs"), cx)
9440 })
9441 .await
9442 .unwrap();
9443
9444 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9445 let (editor, cx) = cx.add_window_view(|window, cx| {
9446 build_editor_with_project(project.clone(), buffer, window, cx)
9447 });
9448 editor.update_in(cx, |editor, window, cx| {
9449 editor.set_text("one\ntwo\nthree\n", window, cx)
9450 });
9451 assert!(cx.read(|cx| editor.is_dirty(cx)));
9452
9453 cx.executor().start_waiting();
9454 let fake_server = fake_servers.next().await.unwrap();
9455
9456 {
9457 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9458 move |params, _| async move {
9459 assert_eq!(
9460 params.text_document.uri,
9461 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9462 );
9463 assert_eq!(params.options.tab_size, 4);
9464 Ok(Some(vec![lsp::TextEdit::new(
9465 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9466 ", ".to_string(),
9467 )]))
9468 },
9469 );
9470 let save = editor
9471 .update_in(cx, |editor, window, cx| {
9472 editor.save(
9473 SaveOptions {
9474 format: true,
9475 autosave: false,
9476 },
9477 project.clone(),
9478 window,
9479 cx,
9480 )
9481 })
9482 .unwrap();
9483 cx.executor().start_waiting();
9484 save.await;
9485
9486 assert_eq!(
9487 editor.update(cx, |editor, cx| editor.text(cx)),
9488 "one, two\nthree\n"
9489 );
9490 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9491 }
9492
9493 {
9494 editor.update_in(cx, |editor, window, cx| {
9495 editor.set_text("one\ntwo\nthree\n", window, cx)
9496 });
9497 assert!(cx.read(|cx| editor.is_dirty(cx)));
9498
9499 // Ensure we can still save even if formatting hangs.
9500 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9501 move |params, _| async move {
9502 assert_eq!(
9503 params.text_document.uri,
9504 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9505 );
9506 futures::future::pending::<()>().await;
9507 unreachable!()
9508 },
9509 );
9510 let save = editor
9511 .update_in(cx, |editor, window, cx| {
9512 editor.save(
9513 SaveOptions {
9514 format: true,
9515 autosave: false,
9516 },
9517 project.clone(),
9518 window,
9519 cx,
9520 )
9521 })
9522 .unwrap();
9523 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9524 cx.executor().start_waiting();
9525 save.await;
9526 assert_eq!(
9527 editor.update(cx, |editor, cx| editor.text(cx)),
9528 "one\ntwo\nthree\n"
9529 );
9530 }
9531
9532 // Set rust language override and assert overridden tabsize is sent to language server
9533 update_test_language_settings(cx, |settings| {
9534 settings.languages.0.insert(
9535 "Rust".into(),
9536 LanguageSettingsContent {
9537 tab_size: NonZeroU32::new(8),
9538 ..Default::default()
9539 },
9540 );
9541 });
9542
9543 {
9544 editor.update_in(cx, |editor, window, cx| {
9545 editor.set_text("somehting_new\n", window, cx)
9546 });
9547 assert!(cx.read(|cx| editor.is_dirty(cx)));
9548 let _formatting_request_signal = fake_server
9549 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9550 assert_eq!(
9551 params.text_document.uri,
9552 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9553 );
9554 assert_eq!(params.options.tab_size, 8);
9555 Ok(Some(vec![]))
9556 });
9557 let save = editor
9558 .update_in(cx, |editor, window, cx| {
9559 editor.save(
9560 SaveOptions {
9561 format: true,
9562 autosave: false,
9563 },
9564 project.clone(),
9565 window,
9566 cx,
9567 )
9568 })
9569 .unwrap();
9570 cx.executor().start_waiting();
9571 save.await;
9572 }
9573}
9574
9575#[gpui::test]
9576async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
9577 init_test(cx, |_| {});
9578
9579 let cols = 4;
9580 let rows = 10;
9581 let sample_text_1 = sample_text(rows, cols, 'a');
9582 assert_eq!(
9583 sample_text_1,
9584 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
9585 );
9586 let sample_text_2 = sample_text(rows, cols, 'l');
9587 assert_eq!(
9588 sample_text_2,
9589 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
9590 );
9591 let sample_text_3 = sample_text(rows, cols, 'v');
9592 assert_eq!(
9593 sample_text_3,
9594 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
9595 );
9596
9597 let fs = FakeFs::new(cx.executor());
9598 fs.insert_tree(
9599 path!("/a"),
9600 json!({
9601 "main.rs": sample_text_1,
9602 "other.rs": sample_text_2,
9603 "lib.rs": sample_text_3,
9604 }),
9605 )
9606 .await;
9607
9608 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
9609 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9610 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9611
9612 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9613 language_registry.add(rust_lang());
9614 let mut fake_servers = language_registry.register_fake_lsp(
9615 "Rust",
9616 FakeLspAdapter {
9617 capabilities: lsp::ServerCapabilities {
9618 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9619 ..Default::default()
9620 },
9621 ..Default::default()
9622 },
9623 );
9624
9625 let worktree = project.update(cx, |project, cx| {
9626 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
9627 assert_eq!(worktrees.len(), 1);
9628 worktrees.pop().unwrap()
9629 });
9630 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9631
9632 let buffer_1 = project
9633 .update(cx, |project, cx| {
9634 project.open_buffer((worktree_id, "main.rs"), cx)
9635 })
9636 .await
9637 .unwrap();
9638 let buffer_2 = project
9639 .update(cx, |project, cx| {
9640 project.open_buffer((worktree_id, "other.rs"), cx)
9641 })
9642 .await
9643 .unwrap();
9644 let buffer_3 = project
9645 .update(cx, |project, cx| {
9646 project.open_buffer((worktree_id, "lib.rs"), cx)
9647 })
9648 .await
9649 .unwrap();
9650
9651 let multi_buffer = cx.new(|cx| {
9652 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9653 multi_buffer.push_excerpts(
9654 buffer_1.clone(),
9655 [
9656 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9657 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9658 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9659 ],
9660 cx,
9661 );
9662 multi_buffer.push_excerpts(
9663 buffer_2.clone(),
9664 [
9665 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9666 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9667 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9668 ],
9669 cx,
9670 );
9671 multi_buffer.push_excerpts(
9672 buffer_3.clone(),
9673 [
9674 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9675 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9676 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9677 ],
9678 cx,
9679 );
9680 multi_buffer
9681 });
9682 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
9683 Editor::new(
9684 EditorMode::full(),
9685 multi_buffer,
9686 Some(project.clone()),
9687 window,
9688 cx,
9689 )
9690 });
9691
9692 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9693 editor.change_selections(
9694 SelectionEffects::scroll(Autoscroll::Next),
9695 window,
9696 cx,
9697 |s| s.select_ranges(Some(1..2)),
9698 );
9699 editor.insert("|one|two|three|", window, cx);
9700 });
9701 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9702 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9703 editor.change_selections(
9704 SelectionEffects::scroll(Autoscroll::Next),
9705 window,
9706 cx,
9707 |s| s.select_ranges(Some(60..70)),
9708 );
9709 editor.insert("|four|five|six|", window, cx);
9710 });
9711 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9712
9713 // First two buffers should be edited, but not the third one.
9714 assert_eq!(
9715 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9716 "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}",
9717 );
9718 buffer_1.update(cx, |buffer, _| {
9719 assert!(buffer.is_dirty());
9720 assert_eq!(
9721 buffer.text(),
9722 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
9723 )
9724 });
9725 buffer_2.update(cx, |buffer, _| {
9726 assert!(buffer.is_dirty());
9727 assert_eq!(
9728 buffer.text(),
9729 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
9730 )
9731 });
9732 buffer_3.update(cx, |buffer, _| {
9733 assert!(!buffer.is_dirty());
9734 assert_eq!(buffer.text(), sample_text_3,)
9735 });
9736 cx.executor().run_until_parked();
9737
9738 cx.executor().start_waiting();
9739 let save = multi_buffer_editor
9740 .update_in(cx, |editor, window, cx| {
9741 editor.save(
9742 SaveOptions {
9743 format: true,
9744 autosave: false,
9745 },
9746 project.clone(),
9747 window,
9748 cx,
9749 )
9750 })
9751 .unwrap();
9752
9753 let fake_server = fake_servers.next().await.unwrap();
9754 fake_server
9755 .server
9756 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
9757 Ok(Some(vec![lsp::TextEdit::new(
9758 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9759 format!("[{} formatted]", params.text_document.uri),
9760 )]))
9761 })
9762 .detach();
9763 save.await;
9764
9765 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
9766 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
9767 assert_eq!(
9768 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9769 uri!(
9770 "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}"
9771 ),
9772 );
9773 buffer_1.update(cx, |buffer, _| {
9774 assert!(!buffer.is_dirty());
9775 assert_eq!(
9776 buffer.text(),
9777 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
9778 )
9779 });
9780 buffer_2.update(cx, |buffer, _| {
9781 assert!(!buffer.is_dirty());
9782 assert_eq!(
9783 buffer.text(),
9784 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
9785 )
9786 });
9787 buffer_3.update(cx, |buffer, _| {
9788 assert!(!buffer.is_dirty());
9789 assert_eq!(buffer.text(), sample_text_3,)
9790 });
9791}
9792
9793#[gpui::test]
9794async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
9795 init_test(cx, |_| {});
9796
9797 let fs = FakeFs::new(cx.executor());
9798 fs.insert_tree(
9799 path!("/dir"),
9800 json!({
9801 "file1.rs": "fn main() { println!(\"hello\"); }",
9802 "file2.rs": "fn test() { println!(\"test\"); }",
9803 "file3.rs": "fn other() { println!(\"other\"); }\n",
9804 }),
9805 )
9806 .await;
9807
9808 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
9809 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9810 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9811
9812 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9813 language_registry.add(rust_lang());
9814
9815 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
9816 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9817
9818 // Open three buffers
9819 let buffer_1 = project
9820 .update(cx, |project, cx| {
9821 project.open_buffer((worktree_id, "file1.rs"), cx)
9822 })
9823 .await
9824 .unwrap();
9825 let buffer_2 = project
9826 .update(cx, |project, cx| {
9827 project.open_buffer((worktree_id, "file2.rs"), cx)
9828 })
9829 .await
9830 .unwrap();
9831 let buffer_3 = project
9832 .update(cx, |project, cx| {
9833 project.open_buffer((worktree_id, "file3.rs"), cx)
9834 })
9835 .await
9836 .unwrap();
9837
9838 // Create a multi-buffer with all three buffers
9839 let multi_buffer = cx.new(|cx| {
9840 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9841 multi_buffer.push_excerpts(
9842 buffer_1.clone(),
9843 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9844 cx,
9845 );
9846 multi_buffer.push_excerpts(
9847 buffer_2.clone(),
9848 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9849 cx,
9850 );
9851 multi_buffer.push_excerpts(
9852 buffer_3.clone(),
9853 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9854 cx,
9855 );
9856 multi_buffer
9857 });
9858
9859 let editor = cx.new_window_entity(|window, cx| {
9860 Editor::new(
9861 EditorMode::full(),
9862 multi_buffer,
9863 Some(project.clone()),
9864 window,
9865 cx,
9866 )
9867 });
9868
9869 // Edit only the first buffer
9870 editor.update_in(cx, |editor, window, cx| {
9871 editor.change_selections(
9872 SelectionEffects::scroll(Autoscroll::Next),
9873 window,
9874 cx,
9875 |s| s.select_ranges(Some(10..10)),
9876 );
9877 editor.insert("// edited", window, cx);
9878 });
9879
9880 // Verify that only buffer 1 is dirty
9881 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
9882 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9883 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9884
9885 // Get write counts after file creation (files were created with initial content)
9886 // We expect each file to have been written once during creation
9887 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
9888 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
9889 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
9890
9891 // Perform autosave
9892 let save_task = editor.update_in(cx, |editor, window, cx| {
9893 editor.save(
9894 SaveOptions {
9895 format: true,
9896 autosave: true,
9897 },
9898 project.clone(),
9899 window,
9900 cx,
9901 )
9902 });
9903 save_task.await.unwrap();
9904
9905 // Only the dirty buffer should have been saved
9906 assert_eq!(
9907 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
9908 1,
9909 "Buffer 1 was dirty, so it should have been written once during autosave"
9910 );
9911 assert_eq!(
9912 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
9913 0,
9914 "Buffer 2 was clean, so it should not have been written during autosave"
9915 );
9916 assert_eq!(
9917 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
9918 0,
9919 "Buffer 3 was clean, so it should not have been written during autosave"
9920 );
9921
9922 // Verify buffer states after autosave
9923 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9924 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9925 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9926
9927 // Now perform a manual save (format = true)
9928 let save_task = editor.update_in(cx, |editor, window, cx| {
9929 editor.save(
9930 SaveOptions {
9931 format: true,
9932 autosave: false,
9933 },
9934 project.clone(),
9935 window,
9936 cx,
9937 )
9938 });
9939 save_task.await.unwrap();
9940
9941 // During manual save, clean buffers don't get written to disk
9942 // They just get did_save called for language server notifications
9943 assert_eq!(
9944 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
9945 1,
9946 "Buffer 1 should only have been written once total (during autosave, not manual save)"
9947 );
9948 assert_eq!(
9949 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
9950 0,
9951 "Buffer 2 should not have been written at all"
9952 );
9953 assert_eq!(
9954 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
9955 0,
9956 "Buffer 3 should not have been written at all"
9957 );
9958}
9959
9960#[gpui::test]
9961async fn test_range_format_during_save(cx: &mut TestAppContext) {
9962 init_test(cx, |_| {});
9963
9964 let fs = FakeFs::new(cx.executor());
9965 fs.insert_file(path!("/file.rs"), Default::default()).await;
9966
9967 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9968
9969 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9970 language_registry.add(rust_lang());
9971 let mut fake_servers = language_registry.register_fake_lsp(
9972 "Rust",
9973 FakeLspAdapter {
9974 capabilities: lsp::ServerCapabilities {
9975 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
9976 ..Default::default()
9977 },
9978 ..Default::default()
9979 },
9980 );
9981
9982 let buffer = project
9983 .update(cx, |project, cx| {
9984 project.open_local_buffer(path!("/file.rs"), cx)
9985 })
9986 .await
9987 .unwrap();
9988
9989 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9990 let (editor, cx) = cx.add_window_view(|window, cx| {
9991 build_editor_with_project(project.clone(), buffer, window, cx)
9992 });
9993 editor.update_in(cx, |editor, window, cx| {
9994 editor.set_text("one\ntwo\nthree\n", window, cx)
9995 });
9996 assert!(cx.read(|cx| editor.is_dirty(cx)));
9997
9998 cx.executor().start_waiting();
9999 let fake_server = fake_servers.next().await.unwrap();
10000
10001 let save = editor
10002 .update_in(cx, |editor, window, cx| {
10003 editor.save(
10004 SaveOptions {
10005 format: true,
10006 autosave: false,
10007 },
10008 project.clone(),
10009 window,
10010 cx,
10011 )
10012 })
10013 .unwrap();
10014 fake_server
10015 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10016 assert_eq!(
10017 params.text_document.uri,
10018 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10019 );
10020 assert_eq!(params.options.tab_size, 4);
10021 Ok(Some(vec![lsp::TextEdit::new(
10022 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10023 ", ".to_string(),
10024 )]))
10025 })
10026 .next()
10027 .await;
10028 cx.executor().start_waiting();
10029 save.await;
10030 assert_eq!(
10031 editor.update(cx, |editor, cx| editor.text(cx)),
10032 "one, two\nthree\n"
10033 );
10034 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10035
10036 editor.update_in(cx, |editor, window, cx| {
10037 editor.set_text("one\ntwo\nthree\n", window, cx)
10038 });
10039 assert!(cx.read(|cx| editor.is_dirty(cx)));
10040
10041 // Ensure we can still save even if formatting hangs.
10042 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
10043 move |params, _| async move {
10044 assert_eq!(
10045 params.text_document.uri,
10046 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10047 );
10048 futures::future::pending::<()>().await;
10049 unreachable!()
10050 },
10051 );
10052 let save = editor
10053 .update_in(cx, |editor, window, cx| {
10054 editor.save(
10055 SaveOptions {
10056 format: true,
10057 autosave: false,
10058 },
10059 project.clone(),
10060 window,
10061 cx,
10062 )
10063 })
10064 .unwrap();
10065 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10066 cx.executor().start_waiting();
10067 save.await;
10068 assert_eq!(
10069 editor.update(cx, |editor, cx| editor.text(cx)),
10070 "one\ntwo\nthree\n"
10071 );
10072 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10073
10074 // For non-dirty buffer, no formatting request should be sent
10075 let save = editor
10076 .update_in(cx, |editor, window, cx| {
10077 editor.save(
10078 SaveOptions {
10079 format: false,
10080 autosave: false,
10081 },
10082 project.clone(),
10083 window,
10084 cx,
10085 )
10086 })
10087 .unwrap();
10088 let _pending_format_request = fake_server
10089 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
10090 panic!("Should not be invoked");
10091 })
10092 .next();
10093 cx.executor().start_waiting();
10094 save.await;
10095
10096 // Set Rust language override and assert overridden tabsize is sent to language server
10097 update_test_language_settings(cx, |settings| {
10098 settings.languages.0.insert(
10099 "Rust".into(),
10100 LanguageSettingsContent {
10101 tab_size: NonZeroU32::new(8),
10102 ..Default::default()
10103 },
10104 );
10105 });
10106
10107 editor.update_in(cx, |editor, window, cx| {
10108 editor.set_text("somehting_new\n", window, cx)
10109 });
10110 assert!(cx.read(|cx| editor.is_dirty(cx)));
10111 let save = editor
10112 .update_in(cx, |editor, window, cx| {
10113 editor.save(
10114 SaveOptions {
10115 format: true,
10116 autosave: false,
10117 },
10118 project.clone(),
10119 window,
10120 cx,
10121 )
10122 })
10123 .unwrap();
10124 fake_server
10125 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10126 assert_eq!(
10127 params.text_document.uri,
10128 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10129 );
10130 assert_eq!(params.options.tab_size, 8);
10131 Ok(Some(Vec::new()))
10132 })
10133 .next()
10134 .await;
10135 save.await;
10136}
10137
10138#[gpui::test]
10139async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
10140 init_test(cx, |settings| {
10141 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
10142 Formatter::LanguageServer { name: None },
10143 )))
10144 });
10145
10146 let fs = FakeFs::new(cx.executor());
10147 fs.insert_file(path!("/file.rs"), Default::default()).await;
10148
10149 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10150
10151 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10152 language_registry.add(Arc::new(Language::new(
10153 LanguageConfig {
10154 name: "Rust".into(),
10155 matcher: LanguageMatcher {
10156 path_suffixes: vec!["rs".to_string()],
10157 ..Default::default()
10158 },
10159 ..LanguageConfig::default()
10160 },
10161 Some(tree_sitter_rust::LANGUAGE.into()),
10162 )));
10163 update_test_language_settings(cx, |settings| {
10164 // Enable Prettier formatting for the same buffer, and ensure
10165 // LSP is called instead of Prettier.
10166 settings.defaults.prettier = Some(PrettierSettings {
10167 allowed: true,
10168 ..PrettierSettings::default()
10169 });
10170 });
10171 let mut fake_servers = language_registry.register_fake_lsp(
10172 "Rust",
10173 FakeLspAdapter {
10174 capabilities: lsp::ServerCapabilities {
10175 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10176 ..Default::default()
10177 },
10178 ..Default::default()
10179 },
10180 );
10181
10182 let buffer = project
10183 .update(cx, |project, cx| {
10184 project.open_local_buffer(path!("/file.rs"), cx)
10185 })
10186 .await
10187 .unwrap();
10188
10189 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10190 let (editor, cx) = cx.add_window_view(|window, cx| {
10191 build_editor_with_project(project.clone(), buffer, window, cx)
10192 });
10193 editor.update_in(cx, |editor, window, cx| {
10194 editor.set_text("one\ntwo\nthree\n", window, cx)
10195 });
10196
10197 cx.executor().start_waiting();
10198 let fake_server = fake_servers.next().await.unwrap();
10199
10200 let format = editor
10201 .update_in(cx, |editor, window, cx| {
10202 editor.perform_format(
10203 project.clone(),
10204 FormatTrigger::Manual,
10205 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10206 window,
10207 cx,
10208 )
10209 })
10210 .unwrap();
10211 fake_server
10212 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
10213 assert_eq!(
10214 params.text_document.uri,
10215 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10216 );
10217 assert_eq!(params.options.tab_size, 4);
10218 Ok(Some(vec![lsp::TextEdit::new(
10219 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10220 ", ".to_string(),
10221 )]))
10222 })
10223 .next()
10224 .await;
10225 cx.executor().start_waiting();
10226 format.await;
10227 assert_eq!(
10228 editor.update(cx, |editor, cx| editor.text(cx)),
10229 "one, two\nthree\n"
10230 );
10231
10232 editor.update_in(cx, |editor, window, cx| {
10233 editor.set_text("one\ntwo\nthree\n", window, cx)
10234 });
10235 // Ensure we don't lock if formatting hangs.
10236 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10237 move |params, _| async move {
10238 assert_eq!(
10239 params.text_document.uri,
10240 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10241 );
10242 futures::future::pending::<()>().await;
10243 unreachable!()
10244 },
10245 );
10246 let format = editor
10247 .update_in(cx, |editor, window, cx| {
10248 editor.perform_format(
10249 project,
10250 FormatTrigger::Manual,
10251 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10252 window,
10253 cx,
10254 )
10255 })
10256 .unwrap();
10257 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10258 cx.executor().start_waiting();
10259 format.await;
10260 assert_eq!(
10261 editor.update(cx, |editor, cx| editor.text(cx)),
10262 "one\ntwo\nthree\n"
10263 );
10264}
10265
10266#[gpui::test]
10267async fn test_multiple_formatters(cx: &mut TestAppContext) {
10268 init_test(cx, |settings| {
10269 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
10270 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10271 Formatter::LanguageServer { name: None },
10272 Formatter::CodeActions(
10273 [
10274 ("code-action-1".into(), true),
10275 ("code-action-2".into(), true),
10276 ]
10277 .into_iter()
10278 .collect(),
10279 ),
10280 ])))
10281 });
10282
10283 let fs = FakeFs::new(cx.executor());
10284 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
10285 .await;
10286
10287 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10288 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10289 language_registry.add(rust_lang());
10290
10291 let mut fake_servers = language_registry.register_fake_lsp(
10292 "Rust",
10293 FakeLspAdapter {
10294 capabilities: lsp::ServerCapabilities {
10295 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10296 execute_command_provider: Some(lsp::ExecuteCommandOptions {
10297 commands: vec!["the-command-for-code-action-1".into()],
10298 ..Default::default()
10299 }),
10300 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10301 ..Default::default()
10302 },
10303 ..Default::default()
10304 },
10305 );
10306
10307 let buffer = project
10308 .update(cx, |project, cx| {
10309 project.open_local_buffer(path!("/file.rs"), cx)
10310 })
10311 .await
10312 .unwrap();
10313
10314 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10315 let (editor, cx) = cx.add_window_view(|window, cx| {
10316 build_editor_with_project(project.clone(), buffer, window, cx)
10317 });
10318
10319 cx.executor().start_waiting();
10320
10321 let fake_server = fake_servers.next().await.unwrap();
10322 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10323 move |_params, _| async move {
10324 Ok(Some(vec![lsp::TextEdit::new(
10325 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10326 "applied-formatting\n".to_string(),
10327 )]))
10328 },
10329 );
10330 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10331 move |params, _| async move {
10332 assert_eq!(
10333 params.context.only,
10334 Some(vec!["code-action-1".into(), "code-action-2".into()])
10335 );
10336 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
10337 Ok(Some(vec![
10338 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10339 kind: Some("code-action-1".into()),
10340 edit: Some(lsp::WorkspaceEdit::new(
10341 [(
10342 uri.clone(),
10343 vec![lsp::TextEdit::new(
10344 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10345 "applied-code-action-1-edit\n".to_string(),
10346 )],
10347 )]
10348 .into_iter()
10349 .collect(),
10350 )),
10351 command: Some(lsp::Command {
10352 command: "the-command-for-code-action-1".into(),
10353 ..Default::default()
10354 }),
10355 ..Default::default()
10356 }),
10357 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10358 kind: Some("code-action-2".into()),
10359 edit: Some(lsp::WorkspaceEdit::new(
10360 [(
10361 uri.clone(),
10362 vec![lsp::TextEdit::new(
10363 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10364 "applied-code-action-2-edit\n".to_string(),
10365 )],
10366 )]
10367 .into_iter()
10368 .collect(),
10369 )),
10370 ..Default::default()
10371 }),
10372 ]))
10373 },
10374 );
10375
10376 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
10377 move |params, _| async move { Ok(params) }
10378 });
10379
10380 let command_lock = Arc::new(futures::lock::Mutex::new(()));
10381 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
10382 let fake = fake_server.clone();
10383 let lock = command_lock.clone();
10384 move |params, _| {
10385 assert_eq!(params.command, "the-command-for-code-action-1");
10386 let fake = fake.clone();
10387 let lock = lock.clone();
10388 async move {
10389 lock.lock().await;
10390 fake.server
10391 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
10392 label: None,
10393 edit: lsp::WorkspaceEdit {
10394 changes: Some(
10395 [(
10396 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
10397 vec![lsp::TextEdit {
10398 range: lsp::Range::new(
10399 lsp::Position::new(0, 0),
10400 lsp::Position::new(0, 0),
10401 ),
10402 new_text: "applied-code-action-1-command\n".into(),
10403 }],
10404 )]
10405 .into_iter()
10406 .collect(),
10407 ),
10408 ..Default::default()
10409 },
10410 })
10411 .await
10412 .into_response()
10413 .unwrap();
10414 Ok(Some(json!(null)))
10415 }
10416 }
10417 });
10418
10419 cx.executor().start_waiting();
10420 editor
10421 .update_in(cx, |editor, window, cx| {
10422 editor.perform_format(
10423 project.clone(),
10424 FormatTrigger::Manual,
10425 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10426 window,
10427 cx,
10428 )
10429 })
10430 .unwrap()
10431 .await;
10432 editor.update(cx, |editor, cx| {
10433 assert_eq!(
10434 editor.text(cx),
10435 r#"
10436 applied-code-action-2-edit
10437 applied-code-action-1-command
10438 applied-code-action-1-edit
10439 applied-formatting
10440 one
10441 two
10442 three
10443 "#
10444 .unindent()
10445 );
10446 });
10447
10448 editor.update_in(cx, |editor, window, cx| {
10449 editor.undo(&Default::default(), window, cx);
10450 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10451 });
10452
10453 // Perform a manual edit while waiting for an LSP command
10454 // that's being run as part of a formatting code action.
10455 let lock_guard = command_lock.lock().await;
10456 let format = editor
10457 .update_in(cx, |editor, window, cx| {
10458 editor.perform_format(
10459 project.clone(),
10460 FormatTrigger::Manual,
10461 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10462 window,
10463 cx,
10464 )
10465 })
10466 .unwrap();
10467 cx.run_until_parked();
10468 editor.update(cx, |editor, cx| {
10469 assert_eq!(
10470 editor.text(cx),
10471 r#"
10472 applied-code-action-1-edit
10473 applied-formatting
10474 one
10475 two
10476 three
10477 "#
10478 .unindent()
10479 );
10480
10481 editor.buffer.update(cx, |buffer, cx| {
10482 let ix = buffer.len(cx);
10483 buffer.edit([(ix..ix, "edited\n")], None, cx);
10484 });
10485 });
10486
10487 // Allow the LSP command to proceed. Because the buffer was edited,
10488 // the second code action will not be run.
10489 drop(lock_guard);
10490 format.await;
10491 editor.update_in(cx, |editor, window, cx| {
10492 assert_eq!(
10493 editor.text(cx),
10494 r#"
10495 applied-code-action-1-command
10496 applied-code-action-1-edit
10497 applied-formatting
10498 one
10499 two
10500 three
10501 edited
10502 "#
10503 .unindent()
10504 );
10505
10506 // The manual edit is undone first, because it is the last thing the user did
10507 // (even though the command completed afterwards).
10508 editor.undo(&Default::default(), window, cx);
10509 assert_eq!(
10510 editor.text(cx),
10511 r#"
10512 applied-code-action-1-command
10513 applied-code-action-1-edit
10514 applied-formatting
10515 one
10516 two
10517 three
10518 "#
10519 .unindent()
10520 );
10521
10522 // All the formatting (including the command, which completed after the manual edit)
10523 // is undone together.
10524 editor.undo(&Default::default(), window, cx);
10525 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10526 });
10527}
10528
10529#[gpui::test]
10530async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
10531 init_test(cx, |settings| {
10532 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10533 Formatter::LanguageServer { name: None },
10534 ])))
10535 });
10536
10537 let fs = FakeFs::new(cx.executor());
10538 fs.insert_file(path!("/file.ts"), Default::default()).await;
10539
10540 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10541
10542 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10543 language_registry.add(Arc::new(Language::new(
10544 LanguageConfig {
10545 name: "TypeScript".into(),
10546 matcher: LanguageMatcher {
10547 path_suffixes: vec!["ts".to_string()],
10548 ..Default::default()
10549 },
10550 ..LanguageConfig::default()
10551 },
10552 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
10553 )));
10554 update_test_language_settings(cx, |settings| {
10555 settings.defaults.prettier = Some(PrettierSettings {
10556 allowed: true,
10557 ..PrettierSettings::default()
10558 });
10559 });
10560 let mut fake_servers = language_registry.register_fake_lsp(
10561 "TypeScript",
10562 FakeLspAdapter {
10563 capabilities: lsp::ServerCapabilities {
10564 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10565 ..Default::default()
10566 },
10567 ..Default::default()
10568 },
10569 );
10570
10571 let buffer = project
10572 .update(cx, |project, cx| {
10573 project.open_local_buffer(path!("/file.ts"), cx)
10574 })
10575 .await
10576 .unwrap();
10577
10578 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10579 let (editor, cx) = cx.add_window_view(|window, cx| {
10580 build_editor_with_project(project.clone(), buffer, window, cx)
10581 });
10582 editor.update_in(cx, |editor, window, cx| {
10583 editor.set_text(
10584 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10585 window,
10586 cx,
10587 )
10588 });
10589
10590 cx.executor().start_waiting();
10591 let fake_server = fake_servers.next().await.unwrap();
10592
10593 let format = editor
10594 .update_in(cx, |editor, window, cx| {
10595 editor.perform_code_action_kind(
10596 project.clone(),
10597 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10598 window,
10599 cx,
10600 )
10601 })
10602 .unwrap();
10603 fake_server
10604 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
10605 assert_eq!(
10606 params.text_document.uri,
10607 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10608 );
10609 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
10610 lsp::CodeAction {
10611 title: "Organize Imports".to_string(),
10612 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
10613 edit: Some(lsp::WorkspaceEdit {
10614 changes: Some(
10615 [(
10616 params.text_document.uri.clone(),
10617 vec![lsp::TextEdit::new(
10618 lsp::Range::new(
10619 lsp::Position::new(1, 0),
10620 lsp::Position::new(2, 0),
10621 ),
10622 "".to_string(),
10623 )],
10624 )]
10625 .into_iter()
10626 .collect(),
10627 ),
10628 ..Default::default()
10629 }),
10630 ..Default::default()
10631 },
10632 )]))
10633 })
10634 .next()
10635 .await;
10636 cx.executor().start_waiting();
10637 format.await;
10638 assert_eq!(
10639 editor.update(cx, |editor, cx| editor.text(cx)),
10640 "import { a } from 'module';\n\nconst x = a;\n"
10641 );
10642
10643 editor.update_in(cx, |editor, window, cx| {
10644 editor.set_text(
10645 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10646 window,
10647 cx,
10648 )
10649 });
10650 // Ensure we don't lock if code action hangs.
10651 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10652 move |params, _| async move {
10653 assert_eq!(
10654 params.text_document.uri,
10655 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10656 );
10657 futures::future::pending::<()>().await;
10658 unreachable!()
10659 },
10660 );
10661 let format = editor
10662 .update_in(cx, |editor, window, cx| {
10663 editor.perform_code_action_kind(
10664 project,
10665 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10666 window,
10667 cx,
10668 )
10669 })
10670 .unwrap();
10671 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
10672 cx.executor().start_waiting();
10673 format.await;
10674 assert_eq!(
10675 editor.update(cx, |editor, cx| editor.text(cx)),
10676 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
10677 );
10678}
10679
10680#[gpui::test]
10681async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
10682 init_test(cx, |_| {});
10683
10684 let mut cx = EditorLspTestContext::new_rust(
10685 lsp::ServerCapabilities {
10686 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10687 ..Default::default()
10688 },
10689 cx,
10690 )
10691 .await;
10692
10693 cx.set_state(indoc! {"
10694 one.twoˇ
10695 "});
10696
10697 // The format request takes a long time. When it completes, it inserts
10698 // a newline and an indent before the `.`
10699 cx.lsp
10700 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
10701 let executor = cx.background_executor().clone();
10702 async move {
10703 executor.timer(Duration::from_millis(100)).await;
10704 Ok(Some(vec![lsp::TextEdit {
10705 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
10706 new_text: "\n ".into(),
10707 }]))
10708 }
10709 });
10710
10711 // Submit a format request.
10712 let format_1 = cx
10713 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10714 .unwrap();
10715 cx.executor().run_until_parked();
10716
10717 // Submit a second format request.
10718 let format_2 = cx
10719 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10720 .unwrap();
10721 cx.executor().run_until_parked();
10722
10723 // Wait for both format requests to complete
10724 cx.executor().advance_clock(Duration::from_millis(200));
10725 cx.executor().start_waiting();
10726 format_1.await.unwrap();
10727 cx.executor().start_waiting();
10728 format_2.await.unwrap();
10729
10730 // The formatting edits only happens once.
10731 cx.assert_editor_state(indoc! {"
10732 one
10733 .twoˇ
10734 "});
10735}
10736
10737#[gpui::test]
10738async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
10739 init_test(cx, |settings| {
10740 settings.defaults.formatter = Some(SelectedFormatter::Auto)
10741 });
10742
10743 let mut cx = EditorLspTestContext::new_rust(
10744 lsp::ServerCapabilities {
10745 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10746 ..Default::default()
10747 },
10748 cx,
10749 )
10750 .await;
10751
10752 // Set up a buffer white some trailing whitespace and no trailing newline.
10753 cx.set_state(
10754 &[
10755 "one ", //
10756 "twoˇ", //
10757 "three ", //
10758 "four", //
10759 ]
10760 .join("\n"),
10761 );
10762
10763 // Submit a format request.
10764 let format = cx
10765 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10766 .unwrap();
10767
10768 // Record which buffer changes have been sent to the language server
10769 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
10770 cx.lsp
10771 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
10772 let buffer_changes = buffer_changes.clone();
10773 move |params, _| {
10774 buffer_changes.lock().extend(
10775 params
10776 .content_changes
10777 .into_iter()
10778 .map(|e| (e.range.unwrap(), e.text)),
10779 );
10780 }
10781 });
10782
10783 // Handle formatting requests to the language server.
10784 cx.lsp
10785 .set_request_handler::<lsp::request::Formatting, _, _>({
10786 let buffer_changes = buffer_changes.clone();
10787 move |_, _| {
10788 // When formatting is requested, trailing whitespace has already been stripped,
10789 // and the trailing newline has already been added.
10790 assert_eq!(
10791 &buffer_changes.lock()[1..],
10792 &[
10793 (
10794 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
10795 "".into()
10796 ),
10797 (
10798 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
10799 "".into()
10800 ),
10801 (
10802 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
10803 "\n".into()
10804 ),
10805 ]
10806 );
10807
10808 // Insert blank lines between each line of the buffer.
10809 async move {
10810 Ok(Some(vec![
10811 lsp::TextEdit {
10812 range: lsp::Range::new(
10813 lsp::Position::new(1, 0),
10814 lsp::Position::new(1, 0),
10815 ),
10816 new_text: "\n".into(),
10817 },
10818 lsp::TextEdit {
10819 range: lsp::Range::new(
10820 lsp::Position::new(2, 0),
10821 lsp::Position::new(2, 0),
10822 ),
10823 new_text: "\n".into(),
10824 },
10825 ]))
10826 }
10827 }
10828 });
10829
10830 // After formatting the buffer, the trailing whitespace is stripped,
10831 // a newline is appended, and the edits provided by the language server
10832 // have been applied.
10833 format.await.unwrap();
10834 cx.assert_editor_state(
10835 &[
10836 "one", //
10837 "", //
10838 "twoˇ", //
10839 "", //
10840 "three", //
10841 "four", //
10842 "", //
10843 ]
10844 .join("\n"),
10845 );
10846
10847 // Undoing the formatting undoes the trailing whitespace removal, the
10848 // trailing newline, and the LSP edits.
10849 cx.update_buffer(|buffer, cx| buffer.undo(cx));
10850 cx.assert_editor_state(
10851 &[
10852 "one ", //
10853 "twoˇ", //
10854 "three ", //
10855 "four", //
10856 ]
10857 .join("\n"),
10858 );
10859}
10860
10861#[gpui::test]
10862async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
10863 cx: &mut TestAppContext,
10864) {
10865 init_test(cx, |_| {});
10866
10867 cx.update(|cx| {
10868 cx.update_global::<SettingsStore, _>(|settings, cx| {
10869 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10870 settings.auto_signature_help = Some(true);
10871 });
10872 });
10873 });
10874
10875 let mut cx = EditorLspTestContext::new_rust(
10876 lsp::ServerCapabilities {
10877 signature_help_provider: Some(lsp::SignatureHelpOptions {
10878 ..Default::default()
10879 }),
10880 ..Default::default()
10881 },
10882 cx,
10883 )
10884 .await;
10885
10886 let language = Language::new(
10887 LanguageConfig {
10888 name: "Rust".into(),
10889 brackets: BracketPairConfig {
10890 pairs: vec![
10891 BracketPair {
10892 start: "{".to_string(),
10893 end: "}".to_string(),
10894 close: true,
10895 surround: true,
10896 newline: true,
10897 },
10898 BracketPair {
10899 start: "(".to_string(),
10900 end: ")".to_string(),
10901 close: true,
10902 surround: true,
10903 newline: true,
10904 },
10905 BracketPair {
10906 start: "/*".to_string(),
10907 end: " */".to_string(),
10908 close: true,
10909 surround: true,
10910 newline: true,
10911 },
10912 BracketPair {
10913 start: "[".to_string(),
10914 end: "]".to_string(),
10915 close: false,
10916 surround: false,
10917 newline: true,
10918 },
10919 BracketPair {
10920 start: "\"".to_string(),
10921 end: "\"".to_string(),
10922 close: true,
10923 surround: true,
10924 newline: false,
10925 },
10926 BracketPair {
10927 start: "<".to_string(),
10928 end: ">".to_string(),
10929 close: false,
10930 surround: true,
10931 newline: true,
10932 },
10933 ],
10934 ..Default::default()
10935 },
10936 autoclose_before: "})]".to_string(),
10937 ..Default::default()
10938 },
10939 Some(tree_sitter_rust::LANGUAGE.into()),
10940 );
10941 let language = Arc::new(language);
10942
10943 cx.language_registry().add(language.clone());
10944 cx.update_buffer(|buffer, cx| {
10945 buffer.set_language(Some(language), cx);
10946 });
10947
10948 cx.set_state(
10949 &r#"
10950 fn main() {
10951 sampleˇ
10952 }
10953 "#
10954 .unindent(),
10955 );
10956
10957 cx.update_editor(|editor, window, cx| {
10958 editor.handle_input("(", window, cx);
10959 });
10960 cx.assert_editor_state(
10961 &"
10962 fn main() {
10963 sample(ˇ)
10964 }
10965 "
10966 .unindent(),
10967 );
10968
10969 let mocked_response = lsp::SignatureHelp {
10970 signatures: vec![lsp::SignatureInformation {
10971 label: "fn sample(param1: u8, param2: u8)".to_string(),
10972 documentation: None,
10973 parameters: Some(vec![
10974 lsp::ParameterInformation {
10975 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10976 documentation: None,
10977 },
10978 lsp::ParameterInformation {
10979 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10980 documentation: None,
10981 },
10982 ]),
10983 active_parameter: None,
10984 }],
10985 active_signature: Some(0),
10986 active_parameter: Some(0),
10987 };
10988 handle_signature_help_request(&mut cx, mocked_response).await;
10989
10990 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10991 .await;
10992
10993 cx.editor(|editor, _, _| {
10994 let signature_help_state = editor.signature_help_state.popover().cloned();
10995 let signature = signature_help_state.unwrap();
10996 assert_eq!(
10997 signature.signatures[signature.current_signature].label,
10998 "fn sample(param1: u8, param2: u8)"
10999 );
11000 });
11001}
11002
11003#[gpui::test]
11004async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
11005 init_test(cx, |_| {});
11006
11007 cx.update(|cx| {
11008 cx.update_global::<SettingsStore, _>(|settings, cx| {
11009 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11010 settings.auto_signature_help = Some(false);
11011 settings.show_signature_help_after_edits = Some(false);
11012 });
11013 });
11014 });
11015
11016 let mut cx = EditorLspTestContext::new_rust(
11017 lsp::ServerCapabilities {
11018 signature_help_provider: Some(lsp::SignatureHelpOptions {
11019 ..Default::default()
11020 }),
11021 ..Default::default()
11022 },
11023 cx,
11024 )
11025 .await;
11026
11027 let language = Language::new(
11028 LanguageConfig {
11029 name: "Rust".into(),
11030 brackets: BracketPairConfig {
11031 pairs: vec![
11032 BracketPair {
11033 start: "{".to_string(),
11034 end: "}".to_string(),
11035 close: true,
11036 surround: true,
11037 newline: true,
11038 },
11039 BracketPair {
11040 start: "(".to_string(),
11041 end: ")".to_string(),
11042 close: true,
11043 surround: true,
11044 newline: true,
11045 },
11046 BracketPair {
11047 start: "/*".to_string(),
11048 end: " */".to_string(),
11049 close: true,
11050 surround: true,
11051 newline: true,
11052 },
11053 BracketPair {
11054 start: "[".to_string(),
11055 end: "]".to_string(),
11056 close: false,
11057 surround: false,
11058 newline: true,
11059 },
11060 BracketPair {
11061 start: "\"".to_string(),
11062 end: "\"".to_string(),
11063 close: true,
11064 surround: true,
11065 newline: false,
11066 },
11067 BracketPair {
11068 start: "<".to_string(),
11069 end: ">".to_string(),
11070 close: false,
11071 surround: true,
11072 newline: true,
11073 },
11074 ],
11075 ..Default::default()
11076 },
11077 autoclose_before: "})]".to_string(),
11078 ..Default::default()
11079 },
11080 Some(tree_sitter_rust::LANGUAGE.into()),
11081 );
11082 let language = Arc::new(language);
11083
11084 cx.language_registry().add(language.clone());
11085 cx.update_buffer(|buffer, cx| {
11086 buffer.set_language(Some(language), cx);
11087 });
11088
11089 // Ensure that signature_help is not called when no signature help is enabled.
11090 cx.set_state(
11091 &r#"
11092 fn main() {
11093 sampleˇ
11094 }
11095 "#
11096 .unindent(),
11097 );
11098 cx.update_editor(|editor, window, cx| {
11099 editor.handle_input("(", window, cx);
11100 });
11101 cx.assert_editor_state(
11102 &"
11103 fn main() {
11104 sample(ˇ)
11105 }
11106 "
11107 .unindent(),
11108 );
11109 cx.editor(|editor, _, _| {
11110 assert!(editor.signature_help_state.task().is_none());
11111 });
11112
11113 let mocked_response = lsp::SignatureHelp {
11114 signatures: vec![lsp::SignatureInformation {
11115 label: "fn sample(param1: u8, param2: u8)".to_string(),
11116 documentation: None,
11117 parameters: Some(vec![
11118 lsp::ParameterInformation {
11119 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11120 documentation: None,
11121 },
11122 lsp::ParameterInformation {
11123 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11124 documentation: None,
11125 },
11126 ]),
11127 active_parameter: None,
11128 }],
11129 active_signature: Some(0),
11130 active_parameter: Some(0),
11131 };
11132
11133 // Ensure that signature_help is called when enabled afte edits
11134 cx.update(|_, cx| {
11135 cx.update_global::<SettingsStore, _>(|settings, cx| {
11136 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11137 settings.auto_signature_help = Some(false);
11138 settings.show_signature_help_after_edits = Some(true);
11139 });
11140 });
11141 });
11142 cx.set_state(
11143 &r#"
11144 fn main() {
11145 sampleˇ
11146 }
11147 "#
11148 .unindent(),
11149 );
11150 cx.update_editor(|editor, window, cx| {
11151 editor.handle_input("(", window, cx);
11152 });
11153 cx.assert_editor_state(
11154 &"
11155 fn main() {
11156 sample(ˇ)
11157 }
11158 "
11159 .unindent(),
11160 );
11161 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11162 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11163 .await;
11164 cx.update_editor(|editor, _, _| {
11165 let signature_help_state = editor.signature_help_state.popover().cloned();
11166 assert!(signature_help_state.is_some());
11167 let signature = signature_help_state.unwrap();
11168 assert_eq!(
11169 signature.signatures[signature.current_signature].label,
11170 "fn sample(param1: u8, param2: u8)"
11171 );
11172 editor.signature_help_state = SignatureHelpState::default();
11173 });
11174
11175 // Ensure that signature_help is called when auto signature help override is enabled
11176 cx.update(|_, cx| {
11177 cx.update_global::<SettingsStore, _>(|settings, cx| {
11178 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11179 settings.auto_signature_help = Some(true);
11180 settings.show_signature_help_after_edits = Some(false);
11181 });
11182 });
11183 });
11184 cx.set_state(
11185 &r#"
11186 fn main() {
11187 sampleˇ
11188 }
11189 "#
11190 .unindent(),
11191 );
11192 cx.update_editor(|editor, window, cx| {
11193 editor.handle_input("(", window, cx);
11194 });
11195 cx.assert_editor_state(
11196 &"
11197 fn main() {
11198 sample(ˇ)
11199 }
11200 "
11201 .unindent(),
11202 );
11203 handle_signature_help_request(&mut cx, mocked_response).await;
11204 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11205 .await;
11206 cx.editor(|editor, _, _| {
11207 let signature_help_state = editor.signature_help_state.popover().cloned();
11208 assert!(signature_help_state.is_some());
11209 let signature = signature_help_state.unwrap();
11210 assert_eq!(
11211 signature.signatures[signature.current_signature].label,
11212 "fn sample(param1: u8, param2: u8)"
11213 );
11214 });
11215}
11216
11217#[gpui::test]
11218async fn test_signature_help(cx: &mut TestAppContext) {
11219 init_test(cx, |_| {});
11220 cx.update(|cx| {
11221 cx.update_global::<SettingsStore, _>(|settings, cx| {
11222 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11223 settings.auto_signature_help = Some(true);
11224 });
11225 });
11226 });
11227
11228 let mut cx = EditorLspTestContext::new_rust(
11229 lsp::ServerCapabilities {
11230 signature_help_provider: Some(lsp::SignatureHelpOptions {
11231 ..Default::default()
11232 }),
11233 ..Default::default()
11234 },
11235 cx,
11236 )
11237 .await;
11238
11239 // A test that directly calls `show_signature_help`
11240 cx.update_editor(|editor, window, cx| {
11241 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11242 });
11243
11244 let mocked_response = lsp::SignatureHelp {
11245 signatures: vec![lsp::SignatureInformation {
11246 label: "fn sample(param1: u8, param2: u8)".to_string(),
11247 documentation: None,
11248 parameters: Some(vec![
11249 lsp::ParameterInformation {
11250 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11251 documentation: None,
11252 },
11253 lsp::ParameterInformation {
11254 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11255 documentation: None,
11256 },
11257 ]),
11258 active_parameter: None,
11259 }],
11260 active_signature: Some(0),
11261 active_parameter: Some(0),
11262 };
11263 handle_signature_help_request(&mut cx, mocked_response).await;
11264
11265 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11266 .await;
11267
11268 cx.editor(|editor, _, _| {
11269 let signature_help_state = editor.signature_help_state.popover().cloned();
11270 assert!(signature_help_state.is_some());
11271 let signature = signature_help_state.unwrap();
11272 assert_eq!(
11273 signature.signatures[signature.current_signature].label,
11274 "fn sample(param1: u8, param2: u8)"
11275 );
11276 });
11277
11278 // When exiting outside from inside the brackets, `signature_help` is closed.
11279 cx.set_state(indoc! {"
11280 fn main() {
11281 sample(ˇ);
11282 }
11283
11284 fn sample(param1: u8, param2: u8) {}
11285 "});
11286
11287 cx.update_editor(|editor, window, cx| {
11288 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11289 s.select_ranges([0..0])
11290 });
11291 });
11292
11293 let mocked_response = lsp::SignatureHelp {
11294 signatures: Vec::new(),
11295 active_signature: None,
11296 active_parameter: None,
11297 };
11298 handle_signature_help_request(&mut cx, mocked_response).await;
11299
11300 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11301 .await;
11302
11303 cx.editor(|editor, _, _| {
11304 assert!(!editor.signature_help_state.is_shown());
11305 });
11306
11307 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
11308 cx.set_state(indoc! {"
11309 fn main() {
11310 sample(ˇ);
11311 }
11312
11313 fn sample(param1: u8, param2: u8) {}
11314 "});
11315
11316 let mocked_response = lsp::SignatureHelp {
11317 signatures: vec![lsp::SignatureInformation {
11318 label: "fn sample(param1: u8, param2: u8)".to_string(),
11319 documentation: None,
11320 parameters: Some(vec![
11321 lsp::ParameterInformation {
11322 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11323 documentation: None,
11324 },
11325 lsp::ParameterInformation {
11326 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11327 documentation: None,
11328 },
11329 ]),
11330 active_parameter: None,
11331 }],
11332 active_signature: Some(0),
11333 active_parameter: Some(0),
11334 };
11335 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11336 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11337 .await;
11338 cx.editor(|editor, _, _| {
11339 assert!(editor.signature_help_state.is_shown());
11340 });
11341
11342 // Restore the popover with more parameter input
11343 cx.set_state(indoc! {"
11344 fn main() {
11345 sample(param1, param2ˇ);
11346 }
11347
11348 fn sample(param1: u8, param2: u8) {}
11349 "});
11350
11351 let mocked_response = lsp::SignatureHelp {
11352 signatures: vec![lsp::SignatureInformation {
11353 label: "fn sample(param1: u8, param2: u8)".to_string(),
11354 documentation: None,
11355 parameters: Some(vec![
11356 lsp::ParameterInformation {
11357 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11358 documentation: None,
11359 },
11360 lsp::ParameterInformation {
11361 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11362 documentation: None,
11363 },
11364 ]),
11365 active_parameter: None,
11366 }],
11367 active_signature: Some(0),
11368 active_parameter: Some(1),
11369 };
11370 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11371 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11372 .await;
11373
11374 // When selecting a range, the popover is gone.
11375 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
11376 cx.update_editor(|editor, window, cx| {
11377 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11378 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11379 })
11380 });
11381 cx.assert_editor_state(indoc! {"
11382 fn main() {
11383 sample(param1, «ˇparam2»);
11384 }
11385
11386 fn sample(param1: u8, param2: u8) {}
11387 "});
11388 cx.editor(|editor, _, _| {
11389 assert!(!editor.signature_help_state.is_shown());
11390 });
11391
11392 // When unselecting again, the popover is back if within the brackets.
11393 cx.update_editor(|editor, window, cx| {
11394 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11395 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11396 })
11397 });
11398 cx.assert_editor_state(indoc! {"
11399 fn main() {
11400 sample(param1, ˇparam2);
11401 }
11402
11403 fn sample(param1: u8, param2: u8) {}
11404 "});
11405 handle_signature_help_request(&mut cx, mocked_response).await;
11406 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11407 .await;
11408 cx.editor(|editor, _, _| {
11409 assert!(editor.signature_help_state.is_shown());
11410 });
11411
11412 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
11413 cx.update_editor(|editor, window, cx| {
11414 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11415 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
11416 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11417 })
11418 });
11419 cx.assert_editor_state(indoc! {"
11420 fn main() {
11421 sample(param1, ˇparam2);
11422 }
11423
11424 fn sample(param1: u8, param2: u8) {}
11425 "});
11426
11427 let mocked_response = lsp::SignatureHelp {
11428 signatures: vec![lsp::SignatureInformation {
11429 label: "fn sample(param1: u8, param2: u8)".to_string(),
11430 documentation: None,
11431 parameters: Some(vec![
11432 lsp::ParameterInformation {
11433 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11434 documentation: None,
11435 },
11436 lsp::ParameterInformation {
11437 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11438 documentation: None,
11439 },
11440 ]),
11441 active_parameter: None,
11442 }],
11443 active_signature: Some(0),
11444 active_parameter: Some(1),
11445 };
11446 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11447 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11448 .await;
11449 cx.update_editor(|editor, _, cx| {
11450 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
11451 });
11452 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11453 .await;
11454 cx.update_editor(|editor, window, cx| {
11455 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11456 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11457 })
11458 });
11459 cx.assert_editor_state(indoc! {"
11460 fn main() {
11461 sample(param1, «ˇparam2»);
11462 }
11463
11464 fn sample(param1: u8, param2: u8) {}
11465 "});
11466 cx.update_editor(|editor, window, cx| {
11467 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11468 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11469 })
11470 });
11471 cx.assert_editor_state(indoc! {"
11472 fn main() {
11473 sample(param1, ˇparam2);
11474 }
11475
11476 fn sample(param1: u8, param2: u8) {}
11477 "});
11478 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
11479 .await;
11480}
11481
11482#[gpui::test]
11483async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
11484 init_test(cx, |_| {});
11485
11486 let mut cx = EditorLspTestContext::new_rust(
11487 lsp::ServerCapabilities {
11488 signature_help_provider: Some(lsp::SignatureHelpOptions {
11489 ..Default::default()
11490 }),
11491 ..Default::default()
11492 },
11493 cx,
11494 )
11495 .await;
11496
11497 cx.set_state(indoc! {"
11498 fn main() {
11499 overloadedˇ
11500 }
11501 "});
11502
11503 cx.update_editor(|editor, window, cx| {
11504 editor.handle_input("(", window, cx);
11505 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11506 });
11507
11508 // Mock response with 3 signatures
11509 let mocked_response = lsp::SignatureHelp {
11510 signatures: vec![
11511 lsp::SignatureInformation {
11512 label: "fn overloaded(x: i32)".to_string(),
11513 documentation: None,
11514 parameters: Some(vec![lsp::ParameterInformation {
11515 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11516 documentation: None,
11517 }]),
11518 active_parameter: None,
11519 },
11520 lsp::SignatureInformation {
11521 label: "fn overloaded(x: i32, y: i32)".to_string(),
11522 documentation: None,
11523 parameters: Some(vec![
11524 lsp::ParameterInformation {
11525 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11526 documentation: None,
11527 },
11528 lsp::ParameterInformation {
11529 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
11530 documentation: None,
11531 },
11532 ]),
11533 active_parameter: None,
11534 },
11535 lsp::SignatureInformation {
11536 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
11537 documentation: None,
11538 parameters: Some(vec![
11539 lsp::ParameterInformation {
11540 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11541 documentation: None,
11542 },
11543 lsp::ParameterInformation {
11544 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
11545 documentation: None,
11546 },
11547 lsp::ParameterInformation {
11548 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
11549 documentation: None,
11550 },
11551 ]),
11552 active_parameter: None,
11553 },
11554 ],
11555 active_signature: Some(1),
11556 active_parameter: Some(0),
11557 };
11558 handle_signature_help_request(&mut cx, mocked_response).await;
11559
11560 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11561 .await;
11562
11563 // Verify we have multiple signatures and the right one is selected
11564 cx.editor(|editor, _, _| {
11565 let popover = editor.signature_help_state.popover().cloned().unwrap();
11566 assert_eq!(popover.signatures.len(), 3);
11567 // active_signature was 1, so that should be the current
11568 assert_eq!(popover.current_signature, 1);
11569 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
11570 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
11571 assert_eq!(
11572 popover.signatures[2].label,
11573 "fn overloaded(x: i32, y: i32, z: i32)"
11574 );
11575 });
11576
11577 // Test navigation functionality
11578 cx.update_editor(|editor, window, cx| {
11579 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
11580 });
11581
11582 cx.editor(|editor, _, _| {
11583 let popover = editor.signature_help_state.popover().cloned().unwrap();
11584 assert_eq!(popover.current_signature, 2);
11585 });
11586
11587 // Test wrap around
11588 cx.update_editor(|editor, window, cx| {
11589 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
11590 });
11591
11592 cx.editor(|editor, _, _| {
11593 let popover = editor.signature_help_state.popover().cloned().unwrap();
11594 assert_eq!(popover.current_signature, 0);
11595 });
11596
11597 // Test previous navigation
11598 cx.update_editor(|editor, window, cx| {
11599 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
11600 });
11601
11602 cx.editor(|editor, _, _| {
11603 let popover = editor.signature_help_state.popover().cloned().unwrap();
11604 assert_eq!(popover.current_signature, 2);
11605 });
11606}
11607
11608#[gpui::test]
11609async fn test_completion_mode(cx: &mut TestAppContext) {
11610 init_test(cx, |_| {});
11611 let mut cx = EditorLspTestContext::new_rust(
11612 lsp::ServerCapabilities {
11613 completion_provider: Some(lsp::CompletionOptions {
11614 resolve_provider: Some(true),
11615 ..Default::default()
11616 }),
11617 ..Default::default()
11618 },
11619 cx,
11620 )
11621 .await;
11622
11623 struct Run {
11624 run_description: &'static str,
11625 initial_state: String,
11626 buffer_marked_text: String,
11627 completion_label: &'static str,
11628 completion_text: &'static str,
11629 expected_with_insert_mode: String,
11630 expected_with_replace_mode: String,
11631 expected_with_replace_subsequence_mode: String,
11632 expected_with_replace_suffix_mode: String,
11633 }
11634
11635 let runs = [
11636 Run {
11637 run_description: "Start of word matches completion text",
11638 initial_state: "before ediˇ after".into(),
11639 buffer_marked_text: "before <edi|> after".into(),
11640 completion_label: "editor",
11641 completion_text: "editor",
11642 expected_with_insert_mode: "before editorˇ after".into(),
11643 expected_with_replace_mode: "before editorˇ after".into(),
11644 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11645 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11646 },
11647 Run {
11648 run_description: "Accept same text at the middle of the word",
11649 initial_state: "before ediˇtor after".into(),
11650 buffer_marked_text: "before <edi|tor> after".into(),
11651 completion_label: "editor",
11652 completion_text: "editor",
11653 expected_with_insert_mode: "before editorˇtor after".into(),
11654 expected_with_replace_mode: "before editorˇ after".into(),
11655 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11656 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11657 },
11658 Run {
11659 run_description: "End of word matches completion text -- cursor at end",
11660 initial_state: "before torˇ after".into(),
11661 buffer_marked_text: "before <tor|> after".into(),
11662 completion_label: "editor",
11663 completion_text: "editor",
11664 expected_with_insert_mode: "before editorˇ after".into(),
11665 expected_with_replace_mode: "before editorˇ after".into(),
11666 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11667 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11668 },
11669 Run {
11670 run_description: "End of word matches completion text -- cursor at start",
11671 initial_state: "before ˇtor after".into(),
11672 buffer_marked_text: "before <|tor> after".into(),
11673 completion_label: "editor",
11674 completion_text: "editor",
11675 expected_with_insert_mode: "before editorˇtor after".into(),
11676 expected_with_replace_mode: "before editorˇ after".into(),
11677 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11678 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11679 },
11680 Run {
11681 run_description: "Prepend text containing whitespace",
11682 initial_state: "pˇfield: bool".into(),
11683 buffer_marked_text: "<p|field>: bool".into(),
11684 completion_label: "pub ",
11685 completion_text: "pub ",
11686 expected_with_insert_mode: "pub ˇfield: bool".into(),
11687 expected_with_replace_mode: "pub ˇ: bool".into(),
11688 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
11689 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
11690 },
11691 Run {
11692 run_description: "Add element to start of list",
11693 initial_state: "[element_ˇelement_2]".into(),
11694 buffer_marked_text: "[<element_|element_2>]".into(),
11695 completion_label: "element_1",
11696 completion_text: "element_1",
11697 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
11698 expected_with_replace_mode: "[element_1ˇ]".into(),
11699 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
11700 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
11701 },
11702 Run {
11703 run_description: "Add element to start of list -- first and second elements are equal",
11704 initial_state: "[elˇelement]".into(),
11705 buffer_marked_text: "[<el|element>]".into(),
11706 completion_label: "element",
11707 completion_text: "element",
11708 expected_with_insert_mode: "[elementˇelement]".into(),
11709 expected_with_replace_mode: "[elementˇ]".into(),
11710 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
11711 expected_with_replace_suffix_mode: "[elementˇ]".into(),
11712 },
11713 Run {
11714 run_description: "Ends with matching suffix",
11715 initial_state: "SubˇError".into(),
11716 buffer_marked_text: "<Sub|Error>".into(),
11717 completion_label: "SubscriptionError",
11718 completion_text: "SubscriptionError",
11719 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
11720 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11721 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11722 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
11723 },
11724 Run {
11725 run_description: "Suffix is a subsequence -- contiguous",
11726 initial_state: "SubˇErr".into(),
11727 buffer_marked_text: "<Sub|Err>".into(),
11728 completion_label: "SubscriptionError",
11729 completion_text: "SubscriptionError",
11730 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
11731 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11732 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11733 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
11734 },
11735 Run {
11736 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
11737 initial_state: "Suˇscrirr".into(),
11738 buffer_marked_text: "<Su|scrirr>".into(),
11739 completion_label: "SubscriptionError",
11740 completion_text: "SubscriptionError",
11741 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
11742 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11743 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11744 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
11745 },
11746 Run {
11747 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
11748 initial_state: "foo(indˇix)".into(),
11749 buffer_marked_text: "foo(<ind|ix>)".into(),
11750 completion_label: "node_index",
11751 completion_text: "node_index",
11752 expected_with_insert_mode: "foo(node_indexˇix)".into(),
11753 expected_with_replace_mode: "foo(node_indexˇ)".into(),
11754 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
11755 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
11756 },
11757 Run {
11758 run_description: "Replace range ends before cursor - should extend to cursor",
11759 initial_state: "before editˇo after".into(),
11760 buffer_marked_text: "before <{ed}>it|o after".into(),
11761 completion_label: "editor",
11762 completion_text: "editor",
11763 expected_with_insert_mode: "before editorˇo after".into(),
11764 expected_with_replace_mode: "before editorˇo after".into(),
11765 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
11766 expected_with_replace_suffix_mode: "before editorˇo after".into(),
11767 },
11768 Run {
11769 run_description: "Uses label for suffix matching",
11770 initial_state: "before ediˇtor after".into(),
11771 buffer_marked_text: "before <edi|tor> after".into(),
11772 completion_label: "editor",
11773 completion_text: "editor()",
11774 expected_with_insert_mode: "before editor()ˇtor after".into(),
11775 expected_with_replace_mode: "before editor()ˇ after".into(),
11776 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
11777 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
11778 },
11779 Run {
11780 run_description: "Case insensitive subsequence and suffix matching",
11781 initial_state: "before EDiˇtoR after".into(),
11782 buffer_marked_text: "before <EDi|toR> after".into(),
11783 completion_label: "editor",
11784 completion_text: "editor",
11785 expected_with_insert_mode: "before editorˇtoR after".into(),
11786 expected_with_replace_mode: "before editorˇ after".into(),
11787 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11788 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11789 },
11790 ];
11791
11792 for run in runs {
11793 let run_variations = [
11794 (LspInsertMode::Insert, run.expected_with_insert_mode),
11795 (LspInsertMode::Replace, run.expected_with_replace_mode),
11796 (
11797 LspInsertMode::ReplaceSubsequence,
11798 run.expected_with_replace_subsequence_mode,
11799 ),
11800 (
11801 LspInsertMode::ReplaceSuffix,
11802 run.expected_with_replace_suffix_mode,
11803 ),
11804 ];
11805
11806 for (lsp_insert_mode, expected_text) in run_variations {
11807 eprintln!(
11808 "run = {:?}, mode = {lsp_insert_mode:.?}",
11809 run.run_description,
11810 );
11811
11812 update_test_language_settings(&mut cx, |settings| {
11813 settings.defaults.completions = Some(CompletionSettings {
11814 lsp_insert_mode,
11815 words: WordsCompletionMode::Disabled,
11816 lsp: true,
11817 lsp_fetch_timeout_ms: 0,
11818 });
11819 });
11820
11821 cx.set_state(&run.initial_state);
11822 cx.update_editor(|editor, window, cx| {
11823 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11824 });
11825
11826 let counter = Arc::new(AtomicUsize::new(0));
11827 handle_completion_request_with_insert_and_replace(
11828 &mut cx,
11829 &run.buffer_marked_text,
11830 vec![(run.completion_label, run.completion_text)],
11831 counter.clone(),
11832 )
11833 .await;
11834 cx.condition(|editor, _| editor.context_menu_visible())
11835 .await;
11836 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11837
11838 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11839 editor
11840 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11841 .unwrap()
11842 });
11843 cx.assert_editor_state(&expected_text);
11844 handle_resolve_completion_request(&mut cx, None).await;
11845 apply_additional_edits.await.unwrap();
11846 }
11847 }
11848}
11849
11850#[gpui::test]
11851async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
11852 init_test(cx, |_| {});
11853 let mut cx = EditorLspTestContext::new_rust(
11854 lsp::ServerCapabilities {
11855 completion_provider: Some(lsp::CompletionOptions {
11856 resolve_provider: Some(true),
11857 ..Default::default()
11858 }),
11859 ..Default::default()
11860 },
11861 cx,
11862 )
11863 .await;
11864
11865 let initial_state = "SubˇError";
11866 let buffer_marked_text = "<Sub|Error>";
11867 let completion_text = "SubscriptionError";
11868 let expected_with_insert_mode = "SubscriptionErrorˇError";
11869 let expected_with_replace_mode = "SubscriptionErrorˇ";
11870
11871 update_test_language_settings(&mut cx, |settings| {
11872 settings.defaults.completions = Some(CompletionSettings {
11873 words: WordsCompletionMode::Disabled,
11874 // set the opposite here to ensure that the action is overriding the default behavior
11875 lsp_insert_mode: LspInsertMode::Insert,
11876 lsp: true,
11877 lsp_fetch_timeout_ms: 0,
11878 });
11879 });
11880
11881 cx.set_state(initial_state);
11882 cx.update_editor(|editor, window, cx| {
11883 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11884 });
11885
11886 let counter = Arc::new(AtomicUsize::new(0));
11887 handle_completion_request_with_insert_and_replace(
11888 &mut cx,
11889 &buffer_marked_text,
11890 vec![(completion_text, completion_text)],
11891 counter.clone(),
11892 )
11893 .await;
11894 cx.condition(|editor, _| editor.context_menu_visible())
11895 .await;
11896 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11897
11898 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11899 editor
11900 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11901 .unwrap()
11902 });
11903 cx.assert_editor_state(&expected_with_replace_mode);
11904 handle_resolve_completion_request(&mut cx, None).await;
11905 apply_additional_edits.await.unwrap();
11906
11907 update_test_language_settings(&mut cx, |settings| {
11908 settings.defaults.completions = Some(CompletionSettings {
11909 words: WordsCompletionMode::Disabled,
11910 // set the opposite here to ensure that the action is overriding the default behavior
11911 lsp_insert_mode: LspInsertMode::Replace,
11912 lsp: true,
11913 lsp_fetch_timeout_ms: 0,
11914 });
11915 });
11916
11917 cx.set_state(initial_state);
11918 cx.update_editor(|editor, window, cx| {
11919 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11920 });
11921 handle_completion_request_with_insert_and_replace(
11922 &mut cx,
11923 &buffer_marked_text,
11924 vec![(completion_text, completion_text)],
11925 counter.clone(),
11926 )
11927 .await;
11928 cx.condition(|editor, _| editor.context_menu_visible())
11929 .await;
11930 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11931
11932 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11933 editor
11934 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
11935 .unwrap()
11936 });
11937 cx.assert_editor_state(&expected_with_insert_mode);
11938 handle_resolve_completion_request(&mut cx, None).await;
11939 apply_additional_edits.await.unwrap();
11940}
11941
11942#[gpui::test]
11943async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
11944 init_test(cx, |_| {});
11945 let mut cx = EditorLspTestContext::new_rust(
11946 lsp::ServerCapabilities {
11947 completion_provider: Some(lsp::CompletionOptions {
11948 resolve_provider: Some(true),
11949 ..Default::default()
11950 }),
11951 ..Default::default()
11952 },
11953 cx,
11954 )
11955 .await;
11956
11957 // scenario: surrounding text matches completion text
11958 let completion_text = "to_offset";
11959 let initial_state = indoc! {"
11960 1. buf.to_offˇsuffix
11961 2. buf.to_offˇsuf
11962 3. buf.to_offˇfix
11963 4. buf.to_offˇ
11964 5. into_offˇensive
11965 6. ˇsuffix
11966 7. let ˇ //
11967 8. aaˇzz
11968 9. buf.to_off«zzzzzˇ»suffix
11969 10. buf.«ˇzzzzz»suffix
11970 11. to_off«ˇzzzzz»
11971
11972 buf.to_offˇsuffix // newest cursor
11973 "};
11974 let completion_marked_buffer = indoc! {"
11975 1. buf.to_offsuffix
11976 2. buf.to_offsuf
11977 3. buf.to_offfix
11978 4. buf.to_off
11979 5. into_offensive
11980 6. suffix
11981 7. let //
11982 8. aazz
11983 9. buf.to_offzzzzzsuffix
11984 10. buf.zzzzzsuffix
11985 11. to_offzzzzz
11986
11987 buf.<to_off|suffix> // newest cursor
11988 "};
11989 let expected = indoc! {"
11990 1. buf.to_offsetˇ
11991 2. buf.to_offsetˇsuf
11992 3. buf.to_offsetˇfix
11993 4. buf.to_offsetˇ
11994 5. into_offsetˇensive
11995 6. to_offsetˇsuffix
11996 7. let to_offsetˇ //
11997 8. aato_offsetˇzz
11998 9. buf.to_offsetˇ
11999 10. buf.to_offsetˇsuffix
12000 11. to_offsetˇ
12001
12002 buf.to_offsetˇ // newest cursor
12003 "};
12004 cx.set_state(initial_state);
12005 cx.update_editor(|editor, window, cx| {
12006 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12007 });
12008 handle_completion_request_with_insert_and_replace(
12009 &mut cx,
12010 completion_marked_buffer,
12011 vec![(completion_text, completion_text)],
12012 Arc::new(AtomicUsize::new(0)),
12013 )
12014 .await;
12015 cx.condition(|editor, _| editor.context_menu_visible())
12016 .await;
12017 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12018 editor
12019 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12020 .unwrap()
12021 });
12022 cx.assert_editor_state(expected);
12023 handle_resolve_completion_request(&mut cx, None).await;
12024 apply_additional_edits.await.unwrap();
12025
12026 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
12027 let completion_text = "foo_and_bar";
12028 let initial_state = indoc! {"
12029 1. ooanbˇ
12030 2. zooanbˇ
12031 3. ooanbˇz
12032 4. zooanbˇz
12033 5. ooanˇ
12034 6. oanbˇ
12035
12036 ooanbˇ
12037 "};
12038 let completion_marked_buffer = indoc! {"
12039 1. ooanb
12040 2. zooanb
12041 3. ooanbz
12042 4. zooanbz
12043 5. ooan
12044 6. oanb
12045
12046 <ooanb|>
12047 "};
12048 let expected = indoc! {"
12049 1. foo_and_barˇ
12050 2. zfoo_and_barˇ
12051 3. foo_and_barˇz
12052 4. zfoo_and_barˇz
12053 5. ooanfoo_and_barˇ
12054 6. oanbfoo_and_barˇ
12055
12056 foo_and_barˇ
12057 "};
12058 cx.set_state(initial_state);
12059 cx.update_editor(|editor, window, cx| {
12060 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12061 });
12062 handle_completion_request_with_insert_and_replace(
12063 &mut cx,
12064 completion_marked_buffer,
12065 vec![(completion_text, completion_text)],
12066 Arc::new(AtomicUsize::new(0)),
12067 )
12068 .await;
12069 cx.condition(|editor, _| editor.context_menu_visible())
12070 .await;
12071 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12072 editor
12073 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12074 .unwrap()
12075 });
12076 cx.assert_editor_state(expected);
12077 handle_resolve_completion_request(&mut cx, None).await;
12078 apply_additional_edits.await.unwrap();
12079
12080 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
12081 // (expects the same as if it was inserted at the end)
12082 let completion_text = "foo_and_bar";
12083 let initial_state = indoc! {"
12084 1. ooˇanb
12085 2. zooˇanb
12086 3. ooˇanbz
12087 4. zooˇanbz
12088
12089 ooˇanb
12090 "};
12091 let completion_marked_buffer = indoc! {"
12092 1. ooanb
12093 2. zooanb
12094 3. ooanbz
12095 4. zooanbz
12096
12097 <oo|anb>
12098 "};
12099 let expected = indoc! {"
12100 1. foo_and_barˇ
12101 2. zfoo_and_barˇ
12102 3. foo_and_barˇz
12103 4. zfoo_and_barˇz
12104
12105 foo_and_barˇ
12106 "};
12107 cx.set_state(initial_state);
12108 cx.update_editor(|editor, window, cx| {
12109 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12110 });
12111 handle_completion_request_with_insert_and_replace(
12112 &mut cx,
12113 completion_marked_buffer,
12114 vec![(completion_text, completion_text)],
12115 Arc::new(AtomicUsize::new(0)),
12116 )
12117 .await;
12118 cx.condition(|editor, _| editor.context_menu_visible())
12119 .await;
12120 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12121 editor
12122 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12123 .unwrap()
12124 });
12125 cx.assert_editor_state(expected);
12126 handle_resolve_completion_request(&mut cx, None).await;
12127 apply_additional_edits.await.unwrap();
12128}
12129
12130// This used to crash
12131#[gpui::test]
12132async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
12133 init_test(cx, |_| {});
12134
12135 let buffer_text = indoc! {"
12136 fn main() {
12137 10.satu;
12138
12139 //
12140 // separate cursors so they open in different excerpts (manually reproducible)
12141 //
12142
12143 10.satu20;
12144 }
12145 "};
12146 let multibuffer_text_with_selections = indoc! {"
12147 fn main() {
12148 10.satuˇ;
12149
12150 //
12151
12152 //
12153
12154 10.satuˇ20;
12155 }
12156 "};
12157 let expected_multibuffer = indoc! {"
12158 fn main() {
12159 10.saturating_sub()ˇ;
12160
12161 //
12162
12163 //
12164
12165 10.saturating_sub()ˇ;
12166 }
12167 "};
12168
12169 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
12170 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
12171
12172 let fs = FakeFs::new(cx.executor());
12173 fs.insert_tree(
12174 path!("/a"),
12175 json!({
12176 "main.rs": buffer_text,
12177 }),
12178 )
12179 .await;
12180
12181 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12182 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12183 language_registry.add(rust_lang());
12184 let mut fake_servers = language_registry.register_fake_lsp(
12185 "Rust",
12186 FakeLspAdapter {
12187 capabilities: lsp::ServerCapabilities {
12188 completion_provider: Some(lsp::CompletionOptions {
12189 resolve_provider: None,
12190 ..lsp::CompletionOptions::default()
12191 }),
12192 ..lsp::ServerCapabilities::default()
12193 },
12194 ..FakeLspAdapter::default()
12195 },
12196 );
12197 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12198 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12199 let buffer = project
12200 .update(cx, |project, cx| {
12201 project.open_local_buffer(path!("/a/main.rs"), cx)
12202 })
12203 .await
12204 .unwrap();
12205
12206 let multi_buffer = cx.new(|cx| {
12207 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
12208 multi_buffer.push_excerpts(
12209 buffer.clone(),
12210 [ExcerptRange::new(0..first_excerpt_end)],
12211 cx,
12212 );
12213 multi_buffer.push_excerpts(
12214 buffer.clone(),
12215 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
12216 cx,
12217 );
12218 multi_buffer
12219 });
12220
12221 let editor = workspace
12222 .update(cx, |_, window, cx| {
12223 cx.new(|cx| {
12224 Editor::new(
12225 EditorMode::Full {
12226 scale_ui_elements_with_buffer_font_size: false,
12227 show_active_line_background: false,
12228 sized_by_content: false,
12229 },
12230 multi_buffer.clone(),
12231 Some(project.clone()),
12232 window,
12233 cx,
12234 )
12235 })
12236 })
12237 .unwrap();
12238
12239 let pane = workspace
12240 .update(cx, |workspace, _, _| workspace.active_pane().clone())
12241 .unwrap();
12242 pane.update_in(cx, |pane, window, cx| {
12243 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
12244 });
12245
12246 let fake_server = fake_servers.next().await.unwrap();
12247
12248 editor.update_in(cx, |editor, window, cx| {
12249 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12250 s.select_ranges([
12251 Point::new(1, 11)..Point::new(1, 11),
12252 Point::new(7, 11)..Point::new(7, 11),
12253 ])
12254 });
12255
12256 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
12257 });
12258
12259 editor.update_in(cx, |editor, window, cx| {
12260 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12261 });
12262
12263 fake_server
12264 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12265 let completion_item = lsp::CompletionItem {
12266 label: "saturating_sub()".into(),
12267 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
12268 lsp::InsertReplaceEdit {
12269 new_text: "saturating_sub()".to_owned(),
12270 insert: lsp::Range::new(
12271 lsp::Position::new(7, 7),
12272 lsp::Position::new(7, 11),
12273 ),
12274 replace: lsp::Range::new(
12275 lsp::Position::new(7, 7),
12276 lsp::Position::new(7, 13),
12277 ),
12278 },
12279 )),
12280 ..lsp::CompletionItem::default()
12281 };
12282
12283 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
12284 })
12285 .next()
12286 .await
12287 .unwrap();
12288
12289 cx.condition(&editor, |editor, _| editor.context_menu_visible())
12290 .await;
12291
12292 editor
12293 .update_in(cx, |editor, window, cx| {
12294 editor
12295 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12296 .unwrap()
12297 })
12298 .await
12299 .unwrap();
12300
12301 editor.update(cx, |editor, cx| {
12302 assert_text_with_selections(editor, expected_multibuffer, cx);
12303 })
12304}
12305
12306#[gpui::test]
12307async fn test_completion(cx: &mut TestAppContext) {
12308 init_test(cx, |_| {});
12309
12310 let mut cx = EditorLspTestContext::new_rust(
12311 lsp::ServerCapabilities {
12312 completion_provider: Some(lsp::CompletionOptions {
12313 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12314 resolve_provider: Some(true),
12315 ..Default::default()
12316 }),
12317 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12318 ..Default::default()
12319 },
12320 cx,
12321 )
12322 .await;
12323 let counter = Arc::new(AtomicUsize::new(0));
12324
12325 cx.set_state(indoc! {"
12326 oneˇ
12327 two
12328 three
12329 "});
12330 cx.simulate_keystroke(".");
12331 handle_completion_request(
12332 indoc! {"
12333 one.|<>
12334 two
12335 three
12336 "},
12337 vec!["first_completion", "second_completion"],
12338 true,
12339 counter.clone(),
12340 &mut cx,
12341 )
12342 .await;
12343 cx.condition(|editor, _| editor.context_menu_visible())
12344 .await;
12345 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12346
12347 let _handler = handle_signature_help_request(
12348 &mut cx,
12349 lsp::SignatureHelp {
12350 signatures: vec![lsp::SignatureInformation {
12351 label: "test signature".to_string(),
12352 documentation: None,
12353 parameters: Some(vec![lsp::ParameterInformation {
12354 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
12355 documentation: None,
12356 }]),
12357 active_parameter: None,
12358 }],
12359 active_signature: None,
12360 active_parameter: None,
12361 },
12362 );
12363 cx.update_editor(|editor, window, cx| {
12364 assert!(
12365 !editor.signature_help_state.is_shown(),
12366 "No signature help was called for"
12367 );
12368 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12369 });
12370 cx.run_until_parked();
12371 cx.update_editor(|editor, _, _| {
12372 assert!(
12373 !editor.signature_help_state.is_shown(),
12374 "No signature help should be shown when completions menu is open"
12375 );
12376 });
12377
12378 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12379 editor.context_menu_next(&Default::default(), window, cx);
12380 editor
12381 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12382 .unwrap()
12383 });
12384 cx.assert_editor_state(indoc! {"
12385 one.second_completionˇ
12386 two
12387 three
12388 "});
12389
12390 handle_resolve_completion_request(
12391 &mut cx,
12392 Some(vec![
12393 (
12394 //This overlaps with the primary completion edit which is
12395 //misbehavior from the LSP spec, test that we filter it out
12396 indoc! {"
12397 one.second_ˇcompletion
12398 two
12399 threeˇ
12400 "},
12401 "overlapping additional edit",
12402 ),
12403 (
12404 indoc! {"
12405 one.second_completion
12406 two
12407 threeˇ
12408 "},
12409 "\nadditional edit",
12410 ),
12411 ]),
12412 )
12413 .await;
12414 apply_additional_edits.await.unwrap();
12415 cx.assert_editor_state(indoc! {"
12416 one.second_completionˇ
12417 two
12418 three
12419 additional edit
12420 "});
12421
12422 cx.set_state(indoc! {"
12423 one.second_completion
12424 twoˇ
12425 threeˇ
12426 additional edit
12427 "});
12428 cx.simulate_keystroke(" ");
12429 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12430 cx.simulate_keystroke("s");
12431 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12432
12433 cx.assert_editor_state(indoc! {"
12434 one.second_completion
12435 two sˇ
12436 three sˇ
12437 additional edit
12438 "});
12439 handle_completion_request(
12440 indoc! {"
12441 one.second_completion
12442 two s
12443 three <s|>
12444 additional edit
12445 "},
12446 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12447 true,
12448 counter.clone(),
12449 &mut cx,
12450 )
12451 .await;
12452 cx.condition(|editor, _| editor.context_menu_visible())
12453 .await;
12454 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12455
12456 cx.simulate_keystroke("i");
12457
12458 handle_completion_request(
12459 indoc! {"
12460 one.second_completion
12461 two si
12462 three <si|>
12463 additional edit
12464 "},
12465 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12466 true,
12467 counter.clone(),
12468 &mut cx,
12469 )
12470 .await;
12471 cx.condition(|editor, _| editor.context_menu_visible())
12472 .await;
12473 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12474
12475 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12476 editor
12477 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12478 .unwrap()
12479 });
12480 cx.assert_editor_state(indoc! {"
12481 one.second_completion
12482 two sixth_completionˇ
12483 three sixth_completionˇ
12484 additional edit
12485 "});
12486
12487 apply_additional_edits.await.unwrap();
12488
12489 update_test_language_settings(&mut cx, |settings| {
12490 settings.defaults.show_completions_on_input = Some(false);
12491 });
12492 cx.set_state("editorˇ");
12493 cx.simulate_keystroke(".");
12494 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12495 cx.simulate_keystrokes("c l o");
12496 cx.assert_editor_state("editor.cloˇ");
12497 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12498 cx.update_editor(|editor, window, cx| {
12499 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12500 });
12501 handle_completion_request(
12502 "editor.<clo|>",
12503 vec!["close", "clobber"],
12504 true,
12505 counter.clone(),
12506 &mut cx,
12507 )
12508 .await;
12509 cx.condition(|editor, _| editor.context_menu_visible())
12510 .await;
12511 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
12512
12513 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12514 editor
12515 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12516 .unwrap()
12517 });
12518 cx.assert_editor_state("editor.clobberˇ");
12519 handle_resolve_completion_request(&mut cx, None).await;
12520 apply_additional_edits.await.unwrap();
12521}
12522
12523#[gpui::test]
12524async fn test_completion_reuse(cx: &mut TestAppContext) {
12525 init_test(cx, |_| {});
12526
12527 let mut cx = EditorLspTestContext::new_rust(
12528 lsp::ServerCapabilities {
12529 completion_provider: Some(lsp::CompletionOptions {
12530 trigger_characters: Some(vec![".".to_string()]),
12531 ..Default::default()
12532 }),
12533 ..Default::default()
12534 },
12535 cx,
12536 )
12537 .await;
12538
12539 let counter = Arc::new(AtomicUsize::new(0));
12540 cx.set_state("objˇ");
12541 cx.simulate_keystroke(".");
12542
12543 // Initial completion request returns complete results
12544 let is_incomplete = false;
12545 handle_completion_request(
12546 "obj.|<>",
12547 vec!["a", "ab", "abc"],
12548 is_incomplete,
12549 counter.clone(),
12550 &mut cx,
12551 )
12552 .await;
12553 cx.run_until_parked();
12554 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12555 cx.assert_editor_state("obj.ˇ");
12556 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12557
12558 // Type "a" - filters existing completions
12559 cx.simulate_keystroke("a");
12560 cx.run_until_parked();
12561 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12562 cx.assert_editor_state("obj.aˇ");
12563 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12564
12565 // Type "b" - filters existing completions
12566 cx.simulate_keystroke("b");
12567 cx.run_until_parked();
12568 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12569 cx.assert_editor_state("obj.abˇ");
12570 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12571
12572 // Type "c" - filters existing completions
12573 cx.simulate_keystroke("c");
12574 cx.run_until_parked();
12575 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12576 cx.assert_editor_state("obj.abcˇ");
12577 check_displayed_completions(vec!["abc"], &mut cx);
12578
12579 // Backspace to delete "c" - filters existing completions
12580 cx.update_editor(|editor, window, cx| {
12581 editor.backspace(&Backspace, window, cx);
12582 });
12583 cx.run_until_parked();
12584 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12585 cx.assert_editor_state("obj.abˇ");
12586 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12587
12588 // Moving cursor to the left dismisses menu.
12589 cx.update_editor(|editor, window, cx| {
12590 editor.move_left(&MoveLeft, window, cx);
12591 });
12592 cx.run_until_parked();
12593 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12594 cx.assert_editor_state("obj.aˇb");
12595 cx.update_editor(|editor, _, _| {
12596 assert_eq!(editor.context_menu_visible(), false);
12597 });
12598
12599 // Type "b" - new request
12600 cx.simulate_keystroke("b");
12601 let is_incomplete = false;
12602 handle_completion_request(
12603 "obj.<ab|>a",
12604 vec!["ab", "abc"],
12605 is_incomplete,
12606 counter.clone(),
12607 &mut cx,
12608 )
12609 .await;
12610 cx.run_until_parked();
12611 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12612 cx.assert_editor_state("obj.abˇb");
12613 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12614
12615 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
12616 cx.update_editor(|editor, window, cx| {
12617 editor.backspace(&Backspace, window, cx);
12618 });
12619 let is_incomplete = false;
12620 handle_completion_request(
12621 "obj.<a|>b",
12622 vec!["a", "ab", "abc"],
12623 is_incomplete,
12624 counter.clone(),
12625 &mut cx,
12626 )
12627 .await;
12628 cx.run_until_parked();
12629 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12630 cx.assert_editor_state("obj.aˇb");
12631 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12632
12633 // Backspace to delete "a" - dismisses menu.
12634 cx.update_editor(|editor, window, cx| {
12635 editor.backspace(&Backspace, window, cx);
12636 });
12637 cx.run_until_parked();
12638 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12639 cx.assert_editor_state("obj.ˇb");
12640 cx.update_editor(|editor, _, _| {
12641 assert_eq!(editor.context_menu_visible(), false);
12642 });
12643}
12644
12645#[gpui::test]
12646async fn test_word_completion(cx: &mut TestAppContext) {
12647 let lsp_fetch_timeout_ms = 10;
12648 init_test(cx, |language_settings| {
12649 language_settings.defaults.completions = Some(CompletionSettings {
12650 words: WordsCompletionMode::Fallback,
12651 lsp: true,
12652 lsp_fetch_timeout_ms: 10,
12653 lsp_insert_mode: LspInsertMode::Insert,
12654 });
12655 });
12656
12657 let mut cx = EditorLspTestContext::new_rust(
12658 lsp::ServerCapabilities {
12659 completion_provider: Some(lsp::CompletionOptions {
12660 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12661 ..lsp::CompletionOptions::default()
12662 }),
12663 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12664 ..lsp::ServerCapabilities::default()
12665 },
12666 cx,
12667 )
12668 .await;
12669
12670 let throttle_completions = Arc::new(AtomicBool::new(false));
12671
12672 let lsp_throttle_completions = throttle_completions.clone();
12673 let _completion_requests_handler =
12674 cx.lsp
12675 .server
12676 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
12677 let lsp_throttle_completions = lsp_throttle_completions.clone();
12678 let cx = cx.clone();
12679 async move {
12680 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
12681 cx.background_executor()
12682 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
12683 .await;
12684 }
12685 Ok(Some(lsp::CompletionResponse::Array(vec![
12686 lsp::CompletionItem {
12687 label: "first".into(),
12688 ..lsp::CompletionItem::default()
12689 },
12690 lsp::CompletionItem {
12691 label: "last".into(),
12692 ..lsp::CompletionItem::default()
12693 },
12694 ])))
12695 }
12696 });
12697
12698 cx.set_state(indoc! {"
12699 oneˇ
12700 two
12701 three
12702 "});
12703 cx.simulate_keystroke(".");
12704 cx.executor().run_until_parked();
12705 cx.condition(|editor, _| editor.context_menu_visible())
12706 .await;
12707 cx.update_editor(|editor, window, cx| {
12708 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12709 {
12710 assert_eq!(
12711 completion_menu_entries(&menu),
12712 &["first", "last"],
12713 "When LSP server is fast to reply, no fallback word completions are used"
12714 );
12715 } else {
12716 panic!("expected completion menu to be open");
12717 }
12718 editor.cancel(&Cancel, window, cx);
12719 });
12720 cx.executor().run_until_parked();
12721 cx.condition(|editor, _| !editor.context_menu_visible())
12722 .await;
12723
12724 throttle_completions.store(true, atomic::Ordering::Release);
12725 cx.simulate_keystroke(".");
12726 cx.executor()
12727 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
12728 cx.executor().run_until_parked();
12729 cx.condition(|editor, _| editor.context_menu_visible())
12730 .await;
12731 cx.update_editor(|editor, _, _| {
12732 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12733 {
12734 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
12735 "When LSP server is slow, document words can be shown instead, if configured accordingly");
12736 } else {
12737 panic!("expected completion menu to be open");
12738 }
12739 });
12740}
12741
12742#[gpui::test]
12743async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
12744 init_test(cx, |language_settings| {
12745 language_settings.defaults.completions = Some(CompletionSettings {
12746 words: WordsCompletionMode::Enabled,
12747 lsp: true,
12748 lsp_fetch_timeout_ms: 0,
12749 lsp_insert_mode: LspInsertMode::Insert,
12750 });
12751 });
12752
12753 let mut cx = EditorLspTestContext::new_rust(
12754 lsp::ServerCapabilities {
12755 completion_provider: Some(lsp::CompletionOptions {
12756 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12757 ..lsp::CompletionOptions::default()
12758 }),
12759 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12760 ..lsp::ServerCapabilities::default()
12761 },
12762 cx,
12763 )
12764 .await;
12765
12766 let _completion_requests_handler =
12767 cx.lsp
12768 .server
12769 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12770 Ok(Some(lsp::CompletionResponse::Array(vec![
12771 lsp::CompletionItem {
12772 label: "first".into(),
12773 ..lsp::CompletionItem::default()
12774 },
12775 lsp::CompletionItem {
12776 label: "last".into(),
12777 ..lsp::CompletionItem::default()
12778 },
12779 ])))
12780 });
12781
12782 cx.set_state(indoc! {"ˇ
12783 first
12784 last
12785 second
12786 "});
12787 cx.simulate_keystroke(".");
12788 cx.executor().run_until_parked();
12789 cx.condition(|editor, _| editor.context_menu_visible())
12790 .await;
12791 cx.update_editor(|editor, _, _| {
12792 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12793 {
12794 assert_eq!(
12795 completion_menu_entries(&menu),
12796 &["first", "last", "second"],
12797 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
12798 );
12799 } else {
12800 panic!("expected completion menu to be open");
12801 }
12802 });
12803}
12804
12805#[gpui::test]
12806async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
12807 init_test(cx, |language_settings| {
12808 language_settings.defaults.completions = Some(CompletionSettings {
12809 words: WordsCompletionMode::Disabled,
12810 lsp: true,
12811 lsp_fetch_timeout_ms: 0,
12812 lsp_insert_mode: LspInsertMode::Insert,
12813 });
12814 });
12815
12816 let mut cx = EditorLspTestContext::new_rust(
12817 lsp::ServerCapabilities {
12818 completion_provider: Some(lsp::CompletionOptions {
12819 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12820 ..lsp::CompletionOptions::default()
12821 }),
12822 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12823 ..lsp::ServerCapabilities::default()
12824 },
12825 cx,
12826 )
12827 .await;
12828
12829 let _completion_requests_handler =
12830 cx.lsp
12831 .server
12832 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12833 panic!("LSP completions should not be queried when dealing with word completions")
12834 });
12835
12836 cx.set_state(indoc! {"ˇ
12837 first
12838 last
12839 second
12840 "});
12841 cx.update_editor(|editor, window, cx| {
12842 editor.show_word_completions(&ShowWordCompletions, window, cx);
12843 });
12844 cx.executor().run_until_parked();
12845 cx.condition(|editor, _| editor.context_menu_visible())
12846 .await;
12847 cx.update_editor(|editor, _, _| {
12848 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12849 {
12850 assert_eq!(
12851 completion_menu_entries(&menu),
12852 &["first", "last", "second"],
12853 "`ShowWordCompletions` action should show word completions"
12854 );
12855 } else {
12856 panic!("expected completion menu to be open");
12857 }
12858 });
12859
12860 cx.simulate_keystroke("l");
12861 cx.executor().run_until_parked();
12862 cx.condition(|editor, _| editor.context_menu_visible())
12863 .await;
12864 cx.update_editor(|editor, _, _| {
12865 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12866 {
12867 assert_eq!(
12868 completion_menu_entries(&menu),
12869 &["last"],
12870 "After showing word completions, further editing should filter them and not query the LSP"
12871 );
12872 } else {
12873 panic!("expected completion menu to be open");
12874 }
12875 });
12876}
12877
12878#[gpui::test]
12879async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
12880 init_test(cx, |language_settings| {
12881 language_settings.defaults.completions = Some(CompletionSettings {
12882 words: WordsCompletionMode::Fallback,
12883 lsp: false,
12884 lsp_fetch_timeout_ms: 0,
12885 lsp_insert_mode: LspInsertMode::Insert,
12886 });
12887 });
12888
12889 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12890
12891 cx.set_state(indoc! {"ˇ
12892 0_usize
12893 let
12894 33
12895 4.5f32
12896 "});
12897 cx.update_editor(|editor, window, cx| {
12898 editor.show_completions(&ShowCompletions::default(), window, cx);
12899 });
12900 cx.executor().run_until_parked();
12901 cx.condition(|editor, _| editor.context_menu_visible())
12902 .await;
12903 cx.update_editor(|editor, window, cx| {
12904 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12905 {
12906 assert_eq!(
12907 completion_menu_entries(&menu),
12908 &["let"],
12909 "With no digits in the completion query, no digits should be in the word completions"
12910 );
12911 } else {
12912 panic!("expected completion menu to be open");
12913 }
12914 editor.cancel(&Cancel, window, cx);
12915 });
12916
12917 cx.set_state(indoc! {"3ˇ
12918 0_usize
12919 let
12920 3
12921 33.35f32
12922 "});
12923 cx.update_editor(|editor, window, cx| {
12924 editor.show_completions(&ShowCompletions::default(), window, cx);
12925 });
12926 cx.executor().run_until_parked();
12927 cx.condition(|editor, _| editor.context_menu_visible())
12928 .await;
12929 cx.update_editor(|editor, _, _| {
12930 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12931 {
12932 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
12933 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
12934 } else {
12935 panic!("expected completion menu to be open");
12936 }
12937 });
12938}
12939
12940fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
12941 let position = || lsp::Position {
12942 line: params.text_document_position.position.line,
12943 character: params.text_document_position.position.character,
12944 };
12945 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12946 range: lsp::Range {
12947 start: position(),
12948 end: position(),
12949 },
12950 new_text: text.to_string(),
12951 }))
12952}
12953
12954#[gpui::test]
12955async fn test_multiline_completion(cx: &mut TestAppContext) {
12956 init_test(cx, |_| {});
12957
12958 let fs = FakeFs::new(cx.executor());
12959 fs.insert_tree(
12960 path!("/a"),
12961 json!({
12962 "main.ts": "a",
12963 }),
12964 )
12965 .await;
12966
12967 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12968 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12969 let typescript_language = Arc::new(Language::new(
12970 LanguageConfig {
12971 name: "TypeScript".into(),
12972 matcher: LanguageMatcher {
12973 path_suffixes: vec!["ts".to_string()],
12974 ..LanguageMatcher::default()
12975 },
12976 line_comments: vec!["// ".into()],
12977 ..LanguageConfig::default()
12978 },
12979 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12980 ));
12981 language_registry.add(typescript_language.clone());
12982 let mut fake_servers = language_registry.register_fake_lsp(
12983 "TypeScript",
12984 FakeLspAdapter {
12985 capabilities: lsp::ServerCapabilities {
12986 completion_provider: Some(lsp::CompletionOptions {
12987 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12988 ..lsp::CompletionOptions::default()
12989 }),
12990 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12991 ..lsp::ServerCapabilities::default()
12992 },
12993 // Emulate vtsls label generation
12994 label_for_completion: Some(Box::new(|item, _| {
12995 let text = if let Some(description) = item
12996 .label_details
12997 .as_ref()
12998 .and_then(|label_details| label_details.description.as_ref())
12999 {
13000 format!("{} {}", item.label, description)
13001 } else if let Some(detail) = &item.detail {
13002 format!("{} {}", item.label, detail)
13003 } else {
13004 item.label.clone()
13005 };
13006 let len = text.len();
13007 Some(language::CodeLabel {
13008 text,
13009 runs: Vec::new(),
13010 filter_range: 0..len,
13011 })
13012 })),
13013 ..FakeLspAdapter::default()
13014 },
13015 );
13016 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13017 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13018 let worktree_id = workspace
13019 .update(cx, |workspace, _window, cx| {
13020 workspace.project().update(cx, |project, cx| {
13021 project.worktrees(cx).next().unwrap().read(cx).id()
13022 })
13023 })
13024 .unwrap();
13025 let _buffer = project
13026 .update(cx, |project, cx| {
13027 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
13028 })
13029 .await
13030 .unwrap();
13031 let editor = workspace
13032 .update(cx, |workspace, window, cx| {
13033 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
13034 })
13035 .unwrap()
13036 .await
13037 .unwrap()
13038 .downcast::<Editor>()
13039 .unwrap();
13040 let fake_server = fake_servers.next().await.unwrap();
13041
13042 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
13043 let multiline_label_2 = "a\nb\nc\n";
13044 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
13045 let multiline_description = "d\ne\nf\n";
13046 let multiline_detail_2 = "g\nh\ni\n";
13047
13048 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
13049 move |params, _| async move {
13050 Ok(Some(lsp::CompletionResponse::Array(vec![
13051 lsp::CompletionItem {
13052 label: multiline_label.to_string(),
13053 text_edit: gen_text_edit(¶ms, "new_text_1"),
13054 ..lsp::CompletionItem::default()
13055 },
13056 lsp::CompletionItem {
13057 label: "single line label 1".to_string(),
13058 detail: Some(multiline_detail.to_string()),
13059 text_edit: gen_text_edit(¶ms, "new_text_2"),
13060 ..lsp::CompletionItem::default()
13061 },
13062 lsp::CompletionItem {
13063 label: "single line label 2".to_string(),
13064 label_details: Some(lsp::CompletionItemLabelDetails {
13065 description: Some(multiline_description.to_string()),
13066 detail: None,
13067 }),
13068 text_edit: gen_text_edit(¶ms, "new_text_2"),
13069 ..lsp::CompletionItem::default()
13070 },
13071 lsp::CompletionItem {
13072 label: multiline_label_2.to_string(),
13073 detail: Some(multiline_detail_2.to_string()),
13074 text_edit: gen_text_edit(¶ms, "new_text_3"),
13075 ..lsp::CompletionItem::default()
13076 },
13077 lsp::CompletionItem {
13078 label: "Label with many spaces and \t but without newlines".to_string(),
13079 detail: Some(
13080 "Details with many spaces and \t but without newlines".to_string(),
13081 ),
13082 text_edit: gen_text_edit(¶ms, "new_text_4"),
13083 ..lsp::CompletionItem::default()
13084 },
13085 ])))
13086 },
13087 );
13088
13089 editor.update_in(cx, |editor, window, cx| {
13090 cx.focus_self(window);
13091 editor.move_to_end(&MoveToEnd, window, cx);
13092 editor.handle_input(".", window, cx);
13093 });
13094 cx.run_until_parked();
13095 completion_handle.next().await.unwrap();
13096
13097 editor.update(cx, |editor, _| {
13098 assert!(editor.context_menu_visible());
13099 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13100 {
13101 let completion_labels = menu
13102 .completions
13103 .borrow()
13104 .iter()
13105 .map(|c| c.label.text.clone())
13106 .collect::<Vec<_>>();
13107 assert_eq!(
13108 completion_labels,
13109 &[
13110 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
13111 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
13112 "single line label 2 d e f ",
13113 "a b c g h i ",
13114 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
13115 ],
13116 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
13117 );
13118
13119 for completion in menu
13120 .completions
13121 .borrow()
13122 .iter() {
13123 assert_eq!(
13124 completion.label.filter_range,
13125 0..completion.label.text.len(),
13126 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
13127 );
13128 }
13129 } else {
13130 panic!("expected completion menu to be open");
13131 }
13132 });
13133}
13134
13135#[gpui::test]
13136async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
13137 init_test(cx, |_| {});
13138 let mut cx = EditorLspTestContext::new_rust(
13139 lsp::ServerCapabilities {
13140 completion_provider: Some(lsp::CompletionOptions {
13141 trigger_characters: Some(vec![".".to_string()]),
13142 ..Default::default()
13143 }),
13144 ..Default::default()
13145 },
13146 cx,
13147 )
13148 .await;
13149 cx.lsp
13150 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13151 Ok(Some(lsp::CompletionResponse::Array(vec![
13152 lsp::CompletionItem {
13153 label: "first".into(),
13154 ..Default::default()
13155 },
13156 lsp::CompletionItem {
13157 label: "last".into(),
13158 ..Default::default()
13159 },
13160 ])))
13161 });
13162 cx.set_state("variableˇ");
13163 cx.simulate_keystroke(".");
13164 cx.executor().run_until_parked();
13165
13166 cx.update_editor(|editor, _, _| {
13167 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13168 {
13169 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
13170 } else {
13171 panic!("expected completion menu to be open");
13172 }
13173 });
13174
13175 cx.update_editor(|editor, window, cx| {
13176 editor.move_page_down(&MovePageDown::default(), window, cx);
13177 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13178 {
13179 assert!(
13180 menu.selected_item == 1,
13181 "expected PageDown to select the last item from the context menu"
13182 );
13183 } else {
13184 panic!("expected completion menu to stay open after PageDown");
13185 }
13186 });
13187
13188 cx.update_editor(|editor, window, cx| {
13189 editor.move_page_up(&MovePageUp::default(), window, cx);
13190 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13191 {
13192 assert!(
13193 menu.selected_item == 0,
13194 "expected PageUp to select the first item from the context menu"
13195 );
13196 } else {
13197 panic!("expected completion menu to stay open after PageUp");
13198 }
13199 });
13200}
13201
13202#[gpui::test]
13203async fn test_as_is_completions(cx: &mut TestAppContext) {
13204 init_test(cx, |_| {});
13205 let mut cx = EditorLspTestContext::new_rust(
13206 lsp::ServerCapabilities {
13207 completion_provider: Some(lsp::CompletionOptions {
13208 ..Default::default()
13209 }),
13210 ..Default::default()
13211 },
13212 cx,
13213 )
13214 .await;
13215 cx.lsp
13216 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13217 Ok(Some(lsp::CompletionResponse::Array(vec![
13218 lsp::CompletionItem {
13219 label: "unsafe".into(),
13220 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13221 range: lsp::Range {
13222 start: lsp::Position {
13223 line: 1,
13224 character: 2,
13225 },
13226 end: lsp::Position {
13227 line: 1,
13228 character: 3,
13229 },
13230 },
13231 new_text: "unsafe".to_string(),
13232 })),
13233 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
13234 ..Default::default()
13235 },
13236 ])))
13237 });
13238 cx.set_state("fn a() {}\n nˇ");
13239 cx.executor().run_until_parked();
13240 cx.update_editor(|editor, window, cx| {
13241 editor.show_completions(
13242 &ShowCompletions {
13243 trigger: Some("\n".into()),
13244 },
13245 window,
13246 cx,
13247 );
13248 });
13249 cx.executor().run_until_parked();
13250
13251 cx.update_editor(|editor, window, cx| {
13252 editor.confirm_completion(&Default::default(), window, cx)
13253 });
13254 cx.executor().run_until_parked();
13255 cx.assert_editor_state("fn a() {}\n unsafeˇ");
13256}
13257
13258#[gpui::test]
13259async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
13260 init_test(cx, |_| {});
13261
13262 let mut cx = EditorLspTestContext::new_rust(
13263 lsp::ServerCapabilities {
13264 completion_provider: Some(lsp::CompletionOptions {
13265 trigger_characters: Some(vec![".".to_string()]),
13266 resolve_provider: Some(true),
13267 ..Default::default()
13268 }),
13269 ..Default::default()
13270 },
13271 cx,
13272 )
13273 .await;
13274
13275 cx.set_state("fn main() { let a = 2ˇ; }");
13276 cx.simulate_keystroke(".");
13277 let completion_item = lsp::CompletionItem {
13278 label: "Some".into(),
13279 kind: Some(lsp::CompletionItemKind::SNIPPET),
13280 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13281 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13282 kind: lsp::MarkupKind::Markdown,
13283 value: "```rust\nSome(2)\n```".to_string(),
13284 })),
13285 deprecated: Some(false),
13286 sort_text: Some("Some".to_string()),
13287 filter_text: Some("Some".to_string()),
13288 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13289 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13290 range: lsp::Range {
13291 start: lsp::Position {
13292 line: 0,
13293 character: 22,
13294 },
13295 end: lsp::Position {
13296 line: 0,
13297 character: 22,
13298 },
13299 },
13300 new_text: "Some(2)".to_string(),
13301 })),
13302 additional_text_edits: Some(vec![lsp::TextEdit {
13303 range: lsp::Range {
13304 start: lsp::Position {
13305 line: 0,
13306 character: 20,
13307 },
13308 end: lsp::Position {
13309 line: 0,
13310 character: 22,
13311 },
13312 },
13313 new_text: "".to_string(),
13314 }]),
13315 ..Default::default()
13316 };
13317
13318 let closure_completion_item = completion_item.clone();
13319 let counter = Arc::new(AtomicUsize::new(0));
13320 let counter_clone = counter.clone();
13321 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13322 let task_completion_item = closure_completion_item.clone();
13323 counter_clone.fetch_add(1, atomic::Ordering::Release);
13324 async move {
13325 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13326 is_incomplete: true,
13327 item_defaults: None,
13328 items: vec![task_completion_item],
13329 })))
13330 }
13331 });
13332
13333 cx.condition(|editor, _| editor.context_menu_visible())
13334 .await;
13335 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
13336 assert!(request.next().await.is_some());
13337 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13338
13339 cx.simulate_keystrokes("S o m");
13340 cx.condition(|editor, _| editor.context_menu_visible())
13341 .await;
13342 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
13343 assert!(request.next().await.is_some());
13344 assert!(request.next().await.is_some());
13345 assert!(request.next().await.is_some());
13346 request.close();
13347 assert!(request.next().await.is_none());
13348 assert_eq!(
13349 counter.load(atomic::Ordering::Acquire),
13350 4,
13351 "With the completions menu open, only one LSP request should happen per input"
13352 );
13353}
13354
13355#[gpui::test]
13356async fn test_toggle_comment(cx: &mut TestAppContext) {
13357 init_test(cx, |_| {});
13358 let mut cx = EditorTestContext::new(cx).await;
13359 let language = Arc::new(Language::new(
13360 LanguageConfig {
13361 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13362 ..Default::default()
13363 },
13364 Some(tree_sitter_rust::LANGUAGE.into()),
13365 ));
13366 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13367
13368 // If multiple selections intersect a line, the line is only toggled once.
13369 cx.set_state(indoc! {"
13370 fn a() {
13371 «//b();
13372 ˇ»// «c();
13373 //ˇ» d();
13374 }
13375 "});
13376
13377 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13378
13379 cx.assert_editor_state(indoc! {"
13380 fn a() {
13381 «b();
13382 c();
13383 ˇ» d();
13384 }
13385 "});
13386
13387 // The comment prefix is inserted at the same column for every line in a
13388 // selection.
13389 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13390
13391 cx.assert_editor_state(indoc! {"
13392 fn a() {
13393 // «b();
13394 // c();
13395 ˇ»// d();
13396 }
13397 "});
13398
13399 // If a selection ends at the beginning of a line, that line is not toggled.
13400 cx.set_selections_state(indoc! {"
13401 fn a() {
13402 // b();
13403 «// c();
13404 ˇ» // d();
13405 }
13406 "});
13407
13408 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13409
13410 cx.assert_editor_state(indoc! {"
13411 fn a() {
13412 // b();
13413 «c();
13414 ˇ» // d();
13415 }
13416 "});
13417
13418 // If a selection span a single line and is empty, the line is toggled.
13419 cx.set_state(indoc! {"
13420 fn a() {
13421 a();
13422 b();
13423 ˇ
13424 }
13425 "});
13426
13427 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13428
13429 cx.assert_editor_state(indoc! {"
13430 fn a() {
13431 a();
13432 b();
13433 //•ˇ
13434 }
13435 "});
13436
13437 // If a selection span multiple lines, empty lines are not toggled.
13438 cx.set_state(indoc! {"
13439 fn a() {
13440 «a();
13441
13442 c();ˇ»
13443 }
13444 "});
13445
13446 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13447
13448 cx.assert_editor_state(indoc! {"
13449 fn a() {
13450 // «a();
13451
13452 // c();ˇ»
13453 }
13454 "});
13455
13456 // If a selection includes multiple comment prefixes, all lines are uncommented.
13457 cx.set_state(indoc! {"
13458 fn a() {
13459 «// a();
13460 /// b();
13461 //! c();ˇ»
13462 }
13463 "});
13464
13465 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13466
13467 cx.assert_editor_state(indoc! {"
13468 fn a() {
13469 «a();
13470 b();
13471 c();ˇ»
13472 }
13473 "});
13474}
13475
13476#[gpui::test]
13477async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
13478 init_test(cx, |_| {});
13479 let mut cx = EditorTestContext::new(cx).await;
13480 let language = Arc::new(Language::new(
13481 LanguageConfig {
13482 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13483 ..Default::default()
13484 },
13485 Some(tree_sitter_rust::LANGUAGE.into()),
13486 ));
13487 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13488
13489 let toggle_comments = &ToggleComments {
13490 advance_downwards: false,
13491 ignore_indent: true,
13492 };
13493
13494 // If multiple selections intersect a line, the line is only toggled once.
13495 cx.set_state(indoc! {"
13496 fn a() {
13497 // «b();
13498 // c();
13499 // ˇ» d();
13500 }
13501 "});
13502
13503 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13504
13505 cx.assert_editor_state(indoc! {"
13506 fn a() {
13507 «b();
13508 c();
13509 ˇ» d();
13510 }
13511 "});
13512
13513 // The comment prefix is inserted at the beginning of each line
13514 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13515
13516 cx.assert_editor_state(indoc! {"
13517 fn a() {
13518 // «b();
13519 // c();
13520 // ˇ» d();
13521 }
13522 "});
13523
13524 // If a selection ends at the beginning of a line, that line is not toggled.
13525 cx.set_selections_state(indoc! {"
13526 fn a() {
13527 // b();
13528 // «c();
13529 ˇ»// d();
13530 }
13531 "});
13532
13533 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13534
13535 cx.assert_editor_state(indoc! {"
13536 fn a() {
13537 // b();
13538 «c();
13539 ˇ»// d();
13540 }
13541 "});
13542
13543 // If a selection span a single line and is empty, the line is toggled.
13544 cx.set_state(indoc! {"
13545 fn a() {
13546 a();
13547 b();
13548 ˇ
13549 }
13550 "});
13551
13552 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13553
13554 cx.assert_editor_state(indoc! {"
13555 fn a() {
13556 a();
13557 b();
13558 //ˇ
13559 }
13560 "});
13561
13562 // If a selection span multiple lines, empty lines are not toggled.
13563 cx.set_state(indoc! {"
13564 fn a() {
13565 «a();
13566
13567 c();ˇ»
13568 }
13569 "});
13570
13571 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13572
13573 cx.assert_editor_state(indoc! {"
13574 fn a() {
13575 // «a();
13576
13577 // c();ˇ»
13578 }
13579 "});
13580
13581 // If a selection includes multiple comment prefixes, all lines are uncommented.
13582 cx.set_state(indoc! {"
13583 fn a() {
13584 // «a();
13585 /// b();
13586 //! c();ˇ»
13587 }
13588 "});
13589
13590 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13591
13592 cx.assert_editor_state(indoc! {"
13593 fn a() {
13594 «a();
13595 b();
13596 c();ˇ»
13597 }
13598 "});
13599}
13600
13601#[gpui::test]
13602async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
13603 init_test(cx, |_| {});
13604
13605 let language = Arc::new(Language::new(
13606 LanguageConfig {
13607 line_comments: vec!["// ".into()],
13608 ..Default::default()
13609 },
13610 Some(tree_sitter_rust::LANGUAGE.into()),
13611 ));
13612
13613 let mut cx = EditorTestContext::new(cx).await;
13614
13615 cx.language_registry().add(language.clone());
13616 cx.update_buffer(|buffer, cx| {
13617 buffer.set_language(Some(language), cx);
13618 });
13619
13620 let toggle_comments = &ToggleComments {
13621 advance_downwards: true,
13622 ignore_indent: false,
13623 };
13624
13625 // Single cursor on one line -> advance
13626 // Cursor moves horizontally 3 characters as well on non-blank line
13627 cx.set_state(indoc!(
13628 "fn a() {
13629 ˇdog();
13630 cat();
13631 }"
13632 ));
13633 cx.update_editor(|editor, window, cx| {
13634 editor.toggle_comments(toggle_comments, window, cx);
13635 });
13636 cx.assert_editor_state(indoc!(
13637 "fn a() {
13638 // dog();
13639 catˇ();
13640 }"
13641 ));
13642
13643 // Single selection on one line -> don't advance
13644 cx.set_state(indoc!(
13645 "fn a() {
13646 «dog()ˇ»;
13647 cat();
13648 }"
13649 ));
13650 cx.update_editor(|editor, window, cx| {
13651 editor.toggle_comments(toggle_comments, window, cx);
13652 });
13653 cx.assert_editor_state(indoc!(
13654 "fn a() {
13655 // «dog()ˇ»;
13656 cat();
13657 }"
13658 ));
13659
13660 // Multiple cursors on one line -> advance
13661 cx.set_state(indoc!(
13662 "fn a() {
13663 ˇdˇog();
13664 cat();
13665 }"
13666 ));
13667 cx.update_editor(|editor, window, cx| {
13668 editor.toggle_comments(toggle_comments, window, cx);
13669 });
13670 cx.assert_editor_state(indoc!(
13671 "fn a() {
13672 // dog();
13673 catˇ(ˇ);
13674 }"
13675 ));
13676
13677 // Multiple cursors on one line, with selection -> don't advance
13678 cx.set_state(indoc!(
13679 "fn a() {
13680 ˇdˇog«()ˇ»;
13681 cat();
13682 }"
13683 ));
13684 cx.update_editor(|editor, window, cx| {
13685 editor.toggle_comments(toggle_comments, window, cx);
13686 });
13687 cx.assert_editor_state(indoc!(
13688 "fn a() {
13689 // ˇdˇog«()ˇ»;
13690 cat();
13691 }"
13692 ));
13693
13694 // Single cursor on one line -> advance
13695 // Cursor moves to column 0 on blank line
13696 cx.set_state(indoc!(
13697 "fn a() {
13698 ˇdog();
13699
13700 cat();
13701 }"
13702 ));
13703 cx.update_editor(|editor, window, cx| {
13704 editor.toggle_comments(toggle_comments, window, cx);
13705 });
13706 cx.assert_editor_state(indoc!(
13707 "fn a() {
13708 // dog();
13709 ˇ
13710 cat();
13711 }"
13712 ));
13713
13714 // Single cursor on one line -> advance
13715 // Cursor starts and ends at column 0
13716 cx.set_state(indoc!(
13717 "fn a() {
13718 ˇ dog();
13719 cat();
13720 }"
13721 ));
13722 cx.update_editor(|editor, window, cx| {
13723 editor.toggle_comments(toggle_comments, window, cx);
13724 });
13725 cx.assert_editor_state(indoc!(
13726 "fn a() {
13727 // dog();
13728 ˇ cat();
13729 }"
13730 ));
13731}
13732
13733#[gpui::test]
13734async fn test_toggle_block_comment(cx: &mut TestAppContext) {
13735 init_test(cx, |_| {});
13736
13737 let mut cx = EditorTestContext::new(cx).await;
13738
13739 let html_language = Arc::new(
13740 Language::new(
13741 LanguageConfig {
13742 name: "HTML".into(),
13743 block_comment: Some(("<!-- ".into(), " -->".into())),
13744 ..Default::default()
13745 },
13746 Some(tree_sitter_html::LANGUAGE.into()),
13747 )
13748 .with_injection_query(
13749 r#"
13750 (script_element
13751 (raw_text) @injection.content
13752 (#set! injection.language "javascript"))
13753 "#,
13754 )
13755 .unwrap(),
13756 );
13757
13758 let javascript_language = Arc::new(Language::new(
13759 LanguageConfig {
13760 name: "JavaScript".into(),
13761 line_comments: vec!["// ".into()],
13762 ..Default::default()
13763 },
13764 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
13765 ));
13766
13767 cx.language_registry().add(html_language.clone());
13768 cx.language_registry().add(javascript_language.clone());
13769 cx.update_buffer(|buffer, cx| {
13770 buffer.set_language(Some(html_language), cx);
13771 });
13772
13773 // Toggle comments for empty selections
13774 cx.set_state(
13775 &r#"
13776 <p>A</p>ˇ
13777 <p>B</p>ˇ
13778 <p>C</p>ˇ
13779 "#
13780 .unindent(),
13781 );
13782 cx.update_editor(|editor, window, cx| {
13783 editor.toggle_comments(&ToggleComments::default(), window, cx)
13784 });
13785 cx.assert_editor_state(
13786 &r#"
13787 <!-- <p>A</p>ˇ -->
13788 <!-- <p>B</p>ˇ -->
13789 <!-- <p>C</p>ˇ -->
13790 "#
13791 .unindent(),
13792 );
13793 cx.update_editor(|editor, window, cx| {
13794 editor.toggle_comments(&ToggleComments::default(), window, cx)
13795 });
13796 cx.assert_editor_state(
13797 &r#"
13798 <p>A</p>ˇ
13799 <p>B</p>ˇ
13800 <p>C</p>ˇ
13801 "#
13802 .unindent(),
13803 );
13804
13805 // Toggle comments for mixture of empty and non-empty selections, where
13806 // multiple selections occupy a given line.
13807 cx.set_state(
13808 &r#"
13809 <p>A«</p>
13810 <p>ˇ»B</p>ˇ
13811 <p>C«</p>
13812 <p>ˇ»D</p>ˇ
13813 "#
13814 .unindent(),
13815 );
13816
13817 cx.update_editor(|editor, window, cx| {
13818 editor.toggle_comments(&ToggleComments::default(), window, cx)
13819 });
13820 cx.assert_editor_state(
13821 &r#"
13822 <!-- <p>A«</p>
13823 <p>ˇ»B</p>ˇ -->
13824 <!-- <p>C«</p>
13825 <p>ˇ»D</p>ˇ -->
13826 "#
13827 .unindent(),
13828 );
13829 cx.update_editor(|editor, window, cx| {
13830 editor.toggle_comments(&ToggleComments::default(), window, cx)
13831 });
13832 cx.assert_editor_state(
13833 &r#"
13834 <p>A«</p>
13835 <p>ˇ»B</p>ˇ
13836 <p>C«</p>
13837 <p>ˇ»D</p>ˇ
13838 "#
13839 .unindent(),
13840 );
13841
13842 // Toggle comments when different languages are active for different
13843 // selections.
13844 cx.set_state(
13845 &r#"
13846 ˇ<script>
13847 ˇvar x = new Y();
13848 ˇ</script>
13849 "#
13850 .unindent(),
13851 );
13852 cx.executor().run_until_parked();
13853 cx.update_editor(|editor, window, cx| {
13854 editor.toggle_comments(&ToggleComments::default(), window, cx)
13855 });
13856 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
13857 // Uncommenting and commenting from this position brings in even more wrong artifacts.
13858 cx.assert_editor_state(
13859 &r#"
13860 <!-- ˇ<script> -->
13861 // ˇvar x = new Y();
13862 <!-- ˇ</script> -->
13863 "#
13864 .unindent(),
13865 );
13866}
13867
13868#[gpui::test]
13869fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
13870 init_test(cx, |_| {});
13871
13872 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13873 let multibuffer = cx.new(|cx| {
13874 let mut multibuffer = MultiBuffer::new(ReadWrite);
13875 multibuffer.push_excerpts(
13876 buffer.clone(),
13877 [
13878 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
13879 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
13880 ],
13881 cx,
13882 );
13883 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
13884 multibuffer
13885 });
13886
13887 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
13888 editor.update_in(cx, |editor, window, cx| {
13889 assert_eq!(editor.text(cx), "aaaa\nbbbb");
13890 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13891 s.select_ranges([
13892 Point::new(0, 0)..Point::new(0, 0),
13893 Point::new(1, 0)..Point::new(1, 0),
13894 ])
13895 });
13896
13897 editor.handle_input("X", window, cx);
13898 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
13899 assert_eq!(
13900 editor.selections.ranges(cx),
13901 [
13902 Point::new(0, 1)..Point::new(0, 1),
13903 Point::new(1, 1)..Point::new(1, 1),
13904 ]
13905 );
13906
13907 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
13908 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13909 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
13910 });
13911 editor.backspace(&Default::default(), window, cx);
13912 assert_eq!(editor.text(cx), "Xa\nbbb");
13913 assert_eq!(
13914 editor.selections.ranges(cx),
13915 [Point::new(1, 0)..Point::new(1, 0)]
13916 );
13917
13918 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13919 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
13920 });
13921 editor.backspace(&Default::default(), window, cx);
13922 assert_eq!(editor.text(cx), "X\nbb");
13923 assert_eq!(
13924 editor.selections.ranges(cx),
13925 [Point::new(0, 1)..Point::new(0, 1)]
13926 );
13927 });
13928}
13929
13930#[gpui::test]
13931fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
13932 init_test(cx, |_| {});
13933
13934 let markers = vec![('[', ']').into(), ('(', ')').into()];
13935 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
13936 indoc! {"
13937 [aaaa
13938 (bbbb]
13939 cccc)",
13940 },
13941 markers.clone(),
13942 );
13943 let excerpt_ranges = markers.into_iter().map(|marker| {
13944 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
13945 ExcerptRange::new(context.clone())
13946 });
13947 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
13948 let multibuffer = cx.new(|cx| {
13949 let mut multibuffer = MultiBuffer::new(ReadWrite);
13950 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
13951 multibuffer
13952 });
13953
13954 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
13955 editor.update_in(cx, |editor, window, cx| {
13956 let (expected_text, selection_ranges) = marked_text_ranges(
13957 indoc! {"
13958 aaaa
13959 bˇbbb
13960 bˇbbˇb
13961 cccc"
13962 },
13963 true,
13964 );
13965 assert_eq!(editor.text(cx), expected_text);
13966 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13967 s.select_ranges(selection_ranges)
13968 });
13969
13970 editor.handle_input("X", window, cx);
13971
13972 let (expected_text, expected_selections) = marked_text_ranges(
13973 indoc! {"
13974 aaaa
13975 bXˇbbXb
13976 bXˇbbXˇb
13977 cccc"
13978 },
13979 false,
13980 );
13981 assert_eq!(editor.text(cx), expected_text);
13982 assert_eq!(editor.selections.ranges(cx), expected_selections);
13983
13984 editor.newline(&Newline, window, cx);
13985 let (expected_text, expected_selections) = marked_text_ranges(
13986 indoc! {"
13987 aaaa
13988 bX
13989 ˇbbX
13990 b
13991 bX
13992 ˇbbX
13993 ˇb
13994 cccc"
13995 },
13996 false,
13997 );
13998 assert_eq!(editor.text(cx), expected_text);
13999 assert_eq!(editor.selections.ranges(cx), expected_selections);
14000 });
14001}
14002
14003#[gpui::test]
14004fn test_refresh_selections(cx: &mut TestAppContext) {
14005 init_test(cx, |_| {});
14006
14007 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14008 let mut excerpt1_id = None;
14009 let multibuffer = cx.new(|cx| {
14010 let mut multibuffer = MultiBuffer::new(ReadWrite);
14011 excerpt1_id = multibuffer
14012 .push_excerpts(
14013 buffer.clone(),
14014 [
14015 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
14016 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
14017 ],
14018 cx,
14019 )
14020 .into_iter()
14021 .next();
14022 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
14023 multibuffer
14024 });
14025
14026 let editor = cx.add_window(|window, cx| {
14027 let mut editor = build_editor(multibuffer.clone(), window, cx);
14028 let snapshot = editor.snapshot(window, cx);
14029 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14030 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
14031 });
14032 editor.begin_selection(
14033 Point::new(2, 1).to_display_point(&snapshot),
14034 true,
14035 1,
14036 window,
14037 cx,
14038 );
14039 assert_eq!(
14040 editor.selections.ranges(cx),
14041 [
14042 Point::new(1, 3)..Point::new(1, 3),
14043 Point::new(2, 1)..Point::new(2, 1),
14044 ]
14045 );
14046 editor
14047 });
14048
14049 // Refreshing selections is a no-op when excerpts haven't changed.
14050 _ = editor.update(cx, |editor, window, cx| {
14051 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14052 assert_eq!(
14053 editor.selections.ranges(cx),
14054 [
14055 Point::new(1, 3)..Point::new(1, 3),
14056 Point::new(2, 1)..Point::new(2, 1),
14057 ]
14058 );
14059 });
14060
14061 multibuffer.update(cx, |multibuffer, cx| {
14062 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14063 });
14064 _ = editor.update(cx, |editor, window, cx| {
14065 // Removing an excerpt causes the first selection to become degenerate.
14066 assert_eq!(
14067 editor.selections.ranges(cx),
14068 [
14069 Point::new(0, 0)..Point::new(0, 0),
14070 Point::new(0, 1)..Point::new(0, 1)
14071 ]
14072 );
14073
14074 // Refreshing selections will relocate the first selection to the original buffer
14075 // location.
14076 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14077 assert_eq!(
14078 editor.selections.ranges(cx),
14079 [
14080 Point::new(0, 1)..Point::new(0, 1),
14081 Point::new(0, 3)..Point::new(0, 3)
14082 ]
14083 );
14084 assert!(editor.selections.pending_anchor().is_some());
14085 });
14086}
14087
14088#[gpui::test]
14089fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
14090 init_test(cx, |_| {});
14091
14092 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14093 let mut excerpt1_id = None;
14094 let multibuffer = cx.new(|cx| {
14095 let mut multibuffer = MultiBuffer::new(ReadWrite);
14096 excerpt1_id = multibuffer
14097 .push_excerpts(
14098 buffer.clone(),
14099 [
14100 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
14101 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
14102 ],
14103 cx,
14104 )
14105 .into_iter()
14106 .next();
14107 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
14108 multibuffer
14109 });
14110
14111 let editor = cx.add_window(|window, cx| {
14112 let mut editor = build_editor(multibuffer.clone(), window, cx);
14113 let snapshot = editor.snapshot(window, cx);
14114 editor.begin_selection(
14115 Point::new(1, 3).to_display_point(&snapshot),
14116 false,
14117 1,
14118 window,
14119 cx,
14120 );
14121 assert_eq!(
14122 editor.selections.ranges(cx),
14123 [Point::new(1, 3)..Point::new(1, 3)]
14124 );
14125 editor
14126 });
14127
14128 multibuffer.update(cx, |multibuffer, cx| {
14129 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14130 });
14131 _ = editor.update(cx, |editor, window, cx| {
14132 assert_eq!(
14133 editor.selections.ranges(cx),
14134 [Point::new(0, 0)..Point::new(0, 0)]
14135 );
14136
14137 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
14138 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14139 assert_eq!(
14140 editor.selections.ranges(cx),
14141 [Point::new(0, 3)..Point::new(0, 3)]
14142 );
14143 assert!(editor.selections.pending_anchor().is_some());
14144 });
14145}
14146
14147#[gpui::test]
14148async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
14149 init_test(cx, |_| {});
14150
14151 let language = Arc::new(
14152 Language::new(
14153 LanguageConfig {
14154 brackets: BracketPairConfig {
14155 pairs: vec![
14156 BracketPair {
14157 start: "{".to_string(),
14158 end: "}".to_string(),
14159 close: true,
14160 surround: true,
14161 newline: true,
14162 },
14163 BracketPair {
14164 start: "/* ".to_string(),
14165 end: " */".to_string(),
14166 close: true,
14167 surround: true,
14168 newline: true,
14169 },
14170 ],
14171 ..Default::default()
14172 },
14173 ..Default::default()
14174 },
14175 Some(tree_sitter_rust::LANGUAGE.into()),
14176 )
14177 .with_indents_query("")
14178 .unwrap(),
14179 );
14180
14181 let text = concat!(
14182 "{ }\n", //
14183 " x\n", //
14184 " /* */\n", //
14185 "x\n", //
14186 "{{} }\n", //
14187 );
14188
14189 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
14190 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14191 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14192 editor
14193 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
14194 .await;
14195
14196 editor.update_in(cx, |editor, window, cx| {
14197 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14198 s.select_display_ranges([
14199 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
14200 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
14201 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
14202 ])
14203 });
14204 editor.newline(&Newline, window, cx);
14205
14206 assert_eq!(
14207 editor.buffer().read(cx).read(cx).text(),
14208 concat!(
14209 "{ \n", // Suppress rustfmt
14210 "\n", //
14211 "}\n", //
14212 " x\n", //
14213 " /* \n", //
14214 " \n", //
14215 " */\n", //
14216 "x\n", //
14217 "{{} \n", //
14218 "}\n", //
14219 )
14220 );
14221 });
14222}
14223
14224#[gpui::test]
14225fn test_highlighted_ranges(cx: &mut TestAppContext) {
14226 init_test(cx, |_| {});
14227
14228 let editor = cx.add_window(|window, cx| {
14229 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
14230 build_editor(buffer.clone(), window, cx)
14231 });
14232
14233 _ = editor.update(cx, |editor, window, cx| {
14234 struct Type1;
14235 struct Type2;
14236
14237 let buffer = editor.buffer.read(cx).snapshot(cx);
14238
14239 let anchor_range =
14240 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
14241
14242 editor.highlight_background::<Type1>(
14243 &[
14244 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
14245 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
14246 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
14247 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
14248 ],
14249 |_| Hsla::red(),
14250 cx,
14251 );
14252 editor.highlight_background::<Type2>(
14253 &[
14254 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
14255 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
14256 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
14257 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
14258 ],
14259 |_| Hsla::green(),
14260 cx,
14261 );
14262
14263 let snapshot = editor.snapshot(window, cx);
14264 let mut highlighted_ranges = editor.background_highlights_in_range(
14265 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
14266 &snapshot,
14267 cx.theme(),
14268 );
14269 // Enforce a consistent ordering based on color without relying on the ordering of the
14270 // highlight's `TypeId` which is non-executor.
14271 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
14272 assert_eq!(
14273 highlighted_ranges,
14274 &[
14275 (
14276 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
14277 Hsla::red(),
14278 ),
14279 (
14280 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14281 Hsla::red(),
14282 ),
14283 (
14284 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
14285 Hsla::green(),
14286 ),
14287 (
14288 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
14289 Hsla::green(),
14290 ),
14291 ]
14292 );
14293 assert_eq!(
14294 editor.background_highlights_in_range(
14295 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
14296 &snapshot,
14297 cx.theme(),
14298 ),
14299 &[(
14300 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14301 Hsla::red(),
14302 )]
14303 );
14304 });
14305}
14306
14307#[gpui::test]
14308async fn test_following(cx: &mut TestAppContext) {
14309 init_test(cx, |_| {});
14310
14311 let fs = FakeFs::new(cx.executor());
14312 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14313
14314 let buffer = project.update(cx, |project, cx| {
14315 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
14316 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
14317 });
14318 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
14319 let follower = cx.update(|cx| {
14320 cx.open_window(
14321 WindowOptions {
14322 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
14323 gpui::Point::new(px(0.), px(0.)),
14324 gpui::Point::new(px(10.), px(80.)),
14325 ))),
14326 ..Default::default()
14327 },
14328 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
14329 )
14330 .unwrap()
14331 });
14332
14333 let is_still_following = Rc::new(RefCell::new(true));
14334 let follower_edit_event_count = Rc::new(RefCell::new(0));
14335 let pending_update = Rc::new(RefCell::new(None));
14336 let leader_entity = leader.root(cx).unwrap();
14337 let follower_entity = follower.root(cx).unwrap();
14338 _ = follower.update(cx, {
14339 let update = pending_update.clone();
14340 let is_still_following = is_still_following.clone();
14341 let follower_edit_event_count = follower_edit_event_count.clone();
14342 |_, window, cx| {
14343 cx.subscribe_in(
14344 &leader_entity,
14345 window,
14346 move |_, leader, event, window, cx| {
14347 leader.read(cx).add_event_to_update_proto(
14348 event,
14349 &mut update.borrow_mut(),
14350 window,
14351 cx,
14352 );
14353 },
14354 )
14355 .detach();
14356
14357 cx.subscribe_in(
14358 &follower_entity,
14359 window,
14360 move |_, _, event: &EditorEvent, _window, _cx| {
14361 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
14362 *is_still_following.borrow_mut() = false;
14363 }
14364
14365 if let EditorEvent::BufferEdited = event {
14366 *follower_edit_event_count.borrow_mut() += 1;
14367 }
14368 },
14369 )
14370 .detach();
14371 }
14372 });
14373
14374 // Update the selections only
14375 _ = leader.update(cx, |leader, window, cx| {
14376 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14377 s.select_ranges([1..1])
14378 });
14379 });
14380 follower
14381 .update(cx, |follower, window, cx| {
14382 follower.apply_update_proto(
14383 &project,
14384 pending_update.borrow_mut().take().unwrap(),
14385 window,
14386 cx,
14387 )
14388 })
14389 .unwrap()
14390 .await
14391 .unwrap();
14392 _ = follower.update(cx, |follower, _, cx| {
14393 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
14394 });
14395 assert!(*is_still_following.borrow());
14396 assert_eq!(*follower_edit_event_count.borrow(), 0);
14397
14398 // Update the scroll position only
14399 _ = leader.update(cx, |leader, window, cx| {
14400 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14401 });
14402 follower
14403 .update(cx, |follower, window, cx| {
14404 follower.apply_update_proto(
14405 &project,
14406 pending_update.borrow_mut().take().unwrap(),
14407 window,
14408 cx,
14409 )
14410 })
14411 .unwrap()
14412 .await
14413 .unwrap();
14414 assert_eq!(
14415 follower
14416 .update(cx, |follower, _, cx| follower.scroll_position(cx))
14417 .unwrap(),
14418 gpui::Point::new(1.5, 3.5)
14419 );
14420 assert!(*is_still_following.borrow());
14421 assert_eq!(*follower_edit_event_count.borrow(), 0);
14422
14423 // Update the selections and scroll position. The follower's scroll position is updated
14424 // via autoscroll, not via the leader's exact scroll position.
14425 _ = leader.update(cx, |leader, window, cx| {
14426 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14427 s.select_ranges([0..0])
14428 });
14429 leader.request_autoscroll(Autoscroll::newest(), cx);
14430 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14431 });
14432 follower
14433 .update(cx, |follower, window, cx| {
14434 follower.apply_update_proto(
14435 &project,
14436 pending_update.borrow_mut().take().unwrap(),
14437 window,
14438 cx,
14439 )
14440 })
14441 .unwrap()
14442 .await
14443 .unwrap();
14444 _ = follower.update(cx, |follower, _, cx| {
14445 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
14446 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
14447 });
14448 assert!(*is_still_following.borrow());
14449
14450 // Creating a pending selection that precedes another selection
14451 _ = leader.update(cx, |leader, window, cx| {
14452 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14453 s.select_ranges([1..1])
14454 });
14455 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
14456 });
14457 follower
14458 .update(cx, |follower, window, cx| {
14459 follower.apply_update_proto(
14460 &project,
14461 pending_update.borrow_mut().take().unwrap(),
14462 window,
14463 cx,
14464 )
14465 })
14466 .unwrap()
14467 .await
14468 .unwrap();
14469 _ = follower.update(cx, |follower, _, cx| {
14470 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
14471 });
14472 assert!(*is_still_following.borrow());
14473
14474 // Extend the pending selection so that it surrounds another selection
14475 _ = leader.update(cx, |leader, window, cx| {
14476 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
14477 });
14478 follower
14479 .update(cx, |follower, window, cx| {
14480 follower.apply_update_proto(
14481 &project,
14482 pending_update.borrow_mut().take().unwrap(),
14483 window,
14484 cx,
14485 )
14486 })
14487 .unwrap()
14488 .await
14489 .unwrap();
14490 _ = follower.update(cx, |follower, _, cx| {
14491 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
14492 });
14493
14494 // Scrolling locally breaks the follow
14495 _ = follower.update(cx, |follower, window, cx| {
14496 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
14497 follower.set_scroll_anchor(
14498 ScrollAnchor {
14499 anchor: top_anchor,
14500 offset: gpui::Point::new(0.0, 0.5),
14501 },
14502 window,
14503 cx,
14504 );
14505 });
14506 assert!(!(*is_still_following.borrow()));
14507}
14508
14509#[gpui::test]
14510async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
14511 init_test(cx, |_| {});
14512
14513 let fs = FakeFs::new(cx.executor());
14514 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14515 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14516 let pane = workspace
14517 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14518 .unwrap();
14519
14520 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14521
14522 let leader = pane.update_in(cx, |_, window, cx| {
14523 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
14524 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
14525 });
14526
14527 // Start following the editor when it has no excerpts.
14528 let mut state_message =
14529 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14530 let workspace_entity = workspace.root(cx).unwrap();
14531 let follower_1 = cx
14532 .update_window(*workspace.deref(), |_, window, cx| {
14533 Editor::from_state_proto(
14534 workspace_entity,
14535 ViewId {
14536 creator: CollaboratorId::PeerId(PeerId::default()),
14537 id: 0,
14538 },
14539 &mut state_message,
14540 window,
14541 cx,
14542 )
14543 })
14544 .unwrap()
14545 .unwrap()
14546 .await
14547 .unwrap();
14548
14549 let update_message = Rc::new(RefCell::new(None));
14550 follower_1.update_in(cx, {
14551 let update = update_message.clone();
14552 |_, window, cx| {
14553 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
14554 leader.read(cx).add_event_to_update_proto(
14555 event,
14556 &mut update.borrow_mut(),
14557 window,
14558 cx,
14559 );
14560 })
14561 .detach();
14562 }
14563 });
14564
14565 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
14566 (
14567 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
14568 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
14569 )
14570 });
14571
14572 // Insert some excerpts.
14573 leader.update(cx, |leader, cx| {
14574 leader.buffer.update(cx, |multibuffer, cx| {
14575 multibuffer.set_excerpts_for_path(
14576 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
14577 buffer_1.clone(),
14578 vec![
14579 Point::row_range(0..3),
14580 Point::row_range(1..6),
14581 Point::row_range(12..15),
14582 ],
14583 0,
14584 cx,
14585 );
14586 multibuffer.set_excerpts_for_path(
14587 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
14588 buffer_2.clone(),
14589 vec![Point::row_range(0..6), Point::row_range(8..12)],
14590 0,
14591 cx,
14592 );
14593 });
14594 });
14595
14596 // Apply the update of adding the excerpts.
14597 follower_1
14598 .update_in(cx, |follower, window, cx| {
14599 follower.apply_update_proto(
14600 &project,
14601 update_message.borrow().clone().unwrap(),
14602 window,
14603 cx,
14604 )
14605 })
14606 .await
14607 .unwrap();
14608 assert_eq!(
14609 follower_1.update(cx, |editor, cx| editor.text(cx)),
14610 leader.update(cx, |editor, cx| editor.text(cx))
14611 );
14612 update_message.borrow_mut().take();
14613
14614 // Start following separately after it already has excerpts.
14615 let mut state_message =
14616 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14617 let workspace_entity = workspace.root(cx).unwrap();
14618 let follower_2 = cx
14619 .update_window(*workspace.deref(), |_, window, cx| {
14620 Editor::from_state_proto(
14621 workspace_entity,
14622 ViewId {
14623 creator: CollaboratorId::PeerId(PeerId::default()),
14624 id: 0,
14625 },
14626 &mut state_message,
14627 window,
14628 cx,
14629 )
14630 })
14631 .unwrap()
14632 .unwrap()
14633 .await
14634 .unwrap();
14635 assert_eq!(
14636 follower_2.update(cx, |editor, cx| editor.text(cx)),
14637 leader.update(cx, |editor, cx| editor.text(cx))
14638 );
14639
14640 // Remove some excerpts.
14641 leader.update(cx, |leader, cx| {
14642 leader.buffer.update(cx, |multibuffer, cx| {
14643 let excerpt_ids = multibuffer.excerpt_ids();
14644 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
14645 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
14646 });
14647 });
14648
14649 // Apply the update of removing the excerpts.
14650 follower_1
14651 .update_in(cx, |follower, window, cx| {
14652 follower.apply_update_proto(
14653 &project,
14654 update_message.borrow().clone().unwrap(),
14655 window,
14656 cx,
14657 )
14658 })
14659 .await
14660 .unwrap();
14661 follower_2
14662 .update_in(cx, |follower, window, cx| {
14663 follower.apply_update_proto(
14664 &project,
14665 update_message.borrow().clone().unwrap(),
14666 window,
14667 cx,
14668 )
14669 })
14670 .await
14671 .unwrap();
14672 update_message.borrow_mut().take();
14673 assert_eq!(
14674 follower_1.update(cx, |editor, cx| editor.text(cx)),
14675 leader.update(cx, |editor, cx| editor.text(cx))
14676 );
14677}
14678
14679#[gpui::test]
14680async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14681 init_test(cx, |_| {});
14682
14683 let mut cx = EditorTestContext::new(cx).await;
14684 let lsp_store =
14685 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
14686
14687 cx.set_state(indoc! {"
14688 ˇfn func(abc def: i32) -> u32 {
14689 }
14690 "});
14691
14692 cx.update(|_, cx| {
14693 lsp_store.update(cx, |lsp_store, cx| {
14694 lsp_store
14695 .update_diagnostics(
14696 LanguageServerId(0),
14697 lsp::PublishDiagnosticsParams {
14698 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
14699 version: None,
14700 diagnostics: vec![
14701 lsp::Diagnostic {
14702 range: lsp::Range::new(
14703 lsp::Position::new(0, 11),
14704 lsp::Position::new(0, 12),
14705 ),
14706 severity: Some(lsp::DiagnosticSeverity::ERROR),
14707 ..Default::default()
14708 },
14709 lsp::Diagnostic {
14710 range: lsp::Range::new(
14711 lsp::Position::new(0, 12),
14712 lsp::Position::new(0, 15),
14713 ),
14714 severity: Some(lsp::DiagnosticSeverity::ERROR),
14715 ..Default::default()
14716 },
14717 lsp::Diagnostic {
14718 range: lsp::Range::new(
14719 lsp::Position::new(0, 25),
14720 lsp::Position::new(0, 28),
14721 ),
14722 severity: Some(lsp::DiagnosticSeverity::ERROR),
14723 ..Default::default()
14724 },
14725 ],
14726 },
14727 None,
14728 DiagnosticSourceKind::Pushed,
14729 &[],
14730 cx,
14731 )
14732 .unwrap()
14733 });
14734 });
14735
14736 executor.run_until_parked();
14737
14738 cx.update_editor(|editor, window, cx| {
14739 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14740 });
14741
14742 cx.assert_editor_state(indoc! {"
14743 fn func(abc def: i32) -> ˇu32 {
14744 }
14745 "});
14746
14747 cx.update_editor(|editor, window, cx| {
14748 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14749 });
14750
14751 cx.assert_editor_state(indoc! {"
14752 fn func(abc ˇdef: i32) -> u32 {
14753 }
14754 "});
14755
14756 cx.update_editor(|editor, window, cx| {
14757 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14758 });
14759
14760 cx.assert_editor_state(indoc! {"
14761 fn func(abcˇ def: i32) -> u32 {
14762 }
14763 "});
14764
14765 cx.update_editor(|editor, window, cx| {
14766 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14767 });
14768
14769 cx.assert_editor_state(indoc! {"
14770 fn func(abc def: i32) -> ˇu32 {
14771 }
14772 "});
14773}
14774
14775#[gpui::test]
14776async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14777 init_test(cx, |_| {});
14778
14779 let mut cx = EditorTestContext::new(cx).await;
14780
14781 let diff_base = r#"
14782 use some::mod;
14783
14784 const A: u32 = 42;
14785
14786 fn main() {
14787 println!("hello");
14788
14789 println!("world");
14790 }
14791 "#
14792 .unindent();
14793
14794 // Edits are modified, removed, modified, added
14795 cx.set_state(
14796 &r#"
14797 use some::modified;
14798
14799 ˇ
14800 fn main() {
14801 println!("hello there");
14802
14803 println!("around the");
14804 println!("world");
14805 }
14806 "#
14807 .unindent(),
14808 );
14809
14810 cx.set_head_text(&diff_base);
14811 executor.run_until_parked();
14812
14813 cx.update_editor(|editor, window, cx| {
14814 //Wrap around the bottom of the buffer
14815 for _ in 0..3 {
14816 editor.go_to_next_hunk(&GoToHunk, window, cx);
14817 }
14818 });
14819
14820 cx.assert_editor_state(
14821 &r#"
14822 ˇuse some::modified;
14823
14824
14825 fn main() {
14826 println!("hello there");
14827
14828 println!("around the");
14829 println!("world");
14830 }
14831 "#
14832 .unindent(),
14833 );
14834
14835 cx.update_editor(|editor, window, cx| {
14836 //Wrap around the top of the buffer
14837 for _ in 0..2 {
14838 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14839 }
14840 });
14841
14842 cx.assert_editor_state(
14843 &r#"
14844 use some::modified;
14845
14846
14847 fn main() {
14848 ˇ println!("hello there");
14849
14850 println!("around the");
14851 println!("world");
14852 }
14853 "#
14854 .unindent(),
14855 );
14856
14857 cx.update_editor(|editor, window, cx| {
14858 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14859 });
14860
14861 cx.assert_editor_state(
14862 &r#"
14863 use some::modified;
14864
14865 ˇ
14866 fn main() {
14867 println!("hello there");
14868
14869 println!("around the");
14870 println!("world");
14871 }
14872 "#
14873 .unindent(),
14874 );
14875
14876 cx.update_editor(|editor, window, cx| {
14877 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14878 });
14879
14880 cx.assert_editor_state(
14881 &r#"
14882 ˇuse some::modified;
14883
14884
14885 fn main() {
14886 println!("hello there");
14887
14888 println!("around the");
14889 println!("world");
14890 }
14891 "#
14892 .unindent(),
14893 );
14894
14895 cx.update_editor(|editor, window, cx| {
14896 for _ in 0..2 {
14897 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14898 }
14899 });
14900
14901 cx.assert_editor_state(
14902 &r#"
14903 use some::modified;
14904
14905
14906 fn main() {
14907 ˇ println!("hello there");
14908
14909 println!("around the");
14910 println!("world");
14911 }
14912 "#
14913 .unindent(),
14914 );
14915
14916 cx.update_editor(|editor, window, cx| {
14917 editor.fold(&Fold, window, cx);
14918 });
14919
14920 cx.update_editor(|editor, window, cx| {
14921 editor.go_to_next_hunk(&GoToHunk, window, cx);
14922 });
14923
14924 cx.assert_editor_state(
14925 &r#"
14926 ˇuse some::modified;
14927
14928
14929 fn main() {
14930 println!("hello there");
14931
14932 println!("around the");
14933 println!("world");
14934 }
14935 "#
14936 .unindent(),
14937 );
14938}
14939
14940#[test]
14941fn test_split_words() {
14942 fn split(text: &str) -> Vec<&str> {
14943 split_words(text).collect()
14944 }
14945
14946 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
14947 assert_eq!(split("hello_world"), &["hello_", "world"]);
14948 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
14949 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
14950 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
14951 assert_eq!(split("helloworld"), &["helloworld"]);
14952
14953 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
14954}
14955
14956#[gpui::test]
14957async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
14958 init_test(cx, |_| {});
14959
14960 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
14961 let mut assert = |before, after| {
14962 let _state_context = cx.set_state(before);
14963 cx.run_until_parked();
14964 cx.update_editor(|editor, window, cx| {
14965 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
14966 });
14967 cx.run_until_parked();
14968 cx.assert_editor_state(after);
14969 };
14970
14971 // Outside bracket jumps to outside of matching bracket
14972 assert("console.logˇ(var);", "console.log(var)ˇ;");
14973 assert("console.log(var)ˇ;", "console.logˇ(var);");
14974
14975 // Inside bracket jumps to inside of matching bracket
14976 assert("console.log(ˇvar);", "console.log(varˇ);");
14977 assert("console.log(varˇ);", "console.log(ˇvar);");
14978
14979 // When outside a bracket and inside, favor jumping to the inside bracket
14980 assert(
14981 "console.log('foo', [1, 2, 3]ˇ);",
14982 "console.log(ˇ'foo', [1, 2, 3]);",
14983 );
14984 assert(
14985 "console.log(ˇ'foo', [1, 2, 3]);",
14986 "console.log('foo', [1, 2, 3]ˇ);",
14987 );
14988
14989 // Bias forward if two options are equally likely
14990 assert(
14991 "let result = curried_fun()ˇ();",
14992 "let result = curried_fun()()ˇ;",
14993 );
14994
14995 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
14996 assert(
14997 indoc! {"
14998 function test() {
14999 console.log('test')ˇ
15000 }"},
15001 indoc! {"
15002 function test() {
15003 console.logˇ('test')
15004 }"},
15005 );
15006}
15007
15008#[gpui::test]
15009async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
15010 init_test(cx, |_| {});
15011
15012 let fs = FakeFs::new(cx.executor());
15013 fs.insert_tree(
15014 path!("/a"),
15015 json!({
15016 "main.rs": "fn main() { let a = 5; }",
15017 "other.rs": "// Test file",
15018 }),
15019 )
15020 .await;
15021 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15022
15023 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15024 language_registry.add(Arc::new(Language::new(
15025 LanguageConfig {
15026 name: "Rust".into(),
15027 matcher: LanguageMatcher {
15028 path_suffixes: vec!["rs".to_string()],
15029 ..Default::default()
15030 },
15031 brackets: BracketPairConfig {
15032 pairs: vec![BracketPair {
15033 start: "{".to_string(),
15034 end: "}".to_string(),
15035 close: true,
15036 surround: true,
15037 newline: true,
15038 }],
15039 disabled_scopes_by_bracket_ix: Vec::new(),
15040 },
15041 ..Default::default()
15042 },
15043 Some(tree_sitter_rust::LANGUAGE.into()),
15044 )));
15045 let mut fake_servers = language_registry.register_fake_lsp(
15046 "Rust",
15047 FakeLspAdapter {
15048 capabilities: lsp::ServerCapabilities {
15049 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
15050 first_trigger_character: "{".to_string(),
15051 more_trigger_character: None,
15052 }),
15053 ..Default::default()
15054 },
15055 ..Default::default()
15056 },
15057 );
15058
15059 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15060
15061 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15062
15063 let worktree_id = workspace
15064 .update(cx, |workspace, _, cx| {
15065 workspace.project().update(cx, |project, cx| {
15066 project.worktrees(cx).next().unwrap().read(cx).id()
15067 })
15068 })
15069 .unwrap();
15070
15071 let buffer = project
15072 .update(cx, |project, cx| {
15073 project.open_local_buffer(path!("/a/main.rs"), cx)
15074 })
15075 .await
15076 .unwrap();
15077 let editor_handle = workspace
15078 .update(cx, |workspace, window, cx| {
15079 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
15080 })
15081 .unwrap()
15082 .await
15083 .unwrap()
15084 .downcast::<Editor>()
15085 .unwrap();
15086
15087 cx.executor().start_waiting();
15088 let fake_server = fake_servers.next().await.unwrap();
15089
15090 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
15091 |params, _| async move {
15092 assert_eq!(
15093 params.text_document_position.text_document.uri,
15094 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
15095 );
15096 assert_eq!(
15097 params.text_document_position.position,
15098 lsp::Position::new(0, 21),
15099 );
15100
15101 Ok(Some(vec![lsp::TextEdit {
15102 new_text: "]".to_string(),
15103 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15104 }]))
15105 },
15106 );
15107
15108 editor_handle.update_in(cx, |editor, window, cx| {
15109 window.focus(&editor.focus_handle(cx));
15110 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15111 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
15112 });
15113 editor.handle_input("{", window, cx);
15114 });
15115
15116 cx.executor().run_until_parked();
15117
15118 buffer.update(cx, |buffer, _| {
15119 assert_eq!(
15120 buffer.text(),
15121 "fn main() { let a = {5}; }",
15122 "No extra braces from on type formatting should appear in the buffer"
15123 )
15124 });
15125}
15126
15127#[gpui::test(iterations = 20, seeds(31))]
15128async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
15129 init_test(cx, |_| {});
15130
15131 let mut cx = EditorLspTestContext::new_rust(
15132 lsp::ServerCapabilities {
15133 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
15134 first_trigger_character: ".".to_string(),
15135 more_trigger_character: None,
15136 }),
15137 ..Default::default()
15138 },
15139 cx,
15140 )
15141 .await;
15142
15143 cx.update_buffer(|buffer, _| {
15144 // This causes autoindent to be async.
15145 buffer.set_sync_parse_timeout(Duration::ZERO)
15146 });
15147
15148 cx.set_state("fn c() {\n d()ˇ\n}\n");
15149 cx.simulate_keystroke("\n");
15150 cx.run_until_parked();
15151
15152 let buffer_cloned =
15153 cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap().clone());
15154 let mut request =
15155 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
15156 let buffer_cloned = buffer_cloned.clone();
15157 async move {
15158 buffer_cloned.update(&mut cx, |buffer, _| {
15159 assert_eq!(
15160 buffer.text(),
15161 "fn c() {\n d()\n .\n}\n",
15162 "OnTypeFormatting should triggered after autoindent applied"
15163 )
15164 })?;
15165
15166 Ok(Some(vec![]))
15167 }
15168 });
15169
15170 cx.simulate_keystroke(".");
15171 cx.run_until_parked();
15172
15173 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
15174 assert!(request.next().await.is_some());
15175 request.close();
15176 assert!(request.next().await.is_none());
15177}
15178
15179#[gpui::test]
15180async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
15181 init_test(cx, |_| {});
15182
15183 let fs = FakeFs::new(cx.executor());
15184 fs.insert_tree(
15185 path!("/a"),
15186 json!({
15187 "main.rs": "fn main() { let a = 5; }",
15188 "other.rs": "// Test file",
15189 }),
15190 )
15191 .await;
15192
15193 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15194
15195 let server_restarts = Arc::new(AtomicUsize::new(0));
15196 let closure_restarts = Arc::clone(&server_restarts);
15197 let language_server_name = "test language server";
15198 let language_name: LanguageName = "Rust".into();
15199
15200 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15201 language_registry.add(Arc::new(Language::new(
15202 LanguageConfig {
15203 name: language_name.clone(),
15204 matcher: LanguageMatcher {
15205 path_suffixes: vec!["rs".to_string()],
15206 ..Default::default()
15207 },
15208 ..Default::default()
15209 },
15210 Some(tree_sitter_rust::LANGUAGE.into()),
15211 )));
15212 let mut fake_servers = language_registry.register_fake_lsp(
15213 "Rust",
15214 FakeLspAdapter {
15215 name: language_server_name,
15216 initialization_options: Some(json!({
15217 "testOptionValue": true
15218 })),
15219 initializer: Some(Box::new(move |fake_server| {
15220 let task_restarts = Arc::clone(&closure_restarts);
15221 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
15222 task_restarts.fetch_add(1, atomic::Ordering::Release);
15223 futures::future::ready(Ok(()))
15224 });
15225 })),
15226 ..Default::default()
15227 },
15228 );
15229
15230 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15231 let _buffer = project
15232 .update(cx, |project, cx| {
15233 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
15234 })
15235 .await
15236 .unwrap();
15237 let _fake_server = fake_servers.next().await.unwrap();
15238 update_test_language_settings(cx, |language_settings| {
15239 language_settings.languages.0.insert(
15240 language_name.clone(),
15241 LanguageSettingsContent {
15242 tab_size: NonZeroU32::new(8),
15243 ..Default::default()
15244 },
15245 );
15246 });
15247 cx.executor().run_until_parked();
15248 assert_eq!(
15249 server_restarts.load(atomic::Ordering::Acquire),
15250 0,
15251 "Should not restart LSP server on an unrelated change"
15252 );
15253
15254 update_test_project_settings(cx, |project_settings| {
15255 project_settings.lsp.insert(
15256 "Some other server name".into(),
15257 LspSettings {
15258 binary: None,
15259 settings: None,
15260 initialization_options: Some(json!({
15261 "some other init value": false
15262 })),
15263 enable_lsp_tasks: false,
15264 },
15265 );
15266 });
15267 cx.executor().run_until_parked();
15268 assert_eq!(
15269 server_restarts.load(atomic::Ordering::Acquire),
15270 0,
15271 "Should not restart LSP server on an unrelated LSP settings change"
15272 );
15273
15274 update_test_project_settings(cx, |project_settings| {
15275 project_settings.lsp.insert(
15276 language_server_name.into(),
15277 LspSettings {
15278 binary: None,
15279 settings: None,
15280 initialization_options: Some(json!({
15281 "anotherInitValue": false
15282 })),
15283 enable_lsp_tasks: false,
15284 },
15285 );
15286 });
15287 cx.executor().run_until_parked();
15288 assert_eq!(
15289 server_restarts.load(atomic::Ordering::Acquire),
15290 1,
15291 "Should restart LSP server on a related LSP settings change"
15292 );
15293
15294 update_test_project_settings(cx, |project_settings| {
15295 project_settings.lsp.insert(
15296 language_server_name.into(),
15297 LspSettings {
15298 binary: None,
15299 settings: None,
15300 initialization_options: Some(json!({
15301 "anotherInitValue": false
15302 })),
15303 enable_lsp_tasks: false,
15304 },
15305 );
15306 });
15307 cx.executor().run_until_parked();
15308 assert_eq!(
15309 server_restarts.load(atomic::Ordering::Acquire),
15310 1,
15311 "Should not restart LSP server on a related LSP settings change that is the same"
15312 );
15313
15314 update_test_project_settings(cx, |project_settings| {
15315 project_settings.lsp.insert(
15316 language_server_name.into(),
15317 LspSettings {
15318 binary: None,
15319 settings: None,
15320 initialization_options: None,
15321 enable_lsp_tasks: false,
15322 },
15323 );
15324 });
15325 cx.executor().run_until_parked();
15326 assert_eq!(
15327 server_restarts.load(atomic::Ordering::Acquire),
15328 2,
15329 "Should restart LSP server on another related LSP settings change"
15330 );
15331}
15332
15333#[gpui::test]
15334async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
15335 init_test(cx, |_| {});
15336
15337 let mut cx = EditorLspTestContext::new_rust(
15338 lsp::ServerCapabilities {
15339 completion_provider: Some(lsp::CompletionOptions {
15340 trigger_characters: Some(vec![".".to_string()]),
15341 resolve_provider: Some(true),
15342 ..Default::default()
15343 }),
15344 ..Default::default()
15345 },
15346 cx,
15347 )
15348 .await;
15349
15350 cx.set_state("fn main() { let a = 2ˇ; }");
15351 cx.simulate_keystroke(".");
15352 let completion_item = lsp::CompletionItem {
15353 label: "some".into(),
15354 kind: Some(lsp::CompletionItemKind::SNIPPET),
15355 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15356 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15357 kind: lsp::MarkupKind::Markdown,
15358 value: "```rust\nSome(2)\n```".to_string(),
15359 })),
15360 deprecated: Some(false),
15361 sort_text: Some("fffffff2".to_string()),
15362 filter_text: Some("some".to_string()),
15363 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15364 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15365 range: lsp::Range {
15366 start: lsp::Position {
15367 line: 0,
15368 character: 22,
15369 },
15370 end: lsp::Position {
15371 line: 0,
15372 character: 22,
15373 },
15374 },
15375 new_text: "Some(2)".to_string(),
15376 })),
15377 additional_text_edits: Some(vec![lsp::TextEdit {
15378 range: lsp::Range {
15379 start: lsp::Position {
15380 line: 0,
15381 character: 20,
15382 },
15383 end: lsp::Position {
15384 line: 0,
15385 character: 22,
15386 },
15387 },
15388 new_text: "".to_string(),
15389 }]),
15390 ..Default::default()
15391 };
15392
15393 let closure_completion_item = completion_item.clone();
15394 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15395 let task_completion_item = closure_completion_item.clone();
15396 async move {
15397 Ok(Some(lsp::CompletionResponse::Array(vec![
15398 task_completion_item,
15399 ])))
15400 }
15401 });
15402
15403 request.next().await;
15404
15405 cx.condition(|editor, _| editor.context_menu_visible())
15406 .await;
15407 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15408 editor
15409 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15410 .unwrap()
15411 });
15412 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
15413
15414 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
15415 let task_completion_item = completion_item.clone();
15416 async move { Ok(task_completion_item) }
15417 })
15418 .next()
15419 .await
15420 .unwrap();
15421 apply_additional_edits.await.unwrap();
15422 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
15423}
15424
15425#[gpui::test]
15426async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
15427 init_test(cx, |_| {});
15428
15429 let mut cx = EditorLspTestContext::new_rust(
15430 lsp::ServerCapabilities {
15431 completion_provider: Some(lsp::CompletionOptions {
15432 trigger_characters: Some(vec![".".to_string()]),
15433 resolve_provider: Some(true),
15434 ..Default::default()
15435 }),
15436 ..Default::default()
15437 },
15438 cx,
15439 )
15440 .await;
15441
15442 cx.set_state("fn main() { let a = 2ˇ; }");
15443 cx.simulate_keystroke(".");
15444
15445 let item1 = lsp::CompletionItem {
15446 label: "method id()".to_string(),
15447 filter_text: Some("id".to_string()),
15448 detail: None,
15449 documentation: None,
15450 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15451 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15452 new_text: ".id".to_string(),
15453 })),
15454 ..lsp::CompletionItem::default()
15455 };
15456
15457 let item2 = lsp::CompletionItem {
15458 label: "other".to_string(),
15459 filter_text: Some("other".to_string()),
15460 detail: None,
15461 documentation: None,
15462 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15463 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15464 new_text: ".other".to_string(),
15465 })),
15466 ..lsp::CompletionItem::default()
15467 };
15468
15469 let item1 = item1.clone();
15470 cx.set_request_handler::<lsp::request::Completion, _, _>({
15471 let item1 = item1.clone();
15472 move |_, _, _| {
15473 let item1 = item1.clone();
15474 let item2 = item2.clone();
15475 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
15476 }
15477 })
15478 .next()
15479 .await;
15480
15481 cx.condition(|editor, _| editor.context_menu_visible())
15482 .await;
15483 cx.update_editor(|editor, _, _| {
15484 let context_menu = editor.context_menu.borrow_mut();
15485 let context_menu = context_menu
15486 .as_ref()
15487 .expect("Should have the context menu deployed");
15488 match context_menu {
15489 CodeContextMenu::Completions(completions_menu) => {
15490 let completions = completions_menu.completions.borrow_mut();
15491 assert_eq!(
15492 completions
15493 .iter()
15494 .map(|completion| &completion.label.text)
15495 .collect::<Vec<_>>(),
15496 vec!["method id()", "other"]
15497 )
15498 }
15499 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15500 }
15501 });
15502
15503 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
15504 let item1 = item1.clone();
15505 move |_, item_to_resolve, _| {
15506 let item1 = item1.clone();
15507 async move {
15508 if item1 == item_to_resolve {
15509 Ok(lsp::CompletionItem {
15510 label: "method id()".to_string(),
15511 filter_text: Some("id".to_string()),
15512 detail: Some("Now resolved!".to_string()),
15513 documentation: Some(lsp::Documentation::String("Docs".to_string())),
15514 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15515 range: lsp::Range::new(
15516 lsp::Position::new(0, 22),
15517 lsp::Position::new(0, 22),
15518 ),
15519 new_text: ".id".to_string(),
15520 })),
15521 ..lsp::CompletionItem::default()
15522 })
15523 } else {
15524 Ok(item_to_resolve)
15525 }
15526 }
15527 }
15528 })
15529 .next()
15530 .await
15531 .unwrap();
15532 cx.run_until_parked();
15533
15534 cx.update_editor(|editor, window, cx| {
15535 editor.context_menu_next(&Default::default(), window, cx);
15536 });
15537
15538 cx.update_editor(|editor, _, _| {
15539 let context_menu = editor.context_menu.borrow_mut();
15540 let context_menu = context_menu
15541 .as_ref()
15542 .expect("Should have the context menu deployed");
15543 match context_menu {
15544 CodeContextMenu::Completions(completions_menu) => {
15545 let completions = completions_menu.completions.borrow_mut();
15546 assert_eq!(
15547 completions
15548 .iter()
15549 .map(|completion| &completion.label.text)
15550 .collect::<Vec<_>>(),
15551 vec!["method id() Now resolved!", "other"],
15552 "Should update first completion label, but not second as the filter text did not match."
15553 );
15554 }
15555 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15556 }
15557 });
15558}
15559
15560#[gpui::test]
15561async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
15562 init_test(cx, |_| {});
15563 let mut cx = EditorLspTestContext::new_rust(
15564 lsp::ServerCapabilities {
15565 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
15566 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
15567 completion_provider: Some(lsp::CompletionOptions {
15568 resolve_provider: Some(true),
15569 ..Default::default()
15570 }),
15571 ..Default::default()
15572 },
15573 cx,
15574 )
15575 .await;
15576 cx.set_state(indoc! {"
15577 struct TestStruct {
15578 field: i32
15579 }
15580
15581 fn mainˇ() {
15582 let unused_var = 42;
15583 let test_struct = TestStruct { field: 42 };
15584 }
15585 "});
15586 let symbol_range = cx.lsp_range(indoc! {"
15587 struct TestStruct {
15588 field: i32
15589 }
15590
15591 «fn main»() {
15592 let unused_var = 42;
15593 let test_struct = TestStruct { field: 42 };
15594 }
15595 "});
15596 let mut hover_requests =
15597 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
15598 Ok(Some(lsp::Hover {
15599 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
15600 kind: lsp::MarkupKind::Markdown,
15601 value: "Function documentation".to_string(),
15602 }),
15603 range: Some(symbol_range),
15604 }))
15605 });
15606
15607 // Case 1: Test that code action menu hide hover popover
15608 cx.dispatch_action(Hover);
15609 hover_requests.next().await;
15610 cx.condition(|editor, _| editor.hover_state.visible()).await;
15611 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
15612 move |_, _, _| async move {
15613 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
15614 lsp::CodeAction {
15615 title: "Remove unused variable".to_string(),
15616 kind: Some(CodeActionKind::QUICKFIX),
15617 edit: Some(lsp::WorkspaceEdit {
15618 changes: Some(
15619 [(
15620 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
15621 vec![lsp::TextEdit {
15622 range: lsp::Range::new(
15623 lsp::Position::new(5, 4),
15624 lsp::Position::new(5, 27),
15625 ),
15626 new_text: "".to_string(),
15627 }],
15628 )]
15629 .into_iter()
15630 .collect(),
15631 ),
15632 ..Default::default()
15633 }),
15634 ..Default::default()
15635 },
15636 )]))
15637 },
15638 );
15639 cx.update_editor(|editor, window, cx| {
15640 editor.toggle_code_actions(
15641 &ToggleCodeActions {
15642 deployed_from: None,
15643 quick_launch: false,
15644 },
15645 window,
15646 cx,
15647 );
15648 });
15649 code_action_requests.next().await;
15650 cx.run_until_parked();
15651 cx.condition(|editor, _| editor.context_menu_visible())
15652 .await;
15653 cx.update_editor(|editor, _, _| {
15654 assert!(
15655 !editor.hover_state.visible(),
15656 "Hover popover should be hidden when code action menu is shown"
15657 );
15658 // Hide code actions
15659 editor.context_menu.take();
15660 });
15661
15662 // Case 2: Test that code completions hide hover popover
15663 cx.dispatch_action(Hover);
15664 hover_requests.next().await;
15665 cx.condition(|editor, _| editor.hover_state.visible()).await;
15666 let counter = Arc::new(AtomicUsize::new(0));
15667 let mut completion_requests =
15668 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15669 let counter = counter.clone();
15670 async move {
15671 counter.fetch_add(1, atomic::Ordering::Release);
15672 Ok(Some(lsp::CompletionResponse::Array(vec![
15673 lsp::CompletionItem {
15674 label: "main".into(),
15675 kind: Some(lsp::CompletionItemKind::FUNCTION),
15676 detail: Some("() -> ()".to_string()),
15677 ..Default::default()
15678 },
15679 lsp::CompletionItem {
15680 label: "TestStruct".into(),
15681 kind: Some(lsp::CompletionItemKind::STRUCT),
15682 detail: Some("struct TestStruct".to_string()),
15683 ..Default::default()
15684 },
15685 ])))
15686 }
15687 });
15688 cx.update_editor(|editor, window, cx| {
15689 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15690 });
15691 completion_requests.next().await;
15692 cx.condition(|editor, _| editor.context_menu_visible())
15693 .await;
15694 cx.update_editor(|editor, _, _| {
15695 assert!(
15696 !editor.hover_state.visible(),
15697 "Hover popover should be hidden when completion menu is shown"
15698 );
15699 });
15700}
15701
15702#[gpui::test]
15703async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
15704 init_test(cx, |_| {});
15705
15706 let mut cx = EditorLspTestContext::new_rust(
15707 lsp::ServerCapabilities {
15708 completion_provider: Some(lsp::CompletionOptions {
15709 trigger_characters: Some(vec![".".to_string()]),
15710 resolve_provider: Some(true),
15711 ..Default::default()
15712 }),
15713 ..Default::default()
15714 },
15715 cx,
15716 )
15717 .await;
15718
15719 cx.set_state("fn main() { let a = 2ˇ; }");
15720 cx.simulate_keystroke(".");
15721
15722 let unresolved_item_1 = lsp::CompletionItem {
15723 label: "id".to_string(),
15724 filter_text: Some("id".to_string()),
15725 detail: None,
15726 documentation: None,
15727 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15728 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15729 new_text: ".id".to_string(),
15730 })),
15731 ..lsp::CompletionItem::default()
15732 };
15733 let resolved_item_1 = lsp::CompletionItem {
15734 additional_text_edits: Some(vec![lsp::TextEdit {
15735 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15736 new_text: "!!".to_string(),
15737 }]),
15738 ..unresolved_item_1.clone()
15739 };
15740 let unresolved_item_2 = lsp::CompletionItem {
15741 label: "other".to_string(),
15742 filter_text: Some("other".to_string()),
15743 detail: None,
15744 documentation: None,
15745 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15746 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15747 new_text: ".other".to_string(),
15748 })),
15749 ..lsp::CompletionItem::default()
15750 };
15751 let resolved_item_2 = lsp::CompletionItem {
15752 additional_text_edits: Some(vec![lsp::TextEdit {
15753 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15754 new_text: "??".to_string(),
15755 }]),
15756 ..unresolved_item_2.clone()
15757 };
15758
15759 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
15760 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
15761 cx.lsp
15762 .server
15763 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
15764 let unresolved_item_1 = unresolved_item_1.clone();
15765 let resolved_item_1 = resolved_item_1.clone();
15766 let unresolved_item_2 = unresolved_item_2.clone();
15767 let resolved_item_2 = resolved_item_2.clone();
15768 let resolve_requests_1 = resolve_requests_1.clone();
15769 let resolve_requests_2 = resolve_requests_2.clone();
15770 move |unresolved_request, _| {
15771 let unresolved_item_1 = unresolved_item_1.clone();
15772 let resolved_item_1 = resolved_item_1.clone();
15773 let unresolved_item_2 = unresolved_item_2.clone();
15774 let resolved_item_2 = resolved_item_2.clone();
15775 let resolve_requests_1 = resolve_requests_1.clone();
15776 let resolve_requests_2 = resolve_requests_2.clone();
15777 async move {
15778 if unresolved_request == unresolved_item_1 {
15779 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
15780 Ok(resolved_item_1.clone())
15781 } else if unresolved_request == unresolved_item_2 {
15782 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
15783 Ok(resolved_item_2.clone())
15784 } else {
15785 panic!("Unexpected completion item {unresolved_request:?}")
15786 }
15787 }
15788 }
15789 })
15790 .detach();
15791
15792 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15793 let unresolved_item_1 = unresolved_item_1.clone();
15794 let unresolved_item_2 = unresolved_item_2.clone();
15795 async move {
15796 Ok(Some(lsp::CompletionResponse::Array(vec![
15797 unresolved_item_1,
15798 unresolved_item_2,
15799 ])))
15800 }
15801 })
15802 .next()
15803 .await;
15804
15805 cx.condition(|editor, _| editor.context_menu_visible())
15806 .await;
15807 cx.update_editor(|editor, _, _| {
15808 let context_menu = editor.context_menu.borrow_mut();
15809 let context_menu = context_menu
15810 .as_ref()
15811 .expect("Should have the context menu deployed");
15812 match context_menu {
15813 CodeContextMenu::Completions(completions_menu) => {
15814 let completions = completions_menu.completions.borrow_mut();
15815 assert_eq!(
15816 completions
15817 .iter()
15818 .map(|completion| &completion.label.text)
15819 .collect::<Vec<_>>(),
15820 vec!["id", "other"]
15821 )
15822 }
15823 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15824 }
15825 });
15826 cx.run_until_parked();
15827
15828 cx.update_editor(|editor, window, cx| {
15829 editor.context_menu_next(&ContextMenuNext, window, cx);
15830 });
15831 cx.run_until_parked();
15832 cx.update_editor(|editor, window, cx| {
15833 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
15834 });
15835 cx.run_until_parked();
15836 cx.update_editor(|editor, window, cx| {
15837 editor.context_menu_next(&ContextMenuNext, window, cx);
15838 });
15839 cx.run_until_parked();
15840 cx.update_editor(|editor, window, cx| {
15841 editor
15842 .compose_completion(&ComposeCompletion::default(), window, cx)
15843 .expect("No task returned")
15844 })
15845 .await
15846 .expect("Completion failed");
15847 cx.run_until_parked();
15848
15849 cx.update_editor(|editor, _, cx| {
15850 assert_eq!(
15851 resolve_requests_1.load(atomic::Ordering::Acquire),
15852 1,
15853 "Should always resolve once despite multiple selections"
15854 );
15855 assert_eq!(
15856 resolve_requests_2.load(atomic::Ordering::Acquire),
15857 1,
15858 "Should always resolve once after multiple selections and applying the completion"
15859 );
15860 assert_eq!(
15861 editor.text(cx),
15862 "fn main() { let a = ??.other; }",
15863 "Should use resolved data when applying the completion"
15864 );
15865 });
15866}
15867
15868#[gpui::test]
15869async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
15870 init_test(cx, |_| {});
15871
15872 let item_0 = lsp::CompletionItem {
15873 label: "abs".into(),
15874 insert_text: Some("abs".into()),
15875 data: Some(json!({ "very": "special"})),
15876 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
15877 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
15878 lsp::InsertReplaceEdit {
15879 new_text: "abs".to_string(),
15880 insert: lsp::Range::default(),
15881 replace: lsp::Range::default(),
15882 },
15883 )),
15884 ..lsp::CompletionItem::default()
15885 };
15886 let items = iter::once(item_0.clone())
15887 .chain((11..51).map(|i| lsp::CompletionItem {
15888 label: format!("item_{}", i),
15889 insert_text: Some(format!("item_{}", i)),
15890 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15891 ..lsp::CompletionItem::default()
15892 }))
15893 .collect::<Vec<_>>();
15894
15895 let default_commit_characters = vec!["?".to_string()];
15896 let default_data = json!({ "default": "data"});
15897 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
15898 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
15899 let default_edit_range = lsp::Range {
15900 start: lsp::Position {
15901 line: 0,
15902 character: 5,
15903 },
15904 end: lsp::Position {
15905 line: 0,
15906 character: 5,
15907 },
15908 };
15909
15910 let mut cx = EditorLspTestContext::new_rust(
15911 lsp::ServerCapabilities {
15912 completion_provider: Some(lsp::CompletionOptions {
15913 trigger_characters: Some(vec![".".to_string()]),
15914 resolve_provider: Some(true),
15915 ..Default::default()
15916 }),
15917 ..Default::default()
15918 },
15919 cx,
15920 )
15921 .await;
15922
15923 cx.set_state("fn main() { let a = 2ˇ; }");
15924 cx.simulate_keystroke(".");
15925
15926 let completion_data = default_data.clone();
15927 let completion_characters = default_commit_characters.clone();
15928 let completion_items = items.clone();
15929 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15930 let default_data = completion_data.clone();
15931 let default_commit_characters = completion_characters.clone();
15932 let items = completion_items.clone();
15933 async move {
15934 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15935 items,
15936 item_defaults: Some(lsp::CompletionListItemDefaults {
15937 data: Some(default_data.clone()),
15938 commit_characters: Some(default_commit_characters.clone()),
15939 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
15940 default_edit_range,
15941 )),
15942 insert_text_format: Some(default_insert_text_format),
15943 insert_text_mode: Some(default_insert_text_mode),
15944 }),
15945 ..lsp::CompletionList::default()
15946 })))
15947 }
15948 })
15949 .next()
15950 .await;
15951
15952 let resolved_items = Arc::new(Mutex::new(Vec::new()));
15953 cx.lsp
15954 .server
15955 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
15956 let closure_resolved_items = resolved_items.clone();
15957 move |item_to_resolve, _| {
15958 let closure_resolved_items = closure_resolved_items.clone();
15959 async move {
15960 closure_resolved_items.lock().push(item_to_resolve.clone());
15961 Ok(item_to_resolve)
15962 }
15963 }
15964 })
15965 .detach();
15966
15967 cx.condition(|editor, _| editor.context_menu_visible())
15968 .await;
15969 cx.run_until_parked();
15970 cx.update_editor(|editor, _, _| {
15971 let menu = editor.context_menu.borrow_mut();
15972 match menu.as_ref().expect("should have the completions menu") {
15973 CodeContextMenu::Completions(completions_menu) => {
15974 assert_eq!(
15975 completions_menu
15976 .entries
15977 .borrow()
15978 .iter()
15979 .map(|mat| mat.string.clone())
15980 .collect::<Vec<String>>(),
15981 items
15982 .iter()
15983 .map(|completion| completion.label.clone())
15984 .collect::<Vec<String>>()
15985 );
15986 }
15987 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
15988 }
15989 });
15990 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
15991 // with 4 from the end.
15992 assert_eq!(
15993 *resolved_items.lock(),
15994 [&items[0..16], &items[items.len() - 4..items.len()]]
15995 .concat()
15996 .iter()
15997 .cloned()
15998 .map(|mut item| {
15999 if item.data.is_none() {
16000 item.data = Some(default_data.clone());
16001 }
16002 item
16003 })
16004 .collect::<Vec<lsp::CompletionItem>>(),
16005 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
16006 );
16007 resolved_items.lock().clear();
16008
16009 cx.update_editor(|editor, window, cx| {
16010 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
16011 });
16012 cx.run_until_parked();
16013 // Completions that have already been resolved are skipped.
16014 assert_eq!(
16015 *resolved_items.lock(),
16016 items[items.len() - 17..items.len() - 4]
16017 .iter()
16018 .cloned()
16019 .map(|mut item| {
16020 if item.data.is_none() {
16021 item.data = Some(default_data.clone());
16022 }
16023 item
16024 })
16025 .collect::<Vec<lsp::CompletionItem>>()
16026 );
16027 resolved_items.lock().clear();
16028}
16029
16030#[gpui::test]
16031async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
16032 init_test(cx, |_| {});
16033
16034 let mut cx = EditorLspTestContext::new(
16035 Language::new(
16036 LanguageConfig {
16037 matcher: LanguageMatcher {
16038 path_suffixes: vec!["jsx".into()],
16039 ..Default::default()
16040 },
16041 overrides: [(
16042 "element".into(),
16043 LanguageConfigOverride {
16044 completion_query_characters: Override::Set(['-'].into_iter().collect()),
16045 ..Default::default()
16046 },
16047 )]
16048 .into_iter()
16049 .collect(),
16050 ..Default::default()
16051 },
16052 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16053 )
16054 .with_override_query("(jsx_self_closing_element) @element")
16055 .unwrap(),
16056 lsp::ServerCapabilities {
16057 completion_provider: Some(lsp::CompletionOptions {
16058 trigger_characters: Some(vec![":".to_string()]),
16059 ..Default::default()
16060 }),
16061 ..Default::default()
16062 },
16063 cx,
16064 )
16065 .await;
16066
16067 cx.lsp
16068 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16069 Ok(Some(lsp::CompletionResponse::Array(vec![
16070 lsp::CompletionItem {
16071 label: "bg-blue".into(),
16072 ..Default::default()
16073 },
16074 lsp::CompletionItem {
16075 label: "bg-red".into(),
16076 ..Default::default()
16077 },
16078 lsp::CompletionItem {
16079 label: "bg-yellow".into(),
16080 ..Default::default()
16081 },
16082 ])))
16083 });
16084
16085 cx.set_state(r#"<p class="bgˇ" />"#);
16086
16087 // Trigger completion when typing a dash, because the dash is an extra
16088 // word character in the 'element' scope, which contains the cursor.
16089 cx.simulate_keystroke("-");
16090 cx.executor().run_until_parked();
16091 cx.update_editor(|editor, _, _| {
16092 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16093 {
16094 assert_eq!(
16095 completion_menu_entries(&menu),
16096 &["bg-blue", "bg-red", "bg-yellow"]
16097 );
16098 } else {
16099 panic!("expected completion menu to be open");
16100 }
16101 });
16102
16103 cx.simulate_keystroke("l");
16104 cx.executor().run_until_parked();
16105 cx.update_editor(|editor, _, _| {
16106 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16107 {
16108 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
16109 } else {
16110 panic!("expected completion menu to be open");
16111 }
16112 });
16113
16114 // When filtering completions, consider the character after the '-' to
16115 // be the start of a subword.
16116 cx.set_state(r#"<p class="yelˇ" />"#);
16117 cx.simulate_keystroke("l");
16118 cx.executor().run_until_parked();
16119 cx.update_editor(|editor, _, _| {
16120 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16121 {
16122 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
16123 } else {
16124 panic!("expected completion menu to be open");
16125 }
16126 });
16127}
16128
16129fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
16130 let entries = menu.entries.borrow();
16131 entries.iter().map(|mat| mat.string.clone()).collect()
16132}
16133
16134#[gpui::test]
16135async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
16136 init_test(cx, |settings| {
16137 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
16138 Formatter::Prettier,
16139 )))
16140 });
16141
16142 let fs = FakeFs::new(cx.executor());
16143 fs.insert_file(path!("/file.ts"), Default::default()).await;
16144
16145 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
16146 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16147
16148 language_registry.add(Arc::new(Language::new(
16149 LanguageConfig {
16150 name: "TypeScript".into(),
16151 matcher: LanguageMatcher {
16152 path_suffixes: vec!["ts".to_string()],
16153 ..Default::default()
16154 },
16155 ..Default::default()
16156 },
16157 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
16158 )));
16159 update_test_language_settings(cx, |settings| {
16160 settings.defaults.prettier = Some(PrettierSettings {
16161 allowed: true,
16162 ..PrettierSettings::default()
16163 });
16164 });
16165
16166 let test_plugin = "test_plugin";
16167 let _ = language_registry.register_fake_lsp(
16168 "TypeScript",
16169 FakeLspAdapter {
16170 prettier_plugins: vec![test_plugin],
16171 ..Default::default()
16172 },
16173 );
16174
16175 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
16176 let buffer = project
16177 .update(cx, |project, cx| {
16178 project.open_local_buffer(path!("/file.ts"), cx)
16179 })
16180 .await
16181 .unwrap();
16182
16183 let buffer_text = "one\ntwo\nthree\n";
16184 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16185 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16186 editor.update_in(cx, |editor, window, cx| {
16187 editor.set_text(buffer_text, window, cx)
16188 });
16189
16190 editor
16191 .update_in(cx, |editor, window, cx| {
16192 editor.perform_format(
16193 project.clone(),
16194 FormatTrigger::Manual,
16195 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16196 window,
16197 cx,
16198 )
16199 })
16200 .unwrap()
16201 .await;
16202 assert_eq!(
16203 editor.update(cx, |editor, cx| editor.text(cx)),
16204 buffer_text.to_string() + prettier_format_suffix,
16205 "Test prettier formatting was not applied to the original buffer text",
16206 );
16207
16208 update_test_language_settings(cx, |settings| {
16209 settings.defaults.formatter = Some(SelectedFormatter::Auto)
16210 });
16211 let format = editor.update_in(cx, |editor, window, cx| {
16212 editor.perform_format(
16213 project.clone(),
16214 FormatTrigger::Manual,
16215 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16216 window,
16217 cx,
16218 )
16219 });
16220 format.await.unwrap();
16221 assert_eq!(
16222 editor.update(cx, |editor, cx| editor.text(cx)),
16223 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
16224 "Autoformatting (via test prettier) was not applied to the original buffer text",
16225 );
16226}
16227
16228#[gpui::test]
16229async fn test_addition_reverts(cx: &mut TestAppContext) {
16230 init_test(cx, |_| {});
16231 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16232 let base_text = indoc! {r#"
16233 struct Row;
16234 struct Row1;
16235 struct Row2;
16236
16237 struct Row4;
16238 struct Row5;
16239 struct Row6;
16240
16241 struct Row8;
16242 struct Row9;
16243 struct Row10;"#};
16244
16245 // When addition hunks are not adjacent to carets, no hunk revert is performed
16246 assert_hunk_revert(
16247 indoc! {r#"struct Row;
16248 struct Row1;
16249 struct Row1.1;
16250 struct Row1.2;
16251 struct Row2;ˇ
16252
16253 struct Row4;
16254 struct Row5;
16255 struct Row6;
16256
16257 struct Row8;
16258 ˇstruct Row9;
16259 struct Row9.1;
16260 struct Row9.2;
16261 struct Row9.3;
16262 struct Row10;"#},
16263 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16264 indoc! {r#"struct Row;
16265 struct Row1;
16266 struct Row1.1;
16267 struct Row1.2;
16268 struct Row2;ˇ
16269
16270 struct Row4;
16271 struct Row5;
16272 struct Row6;
16273
16274 struct Row8;
16275 ˇstruct Row9;
16276 struct Row9.1;
16277 struct Row9.2;
16278 struct Row9.3;
16279 struct Row10;"#},
16280 base_text,
16281 &mut cx,
16282 );
16283 // Same for selections
16284 assert_hunk_revert(
16285 indoc! {r#"struct Row;
16286 struct Row1;
16287 struct Row2;
16288 struct Row2.1;
16289 struct Row2.2;
16290 «ˇ
16291 struct Row4;
16292 struct» Row5;
16293 «struct Row6;
16294 ˇ»
16295 struct Row9.1;
16296 struct Row9.2;
16297 struct Row9.3;
16298 struct Row8;
16299 struct Row9;
16300 struct Row10;"#},
16301 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16302 indoc! {r#"struct Row;
16303 struct Row1;
16304 struct Row2;
16305 struct Row2.1;
16306 struct Row2.2;
16307 «ˇ
16308 struct Row4;
16309 struct» Row5;
16310 «struct Row6;
16311 ˇ»
16312 struct Row9.1;
16313 struct Row9.2;
16314 struct Row9.3;
16315 struct Row8;
16316 struct Row9;
16317 struct Row10;"#},
16318 base_text,
16319 &mut cx,
16320 );
16321
16322 // When carets and selections intersect the addition hunks, those are reverted.
16323 // Adjacent carets got merged.
16324 assert_hunk_revert(
16325 indoc! {r#"struct Row;
16326 ˇ// something on the top
16327 struct Row1;
16328 struct Row2;
16329 struct Roˇw3.1;
16330 struct Row2.2;
16331 struct Row2.3;ˇ
16332
16333 struct Row4;
16334 struct ˇRow5.1;
16335 struct Row5.2;
16336 struct «Rowˇ»5.3;
16337 struct Row5;
16338 struct Row6;
16339 ˇ
16340 struct Row9.1;
16341 struct «Rowˇ»9.2;
16342 struct «ˇRow»9.3;
16343 struct Row8;
16344 struct Row9;
16345 «ˇ// something on bottom»
16346 struct Row10;"#},
16347 vec![
16348 DiffHunkStatusKind::Added,
16349 DiffHunkStatusKind::Added,
16350 DiffHunkStatusKind::Added,
16351 DiffHunkStatusKind::Added,
16352 DiffHunkStatusKind::Added,
16353 ],
16354 indoc! {r#"struct Row;
16355 ˇstruct Row1;
16356 struct Row2;
16357 ˇ
16358 struct Row4;
16359 ˇstruct Row5;
16360 struct Row6;
16361 ˇ
16362 ˇstruct Row8;
16363 struct Row9;
16364 ˇstruct Row10;"#},
16365 base_text,
16366 &mut cx,
16367 );
16368}
16369
16370#[gpui::test]
16371async fn test_modification_reverts(cx: &mut TestAppContext) {
16372 init_test(cx, |_| {});
16373 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16374 let base_text = indoc! {r#"
16375 struct Row;
16376 struct Row1;
16377 struct Row2;
16378
16379 struct Row4;
16380 struct Row5;
16381 struct Row6;
16382
16383 struct Row8;
16384 struct Row9;
16385 struct Row10;"#};
16386
16387 // Modification hunks behave the same as the addition ones.
16388 assert_hunk_revert(
16389 indoc! {r#"struct Row;
16390 struct Row1;
16391 struct Row33;
16392 ˇ
16393 struct Row4;
16394 struct Row5;
16395 struct Row6;
16396 ˇ
16397 struct Row99;
16398 struct Row9;
16399 struct Row10;"#},
16400 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16401 indoc! {r#"struct Row;
16402 struct Row1;
16403 struct Row33;
16404 ˇ
16405 struct Row4;
16406 struct Row5;
16407 struct Row6;
16408 ˇ
16409 struct Row99;
16410 struct Row9;
16411 struct Row10;"#},
16412 base_text,
16413 &mut cx,
16414 );
16415 assert_hunk_revert(
16416 indoc! {r#"struct Row;
16417 struct Row1;
16418 struct Row33;
16419 «ˇ
16420 struct Row4;
16421 struct» Row5;
16422 «struct Row6;
16423 ˇ»
16424 struct Row99;
16425 struct Row9;
16426 struct Row10;"#},
16427 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16428 indoc! {r#"struct Row;
16429 struct Row1;
16430 struct Row33;
16431 «ˇ
16432 struct Row4;
16433 struct» Row5;
16434 «struct Row6;
16435 ˇ»
16436 struct Row99;
16437 struct Row9;
16438 struct Row10;"#},
16439 base_text,
16440 &mut cx,
16441 );
16442
16443 assert_hunk_revert(
16444 indoc! {r#"ˇstruct Row1.1;
16445 struct Row1;
16446 «ˇstr»uct Row22;
16447
16448 struct ˇRow44;
16449 struct Row5;
16450 struct «Rˇ»ow66;ˇ
16451
16452 «struˇ»ct Row88;
16453 struct Row9;
16454 struct Row1011;ˇ"#},
16455 vec![
16456 DiffHunkStatusKind::Modified,
16457 DiffHunkStatusKind::Modified,
16458 DiffHunkStatusKind::Modified,
16459 DiffHunkStatusKind::Modified,
16460 DiffHunkStatusKind::Modified,
16461 DiffHunkStatusKind::Modified,
16462 ],
16463 indoc! {r#"struct Row;
16464 ˇstruct Row1;
16465 struct Row2;
16466 ˇ
16467 struct Row4;
16468 ˇstruct Row5;
16469 struct Row6;
16470 ˇ
16471 struct Row8;
16472 ˇstruct Row9;
16473 struct Row10;ˇ"#},
16474 base_text,
16475 &mut cx,
16476 );
16477}
16478
16479#[gpui::test]
16480async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
16481 init_test(cx, |_| {});
16482 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16483 let base_text = indoc! {r#"
16484 one
16485
16486 two
16487 three
16488 "#};
16489
16490 cx.set_head_text(base_text);
16491 cx.set_state("\nˇ\n");
16492 cx.executor().run_until_parked();
16493 cx.update_editor(|editor, _window, cx| {
16494 editor.expand_selected_diff_hunks(cx);
16495 });
16496 cx.executor().run_until_parked();
16497 cx.update_editor(|editor, window, cx| {
16498 editor.backspace(&Default::default(), window, cx);
16499 });
16500 cx.run_until_parked();
16501 cx.assert_state_with_diff(
16502 indoc! {r#"
16503
16504 - two
16505 - threeˇ
16506 +
16507 "#}
16508 .to_string(),
16509 );
16510}
16511
16512#[gpui::test]
16513async fn test_deletion_reverts(cx: &mut TestAppContext) {
16514 init_test(cx, |_| {});
16515 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16516 let base_text = indoc! {r#"struct Row;
16517struct Row1;
16518struct Row2;
16519
16520struct Row4;
16521struct Row5;
16522struct Row6;
16523
16524struct Row8;
16525struct Row9;
16526struct Row10;"#};
16527
16528 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
16529 assert_hunk_revert(
16530 indoc! {r#"struct Row;
16531 struct Row2;
16532
16533 ˇstruct Row4;
16534 struct Row5;
16535 struct Row6;
16536 ˇ
16537 struct Row8;
16538 struct Row10;"#},
16539 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16540 indoc! {r#"struct Row;
16541 struct Row2;
16542
16543 ˇstruct Row4;
16544 struct Row5;
16545 struct Row6;
16546 ˇ
16547 struct Row8;
16548 struct Row10;"#},
16549 base_text,
16550 &mut cx,
16551 );
16552 assert_hunk_revert(
16553 indoc! {r#"struct Row;
16554 struct Row2;
16555
16556 «ˇstruct Row4;
16557 struct» Row5;
16558 «struct Row6;
16559 ˇ»
16560 struct Row8;
16561 struct Row10;"#},
16562 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16563 indoc! {r#"struct Row;
16564 struct Row2;
16565
16566 «ˇstruct Row4;
16567 struct» Row5;
16568 «struct Row6;
16569 ˇ»
16570 struct Row8;
16571 struct Row10;"#},
16572 base_text,
16573 &mut cx,
16574 );
16575
16576 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
16577 assert_hunk_revert(
16578 indoc! {r#"struct Row;
16579 ˇstruct Row2;
16580
16581 struct Row4;
16582 struct Row5;
16583 struct Row6;
16584
16585 struct Row8;ˇ
16586 struct Row10;"#},
16587 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16588 indoc! {r#"struct Row;
16589 struct Row1;
16590 ˇstruct Row2;
16591
16592 struct Row4;
16593 struct Row5;
16594 struct Row6;
16595
16596 struct Row8;ˇ
16597 struct Row9;
16598 struct Row10;"#},
16599 base_text,
16600 &mut cx,
16601 );
16602 assert_hunk_revert(
16603 indoc! {r#"struct Row;
16604 struct Row2«ˇ;
16605 struct Row4;
16606 struct» Row5;
16607 «struct Row6;
16608
16609 struct Row8;ˇ»
16610 struct Row10;"#},
16611 vec![
16612 DiffHunkStatusKind::Deleted,
16613 DiffHunkStatusKind::Deleted,
16614 DiffHunkStatusKind::Deleted,
16615 ],
16616 indoc! {r#"struct Row;
16617 struct Row1;
16618 struct Row2«ˇ;
16619
16620 struct Row4;
16621 struct» Row5;
16622 «struct Row6;
16623
16624 struct Row8;ˇ»
16625 struct Row9;
16626 struct Row10;"#},
16627 base_text,
16628 &mut cx,
16629 );
16630}
16631
16632#[gpui::test]
16633async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
16634 init_test(cx, |_| {});
16635
16636 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
16637 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
16638 let base_text_3 =
16639 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
16640
16641 let text_1 = edit_first_char_of_every_line(base_text_1);
16642 let text_2 = edit_first_char_of_every_line(base_text_2);
16643 let text_3 = edit_first_char_of_every_line(base_text_3);
16644
16645 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
16646 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
16647 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
16648
16649 let multibuffer = cx.new(|cx| {
16650 let mut multibuffer = MultiBuffer::new(ReadWrite);
16651 multibuffer.push_excerpts(
16652 buffer_1.clone(),
16653 [
16654 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16655 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16656 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16657 ],
16658 cx,
16659 );
16660 multibuffer.push_excerpts(
16661 buffer_2.clone(),
16662 [
16663 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16664 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16665 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16666 ],
16667 cx,
16668 );
16669 multibuffer.push_excerpts(
16670 buffer_3.clone(),
16671 [
16672 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16673 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16674 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16675 ],
16676 cx,
16677 );
16678 multibuffer
16679 });
16680
16681 let fs = FakeFs::new(cx.executor());
16682 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
16683 let (editor, cx) = cx
16684 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
16685 editor.update_in(cx, |editor, _window, cx| {
16686 for (buffer, diff_base) in [
16687 (buffer_1.clone(), base_text_1),
16688 (buffer_2.clone(), base_text_2),
16689 (buffer_3.clone(), base_text_3),
16690 ] {
16691 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
16692 editor
16693 .buffer
16694 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
16695 }
16696 });
16697 cx.executor().run_until_parked();
16698
16699 editor.update_in(cx, |editor, window, cx| {
16700 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}");
16701 editor.select_all(&SelectAll, window, cx);
16702 editor.git_restore(&Default::default(), window, cx);
16703 });
16704 cx.executor().run_until_parked();
16705
16706 // When all ranges are selected, all buffer hunks are reverted.
16707 editor.update(cx, |editor, cx| {
16708 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");
16709 });
16710 buffer_1.update(cx, |buffer, _| {
16711 assert_eq!(buffer.text(), base_text_1);
16712 });
16713 buffer_2.update(cx, |buffer, _| {
16714 assert_eq!(buffer.text(), base_text_2);
16715 });
16716 buffer_3.update(cx, |buffer, _| {
16717 assert_eq!(buffer.text(), base_text_3);
16718 });
16719
16720 editor.update_in(cx, |editor, window, cx| {
16721 editor.undo(&Default::default(), window, cx);
16722 });
16723
16724 editor.update_in(cx, |editor, window, cx| {
16725 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16726 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
16727 });
16728 editor.git_restore(&Default::default(), window, cx);
16729 });
16730
16731 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
16732 // but not affect buffer_2 and its related excerpts.
16733 editor.update(cx, |editor, cx| {
16734 assert_eq!(
16735 editor.text(cx),
16736 "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}"
16737 );
16738 });
16739 buffer_1.update(cx, |buffer, _| {
16740 assert_eq!(buffer.text(), base_text_1);
16741 });
16742 buffer_2.update(cx, |buffer, _| {
16743 assert_eq!(
16744 buffer.text(),
16745 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
16746 );
16747 });
16748 buffer_3.update(cx, |buffer, _| {
16749 assert_eq!(
16750 buffer.text(),
16751 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
16752 );
16753 });
16754
16755 fn edit_first_char_of_every_line(text: &str) -> String {
16756 text.split('\n')
16757 .map(|line| format!("X{}", &line[1..]))
16758 .collect::<Vec<_>>()
16759 .join("\n")
16760 }
16761}
16762
16763#[gpui::test]
16764async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
16765 init_test(cx, |_| {});
16766
16767 let cols = 4;
16768 let rows = 10;
16769 let sample_text_1 = sample_text(rows, cols, 'a');
16770 assert_eq!(
16771 sample_text_1,
16772 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
16773 );
16774 let sample_text_2 = sample_text(rows, cols, 'l');
16775 assert_eq!(
16776 sample_text_2,
16777 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
16778 );
16779 let sample_text_3 = sample_text(rows, cols, 'v');
16780 assert_eq!(
16781 sample_text_3,
16782 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
16783 );
16784
16785 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
16786 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
16787 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
16788
16789 let multi_buffer = cx.new(|cx| {
16790 let mut multibuffer = MultiBuffer::new(ReadWrite);
16791 multibuffer.push_excerpts(
16792 buffer_1.clone(),
16793 [
16794 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16795 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16796 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16797 ],
16798 cx,
16799 );
16800 multibuffer.push_excerpts(
16801 buffer_2.clone(),
16802 [
16803 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16804 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16805 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16806 ],
16807 cx,
16808 );
16809 multibuffer.push_excerpts(
16810 buffer_3.clone(),
16811 [
16812 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16813 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16814 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16815 ],
16816 cx,
16817 );
16818 multibuffer
16819 });
16820
16821 let fs = FakeFs::new(cx.executor());
16822 fs.insert_tree(
16823 "/a",
16824 json!({
16825 "main.rs": sample_text_1,
16826 "other.rs": sample_text_2,
16827 "lib.rs": sample_text_3,
16828 }),
16829 )
16830 .await;
16831 let project = Project::test(fs, ["/a".as_ref()], cx).await;
16832 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16833 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16834 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16835 Editor::new(
16836 EditorMode::full(),
16837 multi_buffer,
16838 Some(project.clone()),
16839 window,
16840 cx,
16841 )
16842 });
16843 let multibuffer_item_id = workspace
16844 .update(cx, |workspace, window, cx| {
16845 assert!(
16846 workspace.active_item(cx).is_none(),
16847 "active item should be None before the first item is added"
16848 );
16849 workspace.add_item_to_active_pane(
16850 Box::new(multi_buffer_editor.clone()),
16851 None,
16852 true,
16853 window,
16854 cx,
16855 );
16856 let active_item = workspace
16857 .active_item(cx)
16858 .expect("should have an active item after adding the multi buffer");
16859 assert!(
16860 !active_item.is_singleton(cx),
16861 "A multi buffer was expected to active after adding"
16862 );
16863 active_item.item_id()
16864 })
16865 .unwrap();
16866 cx.executor().run_until_parked();
16867
16868 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16869 editor.change_selections(
16870 SelectionEffects::scroll(Autoscroll::Next),
16871 window,
16872 cx,
16873 |s| s.select_ranges(Some(1..2)),
16874 );
16875 editor.open_excerpts(&OpenExcerpts, window, cx);
16876 });
16877 cx.executor().run_until_parked();
16878 let first_item_id = workspace
16879 .update(cx, |workspace, window, cx| {
16880 let active_item = workspace
16881 .active_item(cx)
16882 .expect("should have an active item after navigating into the 1st buffer");
16883 let first_item_id = active_item.item_id();
16884 assert_ne!(
16885 first_item_id, multibuffer_item_id,
16886 "Should navigate into the 1st buffer and activate it"
16887 );
16888 assert!(
16889 active_item.is_singleton(cx),
16890 "New active item should be a singleton buffer"
16891 );
16892 assert_eq!(
16893 active_item
16894 .act_as::<Editor>(cx)
16895 .expect("should have navigated into an editor for the 1st buffer")
16896 .read(cx)
16897 .text(cx),
16898 sample_text_1
16899 );
16900
16901 workspace
16902 .go_back(workspace.active_pane().downgrade(), window, cx)
16903 .detach_and_log_err(cx);
16904
16905 first_item_id
16906 })
16907 .unwrap();
16908 cx.executor().run_until_parked();
16909 workspace
16910 .update(cx, |workspace, _, cx| {
16911 let active_item = workspace
16912 .active_item(cx)
16913 .expect("should have an active item after navigating back");
16914 assert_eq!(
16915 active_item.item_id(),
16916 multibuffer_item_id,
16917 "Should navigate back to the multi buffer"
16918 );
16919 assert!(!active_item.is_singleton(cx));
16920 })
16921 .unwrap();
16922
16923 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16924 editor.change_selections(
16925 SelectionEffects::scroll(Autoscroll::Next),
16926 window,
16927 cx,
16928 |s| s.select_ranges(Some(39..40)),
16929 );
16930 editor.open_excerpts(&OpenExcerpts, window, cx);
16931 });
16932 cx.executor().run_until_parked();
16933 let second_item_id = workspace
16934 .update(cx, |workspace, window, cx| {
16935 let active_item = workspace
16936 .active_item(cx)
16937 .expect("should have an active item after navigating into the 2nd buffer");
16938 let second_item_id = active_item.item_id();
16939 assert_ne!(
16940 second_item_id, multibuffer_item_id,
16941 "Should navigate away from the multibuffer"
16942 );
16943 assert_ne!(
16944 second_item_id, first_item_id,
16945 "Should navigate into the 2nd buffer and activate it"
16946 );
16947 assert!(
16948 active_item.is_singleton(cx),
16949 "New active item should be a singleton buffer"
16950 );
16951 assert_eq!(
16952 active_item
16953 .act_as::<Editor>(cx)
16954 .expect("should have navigated into an editor")
16955 .read(cx)
16956 .text(cx),
16957 sample_text_2
16958 );
16959
16960 workspace
16961 .go_back(workspace.active_pane().downgrade(), window, cx)
16962 .detach_and_log_err(cx);
16963
16964 second_item_id
16965 })
16966 .unwrap();
16967 cx.executor().run_until_parked();
16968 workspace
16969 .update(cx, |workspace, _, cx| {
16970 let active_item = workspace
16971 .active_item(cx)
16972 .expect("should have an active item after navigating back from the 2nd buffer");
16973 assert_eq!(
16974 active_item.item_id(),
16975 multibuffer_item_id,
16976 "Should navigate back from the 2nd buffer to the multi buffer"
16977 );
16978 assert!(!active_item.is_singleton(cx));
16979 })
16980 .unwrap();
16981
16982 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16983 editor.change_selections(
16984 SelectionEffects::scroll(Autoscroll::Next),
16985 window,
16986 cx,
16987 |s| s.select_ranges(Some(70..70)),
16988 );
16989 editor.open_excerpts(&OpenExcerpts, window, cx);
16990 });
16991 cx.executor().run_until_parked();
16992 workspace
16993 .update(cx, |workspace, window, cx| {
16994 let active_item = workspace
16995 .active_item(cx)
16996 .expect("should have an active item after navigating into the 3rd buffer");
16997 let third_item_id = active_item.item_id();
16998 assert_ne!(
16999 third_item_id, multibuffer_item_id,
17000 "Should navigate into the 3rd buffer and activate it"
17001 );
17002 assert_ne!(third_item_id, first_item_id);
17003 assert_ne!(third_item_id, second_item_id);
17004 assert!(
17005 active_item.is_singleton(cx),
17006 "New active item should be a singleton buffer"
17007 );
17008 assert_eq!(
17009 active_item
17010 .act_as::<Editor>(cx)
17011 .expect("should have navigated into an editor")
17012 .read(cx)
17013 .text(cx),
17014 sample_text_3
17015 );
17016
17017 workspace
17018 .go_back(workspace.active_pane().downgrade(), window, cx)
17019 .detach_and_log_err(cx);
17020 })
17021 .unwrap();
17022 cx.executor().run_until_parked();
17023 workspace
17024 .update(cx, |workspace, _, cx| {
17025 let active_item = workspace
17026 .active_item(cx)
17027 .expect("should have an active item after navigating back from the 3rd buffer");
17028 assert_eq!(
17029 active_item.item_id(),
17030 multibuffer_item_id,
17031 "Should navigate back from the 3rd buffer to the multi buffer"
17032 );
17033 assert!(!active_item.is_singleton(cx));
17034 })
17035 .unwrap();
17036}
17037
17038#[gpui::test]
17039async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17040 init_test(cx, |_| {});
17041
17042 let mut cx = EditorTestContext::new(cx).await;
17043
17044 let diff_base = r#"
17045 use some::mod;
17046
17047 const A: u32 = 42;
17048
17049 fn main() {
17050 println!("hello");
17051
17052 println!("world");
17053 }
17054 "#
17055 .unindent();
17056
17057 cx.set_state(
17058 &r#"
17059 use some::modified;
17060
17061 ˇ
17062 fn main() {
17063 println!("hello there");
17064
17065 println!("around the");
17066 println!("world");
17067 }
17068 "#
17069 .unindent(),
17070 );
17071
17072 cx.set_head_text(&diff_base);
17073 executor.run_until_parked();
17074
17075 cx.update_editor(|editor, window, cx| {
17076 editor.go_to_next_hunk(&GoToHunk, window, cx);
17077 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17078 });
17079 executor.run_until_parked();
17080 cx.assert_state_with_diff(
17081 r#"
17082 use some::modified;
17083
17084
17085 fn main() {
17086 - println!("hello");
17087 + ˇ println!("hello there");
17088
17089 println!("around the");
17090 println!("world");
17091 }
17092 "#
17093 .unindent(),
17094 );
17095
17096 cx.update_editor(|editor, window, cx| {
17097 for _ in 0..2 {
17098 editor.go_to_next_hunk(&GoToHunk, window, cx);
17099 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17100 }
17101 });
17102 executor.run_until_parked();
17103 cx.assert_state_with_diff(
17104 r#"
17105 - use some::mod;
17106 + ˇuse some::modified;
17107
17108
17109 fn main() {
17110 - println!("hello");
17111 + println!("hello there");
17112
17113 + println!("around the");
17114 println!("world");
17115 }
17116 "#
17117 .unindent(),
17118 );
17119
17120 cx.update_editor(|editor, window, cx| {
17121 editor.go_to_next_hunk(&GoToHunk, window, cx);
17122 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17123 });
17124 executor.run_until_parked();
17125 cx.assert_state_with_diff(
17126 r#"
17127 - use some::mod;
17128 + use some::modified;
17129
17130 - const A: u32 = 42;
17131 ˇ
17132 fn main() {
17133 - println!("hello");
17134 + println!("hello there");
17135
17136 + println!("around the");
17137 println!("world");
17138 }
17139 "#
17140 .unindent(),
17141 );
17142
17143 cx.update_editor(|editor, window, cx| {
17144 editor.cancel(&Cancel, window, cx);
17145 });
17146
17147 cx.assert_state_with_diff(
17148 r#"
17149 use some::modified;
17150
17151 ˇ
17152 fn main() {
17153 println!("hello there");
17154
17155 println!("around the");
17156 println!("world");
17157 }
17158 "#
17159 .unindent(),
17160 );
17161}
17162
17163#[gpui::test]
17164async fn test_diff_base_change_with_expanded_diff_hunks(
17165 executor: BackgroundExecutor,
17166 cx: &mut TestAppContext,
17167) {
17168 init_test(cx, |_| {});
17169
17170 let mut cx = EditorTestContext::new(cx).await;
17171
17172 let diff_base = r#"
17173 use some::mod1;
17174 use some::mod2;
17175
17176 const A: u32 = 42;
17177 const B: u32 = 42;
17178 const C: u32 = 42;
17179
17180 fn main() {
17181 println!("hello");
17182
17183 println!("world");
17184 }
17185 "#
17186 .unindent();
17187
17188 cx.set_state(
17189 &r#"
17190 use some::mod2;
17191
17192 const A: u32 = 42;
17193 const C: u32 = 42;
17194
17195 fn main(ˇ) {
17196 //println!("hello");
17197
17198 println!("world");
17199 //
17200 //
17201 }
17202 "#
17203 .unindent(),
17204 );
17205
17206 cx.set_head_text(&diff_base);
17207 executor.run_until_parked();
17208
17209 cx.update_editor(|editor, window, cx| {
17210 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17211 });
17212 executor.run_until_parked();
17213 cx.assert_state_with_diff(
17214 r#"
17215 - use some::mod1;
17216 use some::mod2;
17217
17218 const A: u32 = 42;
17219 - const B: u32 = 42;
17220 const C: u32 = 42;
17221
17222 fn main(ˇ) {
17223 - println!("hello");
17224 + //println!("hello");
17225
17226 println!("world");
17227 + //
17228 + //
17229 }
17230 "#
17231 .unindent(),
17232 );
17233
17234 cx.set_head_text("new diff base!");
17235 executor.run_until_parked();
17236 cx.assert_state_with_diff(
17237 r#"
17238 - new diff base!
17239 + use some::mod2;
17240 +
17241 + const A: u32 = 42;
17242 + const C: u32 = 42;
17243 +
17244 + fn main(ˇ) {
17245 + //println!("hello");
17246 +
17247 + println!("world");
17248 + //
17249 + //
17250 + }
17251 "#
17252 .unindent(),
17253 );
17254}
17255
17256#[gpui::test]
17257async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
17258 init_test(cx, |_| {});
17259
17260 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17261 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17262 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17263 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17264 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
17265 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
17266
17267 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
17268 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
17269 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
17270
17271 let multi_buffer = cx.new(|cx| {
17272 let mut multibuffer = MultiBuffer::new(ReadWrite);
17273 multibuffer.push_excerpts(
17274 buffer_1.clone(),
17275 [
17276 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17277 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17278 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17279 ],
17280 cx,
17281 );
17282 multibuffer.push_excerpts(
17283 buffer_2.clone(),
17284 [
17285 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17286 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17287 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17288 ],
17289 cx,
17290 );
17291 multibuffer.push_excerpts(
17292 buffer_3.clone(),
17293 [
17294 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17295 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17296 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17297 ],
17298 cx,
17299 );
17300 multibuffer
17301 });
17302
17303 let editor =
17304 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17305 editor
17306 .update(cx, |editor, _window, cx| {
17307 for (buffer, diff_base) in [
17308 (buffer_1.clone(), file_1_old),
17309 (buffer_2.clone(), file_2_old),
17310 (buffer_3.clone(), file_3_old),
17311 ] {
17312 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
17313 editor
17314 .buffer
17315 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
17316 }
17317 })
17318 .unwrap();
17319
17320 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17321 cx.run_until_parked();
17322
17323 cx.assert_editor_state(
17324 &"
17325 ˇaaa
17326 ccc
17327 ddd
17328
17329 ggg
17330 hhh
17331
17332
17333 lll
17334 mmm
17335 NNN
17336
17337 qqq
17338 rrr
17339
17340 uuu
17341 111
17342 222
17343 333
17344
17345 666
17346 777
17347
17348 000
17349 !!!"
17350 .unindent(),
17351 );
17352
17353 cx.update_editor(|editor, window, cx| {
17354 editor.select_all(&SelectAll, window, cx);
17355 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17356 });
17357 cx.executor().run_until_parked();
17358
17359 cx.assert_state_with_diff(
17360 "
17361 «aaa
17362 - bbb
17363 ccc
17364 ddd
17365
17366 ggg
17367 hhh
17368
17369
17370 lll
17371 mmm
17372 - nnn
17373 + NNN
17374
17375 qqq
17376 rrr
17377
17378 uuu
17379 111
17380 222
17381 333
17382
17383 + 666
17384 777
17385
17386 000
17387 !!!ˇ»"
17388 .unindent(),
17389 );
17390}
17391
17392#[gpui::test]
17393async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
17394 init_test(cx, |_| {});
17395
17396 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
17397 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
17398
17399 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
17400 let multi_buffer = cx.new(|cx| {
17401 let mut multibuffer = MultiBuffer::new(ReadWrite);
17402 multibuffer.push_excerpts(
17403 buffer.clone(),
17404 [
17405 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
17406 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
17407 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
17408 ],
17409 cx,
17410 );
17411 multibuffer
17412 });
17413
17414 let editor =
17415 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17416 editor
17417 .update(cx, |editor, _window, cx| {
17418 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
17419 editor
17420 .buffer
17421 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
17422 })
17423 .unwrap();
17424
17425 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17426 cx.run_until_parked();
17427
17428 cx.update_editor(|editor, window, cx| {
17429 editor.expand_all_diff_hunks(&Default::default(), window, cx)
17430 });
17431 cx.executor().run_until_parked();
17432
17433 // When the start of a hunk coincides with the start of its excerpt,
17434 // the hunk is expanded. When the start of a a hunk is earlier than
17435 // the start of its excerpt, the hunk is not expanded.
17436 cx.assert_state_with_diff(
17437 "
17438 ˇaaa
17439 - bbb
17440 + BBB
17441
17442 - ddd
17443 - eee
17444 + DDD
17445 + EEE
17446 fff
17447
17448 iii
17449 "
17450 .unindent(),
17451 );
17452}
17453
17454#[gpui::test]
17455async fn test_edits_around_expanded_insertion_hunks(
17456 executor: BackgroundExecutor,
17457 cx: &mut TestAppContext,
17458) {
17459 init_test(cx, |_| {});
17460
17461 let mut cx = EditorTestContext::new(cx).await;
17462
17463 let diff_base = r#"
17464 use some::mod1;
17465 use some::mod2;
17466
17467 const A: u32 = 42;
17468
17469 fn main() {
17470 println!("hello");
17471
17472 println!("world");
17473 }
17474 "#
17475 .unindent();
17476 executor.run_until_parked();
17477 cx.set_state(
17478 &r#"
17479 use some::mod1;
17480 use some::mod2;
17481
17482 const A: u32 = 42;
17483 const B: u32 = 42;
17484 const C: u32 = 42;
17485 ˇ
17486
17487 fn main() {
17488 println!("hello");
17489
17490 println!("world");
17491 }
17492 "#
17493 .unindent(),
17494 );
17495
17496 cx.set_head_text(&diff_base);
17497 executor.run_until_parked();
17498
17499 cx.update_editor(|editor, window, cx| {
17500 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17501 });
17502 executor.run_until_parked();
17503
17504 cx.assert_state_with_diff(
17505 r#"
17506 use some::mod1;
17507 use some::mod2;
17508
17509 const A: u32 = 42;
17510 + const B: u32 = 42;
17511 + const C: u32 = 42;
17512 + ˇ
17513
17514 fn main() {
17515 println!("hello");
17516
17517 println!("world");
17518 }
17519 "#
17520 .unindent(),
17521 );
17522
17523 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
17524 executor.run_until_parked();
17525
17526 cx.assert_state_with_diff(
17527 r#"
17528 use some::mod1;
17529 use some::mod2;
17530
17531 const A: u32 = 42;
17532 + const B: u32 = 42;
17533 + const C: u32 = 42;
17534 + const D: u32 = 42;
17535 + ˇ
17536
17537 fn main() {
17538 println!("hello");
17539
17540 println!("world");
17541 }
17542 "#
17543 .unindent(),
17544 );
17545
17546 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
17547 executor.run_until_parked();
17548
17549 cx.assert_state_with_diff(
17550 r#"
17551 use some::mod1;
17552 use some::mod2;
17553
17554 const A: u32 = 42;
17555 + const B: u32 = 42;
17556 + const C: u32 = 42;
17557 + const D: u32 = 42;
17558 + const E: u32 = 42;
17559 + ˇ
17560
17561 fn main() {
17562 println!("hello");
17563
17564 println!("world");
17565 }
17566 "#
17567 .unindent(),
17568 );
17569
17570 cx.update_editor(|editor, window, cx| {
17571 editor.delete_line(&DeleteLine, window, cx);
17572 });
17573 executor.run_until_parked();
17574
17575 cx.assert_state_with_diff(
17576 r#"
17577 use some::mod1;
17578 use some::mod2;
17579
17580 const A: u32 = 42;
17581 + const B: u32 = 42;
17582 + const C: u32 = 42;
17583 + const D: u32 = 42;
17584 + const E: u32 = 42;
17585 ˇ
17586 fn main() {
17587 println!("hello");
17588
17589 println!("world");
17590 }
17591 "#
17592 .unindent(),
17593 );
17594
17595 cx.update_editor(|editor, window, cx| {
17596 editor.move_up(&MoveUp, window, cx);
17597 editor.delete_line(&DeleteLine, window, cx);
17598 editor.move_up(&MoveUp, window, cx);
17599 editor.delete_line(&DeleteLine, window, cx);
17600 editor.move_up(&MoveUp, window, cx);
17601 editor.delete_line(&DeleteLine, window, cx);
17602 });
17603 executor.run_until_parked();
17604 cx.assert_state_with_diff(
17605 r#"
17606 use some::mod1;
17607 use some::mod2;
17608
17609 const A: u32 = 42;
17610 + const B: u32 = 42;
17611 ˇ
17612 fn main() {
17613 println!("hello");
17614
17615 println!("world");
17616 }
17617 "#
17618 .unindent(),
17619 );
17620
17621 cx.update_editor(|editor, window, cx| {
17622 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
17623 editor.delete_line(&DeleteLine, window, cx);
17624 });
17625 executor.run_until_parked();
17626 cx.assert_state_with_diff(
17627 r#"
17628 ˇ
17629 fn main() {
17630 println!("hello");
17631
17632 println!("world");
17633 }
17634 "#
17635 .unindent(),
17636 );
17637}
17638
17639#[gpui::test]
17640async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
17641 init_test(cx, |_| {});
17642
17643 let mut cx = EditorTestContext::new(cx).await;
17644 cx.set_head_text(indoc! { "
17645 one
17646 two
17647 three
17648 four
17649 five
17650 "
17651 });
17652 cx.set_state(indoc! { "
17653 one
17654 ˇthree
17655 five
17656 "});
17657 cx.run_until_parked();
17658 cx.update_editor(|editor, window, cx| {
17659 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17660 });
17661 cx.assert_state_with_diff(
17662 indoc! { "
17663 one
17664 - two
17665 ˇthree
17666 - four
17667 five
17668 "}
17669 .to_string(),
17670 );
17671 cx.update_editor(|editor, window, cx| {
17672 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17673 });
17674
17675 cx.assert_state_with_diff(
17676 indoc! { "
17677 one
17678 ˇthree
17679 five
17680 "}
17681 .to_string(),
17682 );
17683
17684 cx.set_state(indoc! { "
17685 one
17686 ˇTWO
17687 three
17688 four
17689 five
17690 "});
17691 cx.run_until_parked();
17692 cx.update_editor(|editor, window, cx| {
17693 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17694 });
17695
17696 cx.assert_state_with_diff(
17697 indoc! { "
17698 one
17699 - two
17700 + ˇTWO
17701 three
17702 four
17703 five
17704 "}
17705 .to_string(),
17706 );
17707 cx.update_editor(|editor, window, cx| {
17708 editor.move_up(&Default::default(), window, cx);
17709 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17710 });
17711 cx.assert_state_with_diff(
17712 indoc! { "
17713 one
17714 ˇTWO
17715 three
17716 four
17717 five
17718 "}
17719 .to_string(),
17720 );
17721}
17722
17723#[gpui::test]
17724async fn test_edits_around_expanded_deletion_hunks(
17725 executor: BackgroundExecutor,
17726 cx: &mut TestAppContext,
17727) {
17728 init_test(cx, |_| {});
17729
17730 let mut cx = EditorTestContext::new(cx).await;
17731
17732 let diff_base = r#"
17733 use some::mod1;
17734 use some::mod2;
17735
17736 const A: u32 = 42;
17737 const B: u32 = 42;
17738 const C: u32 = 42;
17739
17740
17741 fn main() {
17742 println!("hello");
17743
17744 println!("world");
17745 }
17746 "#
17747 .unindent();
17748 executor.run_until_parked();
17749 cx.set_state(
17750 &r#"
17751 use some::mod1;
17752 use some::mod2;
17753
17754 ˇconst B: u32 = 42;
17755 const C: u32 = 42;
17756
17757
17758 fn main() {
17759 println!("hello");
17760
17761 println!("world");
17762 }
17763 "#
17764 .unindent(),
17765 );
17766
17767 cx.set_head_text(&diff_base);
17768 executor.run_until_parked();
17769
17770 cx.update_editor(|editor, window, cx| {
17771 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17772 });
17773 executor.run_until_parked();
17774
17775 cx.assert_state_with_diff(
17776 r#"
17777 use some::mod1;
17778 use some::mod2;
17779
17780 - const A: u32 = 42;
17781 ˇconst B: u32 = 42;
17782 const C: u32 = 42;
17783
17784
17785 fn main() {
17786 println!("hello");
17787
17788 println!("world");
17789 }
17790 "#
17791 .unindent(),
17792 );
17793
17794 cx.update_editor(|editor, window, cx| {
17795 editor.delete_line(&DeleteLine, window, cx);
17796 });
17797 executor.run_until_parked();
17798 cx.assert_state_with_diff(
17799 r#"
17800 use some::mod1;
17801 use some::mod2;
17802
17803 - const A: u32 = 42;
17804 - const B: u32 = 42;
17805 ˇconst C: u32 = 42;
17806
17807
17808 fn main() {
17809 println!("hello");
17810
17811 println!("world");
17812 }
17813 "#
17814 .unindent(),
17815 );
17816
17817 cx.update_editor(|editor, window, cx| {
17818 editor.delete_line(&DeleteLine, window, cx);
17819 });
17820 executor.run_until_parked();
17821 cx.assert_state_with_diff(
17822 r#"
17823 use some::mod1;
17824 use some::mod2;
17825
17826 - const A: u32 = 42;
17827 - const B: u32 = 42;
17828 - const C: u32 = 42;
17829 ˇ
17830
17831 fn main() {
17832 println!("hello");
17833
17834 println!("world");
17835 }
17836 "#
17837 .unindent(),
17838 );
17839
17840 cx.update_editor(|editor, window, cx| {
17841 editor.handle_input("replacement", window, cx);
17842 });
17843 executor.run_until_parked();
17844 cx.assert_state_with_diff(
17845 r#"
17846 use some::mod1;
17847 use some::mod2;
17848
17849 - const A: u32 = 42;
17850 - const B: u32 = 42;
17851 - const C: u32 = 42;
17852 -
17853 + replacementˇ
17854
17855 fn main() {
17856 println!("hello");
17857
17858 println!("world");
17859 }
17860 "#
17861 .unindent(),
17862 );
17863}
17864
17865#[gpui::test]
17866async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17867 init_test(cx, |_| {});
17868
17869 let mut cx = EditorTestContext::new(cx).await;
17870
17871 let base_text = r#"
17872 one
17873 two
17874 three
17875 four
17876 five
17877 "#
17878 .unindent();
17879 executor.run_until_parked();
17880 cx.set_state(
17881 &r#"
17882 one
17883 two
17884 fˇour
17885 five
17886 "#
17887 .unindent(),
17888 );
17889
17890 cx.set_head_text(&base_text);
17891 executor.run_until_parked();
17892
17893 cx.update_editor(|editor, window, cx| {
17894 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17895 });
17896 executor.run_until_parked();
17897
17898 cx.assert_state_with_diff(
17899 r#"
17900 one
17901 two
17902 - three
17903 fˇour
17904 five
17905 "#
17906 .unindent(),
17907 );
17908
17909 cx.update_editor(|editor, window, cx| {
17910 editor.backspace(&Backspace, window, cx);
17911 editor.backspace(&Backspace, window, cx);
17912 });
17913 executor.run_until_parked();
17914 cx.assert_state_with_diff(
17915 r#"
17916 one
17917 two
17918 - threeˇ
17919 - four
17920 + our
17921 five
17922 "#
17923 .unindent(),
17924 );
17925}
17926
17927#[gpui::test]
17928async fn test_edit_after_expanded_modification_hunk(
17929 executor: BackgroundExecutor,
17930 cx: &mut TestAppContext,
17931) {
17932 init_test(cx, |_| {});
17933
17934 let mut cx = EditorTestContext::new(cx).await;
17935
17936 let diff_base = r#"
17937 use some::mod1;
17938 use some::mod2;
17939
17940 const A: u32 = 42;
17941 const B: u32 = 42;
17942 const C: u32 = 42;
17943 const D: u32 = 42;
17944
17945
17946 fn main() {
17947 println!("hello");
17948
17949 println!("world");
17950 }"#
17951 .unindent();
17952
17953 cx.set_state(
17954 &r#"
17955 use some::mod1;
17956 use some::mod2;
17957
17958 const A: u32 = 42;
17959 const B: u32 = 42;
17960 const C: u32 = 43ˇ
17961 const D: u32 = 42;
17962
17963
17964 fn main() {
17965 println!("hello");
17966
17967 println!("world");
17968 }"#
17969 .unindent(),
17970 );
17971
17972 cx.set_head_text(&diff_base);
17973 executor.run_until_parked();
17974 cx.update_editor(|editor, window, cx| {
17975 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17976 });
17977 executor.run_until_parked();
17978
17979 cx.assert_state_with_diff(
17980 r#"
17981 use some::mod1;
17982 use some::mod2;
17983
17984 const A: u32 = 42;
17985 const B: u32 = 42;
17986 - const C: u32 = 42;
17987 + const C: u32 = 43ˇ
17988 const D: u32 = 42;
17989
17990
17991 fn main() {
17992 println!("hello");
17993
17994 println!("world");
17995 }"#
17996 .unindent(),
17997 );
17998
17999 cx.update_editor(|editor, window, cx| {
18000 editor.handle_input("\nnew_line\n", window, cx);
18001 });
18002 executor.run_until_parked();
18003
18004 cx.assert_state_with_diff(
18005 r#"
18006 use some::mod1;
18007 use some::mod2;
18008
18009 const A: u32 = 42;
18010 const B: u32 = 42;
18011 - const C: u32 = 42;
18012 + const C: u32 = 43
18013 + new_line
18014 + ˇ
18015 const D: u32 = 42;
18016
18017
18018 fn main() {
18019 println!("hello");
18020
18021 println!("world");
18022 }"#
18023 .unindent(),
18024 );
18025}
18026
18027#[gpui::test]
18028async fn test_stage_and_unstage_added_file_hunk(
18029 executor: BackgroundExecutor,
18030 cx: &mut TestAppContext,
18031) {
18032 init_test(cx, |_| {});
18033
18034 let mut cx = EditorTestContext::new(cx).await;
18035 cx.update_editor(|editor, _, cx| {
18036 editor.set_expand_all_diff_hunks(cx);
18037 });
18038
18039 let working_copy = r#"
18040 ˇfn main() {
18041 println!("hello, world!");
18042 }
18043 "#
18044 .unindent();
18045
18046 cx.set_state(&working_copy);
18047 executor.run_until_parked();
18048
18049 cx.assert_state_with_diff(
18050 r#"
18051 + ˇfn main() {
18052 + println!("hello, world!");
18053 + }
18054 "#
18055 .unindent(),
18056 );
18057 cx.assert_index_text(None);
18058
18059 cx.update_editor(|editor, window, cx| {
18060 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18061 });
18062 executor.run_until_parked();
18063 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
18064 cx.assert_state_with_diff(
18065 r#"
18066 + ˇfn main() {
18067 + println!("hello, world!");
18068 + }
18069 "#
18070 .unindent(),
18071 );
18072
18073 cx.update_editor(|editor, window, cx| {
18074 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18075 });
18076 executor.run_until_parked();
18077 cx.assert_index_text(None);
18078}
18079
18080async fn setup_indent_guides_editor(
18081 text: &str,
18082 cx: &mut TestAppContext,
18083) -> (BufferId, EditorTestContext) {
18084 init_test(cx, |_| {});
18085
18086 let mut cx = EditorTestContext::new(cx).await;
18087
18088 let buffer_id = cx.update_editor(|editor, window, cx| {
18089 editor.set_text(text, window, cx);
18090 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
18091
18092 buffer_ids[0]
18093 });
18094
18095 (buffer_id, cx)
18096}
18097
18098fn assert_indent_guides(
18099 range: Range<u32>,
18100 expected: Vec<IndentGuide>,
18101 active_indices: Option<Vec<usize>>,
18102 cx: &mut EditorTestContext,
18103) {
18104 let indent_guides = cx.update_editor(|editor, window, cx| {
18105 let snapshot = editor.snapshot(window, cx).display_snapshot;
18106 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
18107 editor,
18108 MultiBufferRow(range.start)..MultiBufferRow(range.end),
18109 true,
18110 &snapshot,
18111 cx,
18112 );
18113
18114 indent_guides.sort_by(|a, b| {
18115 a.depth.cmp(&b.depth).then(
18116 a.start_row
18117 .cmp(&b.start_row)
18118 .then(a.end_row.cmp(&b.end_row)),
18119 )
18120 });
18121 indent_guides
18122 });
18123
18124 if let Some(expected) = active_indices {
18125 let active_indices = cx.update_editor(|editor, window, cx| {
18126 let snapshot = editor.snapshot(window, cx).display_snapshot;
18127 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
18128 });
18129
18130 assert_eq!(
18131 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
18132 expected,
18133 "Active indent guide indices do not match"
18134 );
18135 }
18136
18137 assert_eq!(indent_guides, expected, "Indent guides do not match");
18138}
18139
18140fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
18141 IndentGuide {
18142 buffer_id,
18143 start_row: MultiBufferRow(start_row),
18144 end_row: MultiBufferRow(end_row),
18145 depth,
18146 tab_size: 4,
18147 settings: IndentGuideSettings {
18148 enabled: true,
18149 line_width: 1,
18150 active_line_width: 1,
18151 ..Default::default()
18152 },
18153 }
18154}
18155
18156#[gpui::test]
18157async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
18158 let (buffer_id, mut cx) = setup_indent_guides_editor(
18159 &"
18160 fn main() {
18161 let a = 1;
18162 }"
18163 .unindent(),
18164 cx,
18165 )
18166 .await;
18167
18168 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18169}
18170
18171#[gpui::test]
18172async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
18173 let (buffer_id, mut cx) = setup_indent_guides_editor(
18174 &"
18175 fn main() {
18176 let a = 1;
18177 let b = 2;
18178 }"
18179 .unindent(),
18180 cx,
18181 )
18182 .await;
18183
18184 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
18185}
18186
18187#[gpui::test]
18188async fn test_indent_guide_nested(cx: &mut TestAppContext) {
18189 let (buffer_id, mut cx) = setup_indent_guides_editor(
18190 &"
18191 fn main() {
18192 let a = 1;
18193 if a == 3 {
18194 let b = 2;
18195 } else {
18196 let c = 3;
18197 }
18198 }"
18199 .unindent(),
18200 cx,
18201 )
18202 .await;
18203
18204 assert_indent_guides(
18205 0..8,
18206 vec![
18207 indent_guide(buffer_id, 1, 6, 0),
18208 indent_guide(buffer_id, 3, 3, 1),
18209 indent_guide(buffer_id, 5, 5, 1),
18210 ],
18211 None,
18212 &mut cx,
18213 );
18214}
18215
18216#[gpui::test]
18217async fn test_indent_guide_tab(cx: &mut TestAppContext) {
18218 let (buffer_id, mut cx) = setup_indent_guides_editor(
18219 &"
18220 fn main() {
18221 let a = 1;
18222 let b = 2;
18223 let c = 3;
18224 }"
18225 .unindent(),
18226 cx,
18227 )
18228 .await;
18229
18230 assert_indent_guides(
18231 0..5,
18232 vec![
18233 indent_guide(buffer_id, 1, 3, 0),
18234 indent_guide(buffer_id, 2, 2, 1),
18235 ],
18236 None,
18237 &mut cx,
18238 );
18239}
18240
18241#[gpui::test]
18242async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
18243 let (buffer_id, mut cx) = setup_indent_guides_editor(
18244 &"
18245 fn main() {
18246 let a = 1;
18247
18248 let c = 3;
18249 }"
18250 .unindent(),
18251 cx,
18252 )
18253 .await;
18254
18255 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
18256}
18257
18258#[gpui::test]
18259async fn test_indent_guide_complex(cx: &mut TestAppContext) {
18260 let (buffer_id, mut cx) = setup_indent_guides_editor(
18261 &"
18262 fn main() {
18263 let a = 1;
18264
18265 let c = 3;
18266
18267 if a == 3 {
18268 let b = 2;
18269 } else {
18270 let c = 3;
18271 }
18272 }"
18273 .unindent(),
18274 cx,
18275 )
18276 .await;
18277
18278 assert_indent_guides(
18279 0..11,
18280 vec![
18281 indent_guide(buffer_id, 1, 9, 0),
18282 indent_guide(buffer_id, 6, 6, 1),
18283 indent_guide(buffer_id, 8, 8, 1),
18284 ],
18285 None,
18286 &mut cx,
18287 );
18288}
18289
18290#[gpui::test]
18291async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
18292 let (buffer_id, mut cx) = setup_indent_guides_editor(
18293 &"
18294 fn main() {
18295 let a = 1;
18296
18297 let c = 3;
18298
18299 if a == 3 {
18300 let b = 2;
18301 } else {
18302 let c = 3;
18303 }
18304 }"
18305 .unindent(),
18306 cx,
18307 )
18308 .await;
18309
18310 assert_indent_guides(
18311 1..11,
18312 vec![
18313 indent_guide(buffer_id, 1, 9, 0),
18314 indent_guide(buffer_id, 6, 6, 1),
18315 indent_guide(buffer_id, 8, 8, 1),
18316 ],
18317 None,
18318 &mut cx,
18319 );
18320}
18321
18322#[gpui::test]
18323async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
18324 let (buffer_id, mut cx) = setup_indent_guides_editor(
18325 &"
18326 fn main() {
18327 let a = 1;
18328
18329 let c = 3;
18330
18331 if a == 3 {
18332 let b = 2;
18333 } else {
18334 let c = 3;
18335 }
18336 }"
18337 .unindent(),
18338 cx,
18339 )
18340 .await;
18341
18342 assert_indent_guides(
18343 1..10,
18344 vec![
18345 indent_guide(buffer_id, 1, 9, 0),
18346 indent_guide(buffer_id, 6, 6, 1),
18347 indent_guide(buffer_id, 8, 8, 1),
18348 ],
18349 None,
18350 &mut cx,
18351 );
18352}
18353
18354#[gpui::test]
18355async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
18356 let (buffer_id, mut cx) = setup_indent_guides_editor(
18357 &"
18358 fn main() {
18359 if a {
18360 b(
18361 c,
18362 d,
18363 )
18364 } else {
18365 e(
18366 f
18367 )
18368 }
18369 }"
18370 .unindent(),
18371 cx,
18372 )
18373 .await;
18374
18375 assert_indent_guides(
18376 0..11,
18377 vec![
18378 indent_guide(buffer_id, 1, 10, 0),
18379 indent_guide(buffer_id, 2, 5, 1),
18380 indent_guide(buffer_id, 7, 9, 1),
18381 indent_guide(buffer_id, 3, 4, 2),
18382 indent_guide(buffer_id, 8, 8, 2),
18383 ],
18384 None,
18385 &mut cx,
18386 );
18387
18388 cx.update_editor(|editor, window, cx| {
18389 editor.fold_at(MultiBufferRow(2), window, cx);
18390 assert_eq!(
18391 editor.display_text(cx),
18392 "
18393 fn main() {
18394 if a {
18395 b(⋯
18396 )
18397 } else {
18398 e(
18399 f
18400 )
18401 }
18402 }"
18403 .unindent()
18404 );
18405 });
18406
18407 assert_indent_guides(
18408 0..11,
18409 vec![
18410 indent_guide(buffer_id, 1, 10, 0),
18411 indent_guide(buffer_id, 2, 5, 1),
18412 indent_guide(buffer_id, 7, 9, 1),
18413 indent_guide(buffer_id, 8, 8, 2),
18414 ],
18415 None,
18416 &mut cx,
18417 );
18418}
18419
18420#[gpui::test]
18421async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
18422 let (buffer_id, mut cx) = setup_indent_guides_editor(
18423 &"
18424 block1
18425 block2
18426 block3
18427 block4
18428 block2
18429 block1
18430 block1"
18431 .unindent(),
18432 cx,
18433 )
18434 .await;
18435
18436 assert_indent_guides(
18437 1..10,
18438 vec![
18439 indent_guide(buffer_id, 1, 4, 0),
18440 indent_guide(buffer_id, 2, 3, 1),
18441 indent_guide(buffer_id, 3, 3, 2),
18442 ],
18443 None,
18444 &mut cx,
18445 );
18446}
18447
18448#[gpui::test]
18449async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
18450 let (buffer_id, mut cx) = setup_indent_guides_editor(
18451 &"
18452 block1
18453 block2
18454 block3
18455
18456 block1
18457 block1"
18458 .unindent(),
18459 cx,
18460 )
18461 .await;
18462
18463 assert_indent_guides(
18464 0..6,
18465 vec![
18466 indent_guide(buffer_id, 1, 2, 0),
18467 indent_guide(buffer_id, 2, 2, 1),
18468 ],
18469 None,
18470 &mut cx,
18471 );
18472}
18473
18474#[gpui::test]
18475async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
18476 let (buffer_id, mut cx) = setup_indent_guides_editor(
18477 &"
18478 function component() {
18479 \treturn (
18480 \t\t\t
18481 \t\t<div>
18482 \t\t\t<abc></abc>
18483 \t\t</div>
18484 \t)
18485 }"
18486 .unindent(),
18487 cx,
18488 )
18489 .await;
18490
18491 assert_indent_guides(
18492 0..8,
18493 vec![
18494 indent_guide(buffer_id, 1, 6, 0),
18495 indent_guide(buffer_id, 2, 5, 1),
18496 indent_guide(buffer_id, 4, 4, 2),
18497 ],
18498 None,
18499 &mut cx,
18500 );
18501}
18502
18503#[gpui::test]
18504async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
18505 let (buffer_id, mut cx) = setup_indent_guides_editor(
18506 &"
18507 function component() {
18508 \treturn (
18509 \t
18510 \t\t<div>
18511 \t\t\t<abc></abc>
18512 \t\t</div>
18513 \t)
18514 }"
18515 .unindent(),
18516 cx,
18517 )
18518 .await;
18519
18520 assert_indent_guides(
18521 0..8,
18522 vec![
18523 indent_guide(buffer_id, 1, 6, 0),
18524 indent_guide(buffer_id, 2, 5, 1),
18525 indent_guide(buffer_id, 4, 4, 2),
18526 ],
18527 None,
18528 &mut cx,
18529 );
18530}
18531
18532#[gpui::test]
18533async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
18534 let (buffer_id, mut cx) = setup_indent_guides_editor(
18535 &"
18536 block1
18537
18538
18539
18540 block2
18541 "
18542 .unindent(),
18543 cx,
18544 )
18545 .await;
18546
18547 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18548}
18549
18550#[gpui::test]
18551async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
18552 let (buffer_id, mut cx) = setup_indent_guides_editor(
18553 &"
18554 def a:
18555 \tb = 3
18556 \tif True:
18557 \t\tc = 4
18558 \t\td = 5
18559 \tprint(b)
18560 "
18561 .unindent(),
18562 cx,
18563 )
18564 .await;
18565
18566 assert_indent_guides(
18567 0..6,
18568 vec![
18569 indent_guide(buffer_id, 1, 5, 0),
18570 indent_guide(buffer_id, 3, 4, 1),
18571 ],
18572 None,
18573 &mut cx,
18574 );
18575}
18576
18577#[gpui::test]
18578async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
18579 let (buffer_id, mut cx) = setup_indent_guides_editor(
18580 &"
18581 fn main() {
18582 let a = 1;
18583 }"
18584 .unindent(),
18585 cx,
18586 )
18587 .await;
18588
18589 cx.update_editor(|editor, window, cx| {
18590 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18591 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18592 });
18593 });
18594
18595 assert_indent_guides(
18596 0..3,
18597 vec![indent_guide(buffer_id, 1, 1, 0)],
18598 Some(vec![0]),
18599 &mut cx,
18600 );
18601}
18602
18603#[gpui::test]
18604async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
18605 let (buffer_id, mut cx) = setup_indent_guides_editor(
18606 &"
18607 fn main() {
18608 if 1 == 2 {
18609 let a = 1;
18610 }
18611 }"
18612 .unindent(),
18613 cx,
18614 )
18615 .await;
18616
18617 cx.update_editor(|editor, window, cx| {
18618 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18619 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18620 });
18621 });
18622
18623 assert_indent_guides(
18624 0..4,
18625 vec![
18626 indent_guide(buffer_id, 1, 3, 0),
18627 indent_guide(buffer_id, 2, 2, 1),
18628 ],
18629 Some(vec![1]),
18630 &mut cx,
18631 );
18632
18633 cx.update_editor(|editor, window, cx| {
18634 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18635 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18636 });
18637 });
18638
18639 assert_indent_guides(
18640 0..4,
18641 vec![
18642 indent_guide(buffer_id, 1, 3, 0),
18643 indent_guide(buffer_id, 2, 2, 1),
18644 ],
18645 Some(vec![1]),
18646 &mut cx,
18647 );
18648
18649 cx.update_editor(|editor, window, cx| {
18650 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18651 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
18652 });
18653 });
18654
18655 assert_indent_guides(
18656 0..4,
18657 vec![
18658 indent_guide(buffer_id, 1, 3, 0),
18659 indent_guide(buffer_id, 2, 2, 1),
18660 ],
18661 Some(vec![0]),
18662 &mut cx,
18663 );
18664}
18665
18666#[gpui::test]
18667async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
18668 let (buffer_id, mut cx) = setup_indent_guides_editor(
18669 &"
18670 fn main() {
18671 let a = 1;
18672
18673 let b = 2;
18674 }"
18675 .unindent(),
18676 cx,
18677 )
18678 .await;
18679
18680 cx.update_editor(|editor, window, cx| {
18681 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18682 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18683 });
18684 });
18685
18686 assert_indent_guides(
18687 0..5,
18688 vec![indent_guide(buffer_id, 1, 3, 0)],
18689 Some(vec![0]),
18690 &mut cx,
18691 );
18692}
18693
18694#[gpui::test]
18695async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
18696 let (buffer_id, mut cx) = setup_indent_guides_editor(
18697 &"
18698 def m:
18699 a = 1
18700 pass"
18701 .unindent(),
18702 cx,
18703 )
18704 .await;
18705
18706 cx.update_editor(|editor, window, cx| {
18707 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18708 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18709 });
18710 });
18711
18712 assert_indent_guides(
18713 0..3,
18714 vec![indent_guide(buffer_id, 1, 2, 0)],
18715 Some(vec![0]),
18716 &mut cx,
18717 );
18718}
18719
18720#[gpui::test]
18721async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
18722 init_test(cx, |_| {});
18723 let mut cx = EditorTestContext::new(cx).await;
18724 let text = indoc! {
18725 "
18726 impl A {
18727 fn b() {
18728 0;
18729 3;
18730 5;
18731 6;
18732 7;
18733 }
18734 }
18735 "
18736 };
18737 let base_text = indoc! {
18738 "
18739 impl A {
18740 fn b() {
18741 0;
18742 1;
18743 2;
18744 3;
18745 4;
18746 }
18747 fn c() {
18748 5;
18749 6;
18750 7;
18751 }
18752 }
18753 "
18754 };
18755
18756 cx.update_editor(|editor, window, cx| {
18757 editor.set_text(text, window, cx);
18758
18759 editor.buffer().update(cx, |multibuffer, cx| {
18760 let buffer = multibuffer.as_singleton().unwrap();
18761 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
18762
18763 multibuffer.set_all_diff_hunks_expanded(cx);
18764 multibuffer.add_diff(diff, cx);
18765
18766 buffer.read(cx).remote_id()
18767 })
18768 });
18769 cx.run_until_parked();
18770
18771 cx.assert_state_with_diff(
18772 indoc! { "
18773 impl A {
18774 fn b() {
18775 0;
18776 - 1;
18777 - 2;
18778 3;
18779 - 4;
18780 - }
18781 - fn c() {
18782 5;
18783 6;
18784 7;
18785 }
18786 }
18787 ˇ"
18788 }
18789 .to_string(),
18790 );
18791
18792 let mut actual_guides = cx.update_editor(|editor, window, cx| {
18793 editor
18794 .snapshot(window, cx)
18795 .buffer_snapshot
18796 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
18797 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
18798 .collect::<Vec<_>>()
18799 });
18800 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
18801 assert_eq!(
18802 actual_guides,
18803 vec![
18804 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
18805 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
18806 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
18807 ]
18808 );
18809}
18810
18811#[gpui::test]
18812async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18813 init_test(cx, |_| {});
18814 let mut cx = EditorTestContext::new(cx).await;
18815
18816 let diff_base = r#"
18817 a
18818 b
18819 c
18820 "#
18821 .unindent();
18822
18823 cx.set_state(
18824 &r#"
18825 ˇA
18826 b
18827 C
18828 "#
18829 .unindent(),
18830 );
18831 cx.set_head_text(&diff_base);
18832 cx.update_editor(|editor, window, cx| {
18833 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18834 });
18835 executor.run_until_parked();
18836
18837 let both_hunks_expanded = r#"
18838 - a
18839 + ˇA
18840 b
18841 - c
18842 + C
18843 "#
18844 .unindent();
18845
18846 cx.assert_state_with_diff(both_hunks_expanded.clone());
18847
18848 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18849 let snapshot = editor.snapshot(window, cx);
18850 let hunks = editor
18851 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18852 .collect::<Vec<_>>();
18853 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18854 let buffer_id = hunks[0].buffer_id;
18855 hunks
18856 .into_iter()
18857 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18858 .collect::<Vec<_>>()
18859 });
18860 assert_eq!(hunk_ranges.len(), 2);
18861
18862 cx.update_editor(|editor, _, cx| {
18863 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18864 });
18865 executor.run_until_parked();
18866
18867 let second_hunk_expanded = r#"
18868 ˇA
18869 b
18870 - c
18871 + C
18872 "#
18873 .unindent();
18874
18875 cx.assert_state_with_diff(second_hunk_expanded);
18876
18877 cx.update_editor(|editor, _, cx| {
18878 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18879 });
18880 executor.run_until_parked();
18881
18882 cx.assert_state_with_diff(both_hunks_expanded.clone());
18883
18884 cx.update_editor(|editor, _, cx| {
18885 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18886 });
18887 executor.run_until_parked();
18888
18889 let first_hunk_expanded = r#"
18890 - a
18891 + ˇA
18892 b
18893 C
18894 "#
18895 .unindent();
18896
18897 cx.assert_state_with_diff(first_hunk_expanded);
18898
18899 cx.update_editor(|editor, _, cx| {
18900 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18901 });
18902 executor.run_until_parked();
18903
18904 cx.assert_state_with_diff(both_hunks_expanded);
18905
18906 cx.set_state(
18907 &r#"
18908 ˇA
18909 b
18910 "#
18911 .unindent(),
18912 );
18913 cx.run_until_parked();
18914
18915 // TODO this cursor position seems bad
18916 cx.assert_state_with_diff(
18917 r#"
18918 - ˇa
18919 + A
18920 b
18921 "#
18922 .unindent(),
18923 );
18924
18925 cx.update_editor(|editor, window, cx| {
18926 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18927 });
18928
18929 cx.assert_state_with_diff(
18930 r#"
18931 - ˇa
18932 + A
18933 b
18934 - c
18935 "#
18936 .unindent(),
18937 );
18938
18939 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18940 let snapshot = editor.snapshot(window, cx);
18941 let hunks = editor
18942 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18943 .collect::<Vec<_>>();
18944 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18945 let buffer_id = hunks[0].buffer_id;
18946 hunks
18947 .into_iter()
18948 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18949 .collect::<Vec<_>>()
18950 });
18951 assert_eq!(hunk_ranges.len(), 2);
18952
18953 cx.update_editor(|editor, _, cx| {
18954 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18955 });
18956 executor.run_until_parked();
18957
18958 cx.assert_state_with_diff(
18959 r#"
18960 - ˇa
18961 + A
18962 b
18963 "#
18964 .unindent(),
18965 );
18966}
18967
18968#[gpui::test]
18969async fn test_toggle_deletion_hunk_at_start_of_file(
18970 executor: BackgroundExecutor,
18971 cx: &mut TestAppContext,
18972) {
18973 init_test(cx, |_| {});
18974 let mut cx = EditorTestContext::new(cx).await;
18975
18976 let diff_base = r#"
18977 a
18978 b
18979 c
18980 "#
18981 .unindent();
18982
18983 cx.set_state(
18984 &r#"
18985 ˇb
18986 c
18987 "#
18988 .unindent(),
18989 );
18990 cx.set_head_text(&diff_base);
18991 cx.update_editor(|editor, window, cx| {
18992 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18993 });
18994 executor.run_until_parked();
18995
18996 let hunk_expanded = r#"
18997 - a
18998 ˇb
18999 c
19000 "#
19001 .unindent();
19002
19003 cx.assert_state_with_diff(hunk_expanded.clone());
19004
19005 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19006 let snapshot = editor.snapshot(window, cx);
19007 let hunks = editor
19008 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19009 .collect::<Vec<_>>();
19010 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19011 let buffer_id = hunks[0].buffer_id;
19012 hunks
19013 .into_iter()
19014 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
19015 .collect::<Vec<_>>()
19016 });
19017 assert_eq!(hunk_ranges.len(), 1);
19018
19019 cx.update_editor(|editor, _, cx| {
19020 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19021 });
19022 executor.run_until_parked();
19023
19024 let hunk_collapsed = r#"
19025 ˇb
19026 c
19027 "#
19028 .unindent();
19029
19030 cx.assert_state_with_diff(hunk_collapsed);
19031
19032 cx.update_editor(|editor, _, cx| {
19033 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19034 });
19035 executor.run_until_parked();
19036
19037 cx.assert_state_with_diff(hunk_expanded.clone());
19038}
19039
19040#[gpui::test]
19041async fn test_display_diff_hunks(cx: &mut TestAppContext) {
19042 init_test(cx, |_| {});
19043
19044 let fs = FakeFs::new(cx.executor());
19045 fs.insert_tree(
19046 path!("/test"),
19047 json!({
19048 ".git": {},
19049 "file-1": "ONE\n",
19050 "file-2": "TWO\n",
19051 "file-3": "THREE\n",
19052 }),
19053 )
19054 .await;
19055
19056 fs.set_head_for_repo(
19057 path!("/test/.git").as_ref(),
19058 &[
19059 ("file-1".into(), "one\n".into()),
19060 ("file-2".into(), "two\n".into()),
19061 ("file-3".into(), "three\n".into()),
19062 ],
19063 "deadbeef",
19064 );
19065
19066 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
19067 let mut buffers = vec![];
19068 for i in 1..=3 {
19069 let buffer = project
19070 .update(cx, |project, cx| {
19071 let path = format!(path!("/test/file-{}"), i);
19072 project.open_local_buffer(path, cx)
19073 })
19074 .await
19075 .unwrap();
19076 buffers.push(buffer);
19077 }
19078
19079 let multibuffer = cx.new(|cx| {
19080 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
19081 multibuffer.set_all_diff_hunks_expanded(cx);
19082 for buffer in &buffers {
19083 let snapshot = buffer.read(cx).snapshot();
19084 multibuffer.set_excerpts_for_path(
19085 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
19086 buffer.clone(),
19087 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
19088 DEFAULT_MULTIBUFFER_CONTEXT,
19089 cx,
19090 );
19091 }
19092 multibuffer
19093 });
19094
19095 let editor = cx.add_window(|window, cx| {
19096 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
19097 });
19098 cx.run_until_parked();
19099
19100 let snapshot = editor
19101 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19102 .unwrap();
19103 let hunks = snapshot
19104 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
19105 .map(|hunk| match hunk {
19106 DisplayDiffHunk::Unfolded {
19107 display_row_range, ..
19108 } => display_row_range,
19109 DisplayDiffHunk::Folded { .. } => unreachable!(),
19110 })
19111 .collect::<Vec<_>>();
19112 assert_eq!(
19113 hunks,
19114 [
19115 DisplayRow(2)..DisplayRow(4),
19116 DisplayRow(7)..DisplayRow(9),
19117 DisplayRow(12)..DisplayRow(14),
19118 ]
19119 );
19120}
19121
19122#[gpui::test]
19123async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
19124 init_test(cx, |_| {});
19125
19126 let mut cx = EditorTestContext::new(cx).await;
19127 cx.set_head_text(indoc! { "
19128 one
19129 two
19130 three
19131 four
19132 five
19133 "
19134 });
19135 cx.set_index_text(indoc! { "
19136 one
19137 two
19138 three
19139 four
19140 five
19141 "
19142 });
19143 cx.set_state(indoc! {"
19144 one
19145 TWO
19146 ˇTHREE
19147 FOUR
19148 five
19149 "});
19150 cx.run_until_parked();
19151 cx.update_editor(|editor, window, cx| {
19152 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19153 });
19154 cx.run_until_parked();
19155 cx.assert_index_text(Some(indoc! {"
19156 one
19157 TWO
19158 THREE
19159 FOUR
19160 five
19161 "}));
19162 cx.set_state(indoc! { "
19163 one
19164 TWO
19165 ˇTHREE-HUNDRED
19166 FOUR
19167 five
19168 "});
19169 cx.run_until_parked();
19170 cx.update_editor(|editor, window, cx| {
19171 let snapshot = editor.snapshot(window, cx);
19172 let hunks = editor
19173 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19174 .collect::<Vec<_>>();
19175 assert_eq!(hunks.len(), 1);
19176 assert_eq!(
19177 hunks[0].status(),
19178 DiffHunkStatus {
19179 kind: DiffHunkStatusKind::Modified,
19180 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
19181 }
19182 );
19183
19184 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19185 });
19186 cx.run_until_parked();
19187 cx.assert_index_text(Some(indoc! {"
19188 one
19189 TWO
19190 THREE-HUNDRED
19191 FOUR
19192 five
19193 "}));
19194}
19195
19196#[gpui::test]
19197fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
19198 init_test(cx, |_| {});
19199
19200 let editor = cx.add_window(|window, cx| {
19201 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
19202 build_editor(buffer, window, cx)
19203 });
19204
19205 let render_args = Arc::new(Mutex::new(None));
19206 let snapshot = editor
19207 .update(cx, |editor, window, cx| {
19208 let snapshot = editor.buffer().read(cx).snapshot(cx);
19209 let range =
19210 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
19211
19212 struct RenderArgs {
19213 row: MultiBufferRow,
19214 folded: bool,
19215 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
19216 }
19217
19218 let crease = Crease::inline(
19219 range,
19220 FoldPlaceholder::test(),
19221 {
19222 let toggle_callback = render_args.clone();
19223 move |row, folded, callback, _window, _cx| {
19224 *toggle_callback.lock() = Some(RenderArgs {
19225 row,
19226 folded,
19227 callback,
19228 });
19229 div()
19230 }
19231 },
19232 |_row, _folded, _window, _cx| div(),
19233 );
19234
19235 editor.insert_creases(Some(crease), cx);
19236 let snapshot = editor.snapshot(window, cx);
19237 let _div = snapshot.render_crease_toggle(
19238 MultiBufferRow(1),
19239 false,
19240 cx.entity().clone(),
19241 window,
19242 cx,
19243 );
19244 snapshot
19245 })
19246 .unwrap();
19247
19248 let render_args = render_args.lock().take().unwrap();
19249 assert_eq!(render_args.row, MultiBufferRow(1));
19250 assert!(!render_args.folded);
19251 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19252
19253 cx.update_window(*editor, |_, window, cx| {
19254 (render_args.callback)(true, window, cx)
19255 })
19256 .unwrap();
19257 let snapshot = editor
19258 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19259 .unwrap();
19260 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
19261
19262 cx.update_window(*editor, |_, window, cx| {
19263 (render_args.callback)(false, window, cx)
19264 })
19265 .unwrap();
19266 let snapshot = editor
19267 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19268 .unwrap();
19269 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19270}
19271
19272#[gpui::test]
19273async fn test_input_text(cx: &mut TestAppContext) {
19274 init_test(cx, |_| {});
19275 let mut cx = EditorTestContext::new(cx).await;
19276
19277 cx.set_state(
19278 &r#"ˇone
19279 two
19280
19281 three
19282 fourˇ
19283 five
19284
19285 siˇx"#
19286 .unindent(),
19287 );
19288
19289 cx.dispatch_action(HandleInput(String::new()));
19290 cx.assert_editor_state(
19291 &r#"ˇone
19292 two
19293
19294 three
19295 fourˇ
19296 five
19297
19298 siˇx"#
19299 .unindent(),
19300 );
19301
19302 cx.dispatch_action(HandleInput("AAAA".to_string()));
19303 cx.assert_editor_state(
19304 &r#"AAAAˇone
19305 two
19306
19307 three
19308 fourAAAAˇ
19309 five
19310
19311 siAAAAˇx"#
19312 .unindent(),
19313 );
19314}
19315
19316#[gpui::test]
19317async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
19318 init_test(cx, |_| {});
19319
19320 let mut cx = EditorTestContext::new(cx).await;
19321 cx.set_state(
19322 r#"let foo = 1;
19323let foo = 2;
19324let foo = 3;
19325let fooˇ = 4;
19326let foo = 5;
19327let foo = 6;
19328let foo = 7;
19329let foo = 8;
19330let foo = 9;
19331let foo = 10;
19332let foo = 11;
19333let foo = 12;
19334let foo = 13;
19335let foo = 14;
19336let foo = 15;"#,
19337 );
19338
19339 cx.update_editor(|e, window, cx| {
19340 assert_eq!(
19341 e.next_scroll_position,
19342 NextScrollCursorCenterTopBottom::Center,
19343 "Default next scroll direction is center",
19344 );
19345
19346 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19347 assert_eq!(
19348 e.next_scroll_position,
19349 NextScrollCursorCenterTopBottom::Top,
19350 "After center, next scroll direction should be top",
19351 );
19352
19353 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19354 assert_eq!(
19355 e.next_scroll_position,
19356 NextScrollCursorCenterTopBottom::Bottom,
19357 "After top, next scroll direction should be bottom",
19358 );
19359
19360 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19361 assert_eq!(
19362 e.next_scroll_position,
19363 NextScrollCursorCenterTopBottom::Center,
19364 "After bottom, scrolling should start over",
19365 );
19366
19367 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19368 assert_eq!(
19369 e.next_scroll_position,
19370 NextScrollCursorCenterTopBottom::Top,
19371 "Scrolling continues if retriggered fast enough"
19372 );
19373 });
19374
19375 cx.executor()
19376 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
19377 cx.executor().run_until_parked();
19378 cx.update_editor(|e, _, _| {
19379 assert_eq!(
19380 e.next_scroll_position,
19381 NextScrollCursorCenterTopBottom::Center,
19382 "If scrolling is not triggered fast enough, it should reset"
19383 );
19384 });
19385}
19386
19387#[gpui::test]
19388async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
19389 init_test(cx, |_| {});
19390 let mut cx = EditorLspTestContext::new_rust(
19391 lsp::ServerCapabilities {
19392 definition_provider: Some(lsp::OneOf::Left(true)),
19393 references_provider: Some(lsp::OneOf::Left(true)),
19394 ..lsp::ServerCapabilities::default()
19395 },
19396 cx,
19397 )
19398 .await;
19399
19400 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
19401 let go_to_definition = cx
19402 .lsp
19403 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19404 move |params, _| async move {
19405 if empty_go_to_definition {
19406 Ok(None)
19407 } else {
19408 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
19409 uri: params.text_document_position_params.text_document.uri,
19410 range: lsp::Range::new(
19411 lsp::Position::new(4, 3),
19412 lsp::Position::new(4, 6),
19413 ),
19414 })))
19415 }
19416 },
19417 );
19418 let references = cx
19419 .lsp
19420 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
19421 Ok(Some(vec![lsp::Location {
19422 uri: params.text_document_position.text_document.uri,
19423 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
19424 }]))
19425 });
19426 (go_to_definition, references)
19427 };
19428
19429 cx.set_state(
19430 &r#"fn one() {
19431 let mut a = ˇtwo();
19432 }
19433
19434 fn two() {}"#
19435 .unindent(),
19436 );
19437 set_up_lsp_handlers(false, &mut cx);
19438 let navigated = cx
19439 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19440 .await
19441 .expect("Failed to navigate to definition");
19442 assert_eq!(
19443 navigated,
19444 Navigated::Yes,
19445 "Should have navigated to definition from the GetDefinition response"
19446 );
19447 cx.assert_editor_state(
19448 &r#"fn one() {
19449 let mut a = two();
19450 }
19451
19452 fn «twoˇ»() {}"#
19453 .unindent(),
19454 );
19455
19456 let editors = cx.update_workspace(|workspace, _, cx| {
19457 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19458 });
19459 cx.update_editor(|_, _, test_editor_cx| {
19460 assert_eq!(
19461 editors.len(),
19462 1,
19463 "Initially, only one, test, editor should be open in the workspace"
19464 );
19465 assert_eq!(
19466 test_editor_cx.entity(),
19467 editors.last().expect("Asserted len is 1").clone()
19468 );
19469 });
19470
19471 set_up_lsp_handlers(true, &mut cx);
19472 let navigated = cx
19473 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19474 .await
19475 .expect("Failed to navigate to lookup references");
19476 assert_eq!(
19477 navigated,
19478 Navigated::Yes,
19479 "Should have navigated to references as a fallback after empty GoToDefinition response"
19480 );
19481 // We should not change the selections in the existing file,
19482 // if opening another milti buffer with the references
19483 cx.assert_editor_state(
19484 &r#"fn one() {
19485 let mut a = two();
19486 }
19487
19488 fn «twoˇ»() {}"#
19489 .unindent(),
19490 );
19491 let editors = cx.update_workspace(|workspace, _, cx| {
19492 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19493 });
19494 cx.update_editor(|_, _, test_editor_cx| {
19495 assert_eq!(
19496 editors.len(),
19497 2,
19498 "After falling back to references search, we open a new editor with the results"
19499 );
19500 let references_fallback_text = editors
19501 .into_iter()
19502 .find(|new_editor| *new_editor != test_editor_cx.entity())
19503 .expect("Should have one non-test editor now")
19504 .read(test_editor_cx)
19505 .text(test_editor_cx);
19506 assert_eq!(
19507 references_fallback_text, "fn one() {\n let mut a = two();\n}",
19508 "Should use the range from the references response and not the GoToDefinition one"
19509 );
19510 });
19511}
19512
19513#[gpui::test]
19514async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
19515 init_test(cx, |_| {});
19516 cx.update(|cx| {
19517 let mut editor_settings = EditorSettings::get_global(cx).clone();
19518 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
19519 EditorSettings::override_global(editor_settings, cx);
19520 });
19521 let mut cx = EditorLspTestContext::new_rust(
19522 lsp::ServerCapabilities {
19523 definition_provider: Some(lsp::OneOf::Left(true)),
19524 references_provider: Some(lsp::OneOf::Left(true)),
19525 ..lsp::ServerCapabilities::default()
19526 },
19527 cx,
19528 )
19529 .await;
19530 let original_state = r#"fn one() {
19531 let mut a = ˇtwo();
19532 }
19533
19534 fn two() {}"#
19535 .unindent();
19536 cx.set_state(&original_state);
19537
19538 let mut go_to_definition = cx
19539 .lsp
19540 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19541 move |_, _| async move { Ok(None) },
19542 );
19543 let _references = cx
19544 .lsp
19545 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
19546 panic!("Should not call for references with no go to definition fallback")
19547 });
19548
19549 let navigated = cx
19550 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19551 .await
19552 .expect("Failed to navigate to lookup references");
19553 go_to_definition
19554 .next()
19555 .await
19556 .expect("Should have called the go_to_definition handler");
19557
19558 assert_eq!(
19559 navigated,
19560 Navigated::No,
19561 "Should have navigated to references as a fallback after empty GoToDefinition response"
19562 );
19563 cx.assert_editor_state(&original_state);
19564 let editors = cx.update_workspace(|workspace, _, cx| {
19565 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19566 });
19567 cx.update_editor(|_, _, _| {
19568 assert_eq!(
19569 editors.len(),
19570 1,
19571 "After unsuccessful fallback, no other editor should have been opened"
19572 );
19573 });
19574}
19575
19576#[gpui::test]
19577async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
19578 init_test(cx, |_| {});
19579
19580 let language = Arc::new(Language::new(
19581 LanguageConfig::default(),
19582 Some(tree_sitter_rust::LANGUAGE.into()),
19583 ));
19584
19585 let text = r#"
19586 #[cfg(test)]
19587 mod tests() {
19588 #[test]
19589 fn runnable_1() {
19590 let a = 1;
19591 }
19592
19593 #[test]
19594 fn runnable_2() {
19595 let a = 1;
19596 let b = 2;
19597 }
19598 }
19599 "#
19600 .unindent();
19601
19602 let fs = FakeFs::new(cx.executor());
19603 fs.insert_file("/file.rs", Default::default()).await;
19604
19605 let project = Project::test(fs, ["/a".as_ref()], cx).await;
19606 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19607 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19608 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
19609 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
19610
19611 let editor = cx.new_window_entity(|window, cx| {
19612 Editor::new(
19613 EditorMode::full(),
19614 multi_buffer,
19615 Some(project.clone()),
19616 window,
19617 cx,
19618 )
19619 });
19620
19621 editor.update_in(cx, |editor, window, cx| {
19622 let snapshot = editor.buffer().read(cx).snapshot(cx);
19623 editor.tasks.insert(
19624 (buffer.read(cx).remote_id(), 3),
19625 RunnableTasks {
19626 templates: vec![],
19627 offset: snapshot.anchor_before(43),
19628 column: 0,
19629 extra_variables: HashMap::default(),
19630 context_range: BufferOffset(43)..BufferOffset(85),
19631 },
19632 );
19633 editor.tasks.insert(
19634 (buffer.read(cx).remote_id(), 8),
19635 RunnableTasks {
19636 templates: vec![],
19637 offset: snapshot.anchor_before(86),
19638 column: 0,
19639 extra_variables: HashMap::default(),
19640 context_range: BufferOffset(86)..BufferOffset(191),
19641 },
19642 );
19643
19644 // Test finding task when cursor is inside function body
19645 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19646 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
19647 });
19648 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19649 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
19650
19651 // Test finding task when cursor is on function name
19652 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19653 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
19654 });
19655 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19656 assert_eq!(row, 8, "Should find task when cursor is on function name");
19657 });
19658}
19659
19660#[gpui::test]
19661async fn test_folding_buffers(cx: &mut TestAppContext) {
19662 init_test(cx, |_| {});
19663
19664 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
19665 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
19666 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
19667
19668 let fs = FakeFs::new(cx.executor());
19669 fs.insert_tree(
19670 path!("/a"),
19671 json!({
19672 "first.rs": sample_text_1,
19673 "second.rs": sample_text_2,
19674 "third.rs": sample_text_3,
19675 }),
19676 )
19677 .await;
19678 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19679 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19680 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19681 let worktree = project.update(cx, |project, cx| {
19682 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19683 assert_eq!(worktrees.len(), 1);
19684 worktrees.pop().unwrap()
19685 });
19686 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19687
19688 let buffer_1 = project
19689 .update(cx, |project, cx| {
19690 project.open_buffer((worktree_id, "first.rs"), cx)
19691 })
19692 .await
19693 .unwrap();
19694 let buffer_2 = project
19695 .update(cx, |project, cx| {
19696 project.open_buffer((worktree_id, "second.rs"), cx)
19697 })
19698 .await
19699 .unwrap();
19700 let buffer_3 = project
19701 .update(cx, |project, cx| {
19702 project.open_buffer((worktree_id, "third.rs"), cx)
19703 })
19704 .await
19705 .unwrap();
19706
19707 let multi_buffer = cx.new(|cx| {
19708 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19709 multi_buffer.push_excerpts(
19710 buffer_1.clone(),
19711 [
19712 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19713 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19714 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19715 ],
19716 cx,
19717 );
19718 multi_buffer.push_excerpts(
19719 buffer_2.clone(),
19720 [
19721 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19722 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19723 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19724 ],
19725 cx,
19726 );
19727 multi_buffer.push_excerpts(
19728 buffer_3.clone(),
19729 [
19730 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19731 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19732 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19733 ],
19734 cx,
19735 );
19736 multi_buffer
19737 });
19738 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19739 Editor::new(
19740 EditorMode::full(),
19741 multi_buffer.clone(),
19742 Some(project.clone()),
19743 window,
19744 cx,
19745 )
19746 });
19747
19748 assert_eq!(
19749 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19750 "\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",
19751 );
19752
19753 multi_buffer_editor.update(cx, |editor, cx| {
19754 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
19755 });
19756 assert_eq!(
19757 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19758 "\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",
19759 "After folding the first buffer, its text should not be displayed"
19760 );
19761
19762 multi_buffer_editor.update(cx, |editor, cx| {
19763 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
19764 });
19765 assert_eq!(
19766 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19767 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
19768 "After folding the second buffer, its text should not be displayed"
19769 );
19770
19771 multi_buffer_editor.update(cx, |editor, cx| {
19772 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
19773 });
19774 assert_eq!(
19775 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19776 "\n\n\n\n\n",
19777 "After folding the third buffer, its text should not be displayed"
19778 );
19779
19780 // Emulate selection inside the fold logic, that should work
19781 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19782 editor
19783 .snapshot(window, cx)
19784 .next_line_boundary(Point::new(0, 4));
19785 });
19786
19787 multi_buffer_editor.update(cx, |editor, cx| {
19788 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
19789 });
19790 assert_eq!(
19791 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19792 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19793 "After unfolding the second buffer, its text should be displayed"
19794 );
19795
19796 // Typing inside of buffer 1 causes that buffer to be unfolded.
19797 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19798 assert_eq!(
19799 multi_buffer
19800 .read(cx)
19801 .snapshot(cx)
19802 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
19803 .collect::<String>(),
19804 "bbbb"
19805 );
19806 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
19807 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
19808 });
19809 editor.handle_input("B", window, cx);
19810 });
19811
19812 assert_eq!(
19813 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19814 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19815 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
19816 );
19817
19818 multi_buffer_editor.update(cx, |editor, cx| {
19819 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
19820 });
19821 assert_eq!(
19822 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19823 "\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",
19824 "After unfolding the all buffers, all original text should be displayed"
19825 );
19826}
19827
19828#[gpui::test]
19829async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
19830 init_test(cx, |_| {});
19831
19832 let sample_text_1 = "1111\n2222\n3333".to_string();
19833 let sample_text_2 = "4444\n5555\n6666".to_string();
19834 let sample_text_3 = "7777\n8888\n9999".to_string();
19835
19836 let fs = FakeFs::new(cx.executor());
19837 fs.insert_tree(
19838 path!("/a"),
19839 json!({
19840 "first.rs": sample_text_1,
19841 "second.rs": sample_text_2,
19842 "third.rs": sample_text_3,
19843 }),
19844 )
19845 .await;
19846 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19847 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19848 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19849 let worktree = project.update(cx, |project, cx| {
19850 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19851 assert_eq!(worktrees.len(), 1);
19852 worktrees.pop().unwrap()
19853 });
19854 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19855
19856 let buffer_1 = project
19857 .update(cx, |project, cx| {
19858 project.open_buffer((worktree_id, "first.rs"), cx)
19859 })
19860 .await
19861 .unwrap();
19862 let buffer_2 = project
19863 .update(cx, |project, cx| {
19864 project.open_buffer((worktree_id, "second.rs"), cx)
19865 })
19866 .await
19867 .unwrap();
19868 let buffer_3 = project
19869 .update(cx, |project, cx| {
19870 project.open_buffer((worktree_id, "third.rs"), cx)
19871 })
19872 .await
19873 .unwrap();
19874
19875 let multi_buffer = cx.new(|cx| {
19876 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19877 multi_buffer.push_excerpts(
19878 buffer_1.clone(),
19879 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19880 cx,
19881 );
19882 multi_buffer.push_excerpts(
19883 buffer_2.clone(),
19884 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19885 cx,
19886 );
19887 multi_buffer.push_excerpts(
19888 buffer_3.clone(),
19889 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19890 cx,
19891 );
19892 multi_buffer
19893 });
19894
19895 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19896 Editor::new(
19897 EditorMode::full(),
19898 multi_buffer,
19899 Some(project.clone()),
19900 window,
19901 cx,
19902 )
19903 });
19904
19905 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
19906 assert_eq!(
19907 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19908 full_text,
19909 );
19910
19911 multi_buffer_editor.update(cx, |editor, cx| {
19912 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
19913 });
19914 assert_eq!(
19915 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19916 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
19917 "After folding the first buffer, its text should not be displayed"
19918 );
19919
19920 multi_buffer_editor.update(cx, |editor, cx| {
19921 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
19922 });
19923
19924 assert_eq!(
19925 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19926 "\n\n\n\n\n\n7777\n8888\n9999",
19927 "After folding the second buffer, its text should not be displayed"
19928 );
19929
19930 multi_buffer_editor.update(cx, |editor, cx| {
19931 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
19932 });
19933 assert_eq!(
19934 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19935 "\n\n\n\n\n",
19936 "After folding the third buffer, its text should not be displayed"
19937 );
19938
19939 multi_buffer_editor.update(cx, |editor, cx| {
19940 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
19941 });
19942 assert_eq!(
19943 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19944 "\n\n\n\n4444\n5555\n6666\n\n",
19945 "After unfolding the second buffer, its text should be displayed"
19946 );
19947
19948 multi_buffer_editor.update(cx, |editor, cx| {
19949 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
19950 });
19951 assert_eq!(
19952 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19953 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
19954 "After unfolding the first buffer, its text should be displayed"
19955 );
19956
19957 multi_buffer_editor.update(cx, |editor, cx| {
19958 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
19959 });
19960 assert_eq!(
19961 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19962 full_text,
19963 "After unfolding all buffers, all original text should be displayed"
19964 );
19965}
19966
19967#[gpui::test]
19968async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
19969 init_test(cx, |_| {});
19970
19971 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
19972
19973 let fs = FakeFs::new(cx.executor());
19974 fs.insert_tree(
19975 path!("/a"),
19976 json!({
19977 "main.rs": sample_text,
19978 }),
19979 )
19980 .await;
19981 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19982 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19983 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19984 let worktree = project.update(cx, |project, cx| {
19985 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19986 assert_eq!(worktrees.len(), 1);
19987 worktrees.pop().unwrap()
19988 });
19989 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19990
19991 let buffer_1 = project
19992 .update(cx, |project, cx| {
19993 project.open_buffer((worktree_id, "main.rs"), cx)
19994 })
19995 .await
19996 .unwrap();
19997
19998 let multi_buffer = cx.new(|cx| {
19999 let mut multi_buffer = MultiBuffer::new(ReadWrite);
20000 multi_buffer.push_excerpts(
20001 buffer_1.clone(),
20002 [ExcerptRange::new(
20003 Point::new(0, 0)
20004 ..Point::new(
20005 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
20006 0,
20007 ),
20008 )],
20009 cx,
20010 );
20011 multi_buffer
20012 });
20013 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20014 Editor::new(
20015 EditorMode::full(),
20016 multi_buffer,
20017 Some(project.clone()),
20018 window,
20019 cx,
20020 )
20021 });
20022
20023 let selection_range = Point::new(1, 0)..Point::new(2, 0);
20024 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20025 enum TestHighlight {}
20026 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
20027 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
20028 editor.highlight_text::<TestHighlight>(
20029 vec![highlight_range.clone()],
20030 HighlightStyle::color(Hsla::green()),
20031 cx,
20032 );
20033 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20034 s.select_ranges(Some(highlight_range))
20035 });
20036 });
20037
20038 let full_text = format!("\n\n{sample_text}");
20039 assert_eq!(
20040 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20041 full_text,
20042 );
20043}
20044
20045#[gpui::test]
20046async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
20047 init_test(cx, |_| {});
20048 cx.update(|cx| {
20049 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
20050 "keymaps/default-linux.json",
20051 cx,
20052 )
20053 .unwrap();
20054 cx.bind_keys(default_key_bindings);
20055 });
20056
20057 let (editor, cx) = cx.add_window_view(|window, cx| {
20058 let multi_buffer = MultiBuffer::build_multi(
20059 [
20060 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
20061 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
20062 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
20063 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
20064 ],
20065 cx,
20066 );
20067 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
20068
20069 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
20070 // fold all but the second buffer, so that we test navigating between two
20071 // adjacent folded buffers, as well as folded buffers at the start and
20072 // end the multibuffer
20073 editor.fold_buffer(buffer_ids[0], cx);
20074 editor.fold_buffer(buffer_ids[2], cx);
20075 editor.fold_buffer(buffer_ids[3], cx);
20076
20077 editor
20078 });
20079 cx.simulate_resize(size(px(1000.), px(1000.)));
20080
20081 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
20082 cx.assert_excerpts_with_selections(indoc! {"
20083 [EXCERPT]
20084 ˇ[FOLDED]
20085 [EXCERPT]
20086 a1
20087 b1
20088 [EXCERPT]
20089 [FOLDED]
20090 [EXCERPT]
20091 [FOLDED]
20092 "
20093 });
20094 cx.simulate_keystroke("down");
20095 cx.assert_excerpts_with_selections(indoc! {"
20096 [EXCERPT]
20097 [FOLDED]
20098 [EXCERPT]
20099 ˇa1
20100 b1
20101 [EXCERPT]
20102 [FOLDED]
20103 [EXCERPT]
20104 [FOLDED]
20105 "
20106 });
20107 cx.simulate_keystroke("down");
20108 cx.assert_excerpts_with_selections(indoc! {"
20109 [EXCERPT]
20110 [FOLDED]
20111 [EXCERPT]
20112 a1
20113 ˇb1
20114 [EXCERPT]
20115 [FOLDED]
20116 [EXCERPT]
20117 [FOLDED]
20118 "
20119 });
20120 cx.simulate_keystroke("down");
20121 cx.assert_excerpts_with_selections(indoc! {"
20122 [EXCERPT]
20123 [FOLDED]
20124 [EXCERPT]
20125 a1
20126 b1
20127 ˇ[EXCERPT]
20128 [FOLDED]
20129 [EXCERPT]
20130 [FOLDED]
20131 "
20132 });
20133 cx.simulate_keystroke("down");
20134 cx.assert_excerpts_with_selections(indoc! {"
20135 [EXCERPT]
20136 [FOLDED]
20137 [EXCERPT]
20138 a1
20139 b1
20140 [EXCERPT]
20141 ˇ[FOLDED]
20142 [EXCERPT]
20143 [FOLDED]
20144 "
20145 });
20146 for _ in 0..5 {
20147 cx.simulate_keystroke("down");
20148 cx.assert_excerpts_with_selections(indoc! {"
20149 [EXCERPT]
20150 [FOLDED]
20151 [EXCERPT]
20152 a1
20153 b1
20154 [EXCERPT]
20155 [FOLDED]
20156 [EXCERPT]
20157 ˇ[FOLDED]
20158 "
20159 });
20160 }
20161
20162 cx.simulate_keystroke("up");
20163 cx.assert_excerpts_with_selections(indoc! {"
20164 [EXCERPT]
20165 [FOLDED]
20166 [EXCERPT]
20167 a1
20168 b1
20169 [EXCERPT]
20170 ˇ[FOLDED]
20171 [EXCERPT]
20172 [FOLDED]
20173 "
20174 });
20175 cx.simulate_keystroke("up");
20176 cx.assert_excerpts_with_selections(indoc! {"
20177 [EXCERPT]
20178 [FOLDED]
20179 [EXCERPT]
20180 a1
20181 b1
20182 ˇ[EXCERPT]
20183 [FOLDED]
20184 [EXCERPT]
20185 [FOLDED]
20186 "
20187 });
20188 cx.simulate_keystroke("up");
20189 cx.assert_excerpts_with_selections(indoc! {"
20190 [EXCERPT]
20191 [FOLDED]
20192 [EXCERPT]
20193 a1
20194 ˇb1
20195 [EXCERPT]
20196 [FOLDED]
20197 [EXCERPT]
20198 [FOLDED]
20199 "
20200 });
20201 cx.simulate_keystroke("up");
20202 cx.assert_excerpts_with_selections(indoc! {"
20203 [EXCERPT]
20204 [FOLDED]
20205 [EXCERPT]
20206 ˇa1
20207 b1
20208 [EXCERPT]
20209 [FOLDED]
20210 [EXCERPT]
20211 [FOLDED]
20212 "
20213 });
20214 for _ in 0..5 {
20215 cx.simulate_keystroke("up");
20216 cx.assert_excerpts_with_selections(indoc! {"
20217 [EXCERPT]
20218 ˇ[FOLDED]
20219 [EXCERPT]
20220 a1
20221 b1
20222 [EXCERPT]
20223 [FOLDED]
20224 [EXCERPT]
20225 [FOLDED]
20226 "
20227 });
20228 }
20229}
20230
20231#[gpui::test]
20232async fn test_inline_completion_text(cx: &mut TestAppContext) {
20233 init_test(cx, |_| {});
20234
20235 // Simple insertion
20236 assert_highlighted_edits(
20237 "Hello, world!",
20238 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
20239 true,
20240 cx,
20241 |highlighted_edits, cx| {
20242 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
20243 assert_eq!(highlighted_edits.highlights.len(), 1);
20244 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
20245 assert_eq!(
20246 highlighted_edits.highlights[0].1.background_color,
20247 Some(cx.theme().status().created_background)
20248 );
20249 },
20250 )
20251 .await;
20252
20253 // Replacement
20254 assert_highlighted_edits(
20255 "This is a test.",
20256 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
20257 false,
20258 cx,
20259 |highlighted_edits, cx| {
20260 assert_eq!(highlighted_edits.text, "That is a test.");
20261 assert_eq!(highlighted_edits.highlights.len(), 1);
20262 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
20263 assert_eq!(
20264 highlighted_edits.highlights[0].1.background_color,
20265 Some(cx.theme().status().created_background)
20266 );
20267 },
20268 )
20269 .await;
20270
20271 // Multiple edits
20272 assert_highlighted_edits(
20273 "Hello, world!",
20274 vec![
20275 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
20276 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
20277 ],
20278 false,
20279 cx,
20280 |highlighted_edits, cx| {
20281 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
20282 assert_eq!(highlighted_edits.highlights.len(), 2);
20283 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
20284 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
20285 assert_eq!(
20286 highlighted_edits.highlights[0].1.background_color,
20287 Some(cx.theme().status().created_background)
20288 );
20289 assert_eq!(
20290 highlighted_edits.highlights[1].1.background_color,
20291 Some(cx.theme().status().created_background)
20292 );
20293 },
20294 )
20295 .await;
20296
20297 // Multiple lines with edits
20298 assert_highlighted_edits(
20299 "First line\nSecond line\nThird line\nFourth line",
20300 vec![
20301 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
20302 (
20303 Point::new(2, 0)..Point::new(2, 10),
20304 "New third line".to_string(),
20305 ),
20306 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
20307 ],
20308 false,
20309 cx,
20310 |highlighted_edits, cx| {
20311 assert_eq!(
20312 highlighted_edits.text,
20313 "Second modified\nNew third line\nFourth updated line"
20314 );
20315 assert_eq!(highlighted_edits.highlights.len(), 3);
20316 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
20317 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
20318 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
20319 for highlight in &highlighted_edits.highlights {
20320 assert_eq!(
20321 highlight.1.background_color,
20322 Some(cx.theme().status().created_background)
20323 );
20324 }
20325 },
20326 )
20327 .await;
20328}
20329
20330#[gpui::test]
20331async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
20332 init_test(cx, |_| {});
20333
20334 // Deletion
20335 assert_highlighted_edits(
20336 "Hello, world!",
20337 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
20338 true,
20339 cx,
20340 |highlighted_edits, cx| {
20341 assert_eq!(highlighted_edits.text, "Hello, world!");
20342 assert_eq!(highlighted_edits.highlights.len(), 1);
20343 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
20344 assert_eq!(
20345 highlighted_edits.highlights[0].1.background_color,
20346 Some(cx.theme().status().deleted_background)
20347 );
20348 },
20349 )
20350 .await;
20351
20352 // Insertion
20353 assert_highlighted_edits(
20354 "Hello, world!",
20355 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
20356 true,
20357 cx,
20358 |highlighted_edits, cx| {
20359 assert_eq!(highlighted_edits.highlights.len(), 1);
20360 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
20361 assert_eq!(
20362 highlighted_edits.highlights[0].1.background_color,
20363 Some(cx.theme().status().created_background)
20364 );
20365 },
20366 )
20367 .await;
20368}
20369
20370async fn assert_highlighted_edits(
20371 text: &str,
20372 edits: Vec<(Range<Point>, String)>,
20373 include_deletions: bool,
20374 cx: &mut TestAppContext,
20375 assertion_fn: impl Fn(HighlightedText, &App),
20376) {
20377 let window = cx.add_window(|window, cx| {
20378 let buffer = MultiBuffer::build_simple(text, cx);
20379 Editor::new(EditorMode::full(), buffer, None, window, cx)
20380 });
20381 let cx = &mut VisualTestContext::from_window(*window, cx);
20382
20383 let (buffer, snapshot) = window
20384 .update(cx, |editor, _window, cx| {
20385 (
20386 editor.buffer().clone(),
20387 editor.buffer().read(cx).snapshot(cx),
20388 )
20389 })
20390 .unwrap();
20391
20392 let edits = edits
20393 .into_iter()
20394 .map(|(range, edit)| {
20395 (
20396 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
20397 edit,
20398 )
20399 })
20400 .collect::<Vec<_>>();
20401
20402 let text_anchor_edits = edits
20403 .clone()
20404 .into_iter()
20405 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
20406 .collect::<Vec<_>>();
20407
20408 let edit_preview = window
20409 .update(cx, |_, _window, cx| {
20410 buffer
20411 .read(cx)
20412 .as_singleton()
20413 .unwrap()
20414 .read(cx)
20415 .preview_edits(text_anchor_edits.into(), cx)
20416 })
20417 .unwrap()
20418 .await;
20419
20420 cx.update(|_window, cx| {
20421 let highlighted_edits = inline_completion_edit_text(
20422 &snapshot.as_singleton().unwrap().2,
20423 &edits,
20424 &edit_preview,
20425 include_deletions,
20426 cx,
20427 );
20428 assertion_fn(highlighted_edits, cx)
20429 });
20430}
20431
20432#[track_caller]
20433fn assert_breakpoint(
20434 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
20435 path: &Arc<Path>,
20436 expected: Vec<(u32, Breakpoint)>,
20437) {
20438 if expected.len() == 0usize {
20439 assert!(!breakpoints.contains_key(path), "{}", path.display());
20440 } else {
20441 let mut breakpoint = breakpoints
20442 .get(path)
20443 .unwrap()
20444 .into_iter()
20445 .map(|breakpoint| {
20446 (
20447 breakpoint.row,
20448 Breakpoint {
20449 message: breakpoint.message.clone(),
20450 state: breakpoint.state,
20451 condition: breakpoint.condition.clone(),
20452 hit_condition: breakpoint.hit_condition.clone(),
20453 },
20454 )
20455 })
20456 .collect::<Vec<_>>();
20457
20458 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
20459
20460 assert_eq!(expected, breakpoint);
20461 }
20462}
20463
20464fn add_log_breakpoint_at_cursor(
20465 editor: &mut Editor,
20466 log_message: &str,
20467 window: &mut Window,
20468 cx: &mut Context<Editor>,
20469) {
20470 let (anchor, bp) = editor
20471 .breakpoints_at_cursors(window, cx)
20472 .first()
20473 .and_then(|(anchor, bp)| {
20474 if let Some(bp) = bp {
20475 Some((*anchor, bp.clone()))
20476 } else {
20477 None
20478 }
20479 })
20480 .unwrap_or_else(|| {
20481 let cursor_position: Point = editor.selections.newest(cx).head();
20482
20483 let breakpoint_position = editor
20484 .snapshot(window, cx)
20485 .display_snapshot
20486 .buffer_snapshot
20487 .anchor_before(Point::new(cursor_position.row, 0));
20488
20489 (breakpoint_position, Breakpoint::new_log(&log_message))
20490 });
20491
20492 editor.edit_breakpoint_at_anchor(
20493 anchor,
20494 bp,
20495 BreakpointEditAction::EditLogMessage(log_message.into()),
20496 cx,
20497 );
20498}
20499
20500#[gpui::test]
20501async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
20502 init_test(cx, |_| {});
20503
20504 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20505 let fs = FakeFs::new(cx.executor());
20506 fs.insert_tree(
20507 path!("/a"),
20508 json!({
20509 "main.rs": sample_text,
20510 }),
20511 )
20512 .await;
20513 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20514 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20515 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20516
20517 let fs = FakeFs::new(cx.executor());
20518 fs.insert_tree(
20519 path!("/a"),
20520 json!({
20521 "main.rs": sample_text,
20522 }),
20523 )
20524 .await;
20525 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20526 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20527 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20528 let worktree_id = workspace
20529 .update(cx, |workspace, _window, cx| {
20530 workspace.project().update(cx, |project, cx| {
20531 project.worktrees(cx).next().unwrap().read(cx).id()
20532 })
20533 })
20534 .unwrap();
20535
20536 let buffer = project
20537 .update(cx, |project, cx| {
20538 project.open_buffer((worktree_id, "main.rs"), cx)
20539 })
20540 .await
20541 .unwrap();
20542
20543 let (editor, cx) = cx.add_window_view(|window, cx| {
20544 Editor::new(
20545 EditorMode::full(),
20546 MultiBuffer::build_from_buffer(buffer, cx),
20547 Some(project.clone()),
20548 window,
20549 cx,
20550 )
20551 });
20552
20553 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20554 let abs_path = project.read_with(cx, |project, cx| {
20555 project
20556 .absolute_path(&project_path, cx)
20557 .map(|path_buf| Arc::from(path_buf.to_owned()))
20558 .unwrap()
20559 });
20560
20561 // assert we can add breakpoint on the first line
20562 editor.update_in(cx, |editor, window, cx| {
20563 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20564 editor.move_to_end(&MoveToEnd, window, cx);
20565 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20566 });
20567
20568 let breakpoints = editor.update(cx, |editor, cx| {
20569 editor
20570 .breakpoint_store()
20571 .as_ref()
20572 .unwrap()
20573 .read(cx)
20574 .all_source_breakpoints(cx)
20575 .clone()
20576 });
20577
20578 assert_eq!(1, breakpoints.len());
20579 assert_breakpoint(
20580 &breakpoints,
20581 &abs_path,
20582 vec![
20583 (0, Breakpoint::new_standard()),
20584 (3, Breakpoint::new_standard()),
20585 ],
20586 );
20587
20588 editor.update_in(cx, |editor, window, cx| {
20589 editor.move_to_beginning(&MoveToBeginning, window, cx);
20590 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20591 });
20592
20593 let breakpoints = editor.update(cx, |editor, cx| {
20594 editor
20595 .breakpoint_store()
20596 .as_ref()
20597 .unwrap()
20598 .read(cx)
20599 .all_source_breakpoints(cx)
20600 .clone()
20601 });
20602
20603 assert_eq!(1, breakpoints.len());
20604 assert_breakpoint(
20605 &breakpoints,
20606 &abs_path,
20607 vec![(3, Breakpoint::new_standard())],
20608 );
20609
20610 editor.update_in(cx, |editor, window, cx| {
20611 editor.move_to_end(&MoveToEnd, window, cx);
20612 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20613 });
20614
20615 let breakpoints = editor.update(cx, |editor, cx| {
20616 editor
20617 .breakpoint_store()
20618 .as_ref()
20619 .unwrap()
20620 .read(cx)
20621 .all_source_breakpoints(cx)
20622 .clone()
20623 });
20624
20625 assert_eq!(0, breakpoints.len());
20626 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20627}
20628
20629#[gpui::test]
20630async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
20631 init_test(cx, |_| {});
20632
20633 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20634
20635 let fs = FakeFs::new(cx.executor());
20636 fs.insert_tree(
20637 path!("/a"),
20638 json!({
20639 "main.rs": sample_text,
20640 }),
20641 )
20642 .await;
20643 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20644 let (workspace, cx) =
20645 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20646
20647 let worktree_id = workspace.update(cx, |workspace, cx| {
20648 workspace.project().update(cx, |project, cx| {
20649 project.worktrees(cx).next().unwrap().read(cx).id()
20650 })
20651 });
20652
20653 let buffer = project
20654 .update(cx, |project, cx| {
20655 project.open_buffer((worktree_id, "main.rs"), cx)
20656 })
20657 .await
20658 .unwrap();
20659
20660 let (editor, cx) = cx.add_window_view(|window, cx| {
20661 Editor::new(
20662 EditorMode::full(),
20663 MultiBuffer::build_from_buffer(buffer, cx),
20664 Some(project.clone()),
20665 window,
20666 cx,
20667 )
20668 });
20669
20670 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20671 let abs_path = project.read_with(cx, |project, cx| {
20672 project
20673 .absolute_path(&project_path, cx)
20674 .map(|path_buf| Arc::from(path_buf.to_owned()))
20675 .unwrap()
20676 });
20677
20678 editor.update_in(cx, |editor, window, cx| {
20679 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20680 });
20681
20682 let breakpoints = editor.update(cx, |editor, cx| {
20683 editor
20684 .breakpoint_store()
20685 .as_ref()
20686 .unwrap()
20687 .read(cx)
20688 .all_source_breakpoints(cx)
20689 .clone()
20690 });
20691
20692 assert_breakpoint(
20693 &breakpoints,
20694 &abs_path,
20695 vec![(0, Breakpoint::new_log("hello world"))],
20696 );
20697
20698 // Removing a log message from a log breakpoint should remove it
20699 editor.update_in(cx, |editor, window, cx| {
20700 add_log_breakpoint_at_cursor(editor, "", window, cx);
20701 });
20702
20703 let breakpoints = editor.update(cx, |editor, cx| {
20704 editor
20705 .breakpoint_store()
20706 .as_ref()
20707 .unwrap()
20708 .read(cx)
20709 .all_source_breakpoints(cx)
20710 .clone()
20711 });
20712
20713 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20714
20715 editor.update_in(cx, |editor, window, cx| {
20716 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20717 editor.move_to_end(&MoveToEnd, window, cx);
20718 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20719 // Not adding a log message to a standard breakpoint shouldn't remove it
20720 add_log_breakpoint_at_cursor(editor, "", window, cx);
20721 });
20722
20723 let breakpoints = editor.update(cx, |editor, cx| {
20724 editor
20725 .breakpoint_store()
20726 .as_ref()
20727 .unwrap()
20728 .read(cx)
20729 .all_source_breakpoints(cx)
20730 .clone()
20731 });
20732
20733 assert_breakpoint(
20734 &breakpoints,
20735 &abs_path,
20736 vec![
20737 (0, Breakpoint::new_standard()),
20738 (3, Breakpoint::new_standard()),
20739 ],
20740 );
20741
20742 editor.update_in(cx, |editor, window, cx| {
20743 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20744 });
20745
20746 let breakpoints = editor.update(cx, |editor, cx| {
20747 editor
20748 .breakpoint_store()
20749 .as_ref()
20750 .unwrap()
20751 .read(cx)
20752 .all_source_breakpoints(cx)
20753 .clone()
20754 });
20755
20756 assert_breakpoint(
20757 &breakpoints,
20758 &abs_path,
20759 vec![
20760 (0, Breakpoint::new_standard()),
20761 (3, Breakpoint::new_log("hello world")),
20762 ],
20763 );
20764
20765 editor.update_in(cx, |editor, window, cx| {
20766 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
20767 });
20768
20769 let breakpoints = editor.update(cx, |editor, cx| {
20770 editor
20771 .breakpoint_store()
20772 .as_ref()
20773 .unwrap()
20774 .read(cx)
20775 .all_source_breakpoints(cx)
20776 .clone()
20777 });
20778
20779 assert_breakpoint(
20780 &breakpoints,
20781 &abs_path,
20782 vec![
20783 (0, Breakpoint::new_standard()),
20784 (3, Breakpoint::new_log("hello Earth!!")),
20785 ],
20786 );
20787}
20788
20789/// This also tests that Editor::breakpoint_at_cursor_head is working properly
20790/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
20791/// or when breakpoints were placed out of order. This tests for a regression too
20792#[gpui::test]
20793async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
20794 init_test(cx, |_| {});
20795
20796 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20797 let fs = FakeFs::new(cx.executor());
20798 fs.insert_tree(
20799 path!("/a"),
20800 json!({
20801 "main.rs": sample_text,
20802 }),
20803 )
20804 .await;
20805 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20806 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20807 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20808
20809 let fs = FakeFs::new(cx.executor());
20810 fs.insert_tree(
20811 path!("/a"),
20812 json!({
20813 "main.rs": sample_text,
20814 }),
20815 )
20816 .await;
20817 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20818 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20819 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20820 let worktree_id = workspace
20821 .update(cx, |workspace, _window, cx| {
20822 workspace.project().update(cx, |project, cx| {
20823 project.worktrees(cx).next().unwrap().read(cx).id()
20824 })
20825 })
20826 .unwrap();
20827
20828 let buffer = project
20829 .update(cx, |project, cx| {
20830 project.open_buffer((worktree_id, "main.rs"), cx)
20831 })
20832 .await
20833 .unwrap();
20834
20835 let (editor, cx) = cx.add_window_view(|window, cx| {
20836 Editor::new(
20837 EditorMode::full(),
20838 MultiBuffer::build_from_buffer(buffer, cx),
20839 Some(project.clone()),
20840 window,
20841 cx,
20842 )
20843 });
20844
20845 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20846 let abs_path = project.read_with(cx, |project, cx| {
20847 project
20848 .absolute_path(&project_path, cx)
20849 .map(|path_buf| Arc::from(path_buf.to_owned()))
20850 .unwrap()
20851 });
20852
20853 // assert we can add breakpoint on the first line
20854 editor.update_in(cx, |editor, window, cx| {
20855 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20856 editor.move_to_end(&MoveToEnd, window, cx);
20857 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20858 editor.move_up(&MoveUp, window, cx);
20859 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20860 });
20861
20862 let breakpoints = editor.update(cx, |editor, cx| {
20863 editor
20864 .breakpoint_store()
20865 .as_ref()
20866 .unwrap()
20867 .read(cx)
20868 .all_source_breakpoints(cx)
20869 .clone()
20870 });
20871
20872 assert_eq!(1, breakpoints.len());
20873 assert_breakpoint(
20874 &breakpoints,
20875 &abs_path,
20876 vec![
20877 (0, Breakpoint::new_standard()),
20878 (2, Breakpoint::new_standard()),
20879 (3, Breakpoint::new_standard()),
20880 ],
20881 );
20882
20883 editor.update_in(cx, |editor, window, cx| {
20884 editor.move_to_beginning(&MoveToBeginning, window, cx);
20885 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20886 editor.move_to_end(&MoveToEnd, window, cx);
20887 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20888 // Disabling a breakpoint that doesn't exist should do nothing
20889 editor.move_up(&MoveUp, window, cx);
20890 editor.move_up(&MoveUp, window, cx);
20891 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20892 });
20893
20894 let breakpoints = editor.update(cx, |editor, cx| {
20895 editor
20896 .breakpoint_store()
20897 .as_ref()
20898 .unwrap()
20899 .read(cx)
20900 .all_source_breakpoints(cx)
20901 .clone()
20902 });
20903
20904 let disable_breakpoint = {
20905 let mut bp = Breakpoint::new_standard();
20906 bp.state = BreakpointState::Disabled;
20907 bp
20908 };
20909
20910 assert_eq!(1, breakpoints.len());
20911 assert_breakpoint(
20912 &breakpoints,
20913 &abs_path,
20914 vec![
20915 (0, disable_breakpoint.clone()),
20916 (2, Breakpoint::new_standard()),
20917 (3, disable_breakpoint.clone()),
20918 ],
20919 );
20920
20921 editor.update_in(cx, |editor, window, cx| {
20922 editor.move_to_beginning(&MoveToBeginning, window, cx);
20923 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
20924 editor.move_to_end(&MoveToEnd, window, cx);
20925 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
20926 editor.move_up(&MoveUp, window, cx);
20927 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20928 });
20929
20930 let breakpoints = editor.update(cx, |editor, cx| {
20931 editor
20932 .breakpoint_store()
20933 .as_ref()
20934 .unwrap()
20935 .read(cx)
20936 .all_source_breakpoints(cx)
20937 .clone()
20938 });
20939
20940 assert_eq!(1, breakpoints.len());
20941 assert_breakpoint(
20942 &breakpoints,
20943 &abs_path,
20944 vec![
20945 (0, Breakpoint::new_standard()),
20946 (2, disable_breakpoint),
20947 (3, Breakpoint::new_standard()),
20948 ],
20949 );
20950}
20951
20952#[gpui::test]
20953async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
20954 init_test(cx, |_| {});
20955 let capabilities = lsp::ServerCapabilities {
20956 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
20957 prepare_provider: Some(true),
20958 work_done_progress_options: Default::default(),
20959 })),
20960 ..Default::default()
20961 };
20962 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
20963
20964 cx.set_state(indoc! {"
20965 struct Fˇoo {}
20966 "});
20967
20968 cx.update_editor(|editor, _, cx| {
20969 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
20970 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
20971 editor.highlight_background::<DocumentHighlightRead>(
20972 &[highlight_range],
20973 |theme| theme.colors().editor_document_highlight_read_background,
20974 cx,
20975 );
20976 });
20977
20978 let mut prepare_rename_handler = cx
20979 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
20980 move |_, _, _| async move {
20981 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
20982 start: lsp::Position {
20983 line: 0,
20984 character: 7,
20985 },
20986 end: lsp::Position {
20987 line: 0,
20988 character: 10,
20989 },
20990 })))
20991 },
20992 );
20993 let prepare_rename_task = cx
20994 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
20995 .expect("Prepare rename was not started");
20996 prepare_rename_handler.next().await.unwrap();
20997 prepare_rename_task.await.expect("Prepare rename failed");
20998
20999 let mut rename_handler =
21000 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
21001 let edit = lsp::TextEdit {
21002 range: lsp::Range {
21003 start: lsp::Position {
21004 line: 0,
21005 character: 7,
21006 },
21007 end: lsp::Position {
21008 line: 0,
21009 character: 10,
21010 },
21011 },
21012 new_text: "FooRenamed".to_string(),
21013 };
21014 Ok(Some(lsp::WorkspaceEdit::new(
21015 // Specify the same edit twice
21016 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
21017 )))
21018 });
21019 let rename_task = cx
21020 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
21021 .expect("Confirm rename was not started");
21022 rename_handler.next().await.unwrap();
21023 rename_task.await.expect("Confirm rename failed");
21024 cx.run_until_parked();
21025
21026 // Despite two edits, only one is actually applied as those are identical
21027 cx.assert_editor_state(indoc! {"
21028 struct FooRenamedˇ {}
21029 "});
21030}
21031
21032#[gpui::test]
21033async fn test_rename_without_prepare(cx: &mut TestAppContext) {
21034 init_test(cx, |_| {});
21035 // These capabilities indicate that the server does not support prepare rename.
21036 let capabilities = lsp::ServerCapabilities {
21037 rename_provider: Some(lsp::OneOf::Left(true)),
21038 ..Default::default()
21039 };
21040 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
21041
21042 cx.set_state(indoc! {"
21043 struct Fˇoo {}
21044 "});
21045
21046 cx.update_editor(|editor, _window, cx| {
21047 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
21048 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
21049 editor.highlight_background::<DocumentHighlightRead>(
21050 &[highlight_range],
21051 |theme| theme.colors().editor_document_highlight_read_background,
21052 cx,
21053 );
21054 });
21055
21056 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
21057 .expect("Prepare rename was not started")
21058 .await
21059 .expect("Prepare rename failed");
21060
21061 let mut rename_handler =
21062 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
21063 let edit = lsp::TextEdit {
21064 range: lsp::Range {
21065 start: lsp::Position {
21066 line: 0,
21067 character: 7,
21068 },
21069 end: lsp::Position {
21070 line: 0,
21071 character: 10,
21072 },
21073 },
21074 new_text: "FooRenamed".to_string(),
21075 };
21076 Ok(Some(lsp::WorkspaceEdit::new(
21077 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
21078 )))
21079 });
21080 let rename_task = cx
21081 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
21082 .expect("Confirm rename was not started");
21083 rename_handler.next().await.unwrap();
21084 rename_task.await.expect("Confirm rename failed");
21085 cx.run_until_parked();
21086
21087 // Correct range is renamed, as `surrounding_word` is used to find it.
21088 cx.assert_editor_state(indoc! {"
21089 struct FooRenamedˇ {}
21090 "});
21091}
21092
21093#[gpui::test]
21094async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
21095 init_test(cx, |_| {});
21096 let mut cx = EditorTestContext::new(cx).await;
21097
21098 let language = Arc::new(
21099 Language::new(
21100 LanguageConfig::default(),
21101 Some(tree_sitter_html::LANGUAGE.into()),
21102 )
21103 .with_brackets_query(
21104 r#"
21105 ("<" @open "/>" @close)
21106 ("</" @open ">" @close)
21107 ("<" @open ">" @close)
21108 ("\"" @open "\"" @close)
21109 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
21110 "#,
21111 )
21112 .unwrap(),
21113 );
21114 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21115
21116 cx.set_state(indoc! {"
21117 <span>ˇ</span>
21118 "});
21119 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21120 cx.assert_editor_state(indoc! {"
21121 <span>
21122 ˇ
21123 </span>
21124 "});
21125
21126 cx.set_state(indoc! {"
21127 <span><span></span>ˇ</span>
21128 "});
21129 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21130 cx.assert_editor_state(indoc! {"
21131 <span><span></span>
21132 ˇ</span>
21133 "});
21134
21135 cx.set_state(indoc! {"
21136 <span>ˇ
21137 </span>
21138 "});
21139 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21140 cx.assert_editor_state(indoc! {"
21141 <span>
21142 ˇ
21143 </span>
21144 "});
21145}
21146
21147#[gpui::test(iterations = 10)]
21148async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
21149 init_test(cx, |_| {});
21150
21151 let fs = FakeFs::new(cx.executor());
21152 fs.insert_tree(
21153 path!("/dir"),
21154 json!({
21155 "a.ts": "a",
21156 }),
21157 )
21158 .await;
21159
21160 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
21161 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21162 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21163
21164 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21165 language_registry.add(Arc::new(Language::new(
21166 LanguageConfig {
21167 name: "TypeScript".into(),
21168 matcher: LanguageMatcher {
21169 path_suffixes: vec!["ts".to_string()],
21170 ..Default::default()
21171 },
21172 ..Default::default()
21173 },
21174 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
21175 )));
21176 let mut fake_language_servers = language_registry.register_fake_lsp(
21177 "TypeScript",
21178 FakeLspAdapter {
21179 capabilities: lsp::ServerCapabilities {
21180 code_lens_provider: Some(lsp::CodeLensOptions {
21181 resolve_provider: Some(true),
21182 }),
21183 execute_command_provider: Some(lsp::ExecuteCommandOptions {
21184 commands: vec!["_the/command".to_string()],
21185 ..lsp::ExecuteCommandOptions::default()
21186 }),
21187 ..lsp::ServerCapabilities::default()
21188 },
21189 ..FakeLspAdapter::default()
21190 },
21191 );
21192
21193 let (buffer, _handle) = project
21194 .update(cx, |p, cx| {
21195 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
21196 })
21197 .await
21198 .unwrap();
21199 cx.executor().run_until_parked();
21200
21201 let fake_server = fake_language_servers.next().await.unwrap();
21202
21203 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
21204 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
21205 drop(buffer_snapshot);
21206 let actions = cx
21207 .update_window(*workspace, |_, window, cx| {
21208 project.code_actions(&buffer, anchor..anchor, window, cx)
21209 })
21210 .unwrap();
21211
21212 fake_server
21213 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
21214 Ok(Some(vec![
21215 lsp::CodeLens {
21216 range: lsp::Range::default(),
21217 command: Some(lsp::Command {
21218 title: "Code lens command".to_owned(),
21219 command: "_the/command".to_owned(),
21220 arguments: None,
21221 }),
21222 data: None,
21223 },
21224 lsp::CodeLens {
21225 range: lsp::Range::default(),
21226 command: Some(lsp::Command {
21227 title: "Command not in capabilities".to_owned(),
21228 command: "not in capabilities".to_owned(),
21229 arguments: None,
21230 }),
21231 data: None,
21232 },
21233 lsp::CodeLens {
21234 range: lsp::Range {
21235 start: lsp::Position {
21236 line: 1,
21237 character: 1,
21238 },
21239 end: lsp::Position {
21240 line: 1,
21241 character: 1,
21242 },
21243 },
21244 command: Some(lsp::Command {
21245 title: "Command not in range".to_owned(),
21246 command: "_the/command".to_owned(),
21247 arguments: None,
21248 }),
21249 data: None,
21250 },
21251 ]))
21252 })
21253 .next()
21254 .await;
21255
21256 let actions = actions.await.unwrap();
21257 assert_eq!(
21258 actions.len(),
21259 1,
21260 "Should have only one valid action for the 0..0 range"
21261 );
21262 let action = actions[0].clone();
21263 let apply = project.update(cx, |project, cx| {
21264 project.apply_code_action(buffer.clone(), action, true, cx)
21265 });
21266
21267 // Resolving the code action does not populate its edits. In absence of
21268 // edits, we must execute the given command.
21269 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
21270 |mut lens, _| async move {
21271 let lens_command = lens.command.as_mut().expect("should have a command");
21272 assert_eq!(lens_command.title, "Code lens command");
21273 lens_command.arguments = Some(vec![json!("the-argument")]);
21274 Ok(lens)
21275 },
21276 );
21277
21278 // While executing the command, the language server sends the editor
21279 // a `workspaceEdit` request.
21280 fake_server
21281 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
21282 let fake = fake_server.clone();
21283 move |params, _| {
21284 assert_eq!(params.command, "_the/command");
21285 let fake = fake.clone();
21286 async move {
21287 fake.server
21288 .request::<lsp::request::ApplyWorkspaceEdit>(
21289 lsp::ApplyWorkspaceEditParams {
21290 label: None,
21291 edit: lsp::WorkspaceEdit {
21292 changes: Some(
21293 [(
21294 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
21295 vec![lsp::TextEdit {
21296 range: lsp::Range::new(
21297 lsp::Position::new(0, 0),
21298 lsp::Position::new(0, 0),
21299 ),
21300 new_text: "X".into(),
21301 }],
21302 )]
21303 .into_iter()
21304 .collect(),
21305 ),
21306 ..Default::default()
21307 },
21308 },
21309 )
21310 .await
21311 .into_response()
21312 .unwrap();
21313 Ok(Some(json!(null)))
21314 }
21315 }
21316 })
21317 .next()
21318 .await;
21319
21320 // Applying the code lens command returns a project transaction containing the edits
21321 // sent by the language server in its `workspaceEdit` request.
21322 let transaction = apply.await.unwrap();
21323 assert!(transaction.0.contains_key(&buffer));
21324 buffer.update(cx, |buffer, cx| {
21325 assert_eq!(buffer.text(), "Xa");
21326 buffer.undo(cx);
21327 assert_eq!(buffer.text(), "a");
21328 });
21329}
21330
21331#[gpui::test]
21332async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
21333 init_test(cx, |_| {});
21334
21335 let fs = FakeFs::new(cx.executor());
21336 let main_text = r#"fn main() {
21337println!("1");
21338println!("2");
21339println!("3");
21340println!("4");
21341println!("5");
21342}"#;
21343 let lib_text = "mod foo {}";
21344 fs.insert_tree(
21345 path!("/a"),
21346 json!({
21347 "lib.rs": lib_text,
21348 "main.rs": main_text,
21349 }),
21350 )
21351 .await;
21352
21353 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21354 let (workspace, cx) =
21355 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21356 let worktree_id = workspace.update(cx, |workspace, cx| {
21357 workspace.project().update(cx, |project, cx| {
21358 project.worktrees(cx).next().unwrap().read(cx).id()
21359 })
21360 });
21361
21362 let expected_ranges = vec![
21363 Point::new(0, 0)..Point::new(0, 0),
21364 Point::new(1, 0)..Point::new(1, 1),
21365 Point::new(2, 0)..Point::new(2, 2),
21366 Point::new(3, 0)..Point::new(3, 3),
21367 ];
21368
21369 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21370 let editor_1 = workspace
21371 .update_in(cx, |workspace, window, cx| {
21372 workspace.open_path(
21373 (worktree_id, "main.rs"),
21374 Some(pane_1.downgrade()),
21375 true,
21376 window,
21377 cx,
21378 )
21379 })
21380 .unwrap()
21381 .await
21382 .downcast::<Editor>()
21383 .unwrap();
21384 pane_1.update(cx, |pane, cx| {
21385 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21386 open_editor.update(cx, |editor, cx| {
21387 assert_eq!(
21388 editor.display_text(cx),
21389 main_text,
21390 "Original main.rs text on initial open",
21391 );
21392 assert_eq!(
21393 editor
21394 .selections
21395 .all::<Point>(cx)
21396 .into_iter()
21397 .map(|s| s.range())
21398 .collect::<Vec<_>>(),
21399 vec![Point::zero()..Point::zero()],
21400 "Default selections on initial open",
21401 );
21402 })
21403 });
21404 editor_1.update_in(cx, |editor, window, cx| {
21405 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21406 s.select_ranges(expected_ranges.clone());
21407 });
21408 });
21409
21410 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
21411 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
21412 });
21413 let editor_2 = workspace
21414 .update_in(cx, |workspace, window, cx| {
21415 workspace.open_path(
21416 (worktree_id, "main.rs"),
21417 Some(pane_2.downgrade()),
21418 true,
21419 window,
21420 cx,
21421 )
21422 })
21423 .unwrap()
21424 .await
21425 .downcast::<Editor>()
21426 .unwrap();
21427 pane_2.update(cx, |pane, cx| {
21428 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21429 open_editor.update(cx, |editor, cx| {
21430 assert_eq!(
21431 editor.display_text(cx),
21432 main_text,
21433 "Original main.rs text on initial open in another panel",
21434 );
21435 assert_eq!(
21436 editor
21437 .selections
21438 .all::<Point>(cx)
21439 .into_iter()
21440 .map(|s| s.range())
21441 .collect::<Vec<_>>(),
21442 vec![Point::zero()..Point::zero()],
21443 "Default selections on initial open in another panel",
21444 );
21445 })
21446 });
21447
21448 editor_2.update_in(cx, |editor, window, cx| {
21449 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
21450 });
21451
21452 let _other_editor_1 = workspace
21453 .update_in(cx, |workspace, window, cx| {
21454 workspace.open_path(
21455 (worktree_id, "lib.rs"),
21456 Some(pane_1.downgrade()),
21457 true,
21458 window,
21459 cx,
21460 )
21461 })
21462 .unwrap()
21463 .await
21464 .downcast::<Editor>()
21465 .unwrap();
21466 pane_1
21467 .update_in(cx, |pane, window, cx| {
21468 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
21469 })
21470 .await
21471 .unwrap();
21472 drop(editor_1);
21473 pane_1.update(cx, |pane, cx| {
21474 pane.active_item()
21475 .unwrap()
21476 .downcast::<Editor>()
21477 .unwrap()
21478 .update(cx, |editor, cx| {
21479 assert_eq!(
21480 editor.display_text(cx),
21481 lib_text,
21482 "Other file should be open and active",
21483 );
21484 });
21485 assert_eq!(pane.items().count(), 1, "No other editors should be open");
21486 });
21487
21488 let _other_editor_2 = workspace
21489 .update_in(cx, |workspace, window, cx| {
21490 workspace.open_path(
21491 (worktree_id, "lib.rs"),
21492 Some(pane_2.downgrade()),
21493 true,
21494 window,
21495 cx,
21496 )
21497 })
21498 .unwrap()
21499 .await
21500 .downcast::<Editor>()
21501 .unwrap();
21502 pane_2
21503 .update_in(cx, |pane, window, cx| {
21504 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
21505 })
21506 .await
21507 .unwrap();
21508 drop(editor_2);
21509 pane_2.update(cx, |pane, cx| {
21510 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21511 open_editor.update(cx, |editor, cx| {
21512 assert_eq!(
21513 editor.display_text(cx),
21514 lib_text,
21515 "Other file should be open and active in another panel too",
21516 );
21517 });
21518 assert_eq!(
21519 pane.items().count(),
21520 1,
21521 "No other editors should be open in another pane",
21522 );
21523 });
21524
21525 let _editor_1_reopened = workspace
21526 .update_in(cx, |workspace, window, cx| {
21527 workspace.open_path(
21528 (worktree_id, "main.rs"),
21529 Some(pane_1.downgrade()),
21530 true,
21531 window,
21532 cx,
21533 )
21534 })
21535 .unwrap()
21536 .await
21537 .downcast::<Editor>()
21538 .unwrap();
21539 let _editor_2_reopened = workspace
21540 .update_in(cx, |workspace, window, cx| {
21541 workspace.open_path(
21542 (worktree_id, "main.rs"),
21543 Some(pane_2.downgrade()),
21544 true,
21545 window,
21546 cx,
21547 )
21548 })
21549 .unwrap()
21550 .await
21551 .downcast::<Editor>()
21552 .unwrap();
21553 pane_1.update(cx, |pane, cx| {
21554 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21555 open_editor.update(cx, |editor, cx| {
21556 assert_eq!(
21557 editor.display_text(cx),
21558 main_text,
21559 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
21560 );
21561 assert_eq!(
21562 editor
21563 .selections
21564 .all::<Point>(cx)
21565 .into_iter()
21566 .map(|s| s.range())
21567 .collect::<Vec<_>>(),
21568 expected_ranges,
21569 "Previous editor in the 1st panel had selections and should get them restored on reopen",
21570 );
21571 })
21572 });
21573 pane_2.update(cx, |pane, cx| {
21574 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21575 open_editor.update(cx, |editor, cx| {
21576 assert_eq!(
21577 editor.display_text(cx),
21578 r#"fn main() {
21579⋯rintln!("1");
21580⋯intln!("2");
21581⋯ntln!("3");
21582println!("4");
21583println!("5");
21584}"#,
21585 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
21586 );
21587 assert_eq!(
21588 editor
21589 .selections
21590 .all::<Point>(cx)
21591 .into_iter()
21592 .map(|s| s.range())
21593 .collect::<Vec<_>>(),
21594 vec![Point::zero()..Point::zero()],
21595 "Previous editor in the 2nd pane had no selections changed hence should restore none",
21596 );
21597 })
21598 });
21599}
21600
21601#[gpui::test]
21602async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
21603 init_test(cx, |_| {});
21604
21605 let fs = FakeFs::new(cx.executor());
21606 let main_text = r#"fn main() {
21607println!("1");
21608println!("2");
21609println!("3");
21610println!("4");
21611println!("5");
21612}"#;
21613 let lib_text = "mod foo {}";
21614 fs.insert_tree(
21615 path!("/a"),
21616 json!({
21617 "lib.rs": lib_text,
21618 "main.rs": main_text,
21619 }),
21620 )
21621 .await;
21622
21623 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21624 let (workspace, cx) =
21625 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21626 let worktree_id = workspace.update(cx, |workspace, cx| {
21627 workspace.project().update(cx, |project, cx| {
21628 project.worktrees(cx).next().unwrap().read(cx).id()
21629 })
21630 });
21631
21632 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21633 let editor = workspace
21634 .update_in(cx, |workspace, window, cx| {
21635 workspace.open_path(
21636 (worktree_id, "main.rs"),
21637 Some(pane.downgrade()),
21638 true,
21639 window,
21640 cx,
21641 )
21642 })
21643 .unwrap()
21644 .await
21645 .downcast::<Editor>()
21646 .unwrap();
21647 pane.update(cx, |pane, cx| {
21648 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21649 open_editor.update(cx, |editor, cx| {
21650 assert_eq!(
21651 editor.display_text(cx),
21652 main_text,
21653 "Original main.rs text on initial open",
21654 );
21655 })
21656 });
21657 editor.update_in(cx, |editor, window, cx| {
21658 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
21659 });
21660
21661 cx.update_global(|store: &mut SettingsStore, cx| {
21662 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21663 s.restore_on_file_reopen = Some(false);
21664 });
21665 });
21666 editor.update_in(cx, |editor, window, cx| {
21667 editor.fold_ranges(
21668 vec![
21669 Point::new(1, 0)..Point::new(1, 1),
21670 Point::new(2, 0)..Point::new(2, 2),
21671 Point::new(3, 0)..Point::new(3, 3),
21672 ],
21673 false,
21674 window,
21675 cx,
21676 );
21677 });
21678 pane.update_in(cx, |pane, window, cx| {
21679 pane.close_all_items(&CloseAllItems::default(), window, cx)
21680 })
21681 .await
21682 .unwrap();
21683 pane.update(cx, |pane, _| {
21684 assert!(pane.active_item().is_none());
21685 });
21686 cx.update_global(|store: &mut SettingsStore, cx| {
21687 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21688 s.restore_on_file_reopen = Some(true);
21689 });
21690 });
21691
21692 let _editor_reopened = workspace
21693 .update_in(cx, |workspace, window, cx| {
21694 workspace.open_path(
21695 (worktree_id, "main.rs"),
21696 Some(pane.downgrade()),
21697 true,
21698 window,
21699 cx,
21700 )
21701 })
21702 .unwrap()
21703 .await
21704 .downcast::<Editor>()
21705 .unwrap();
21706 pane.update(cx, |pane, cx| {
21707 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21708 open_editor.update(cx, |editor, cx| {
21709 assert_eq!(
21710 editor.display_text(cx),
21711 main_text,
21712 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
21713 );
21714 })
21715 });
21716}
21717
21718#[gpui::test]
21719async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
21720 struct EmptyModalView {
21721 focus_handle: gpui::FocusHandle,
21722 }
21723 impl EventEmitter<DismissEvent> for EmptyModalView {}
21724 impl Render for EmptyModalView {
21725 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
21726 div()
21727 }
21728 }
21729 impl Focusable for EmptyModalView {
21730 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
21731 self.focus_handle.clone()
21732 }
21733 }
21734 impl workspace::ModalView for EmptyModalView {}
21735 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
21736 EmptyModalView {
21737 focus_handle: cx.focus_handle(),
21738 }
21739 }
21740
21741 init_test(cx, |_| {});
21742
21743 let fs = FakeFs::new(cx.executor());
21744 let project = Project::test(fs, [], cx).await;
21745 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21746 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
21747 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21748 let editor = cx.new_window_entity(|window, cx| {
21749 Editor::new(
21750 EditorMode::full(),
21751 buffer,
21752 Some(project.clone()),
21753 window,
21754 cx,
21755 )
21756 });
21757 workspace
21758 .update(cx, |workspace, window, cx| {
21759 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
21760 })
21761 .unwrap();
21762 editor.update_in(cx, |editor, window, cx| {
21763 editor.open_context_menu(&OpenContextMenu, window, cx);
21764 assert!(editor.mouse_context_menu.is_some());
21765 });
21766 workspace
21767 .update(cx, |workspace, window, cx| {
21768 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
21769 })
21770 .unwrap();
21771 cx.read(|cx| {
21772 assert!(editor.read(cx).mouse_context_menu.is_none());
21773 });
21774}
21775
21776#[gpui::test]
21777async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
21778 init_test(cx, |_| {});
21779
21780 let fs = FakeFs::new(cx.executor());
21781 fs.insert_file(path!("/file.html"), Default::default())
21782 .await;
21783
21784 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
21785
21786 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21787 let html_language = Arc::new(Language::new(
21788 LanguageConfig {
21789 name: "HTML".into(),
21790 matcher: LanguageMatcher {
21791 path_suffixes: vec!["html".to_string()],
21792 ..LanguageMatcher::default()
21793 },
21794 brackets: BracketPairConfig {
21795 pairs: vec![BracketPair {
21796 start: "<".into(),
21797 end: ">".into(),
21798 close: true,
21799 ..Default::default()
21800 }],
21801 ..Default::default()
21802 },
21803 ..Default::default()
21804 },
21805 Some(tree_sitter_html::LANGUAGE.into()),
21806 ));
21807 language_registry.add(html_language);
21808 let mut fake_servers = language_registry.register_fake_lsp(
21809 "HTML",
21810 FakeLspAdapter {
21811 capabilities: lsp::ServerCapabilities {
21812 completion_provider: Some(lsp::CompletionOptions {
21813 resolve_provider: Some(true),
21814 ..Default::default()
21815 }),
21816 ..Default::default()
21817 },
21818 ..Default::default()
21819 },
21820 );
21821
21822 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21823 let cx = &mut VisualTestContext::from_window(*workspace, cx);
21824
21825 let worktree_id = workspace
21826 .update(cx, |workspace, _window, cx| {
21827 workspace.project().update(cx, |project, cx| {
21828 project.worktrees(cx).next().unwrap().read(cx).id()
21829 })
21830 })
21831 .unwrap();
21832 project
21833 .update(cx, |project, cx| {
21834 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
21835 })
21836 .await
21837 .unwrap();
21838 let editor = workspace
21839 .update(cx, |workspace, window, cx| {
21840 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
21841 })
21842 .unwrap()
21843 .await
21844 .unwrap()
21845 .downcast::<Editor>()
21846 .unwrap();
21847
21848 let fake_server = fake_servers.next().await.unwrap();
21849 editor.update_in(cx, |editor, window, cx| {
21850 editor.set_text("<ad></ad>", window, cx);
21851 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
21852 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
21853 });
21854 let Some((buffer, _)) = editor
21855 .buffer
21856 .read(cx)
21857 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
21858 else {
21859 panic!("Failed to get buffer for selection position");
21860 };
21861 let buffer = buffer.read(cx);
21862 let buffer_id = buffer.remote_id();
21863 let opening_range =
21864 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
21865 let closing_range =
21866 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
21867 let mut linked_ranges = HashMap::default();
21868 linked_ranges.insert(
21869 buffer_id,
21870 vec![(opening_range.clone(), vec![closing_range.clone()])],
21871 );
21872 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
21873 });
21874 let mut completion_handle =
21875 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
21876 Ok(Some(lsp::CompletionResponse::Array(vec![
21877 lsp::CompletionItem {
21878 label: "head".to_string(),
21879 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
21880 lsp::InsertReplaceEdit {
21881 new_text: "head".to_string(),
21882 insert: lsp::Range::new(
21883 lsp::Position::new(0, 1),
21884 lsp::Position::new(0, 3),
21885 ),
21886 replace: lsp::Range::new(
21887 lsp::Position::new(0, 1),
21888 lsp::Position::new(0, 3),
21889 ),
21890 },
21891 )),
21892 ..Default::default()
21893 },
21894 ])))
21895 });
21896 editor.update_in(cx, |editor, window, cx| {
21897 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
21898 });
21899 cx.run_until_parked();
21900 completion_handle.next().await.unwrap();
21901 editor.update(cx, |editor, _| {
21902 assert!(
21903 editor.context_menu_visible(),
21904 "Completion menu should be visible"
21905 );
21906 });
21907 editor.update_in(cx, |editor, window, cx| {
21908 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
21909 });
21910 cx.executor().run_until_parked();
21911 editor.update(cx, |editor, cx| {
21912 assert_eq!(editor.text(cx), "<head></head>");
21913 });
21914}
21915
21916#[gpui::test]
21917async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
21918 init_test(cx, |_| {});
21919
21920 let fs = FakeFs::new(cx.executor());
21921 fs.insert_tree(
21922 path!("/root"),
21923 json!({
21924 "a": {
21925 "main.rs": "fn main() {}",
21926 },
21927 "foo": {
21928 "bar": {
21929 "external_file.rs": "pub mod external {}",
21930 }
21931 }
21932 }),
21933 )
21934 .await;
21935
21936 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
21937 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21938 language_registry.add(rust_lang());
21939 let _fake_servers = language_registry.register_fake_lsp(
21940 "Rust",
21941 FakeLspAdapter {
21942 ..FakeLspAdapter::default()
21943 },
21944 );
21945 let (workspace, cx) =
21946 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21947 let worktree_id = workspace.update(cx, |workspace, cx| {
21948 workspace.project().update(cx, |project, cx| {
21949 project.worktrees(cx).next().unwrap().read(cx).id()
21950 })
21951 });
21952
21953 let assert_language_servers_count =
21954 |expected: usize, context: &str, cx: &mut VisualTestContext| {
21955 project.update(cx, |project, cx| {
21956 let current = project
21957 .lsp_store()
21958 .read(cx)
21959 .as_local()
21960 .unwrap()
21961 .language_servers
21962 .len();
21963 assert_eq!(expected, current, "{context}");
21964 });
21965 };
21966
21967 assert_language_servers_count(
21968 0,
21969 "No servers should be running before any file is open",
21970 cx,
21971 );
21972 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21973 let main_editor = workspace
21974 .update_in(cx, |workspace, window, cx| {
21975 workspace.open_path(
21976 (worktree_id, "main.rs"),
21977 Some(pane.downgrade()),
21978 true,
21979 window,
21980 cx,
21981 )
21982 })
21983 .unwrap()
21984 .await
21985 .downcast::<Editor>()
21986 .unwrap();
21987 pane.update(cx, |pane, cx| {
21988 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21989 open_editor.update(cx, |editor, cx| {
21990 assert_eq!(
21991 editor.display_text(cx),
21992 "fn main() {}",
21993 "Original main.rs text on initial open",
21994 );
21995 });
21996 assert_eq!(open_editor, main_editor);
21997 });
21998 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
21999
22000 let external_editor = workspace
22001 .update_in(cx, |workspace, window, cx| {
22002 workspace.open_abs_path(
22003 PathBuf::from("/root/foo/bar/external_file.rs"),
22004 OpenOptions::default(),
22005 window,
22006 cx,
22007 )
22008 })
22009 .await
22010 .expect("opening external file")
22011 .downcast::<Editor>()
22012 .expect("downcasted external file's open element to editor");
22013 pane.update(cx, |pane, cx| {
22014 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22015 open_editor.update(cx, |editor, cx| {
22016 assert_eq!(
22017 editor.display_text(cx),
22018 "pub mod external {}",
22019 "External file is open now",
22020 );
22021 });
22022 assert_eq!(open_editor, external_editor);
22023 });
22024 assert_language_servers_count(
22025 1,
22026 "Second, external, *.rs file should join the existing server",
22027 cx,
22028 );
22029
22030 pane.update_in(cx, |pane, window, cx| {
22031 pane.close_active_item(&CloseActiveItem::default(), window, cx)
22032 })
22033 .await
22034 .unwrap();
22035 pane.update_in(cx, |pane, window, cx| {
22036 pane.navigate_backward(window, cx);
22037 });
22038 cx.run_until_parked();
22039 pane.update(cx, |pane, cx| {
22040 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22041 open_editor.update(cx, |editor, cx| {
22042 assert_eq!(
22043 editor.display_text(cx),
22044 "pub mod external {}",
22045 "External file is open now",
22046 );
22047 });
22048 });
22049 assert_language_servers_count(
22050 1,
22051 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
22052 cx,
22053 );
22054
22055 cx.update(|_, cx| {
22056 workspace::reload(&workspace::Reload::default(), cx);
22057 });
22058 assert_language_servers_count(
22059 1,
22060 "After reloading the worktree with local and external files opened, only one project should be started",
22061 cx,
22062 );
22063}
22064
22065#[gpui::test]
22066async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
22067 init_test(cx, |_| {});
22068
22069 let mut cx = EditorTestContext::new(cx).await;
22070 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22071 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22072
22073 // test cursor move to start of each line on tab
22074 // for `if`, `elif`, `else`, `while`, `with` and `for`
22075 cx.set_state(indoc! {"
22076 def main():
22077 ˇ for item in items:
22078 ˇ while item.active:
22079 ˇ if item.value > 10:
22080 ˇ continue
22081 ˇ elif item.value < 0:
22082 ˇ break
22083 ˇ else:
22084 ˇ with item.context() as ctx:
22085 ˇ yield count
22086 ˇ else:
22087 ˇ log('while else')
22088 ˇ else:
22089 ˇ log('for else')
22090 "});
22091 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22092 cx.assert_editor_state(indoc! {"
22093 def main():
22094 ˇfor item in items:
22095 ˇwhile item.active:
22096 ˇif item.value > 10:
22097 ˇcontinue
22098 ˇelif item.value < 0:
22099 ˇbreak
22100 ˇelse:
22101 ˇwith item.context() as ctx:
22102 ˇyield count
22103 ˇelse:
22104 ˇlog('while else')
22105 ˇelse:
22106 ˇlog('for else')
22107 "});
22108 // test relative indent is preserved when tab
22109 // for `if`, `elif`, `else`, `while`, `with` and `for`
22110 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22111 cx.assert_editor_state(indoc! {"
22112 def main():
22113 ˇfor item in items:
22114 ˇwhile item.active:
22115 ˇif item.value > 10:
22116 ˇcontinue
22117 ˇelif item.value < 0:
22118 ˇbreak
22119 ˇelse:
22120 ˇwith item.context() as ctx:
22121 ˇyield count
22122 ˇelse:
22123 ˇlog('while else')
22124 ˇelse:
22125 ˇlog('for else')
22126 "});
22127
22128 // test cursor move to start of each line on tab
22129 // for `try`, `except`, `else`, `finally`, `match` and `def`
22130 cx.set_state(indoc! {"
22131 def main():
22132 ˇ try:
22133 ˇ fetch()
22134 ˇ except ValueError:
22135 ˇ handle_error()
22136 ˇ else:
22137 ˇ match value:
22138 ˇ case _:
22139 ˇ finally:
22140 ˇ def status():
22141 ˇ return 0
22142 "});
22143 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22144 cx.assert_editor_state(indoc! {"
22145 def main():
22146 ˇtry:
22147 ˇfetch()
22148 ˇexcept ValueError:
22149 ˇhandle_error()
22150 ˇelse:
22151 ˇmatch value:
22152 ˇcase _:
22153 ˇfinally:
22154 ˇdef status():
22155 ˇreturn 0
22156 "});
22157 // test relative indent is preserved when tab
22158 // for `try`, `except`, `else`, `finally`, `match` and `def`
22159 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22160 cx.assert_editor_state(indoc! {"
22161 def main():
22162 ˇtry:
22163 ˇfetch()
22164 ˇexcept ValueError:
22165 ˇhandle_error()
22166 ˇelse:
22167 ˇmatch value:
22168 ˇcase _:
22169 ˇfinally:
22170 ˇdef status():
22171 ˇreturn 0
22172 "});
22173}
22174
22175#[gpui::test]
22176async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
22177 init_test(cx, |_| {});
22178
22179 let mut cx = EditorTestContext::new(cx).await;
22180 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22181 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22182
22183 // test `else` auto outdents when typed inside `if` block
22184 cx.set_state(indoc! {"
22185 def main():
22186 if i == 2:
22187 return
22188 ˇ
22189 "});
22190 cx.update_editor(|editor, window, cx| {
22191 editor.handle_input("else:", window, cx);
22192 });
22193 cx.assert_editor_state(indoc! {"
22194 def main():
22195 if i == 2:
22196 return
22197 else:ˇ
22198 "});
22199
22200 // test `except` auto outdents when typed inside `try` block
22201 cx.set_state(indoc! {"
22202 def main():
22203 try:
22204 i = 2
22205 ˇ
22206 "});
22207 cx.update_editor(|editor, window, cx| {
22208 editor.handle_input("except:", window, cx);
22209 });
22210 cx.assert_editor_state(indoc! {"
22211 def main():
22212 try:
22213 i = 2
22214 except:ˇ
22215 "});
22216
22217 // test `else` auto outdents when typed inside `except` block
22218 cx.set_state(indoc! {"
22219 def main():
22220 try:
22221 i = 2
22222 except:
22223 j = 2
22224 ˇ
22225 "});
22226 cx.update_editor(|editor, window, cx| {
22227 editor.handle_input("else:", window, cx);
22228 });
22229 cx.assert_editor_state(indoc! {"
22230 def main():
22231 try:
22232 i = 2
22233 except:
22234 j = 2
22235 else:ˇ
22236 "});
22237
22238 // test `finally` auto outdents when typed inside `else` block
22239 cx.set_state(indoc! {"
22240 def main():
22241 try:
22242 i = 2
22243 except:
22244 j = 2
22245 else:
22246 k = 2
22247 ˇ
22248 "});
22249 cx.update_editor(|editor, window, cx| {
22250 editor.handle_input("finally:", window, cx);
22251 });
22252 cx.assert_editor_state(indoc! {"
22253 def main():
22254 try:
22255 i = 2
22256 except:
22257 j = 2
22258 else:
22259 k = 2
22260 finally:ˇ
22261 "});
22262
22263 // test `else` does not outdents when typed inside `except` block right after for block
22264 cx.set_state(indoc! {"
22265 def main():
22266 try:
22267 i = 2
22268 except:
22269 for i in range(n):
22270 pass
22271 ˇ
22272 "});
22273 cx.update_editor(|editor, window, cx| {
22274 editor.handle_input("else:", window, cx);
22275 });
22276 cx.assert_editor_state(indoc! {"
22277 def main():
22278 try:
22279 i = 2
22280 except:
22281 for i in range(n):
22282 pass
22283 else:ˇ
22284 "});
22285
22286 // test `finally` auto outdents when typed inside `else` block right after for block
22287 cx.set_state(indoc! {"
22288 def main():
22289 try:
22290 i = 2
22291 except:
22292 j = 2
22293 else:
22294 for i in range(n):
22295 pass
22296 ˇ
22297 "});
22298 cx.update_editor(|editor, window, cx| {
22299 editor.handle_input("finally:", window, cx);
22300 });
22301 cx.assert_editor_state(indoc! {"
22302 def main():
22303 try:
22304 i = 2
22305 except:
22306 j = 2
22307 else:
22308 for i in range(n):
22309 pass
22310 finally:ˇ
22311 "});
22312
22313 // test `except` outdents to inner "try" block
22314 cx.set_state(indoc! {"
22315 def main():
22316 try:
22317 i = 2
22318 if i == 2:
22319 try:
22320 i = 3
22321 ˇ
22322 "});
22323 cx.update_editor(|editor, window, cx| {
22324 editor.handle_input("except:", window, cx);
22325 });
22326 cx.assert_editor_state(indoc! {"
22327 def main():
22328 try:
22329 i = 2
22330 if i == 2:
22331 try:
22332 i = 3
22333 except:ˇ
22334 "});
22335
22336 // test `except` outdents to outer "try" block
22337 cx.set_state(indoc! {"
22338 def main():
22339 try:
22340 i = 2
22341 if i == 2:
22342 try:
22343 i = 3
22344 ˇ
22345 "});
22346 cx.update_editor(|editor, window, cx| {
22347 editor.handle_input("except:", window, cx);
22348 });
22349 cx.assert_editor_state(indoc! {"
22350 def main():
22351 try:
22352 i = 2
22353 if i == 2:
22354 try:
22355 i = 3
22356 except:ˇ
22357 "});
22358
22359 // test `else` stays at correct indent when typed after `for` block
22360 cx.set_state(indoc! {"
22361 def main():
22362 for i in range(10):
22363 if i == 3:
22364 break
22365 ˇ
22366 "});
22367 cx.update_editor(|editor, window, cx| {
22368 editor.handle_input("else:", window, cx);
22369 });
22370 cx.assert_editor_state(indoc! {"
22371 def main():
22372 for i in range(10):
22373 if i == 3:
22374 break
22375 else:ˇ
22376 "});
22377
22378 // test does not outdent on typing after line with square brackets
22379 cx.set_state(indoc! {"
22380 def f() -> list[str]:
22381 ˇ
22382 "});
22383 cx.update_editor(|editor, window, cx| {
22384 editor.handle_input("a", window, cx);
22385 });
22386 cx.assert_editor_state(indoc! {"
22387 def f() -> list[str]:
22388 aˇ
22389 "});
22390
22391 // test does not outdent on typing : after case keyword
22392 cx.set_state(indoc! {"
22393 match 1:
22394 caseˇ
22395 "});
22396 cx.update_editor(|editor, window, cx| {
22397 editor.handle_input(":", window, cx);
22398 });
22399 cx.assert_editor_state(indoc! {"
22400 match 1:
22401 case:ˇ
22402 "});
22403}
22404
22405#[gpui::test]
22406async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
22407 init_test(cx, |_| {});
22408 update_test_language_settings(cx, |settings| {
22409 settings.defaults.extend_comment_on_newline = Some(false);
22410 });
22411 let mut cx = EditorTestContext::new(cx).await;
22412 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22413 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22414
22415 // test correct indent after newline on comment
22416 cx.set_state(indoc! {"
22417 # COMMENT:ˇ
22418 "});
22419 cx.update_editor(|editor, window, cx| {
22420 editor.newline(&Newline, window, cx);
22421 });
22422 cx.assert_editor_state(indoc! {"
22423 # COMMENT:
22424 ˇ
22425 "});
22426
22427 // test correct indent after newline in brackets
22428 cx.set_state(indoc! {"
22429 {ˇ}
22430 "});
22431 cx.update_editor(|editor, window, cx| {
22432 editor.newline(&Newline, window, cx);
22433 });
22434 cx.run_until_parked();
22435 cx.assert_editor_state(indoc! {"
22436 {
22437 ˇ
22438 }
22439 "});
22440
22441 cx.set_state(indoc! {"
22442 (ˇ)
22443 "});
22444 cx.update_editor(|editor, window, cx| {
22445 editor.newline(&Newline, window, cx);
22446 });
22447 cx.run_until_parked();
22448 cx.assert_editor_state(indoc! {"
22449 (
22450 ˇ
22451 )
22452 "});
22453
22454 // do not indent after empty lists or dictionaries
22455 cx.set_state(indoc! {"
22456 a = []ˇ
22457 "});
22458 cx.update_editor(|editor, window, cx| {
22459 editor.newline(&Newline, window, cx);
22460 });
22461 cx.run_until_parked();
22462 cx.assert_editor_state(indoc! {"
22463 a = []
22464 ˇ
22465 "});
22466}
22467
22468fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
22469 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
22470 point..point
22471}
22472
22473#[track_caller]
22474fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
22475 let (text, ranges) = marked_text_ranges(marked_text, true);
22476 assert_eq!(editor.text(cx), text);
22477 assert_eq!(
22478 editor.selections.ranges(cx),
22479 ranges,
22480 "Assert selections are {}",
22481 marked_text
22482 );
22483}
22484
22485pub fn handle_signature_help_request(
22486 cx: &mut EditorLspTestContext,
22487 mocked_response: lsp::SignatureHelp,
22488) -> impl Future<Output = ()> + use<> {
22489 let mut request =
22490 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
22491 let mocked_response = mocked_response.clone();
22492 async move { Ok(Some(mocked_response)) }
22493 });
22494
22495 async move {
22496 request.next().await;
22497 }
22498}
22499
22500#[track_caller]
22501pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
22502 cx.update_editor(|editor, _, _| {
22503 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
22504 let entries = menu.entries.borrow();
22505 let entries = entries
22506 .iter()
22507 .map(|entry| entry.string.as_str())
22508 .collect::<Vec<_>>();
22509 assert_eq!(entries, expected);
22510 } else {
22511 panic!("Expected completions menu");
22512 }
22513 });
22514}
22515
22516/// Handle completion request passing a marked string specifying where the completion
22517/// should be triggered from using '|' character, what range should be replaced, and what completions
22518/// should be returned using '<' and '>' to delimit the range.
22519///
22520/// Also see `handle_completion_request_with_insert_and_replace`.
22521#[track_caller]
22522pub fn handle_completion_request(
22523 marked_string: &str,
22524 completions: Vec<&'static str>,
22525 is_incomplete: bool,
22526 counter: Arc<AtomicUsize>,
22527 cx: &mut EditorLspTestContext,
22528) -> impl Future<Output = ()> {
22529 let complete_from_marker: TextRangeMarker = '|'.into();
22530 let replace_range_marker: TextRangeMarker = ('<', '>').into();
22531 let (_, mut marked_ranges) = marked_text_ranges_by(
22532 marked_string,
22533 vec![complete_from_marker.clone(), replace_range_marker.clone()],
22534 );
22535
22536 let complete_from_position =
22537 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
22538 let replace_range =
22539 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
22540
22541 let mut request =
22542 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
22543 let completions = completions.clone();
22544 counter.fetch_add(1, atomic::Ordering::Release);
22545 async move {
22546 assert_eq!(params.text_document_position.text_document.uri, url.clone());
22547 assert_eq!(
22548 params.text_document_position.position,
22549 complete_from_position
22550 );
22551 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
22552 is_incomplete: is_incomplete,
22553 item_defaults: None,
22554 items: completions
22555 .iter()
22556 .map(|completion_text| lsp::CompletionItem {
22557 label: completion_text.to_string(),
22558 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
22559 range: replace_range,
22560 new_text: completion_text.to_string(),
22561 })),
22562 ..Default::default()
22563 })
22564 .collect(),
22565 })))
22566 }
22567 });
22568
22569 async move {
22570 request.next().await;
22571 }
22572}
22573
22574/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
22575/// given instead, which also contains an `insert` range.
22576///
22577/// This function uses markers to define ranges:
22578/// - `|` marks the cursor position
22579/// - `<>` marks the replace range
22580/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
22581pub fn handle_completion_request_with_insert_and_replace(
22582 cx: &mut EditorLspTestContext,
22583 marked_string: &str,
22584 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
22585 counter: Arc<AtomicUsize>,
22586) -> impl Future<Output = ()> {
22587 let complete_from_marker: TextRangeMarker = '|'.into();
22588 let replace_range_marker: TextRangeMarker = ('<', '>').into();
22589 let insert_range_marker: TextRangeMarker = ('{', '}').into();
22590
22591 let (_, mut marked_ranges) = marked_text_ranges_by(
22592 marked_string,
22593 vec![
22594 complete_from_marker.clone(),
22595 replace_range_marker.clone(),
22596 insert_range_marker.clone(),
22597 ],
22598 );
22599
22600 let complete_from_position =
22601 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
22602 let replace_range =
22603 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
22604
22605 let insert_range = match marked_ranges.remove(&insert_range_marker) {
22606 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
22607 _ => lsp::Range {
22608 start: replace_range.start,
22609 end: complete_from_position,
22610 },
22611 };
22612
22613 let mut request =
22614 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
22615 let completions = completions.clone();
22616 counter.fetch_add(1, atomic::Ordering::Release);
22617 async move {
22618 assert_eq!(params.text_document_position.text_document.uri, url.clone());
22619 assert_eq!(
22620 params.text_document_position.position, complete_from_position,
22621 "marker `|` position doesn't match",
22622 );
22623 Ok(Some(lsp::CompletionResponse::Array(
22624 completions
22625 .iter()
22626 .map(|(label, new_text)| lsp::CompletionItem {
22627 label: label.to_string(),
22628 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
22629 lsp::InsertReplaceEdit {
22630 insert: insert_range,
22631 replace: replace_range,
22632 new_text: new_text.to_string(),
22633 },
22634 )),
22635 ..Default::default()
22636 })
22637 .collect(),
22638 )))
22639 }
22640 });
22641
22642 async move {
22643 request.next().await;
22644 }
22645}
22646
22647fn handle_resolve_completion_request(
22648 cx: &mut EditorLspTestContext,
22649 edits: Option<Vec<(&'static str, &'static str)>>,
22650) -> impl Future<Output = ()> {
22651 let edits = edits.map(|edits| {
22652 edits
22653 .iter()
22654 .map(|(marked_string, new_text)| {
22655 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
22656 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
22657 lsp::TextEdit::new(replace_range, new_text.to_string())
22658 })
22659 .collect::<Vec<_>>()
22660 });
22661
22662 let mut request =
22663 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
22664 let edits = edits.clone();
22665 async move {
22666 Ok(lsp::CompletionItem {
22667 additional_text_edits: edits,
22668 ..Default::default()
22669 })
22670 }
22671 });
22672
22673 async move {
22674 request.next().await;
22675 }
22676}
22677
22678pub(crate) fn update_test_language_settings(
22679 cx: &mut TestAppContext,
22680 f: impl Fn(&mut AllLanguageSettingsContent),
22681) {
22682 cx.update(|cx| {
22683 SettingsStore::update_global(cx, |store, cx| {
22684 store.update_user_settings::<AllLanguageSettings>(cx, f);
22685 });
22686 });
22687}
22688
22689pub(crate) fn update_test_project_settings(
22690 cx: &mut TestAppContext,
22691 f: impl Fn(&mut ProjectSettings),
22692) {
22693 cx.update(|cx| {
22694 SettingsStore::update_global(cx, |store, cx| {
22695 store.update_user_settings::<ProjectSettings>(cx, f);
22696 });
22697 });
22698}
22699
22700pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
22701 cx.update(|cx| {
22702 assets::Assets.load_test_fonts(cx);
22703 let store = SettingsStore::test(cx);
22704 cx.set_global(store);
22705 theme::init(theme::LoadThemes::JustBase, cx);
22706 release_channel::init(SemanticVersion::default(), cx);
22707 client::init_settings(cx);
22708 language::init(cx);
22709 Project::init_settings(cx);
22710 workspace::init_settings(cx);
22711 crate::init(cx);
22712 });
22713
22714 update_test_language_settings(cx, f);
22715}
22716
22717#[track_caller]
22718fn assert_hunk_revert(
22719 not_reverted_text_with_selections: &str,
22720 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
22721 expected_reverted_text_with_selections: &str,
22722 base_text: &str,
22723 cx: &mut EditorLspTestContext,
22724) {
22725 cx.set_state(not_reverted_text_with_selections);
22726 cx.set_head_text(base_text);
22727 cx.executor().run_until_parked();
22728
22729 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
22730 let snapshot = editor.snapshot(window, cx);
22731 let reverted_hunk_statuses = snapshot
22732 .buffer_snapshot
22733 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
22734 .map(|hunk| hunk.status().kind)
22735 .collect::<Vec<_>>();
22736
22737 editor.git_restore(&Default::default(), window, cx);
22738 reverted_hunk_statuses
22739 });
22740 cx.executor().run_until_parked();
22741 cx.assert_editor_state(expected_reverted_text_with_selections);
22742 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
22743}
22744
22745#[gpui::test(iterations = 10)]
22746async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
22747 init_test(cx, |_| {});
22748
22749 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
22750 let counter = diagnostic_requests.clone();
22751
22752 let fs = FakeFs::new(cx.executor());
22753 fs.insert_tree(
22754 path!("/a"),
22755 json!({
22756 "first.rs": "fn main() { let a = 5; }",
22757 "second.rs": "// Test file",
22758 }),
22759 )
22760 .await;
22761
22762 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22763 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22764 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22765
22766 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22767 language_registry.add(rust_lang());
22768 let mut fake_servers = language_registry.register_fake_lsp(
22769 "Rust",
22770 FakeLspAdapter {
22771 capabilities: lsp::ServerCapabilities {
22772 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
22773 lsp::DiagnosticOptions {
22774 identifier: None,
22775 inter_file_dependencies: true,
22776 workspace_diagnostics: true,
22777 work_done_progress_options: Default::default(),
22778 },
22779 )),
22780 ..Default::default()
22781 },
22782 ..Default::default()
22783 },
22784 );
22785
22786 let editor = workspace
22787 .update(cx, |workspace, window, cx| {
22788 workspace.open_abs_path(
22789 PathBuf::from(path!("/a/first.rs")),
22790 OpenOptions::default(),
22791 window,
22792 cx,
22793 )
22794 })
22795 .unwrap()
22796 .await
22797 .unwrap()
22798 .downcast::<Editor>()
22799 .unwrap();
22800 let fake_server = fake_servers.next().await.unwrap();
22801 let server_id = fake_server.server.server_id();
22802 let mut first_request = fake_server
22803 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
22804 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
22805 let result_id = Some(new_result_id.to_string());
22806 assert_eq!(
22807 params.text_document.uri,
22808 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
22809 );
22810 async move {
22811 Ok(lsp::DocumentDiagnosticReportResult::Report(
22812 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
22813 related_documents: None,
22814 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
22815 items: Vec::new(),
22816 result_id,
22817 },
22818 }),
22819 ))
22820 }
22821 });
22822
22823 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
22824 project.update(cx, |project, cx| {
22825 let buffer_id = editor
22826 .read(cx)
22827 .buffer()
22828 .read(cx)
22829 .as_singleton()
22830 .expect("created a singleton buffer")
22831 .read(cx)
22832 .remote_id();
22833 let buffer_result_id = project
22834 .lsp_store()
22835 .read(cx)
22836 .result_id(server_id, buffer_id, cx);
22837 assert_eq!(expected, buffer_result_id);
22838 });
22839 };
22840
22841 ensure_result_id(None, cx);
22842 cx.executor().advance_clock(Duration::from_millis(60));
22843 cx.executor().run_until_parked();
22844 assert_eq!(
22845 diagnostic_requests.load(atomic::Ordering::Acquire),
22846 1,
22847 "Opening file should trigger diagnostic request"
22848 );
22849 first_request
22850 .next()
22851 .await
22852 .expect("should have sent the first diagnostics pull request");
22853 ensure_result_id(Some("1".to_string()), cx);
22854
22855 // Editing should trigger diagnostics
22856 editor.update_in(cx, |editor, window, cx| {
22857 editor.handle_input("2", window, cx)
22858 });
22859 cx.executor().advance_clock(Duration::from_millis(60));
22860 cx.executor().run_until_parked();
22861 assert_eq!(
22862 diagnostic_requests.load(atomic::Ordering::Acquire),
22863 2,
22864 "Editing should trigger diagnostic request"
22865 );
22866 ensure_result_id(Some("2".to_string()), cx);
22867
22868 // Moving cursor should not trigger diagnostic request
22869 editor.update_in(cx, |editor, window, cx| {
22870 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22871 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
22872 });
22873 });
22874 cx.executor().advance_clock(Duration::from_millis(60));
22875 cx.executor().run_until_parked();
22876 assert_eq!(
22877 diagnostic_requests.load(atomic::Ordering::Acquire),
22878 2,
22879 "Cursor movement should not trigger diagnostic request"
22880 );
22881 ensure_result_id(Some("2".to_string()), cx);
22882 // Multiple rapid edits should be debounced
22883 for _ in 0..5 {
22884 editor.update_in(cx, |editor, window, cx| {
22885 editor.handle_input("x", window, cx)
22886 });
22887 }
22888 cx.executor().advance_clock(Duration::from_millis(60));
22889 cx.executor().run_until_parked();
22890
22891 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
22892 assert!(
22893 final_requests <= 4,
22894 "Multiple rapid edits should be debounced (got {final_requests} requests)",
22895 );
22896 ensure_result_id(Some(final_requests.to_string()), cx);
22897}
22898
22899#[gpui::test]
22900async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
22901 // Regression test for issue #11671
22902 // Previously, adding a cursor after moving multiple cursors would reset
22903 // the cursor count instead of adding to the existing cursors.
22904 init_test(cx, |_| {});
22905 let mut cx = EditorTestContext::new(cx).await;
22906
22907 // Create a simple buffer with cursor at start
22908 cx.set_state(indoc! {"
22909 ˇaaaa
22910 bbbb
22911 cccc
22912 dddd
22913 eeee
22914 ffff
22915 gggg
22916 hhhh"});
22917
22918 // Add 2 cursors below (so we have 3 total)
22919 cx.update_editor(|editor, window, cx| {
22920 editor.add_selection_below(&Default::default(), window, cx);
22921 editor.add_selection_below(&Default::default(), window, cx);
22922 });
22923
22924 // Verify we have 3 cursors
22925 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
22926 assert_eq!(
22927 initial_count, 3,
22928 "Should have 3 cursors after adding 2 below"
22929 );
22930
22931 // Move down one line
22932 cx.update_editor(|editor, window, cx| {
22933 editor.move_down(&MoveDown, window, cx);
22934 });
22935
22936 // Add another cursor below
22937 cx.update_editor(|editor, window, cx| {
22938 editor.add_selection_below(&Default::default(), window, cx);
22939 });
22940
22941 // Should now have 4 cursors (3 original + 1 new)
22942 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
22943 assert_eq!(
22944 final_count, 4,
22945 "Should have 4 cursors after moving and adding another"
22946 );
22947}
22948
22949#[gpui::test(iterations = 10)]
22950async fn test_document_colors(cx: &mut TestAppContext) {
22951 let expected_color = Rgba {
22952 r: 0.33,
22953 g: 0.33,
22954 b: 0.33,
22955 a: 0.33,
22956 };
22957
22958 init_test(cx, |_| {});
22959
22960 let fs = FakeFs::new(cx.executor());
22961 fs.insert_tree(
22962 path!("/a"),
22963 json!({
22964 "first.rs": "fn main() { let a = 5; }",
22965 }),
22966 )
22967 .await;
22968
22969 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22970 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22971 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22972
22973 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22974 language_registry.add(rust_lang());
22975 let mut fake_servers = language_registry.register_fake_lsp(
22976 "Rust",
22977 FakeLspAdapter {
22978 capabilities: lsp::ServerCapabilities {
22979 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
22980 ..lsp::ServerCapabilities::default()
22981 },
22982 name: "rust-analyzer",
22983 ..FakeLspAdapter::default()
22984 },
22985 );
22986 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
22987 "Rust",
22988 FakeLspAdapter {
22989 capabilities: lsp::ServerCapabilities {
22990 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
22991 ..lsp::ServerCapabilities::default()
22992 },
22993 name: "not-rust-analyzer",
22994 ..FakeLspAdapter::default()
22995 },
22996 );
22997
22998 let editor = workspace
22999 .update(cx, |workspace, window, cx| {
23000 workspace.open_abs_path(
23001 PathBuf::from(path!("/a/first.rs")),
23002 OpenOptions::default(),
23003 window,
23004 cx,
23005 )
23006 })
23007 .unwrap()
23008 .await
23009 .unwrap()
23010 .downcast::<Editor>()
23011 .unwrap();
23012 let fake_language_server = fake_servers.next().await.unwrap();
23013 let fake_language_server_without_capabilities =
23014 fake_servers_without_capabilities.next().await.unwrap();
23015 let requests_made = Arc::new(AtomicUsize::new(0));
23016 let closure_requests_made = Arc::clone(&requests_made);
23017 let mut color_request_handle = fake_language_server
23018 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
23019 let requests_made = Arc::clone(&closure_requests_made);
23020 async move {
23021 assert_eq!(
23022 params.text_document.uri,
23023 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
23024 );
23025 requests_made.fetch_add(1, atomic::Ordering::Release);
23026 Ok(vec![
23027 lsp::ColorInformation {
23028 range: lsp::Range {
23029 start: lsp::Position {
23030 line: 0,
23031 character: 0,
23032 },
23033 end: lsp::Position {
23034 line: 0,
23035 character: 1,
23036 },
23037 },
23038 color: lsp::Color {
23039 red: 0.33,
23040 green: 0.33,
23041 blue: 0.33,
23042 alpha: 0.33,
23043 },
23044 },
23045 lsp::ColorInformation {
23046 range: lsp::Range {
23047 start: lsp::Position {
23048 line: 0,
23049 character: 0,
23050 },
23051 end: lsp::Position {
23052 line: 0,
23053 character: 1,
23054 },
23055 },
23056 color: lsp::Color {
23057 red: 0.33,
23058 green: 0.33,
23059 blue: 0.33,
23060 alpha: 0.33,
23061 },
23062 },
23063 ])
23064 }
23065 });
23066
23067 let _handle = fake_language_server_without_capabilities
23068 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
23069 panic!("Should not be called");
23070 });
23071 cx.executor().advance_clock(Duration::from_millis(100));
23072 color_request_handle.next().await.unwrap();
23073 cx.run_until_parked();
23074 assert_eq!(
23075 1,
23076 requests_made.load(atomic::Ordering::Acquire),
23077 "Should query for colors once per editor open"
23078 );
23079 editor.update_in(cx, |editor, _, cx| {
23080 assert_eq!(
23081 vec![expected_color],
23082 extract_color_inlays(editor, cx),
23083 "Should have an initial inlay"
23084 );
23085 });
23086
23087 // opening another file in a split should not influence the LSP query counter
23088 workspace
23089 .update(cx, |workspace, window, cx| {
23090 assert_eq!(
23091 workspace.panes().len(),
23092 1,
23093 "Should have one pane with one editor"
23094 );
23095 workspace.move_item_to_pane_in_direction(
23096 &MoveItemToPaneInDirection {
23097 direction: SplitDirection::Right,
23098 focus: false,
23099 clone: true,
23100 },
23101 window,
23102 cx,
23103 );
23104 })
23105 .unwrap();
23106 cx.run_until_parked();
23107 workspace
23108 .update(cx, |workspace, _, cx| {
23109 let panes = workspace.panes();
23110 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
23111 for pane in panes {
23112 let editor = pane
23113 .read(cx)
23114 .active_item()
23115 .and_then(|item| item.downcast::<Editor>())
23116 .expect("Should have opened an editor in each split");
23117 let editor_file = editor
23118 .read(cx)
23119 .buffer()
23120 .read(cx)
23121 .as_singleton()
23122 .expect("test deals with singleton buffers")
23123 .read(cx)
23124 .file()
23125 .expect("test buffese should have a file")
23126 .path();
23127 assert_eq!(
23128 editor_file.as_ref(),
23129 Path::new("first.rs"),
23130 "Both editors should be opened for the same file"
23131 )
23132 }
23133 })
23134 .unwrap();
23135
23136 cx.executor().advance_clock(Duration::from_millis(500));
23137 let save = editor.update_in(cx, |editor, window, cx| {
23138 editor.move_to_end(&MoveToEnd, window, cx);
23139 editor.handle_input("dirty", window, cx);
23140 editor.save(
23141 SaveOptions {
23142 format: true,
23143 autosave: true,
23144 },
23145 project.clone(),
23146 window,
23147 cx,
23148 )
23149 });
23150 save.await.unwrap();
23151
23152 color_request_handle.next().await.unwrap();
23153 cx.run_until_parked();
23154 assert_eq!(
23155 3,
23156 requests_made.load(atomic::Ordering::Acquire),
23157 "Should query for colors once per save and once per formatting after save"
23158 );
23159
23160 drop(editor);
23161 let close = workspace
23162 .update(cx, |workspace, window, cx| {
23163 workspace.active_pane().update(cx, |pane, cx| {
23164 pane.close_active_item(&CloseActiveItem::default(), window, cx)
23165 })
23166 })
23167 .unwrap();
23168 close.await.unwrap();
23169 let close = workspace
23170 .update(cx, |workspace, window, cx| {
23171 workspace.active_pane().update(cx, |pane, cx| {
23172 pane.close_active_item(&CloseActiveItem::default(), window, cx)
23173 })
23174 })
23175 .unwrap();
23176 close.await.unwrap();
23177 assert_eq!(
23178 3,
23179 requests_made.load(atomic::Ordering::Acquire),
23180 "After saving and closing all editors, no extra requests should be made"
23181 );
23182 workspace
23183 .update(cx, |workspace, _, cx| {
23184 assert!(
23185 workspace.active_item(cx).is_none(),
23186 "Should close all editors"
23187 )
23188 })
23189 .unwrap();
23190
23191 workspace
23192 .update(cx, |workspace, window, cx| {
23193 workspace.active_pane().update(cx, |pane, cx| {
23194 pane.navigate_backward(window, cx);
23195 })
23196 })
23197 .unwrap();
23198 cx.executor().advance_clock(Duration::from_millis(100));
23199 cx.run_until_parked();
23200 let editor = workspace
23201 .update(cx, |workspace, _, cx| {
23202 workspace
23203 .active_item(cx)
23204 .expect("Should have reopened the editor again after navigating back")
23205 .downcast::<Editor>()
23206 .expect("Should be an editor")
23207 })
23208 .unwrap();
23209 color_request_handle.next().await.unwrap();
23210 assert_eq!(
23211 3,
23212 requests_made.load(atomic::Ordering::Acquire),
23213 "Cache should be reused on buffer close and reopen"
23214 );
23215 editor.update(cx, |editor, cx| {
23216 assert_eq!(
23217 vec![expected_color],
23218 extract_color_inlays(editor, cx),
23219 "Should have an initial inlay"
23220 );
23221 });
23222}
23223
23224#[gpui::test]
23225async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
23226 init_test(cx, |_| {});
23227 let (editor, cx) = cx.add_window_view(Editor::single_line);
23228 editor.update_in(cx, |editor, window, cx| {
23229 editor.set_text("oops\n\nwow\n", window, cx)
23230 });
23231 cx.run_until_parked();
23232 editor.update(cx, |editor, cx| {
23233 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
23234 });
23235 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
23236 cx.run_until_parked();
23237 editor.update(cx, |editor, cx| {
23238 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
23239 });
23240}
23241
23242#[track_caller]
23243fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
23244 editor
23245 .all_inlays(cx)
23246 .into_iter()
23247 .filter_map(|inlay| inlay.get_color())
23248 .map(Rgba::from)
23249 .collect()
23250}