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,
29 LanguageSettingsContent, LspInsertMode, PrettierSettings,
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]
3084fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3085 init_test(cx, |_| {});
3086
3087 let editor = cx.add_window(|window, cx| {
3088 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3089 let mut editor = build_editor(buffer.clone(), window, cx);
3090 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3091 s.select_ranges([3..4, 11..12, 19..20])
3092 });
3093 editor
3094 });
3095
3096 _ = editor.update(cx, |editor, window, cx| {
3097 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3098 editor.buffer.update(cx, |buffer, cx| {
3099 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3100 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3101 });
3102 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3103
3104 editor.insert("Z", window, cx);
3105 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3106
3107 // The selections are moved after the inserted characters
3108 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3109 });
3110}
3111
3112#[gpui::test]
3113async fn test_tab(cx: &mut TestAppContext) {
3114 init_test(cx, |settings| {
3115 settings.defaults.tab_size = NonZeroU32::new(3)
3116 });
3117
3118 let mut cx = EditorTestContext::new(cx).await;
3119 cx.set_state(indoc! {"
3120 ˇabˇc
3121 ˇ🏀ˇ🏀ˇefg
3122 dˇ
3123 "});
3124 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3125 cx.assert_editor_state(indoc! {"
3126 ˇab ˇc
3127 ˇ🏀 ˇ🏀 ˇefg
3128 d ˇ
3129 "});
3130
3131 cx.set_state(indoc! {"
3132 a
3133 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3134 "});
3135 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3136 cx.assert_editor_state(indoc! {"
3137 a
3138 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3139 "});
3140}
3141
3142#[gpui::test]
3143async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3144 init_test(cx, |_| {});
3145
3146 let mut cx = EditorTestContext::new(cx).await;
3147 let language = Arc::new(
3148 Language::new(
3149 LanguageConfig::default(),
3150 Some(tree_sitter_rust::LANGUAGE.into()),
3151 )
3152 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3153 .unwrap(),
3154 );
3155 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3156
3157 // test when all cursors are not at suggested indent
3158 // then simply move to their suggested indent location
3159 cx.set_state(indoc! {"
3160 const a: B = (
3161 c(
3162 ˇ
3163 ˇ )
3164 );
3165 "});
3166 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3167 cx.assert_editor_state(indoc! {"
3168 const a: B = (
3169 c(
3170 ˇ
3171 ˇ)
3172 );
3173 "});
3174
3175 // test cursor already at suggested indent not moving when
3176 // other cursors are yet to reach their suggested indents
3177 cx.set_state(indoc! {"
3178 ˇ
3179 const a: B = (
3180 c(
3181 d(
3182 ˇ
3183 )
3184 ˇ
3185 ˇ )
3186 );
3187 "});
3188 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3189 cx.assert_editor_state(indoc! {"
3190 ˇ
3191 const a: B = (
3192 c(
3193 d(
3194 ˇ
3195 )
3196 ˇ
3197 ˇ)
3198 );
3199 "});
3200 // test when all cursors are at suggested indent then tab is inserted
3201 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3202 cx.assert_editor_state(indoc! {"
3203 ˇ
3204 const a: B = (
3205 c(
3206 d(
3207 ˇ
3208 )
3209 ˇ
3210 ˇ)
3211 );
3212 "});
3213
3214 // test when current indent is less than suggested indent,
3215 // we adjust line to match suggested indent and move cursor to it
3216 //
3217 // when no other cursor is at word boundary, all of them should move
3218 cx.set_state(indoc! {"
3219 const a: B = (
3220 c(
3221 d(
3222 ˇ
3223 ˇ )
3224 ˇ )
3225 );
3226 "});
3227 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3228 cx.assert_editor_state(indoc! {"
3229 const a: B = (
3230 c(
3231 d(
3232 ˇ
3233 ˇ)
3234 ˇ)
3235 );
3236 "});
3237
3238 // test when current indent is less than suggested indent,
3239 // we adjust line to match suggested indent and move cursor to it
3240 //
3241 // when some other cursor is at word boundary, it should not move
3242 cx.set_state(indoc! {"
3243 const a: B = (
3244 c(
3245 d(
3246 ˇ
3247 ˇ )
3248 ˇ)
3249 );
3250 "});
3251 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3252 cx.assert_editor_state(indoc! {"
3253 const a: B = (
3254 c(
3255 d(
3256 ˇ
3257 ˇ)
3258 ˇ)
3259 );
3260 "});
3261
3262 // test when current indent is more than suggested indent,
3263 // we just move cursor to current indent instead of suggested indent
3264 //
3265 // when no other cursor is at word boundary, all of them should move
3266 cx.set_state(indoc! {"
3267 const a: B = (
3268 c(
3269 d(
3270 ˇ
3271 ˇ )
3272 ˇ )
3273 );
3274 "});
3275 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3276 cx.assert_editor_state(indoc! {"
3277 const a: B = (
3278 c(
3279 d(
3280 ˇ
3281 ˇ)
3282 ˇ)
3283 );
3284 "});
3285 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3286 cx.assert_editor_state(indoc! {"
3287 const a: B = (
3288 c(
3289 d(
3290 ˇ
3291 ˇ)
3292 ˇ)
3293 );
3294 "});
3295
3296 // test when current indent is more than suggested indent,
3297 // we just move cursor to current indent instead of suggested indent
3298 //
3299 // when some other cursor is at word boundary, it doesn't move
3300 cx.set_state(indoc! {"
3301 const a: B = (
3302 c(
3303 d(
3304 ˇ
3305 ˇ )
3306 ˇ)
3307 );
3308 "});
3309 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3310 cx.assert_editor_state(indoc! {"
3311 const a: B = (
3312 c(
3313 d(
3314 ˇ
3315 ˇ)
3316 ˇ)
3317 );
3318 "});
3319
3320 // handle auto-indent when there are multiple cursors on the same line
3321 cx.set_state(indoc! {"
3322 const a: B = (
3323 c(
3324 ˇ ˇ
3325 ˇ )
3326 );
3327 "});
3328 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3329 cx.assert_editor_state(indoc! {"
3330 const a: B = (
3331 c(
3332 ˇ
3333 ˇ)
3334 );
3335 "});
3336}
3337
3338#[gpui::test]
3339async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3340 init_test(cx, |settings| {
3341 settings.defaults.tab_size = NonZeroU32::new(3)
3342 });
3343
3344 let mut cx = EditorTestContext::new(cx).await;
3345 cx.set_state(indoc! {"
3346 ˇ
3347 \t ˇ
3348 \t ˇ
3349 \t ˇ
3350 \t \t\t \t \t\t \t\t \t \t ˇ
3351 "});
3352
3353 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3354 cx.assert_editor_state(indoc! {"
3355 ˇ
3356 \t ˇ
3357 \t ˇ
3358 \t ˇ
3359 \t \t\t \t \t\t \t\t \t \t ˇ
3360 "});
3361}
3362
3363#[gpui::test]
3364async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3365 init_test(cx, |settings| {
3366 settings.defaults.tab_size = NonZeroU32::new(4)
3367 });
3368
3369 let language = Arc::new(
3370 Language::new(
3371 LanguageConfig::default(),
3372 Some(tree_sitter_rust::LANGUAGE.into()),
3373 )
3374 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3375 .unwrap(),
3376 );
3377
3378 let mut cx = EditorTestContext::new(cx).await;
3379 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3380 cx.set_state(indoc! {"
3381 fn a() {
3382 if b {
3383 \t ˇc
3384 }
3385 }
3386 "});
3387
3388 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3389 cx.assert_editor_state(indoc! {"
3390 fn a() {
3391 if b {
3392 ˇc
3393 }
3394 }
3395 "});
3396}
3397
3398#[gpui::test]
3399async fn test_indent_outdent(cx: &mut TestAppContext) {
3400 init_test(cx, |settings| {
3401 settings.defaults.tab_size = NonZeroU32::new(4);
3402 });
3403
3404 let mut cx = EditorTestContext::new(cx).await;
3405
3406 cx.set_state(indoc! {"
3407 «oneˇ» «twoˇ»
3408 three
3409 four
3410 "});
3411 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3412 cx.assert_editor_state(indoc! {"
3413 «oneˇ» «twoˇ»
3414 three
3415 four
3416 "});
3417
3418 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3419 cx.assert_editor_state(indoc! {"
3420 «oneˇ» «twoˇ»
3421 three
3422 four
3423 "});
3424
3425 // select across line ending
3426 cx.set_state(indoc! {"
3427 one two
3428 t«hree
3429 ˇ» four
3430 "});
3431 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3432 cx.assert_editor_state(indoc! {"
3433 one two
3434 t«hree
3435 ˇ» four
3436 "});
3437
3438 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3439 cx.assert_editor_state(indoc! {"
3440 one two
3441 t«hree
3442 ˇ» four
3443 "});
3444
3445 // Ensure that indenting/outdenting works when the cursor is at column 0.
3446 cx.set_state(indoc! {"
3447 one two
3448 ˇthree
3449 four
3450 "});
3451 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3452 cx.assert_editor_state(indoc! {"
3453 one two
3454 ˇthree
3455 four
3456 "});
3457
3458 cx.set_state(indoc! {"
3459 one two
3460 ˇ three
3461 four
3462 "});
3463 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3464 cx.assert_editor_state(indoc! {"
3465 one two
3466 ˇthree
3467 four
3468 "});
3469}
3470
3471#[gpui::test]
3472async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3473 init_test(cx, |settings| {
3474 settings.defaults.hard_tabs = Some(true);
3475 });
3476
3477 let mut cx = EditorTestContext::new(cx).await;
3478
3479 // select two ranges on one line
3480 cx.set_state(indoc! {"
3481 «oneˇ» «twoˇ»
3482 three
3483 four
3484 "});
3485 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3486 cx.assert_editor_state(indoc! {"
3487 \t«oneˇ» «twoˇ»
3488 three
3489 four
3490 "});
3491 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3492 cx.assert_editor_state(indoc! {"
3493 \t\t«oneˇ» «twoˇ»
3494 three
3495 four
3496 "});
3497 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3498 cx.assert_editor_state(indoc! {"
3499 \t«oneˇ» «twoˇ»
3500 three
3501 four
3502 "});
3503 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3504 cx.assert_editor_state(indoc! {"
3505 «oneˇ» «twoˇ»
3506 three
3507 four
3508 "});
3509
3510 // select across a line ending
3511 cx.set_state(indoc! {"
3512 one two
3513 t«hree
3514 ˇ»four
3515 "});
3516 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3517 cx.assert_editor_state(indoc! {"
3518 one two
3519 \tt«hree
3520 ˇ»four
3521 "});
3522 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3523 cx.assert_editor_state(indoc! {"
3524 one two
3525 \t\tt«hree
3526 ˇ»four
3527 "});
3528 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3529 cx.assert_editor_state(indoc! {"
3530 one two
3531 \tt«hree
3532 ˇ»four
3533 "});
3534 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3535 cx.assert_editor_state(indoc! {"
3536 one two
3537 t«hree
3538 ˇ»four
3539 "});
3540
3541 // Ensure that indenting/outdenting works when the cursor is at column 0.
3542 cx.set_state(indoc! {"
3543 one two
3544 ˇthree
3545 four
3546 "});
3547 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3548 cx.assert_editor_state(indoc! {"
3549 one two
3550 ˇthree
3551 four
3552 "});
3553 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3554 cx.assert_editor_state(indoc! {"
3555 one two
3556 \tˇthree
3557 four
3558 "});
3559 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3560 cx.assert_editor_state(indoc! {"
3561 one two
3562 ˇthree
3563 four
3564 "});
3565}
3566
3567#[gpui::test]
3568fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3569 init_test(cx, |settings| {
3570 settings.languages.0.extend([
3571 (
3572 "TOML".into(),
3573 LanguageSettingsContent {
3574 tab_size: NonZeroU32::new(2),
3575 ..Default::default()
3576 },
3577 ),
3578 (
3579 "Rust".into(),
3580 LanguageSettingsContent {
3581 tab_size: NonZeroU32::new(4),
3582 ..Default::default()
3583 },
3584 ),
3585 ]);
3586 });
3587
3588 let toml_language = Arc::new(Language::new(
3589 LanguageConfig {
3590 name: "TOML".into(),
3591 ..Default::default()
3592 },
3593 None,
3594 ));
3595 let rust_language = Arc::new(Language::new(
3596 LanguageConfig {
3597 name: "Rust".into(),
3598 ..Default::default()
3599 },
3600 None,
3601 ));
3602
3603 let toml_buffer =
3604 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3605 let rust_buffer =
3606 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3607 let multibuffer = cx.new(|cx| {
3608 let mut multibuffer = MultiBuffer::new(ReadWrite);
3609 multibuffer.push_excerpts(
3610 toml_buffer.clone(),
3611 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3612 cx,
3613 );
3614 multibuffer.push_excerpts(
3615 rust_buffer.clone(),
3616 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3617 cx,
3618 );
3619 multibuffer
3620 });
3621
3622 cx.add_window(|window, cx| {
3623 let mut editor = build_editor(multibuffer, window, cx);
3624
3625 assert_eq!(
3626 editor.text(cx),
3627 indoc! {"
3628 a = 1
3629 b = 2
3630
3631 const c: usize = 3;
3632 "}
3633 );
3634
3635 select_ranges(
3636 &mut editor,
3637 indoc! {"
3638 «aˇ» = 1
3639 b = 2
3640
3641 «const c:ˇ» usize = 3;
3642 "},
3643 window,
3644 cx,
3645 );
3646
3647 editor.tab(&Tab, window, cx);
3648 assert_text_with_selections(
3649 &mut editor,
3650 indoc! {"
3651 «aˇ» = 1
3652 b = 2
3653
3654 «const c:ˇ» usize = 3;
3655 "},
3656 cx,
3657 );
3658 editor.backtab(&Backtab, window, cx);
3659 assert_text_with_selections(
3660 &mut editor,
3661 indoc! {"
3662 «aˇ» = 1
3663 b = 2
3664
3665 «const c:ˇ» usize = 3;
3666 "},
3667 cx,
3668 );
3669
3670 editor
3671 });
3672}
3673
3674#[gpui::test]
3675async fn test_backspace(cx: &mut TestAppContext) {
3676 init_test(cx, |_| {});
3677
3678 let mut cx = EditorTestContext::new(cx).await;
3679
3680 // Basic backspace
3681 cx.set_state(indoc! {"
3682 onˇe two three
3683 fou«rˇ» five six
3684 seven «ˇeight nine
3685 »ten
3686 "});
3687 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3688 cx.assert_editor_state(indoc! {"
3689 oˇe two three
3690 fouˇ five six
3691 seven ˇten
3692 "});
3693
3694 // Test backspace inside and around indents
3695 cx.set_state(indoc! {"
3696 zero
3697 ˇone
3698 ˇtwo
3699 ˇ ˇ ˇ three
3700 ˇ ˇ four
3701 "});
3702 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3703 cx.assert_editor_state(indoc! {"
3704 zero
3705 ˇone
3706 ˇtwo
3707 ˇ threeˇ four
3708 "});
3709}
3710
3711#[gpui::test]
3712async fn test_delete(cx: &mut TestAppContext) {
3713 init_test(cx, |_| {});
3714
3715 let mut cx = EditorTestContext::new(cx).await;
3716 cx.set_state(indoc! {"
3717 onˇe two three
3718 fou«rˇ» five six
3719 seven «ˇeight nine
3720 »ten
3721 "});
3722 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3723 cx.assert_editor_state(indoc! {"
3724 onˇ two three
3725 fouˇ five six
3726 seven ˇten
3727 "});
3728}
3729
3730#[gpui::test]
3731fn test_delete_line(cx: &mut TestAppContext) {
3732 init_test(cx, |_| {});
3733
3734 let editor = cx.add_window(|window, cx| {
3735 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3736 build_editor(buffer, window, cx)
3737 });
3738 _ = editor.update(cx, |editor, window, cx| {
3739 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3740 s.select_display_ranges([
3741 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3742 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3743 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3744 ])
3745 });
3746 editor.delete_line(&DeleteLine, window, cx);
3747 assert_eq!(editor.display_text(cx), "ghi");
3748 assert_eq!(
3749 editor.selections.display_ranges(cx),
3750 vec![
3751 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3752 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3753 ]
3754 );
3755 });
3756
3757 let editor = cx.add_window(|window, cx| {
3758 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3759 build_editor(buffer, window, cx)
3760 });
3761 _ = editor.update(cx, |editor, window, cx| {
3762 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3763 s.select_display_ranges([
3764 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3765 ])
3766 });
3767 editor.delete_line(&DeleteLine, window, cx);
3768 assert_eq!(editor.display_text(cx), "ghi\n");
3769 assert_eq!(
3770 editor.selections.display_ranges(cx),
3771 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3772 );
3773 });
3774}
3775
3776#[gpui::test]
3777fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3778 init_test(cx, |_| {});
3779
3780 cx.add_window(|window, cx| {
3781 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3782 let mut editor = build_editor(buffer.clone(), window, cx);
3783 let buffer = buffer.read(cx).as_singleton().unwrap();
3784
3785 assert_eq!(
3786 editor.selections.ranges::<Point>(cx),
3787 &[Point::new(0, 0)..Point::new(0, 0)]
3788 );
3789
3790 // When on single line, replace newline at end by space
3791 editor.join_lines(&JoinLines, window, cx);
3792 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3793 assert_eq!(
3794 editor.selections.ranges::<Point>(cx),
3795 &[Point::new(0, 3)..Point::new(0, 3)]
3796 );
3797
3798 // When multiple lines are selected, remove newlines that are spanned by the selection
3799 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3800 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3801 });
3802 editor.join_lines(&JoinLines, window, cx);
3803 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3804 assert_eq!(
3805 editor.selections.ranges::<Point>(cx),
3806 &[Point::new(0, 11)..Point::new(0, 11)]
3807 );
3808
3809 // Undo should be transactional
3810 editor.undo(&Undo, window, cx);
3811 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3812 assert_eq!(
3813 editor.selections.ranges::<Point>(cx),
3814 &[Point::new(0, 5)..Point::new(2, 2)]
3815 );
3816
3817 // When joining an empty line don't insert a space
3818 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3819 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3820 });
3821 editor.join_lines(&JoinLines, window, cx);
3822 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3823 assert_eq!(
3824 editor.selections.ranges::<Point>(cx),
3825 [Point::new(2, 3)..Point::new(2, 3)]
3826 );
3827
3828 // We can remove trailing newlines
3829 editor.join_lines(&JoinLines, window, cx);
3830 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3831 assert_eq!(
3832 editor.selections.ranges::<Point>(cx),
3833 [Point::new(2, 3)..Point::new(2, 3)]
3834 );
3835
3836 // We don't blow up on the last line
3837 editor.join_lines(&JoinLines, window, cx);
3838 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3839 assert_eq!(
3840 editor.selections.ranges::<Point>(cx),
3841 [Point::new(2, 3)..Point::new(2, 3)]
3842 );
3843
3844 // reset to test indentation
3845 editor.buffer.update(cx, |buffer, cx| {
3846 buffer.edit(
3847 [
3848 (Point::new(1, 0)..Point::new(1, 2), " "),
3849 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3850 ],
3851 None,
3852 cx,
3853 )
3854 });
3855
3856 // We remove any leading spaces
3857 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3858 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3859 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3860 });
3861 editor.join_lines(&JoinLines, window, cx);
3862 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3863
3864 // We don't insert a space for a line containing only spaces
3865 editor.join_lines(&JoinLines, window, cx);
3866 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3867
3868 // We ignore any leading tabs
3869 editor.join_lines(&JoinLines, window, cx);
3870 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3871
3872 editor
3873 });
3874}
3875
3876#[gpui::test]
3877fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3878 init_test(cx, |_| {});
3879
3880 cx.add_window(|window, cx| {
3881 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3882 let mut editor = build_editor(buffer.clone(), window, cx);
3883 let buffer = buffer.read(cx).as_singleton().unwrap();
3884
3885 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3886 s.select_ranges([
3887 Point::new(0, 2)..Point::new(1, 1),
3888 Point::new(1, 2)..Point::new(1, 2),
3889 Point::new(3, 1)..Point::new(3, 2),
3890 ])
3891 });
3892
3893 editor.join_lines(&JoinLines, window, cx);
3894 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3895
3896 assert_eq!(
3897 editor.selections.ranges::<Point>(cx),
3898 [
3899 Point::new(0, 7)..Point::new(0, 7),
3900 Point::new(1, 3)..Point::new(1, 3)
3901 ]
3902 );
3903 editor
3904 });
3905}
3906
3907#[gpui::test]
3908async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3909 init_test(cx, |_| {});
3910
3911 let mut cx = EditorTestContext::new(cx).await;
3912
3913 let diff_base = r#"
3914 Line 0
3915 Line 1
3916 Line 2
3917 Line 3
3918 "#
3919 .unindent();
3920
3921 cx.set_state(
3922 &r#"
3923 ˇLine 0
3924 Line 1
3925 Line 2
3926 Line 3
3927 "#
3928 .unindent(),
3929 );
3930
3931 cx.set_head_text(&diff_base);
3932 executor.run_until_parked();
3933
3934 // Join lines
3935 cx.update_editor(|editor, window, cx| {
3936 editor.join_lines(&JoinLines, window, cx);
3937 });
3938 executor.run_until_parked();
3939
3940 cx.assert_editor_state(
3941 &r#"
3942 Line 0ˇ Line 1
3943 Line 2
3944 Line 3
3945 "#
3946 .unindent(),
3947 );
3948 // Join again
3949 cx.update_editor(|editor, window, cx| {
3950 editor.join_lines(&JoinLines, window, cx);
3951 });
3952 executor.run_until_parked();
3953
3954 cx.assert_editor_state(
3955 &r#"
3956 Line 0 Line 1ˇ Line 2
3957 Line 3
3958 "#
3959 .unindent(),
3960 );
3961}
3962
3963#[gpui::test]
3964async fn test_custom_newlines_cause_no_false_positive_diffs(
3965 executor: BackgroundExecutor,
3966 cx: &mut TestAppContext,
3967) {
3968 init_test(cx, |_| {});
3969 let mut cx = EditorTestContext::new(cx).await;
3970 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3971 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3972 executor.run_until_parked();
3973
3974 cx.update_editor(|editor, window, cx| {
3975 let snapshot = editor.snapshot(window, cx);
3976 assert_eq!(
3977 snapshot
3978 .buffer_snapshot
3979 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3980 .collect::<Vec<_>>(),
3981 Vec::new(),
3982 "Should not have any diffs for files with custom newlines"
3983 );
3984 });
3985}
3986
3987#[gpui::test]
3988async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
3989 init_test(cx, |_| {});
3990
3991 let mut cx = EditorTestContext::new(cx).await;
3992
3993 // Test sort_lines_case_insensitive()
3994 cx.set_state(indoc! {"
3995 «z
3996 y
3997 x
3998 Z
3999 Y
4000 Xˇ»
4001 "});
4002 cx.update_editor(|e, window, cx| {
4003 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4004 });
4005 cx.assert_editor_state(indoc! {"
4006 «x
4007 X
4008 y
4009 Y
4010 z
4011 Zˇ»
4012 "});
4013
4014 // Test reverse_lines()
4015 cx.set_state(indoc! {"
4016 «5
4017 4
4018 3
4019 2
4020 1ˇ»
4021 "});
4022 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4023 cx.assert_editor_state(indoc! {"
4024 «1
4025 2
4026 3
4027 4
4028 5ˇ»
4029 "});
4030
4031 // Skip testing shuffle_line()
4032
4033 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4034 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4035
4036 // Don't manipulate when cursor is on single line, but expand the selection
4037 cx.set_state(indoc! {"
4038 ddˇdd
4039 ccc
4040 bb
4041 a
4042 "});
4043 cx.update_editor(|e, window, cx| {
4044 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4045 });
4046 cx.assert_editor_state(indoc! {"
4047 «ddddˇ»
4048 ccc
4049 bb
4050 a
4051 "});
4052
4053 // Basic manipulate case
4054 // Start selection moves to column 0
4055 // End of selection shrinks to fit shorter line
4056 cx.set_state(indoc! {"
4057 dd«d
4058 ccc
4059 bb
4060 aaaaaˇ»
4061 "});
4062 cx.update_editor(|e, window, cx| {
4063 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4064 });
4065 cx.assert_editor_state(indoc! {"
4066 «aaaaa
4067 bb
4068 ccc
4069 dddˇ»
4070 "});
4071
4072 // Manipulate case with newlines
4073 cx.set_state(indoc! {"
4074 dd«d
4075 ccc
4076
4077 bb
4078 aaaaa
4079
4080 ˇ»
4081 "});
4082 cx.update_editor(|e, window, cx| {
4083 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4084 });
4085 cx.assert_editor_state(indoc! {"
4086 «
4087
4088 aaaaa
4089 bb
4090 ccc
4091 dddˇ»
4092
4093 "});
4094
4095 // Adding new line
4096 cx.set_state(indoc! {"
4097 aa«a
4098 bbˇ»b
4099 "});
4100 cx.update_editor(|e, window, cx| {
4101 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4102 });
4103 cx.assert_editor_state(indoc! {"
4104 «aaa
4105 bbb
4106 added_lineˇ»
4107 "});
4108
4109 // Removing line
4110 cx.set_state(indoc! {"
4111 aa«a
4112 bbbˇ»
4113 "});
4114 cx.update_editor(|e, window, cx| {
4115 e.manipulate_immutable_lines(window, cx, |lines| {
4116 lines.pop();
4117 })
4118 });
4119 cx.assert_editor_state(indoc! {"
4120 «aaaˇ»
4121 "});
4122
4123 // Removing all lines
4124 cx.set_state(indoc! {"
4125 aa«a
4126 bbbˇ»
4127 "});
4128 cx.update_editor(|e, window, cx| {
4129 e.manipulate_immutable_lines(window, cx, |lines| {
4130 lines.drain(..);
4131 })
4132 });
4133 cx.assert_editor_state(indoc! {"
4134 ˇ
4135 "});
4136}
4137
4138#[gpui::test]
4139async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4140 init_test(cx, |_| {});
4141
4142 let mut cx = EditorTestContext::new(cx).await;
4143
4144 // Consider continuous selection as single selection
4145 cx.set_state(indoc! {"
4146 Aaa«aa
4147 cˇ»c«c
4148 bb
4149 aaaˇ»aa
4150 "});
4151 cx.update_editor(|e, window, cx| {
4152 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4153 });
4154 cx.assert_editor_state(indoc! {"
4155 «Aaaaa
4156 ccc
4157 bb
4158 aaaaaˇ»
4159 "});
4160
4161 cx.set_state(indoc! {"
4162 Aaa«aa
4163 cˇ»c«c
4164 bb
4165 aaaˇ»aa
4166 "});
4167 cx.update_editor(|e, window, cx| {
4168 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4169 });
4170 cx.assert_editor_state(indoc! {"
4171 «Aaaaa
4172 ccc
4173 bbˇ»
4174 "});
4175
4176 // Consider non continuous selection as distinct dedup operations
4177 cx.set_state(indoc! {"
4178 «aaaaa
4179 bb
4180 aaaaa
4181 aaaaaˇ»
4182
4183 aaa«aaˇ»
4184 "});
4185 cx.update_editor(|e, window, cx| {
4186 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4187 });
4188 cx.assert_editor_state(indoc! {"
4189 «aaaaa
4190 bbˇ»
4191
4192 «aaaaaˇ»
4193 "});
4194}
4195
4196#[gpui::test]
4197async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4198 init_test(cx, |_| {});
4199
4200 let mut cx = EditorTestContext::new(cx).await;
4201
4202 cx.set_state(indoc! {"
4203 «Aaa
4204 aAa
4205 Aaaˇ»
4206 "});
4207 cx.update_editor(|e, window, cx| {
4208 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4209 });
4210 cx.assert_editor_state(indoc! {"
4211 «Aaa
4212 aAaˇ»
4213 "});
4214
4215 cx.set_state(indoc! {"
4216 «Aaa
4217 aAa
4218 aaAˇ»
4219 "});
4220 cx.update_editor(|e, window, cx| {
4221 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4222 });
4223 cx.assert_editor_state(indoc! {"
4224 «Aaaˇ»
4225 "});
4226}
4227
4228#[gpui::test]
4229async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
4230 init_test(cx, |_| {});
4231
4232 let mut cx = EditorTestContext::new(cx).await;
4233
4234 // Manipulate with multiple selections on a single line
4235 cx.set_state(indoc! {"
4236 dd«dd
4237 cˇ»c«c
4238 bb
4239 aaaˇ»aa
4240 "});
4241 cx.update_editor(|e, window, cx| {
4242 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4243 });
4244 cx.assert_editor_state(indoc! {"
4245 «aaaaa
4246 bb
4247 ccc
4248 ddddˇ»
4249 "});
4250
4251 // Manipulate with multiple disjoin selections
4252 cx.set_state(indoc! {"
4253 5«
4254 4
4255 3
4256 2
4257 1ˇ»
4258
4259 dd«dd
4260 ccc
4261 bb
4262 aaaˇ»aa
4263 "});
4264 cx.update_editor(|e, window, cx| {
4265 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4266 });
4267 cx.assert_editor_state(indoc! {"
4268 «1
4269 2
4270 3
4271 4
4272 5ˇ»
4273
4274 «aaaaa
4275 bb
4276 ccc
4277 ddddˇ»
4278 "});
4279
4280 // Adding lines on each selection
4281 cx.set_state(indoc! {"
4282 2«
4283 1ˇ»
4284
4285 bb«bb
4286 aaaˇ»aa
4287 "});
4288 cx.update_editor(|e, window, cx| {
4289 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
4290 });
4291 cx.assert_editor_state(indoc! {"
4292 «2
4293 1
4294 added lineˇ»
4295
4296 «bbbb
4297 aaaaa
4298 added lineˇ»
4299 "});
4300
4301 // Removing lines on each selection
4302 cx.set_state(indoc! {"
4303 2«
4304 1ˇ»
4305
4306 bb«bb
4307 aaaˇ»aa
4308 "});
4309 cx.update_editor(|e, window, cx| {
4310 e.manipulate_immutable_lines(window, cx, |lines| {
4311 lines.pop();
4312 })
4313 });
4314 cx.assert_editor_state(indoc! {"
4315 «2ˇ»
4316
4317 «bbbbˇ»
4318 "});
4319}
4320
4321#[gpui::test]
4322async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
4323 init_test(cx, |settings| {
4324 settings.defaults.tab_size = NonZeroU32::new(3)
4325 });
4326
4327 let mut cx = EditorTestContext::new(cx).await;
4328
4329 // MULTI SELECTION
4330 // Ln.1 "«" tests empty lines
4331 // Ln.9 tests just leading whitespace
4332 cx.set_state(indoc! {"
4333 «
4334 abc // No indentationˇ»
4335 «\tabc // 1 tabˇ»
4336 \t\tabc « ˇ» // 2 tabs
4337 \t ab«c // Tab followed by space
4338 \tabc // Space followed by tab (3 spaces should be the result)
4339 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4340 abˇ»ˇc ˇ ˇ // Already space indented«
4341 \t
4342 \tabc\tdef // Only the leading tab is manipulatedˇ»
4343 "});
4344 cx.update_editor(|e, window, cx| {
4345 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4346 });
4347 cx.assert_editor_state(
4348 indoc! {"
4349 «
4350 abc // No indentation
4351 abc // 1 tab
4352 abc // 2 tabs
4353 abc // Tab followed by space
4354 abc // Space followed by tab (3 spaces should be the result)
4355 abc // Mixed indentation (tab conversion depends on the column)
4356 abc // Already space indented
4357 ·
4358 abc\tdef // Only the leading tab is manipulatedˇ»
4359 "}
4360 .replace("·", "")
4361 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4362 );
4363
4364 // Test on just a few lines, the others should remain unchanged
4365 // Only lines (3, 5, 10, 11) should change
4366 cx.set_state(
4367 indoc! {"
4368 ·
4369 abc // No indentation
4370 \tabcˇ // 1 tab
4371 \t\tabc // 2 tabs
4372 \t abcˇ // Tab followed by space
4373 \tabc // Space followed by tab (3 spaces should be the result)
4374 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4375 abc // Already space indented
4376 «\t
4377 \tabc\tdef // Only the leading tab is manipulatedˇ»
4378 "}
4379 .replace("·", "")
4380 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4381 );
4382 cx.update_editor(|e, window, cx| {
4383 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4384 });
4385 cx.assert_editor_state(
4386 indoc! {"
4387 ·
4388 abc // No indentation
4389 « abc // 1 tabˇ»
4390 \t\tabc // 2 tabs
4391 « abc // Tab followed by spaceˇ»
4392 \tabc // Space followed by tab (3 spaces should be the result)
4393 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4394 abc // Already space indented
4395 « ·
4396 abc\tdef // Only the leading tab is manipulatedˇ»
4397 "}
4398 .replace("·", "")
4399 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4400 );
4401
4402 // SINGLE SELECTION
4403 // Ln.1 "«" tests empty lines
4404 // Ln.9 tests just leading whitespace
4405 cx.set_state(indoc! {"
4406 «
4407 abc // No indentation
4408 \tabc // 1 tab
4409 \t\tabc // 2 tabs
4410 \t abc // Tab followed by space
4411 \tabc // Space followed by tab (3 spaces should be the result)
4412 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4413 abc // Already space indented
4414 \t
4415 \tabc\tdef // Only the leading tab is manipulatedˇ»
4416 "});
4417 cx.update_editor(|e, window, cx| {
4418 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4419 });
4420 cx.assert_editor_state(
4421 indoc! {"
4422 «
4423 abc // No indentation
4424 abc // 1 tab
4425 abc // 2 tabs
4426 abc // Tab followed by space
4427 abc // Space followed by tab (3 spaces should be the result)
4428 abc // Mixed indentation (tab conversion depends on the column)
4429 abc // Already space indented
4430 ·
4431 abc\tdef // Only the leading tab is manipulatedˇ»
4432 "}
4433 .replace("·", "")
4434 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4435 );
4436}
4437
4438#[gpui::test]
4439async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
4440 init_test(cx, |settings| {
4441 settings.defaults.tab_size = NonZeroU32::new(3)
4442 });
4443
4444 let mut cx = EditorTestContext::new(cx).await;
4445
4446 // MULTI SELECTION
4447 // Ln.1 "«" tests empty lines
4448 // Ln.11 tests just leading whitespace
4449 cx.set_state(indoc! {"
4450 «
4451 abˇ»ˇc // No indentation
4452 abc ˇ ˇ // 1 space (< 3 so dont convert)
4453 abc « // 2 spaces (< 3 so dont convert)
4454 abc // 3 spaces (convert)
4455 abc ˇ» // 5 spaces (1 tab + 2 spaces)
4456 «\tˇ»\t«\tˇ»abc // Already tab indented
4457 «\t abc // Tab followed by space
4458 \tabc // Space followed by tab (should be consumed due to tab)
4459 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4460 \tˇ» «\t
4461 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
4462 "});
4463 cx.update_editor(|e, window, cx| {
4464 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4465 });
4466 cx.assert_editor_state(indoc! {"
4467 «
4468 abc // No indentation
4469 abc // 1 space (< 3 so dont convert)
4470 abc // 2 spaces (< 3 so dont convert)
4471 \tabc // 3 spaces (convert)
4472 \t abc // 5 spaces (1 tab + 2 spaces)
4473 \t\t\tabc // Already tab indented
4474 \t abc // Tab followed by space
4475 \tabc // Space followed by tab (should be consumed due to tab)
4476 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4477 \t\t\t
4478 \tabc \t // Only the leading spaces should be convertedˇ»
4479 "});
4480
4481 // Test on just a few lines, the other should remain unchanged
4482 // Only lines (4, 8, 11, 12) should change
4483 cx.set_state(
4484 indoc! {"
4485 ·
4486 abc // No indentation
4487 abc // 1 space (< 3 so dont convert)
4488 abc // 2 spaces (< 3 so dont convert)
4489 « abc // 3 spaces (convert)ˇ»
4490 abc // 5 spaces (1 tab + 2 spaces)
4491 \t\t\tabc // Already tab indented
4492 \t abc // Tab followed by space
4493 \tabc ˇ // Space followed by tab (should be consumed due to tab)
4494 \t\t \tabc // Mixed indentation
4495 \t \t \t \tabc // Mixed indentation
4496 \t \tˇ
4497 « abc \t // Only the leading spaces should be convertedˇ»
4498 "}
4499 .replace("·", "")
4500 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4501 );
4502 cx.update_editor(|e, window, cx| {
4503 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4504 });
4505 cx.assert_editor_state(
4506 indoc! {"
4507 ·
4508 abc // No indentation
4509 abc // 1 space (< 3 so dont convert)
4510 abc // 2 spaces (< 3 so dont convert)
4511 «\tabc // 3 spaces (convert)ˇ»
4512 abc // 5 spaces (1 tab + 2 spaces)
4513 \t\t\tabc // Already tab indented
4514 \t abc // Tab followed by space
4515 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
4516 \t\t \tabc // Mixed indentation
4517 \t \t \t \tabc // Mixed indentation
4518 «\t\t\t
4519 \tabc \t // Only the leading spaces should be convertedˇ»
4520 "}
4521 .replace("·", "")
4522 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4523 );
4524
4525 // SINGLE SELECTION
4526 // Ln.1 "«" tests empty lines
4527 // Ln.11 tests just leading whitespace
4528 cx.set_state(indoc! {"
4529 «
4530 abc // No indentation
4531 abc // 1 space (< 3 so dont convert)
4532 abc // 2 spaces (< 3 so dont convert)
4533 abc // 3 spaces (convert)
4534 abc // 5 spaces (1 tab + 2 spaces)
4535 \t\t\tabc // Already tab indented
4536 \t abc // Tab followed by space
4537 \tabc // Space followed by tab (should be consumed due to tab)
4538 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4539 \t \t
4540 abc \t // Only the leading spaces should be convertedˇ»
4541 "});
4542 cx.update_editor(|e, window, cx| {
4543 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4544 });
4545 cx.assert_editor_state(indoc! {"
4546 «
4547 abc // No indentation
4548 abc // 1 space (< 3 so dont convert)
4549 abc // 2 spaces (< 3 so dont convert)
4550 \tabc // 3 spaces (convert)
4551 \t abc // 5 spaces (1 tab + 2 spaces)
4552 \t\t\tabc // Already tab indented
4553 \t abc // Tab followed by space
4554 \tabc // Space followed by tab (should be consumed due to tab)
4555 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4556 \t\t\t
4557 \tabc \t // Only the leading spaces should be convertedˇ»
4558 "});
4559}
4560
4561#[gpui::test]
4562async fn test_toggle_case(cx: &mut TestAppContext) {
4563 init_test(cx, |_| {});
4564
4565 let mut cx = EditorTestContext::new(cx).await;
4566
4567 // If all lower case -> upper case
4568 cx.set_state(indoc! {"
4569 «hello worldˇ»
4570 "});
4571 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4572 cx.assert_editor_state(indoc! {"
4573 «HELLO WORLDˇ»
4574 "});
4575
4576 // If all upper case -> lower case
4577 cx.set_state(indoc! {"
4578 «HELLO WORLDˇ»
4579 "});
4580 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4581 cx.assert_editor_state(indoc! {"
4582 «hello worldˇ»
4583 "});
4584
4585 // If any upper case characters are identified -> lower case
4586 // This matches JetBrains IDEs
4587 cx.set_state(indoc! {"
4588 «hEllo worldˇ»
4589 "});
4590 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4591 cx.assert_editor_state(indoc! {"
4592 «hello worldˇ»
4593 "});
4594}
4595
4596#[gpui::test]
4597async fn test_manipulate_text(cx: &mut TestAppContext) {
4598 init_test(cx, |_| {});
4599
4600 let mut cx = EditorTestContext::new(cx).await;
4601
4602 // Test convert_to_upper_case()
4603 cx.set_state(indoc! {"
4604 «hello worldˇ»
4605 "});
4606 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4607 cx.assert_editor_state(indoc! {"
4608 «HELLO WORLDˇ»
4609 "});
4610
4611 // Test convert_to_lower_case()
4612 cx.set_state(indoc! {"
4613 «HELLO WORLDˇ»
4614 "});
4615 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4616 cx.assert_editor_state(indoc! {"
4617 «hello worldˇ»
4618 "});
4619
4620 // Test multiple line, single selection case
4621 cx.set_state(indoc! {"
4622 «The quick brown
4623 fox jumps over
4624 the lazy dogˇ»
4625 "});
4626 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4627 cx.assert_editor_state(indoc! {"
4628 «The Quick Brown
4629 Fox Jumps Over
4630 The Lazy Dogˇ»
4631 "});
4632
4633 // Test multiple line, single selection case
4634 cx.set_state(indoc! {"
4635 «The quick brown
4636 fox jumps over
4637 the lazy dogˇ»
4638 "});
4639 cx.update_editor(|e, window, cx| {
4640 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4641 });
4642 cx.assert_editor_state(indoc! {"
4643 «TheQuickBrown
4644 FoxJumpsOver
4645 TheLazyDogˇ»
4646 "});
4647
4648 // From here on out, test more complex cases of manipulate_text()
4649
4650 // Test no selection case - should affect words cursors are in
4651 // Cursor at beginning, middle, and end of word
4652 cx.set_state(indoc! {"
4653 ˇhello big beauˇtiful worldˇ
4654 "});
4655 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4656 cx.assert_editor_state(indoc! {"
4657 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4658 "});
4659
4660 // Test multiple selections on a single line and across multiple lines
4661 cx.set_state(indoc! {"
4662 «Theˇ» quick «brown
4663 foxˇ» jumps «overˇ»
4664 the «lazyˇ» dog
4665 "});
4666 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4667 cx.assert_editor_state(indoc! {"
4668 «THEˇ» quick «BROWN
4669 FOXˇ» jumps «OVERˇ»
4670 the «LAZYˇ» dog
4671 "});
4672
4673 // Test case where text length grows
4674 cx.set_state(indoc! {"
4675 «tschüߡ»
4676 "});
4677 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4678 cx.assert_editor_state(indoc! {"
4679 «TSCHÜSSˇ»
4680 "});
4681
4682 // Test to make sure we don't crash when text shrinks
4683 cx.set_state(indoc! {"
4684 aaa_bbbˇ
4685 "});
4686 cx.update_editor(|e, window, cx| {
4687 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4688 });
4689 cx.assert_editor_state(indoc! {"
4690 «aaaBbbˇ»
4691 "});
4692
4693 // Test to make sure we all aware of the fact that each word can grow and shrink
4694 // Final selections should be aware of this fact
4695 cx.set_state(indoc! {"
4696 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4697 "});
4698 cx.update_editor(|e, window, cx| {
4699 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4700 });
4701 cx.assert_editor_state(indoc! {"
4702 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4703 "});
4704
4705 cx.set_state(indoc! {"
4706 «hElLo, WoRld!ˇ»
4707 "});
4708 cx.update_editor(|e, window, cx| {
4709 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4710 });
4711 cx.assert_editor_state(indoc! {"
4712 «HeLlO, wOrLD!ˇ»
4713 "});
4714}
4715
4716#[gpui::test]
4717fn test_duplicate_line(cx: &mut TestAppContext) {
4718 init_test(cx, |_| {});
4719
4720 let editor = cx.add_window(|window, cx| {
4721 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4722 build_editor(buffer, window, cx)
4723 });
4724 _ = editor.update(cx, |editor, window, cx| {
4725 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4726 s.select_display_ranges([
4727 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4728 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4729 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4730 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4731 ])
4732 });
4733 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4734 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4735 assert_eq!(
4736 editor.selections.display_ranges(cx),
4737 vec![
4738 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4739 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4740 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4741 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4742 ]
4743 );
4744 });
4745
4746 let editor = cx.add_window(|window, cx| {
4747 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4748 build_editor(buffer, window, cx)
4749 });
4750 _ = editor.update(cx, |editor, window, cx| {
4751 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4752 s.select_display_ranges([
4753 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4754 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4755 ])
4756 });
4757 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4758 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4759 assert_eq!(
4760 editor.selections.display_ranges(cx),
4761 vec![
4762 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4763 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4764 ]
4765 );
4766 });
4767
4768 // With `move_upwards` the selections stay in place, except for
4769 // the lines inserted above them
4770 let editor = cx.add_window(|window, cx| {
4771 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4772 build_editor(buffer, window, cx)
4773 });
4774 _ = editor.update(cx, |editor, window, cx| {
4775 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4776 s.select_display_ranges([
4777 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4778 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4779 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4780 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4781 ])
4782 });
4783 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4784 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4785 assert_eq!(
4786 editor.selections.display_ranges(cx),
4787 vec![
4788 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4789 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4790 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4791 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4792 ]
4793 );
4794 });
4795
4796 let editor = cx.add_window(|window, cx| {
4797 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4798 build_editor(buffer, window, cx)
4799 });
4800 _ = editor.update(cx, |editor, window, cx| {
4801 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4802 s.select_display_ranges([
4803 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4804 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4805 ])
4806 });
4807 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4808 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4809 assert_eq!(
4810 editor.selections.display_ranges(cx),
4811 vec![
4812 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4813 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4814 ]
4815 );
4816 });
4817
4818 let editor = cx.add_window(|window, cx| {
4819 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4820 build_editor(buffer, window, cx)
4821 });
4822 _ = editor.update(cx, |editor, window, cx| {
4823 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4824 s.select_display_ranges([
4825 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4826 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4827 ])
4828 });
4829 editor.duplicate_selection(&DuplicateSelection, window, cx);
4830 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4831 assert_eq!(
4832 editor.selections.display_ranges(cx),
4833 vec![
4834 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4835 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4836 ]
4837 );
4838 });
4839}
4840
4841#[gpui::test]
4842fn test_move_line_up_down(cx: &mut TestAppContext) {
4843 init_test(cx, |_| {});
4844
4845 let editor = cx.add_window(|window, cx| {
4846 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4847 build_editor(buffer, window, cx)
4848 });
4849 _ = editor.update(cx, |editor, window, cx| {
4850 editor.fold_creases(
4851 vec![
4852 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4853 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4854 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4855 ],
4856 true,
4857 window,
4858 cx,
4859 );
4860 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4861 s.select_display_ranges([
4862 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4863 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4864 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4865 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4866 ])
4867 });
4868 assert_eq!(
4869 editor.display_text(cx),
4870 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4871 );
4872
4873 editor.move_line_up(&MoveLineUp, window, cx);
4874 assert_eq!(
4875 editor.display_text(cx),
4876 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4877 );
4878 assert_eq!(
4879 editor.selections.display_ranges(cx),
4880 vec![
4881 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4882 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4883 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4884 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4885 ]
4886 );
4887 });
4888
4889 _ = editor.update(cx, |editor, window, cx| {
4890 editor.move_line_down(&MoveLineDown, window, cx);
4891 assert_eq!(
4892 editor.display_text(cx),
4893 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4894 );
4895 assert_eq!(
4896 editor.selections.display_ranges(cx),
4897 vec![
4898 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4899 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4900 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4901 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4902 ]
4903 );
4904 });
4905
4906 _ = editor.update(cx, |editor, window, cx| {
4907 editor.move_line_down(&MoveLineDown, window, cx);
4908 assert_eq!(
4909 editor.display_text(cx),
4910 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4911 );
4912 assert_eq!(
4913 editor.selections.display_ranges(cx),
4914 vec![
4915 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4916 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4917 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4918 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4919 ]
4920 );
4921 });
4922
4923 _ = editor.update(cx, |editor, window, cx| {
4924 editor.move_line_up(&MoveLineUp, window, cx);
4925 assert_eq!(
4926 editor.display_text(cx),
4927 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4928 );
4929 assert_eq!(
4930 editor.selections.display_ranges(cx),
4931 vec![
4932 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4933 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4934 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4935 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4936 ]
4937 );
4938 });
4939}
4940
4941#[gpui::test]
4942fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4943 init_test(cx, |_| {});
4944
4945 let editor = cx.add_window(|window, cx| {
4946 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4947 build_editor(buffer, window, cx)
4948 });
4949 _ = editor.update(cx, |editor, window, cx| {
4950 let snapshot = editor.buffer.read(cx).snapshot(cx);
4951 editor.insert_blocks(
4952 [BlockProperties {
4953 style: BlockStyle::Fixed,
4954 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4955 height: Some(1),
4956 render: Arc::new(|_| div().into_any()),
4957 priority: 0,
4958 render_in_minimap: true,
4959 }],
4960 Some(Autoscroll::fit()),
4961 cx,
4962 );
4963 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4964 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4965 });
4966 editor.move_line_down(&MoveLineDown, window, cx);
4967 });
4968}
4969
4970#[gpui::test]
4971async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4972 init_test(cx, |_| {});
4973
4974 let mut cx = EditorTestContext::new(cx).await;
4975 cx.set_state(
4976 &"
4977 ˇzero
4978 one
4979 two
4980 three
4981 four
4982 five
4983 "
4984 .unindent(),
4985 );
4986
4987 // Create a four-line block that replaces three lines of text.
4988 cx.update_editor(|editor, window, cx| {
4989 let snapshot = editor.snapshot(window, cx);
4990 let snapshot = &snapshot.buffer_snapshot;
4991 let placement = BlockPlacement::Replace(
4992 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4993 );
4994 editor.insert_blocks(
4995 [BlockProperties {
4996 placement,
4997 height: Some(4),
4998 style: BlockStyle::Sticky,
4999 render: Arc::new(|_| gpui::div().into_any_element()),
5000 priority: 0,
5001 render_in_minimap: true,
5002 }],
5003 None,
5004 cx,
5005 );
5006 });
5007
5008 // Move down so that the cursor touches the block.
5009 cx.update_editor(|editor, window, cx| {
5010 editor.move_down(&Default::default(), window, cx);
5011 });
5012 cx.assert_editor_state(
5013 &"
5014 zero
5015 «one
5016 two
5017 threeˇ»
5018 four
5019 five
5020 "
5021 .unindent(),
5022 );
5023
5024 // Move down past the block.
5025 cx.update_editor(|editor, window, cx| {
5026 editor.move_down(&Default::default(), window, cx);
5027 });
5028 cx.assert_editor_state(
5029 &"
5030 zero
5031 one
5032 two
5033 three
5034 ˇfour
5035 five
5036 "
5037 .unindent(),
5038 );
5039}
5040
5041#[gpui::test]
5042fn test_transpose(cx: &mut TestAppContext) {
5043 init_test(cx, |_| {});
5044
5045 _ = cx.add_window(|window, cx| {
5046 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5047 editor.set_style(EditorStyle::default(), window, cx);
5048 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5049 s.select_ranges([1..1])
5050 });
5051 editor.transpose(&Default::default(), window, cx);
5052 assert_eq!(editor.text(cx), "bac");
5053 assert_eq!(editor.selections.ranges(cx), [2..2]);
5054
5055 editor.transpose(&Default::default(), window, cx);
5056 assert_eq!(editor.text(cx), "bca");
5057 assert_eq!(editor.selections.ranges(cx), [3..3]);
5058
5059 editor.transpose(&Default::default(), window, cx);
5060 assert_eq!(editor.text(cx), "bac");
5061 assert_eq!(editor.selections.ranges(cx), [3..3]);
5062
5063 editor
5064 });
5065
5066 _ = cx.add_window(|window, cx| {
5067 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5068 editor.set_style(EditorStyle::default(), window, cx);
5069 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5070 s.select_ranges([3..3])
5071 });
5072 editor.transpose(&Default::default(), window, cx);
5073 assert_eq!(editor.text(cx), "acb\nde");
5074 assert_eq!(editor.selections.ranges(cx), [3..3]);
5075
5076 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5077 s.select_ranges([4..4])
5078 });
5079 editor.transpose(&Default::default(), window, cx);
5080 assert_eq!(editor.text(cx), "acbd\ne");
5081 assert_eq!(editor.selections.ranges(cx), [5..5]);
5082
5083 editor.transpose(&Default::default(), window, cx);
5084 assert_eq!(editor.text(cx), "acbde\n");
5085 assert_eq!(editor.selections.ranges(cx), [6..6]);
5086
5087 editor.transpose(&Default::default(), window, cx);
5088 assert_eq!(editor.text(cx), "acbd\ne");
5089 assert_eq!(editor.selections.ranges(cx), [6..6]);
5090
5091 editor
5092 });
5093
5094 _ = cx.add_window(|window, cx| {
5095 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5096 editor.set_style(EditorStyle::default(), window, cx);
5097 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5098 s.select_ranges([1..1, 2..2, 4..4])
5099 });
5100 editor.transpose(&Default::default(), window, cx);
5101 assert_eq!(editor.text(cx), "bacd\ne");
5102 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5103
5104 editor.transpose(&Default::default(), window, cx);
5105 assert_eq!(editor.text(cx), "bcade\n");
5106 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5107
5108 editor.transpose(&Default::default(), window, cx);
5109 assert_eq!(editor.text(cx), "bcda\ne");
5110 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5111
5112 editor.transpose(&Default::default(), window, cx);
5113 assert_eq!(editor.text(cx), "bcade\n");
5114 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5115
5116 editor.transpose(&Default::default(), window, cx);
5117 assert_eq!(editor.text(cx), "bcaed\n");
5118 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5119
5120 editor
5121 });
5122
5123 _ = cx.add_window(|window, cx| {
5124 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5125 editor.set_style(EditorStyle::default(), window, cx);
5126 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5127 s.select_ranges([4..4])
5128 });
5129 editor.transpose(&Default::default(), window, cx);
5130 assert_eq!(editor.text(cx), "🏀🍐✋");
5131 assert_eq!(editor.selections.ranges(cx), [8..8]);
5132
5133 editor.transpose(&Default::default(), window, cx);
5134 assert_eq!(editor.text(cx), "🏀✋🍐");
5135 assert_eq!(editor.selections.ranges(cx), [11..11]);
5136
5137 editor.transpose(&Default::default(), window, cx);
5138 assert_eq!(editor.text(cx), "🏀🍐✋");
5139 assert_eq!(editor.selections.ranges(cx), [11..11]);
5140
5141 editor
5142 });
5143}
5144
5145#[gpui::test]
5146async fn test_rewrap(cx: &mut TestAppContext) {
5147 init_test(cx, |settings| {
5148 settings.languages.0.extend([
5149 (
5150 "Markdown".into(),
5151 LanguageSettingsContent {
5152 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5153 preferred_line_length: Some(40),
5154 ..Default::default()
5155 },
5156 ),
5157 (
5158 "Plain Text".into(),
5159 LanguageSettingsContent {
5160 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5161 preferred_line_length: Some(40),
5162 ..Default::default()
5163 },
5164 ),
5165 (
5166 "C++".into(),
5167 LanguageSettingsContent {
5168 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5169 preferred_line_length: Some(40),
5170 ..Default::default()
5171 },
5172 ),
5173 (
5174 "Python".into(),
5175 LanguageSettingsContent {
5176 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5177 preferred_line_length: Some(40),
5178 ..Default::default()
5179 },
5180 ),
5181 (
5182 "Rust".into(),
5183 LanguageSettingsContent {
5184 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5185 preferred_line_length: Some(40),
5186 ..Default::default()
5187 },
5188 ),
5189 ])
5190 });
5191
5192 let mut cx = EditorTestContext::new(cx).await;
5193
5194 let cpp_language = Arc::new(Language::new(
5195 LanguageConfig {
5196 name: "C++".into(),
5197 line_comments: vec!["// ".into()],
5198 ..LanguageConfig::default()
5199 },
5200 None,
5201 ));
5202 let python_language = Arc::new(Language::new(
5203 LanguageConfig {
5204 name: "Python".into(),
5205 line_comments: vec!["# ".into()],
5206 ..LanguageConfig::default()
5207 },
5208 None,
5209 ));
5210 let markdown_language = Arc::new(Language::new(
5211 LanguageConfig {
5212 name: "Markdown".into(),
5213 ..LanguageConfig::default()
5214 },
5215 None,
5216 ));
5217 let rust_language = Arc::new(Language::new(
5218 LanguageConfig {
5219 name: "Rust".into(),
5220 line_comments: vec!["// ".into(), "/// ".into()],
5221 ..LanguageConfig::default()
5222 },
5223 Some(tree_sitter_rust::LANGUAGE.into()),
5224 ));
5225
5226 let plaintext_language = Arc::new(Language::new(
5227 LanguageConfig {
5228 name: "Plain Text".into(),
5229 ..LanguageConfig::default()
5230 },
5231 None,
5232 ));
5233
5234 // Test basic rewrapping of a long line with a cursor
5235 assert_rewrap(
5236 indoc! {"
5237 // ˇThis is a long comment that needs to be wrapped.
5238 "},
5239 indoc! {"
5240 // ˇThis is a long comment that needs to
5241 // be wrapped.
5242 "},
5243 cpp_language.clone(),
5244 &mut cx,
5245 );
5246
5247 // Test rewrapping a full selection
5248 assert_rewrap(
5249 indoc! {"
5250 «// This selected long comment needs to be wrapped.ˇ»"
5251 },
5252 indoc! {"
5253 «// This selected long comment needs to
5254 // be wrapped.ˇ»"
5255 },
5256 cpp_language.clone(),
5257 &mut cx,
5258 );
5259
5260 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
5261 assert_rewrap(
5262 indoc! {"
5263 // ˇThis is the first line.
5264 // Thisˇ is the second line.
5265 // This is the thirdˇ line, all part of one paragraph.
5266 "},
5267 indoc! {"
5268 // ˇThis is the first line. Thisˇ is the
5269 // second line. This is the thirdˇ line,
5270 // all part of one paragraph.
5271 "},
5272 cpp_language.clone(),
5273 &mut cx,
5274 );
5275
5276 // Test multiple cursors in different paragraphs trigger separate rewraps
5277 assert_rewrap(
5278 indoc! {"
5279 // ˇThis is the first paragraph, first line.
5280 // ˇThis is the first paragraph, second line.
5281
5282 // ˇThis is the second paragraph, first line.
5283 // ˇThis is the second paragraph, second line.
5284 "},
5285 indoc! {"
5286 // ˇThis is the first paragraph, first
5287 // line. ˇThis is the first paragraph,
5288 // second line.
5289
5290 // ˇThis is the second paragraph, first
5291 // line. ˇThis is the second paragraph,
5292 // second line.
5293 "},
5294 cpp_language.clone(),
5295 &mut cx,
5296 );
5297
5298 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
5299 assert_rewrap(
5300 indoc! {"
5301 «// A regular long long comment to be wrapped.
5302 /// A documentation long comment to be wrapped.ˇ»
5303 "},
5304 indoc! {"
5305 «// A regular long long comment to be
5306 // wrapped.
5307 /// A documentation long comment to be
5308 /// wrapped.ˇ»
5309 "},
5310 rust_language.clone(),
5311 &mut cx,
5312 );
5313
5314 // Test that change in indentation level trigger seperate rewraps
5315 assert_rewrap(
5316 indoc! {"
5317 fn foo() {
5318 «// This is a long comment at the base indent.
5319 // This is a long comment at the next indent.ˇ»
5320 }
5321 "},
5322 indoc! {"
5323 fn foo() {
5324 «// This is a long comment at the
5325 // base indent.
5326 // This is a long comment at the
5327 // next indent.ˇ»
5328 }
5329 "},
5330 rust_language.clone(),
5331 &mut cx,
5332 );
5333
5334 // Test that different comment prefix characters (e.g., '#') are handled correctly
5335 assert_rewrap(
5336 indoc! {"
5337 # ˇThis is a long comment using a pound sign.
5338 "},
5339 indoc! {"
5340 # ˇThis is a long comment using a pound
5341 # sign.
5342 "},
5343 python_language.clone(),
5344 &mut cx,
5345 );
5346
5347 // Test rewrapping only affects comments, not code even when selected
5348 assert_rewrap(
5349 indoc! {"
5350 «/// This doc comment is long and should be wrapped.
5351 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5352 "},
5353 indoc! {"
5354 «/// This doc comment is long and should
5355 /// be wrapped.
5356 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5357 "},
5358 rust_language.clone(),
5359 &mut cx,
5360 );
5361
5362 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
5363 assert_rewrap(
5364 indoc! {"
5365 # Header
5366
5367 A long long long line of markdown text to wrap.ˇ
5368 "},
5369 indoc! {"
5370 # Header
5371
5372 A long long long line of markdown text
5373 to wrap.ˇ
5374 "},
5375 markdown_language,
5376 &mut cx,
5377 );
5378
5379 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
5380 assert_rewrap(
5381 indoc! {"
5382 ˇThis is a very long line of plain text that will be wrapped.
5383 "},
5384 indoc! {"
5385 ˇThis is a very long line of plain text
5386 that will be wrapped.
5387 "},
5388 plaintext_language.clone(),
5389 &mut cx,
5390 );
5391
5392 // Test that non-commented code acts as a paragraph boundary within a selection
5393 assert_rewrap(
5394 indoc! {"
5395 «// This is the first long comment block to be wrapped.
5396 fn my_func(a: u32);
5397 // This is the second long comment block to be wrapped.ˇ»
5398 "},
5399 indoc! {"
5400 «// This is the first long comment block
5401 // to be wrapped.
5402 fn my_func(a: u32);
5403 // This is the second long comment block
5404 // to be wrapped.ˇ»
5405 "},
5406 rust_language.clone(),
5407 &mut cx,
5408 );
5409
5410 // Test rewrapping multiple selections, including ones with blank lines or tabs
5411 assert_rewrap(
5412 indoc! {"
5413 «ˇThis is a very long line that will be wrapped.
5414
5415 This is another paragraph in the same selection.»
5416
5417 «\tThis is a very long indented line that will be wrapped.ˇ»
5418 "},
5419 indoc! {"
5420 «ˇThis is a very long line that will be
5421 wrapped.
5422
5423 This is another paragraph in the same
5424 selection.»
5425
5426 «\tThis is a very long indented line
5427 \tthat will be wrapped.ˇ»
5428 "},
5429 plaintext_language.clone(),
5430 &mut cx,
5431 );
5432
5433 // Test that an empty comment line acts as a paragraph boundary
5434 assert_rewrap(
5435 indoc! {"
5436 // ˇThis is a long comment that will be wrapped.
5437 //
5438 // And this is another long comment that will also be wrapped.ˇ
5439 "},
5440 indoc! {"
5441 // ˇThis is a long comment that will be
5442 // wrapped.
5443 //
5444 // And this is another long comment that
5445 // will also be wrapped.ˇ
5446 "},
5447 cpp_language,
5448 &mut cx,
5449 );
5450
5451 #[track_caller]
5452 fn assert_rewrap(
5453 unwrapped_text: &str,
5454 wrapped_text: &str,
5455 language: Arc<Language>,
5456 cx: &mut EditorTestContext,
5457 ) {
5458 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5459 cx.set_state(unwrapped_text);
5460 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5461 cx.assert_editor_state(wrapped_text);
5462 }
5463}
5464
5465#[gpui::test]
5466async fn test_hard_wrap(cx: &mut TestAppContext) {
5467 init_test(cx, |_| {});
5468 let mut cx = EditorTestContext::new(cx).await;
5469
5470 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5471 cx.update_editor(|editor, _, cx| {
5472 editor.set_hard_wrap(Some(14), cx);
5473 });
5474
5475 cx.set_state(indoc!(
5476 "
5477 one two three ˇ
5478 "
5479 ));
5480 cx.simulate_input("four");
5481 cx.run_until_parked();
5482
5483 cx.assert_editor_state(indoc!(
5484 "
5485 one two three
5486 fourˇ
5487 "
5488 ));
5489
5490 cx.update_editor(|editor, window, cx| {
5491 editor.newline(&Default::default(), window, cx);
5492 });
5493 cx.run_until_parked();
5494 cx.assert_editor_state(indoc!(
5495 "
5496 one two three
5497 four
5498 ˇ
5499 "
5500 ));
5501
5502 cx.simulate_input("five");
5503 cx.run_until_parked();
5504 cx.assert_editor_state(indoc!(
5505 "
5506 one two three
5507 four
5508 fiveˇ
5509 "
5510 ));
5511
5512 cx.update_editor(|editor, window, cx| {
5513 editor.newline(&Default::default(), window, cx);
5514 });
5515 cx.run_until_parked();
5516 cx.simulate_input("# ");
5517 cx.run_until_parked();
5518 cx.assert_editor_state(indoc!(
5519 "
5520 one two three
5521 four
5522 five
5523 # ˇ
5524 "
5525 ));
5526
5527 cx.update_editor(|editor, window, cx| {
5528 editor.newline(&Default::default(), window, cx);
5529 });
5530 cx.run_until_parked();
5531 cx.assert_editor_state(indoc!(
5532 "
5533 one two three
5534 four
5535 five
5536 #\x20
5537 #ˇ
5538 "
5539 ));
5540
5541 cx.simulate_input(" 6");
5542 cx.run_until_parked();
5543 cx.assert_editor_state(indoc!(
5544 "
5545 one two three
5546 four
5547 five
5548 #
5549 # 6ˇ
5550 "
5551 ));
5552}
5553
5554#[gpui::test]
5555async fn test_clipboard(cx: &mut TestAppContext) {
5556 init_test(cx, |_| {});
5557
5558 let mut cx = EditorTestContext::new(cx).await;
5559
5560 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5561 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5562 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5563
5564 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5565 cx.set_state("two ˇfour ˇsix ˇ");
5566 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5567 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5568
5569 // Paste again but with only two cursors. Since the number of cursors doesn't
5570 // match the number of slices in the clipboard, the entire clipboard text
5571 // is pasted at each cursor.
5572 cx.set_state("ˇtwo one✅ four three six five ˇ");
5573 cx.update_editor(|e, window, cx| {
5574 e.handle_input("( ", window, cx);
5575 e.paste(&Paste, window, cx);
5576 e.handle_input(") ", window, cx);
5577 });
5578 cx.assert_editor_state(
5579 &([
5580 "( one✅ ",
5581 "three ",
5582 "five ) ˇtwo one✅ four three six five ( one✅ ",
5583 "three ",
5584 "five ) ˇ",
5585 ]
5586 .join("\n")),
5587 );
5588
5589 // Cut with three selections, one of which is full-line.
5590 cx.set_state(indoc! {"
5591 1«2ˇ»3
5592 4ˇ567
5593 «8ˇ»9"});
5594 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5595 cx.assert_editor_state(indoc! {"
5596 1ˇ3
5597 ˇ9"});
5598
5599 // Paste with three selections, noticing how the copied selection that was full-line
5600 // gets inserted before the second cursor.
5601 cx.set_state(indoc! {"
5602 1ˇ3
5603 9ˇ
5604 «oˇ»ne"});
5605 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5606 cx.assert_editor_state(indoc! {"
5607 12ˇ3
5608 4567
5609 9ˇ
5610 8ˇne"});
5611
5612 // Copy with a single cursor only, which writes the whole line into the clipboard.
5613 cx.set_state(indoc! {"
5614 The quick brown
5615 fox juˇmps over
5616 the lazy dog"});
5617 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5618 assert_eq!(
5619 cx.read_from_clipboard()
5620 .and_then(|item| item.text().as_deref().map(str::to_string)),
5621 Some("fox jumps over\n".to_string())
5622 );
5623
5624 // Paste with three selections, noticing how the copied full-line selection is inserted
5625 // before the empty selections but replaces the selection that is non-empty.
5626 cx.set_state(indoc! {"
5627 Tˇhe quick brown
5628 «foˇ»x jumps over
5629 tˇhe lazy dog"});
5630 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5631 cx.assert_editor_state(indoc! {"
5632 fox jumps over
5633 Tˇhe quick brown
5634 fox jumps over
5635 ˇx jumps over
5636 fox jumps over
5637 tˇhe lazy dog"});
5638}
5639
5640#[gpui::test]
5641async fn test_copy_trim(cx: &mut TestAppContext) {
5642 init_test(cx, |_| {});
5643
5644 let mut cx = EditorTestContext::new(cx).await;
5645 cx.set_state(
5646 r#" «for selection in selections.iter() {
5647 let mut start = selection.start;
5648 let mut end = selection.end;
5649 let is_entire_line = selection.is_empty();
5650 if is_entire_line {
5651 start = Point::new(start.row, 0);ˇ»
5652 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5653 }
5654 "#,
5655 );
5656 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5657 assert_eq!(
5658 cx.read_from_clipboard()
5659 .and_then(|item| item.text().as_deref().map(str::to_string)),
5660 Some(
5661 "for selection in selections.iter() {
5662 let mut start = selection.start;
5663 let mut end = selection.end;
5664 let is_entire_line = selection.is_empty();
5665 if is_entire_line {
5666 start = Point::new(start.row, 0);"
5667 .to_string()
5668 ),
5669 "Regular copying preserves all indentation selected",
5670 );
5671 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5672 assert_eq!(
5673 cx.read_from_clipboard()
5674 .and_then(|item| item.text().as_deref().map(str::to_string)),
5675 Some(
5676 "for selection in selections.iter() {
5677let mut start = selection.start;
5678let mut end = selection.end;
5679let is_entire_line = selection.is_empty();
5680if is_entire_line {
5681 start = Point::new(start.row, 0);"
5682 .to_string()
5683 ),
5684 "Copying with stripping should strip all leading whitespaces"
5685 );
5686
5687 cx.set_state(
5688 r#" « for selection in selections.iter() {
5689 let mut start = selection.start;
5690 let mut end = selection.end;
5691 let is_entire_line = selection.is_empty();
5692 if is_entire_line {
5693 start = Point::new(start.row, 0);ˇ»
5694 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5695 }
5696 "#,
5697 );
5698 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5699 assert_eq!(
5700 cx.read_from_clipboard()
5701 .and_then(|item| item.text().as_deref().map(str::to_string)),
5702 Some(
5703 " for selection in selections.iter() {
5704 let mut start = selection.start;
5705 let mut end = selection.end;
5706 let is_entire_line = selection.is_empty();
5707 if is_entire_line {
5708 start = Point::new(start.row, 0);"
5709 .to_string()
5710 ),
5711 "Regular copying preserves all indentation selected",
5712 );
5713 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5714 assert_eq!(
5715 cx.read_from_clipboard()
5716 .and_then(|item| item.text().as_deref().map(str::to_string)),
5717 Some(
5718 "for selection in selections.iter() {
5719let mut start = selection.start;
5720let mut end = selection.end;
5721let is_entire_line = selection.is_empty();
5722if is_entire_line {
5723 start = Point::new(start.row, 0);"
5724 .to_string()
5725 ),
5726 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5727 );
5728
5729 cx.set_state(
5730 r#" «ˇ for selection in selections.iter() {
5731 let mut start = selection.start;
5732 let mut end = selection.end;
5733 let is_entire_line = selection.is_empty();
5734 if is_entire_line {
5735 start = Point::new(start.row, 0);»
5736 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5737 }
5738 "#,
5739 );
5740 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5741 assert_eq!(
5742 cx.read_from_clipboard()
5743 .and_then(|item| item.text().as_deref().map(str::to_string)),
5744 Some(
5745 " for selection in selections.iter() {
5746 let mut start = selection.start;
5747 let mut end = selection.end;
5748 let is_entire_line = selection.is_empty();
5749 if is_entire_line {
5750 start = Point::new(start.row, 0);"
5751 .to_string()
5752 ),
5753 "Regular copying for reverse selection works the same",
5754 );
5755 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5756 assert_eq!(
5757 cx.read_from_clipboard()
5758 .and_then(|item| item.text().as_deref().map(str::to_string)),
5759 Some(
5760 "for selection in selections.iter() {
5761let mut start = selection.start;
5762let mut end = selection.end;
5763let is_entire_line = selection.is_empty();
5764if is_entire_line {
5765 start = Point::new(start.row, 0);"
5766 .to_string()
5767 ),
5768 "Copying with stripping for reverse selection works the same"
5769 );
5770
5771 cx.set_state(
5772 r#" for selection «in selections.iter() {
5773 let mut start = selection.start;
5774 let mut end = selection.end;
5775 let is_entire_line = selection.is_empty();
5776 if is_entire_line {
5777 start = Point::new(start.row, 0);ˇ»
5778 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5779 }
5780 "#,
5781 );
5782 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5783 assert_eq!(
5784 cx.read_from_clipboard()
5785 .and_then(|item| item.text().as_deref().map(str::to_string)),
5786 Some(
5787 "in selections.iter() {
5788 let mut start = selection.start;
5789 let mut end = selection.end;
5790 let is_entire_line = selection.is_empty();
5791 if is_entire_line {
5792 start = Point::new(start.row, 0);"
5793 .to_string()
5794 ),
5795 "When selecting past the indent, the copying works as usual",
5796 );
5797 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5798 assert_eq!(
5799 cx.read_from_clipboard()
5800 .and_then(|item| item.text().as_deref().map(str::to_string)),
5801 Some(
5802 "in selections.iter() {
5803 let mut start = selection.start;
5804 let mut end = selection.end;
5805 let is_entire_line = selection.is_empty();
5806 if is_entire_line {
5807 start = Point::new(start.row, 0);"
5808 .to_string()
5809 ),
5810 "When selecting past the indent, nothing is trimmed"
5811 );
5812
5813 cx.set_state(
5814 r#" «for selection in selections.iter() {
5815 let mut start = selection.start;
5816
5817 let mut end = selection.end;
5818 let is_entire_line = selection.is_empty();
5819 if is_entire_line {
5820 start = Point::new(start.row, 0);
5821ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
5822 }
5823 "#,
5824 );
5825 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5826 assert_eq!(
5827 cx.read_from_clipboard()
5828 .and_then(|item| item.text().as_deref().map(str::to_string)),
5829 Some(
5830 "for selection in selections.iter() {
5831let mut start = selection.start;
5832
5833let mut end = selection.end;
5834let is_entire_line = selection.is_empty();
5835if is_entire_line {
5836 start = Point::new(start.row, 0);
5837"
5838 .to_string()
5839 ),
5840 "Copying with stripping should ignore empty lines"
5841 );
5842}
5843
5844#[gpui::test]
5845async fn test_paste_multiline(cx: &mut TestAppContext) {
5846 init_test(cx, |_| {});
5847
5848 let mut cx = EditorTestContext::new(cx).await;
5849 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5850
5851 // Cut an indented block, without the leading whitespace.
5852 cx.set_state(indoc! {"
5853 const a: B = (
5854 c(),
5855 «d(
5856 e,
5857 f
5858 )ˇ»
5859 );
5860 "});
5861 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5862 cx.assert_editor_state(indoc! {"
5863 const a: B = (
5864 c(),
5865 ˇ
5866 );
5867 "});
5868
5869 // Paste it at the same position.
5870 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5871 cx.assert_editor_state(indoc! {"
5872 const a: B = (
5873 c(),
5874 d(
5875 e,
5876 f
5877 )ˇ
5878 );
5879 "});
5880
5881 // Paste it at a line with a lower indent level.
5882 cx.set_state(indoc! {"
5883 ˇ
5884 const a: B = (
5885 c(),
5886 );
5887 "});
5888 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5889 cx.assert_editor_state(indoc! {"
5890 d(
5891 e,
5892 f
5893 )ˇ
5894 const a: B = (
5895 c(),
5896 );
5897 "});
5898
5899 // Cut an indented block, with the leading whitespace.
5900 cx.set_state(indoc! {"
5901 const a: B = (
5902 c(),
5903 « d(
5904 e,
5905 f
5906 )
5907 ˇ»);
5908 "});
5909 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5910 cx.assert_editor_state(indoc! {"
5911 const a: B = (
5912 c(),
5913 ˇ);
5914 "});
5915
5916 // Paste it at the same position.
5917 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5918 cx.assert_editor_state(indoc! {"
5919 const a: B = (
5920 c(),
5921 d(
5922 e,
5923 f
5924 )
5925 ˇ);
5926 "});
5927
5928 // Paste it at a line with a higher indent level.
5929 cx.set_state(indoc! {"
5930 const a: B = (
5931 c(),
5932 d(
5933 e,
5934 fˇ
5935 )
5936 );
5937 "});
5938 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5939 cx.assert_editor_state(indoc! {"
5940 const a: B = (
5941 c(),
5942 d(
5943 e,
5944 f d(
5945 e,
5946 f
5947 )
5948 ˇ
5949 )
5950 );
5951 "});
5952
5953 // Copy an indented block, starting mid-line
5954 cx.set_state(indoc! {"
5955 const a: B = (
5956 c(),
5957 somethin«g(
5958 e,
5959 f
5960 )ˇ»
5961 );
5962 "});
5963 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5964
5965 // Paste it on a line with a lower indent level
5966 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
5967 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5968 cx.assert_editor_state(indoc! {"
5969 const a: B = (
5970 c(),
5971 something(
5972 e,
5973 f
5974 )
5975 );
5976 g(
5977 e,
5978 f
5979 )ˇ"});
5980}
5981
5982#[gpui::test]
5983async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
5984 init_test(cx, |_| {});
5985
5986 cx.write_to_clipboard(ClipboardItem::new_string(
5987 " d(\n e\n );\n".into(),
5988 ));
5989
5990 let mut cx = EditorTestContext::new(cx).await;
5991 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5992
5993 cx.set_state(indoc! {"
5994 fn a() {
5995 b();
5996 if c() {
5997 ˇ
5998 }
5999 }
6000 "});
6001
6002 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6003 cx.assert_editor_state(indoc! {"
6004 fn a() {
6005 b();
6006 if c() {
6007 d(
6008 e
6009 );
6010 ˇ
6011 }
6012 }
6013 "});
6014
6015 cx.set_state(indoc! {"
6016 fn a() {
6017 b();
6018 ˇ
6019 }
6020 "});
6021
6022 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6023 cx.assert_editor_state(indoc! {"
6024 fn a() {
6025 b();
6026 d(
6027 e
6028 );
6029 ˇ
6030 }
6031 "});
6032}
6033
6034#[gpui::test]
6035fn test_select_all(cx: &mut TestAppContext) {
6036 init_test(cx, |_| {});
6037
6038 let editor = cx.add_window(|window, cx| {
6039 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
6040 build_editor(buffer, window, cx)
6041 });
6042 _ = editor.update(cx, |editor, window, cx| {
6043 editor.select_all(&SelectAll, window, cx);
6044 assert_eq!(
6045 editor.selections.display_ranges(cx),
6046 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
6047 );
6048 });
6049}
6050
6051#[gpui::test]
6052fn test_select_line(cx: &mut TestAppContext) {
6053 init_test(cx, |_| {});
6054
6055 let editor = cx.add_window(|window, cx| {
6056 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
6057 build_editor(buffer, window, cx)
6058 });
6059 _ = editor.update(cx, |editor, window, cx| {
6060 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6061 s.select_display_ranges([
6062 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6063 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6064 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6065 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
6066 ])
6067 });
6068 editor.select_line(&SelectLine, window, cx);
6069 assert_eq!(
6070 editor.selections.display_ranges(cx),
6071 vec![
6072 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
6073 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
6074 ]
6075 );
6076 });
6077
6078 _ = editor.update(cx, |editor, window, cx| {
6079 editor.select_line(&SelectLine, window, cx);
6080 assert_eq!(
6081 editor.selections.display_ranges(cx),
6082 vec![
6083 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
6084 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
6085 ]
6086 );
6087 });
6088
6089 _ = editor.update(cx, |editor, window, cx| {
6090 editor.select_line(&SelectLine, window, cx);
6091 assert_eq!(
6092 editor.selections.display_ranges(cx),
6093 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
6094 );
6095 });
6096}
6097
6098#[gpui::test]
6099async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
6100 init_test(cx, |_| {});
6101 let mut cx = EditorTestContext::new(cx).await;
6102
6103 #[track_caller]
6104 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
6105 cx.set_state(initial_state);
6106 cx.update_editor(|e, window, cx| {
6107 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
6108 });
6109 cx.assert_editor_state(expected_state);
6110 }
6111
6112 // Selection starts and ends at the middle of lines, left-to-right
6113 test(
6114 &mut cx,
6115 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
6116 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6117 );
6118 // Same thing, right-to-left
6119 test(
6120 &mut cx,
6121 "aa\nb«b\ncc\ndd\neˇ»e\nff",
6122 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6123 );
6124
6125 // Whole buffer, left-to-right, last line *doesn't* end with newline
6126 test(
6127 &mut cx,
6128 "«ˇaa\nbb\ncc\ndd\nee\nff»",
6129 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6130 );
6131 // Same thing, right-to-left
6132 test(
6133 &mut cx,
6134 "«aa\nbb\ncc\ndd\nee\nffˇ»",
6135 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6136 );
6137
6138 // Whole buffer, left-to-right, last line ends with newline
6139 test(
6140 &mut cx,
6141 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
6142 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6143 );
6144 // Same thing, right-to-left
6145 test(
6146 &mut cx,
6147 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
6148 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6149 );
6150
6151 // Starts at the end of a line, ends at the start of another
6152 test(
6153 &mut cx,
6154 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
6155 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
6156 );
6157}
6158
6159#[gpui::test]
6160async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
6161 init_test(cx, |_| {});
6162
6163 let editor = cx.add_window(|window, cx| {
6164 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
6165 build_editor(buffer, window, cx)
6166 });
6167
6168 // setup
6169 _ = editor.update(cx, |editor, window, cx| {
6170 editor.fold_creases(
6171 vec![
6172 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
6173 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
6174 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
6175 ],
6176 true,
6177 window,
6178 cx,
6179 );
6180 assert_eq!(
6181 editor.display_text(cx),
6182 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6183 );
6184 });
6185
6186 _ = editor.update(cx, |editor, window, cx| {
6187 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6188 s.select_display_ranges([
6189 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6190 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6191 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6192 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
6193 ])
6194 });
6195 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6196 assert_eq!(
6197 editor.display_text(cx),
6198 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6199 );
6200 });
6201 EditorTestContext::for_editor(editor, cx)
6202 .await
6203 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
6204
6205 _ = editor.update(cx, |editor, window, cx| {
6206 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6207 s.select_display_ranges([
6208 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
6209 ])
6210 });
6211 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6212 assert_eq!(
6213 editor.display_text(cx),
6214 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
6215 );
6216 assert_eq!(
6217 editor.selections.display_ranges(cx),
6218 [
6219 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
6220 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
6221 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
6222 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
6223 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
6224 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
6225 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
6226 ]
6227 );
6228 });
6229 EditorTestContext::for_editor(editor, cx)
6230 .await
6231 .assert_editor_state(
6232 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
6233 );
6234}
6235
6236#[gpui::test]
6237async fn test_add_selection_above_below(cx: &mut TestAppContext) {
6238 init_test(cx, |_| {});
6239
6240 let mut cx = EditorTestContext::new(cx).await;
6241
6242 cx.set_state(indoc!(
6243 r#"abc
6244 defˇghi
6245
6246 jk
6247 nlmo
6248 "#
6249 ));
6250
6251 cx.update_editor(|editor, window, cx| {
6252 editor.add_selection_above(&Default::default(), window, cx);
6253 });
6254
6255 cx.assert_editor_state(indoc!(
6256 r#"abcˇ
6257 defˇghi
6258
6259 jk
6260 nlmo
6261 "#
6262 ));
6263
6264 cx.update_editor(|editor, window, cx| {
6265 editor.add_selection_above(&Default::default(), window, cx);
6266 });
6267
6268 cx.assert_editor_state(indoc!(
6269 r#"abcˇ
6270 defˇghi
6271
6272 jk
6273 nlmo
6274 "#
6275 ));
6276
6277 cx.update_editor(|editor, window, cx| {
6278 editor.add_selection_below(&Default::default(), window, cx);
6279 });
6280
6281 cx.assert_editor_state(indoc!(
6282 r#"abc
6283 defˇghi
6284
6285 jk
6286 nlmo
6287 "#
6288 ));
6289
6290 cx.update_editor(|editor, window, cx| {
6291 editor.undo_selection(&Default::default(), window, cx);
6292 });
6293
6294 cx.assert_editor_state(indoc!(
6295 r#"abcˇ
6296 defˇghi
6297
6298 jk
6299 nlmo
6300 "#
6301 ));
6302
6303 cx.update_editor(|editor, window, cx| {
6304 editor.redo_selection(&Default::default(), window, cx);
6305 });
6306
6307 cx.assert_editor_state(indoc!(
6308 r#"abc
6309 defˇghi
6310
6311 jk
6312 nlmo
6313 "#
6314 ));
6315
6316 cx.update_editor(|editor, window, cx| {
6317 editor.add_selection_below(&Default::default(), window, cx);
6318 });
6319
6320 cx.assert_editor_state(indoc!(
6321 r#"abc
6322 defˇghi
6323 ˇ
6324 jk
6325 nlmo
6326 "#
6327 ));
6328
6329 cx.update_editor(|editor, window, cx| {
6330 editor.add_selection_below(&Default::default(), window, cx);
6331 });
6332
6333 cx.assert_editor_state(indoc!(
6334 r#"abc
6335 defˇghi
6336 ˇ
6337 jkˇ
6338 nlmo
6339 "#
6340 ));
6341
6342 cx.update_editor(|editor, window, cx| {
6343 editor.add_selection_below(&Default::default(), window, cx);
6344 });
6345
6346 cx.assert_editor_state(indoc!(
6347 r#"abc
6348 defˇghi
6349 ˇ
6350 jkˇ
6351 nlmˇo
6352 "#
6353 ));
6354
6355 cx.update_editor(|editor, window, cx| {
6356 editor.add_selection_below(&Default::default(), window, cx);
6357 });
6358
6359 cx.assert_editor_state(indoc!(
6360 r#"abc
6361 defˇghi
6362 ˇ
6363 jkˇ
6364 nlmˇo
6365 ˇ"#
6366 ));
6367
6368 // change selections
6369 cx.set_state(indoc!(
6370 r#"abc
6371 def«ˇg»hi
6372
6373 jk
6374 nlmo
6375 "#
6376 ));
6377
6378 cx.update_editor(|editor, window, cx| {
6379 editor.add_selection_below(&Default::default(), window, cx);
6380 });
6381
6382 cx.assert_editor_state(indoc!(
6383 r#"abc
6384 def«ˇg»hi
6385
6386 jk
6387 nlm«ˇo»
6388 "#
6389 ));
6390
6391 cx.update_editor(|editor, window, cx| {
6392 editor.add_selection_below(&Default::default(), window, cx);
6393 });
6394
6395 cx.assert_editor_state(indoc!(
6396 r#"abc
6397 def«ˇg»hi
6398
6399 jk
6400 nlm«ˇo»
6401 "#
6402 ));
6403
6404 cx.update_editor(|editor, window, cx| {
6405 editor.add_selection_above(&Default::default(), window, cx);
6406 });
6407
6408 cx.assert_editor_state(indoc!(
6409 r#"abc
6410 def«ˇg»hi
6411
6412 jk
6413 nlmo
6414 "#
6415 ));
6416
6417 cx.update_editor(|editor, window, cx| {
6418 editor.add_selection_above(&Default::default(), window, cx);
6419 });
6420
6421 cx.assert_editor_state(indoc!(
6422 r#"abc
6423 def«ˇg»hi
6424
6425 jk
6426 nlmo
6427 "#
6428 ));
6429
6430 // Change selections again
6431 cx.set_state(indoc!(
6432 r#"a«bc
6433 defgˇ»hi
6434
6435 jk
6436 nlmo
6437 "#
6438 ));
6439
6440 cx.update_editor(|editor, window, cx| {
6441 editor.add_selection_below(&Default::default(), window, cx);
6442 });
6443
6444 cx.assert_editor_state(indoc!(
6445 r#"a«bcˇ»
6446 d«efgˇ»hi
6447
6448 j«kˇ»
6449 nlmo
6450 "#
6451 ));
6452
6453 cx.update_editor(|editor, window, cx| {
6454 editor.add_selection_below(&Default::default(), window, cx);
6455 });
6456 cx.assert_editor_state(indoc!(
6457 r#"a«bcˇ»
6458 d«efgˇ»hi
6459
6460 j«kˇ»
6461 n«lmoˇ»
6462 "#
6463 ));
6464 cx.update_editor(|editor, window, cx| {
6465 editor.add_selection_above(&Default::default(), window, cx);
6466 });
6467
6468 cx.assert_editor_state(indoc!(
6469 r#"a«bcˇ»
6470 d«efgˇ»hi
6471
6472 j«kˇ»
6473 nlmo
6474 "#
6475 ));
6476
6477 // Change selections again
6478 cx.set_state(indoc!(
6479 r#"abc
6480 d«ˇefghi
6481
6482 jk
6483 nlm»o
6484 "#
6485 ));
6486
6487 cx.update_editor(|editor, window, cx| {
6488 editor.add_selection_above(&Default::default(), window, cx);
6489 });
6490
6491 cx.assert_editor_state(indoc!(
6492 r#"a«ˇbc»
6493 d«ˇef»ghi
6494
6495 j«ˇk»
6496 n«ˇlm»o
6497 "#
6498 ));
6499
6500 cx.update_editor(|editor, window, cx| {
6501 editor.add_selection_below(&Default::default(), window, cx);
6502 });
6503
6504 cx.assert_editor_state(indoc!(
6505 r#"abc
6506 d«ˇef»ghi
6507
6508 j«ˇk»
6509 n«ˇlm»o
6510 "#
6511 ));
6512}
6513
6514#[gpui::test]
6515async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
6516 init_test(cx, |_| {});
6517 let mut cx = EditorTestContext::new(cx).await;
6518
6519 cx.set_state(indoc!(
6520 r#"line onˇe
6521 liˇne two
6522 line three
6523 line four"#
6524 ));
6525
6526 cx.update_editor(|editor, window, cx| {
6527 editor.add_selection_below(&Default::default(), window, cx);
6528 });
6529
6530 // test multiple cursors expand in the same direction
6531 cx.assert_editor_state(indoc!(
6532 r#"line onˇe
6533 liˇne twˇo
6534 liˇne three
6535 line four"#
6536 ));
6537
6538 cx.update_editor(|editor, window, cx| {
6539 editor.add_selection_below(&Default::default(), window, cx);
6540 });
6541
6542 cx.update_editor(|editor, window, cx| {
6543 editor.add_selection_below(&Default::default(), window, cx);
6544 });
6545
6546 // test multiple cursors expand below overflow
6547 cx.assert_editor_state(indoc!(
6548 r#"line onˇe
6549 liˇne twˇo
6550 liˇne thˇree
6551 liˇne foˇur"#
6552 ));
6553
6554 cx.update_editor(|editor, window, cx| {
6555 editor.add_selection_above(&Default::default(), window, cx);
6556 });
6557
6558 // test multiple cursors retrieves back correctly
6559 cx.assert_editor_state(indoc!(
6560 r#"line onˇe
6561 liˇne twˇo
6562 liˇne thˇree
6563 line four"#
6564 ));
6565
6566 cx.update_editor(|editor, window, cx| {
6567 editor.add_selection_above(&Default::default(), window, cx);
6568 });
6569
6570 cx.update_editor(|editor, window, cx| {
6571 editor.add_selection_above(&Default::default(), window, cx);
6572 });
6573
6574 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
6575 cx.assert_editor_state(indoc!(
6576 r#"liˇne onˇe
6577 liˇne two
6578 line three
6579 line four"#
6580 ));
6581
6582 cx.update_editor(|editor, window, cx| {
6583 editor.undo_selection(&Default::default(), window, cx);
6584 });
6585
6586 // test undo
6587 cx.assert_editor_state(indoc!(
6588 r#"line onˇe
6589 liˇne twˇo
6590 line three
6591 line four"#
6592 ));
6593
6594 cx.update_editor(|editor, window, cx| {
6595 editor.redo_selection(&Default::default(), window, cx);
6596 });
6597
6598 // test redo
6599 cx.assert_editor_state(indoc!(
6600 r#"liˇne onˇe
6601 liˇne two
6602 line three
6603 line four"#
6604 ));
6605
6606 cx.set_state(indoc!(
6607 r#"abcd
6608 ef«ghˇ»
6609 ijkl
6610 «mˇ»nop"#
6611 ));
6612
6613 cx.update_editor(|editor, window, cx| {
6614 editor.add_selection_above(&Default::default(), window, cx);
6615 });
6616
6617 // test multiple selections expand in the same direction
6618 cx.assert_editor_state(indoc!(
6619 r#"ab«cdˇ»
6620 ef«ghˇ»
6621 «iˇ»jkl
6622 «mˇ»nop"#
6623 ));
6624
6625 cx.update_editor(|editor, window, cx| {
6626 editor.add_selection_above(&Default::default(), window, cx);
6627 });
6628
6629 // test multiple selection upward overflow
6630 cx.assert_editor_state(indoc!(
6631 r#"ab«cdˇ»
6632 «eˇ»f«ghˇ»
6633 «iˇ»jkl
6634 «mˇ»nop"#
6635 ));
6636
6637 cx.update_editor(|editor, window, cx| {
6638 editor.add_selection_below(&Default::default(), window, cx);
6639 });
6640
6641 // test multiple selection retrieves back correctly
6642 cx.assert_editor_state(indoc!(
6643 r#"abcd
6644 ef«ghˇ»
6645 «iˇ»jkl
6646 «mˇ»nop"#
6647 ));
6648
6649 cx.update_editor(|editor, window, cx| {
6650 editor.add_selection_below(&Default::default(), window, cx);
6651 });
6652
6653 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
6654 cx.assert_editor_state(indoc!(
6655 r#"abcd
6656 ef«ghˇ»
6657 ij«klˇ»
6658 «mˇ»nop"#
6659 ));
6660
6661 cx.update_editor(|editor, window, cx| {
6662 editor.undo_selection(&Default::default(), window, cx);
6663 });
6664
6665 // test undo
6666 cx.assert_editor_state(indoc!(
6667 r#"abcd
6668 ef«ghˇ»
6669 «iˇ»jkl
6670 «mˇ»nop"#
6671 ));
6672
6673 cx.update_editor(|editor, window, cx| {
6674 editor.redo_selection(&Default::default(), window, cx);
6675 });
6676
6677 // test redo
6678 cx.assert_editor_state(indoc!(
6679 r#"abcd
6680 ef«ghˇ»
6681 ij«klˇ»
6682 «mˇ»nop"#
6683 ));
6684}
6685
6686#[gpui::test]
6687async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
6688 init_test(cx, |_| {});
6689 let mut cx = EditorTestContext::new(cx).await;
6690
6691 cx.set_state(indoc!(
6692 r#"line onˇe
6693 liˇne two
6694 line three
6695 line four"#
6696 ));
6697
6698 cx.update_editor(|editor, window, cx| {
6699 editor.add_selection_below(&Default::default(), window, cx);
6700 editor.add_selection_below(&Default::default(), window, cx);
6701 editor.add_selection_below(&Default::default(), window, cx);
6702 });
6703
6704 // initial state with two multi cursor groups
6705 cx.assert_editor_state(indoc!(
6706 r#"line onˇe
6707 liˇne twˇo
6708 liˇne thˇree
6709 liˇne foˇur"#
6710 ));
6711
6712 // add single cursor in middle - simulate opt click
6713 cx.update_editor(|editor, window, cx| {
6714 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
6715 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6716 editor.end_selection(window, cx);
6717 });
6718
6719 cx.assert_editor_state(indoc!(
6720 r#"line onˇe
6721 liˇne twˇo
6722 liˇneˇ thˇree
6723 liˇne foˇur"#
6724 ));
6725
6726 cx.update_editor(|editor, window, cx| {
6727 editor.add_selection_above(&Default::default(), window, cx);
6728 });
6729
6730 // test new added selection expands above and existing selection shrinks
6731 cx.assert_editor_state(indoc!(
6732 r#"line onˇe
6733 liˇneˇ twˇo
6734 liˇneˇ thˇree
6735 line four"#
6736 ));
6737
6738 cx.update_editor(|editor, window, cx| {
6739 editor.add_selection_above(&Default::default(), window, cx);
6740 });
6741
6742 // test new added selection expands above and existing selection shrinks
6743 cx.assert_editor_state(indoc!(
6744 r#"lineˇ onˇe
6745 liˇneˇ twˇo
6746 lineˇ three
6747 line four"#
6748 ));
6749
6750 // intial state with two selection groups
6751 cx.set_state(indoc!(
6752 r#"abcd
6753 ef«ghˇ»
6754 ijkl
6755 «mˇ»nop"#
6756 ));
6757
6758 cx.update_editor(|editor, window, cx| {
6759 editor.add_selection_above(&Default::default(), window, cx);
6760 editor.add_selection_above(&Default::default(), window, cx);
6761 });
6762
6763 cx.assert_editor_state(indoc!(
6764 r#"ab«cdˇ»
6765 «eˇ»f«ghˇ»
6766 «iˇ»jkl
6767 «mˇ»nop"#
6768 ));
6769
6770 // add single selection in middle - simulate opt drag
6771 cx.update_editor(|editor, window, cx| {
6772 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
6773 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6774 editor.update_selection(
6775 DisplayPoint::new(DisplayRow(2), 4),
6776 0,
6777 gpui::Point::<f32>::default(),
6778 window,
6779 cx,
6780 );
6781 editor.end_selection(window, cx);
6782 });
6783
6784 cx.assert_editor_state(indoc!(
6785 r#"ab«cdˇ»
6786 «eˇ»f«ghˇ»
6787 «iˇ»jk«lˇ»
6788 «mˇ»nop"#
6789 ));
6790
6791 cx.update_editor(|editor, window, cx| {
6792 editor.add_selection_below(&Default::default(), window, cx);
6793 });
6794
6795 // test new added selection expands below, others shrinks from above
6796 cx.assert_editor_state(indoc!(
6797 r#"abcd
6798 ef«ghˇ»
6799 «iˇ»jk«lˇ»
6800 «mˇ»no«pˇ»"#
6801 ));
6802}
6803
6804#[gpui::test]
6805async fn test_select_next(cx: &mut TestAppContext) {
6806 init_test(cx, |_| {});
6807
6808 let mut cx = EditorTestContext::new(cx).await;
6809 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6810
6811 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6812 .unwrap();
6813 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6814
6815 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6816 .unwrap();
6817 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6818
6819 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6820 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6821
6822 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6823 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6824
6825 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6826 .unwrap();
6827 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6828
6829 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6830 .unwrap();
6831 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6832
6833 // Test selection direction should be preserved
6834 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6835
6836 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6837 .unwrap();
6838 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
6839}
6840
6841#[gpui::test]
6842async fn test_select_all_matches(cx: &mut TestAppContext) {
6843 init_test(cx, |_| {});
6844
6845 let mut cx = EditorTestContext::new(cx).await;
6846
6847 // Test caret-only selections
6848 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6849 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6850 .unwrap();
6851 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6852
6853 // Test left-to-right selections
6854 cx.set_state("abc\n«abcˇ»\nabc");
6855 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6856 .unwrap();
6857 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
6858
6859 // Test right-to-left selections
6860 cx.set_state("abc\n«ˇabc»\nabc");
6861 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6862 .unwrap();
6863 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
6864
6865 // Test selecting whitespace with caret selection
6866 cx.set_state("abc\nˇ abc\nabc");
6867 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6868 .unwrap();
6869 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6870
6871 // Test selecting whitespace with left-to-right selection
6872 cx.set_state("abc\n«ˇ »abc\nabc");
6873 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6874 .unwrap();
6875 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
6876
6877 // Test no matches with right-to-left selection
6878 cx.set_state("abc\n« ˇ»abc\nabc");
6879 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6880 .unwrap();
6881 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6882
6883 // Test with a single word and clip_at_line_ends=true (#29823)
6884 cx.set_state("aˇbc");
6885 cx.update_editor(|e, window, cx| {
6886 e.set_clip_at_line_ends(true, cx);
6887 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
6888 e.set_clip_at_line_ends(false, cx);
6889 });
6890 cx.assert_editor_state("«abcˇ»");
6891}
6892
6893#[gpui::test]
6894async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
6895 init_test(cx, |_| {});
6896
6897 let mut cx = EditorTestContext::new(cx).await;
6898
6899 let large_body_1 = "\nd".repeat(200);
6900 let large_body_2 = "\ne".repeat(200);
6901
6902 cx.set_state(&format!(
6903 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
6904 ));
6905 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
6906 let scroll_position = editor.scroll_position(cx);
6907 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
6908 scroll_position
6909 });
6910
6911 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6912 .unwrap();
6913 cx.assert_editor_state(&format!(
6914 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
6915 ));
6916 let scroll_position_after_selection =
6917 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
6918 assert_eq!(
6919 initial_scroll_position, scroll_position_after_selection,
6920 "Scroll position should not change after selecting all matches"
6921 );
6922}
6923
6924#[gpui::test]
6925async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
6926 init_test(cx, |_| {});
6927
6928 let mut cx = EditorLspTestContext::new_rust(
6929 lsp::ServerCapabilities {
6930 document_formatting_provider: Some(lsp::OneOf::Left(true)),
6931 ..Default::default()
6932 },
6933 cx,
6934 )
6935 .await;
6936
6937 cx.set_state(indoc! {"
6938 line 1
6939 line 2
6940 linˇe 3
6941 line 4
6942 line 5
6943 "});
6944
6945 // Make an edit
6946 cx.update_editor(|editor, window, cx| {
6947 editor.handle_input("X", window, cx);
6948 });
6949
6950 // Move cursor to a different position
6951 cx.update_editor(|editor, window, cx| {
6952 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6953 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
6954 });
6955 });
6956
6957 cx.assert_editor_state(indoc! {"
6958 line 1
6959 line 2
6960 linXe 3
6961 line 4
6962 liˇne 5
6963 "});
6964
6965 cx.lsp
6966 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
6967 Ok(Some(vec![lsp::TextEdit::new(
6968 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
6969 "PREFIX ".to_string(),
6970 )]))
6971 });
6972
6973 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
6974 .unwrap()
6975 .await
6976 .unwrap();
6977
6978 cx.assert_editor_state(indoc! {"
6979 PREFIX line 1
6980 line 2
6981 linXe 3
6982 line 4
6983 liˇne 5
6984 "});
6985
6986 // Undo formatting
6987 cx.update_editor(|editor, window, cx| {
6988 editor.undo(&Default::default(), window, cx);
6989 });
6990
6991 // Verify cursor moved back to position after edit
6992 cx.assert_editor_state(indoc! {"
6993 line 1
6994 line 2
6995 linXˇe 3
6996 line 4
6997 line 5
6998 "});
6999}
7000
7001#[gpui::test]
7002async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext) {
7003 init_test(cx, |_| {});
7004
7005 let mut cx = EditorTestContext::new(cx).await;
7006
7007 let provider = cx.new(|_| FakeInlineCompletionProvider::default());
7008 cx.update_editor(|editor, window, cx| {
7009 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
7010 });
7011
7012 cx.set_state(indoc! {"
7013 line 1
7014 line 2
7015 linˇe 3
7016 line 4
7017 line 5
7018 line 6
7019 line 7
7020 line 8
7021 line 9
7022 line 10
7023 "});
7024
7025 let snapshot = cx.buffer_snapshot();
7026 let edit_position = snapshot.anchor_after(Point::new(2, 4));
7027
7028 cx.update(|_, cx| {
7029 provider.update(cx, |provider, _| {
7030 provider.set_inline_completion(Some(inline_completion::InlineCompletion {
7031 id: None,
7032 edits: vec![(edit_position..edit_position, "X".into())],
7033 edit_preview: None,
7034 }))
7035 })
7036 });
7037
7038 cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx));
7039 cx.update_editor(|editor, window, cx| {
7040 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
7041 });
7042
7043 cx.assert_editor_state(indoc! {"
7044 line 1
7045 line 2
7046 lineXˇ 3
7047 line 4
7048 line 5
7049 line 6
7050 line 7
7051 line 8
7052 line 9
7053 line 10
7054 "});
7055
7056 cx.update_editor(|editor, window, cx| {
7057 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7058 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
7059 });
7060 });
7061
7062 cx.assert_editor_state(indoc! {"
7063 line 1
7064 line 2
7065 lineX 3
7066 line 4
7067 line 5
7068 line 6
7069 line 7
7070 line 8
7071 line 9
7072 liˇne 10
7073 "});
7074
7075 cx.update_editor(|editor, window, cx| {
7076 editor.undo(&Default::default(), window, cx);
7077 });
7078
7079 cx.assert_editor_state(indoc! {"
7080 line 1
7081 line 2
7082 lineˇ 3
7083 line 4
7084 line 5
7085 line 6
7086 line 7
7087 line 8
7088 line 9
7089 line 10
7090 "});
7091}
7092
7093#[gpui::test]
7094async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
7095 init_test(cx, |_| {});
7096
7097 let mut cx = EditorTestContext::new(cx).await;
7098 cx.set_state(
7099 r#"let foo = 2;
7100lˇet foo = 2;
7101let fooˇ = 2;
7102let foo = 2;
7103let foo = ˇ2;"#,
7104 );
7105
7106 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7107 .unwrap();
7108 cx.assert_editor_state(
7109 r#"let foo = 2;
7110«letˇ» foo = 2;
7111let «fooˇ» = 2;
7112let foo = 2;
7113let foo = «2ˇ»;"#,
7114 );
7115
7116 // noop for multiple selections with different contents
7117 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7118 .unwrap();
7119 cx.assert_editor_state(
7120 r#"let foo = 2;
7121«letˇ» foo = 2;
7122let «fooˇ» = 2;
7123let foo = 2;
7124let foo = «2ˇ»;"#,
7125 );
7126
7127 // Test last selection direction should be preserved
7128 cx.set_state(
7129 r#"let foo = 2;
7130let foo = 2;
7131let «fooˇ» = 2;
7132let «ˇfoo» = 2;
7133let foo = 2;"#,
7134 );
7135
7136 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7137 .unwrap();
7138 cx.assert_editor_state(
7139 r#"let foo = 2;
7140let foo = 2;
7141let «fooˇ» = 2;
7142let «ˇfoo» = 2;
7143let «ˇfoo» = 2;"#,
7144 );
7145}
7146
7147#[gpui::test]
7148async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
7149 init_test(cx, |_| {});
7150
7151 let mut cx =
7152 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
7153
7154 cx.assert_editor_state(indoc! {"
7155 ˇbbb
7156 ccc
7157
7158 bbb
7159 ccc
7160 "});
7161 cx.dispatch_action(SelectPrevious::default());
7162 cx.assert_editor_state(indoc! {"
7163 «bbbˇ»
7164 ccc
7165
7166 bbb
7167 ccc
7168 "});
7169 cx.dispatch_action(SelectPrevious::default());
7170 cx.assert_editor_state(indoc! {"
7171 «bbbˇ»
7172 ccc
7173
7174 «bbbˇ»
7175 ccc
7176 "});
7177}
7178
7179#[gpui::test]
7180async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
7181 init_test(cx, |_| {});
7182
7183 let mut cx = EditorTestContext::new(cx).await;
7184 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7185
7186 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7187 .unwrap();
7188 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7189
7190 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7191 .unwrap();
7192 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7193
7194 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7195 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7196
7197 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7198 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7199
7200 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7201 .unwrap();
7202 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
7203
7204 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7205 .unwrap();
7206 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7207}
7208
7209#[gpui::test]
7210async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
7211 init_test(cx, |_| {});
7212
7213 let mut cx = EditorTestContext::new(cx).await;
7214 cx.set_state("aˇ");
7215
7216 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7217 .unwrap();
7218 cx.assert_editor_state("«aˇ»");
7219 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7220 .unwrap();
7221 cx.assert_editor_state("«aˇ»");
7222}
7223
7224#[gpui::test]
7225async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
7226 init_test(cx, |_| {});
7227
7228 let mut cx = EditorTestContext::new(cx).await;
7229 cx.set_state(
7230 r#"let foo = 2;
7231lˇet foo = 2;
7232let fooˇ = 2;
7233let foo = 2;
7234let foo = ˇ2;"#,
7235 );
7236
7237 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7238 .unwrap();
7239 cx.assert_editor_state(
7240 r#"let foo = 2;
7241«letˇ» foo = 2;
7242let «fooˇ» = 2;
7243let foo = 2;
7244let foo = «2ˇ»;"#,
7245 );
7246
7247 // noop for multiple selections with different contents
7248 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7249 .unwrap();
7250 cx.assert_editor_state(
7251 r#"let foo = 2;
7252«letˇ» foo = 2;
7253let «fooˇ» = 2;
7254let foo = 2;
7255let foo = «2ˇ»;"#,
7256 );
7257}
7258
7259#[gpui::test]
7260async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
7261 init_test(cx, |_| {});
7262
7263 let mut cx = EditorTestContext::new(cx).await;
7264 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7265
7266 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7267 .unwrap();
7268 // selection direction is preserved
7269 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7270
7271 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7272 .unwrap();
7273 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7274
7275 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7276 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7277
7278 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7279 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7280
7281 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7282 .unwrap();
7283 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
7284
7285 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7286 .unwrap();
7287 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
7288}
7289
7290#[gpui::test]
7291async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
7292 init_test(cx, |_| {});
7293
7294 let language = Arc::new(Language::new(
7295 LanguageConfig::default(),
7296 Some(tree_sitter_rust::LANGUAGE.into()),
7297 ));
7298
7299 let text = r#"
7300 use mod1::mod2::{mod3, mod4};
7301
7302 fn fn_1(param1: bool, param2: &str) {
7303 let var1 = "text";
7304 }
7305 "#
7306 .unindent();
7307
7308 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7309 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7310 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7311
7312 editor
7313 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7314 .await;
7315
7316 editor.update_in(cx, |editor, window, cx| {
7317 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7318 s.select_display_ranges([
7319 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
7320 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
7321 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
7322 ]);
7323 });
7324 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7325 });
7326 editor.update(cx, |editor, cx| {
7327 assert_text_with_selections(
7328 editor,
7329 indoc! {r#"
7330 use mod1::mod2::{mod3, «mod4ˇ»};
7331
7332 fn fn_1«ˇ(param1: bool, param2: &str)» {
7333 let var1 = "«ˇtext»";
7334 }
7335 "#},
7336 cx,
7337 );
7338 });
7339
7340 editor.update_in(cx, |editor, window, cx| {
7341 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7342 });
7343 editor.update(cx, |editor, cx| {
7344 assert_text_with_selections(
7345 editor,
7346 indoc! {r#"
7347 use mod1::mod2::«{mod3, mod4}ˇ»;
7348
7349 «ˇfn fn_1(param1: bool, param2: &str) {
7350 let var1 = "text";
7351 }»
7352 "#},
7353 cx,
7354 );
7355 });
7356
7357 editor.update_in(cx, |editor, window, cx| {
7358 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7359 });
7360 assert_eq!(
7361 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7362 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7363 );
7364
7365 // Trying to expand the selected syntax node one more time has no effect.
7366 editor.update_in(cx, |editor, window, cx| {
7367 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7368 });
7369 assert_eq!(
7370 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7371 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7372 );
7373
7374 editor.update_in(cx, |editor, window, cx| {
7375 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7376 });
7377 editor.update(cx, |editor, cx| {
7378 assert_text_with_selections(
7379 editor,
7380 indoc! {r#"
7381 use mod1::mod2::«{mod3, mod4}ˇ»;
7382
7383 «ˇfn fn_1(param1: bool, param2: &str) {
7384 let var1 = "text";
7385 }»
7386 "#},
7387 cx,
7388 );
7389 });
7390
7391 editor.update_in(cx, |editor, window, cx| {
7392 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7393 });
7394 editor.update(cx, |editor, cx| {
7395 assert_text_with_selections(
7396 editor,
7397 indoc! {r#"
7398 use mod1::mod2::{mod3, «mod4ˇ»};
7399
7400 fn fn_1«ˇ(param1: bool, param2: &str)» {
7401 let var1 = "«ˇtext»";
7402 }
7403 "#},
7404 cx,
7405 );
7406 });
7407
7408 editor.update_in(cx, |editor, window, cx| {
7409 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7410 });
7411 editor.update(cx, |editor, cx| {
7412 assert_text_with_selections(
7413 editor,
7414 indoc! {r#"
7415 use mod1::mod2::{mod3, mo«ˇ»d4};
7416
7417 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7418 let var1 = "te«ˇ»xt";
7419 }
7420 "#},
7421 cx,
7422 );
7423 });
7424
7425 // Trying to shrink the selected syntax node one more time has no effect.
7426 editor.update_in(cx, |editor, window, cx| {
7427 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7428 });
7429 editor.update_in(cx, |editor, _, cx| {
7430 assert_text_with_selections(
7431 editor,
7432 indoc! {r#"
7433 use mod1::mod2::{mod3, mo«ˇ»d4};
7434
7435 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7436 let var1 = "te«ˇ»xt";
7437 }
7438 "#},
7439 cx,
7440 );
7441 });
7442
7443 // Ensure that we keep expanding the selection if the larger selection starts or ends within
7444 // a fold.
7445 editor.update_in(cx, |editor, window, cx| {
7446 editor.fold_creases(
7447 vec![
7448 Crease::simple(
7449 Point::new(0, 21)..Point::new(0, 24),
7450 FoldPlaceholder::test(),
7451 ),
7452 Crease::simple(
7453 Point::new(3, 20)..Point::new(3, 22),
7454 FoldPlaceholder::test(),
7455 ),
7456 ],
7457 true,
7458 window,
7459 cx,
7460 );
7461 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7462 });
7463 editor.update(cx, |editor, cx| {
7464 assert_text_with_selections(
7465 editor,
7466 indoc! {r#"
7467 use mod1::mod2::«{mod3, mod4}ˇ»;
7468
7469 fn fn_1«ˇ(param1: bool, param2: &str)» {
7470 let var1 = "«ˇtext»";
7471 }
7472 "#},
7473 cx,
7474 );
7475 });
7476}
7477
7478#[gpui::test]
7479async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
7480 init_test(cx, |_| {});
7481
7482 let language = Arc::new(Language::new(
7483 LanguageConfig::default(),
7484 Some(tree_sitter_rust::LANGUAGE.into()),
7485 ));
7486
7487 let text = "let a = 2;";
7488
7489 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7490 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7491 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7492
7493 editor
7494 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7495 .await;
7496
7497 // Test case 1: Cursor at end of word
7498 editor.update_in(cx, |editor, window, cx| {
7499 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7500 s.select_display_ranges([
7501 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
7502 ]);
7503 });
7504 });
7505 editor.update(cx, |editor, cx| {
7506 assert_text_with_selections(editor, "let aˇ = 2;", cx);
7507 });
7508 editor.update_in(cx, |editor, window, cx| {
7509 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7510 });
7511 editor.update(cx, |editor, cx| {
7512 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
7513 });
7514 editor.update_in(cx, |editor, window, cx| {
7515 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7516 });
7517 editor.update(cx, |editor, cx| {
7518 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7519 });
7520
7521 // Test case 2: Cursor at end of statement
7522 editor.update_in(cx, |editor, window, cx| {
7523 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7524 s.select_display_ranges([
7525 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
7526 ]);
7527 });
7528 });
7529 editor.update(cx, |editor, cx| {
7530 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
7531 });
7532 editor.update_in(cx, |editor, window, cx| {
7533 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7534 });
7535 editor.update(cx, |editor, cx| {
7536 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7537 });
7538}
7539
7540#[gpui::test]
7541async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
7542 init_test(cx, |_| {});
7543
7544 let language = Arc::new(Language::new(
7545 LanguageConfig::default(),
7546 Some(tree_sitter_rust::LANGUAGE.into()),
7547 ));
7548
7549 let text = r#"
7550 use mod1::mod2::{mod3, mod4};
7551
7552 fn fn_1(param1: bool, param2: &str) {
7553 let var1 = "hello world";
7554 }
7555 "#
7556 .unindent();
7557
7558 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7559 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7560 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7561
7562 editor
7563 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7564 .await;
7565
7566 // Test 1: Cursor on a letter of a string word
7567 editor.update_in(cx, |editor, window, cx| {
7568 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7569 s.select_display_ranges([
7570 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
7571 ]);
7572 });
7573 });
7574 editor.update_in(cx, |editor, window, cx| {
7575 assert_text_with_selections(
7576 editor,
7577 indoc! {r#"
7578 use mod1::mod2::{mod3, mod4};
7579
7580 fn fn_1(param1: bool, param2: &str) {
7581 let var1 = "hˇello world";
7582 }
7583 "#},
7584 cx,
7585 );
7586 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7587 assert_text_with_selections(
7588 editor,
7589 indoc! {r#"
7590 use mod1::mod2::{mod3, mod4};
7591
7592 fn fn_1(param1: bool, param2: &str) {
7593 let var1 = "«ˇhello» world";
7594 }
7595 "#},
7596 cx,
7597 );
7598 });
7599
7600 // Test 2: Partial selection within a word
7601 editor.update_in(cx, |editor, window, cx| {
7602 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7603 s.select_display_ranges([
7604 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
7605 ]);
7606 });
7607 });
7608 editor.update_in(cx, |editor, window, cx| {
7609 assert_text_with_selections(
7610 editor,
7611 indoc! {r#"
7612 use mod1::mod2::{mod3, mod4};
7613
7614 fn fn_1(param1: bool, param2: &str) {
7615 let var1 = "h«elˇ»lo world";
7616 }
7617 "#},
7618 cx,
7619 );
7620 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7621 assert_text_with_selections(
7622 editor,
7623 indoc! {r#"
7624 use mod1::mod2::{mod3, mod4};
7625
7626 fn fn_1(param1: bool, param2: &str) {
7627 let var1 = "«ˇhello» world";
7628 }
7629 "#},
7630 cx,
7631 );
7632 });
7633
7634 // Test 3: Complete word already selected
7635 editor.update_in(cx, |editor, window, cx| {
7636 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7637 s.select_display_ranges([
7638 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
7639 ]);
7640 });
7641 });
7642 editor.update_in(cx, |editor, window, cx| {
7643 assert_text_with_selections(
7644 editor,
7645 indoc! {r#"
7646 use mod1::mod2::{mod3, mod4};
7647
7648 fn fn_1(param1: bool, param2: &str) {
7649 let var1 = "«helloˇ» world";
7650 }
7651 "#},
7652 cx,
7653 );
7654 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7655 assert_text_with_selections(
7656 editor,
7657 indoc! {r#"
7658 use mod1::mod2::{mod3, mod4};
7659
7660 fn fn_1(param1: bool, param2: &str) {
7661 let var1 = "«hello worldˇ»";
7662 }
7663 "#},
7664 cx,
7665 );
7666 });
7667
7668 // Test 4: Selection spanning across words
7669 editor.update_in(cx, |editor, window, cx| {
7670 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7671 s.select_display_ranges([
7672 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
7673 ]);
7674 });
7675 });
7676 editor.update_in(cx, |editor, window, cx| {
7677 assert_text_with_selections(
7678 editor,
7679 indoc! {r#"
7680 use mod1::mod2::{mod3, mod4};
7681
7682 fn fn_1(param1: bool, param2: &str) {
7683 let var1 = "hel«lo woˇ»rld";
7684 }
7685 "#},
7686 cx,
7687 );
7688 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7689 assert_text_with_selections(
7690 editor,
7691 indoc! {r#"
7692 use mod1::mod2::{mod3, mod4};
7693
7694 fn fn_1(param1: bool, param2: &str) {
7695 let var1 = "«ˇhello world»";
7696 }
7697 "#},
7698 cx,
7699 );
7700 });
7701
7702 // Test 5: Expansion beyond string
7703 editor.update_in(cx, |editor, window, cx| {
7704 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7705 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7706 assert_text_with_selections(
7707 editor,
7708 indoc! {r#"
7709 use mod1::mod2::{mod3, mod4};
7710
7711 fn fn_1(param1: bool, param2: &str) {
7712 «ˇlet var1 = "hello world";»
7713 }
7714 "#},
7715 cx,
7716 );
7717 });
7718}
7719
7720#[gpui::test]
7721async fn test_fold_function_bodies(cx: &mut TestAppContext) {
7722 init_test(cx, |_| {});
7723
7724 let base_text = r#"
7725 impl A {
7726 // this is an uncommitted comment
7727
7728 fn b() {
7729 c();
7730 }
7731
7732 // this is another uncommitted comment
7733
7734 fn d() {
7735 // e
7736 // f
7737 }
7738 }
7739
7740 fn g() {
7741 // h
7742 }
7743 "#
7744 .unindent();
7745
7746 let text = r#"
7747 ˇimpl A {
7748
7749 fn b() {
7750 c();
7751 }
7752
7753 fn d() {
7754 // e
7755 // f
7756 }
7757 }
7758
7759 fn g() {
7760 // h
7761 }
7762 "#
7763 .unindent();
7764
7765 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7766 cx.set_state(&text);
7767 cx.set_head_text(&base_text);
7768 cx.update_editor(|editor, window, cx| {
7769 editor.expand_all_diff_hunks(&Default::default(), window, cx);
7770 });
7771
7772 cx.assert_state_with_diff(
7773 "
7774 ˇimpl A {
7775 - // this is an uncommitted comment
7776
7777 fn b() {
7778 c();
7779 }
7780
7781 - // this is another uncommitted comment
7782 -
7783 fn d() {
7784 // e
7785 // f
7786 }
7787 }
7788
7789 fn g() {
7790 // h
7791 }
7792 "
7793 .unindent(),
7794 );
7795
7796 let expected_display_text = "
7797 impl A {
7798 // this is an uncommitted comment
7799
7800 fn b() {
7801 ⋯
7802 }
7803
7804 // this is another uncommitted comment
7805
7806 fn d() {
7807 ⋯
7808 }
7809 }
7810
7811 fn g() {
7812 ⋯
7813 }
7814 "
7815 .unindent();
7816
7817 cx.update_editor(|editor, window, cx| {
7818 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
7819 assert_eq!(editor.display_text(cx), expected_display_text);
7820 });
7821}
7822
7823#[gpui::test]
7824async fn test_autoindent(cx: &mut TestAppContext) {
7825 init_test(cx, |_| {});
7826
7827 let language = Arc::new(
7828 Language::new(
7829 LanguageConfig {
7830 brackets: BracketPairConfig {
7831 pairs: vec![
7832 BracketPair {
7833 start: "{".to_string(),
7834 end: "}".to_string(),
7835 close: false,
7836 surround: false,
7837 newline: true,
7838 },
7839 BracketPair {
7840 start: "(".to_string(),
7841 end: ")".to_string(),
7842 close: false,
7843 surround: false,
7844 newline: true,
7845 },
7846 ],
7847 ..Default::default()
7848 },
7849 ..Default::default()
7850 },
7851 Some(tree_sitter_rust::LANGUAGE.into()),
7852 )
7853 .with_indents_query(
7854 r#"
7855 (_ "(" ")" @end) @indent
7856 (_ "{" "}" @end) @indent
7857 "#,
7858 )
7859 .unwrap(),
7860 );
7861
7862 let text = "fn a() {}";
7863
7864 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7865 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7866 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7867 editor
7868 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7869 .await;
7870
7871 editor.update_in(cx, |editor, window, cx| {
7872 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7873 s.select_ranges([5..5, 8..8, 9..9])
7874 });
7875 editor.newline(&Newline, window, cx);
7876 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
7877 assert_eq!(
7878 editor.selections.ranges(cx),
7879 &[
7880 Point::new(1, 4)..Point::new(1, 4),
7881 Point::new(3, 4)..Point::new(3, 4),
7882 Point::new(5, 0)..Point::new(5, 0)
7883 ]
7884 );
7885 });
7886}
7887
7888#[gpui::test]
7889async fn test_autoindent_selections(cx: &mut TestAppContext) {
7890 init_test(cx, |_| {});
7891
7892 {
7893 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7894 cx.set_state(indoc! {"
7895 impl A {
7896
7897 fn b() {}
7898
7899 «fn c() {
7900
7901 }ˇ»
7902 }
7903 "});
7904
7905 cx.update_editor(|editor, window, cx| {
7906 editor.autoindent(&Default::default(), window, cx);
7907 });
7908
7909 cx.assert_editor_state(indoc! {"
7910 impl A {
7911
7912 fn b() {}
7913
7914 «fn c() {
7915
7916 }ˇ»
7917 }
7918 "});
7919 }
7920
7921 {
7922 let mut cx = EditorTestContext::new_multibuffer(
7923 cx,
7924 [indoc! { "
7925 impl A {
7926 «
7927 // a
7928 fn b(){}
7929 »
7930 «
7931 }
7932 fn c(){}
7933 »
7934 "}],
7935 );
7936
7937 let buffer = cx.update_editor(|editor, _, cx| {
7938 let buffer = editor.buffer().update(cx, |buffer, _| {
7939 buffer.all_buffers().iter().next().unwrap().clone()
7940 });
7941 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7942 buffer
7943 });
7944
7945 cx.run_until_parked();
7946 cx.update_editor(|editor, window, cx| {
7947 editor.select_all(&Default::default(), window, cx);
7948 editor.autoindent(&Default::default(), window, cx)
7949 });
7950 cx.run_until_parked();
7951
7952 cx.update(|_, cx| {
7953 assert_eq!(
7954 buffer.read(cx).text(),
7955 indoc! { "
7956 impl A {
7957
7958 // a
7959 fn b(){}
7960
7961
7962 }
7963 fn c(){}
7964
7965 " }
7966 )
7967 });
7968 }
7969}
7970
7971#[gpui::test]
7972async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
7973 init_test(cx, |_| {});
7974
7975 let mut cx = EditorTestContext::new(cx).await;
7976
7977 let language = Arc::new(Language::new(
7978 LanguageConfig {
7979 brackets: BracketPairConfig {
7980 pairs: vec![
7981 BracketPair {
7982 start: "{".to_string(),
7983 end: "}".to_string(),
7984 close: true,
7985 surround: true,
7986 newline: true,
7987 },
7988 BracketPair {
7989 start: "(".to_string(),
7990 end: ")".to_string(),
7991 close: true,
7992 surround: true,
7993 newline: true,
7994 },
7995 BracketPair {
7996 start: "/*".to_string(),
7997 end: " */".to_string(),
7998 close: true,
7999 surround: true,
8000 newline: true,
8001 },
8002 BracketPair {
8003 start: "[".to_string(),
8004 end: "]".to_string(),
8005 close: false,
8006 surround: false,
8007 newline: true,
8008 },
8009 BracketPair {
8010 start: "\"".to_string(),
8011 end: "\"".to_string(),
8012 close: true,
8013 surround: true,
8014 newline: false,
8015 },
8016 BracketPair {
8017 start: "<".to_string(),
8018 end: ">".to_string(),
8019 close: false,
8020 surround: true,
8021 newline: true,
8022 },
8023 ],
8024 ..Default::default()
8025 },
8026 autoclose_before: "})]".to_string(),
8027 ..Default::default()
8028 },
8029 Some(tree_sitter_rust::LANGUAGE.into()),
8030 ));
8031
8032 cx.language_registry().add(language.clone());
8033 cx.update_buffer(|buffer, cx| {
8034 buffer.set_language(Some(language), cx);
8035 });
8036
8037 cx.set_state(
8038 &r#"
8039 🏀ˇ
8040 εˇ
8041 ❤️ˇ
8042 "#
8043 .unindent(),
8044 );
8045
8046 // autoclose multiple nested brackets at multiple cursors
8047 cx.update_editor(|editor, window, cx| {
8048 editor.handle_input("{", window, cx);
8049 editor.handle_input("{", window, cx);
8050 editor.handle_input("{", window, cx);
8051 });
8052 cx.assert_editor_state(
8053 &"
8054 🏀{{{ˇ}}}
8055 ε{{{ˇ}}}
8056 ❤️{{{ˇ}}}
8057 "
8058 .unindent(),
8059 );
8060
8061 // insert a different closing bracket
8062 cx.update_editor(|editor, window, cx| {
8063 editor.handle_input(")", window, cx);
8064 });
8065 cx.assert_editor_state(
8066 &"
8067 🏀{{{)ˇ}}}
8068 ε{{{)ˇ}}}
8069 ❤️{{{)ˇ}}}
8070 "
8071 .unindent(),
8072 );
8073
8074 // skip over the auto-closed brackets when typing a closing bracket
8075 cx.update_editor(|editor, window, cx| {
8076 editor.move_right(&MoveRight, window, cx);
8077 editor.handle_input("}", window, cx);
8078 editor.handle_input("}", window, cx);
8079 editor.handle_input("}", window, cx);
8080 });
8081 cx.assert_editor_state(
8082 &"
8083 🏀{{{)}}}}ˇ
8084 ε{{{)}}}}ˇ
8085 ❤️{{{)}}}}ˇ
8086 "
8087 .unindent(),
8088 );
8089
8090 // autoclose multi-character pairs
8091 cx.set_state(
8092 &"
8093 ˇ
8094 ˇ
8095 "
8096 .unindent(),
8097 );
8098 cx.update_editor(|editor, window, cx| {
8099 editor.handle_input("/", window, cx);
8100 editor.handle_input("*", window, cx);
8101 });
8102 cx.assert_editor_state(
8103 &"
8104 /*ˇ */
8105 /*ˇ */
8106 "
8107 .unindent(),
8108 );
8109
8110 // one cursor autocloses a multi-character pair, one cursor
8111 // does not autoclose.
8112 cx.set_state(
8113 &"
8114 /ˇ
8115 ˇ
8116 "
8117 .unindent(),
8118 );
8119 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
8120 cx.assert_editor_state(
8121 &"
8122 /*ˇ */
8123 *ˇ
8124 "
8125 .unindent(),
8126 );
8127
8128 // Don't autoclose if the next character isn't whitespace and isn't
8129 // listed in the language's "autoclose_before" section.
8130 cx.set_state("ˇa b");
8131 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8132 cx.assert_editor_state("{ˇa b");
8133
8134 // Don't autoclose if `close` is false for the bracket pair
8135 cx.set_state("ˇ");
8136 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
8137 cx.assert_editor_state("[ˇ");
8138
8139 // Surround with brackets if text is selected
8140 cx.set_state("«aˇ» b");
8141 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8142 cx.assert_editor_state("{«aˇ»} b");
8143
8144 // Autoclose when not immediately after a word character
8145 cx.set_state("a ˇ");
8146 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8147 cx.assert_editor_state("a \"ˇ\"");
8148
8149 // Autoclose pair where the start and end characters are the same
8150 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8151 cx.assert_editor_state("a \"\"ˇ");
8152
8153 // Don't autoclose when immediately after a word character
8154 cx.set_state("aˇ");
8155 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8156 cx.assert_editor_state("a\"ˇ");
8157
8158 // Do autoclose when after a non-word character
8159 cx.set_state("{ˇ");
8160 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8161 cx.assert_editor_state("{\"ˇ\"");
8162
8163 // Non identical pairs autoclose regardless of preceding character
8164 cx.set_state("aˇ");
8165 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8166 cx.assert_editor_state("a{ˇ}");
8167
8168 // Don't autoclose pair if autoclose is disabled
8169 cx.set_state("ˇ");
8170 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8171 cx.assert_editor_state("<ˇ");
8172
8173 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
8174 cx.set_state("«aˇ» b");
8175 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8176 cx.assert_editor_state("<«aˇ»> b");
8177}
8178
8179#[gpui::test]
8180async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
8181 init_test(cx, |settings| {
8182 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8183 });
8184
8185 let mut cx = EditorTestContext::new(cx).await;
8186
8187 let language = Arc::new(Language::new(
8188 LanguageConfig {
8189 brackets: BracketPairConfig {
8190 pairs: vec![
8191 BracketPair {
8192 start: "{".to_string(),
8193 end: "}".to_string(),
8194 close: true,
8195 surround: true,
8196 newline: true,
8197 },
8198 BracketPair {
8199 start: "(".to_string(),
8200 end: ")".to_string(),
8201 close: true,
8202 surround: true,
8203 newline: true,
8204 },
8205 BracketPair {
8206 start: "[".to_string(),
8207 end: "]".to_string(),
8208 close: false,
8209 surround: false,
8210 newline: true,
8211 },
8212 ],
8213 ..Default::default()
8214 },
8215 autoclose_before: "})]".to_string(),
8216 ..Default::default()
8217 },
8218 Some(tree_sitter_rust::LANGUAGE.into()),
8219 ));
8220
8221 cx.language_registry().add(language.clone());
8222 cx.update_buffer(|buffer, cx| {
8223 buffer.set_language(Some(language), cx);
8224 });
8225
8226 cx.set_state(
8227 &"
8228 ˇ
8229 ˇ
8230 ˇ
8231 "
8232 .unindent(),
8233 );
8234
8235 // ensure only matching closing brackets are skipped over
8236 cx.update_editor(|editor, window, cx| {
8237 editor.handle_input("}", window, cx);
8238 editor.move_left(&MoveLeft, window, cx);
8239 editor.handle_input(")", window, cx);
8240 editor.move_left(&MoveLeft, window, cx);
8241 });
8242 cx.assert_editor_state(
8243 &"
8244 ˇ)}
8245 ˇ)}
8246 ˇ)}
8247 "
8248 .unindent(),
8249 );
8250
8251 // skip-over closing brackets at multiple cursors
8252 cx.update_editor(|editor, window, cx| {
8253 editor.handle_input(")", window, cx);
8254 editor.handle_input("}", window, cx);
8255 });
8256 cx.assert_editor_state(
8257 &"
8258 )}ˇ
8259 )}ˇ
8260 )}ˇ
8261 "
8262 .unindent(),
8263 );
8264
8265 // ignore non-close brackets
8266 cx.update_editor(|editor, window, cx| {
8267 editor.handle_input("]", window, cx);
8268 editor.move_left(&MoveLeft, window, cx);
8269 editor.handle_input("]", window, cx);
8270 });
8271 cx.assert_editor_state(
8272 &"
8273 )}]ˇ]
8274 )}]ˇ]
8275 )}]ˇ]
8276 "
8277 .unindent(),
8278 );
8279}
8280
8281#[gpui::test]
8282async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
8283 init_test(cx, |_| {});
8284
8285 let mut cx = EditorTestContext::new(cx).await;
8286
8287 let html_language = Arc::new(
8288 Language::new(
8289 LanguageConfig {
8290 name: "HTML".into(),
8291 brackets: BracketPairConfig {
8292 pairs: vec![
8293 BracketPair {
8294 start: "<".into(),
8295 end: ">".into(),
8296 close: true,
8297 ..Default::default()
8298 },
8299 BracketPair {
8300 start: "{".into(),
8301 end: "}".into(),
8302 close: true,
8303 ..Default::default()
8304 },
8305 BracketPair {
8306 start: "(".into(),
8307 end: ")".into(),
8308 close: true,
8309 ..Default::default()
8310 },
8311 ],
8312 ..Default::default()
8313 },
8314 autoclose_before: "})]>".into(),
8315 ..Default::default()
8316 },
8317 Some(tree_sitter_html::LANGUAGE.into()),
8318 )
8319 .with_injection_query(
8320 r#"
8321 (script_element
8322 (raw_text) @injection.content
8323 (#set! injection.language "javascript"))
8324 "#,
8325 )
8326 .unwrap(),
8327 );
8328
8329 let javascript_language = Arc::new(Language::new(
8330 LanguageConfig {
8331 name: "JavaScript".into(),
8332 brackets: BracketPairConfig {
8333 pairs: vec![
8334 BracketPair {
8335 start: "/*".into(),
8336 end: " */".into(),
8337 close: true,
8338 ..Default::default()
8339 },
8340 BracketPair {
8341 start: "{".into(),
8342 end: "}".into(),
8343 close: true,
8344 ..Default::default()
8345 },
8346 BracketPair {
8347 start: "(".into(),
8348 end: ")".into(),
8349 close: true,
8350 ..Default::default()
8351 },
8352 ],
8353 ..Default::default()
8354 },
8355 autoclose_before: "})]>".into(),
8356 ..Default::default()
8357 },
8358 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8359 ));
8360
8361 cx.language_registry().add(html_language.clone());
8362 cx.language_registry().add(javascript_language.clone());
8363
8364 cx.update_buffer(|buffer, cx| {
8365 buffer.set_language(Some(html_language), cx);
8366 });
8367
8368 cx.set_state(
8369 &r#"
8370 <body>ˇ
8371 <script>
8372 var x = 1;ˇ
8373 </script>
8374 </body>ˇ
8375 "#
8376 .unindent(),
8377 );
8378
8379 // Precondition: different languages are active at different locations.
8380 cx.update_editor(|editor, window, cx| {
8381 let snapshot = editor.snapshot(window, cx);
8382 let cursors = editor.selections.ranges::<usize>(cx);
8383 let languages = cursors
8384 .iter()
8385 .map(|c| snapshot.language_at(c.start).unwrap().name())
8386 .collect::<Vec<_>>();
8387 assert_eq!(
8388 languages,
8389 &["HTML".into(), "JavaScript".into(), "HTML".into()]
8390 );
8391 });
8392
8393 // Angle brackets autoclose in HTML, but not JavaScript.
8394 cx.update_editor(|editor, window, cx| {
8395 editor.handle_input("<", window, cx);
8396 editor.handle_input("a", window, cx);
8397 });
8398 cx.assert_editor_state(
8399 &r#"
8400 <body><aˇ>
8401 <script>
8402 var x = 1;<aˇ
8403 </script>
8404 </body><aˇ>
8405 "#
8406 .unindent(),
8407 );
8408
8409 // Curly braces and parens autoclose in both HTML and JavaScript.
8410 cx.update_editor(|editor, window, cx| {
8411 editor.handle_input(" b=", window, cx);
8412 editor.handle_input("{", window, cx);
8413 editor.handle_input("c", window, cx);
8414 editor.handle_input("(", window, cx);
8415 });
8416 cx.assert_editor_state(
8417 &r#"
8418 <body><a b={c(ˇ)}>
8419 <script>
8420 var x = 1;<a b={c(ˇ)}
8421 </script>
8422 </body><a b={c(ˇ)}>
8423 "#
8424 .unindent(),
8425 );
8426
8427 // Brackets that were already autoclosed are skipped.
8428 cx.update_editor(|editor, window, cx| {
8429 editor.handle_input(")", window, cx);
8430 editor.handle_input("d", window, cx);
8431 editor.handle_input("}", window, cx);
8432 });
8433 cx.assert_editor_state(
8434 &r#"
8435 <body><a b={c()d}ˇ>
8436 <script>
8437 var x = 1;<a b={c()d}ˇ
8438 </script>
8439 </body><a b={c()d}ˇ>
8440 "#
8441 .unindent(),
8442 );
8443 cx.update_editor(|editor, window, cx| {
8444 editor.handle_input(">", window, cx);
8445 });
8446 cx.assert_editor_state(
8447 &r#"
8448 <body><a b={c()d}>ˇ
8449 <script>
8450 var x = 1;<a b={c()d}>ˇ
8451 </script>
8452 </body><a b={c()d}>ˇ
8453 "#
8454 .unindent(),
8455 );
8456
8457 // Reset
8458 cx.set_state(
8459 &r#"
8460 <body>ˇ
8461 <script>
8462 var x = 1;ˇ
8463 </script>
8464 </body>ˇ
8465 "#
8466 .unindent(),
8467 );
8468
8469 cx.update_editor(|editor, window, cx| {
8470 editor.handle_input("<", window, cx);
8471 });
8472 cx.assert_editor_state(
8473 &r#"
8474 <body><ˇ>
8475 <script>
8476 var x = 1;<ˇ
8477 </script>
8478 </body><ˇ>
8479 "#
8480 .unindent(),
8481 );
8482
8483 // When backspacing, the closing angle brackets are removed.
8484 cx.update_editor(|editor, window, cx| {
8485 editor.backspace(&Backspace, window, cx);
8486 });
8487 cx.assert_editor_state(
8488 &r#"
8489 <body>ˇ
8490 <script>
8491 var x = 1;ˇ
8492 </script>
8493 </body>ˇ
8494 "#
8495 .unindent(),
8496 );
8497
8498 // Block comments autoclose in JavaScript, but not HTML.
8499 cx.update_editor(|editor, window, cx| {
8500 editor.handle_input("/", window, cx);
8501 editor.handle_input("*", window, cx);
8502 });
8503 cx.assert_editor_state(
8504 &r#"
8505 <body>/*ˇ
8506 <script>
8507 var x = 1;/*ˇ */
8508 </script>
8509 </body>/*ˇ
8510 "#
8511 .unindent(),
8512 );
8513}
8514
8515#[gpui::test]
8516async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
8517 init_test(cx, |_| {});
8518
8519 let mut cx = EditorTestContext::new(cx).await;
8520
8521 let rust_language = Arc::new(
8522 Language::new(
8523 LanguageConfig {
8524 name: "Rust".into(),
8525 brackets: serde_json::from_value(json!([
8526 { "start": "{", "end": "}", "close": true, "newline": true },
8527 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
8528 ]))
8529 .unwrap(),
8530 autoclose_before: "})]>".into(),
8531 ..Default::default()
8532 },
8533 Some(tree_sitter_rust::LANGUAGE.into()),
8534 )
8535 .with_override_query("(string_literal) @string")
8536 .unwrap(),
8537 );
8538
8539 cx.language_registry().add(rust_language.clone());
8540 cx.update_buffer(|buffer, cx| {
8541 buffer.set_language(Some(rust_language), cx);
8542 });
8543
8544 cx.set_state(
8545 &r#"
8546 let x = ˇ
8547 "#
8548 .unindent(),
8549 );
8550
8551 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
8552 cx.update_editor(|editor, window, cx| {
8553 editor.handle_input("\"", window, cx);
8554 });
8555 cx.assert_editor_state(
8556 &r#"
8557 let x = "ˇ"
8558 "#
8559 .unindent(),
8560 );
8561
8562 // Inserting another quotation mark. The cursor moves across the existing
8563 // automatically-inserted quotation mark.
8564 cx.update_editor(|editor, window, cx| {
8565 editor.handle_input("\"", window, cx);
8566 });
8567 cx.assert_editor_state(
8568 &r#"
8569 let x = ""ˇ
8570 "#
8571 .unindent(),
8572 );
8573
8574 // Reset
8575 cx.set_state(
8576 &r#"
8577 let x = ˇ
8578 "#
8579 .unindent(),
8580 );
8581
8582 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
8583 cx.update_editor(|editor, window, cx| {
8584 editor.handle_input("\"", window, cx);
8585 editor.handle_input(" ", window, cx);
8586 editor.move_left(&Default::default(), window, cx);
8587 editor.handle_input("\\", window, cx);
8588 editor.handle_input("\"", window, cx);
8589 });
8590 cx.assert_editor_state(
8591 &r#"
8592 let x = "\"ˇ "
8593 "#
8594 .unindent(),
8595 );
8596
8597 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
8598 // mark. Nothing is inserted.
8599 cx.update_editor(|editor, window, cx| {
8600 editor.move_right(&Default::default(), window, cx);
8601 editor.handle_input("\"", window, cx);
8602 });
8603 cx.assert_editor_state(
8604 &r#"
8605 let x = "\" "ˇ
8606 "#
8607 .unindent(),
8608 );
8609}
8610
8611#[gpui::test]
8612async fn test_surround_with_pair(cx: &mut TestAppContext) {
8613 init_test(cx, |_| {});
8614
8615 let language = Arc::new(Language::new(
8616 LanguageConfig {
8617 brackets: BracketPairConfig {
8618 pairs: vec![
8619 BracketPair {
8620 start: "{".to_string(),
8621 end: "}".to_string(),
8622 close: true,
8623 surround: true,
8624 newline: true,
8625 },
8626 BracketPair {
8627 start: "/* ".to_string(),
8628 end: "*/".to_string(),
8629 close: true,
8630 surround: true,
8631 ..Default::default()
8632 },
8633 ],
8634 ..Default::default()
8635 },
8636 ..Default::default()
8637 },
8638 Some(tree_sitter_rust::LANGUAGE.into()),
8639 ));
8640
8641 let text = r#"
8642 a
8643 b
8644 c
8645 "#
8646 .unindent();
8647
8648 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8649 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8650 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8651 editor
8652 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8653 .await;
8654
8655 editor.update_in(cx, |editor, window, cx| {
8656 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8657 s.select_display_ranges([
8658 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8659 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8660 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
8661 ])
8662 });
8663
8664 editor.handle_input("{", window, cx);
8665 editor.handle_input("{", window, cx);
8666 editor.handle_input("{", window, cx);
8667 assert_eq!(
8668 editor.text(cx),
8669 "
8670 {{{a}}}
8671 {{{b}}}
8672 {{{c}}}
8673 "
8674 .unindent()
8675 );
8676 assert_eq!(
8677 editor.selections.display_ranges(cx),
8678 [
8679 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
8680 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
8681 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
8682 ]
8683 );
8684
8685 editor.undo(&Undo, window, cx);
8686 editor.undo(&Undo, window, cx);
8687 editor.undo(&Undo, window, cx);
8688 assert_eq!(
8689 editor.text(cx),
8690 "
8691 a
8692 b
8693 c
8694 "
8695 .unindent()
8696 );
8697 assert_eq!(
8698 editor.selections.display_ranges(cx),
8699 [
8700 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8701 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8702 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8703 ]
8704 );
8705
8706 // Ensure inserting the first character of a multi-byte bracket pair
8707 // doesn't surround the selections with the bracket.
8708 editor.handle_input("/", window, cx);
8709 assert_eq!(
8710 editor.text(cx),
8711 "
8712 /
8713 /
8714 /
8715 "
8716 .unindent()
8717 );
8718 assert_eq!(
8719 editor.selections.display_ranges(cx),
8720 [
8721 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8722 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8723 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8724 ]
8725 );
8726
8727 editor.undo(&Undo, window, cx);
8728 assert_eq!(
8729 editor.text(cx),
8730 "
8731 a
8732 b
8733 c
8734 "
8735 .unindent()
8736 );
8737 assert_eq!(
8738 editor.selections.display_ranges(cx),
8739 [
8740 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8741 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8742 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8743 ]
8744 );
8745
8746 // Ensure inserting the last character of a multi-byte bracket pair
8747 // doesn't surround the selections with the bracket.
8748 editor.handle_input("*", window, cx);
8749 assert_eq!(
8750 editor.text(cx),
8751 "
8752 *
8753 *
8754 *
8755 "
8756 .unindent()
8757 );
8758 assert_eq!(
8759 editor.selections.display_ranges(cx),
8760 [
8761 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8762 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8763 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8764 ]
8765 );
8766 });
8767}
8768
8769#[gpui::test]
8770async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
8771 init_test(cx, |_| {});
8772
8773 let language = Arc::new(Language::new(
8774 LanguageConfig {
8775 brackets: BracketPairConfig {
8776 pairs: vec![BracketPair {
8777 start: "{".to_string(),
8778 end: "}".to_string(),
8779 close: true,
8780 surround: true,
8781 newline: true,
8782 }],
8783 ..Default::default()
8784 },
8785 autoclose_before: "}".to_string(),
8786 ..Default::default()
8787 },
8788 Some(tree_sitter_rust::LANGUAGE.into()),
8789 ));
8790
8791 let text = r#"
8792 a
8793 b
8794 c
8795 "#
8796 .unindent();
8797
8798 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8799 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8800 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8801 editor
8802 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8803 .await;
8804
8805 editor.update_in(cx, |editor, window, cx| {
8806 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8807 s.select_ranges([
8808 Point::new(0, 1)..Point::new(0, 1),
8809 Point::new(1, 1)..Point::new(1, 1),
8810 Point::new(2, 1)..Point::new(2, 1),
8811 ])
8812 });
8813
8814 editor.handle_input("{", window, cx);
8815 editor.handle_input("{", window, cx);
8816 editor.handle_input("_", window, cx);
8817 assert_eq!(
8818 editor.text(cx),
8819 "
8820 a{{_}}
8821 b{{_}}
8822 c{{_}}
8823 "
8824 .unindent()
8825 );
8826 assert_eq!(
8827 editor.selections.ranges::<Point>(cx),
8828 [
8829 Point::new(0, 4)..Point::new(0, 4),
8830 Point::new(1, 4)..Point::new(1, 4),
8831 Point::new(2, 4)..Point::new(2, 4)
8832 ]
8833 );
8834
8835 editor.backspace(&Default::default(), window, cx);
8836 editor.backspace(&Default::default(), window, cx);
8837 assert_eq!(
8838 editor.text(cx),
8839 "
8840 a{}
8841 b{}
8842 c{}
8843 "
8844 .unindent()
8845 );
8846 assert_eq!(
8847 editor.selections.ranges::<Point>(cx),
8848 [
8849 Point::new(0, 2)..Point::new(0, 2),
8850 Point::new(1, 2)..Point::new(1, 2),
8851 Point::new(2, 2)..Point::new(2, 2)
8852 ]
8853 );
8854
8855 editor.delete_to_previous_word_start(&Default::default(), window, cx);
8856 assert_eq!(
8857 editor.text(cx),
8858 "
8859 a
8860 b
8861 c
8862 "
8863 .unindent()
8864 );
8865 assert_eq!(
8866 editor.selections.ranges::<Point>(cx),
8867 [
8868 Point::new(0, 1)..Point::new(0, 1),
8869 Point::new(1, 1)..Point::new(1, 1),
8870 Point::new(2, 1)..Point::new(2, 1)
8871 ]
8872 );
8873 });
8874}
8875
8876#[gpui::test]
8877async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
8878 init_test(cx, |settings| {
8879 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8880 });
8881
8882 let mut cx = EditorTestContext::new(cx).await;
8883
8884 let language = Arc::new(Language::new(
8885 LanguageConfig {
8886 brackets: BracketPairConfig {
8887 pairs: vec![
8888 BracketPair {
8889 start: "{".to_string(),
8890 end: "}".to_string(),
8891 close: true,
8892 surround: true,
8893 newline: true,
8894 },
8895 BracketPair {
8896 start: "(".to_string(),
8897 end: ")".to_string(),
8898 close: true,
8899 surround: true,
8900 newline: true,
8901 },
8902 BracketPair {
8903 start: "[".to_string(),
8904 end: "]".to_string(),
8905 close: false,
8906 surround: true,
8907 newline: true,
8908 },
8909 ],
8910 ..Default::default()
8911 },
8912 autoclose_before: "})]".to_string(),
8913 ..Default::default()
8914 },
8915 Some(tree_sitter_rust::LANGUAGE.into()),
8916 ));
8917
8918 cx.language_registry().add(language.clone());
8919 cx.update_buffer(|buffer, cx| {
8920 buffer.set_language(Some(language), cx);
8921 });
8922
8923 cx.set_state(
8924 &"
8925 {(ˇ)}
8926 [[ˇ]]
8927 {(ˇ)}
8928 "
8929 .unindent(),
8930 );
8931
8932 cx.update_editor(|editor, window, cx| {
8933 editor.backspace(&Default::default(), window, cx);
8934 editor.backspace(&Default::default(), window, cx);
8935 });
8936
8937 cx.assert_editor_state(
8938 &"
8939 ˇ
8940 ˇ]]
8941 ˇ
8942 "
8943 .unindent(),
8944 );
8945
8946 cx.update_editor(|editor, window, cx| {
8947 editor.handle_input("{", window, cx);
8948 editor.handle_input("{", window, cx);
8949 editor.move_right(&MoveRight, window, cx);
8950 editor.move_right(&MoveRight, window, cx);
8951 editor.move_left(&MoveLeft, window, cx);
8952 editor.move_left(&MoveLeft, window, cx);
8953 editor.backspace(&Default::default(), window, cx);
8954 });
8955
8956 cx.assert_editor_state(
8957 &"
8958 {ˇ}
8959 {ˇ}]]
8960 {ˇ}
8961 "
8962 .unindent(),
8963 );
8964
8965 cx.update_editor(|editor, window, cx| {
8966 editor.backspace(&Default::default(), window, cx);
8967 });
8968
8969 cx.assert_editor_state(
8970 &"
8971 ˇ
8972 ˇ]]
8973 ˇ
8974 "
8975 .unindent(),
8976 );
8977}
8978
8979#[gpui::test]
8980async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
8981 init_test(cx, |_| {});
8982
8983 let language = Arc::new(Language::new(
8984 LanguageConfig::default(),
8985 Some(tree_sitter_rust::LANGUAGE.into()),
8986 ));
8987
8988 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
8989 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8990 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8991 editor
8992 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8993 .await;
8994
8995 editor.update_in(cx, |editor, window, cx| {
8996 editor.set_auto_replace_emoji_shortcode(true);
8997
8998 editor.handle_input("Hello ", window, cx);
8999 editor.handle_input(":wave", window, cx);
9000 assert_eq!(editor.text(cx), "Hello :wave".unindent());
9001
9002 editor.handle_input(":", window, cx);
9003 assert_eq!(editor.text(cx), "Hello 👋".unindent());
9004
9005 editor.handle_input(" :smile", window, cx);
9006 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
9007
9008 editor.handle_input(":", window, cx);
9009 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
9010
9011 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
9012 editor.handle_input(":wave", window, cx);
9013 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
9014
9015 editor.handle_input(":", window, cx);
9016 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
9017
9018 editor.handle_input(":1", window, cx);
9019 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
9020
9021 editor.handle_input(":", window, cx);
9022 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
9023
9024 // Ensure shortcode does not get replaced when it is part of a word
9025 editor.handle_input(" Test:wave", window, cx);
9026 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
9027
9028 editor.handle_input(":", window, cx);
9029 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
9030
9031 editor.set_auto_replace_emoji_shortcode(false);
9032
9033 // Ensure shortcode does not get replaced when auto replace is off
9034 editor.handle_input(" :wave", window, cx);
9035 assert_eq!(
9036 editor.text(cx),
9037 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
9038 );
9039
9040 editor.handle_input(":", window, cx);
9041 assert_eq!(
9042 editor.text(cx),
9043 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
9044 );
9045 });
9046}
9047
9048#[gpui::test]
9049async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
9050 init_test(cx, |_| {});
9051
9052 let (text, insertion_ranges) = marked_text_ranges(
9053 indoc! {"
9054 ˇ
9055 "},
9056 false,
9057 );
9058
9059 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
9060 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9061
9062 _ = editor.update_in(cx, |editor, window, cx| {
9063 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
9064
9065 editor
9066 .insert_snippet(&insertion_ranges, snippet, window, cx)
9067 .unwrap();
9068
9069 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
9070 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
9071 assert_eq!(editor.text(cx), expected_text);
9072 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
9073 }
9074
9075 assert(
9076 editor,
9077 cx,
9078 indoc! {"
9079 type «» =•
9080 "},
9081 );
9082
9083 assert!(editor.context_menu_visible(), "There should be a matches");
9084 });
9085}
9086
9087#[gpui::test]
9088async fn test_snippets(cx: &mut TestAppContext) {
9089 init_test(cx, |_| {});
9090
9091 let mut cx = EditorTestContext::new(cx).await;
9092
9093 cx.set_state(indoc! {"
9094 a.ˇ b
9095 a.ˇ b
9096 a.ˇ b
9097 "});
9098
9099 cx.update_editor(|editor, window, cx| {
9100 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
9101 let insertion_ranges = editor
9102 .selections
9103 .all(cx)
9104 .iter()
9105 .map(|s| s.range().clone())
9106 .collect::<Vec<_>>();
9107 editor
9108 .insert_snippet(&insertion_ranges, snippet, window, cx)
9109 .unwrap();
9110 });
9111
9112 cx.assert_editor_state(indoc! {"
9113 a.f(«oneˇ», two, «threeˇ») b
9114 a.f(«oneˇ», two, «threeˇ») b
9115 a.f(«oneˇ», two, «threeˇ») b
9116 "});
9117
9118 // Can't move earlier than the first tab stop
9119 cx.update_editor(|editor, window, cx| {
9120 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9121 });
9122 cx.assert_editor_state(indoc! {"
9123 a.f(«oneˇ», two, «threeˇ») b
9124 a.f(«oneˇ», two, «threeˇ») b
9125 a.f(«oneˇ», two, «threeˇ») b
9126 "});
9127
9128 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9129 cx.assert_editor_state(indoc! {"
9130 a.f(one, «twoˇ», three) b
9131 a.f(one, «twoˇ», three) b
9132 a.f(one, «twoˇ», three) b
9133 "});
9134
9135 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
9136 cx.assert_editor_state(indoc! {"
9137 a.f(«oneˇ», two, «threeˇ») b
9138 a.f(«oneˇ», two, «threeˇ») b
9139 a.f(«oneˇ», two, «threeˇ») b
9140 "});
9141
9142 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9143 cx.assert_editor_state(indoc! {"
9144 a.f(one, «twoˇ», three) b
9145 a.f(one, «twoˇ», three) b
9146 a.f(one, «twoˇ», three) b
9147 "});
9148 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9149 cx.assert_editor_state(indoc! {"
9150 a.f(one, two, three)ˇ b
9151 a.f(one, two, three)ˇ b
9152 a.f(one, two, three)ˇ b
9153 "});
9154
9155 // As soon as the last tab stop is reached, snippet state is gone
9156 cx.update_editor(|editor, window, cx| {
9157 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9158 });
9159 cx.assert_editor_state(indoc! {"
9160 a.f(one, two, three)ˇ b
9161 a.f(one, two, three)ˇ b
9162 a.f(one, two, three)ˇ b
9163 "});
9164}
9165
9166#[gpui::test]
9167async fn test_snippet_indentation(cx: &mut TestAppContext) {
9168 init_test(cx, |_| {});
9169
9170 let mut cx = EditorTestContext::new(cx).await;
9171
9172 cx.update_editor(|editor, window, cx| {
9173 let snippet = Snippet::parse(indoc! {"
9174 /*
9175 * Multiline comment with leading indentation
9176 *
9177 * $1
9178 */
9179 $0"})
9180 .unwrap();
9181 let insertion_ranges = editor
9182 .selections
9183 .all(cx)
9184 .iter()
9185 .map(|s| s.range().clone())
9186 .collect::<Vec<_>>();
9187 editor
9188 .insert_snippet(&insertion_ranges, snippet, window, cx)
9189 .unwrap();
9190 });
9191
9192 cx.assert_editor_state(indoc! {"
9193 /*
9194 * Multiline comment with leading indentation
9195 *
9196 * ˇ
9197 */
9198 "});
9199
9200 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9201 cx.assert_editor_state(indoc! {"
9202 /*
9203 * Multiline comment with leading indentation
9204 *
9205 *•
9206 */
9207 ˇ"});
9208}
9209
9210#[gpui::test]
9211async fn test_document_format_during_save(cx: &mut TestAppContext) {
9212 init_test(cx, |_| {});
9213
9214 let fs = FakeFs::new(cx.executor());
9215 fs.insert_file(path!("/file.rs"), Default::default()).await;
9216
9217 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
9218
9219 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9220 language_registry.add(rust_lang());
9221 let mut fake_servers = language_registry.register_fake_lsp(
9222 "Rust",
9223 FakeLspAdapter {
9224 capabilities: lsp::ServerCapabilities {
9225 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9226 ..Default::default()
9227 },
9228 ..Default::default()
9229 },
9230 );
9231
9232 let buffer = project
9233 .update(cx, |project, cx| {
9234 project.open_local_buffer(path!("/file.rs"), cx)
9235 })
9236 .await
9237 .unwrap();
9238
9239 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9240 let (editor, cx) = cx.add_window_view(|window, cx| {
9241 build_editor_with_project(project.clone(), buffer, window, cx)
9242 });
9243 editor.update_in(cx, |editor, window, cx| {
9244 editor.set_text("one\ntwo\nthree\n", window, cx)
9245 });
9246 assert!(cx.read(|cx| editor.is_dirty(cx)));
9247
9248 cx.executor().start_waiting();
9249 let fake_server = fake_servers.next().await.unwrap();
9250
9251 {
9252 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9253 move |params, _| async move {
9254 assert_eq!(
9255 params.text_document.uri,
9256 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9257 );
9258 assert_eq!(params.options.tab_size, 4);
9259 Ok(Some(vec![lsp::TextEdit::new(
9260 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9261 ", ".to_string(),
9262 )]))
9263 },
9264 );
9265 let save = editor
9266 .update_in(cx, |editor, window, cx| {
9267 editor.save(
9268 SaveOptions {
9269 format: true,
9270 autosave: false,
9271 },
9272 project.clone(),
9273 window,
9274 cx,
9275 )
9276 })
9277 .unwrap();
9278 cx.executor().start_waiting();
9279 save.await;
9280
9281 assert_eq!(
9282 editor.update(cx, |editor, cx| editor.text(cx)),
9283 "one, two\nthree\n"
9284 );
9285 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9286 }
9287
9288 {
9289 editor.update_in(cx, |editor, window, cx| {
9290 editor.set_text("one\ntwo\nthree\n", window, cx)
9291 });
9292 assert!(cx.read(|cx| editor.is_dirty(cx)));
9293
9294 // Ensure we can still save even if formatting hangs.
9295 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9296 move |params, _| async move {
9297 assert_eq!(
9298 params.text_document.uri,
9299 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9300 );
9301 futures::future::pending::<()>().await;
9302 unreachable!()
9303 },
9304 );
9305 let save = editor
9306 .update_in(cx, |editor, window, cx| {
9307 editor.save(
9308 SaveOptions {
9309 format: true,
9310 autosave: false,
9311 },
9312 project.clone(),
9313 window,
9314 cx,
9315 )
9316 })
9317 .unwrap();
9318 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9319 cx.executor().start_waiting();
9320 save.await;
9321 assert_eq!(
9322 editor.update(cx, |editor, cx| editor.text(cx)),
9323 "one\ntwo\nthree\n"
9324 );
9325 }
9326
9327 // Set rust language override and assert overridden tabsize is sent to language server
9328 update_test_language_settings(cx, |settings| {
9329 settings.languages.0.insert(
9330 "Rust".into(),
9331 LanguageSettingsContent {
9332 tab_size: NonZeroU32::new(8),
9333 ..Default::default()
9334 },
9335 );
9336 });
9337
9338 {
9339 editor.update_in(cx, |editor, window, cx| {
9340 editor.set_text("somehting_new\n", window, cx)
9341 });
9342 assert!(cx.read(|cx| editor.is_dirty(cx)));
9343 let _formatting_request_signal = fake_server
9344 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9345 assert_eq!(
9346 params.text_document.uri,
9347 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9348 );
9349 assert_eq!(params.options.tab_size, 8);
9350 Ok(Some(vec![]))
9351 });
9352 let save = editor
9353 .update_in(cx, |editor, window, cx| {
9354 editor.save(
9355 SaveOptions {
9356 format: true,
9357 autosave: false,
9358 },
9359 project.clone(),
9360 window,
9361 cx,
9362 )
9363 })
9364 .unwrap();
9365 cx.executor().start_waiting();
9366 save.await;
9367 }
9368}
9369
9370#[gpui::test]
9371async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
9372 init_test(cx, |_| {});
9373
9374 let cols = 4;
9375 let rows = 10;
9376 let sample_text_1 = sample_text(rows, cols, 'a');
9377 assert_eq!(
9378 sample_text_1,
9379 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
9380 );
9381 let sample_text_2 = sample_text(rows, cols, 'l');
9382 assert_eq!(
9383 sample_text_2,
9384 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
9385 );
9386 let sample_text_3 = sample_text(rows, cols, 'v');
9387 assert_eq!(
9388 sample_text_3,
9389 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
9390 );
9391
9392 let fs = FakeFs::new(cx.executor());
9393 fs.insert_tree(
9394 path!("/a"),
9395 json!({
9396 "main.rs": sample_text_1,
9397 "other.rs": sample_text_2,
9398 "lib.rs": sample_text_3,
9399 }),
9400 )
9401 .await;
9402
9403 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
9404 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9405 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9406
9407 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9408 language_registry.add(rust_lang());
9409 let mut fake_servers = language_registry.register_fake_lsp(
9410 "Rust",
9411 FakeLspAdapter {
9412 capabilities: lsp::ServerCapabilities {
9413 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9414 ..Default::default()
9415 },
9416 ..Default::default()
9417 },
9418 );
9419
9420 let worktree = project.update(cx, |project, cx| {
9421 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
9422 assert_eq!(worktrees.len(), 1);
9423 worktrees.pop().unwrap()
9424 });
9425 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9426
9427 let buffer_1 = project
9428 .update(cx, |project, cx| {
9429 project.open_buffer((worktree_id, "main.rs"), cx)
9430 })
9431 .await
9432 .unwrap();
9433 let buffer_2 = project
9434 .update(cx, |project, cx| {
9435 project.open_buffer((worktree_id, "other.rs"), cx)
9436 })
9437 .await
9438 .unwrap();
9439 let buffer_3 = project
9440 .update(cx, |project, cx| {
9441 project.open_buffer((worktree_id, "lib.rs"), cx)
9442 })
9443 .await
9444 .unwrap();
9445
9446 let multi_buffer = cx.new(|cx| {
9447 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9448 multi_buffer.push_excerpts(
9449 buffer_1.clone(),
9450 [
9451 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9452 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9453 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9454 ],
9455 cx,
9456 );
9457 multi_buffer.push_excerpts(
9458 buffer_2.clone(),
9459 [
9460 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9461 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9462 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9463 ],
9464 cx,
9465 );
9466 multi_buffer.push_excerpts(
9467 buffer_3.clone(),
9468 [
9469 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9470 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9471 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9472 ],
9473 cx,
9474 );
9475 multi_buffer
9476 });
9477 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
9478 Editor::new(
9479 EditorMode::full(),
9480 multi_buffer,
9481 Some(project.clone()),
9482 window,
9483 cx,
9484 )
9485 });
9486
9487 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9488 editor.change_selections(
9489 SelectionEffects::scroll(Autoscroll::Next),
9490 window,
9491 cx,
9492 |s| s.select_ranges(Some(1..2)),
9493 );
9494 editor.insert("|one|two|three|", window, cx);
9495 });
9496 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9497 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9498 editor.change_selections(
9499 SelectionEffects::scroll(Autoscroll::Next),
9500 window,
9501 cx,
9502 |s| s.select_ranges(Some(60..70)),
9503 );
9504 editor.insert("|four|five|six|", window, cx);
9505 });
9506 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9507
9508 // First two buffers should be edited, but not the third one.
9509 assert_eq!(
9510 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9511 "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}",
9512 );
9513 buffer_1.update(cx, |buffer, _| {
9514 assert!(buffer.is_dirty());
9515 assert_eq!(
9516 buffer.text(),
9517 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
9518 )
9519 });
9520 buffer_2.update(cx, |buffer, _| {
9521 assert!(buffer.is_dirty());
9522 assert_eq!(
9523 buffer.text(),
9524 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
9525 )
9526 });
9527 buffer_3.update(cx, |buffer, _| {
9528 assert!(!buffer.is_dirty());
9529 assert_eq!(buffer.text(), sample_text_3,)
9530 });
9531 cx.executor().run_until_parked();
9532
9533 cx.executor().start_waiting();
9534 let save = multi_buffer_editor
9535 .update_in(cx, |editor, window, cx| {
9536 editor.save(
9537 SaveOptions {
9538 format: true,
9539 autosave: false,
9540 },
9541 project.clone(),
9542 window,
9543 cx,
9544 )
9545 })
9546 .unwrap();
9547
9548 let fake_server = fake_servers.next().await.unwrap();
9549 fake_server
9550 .server
9551 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
9552 Ok(Some(vec![lsp::TextEdit::new(
9553 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9554 format!("[{} formatted]", params.text_document.uri),
9555 )]))
9556 })
9557 .detach();
9558 save.await;
9559
9560 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
9561 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
9562 assert_eq!(
9563 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9564 uri!(
9565 "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}"
9566 ),
9567 );
9568 buffer_1.update(cx, |buffer, _| {
9569 assert!(!buffer.is_dirty());
9570 assert_eq!(
9571 buffer.text(),
9572 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
9573 )
9574 });
9575 buffer_2.update(cx, |buffer, _| {
9576 assert!(!buffer.is_dirty());
9577 assert_eq!(
9578 buffer.text(),
9579 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
9580 )
9581 });
9582 buffer_3.update(cx, |buffer, _| {
9583 assert!(!buffer.is_dirty());
9584 assert_eq!(buffer.text(), sample_text_3,)
9585 });
9586}
9587
9588#[gpui::test]
9589async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
9590 init_test(cx, |_| {});
9591
9592 let fs = FakeFs::new(cx.executor());
9593 fs.insert_tree(
9594 path!("/dir"),
9595 json!({
9596 "file1.rs": "fn main() { println!(\"hello\"); }",
9597 "file2.rs": "fn test() { println!(\"test\"); }",
9598 "file3.rs": "fn other() { println!(\"other\"); }\n",
9599 }),
9600 )
9601 .await;
9602
9603 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
9604 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9605 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9606
9607 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9608 language_registry.add(rust_lang());
9609
9610 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
9611 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9612
9613 // Open three buffers
9614 let buffer_1 = project
9615 .update(cx, |project, cx| {
9616 project.open_buffer((worktree_id, "file1.rs"), cx)
9617 })
9618 .await
9619 .unwrap();
9620 let buffer_2 = project
9621 .update(cx, |project, cx| {
9622 project.open_buffer((worktree_id, "file2.rs"), cx)
9623 })
9624 .await
9625 .unwrap();
9626 let buffer_3 = project
9627 .update(cx, |project, cx| {
9628 project.open_buffer((worktree_id, "file3.rs"), cx)
9629 })
9630 .await
9631 .unwrap();
9632
9633 // Create a multi-buffer with all three buffers
9634 let multi_buffer = cx.new(|cx| {
9635 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9636 multi_buffer.push_excerpts(
9637 buffer_1.clone(),
9638 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9639 cx,
9640 );
9641 multi_buffer.push_excerpts(
9642 buffer_2.clone(),
9643 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9644 cx,
9645 );
9646 multi_buffer.push_excerpts(
9647 buffer_3.clone(),
9648 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9649 cx,
9650 );
9651 multi_buffer
9652 });
9653
9654 let editor = cx.new_window_entity(|window, cx| {
9655 Editor::new(
9656 EditorMode::full(),
9657 multi_buffer,
9658 Some(project.clone()),
9659 window,
9660 cx,
9661 )
9662 });
9663
9664 // Edit only the first buffer
9665 editor.update_in(cx, |editor, window, cx| {
9666 editor.change_selections(
9667 SelectionEffects::scroll(Autoscroll::Next),
9668 window,
9669 cx,
9670 |s| s.select_ranges(Some(10..10)),
9671 );
9672 editor.insert("// edited", window, cx);
9673 });
9674
9675 // Verify that only buffer 1 is dirty
9676 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
9677 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9678 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9679
9680 // Get write counts after file creation (files were created with initial content)
9681 // We expect each file to have been written once during creation
9682 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
9683 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
9684 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
9685
9686 // Perform autosave
9687 let save_task = editor.update_in(cx, |editor, window, cx| {
9688 editor.save(
9689 SaveOptions {
9690 format: true,
9691 autosave: true,
9692 },
9693 project.clone(),
9694 window,
9695 cx,
9696 )
9697 });
9698 save_task.await.unwrap();
9699
9700 // Only the dirty buffer should have been saved
9701 assert_eq!(
9702 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
9703 1,
9704 "Buffer 1 was dirty, so it should have been written once during autosave"
9705 );
9706 assert_eq!(
9707 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
9708 0,
9709 "Buffer 2 was clean, so it should not have been written during autosave"
9710 );
9711 assert_eq!(
9712 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
9713 0,
9714 "Buffer 3 was clean, so it should not have been written during autosave"
9715 );
9716
9717 // Verify buffer states after autosave
9718 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9719 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9720 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9721
9722 // Now perform a manual save (format = true)
9723 let save_task = editor.update_in(cx, |editor, window, cx| {
9724 editor.save(
9725 SaveOptions {
9726 format: true,
9727 autosave: false,
9728 },
9729 project.clone(),
9730 window,
9731 cx,
9732 )
9733 });
9734 save_task.await.unwrap();
9735
9736 // During manual save, clean buffers don't get written to disk
9737 // They just get did_save called for language server notifications
9738 assert_eq!(
9739 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
9740 1,
9741 "Buffer 1 should only have been written once total (during autosave, not manual save)"
9742 );
9743 assert_eq!(
9744 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
9745 0,
9746 "Buffer 2 should not have been written at all"
9747 );
9748 assert_eq!(
9749 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
9750 0,
9751 "Buffer 3 should not have been written at all"
9752 );
9753}
9754
9755#[gpui::test]
9756async fn test_range_format_during_save(cx: &mut TestAppContext) {
9757 init_test(cx, |_| {});
9758
9759 let fs = FakeFs::new(cx.executor());
9760 fs.insert_file(path!("/file.rs"), Default::default()).await;
9761
9762 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9763
9764 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9765 language_registry.add(rust_lang());
9766 let mut fake_servers = language_registry.register_fake_lsp(
9767 "Rust",
9768 FakeLspAdapter {
9769 capabilities: lsp::ServerCapabilities {
9770 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
9771 ..Default::default()
9772 },
9773 ..Default::default()
9774 },
9775 );
9776
9777 let buffer = project
9778 .update(cx, |project, cx| {
9779 project.open_local_buffer(path!("/file.rs"), cx)
9780 })
9781 .await
9782 .unwrap();
9783
9784 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9785 let (editor, cx) = cx.add_window_view(|window, cx| {
9786 build_editor_with_project(project.clone(), buffer, window, cx)
9787 });
9788 editor.update_in(cx, |editor, window, cx| {
9789 editor.set_text("one\ntwo\nthree\n", window, cx)
9790 });
9791 assert!(cx.read(|cx| editor.is_dirty(cx)));
9792
9793 cx.executor().start_waiting();
9794 let fake_server = fake_servers.next().await.unwrap();
9795
9796 let save = editor
9797 .update_in(cx, |editor, window, cx| {
9798 editor.save(
9799 SaveOptions {
9800 format: true,
9801 autosave: false,
9802 },
9803 project.clone(),
9804 window,
9805 cx,
9806 )
9807 })
9808 .unwrap();
9809 fake_server
9810 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
9811 assert_eq!(
9812 params.text_document.uri,
9813 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9814 );
9815 assert_eq!(params.options.tab_size, 4);
9816 Ok(Some(vec![lsp::TextEdit::new(
9817 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9818 ", ".to_string(),
9819 )]))
9820 })
9821 .next()
9822 .await;
9823 cx.executor().start_waiting();
9824 save.await;
9825 assert_eq!(
9826 editor.update(cx, |editor, cx| editor.text(cx)),
9827 "one, two\nthree\n"
9828 );
9829 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9830
9831 editor.update_in(cx, |editor, window, cx| {
9832 editor.set_text("one\ntwo\nthree\n", window, cx)
9833 });
9834 assert!(cx.read(|cx| editor.is_dirty(cx)));
9835
9836 // Ensure we can still save even if formatting hangs.
9837 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
9838 move |params, _| async move {
9839 assert_eq!(
9840 params.text_document.uri,
9841 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9842 );
9843 futures::future::pending::<()>().await;
9844 unreachable!()
9845 },
9846 );
9847 let save = editor
9848 .update_in(cx, |editor, window, cx| {
9849 editor.save(
9850 SaveOptions {
9851 format: true,
9852 autosave: false,
9853 },
9854 project.clone(),
9855 window,
9856 cx,
9857 )
9858 })
9859 .unwrap();
9860 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9861 cx.executor().start_waiting();
9862 save.await;
9863 assert_eq!(
9864 editor.update(cx, |editor, cx| editor.text(cx)),
9865 "one\ntwo\nthree\n"
9866 );
9867 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9868
9869 // For non-dirty buffer, no formatting request should be sent
9870 let save = editor
9871 .update_in(cx, |editor, window, cx| {
9872 editor.save(
9873 SaveOptions {
9874 format: false,
9875 autosave: false,
9876 },
9877 project.clone(),
9878 window,
9879 cx,
9880 )
9881 })
9882 .unwrap();
9883 let _pending_format_request = fake_server
9884 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
9885 panic!("Should not be invoked");
9886 })
9887 .next();
9888 cx.executor().start_waiting();
9889 save.await;
9890
9891 // Set Rust language override and assert overridden tabsize is sent to language server
9892 update_test_language_settings(cx, |settings| {
9893 settings.languages.0.insert(
9894 "Rust".into(),
9895 LanguageSettingsContent {
9896 tab_size: NonZeroU32::new(8),
9897 ..Default::default()
9898 },
9899 );
9900 });
9901
9902 editor.update_in(cx, |editor, window, cx| {
9903 editor.set_text("somehting_new\n", window, cx)
9904 });
9905 assert!(cx.read(|cx| editor.is_dirty(cx)));
9906 let save = editor
9907 .update_in(cx, |editor, window, cx| {
9908 editor.save(
9909 SaveOptions {
9910 format: true,
9911 autosave: false,
9912 },
9913 project.clone(),
9914 window,
9915 cx,
9916 )
9917 })
9918 .unwrap();
9919 fake_server
9920 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
9921 assert_eq!(
9922 params.text_document.uri,
9923 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9924 );
9925 assert_eq!(params.options.tab_size, 8);
9926 Ok(Some(Vec::new()))
9927 })
9928 .next()
9929 .await;
9930 save.await;
9931}
9932
9933#[gpui::test]
9934async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
9935 init_test(cx, |settings| {
9936 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(vec![
9937 Formatter::LanguageServer { name: None },
9938 ]))
9939 });
9940
9941 let fs = FakeFs::new(cx.executor());
9942 fs.insert_file(path!("/file.rs"), Default::default()).await;
9943
9944 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9945
9946 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9947 language_registry.add(Arc::new(Language::new(
9948 LanguageConfig {
9949 name: "Rust".into(),
9950 matcher: LanguageMatcher {
9951 path_suffixes: vec!["rs".to_string()],
9952 ..Default::default()
9953 },
9954 ..LanguageConfig::default()
9955 },
9956 Some(tree_sitter_rust::LANGUAGE.into()),
9957 )));
9958 update_test_language_settings(cx, |settings| {
9959 // Enable Prettier formatting for the same buffer, and ensure
9960 // LSP is called instead of Prettier.
9961 settings.defaults.prettier = Some(PrettierSettings {
9962 allowed: true,
9963 ..PrettierSettings::default()
9964 });
9965 });
9966 let mut fake_servers = language_registry.register_fake_lsp(
9967 "Rust",
9968 FakeLspAdapter {
9969 capabilities: lsp::ServerCapabilities {
9970 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9971 ..Default::default()
9972 },
9973 ..Default::default()
9974 },
9975 );
9976
9977 let buffer = project
9978 .update(cx, |project, cx| {
9979 project.open_local_buffer(path!("/file.rs"), cx)
9980 })
9981 .await
9982 .unwrap();
9983
9984 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9985 let (editor, cx) = cx.add_window_view(|window, cx| {
9986 build_editor_with_project(project.clone(), buffer, window, cx)
9987 });
9988 editor.update_in(cx, |editor, window, cx| {
9989 editor.set_text("one\ntwo\nthree\n", window, cx)
9990 });
9991
9992 cx.executor().start_waiting();
9993 let fake_server = fake_servers.next().await.unwrap();
9994
9995 let format = editor
9996 .update_in(cx, |editor, window, cx| {
9997 editor.perform_format(
9998 project.clone(),
9999 FormatTrigger::Manual,
10000 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10001 window,
10002 cx,
10003 )
10004 })
10005 .unwrap();
10006 fake_server
10007 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
10008 assert_eq!(
10009 params.text_document.uri,
10010 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10011 );
10012 assert_eq!(params.options.tab_size, 4);
10013 Ok(Some(vec![lsp::TextEdit::new(
10014 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10015 ", ".to_string(),
10016 )]))
10017 })
10018 .next()
10019 .await;
10020 cx.executor().start_waiting();
10021 format.await;
10022 assert_eq!(
10023 editor.update(cx, |editor, cx| editor.text(cx)),
10024 "one, two\nthree\n"
10025 );
10026
10027 editor.update_in(cx, |editor, window, cx| {
10028 editor.set_text("one\ntwo\nthree\n", window, cx)
10029 });
10030 // Ensure we don't lock if formatting hangs.
10031 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10032 move |params, _| async move {
10033 assert_eq!(
10034 params.text_document.uri,
10035 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10036 );
10037 futures::future::pending::<()>().await;
10038 unreachable!()
10039 },
10040 );
10041 let format = editor
10042 .update_in(cx, |editor, window, cx| {
10043 editor.perform_format(
10044 project,
10045 FormatTrigger::Manual,
10046 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10047 window,
10048 cx,
10049 )
10050 })
10051 .unwrap();
10052 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10053 cx.executor().start_waiting();
10054 format.await;
10055 assert_eq!(
10056 editor.update(cx, |editor, cx| editor.text(cx)),
10057 "one\ntwo\nthree\n"
10058 );
10059}
10060
10061#[gpui::test]
10062async fn test_multiple_formatters(cx: &mut TestAppContext) {
10063 init_test(cx, |settings| {
10064 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
10065 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(vec![
10066 Formatter::LanguageServer { name: None },
10067 Formatter::CodeActions(
10068 [
10069 ("code-action-1".into(), true),
10070 ("code-action-2".into(), true),
10071 ]
10072 .into_iter()
10073 .collect(),
10074 ),
10075 ]))
10076 });
10077
10078 let fs = FakeFs::new(cx.executor());
10079 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
10080 .await;
10081
10082 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10083 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10084 language_registry.add(rust_lang());
10085
10086 let mut fake_servers = language_registry.register_fake_lsp(
10087 "Rust",
10088 FakeLspAdapter {
10089 capabilities: lsp::ServerCapabilities {
10090 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10091 execute_command_provider: Some(lsp::ExecuteCommandOptions {
10092 commands: vec!["the-command-for-code-action-1".into()],
10093 ..Default::default()
10094 }),
10095 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10096 ..Default::default()
10097 },
10098 ..Default::default()
10099 },
10100 );
10101
10102 let buffer = project
10103 .update(cx, |project, cx| {
10104 project.open_local_buffer(path!("/file.rs"), cx)
10105 })
10106 .await
10107 .unwrap();
10108
10109 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10110 let (editor, cx) = cx.add_window_view(|window, cx| {
10111 build_editor_with_project(project.clone(), buffer, window, cx)
10112 });
10113
10114 cx.executor().start_waiting();
10115
10116 let fake_server = fake_servers.next().await.unwrap();
10117 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10118 move |_params, _| async move {
10119 Ok(Some(vec![lsp::TextEdit::new(
10120 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10121 "applied-formatting\n".to_string(),
10122 )]))
10123 },
10124 );
10125 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10126 move |params, _| async move {
10127 assert_eq!(
10128 params.context.only,
10129 Some(vec!["code-action-1".into(), "code-action-2".into()])
10130 );
10131 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
10132 Ok(Some(vec![
10133 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10134 kind: Some("code-action-1".into()),
10135 edit: Some(lsp::WorkspaceEdit::new(
10136 [(
10137 uri.clone(),
10138 vec![lsp::TextEdit::new(
10139 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10140 "applied-code-action-1-edit\n".to_string(),
10141 )],
10142 )]
10143 .into_iter()
10144 .collect(),
10145 )),
10146 command: Some(lsp::Command {
10147 command: "the-command-for-code-action-1".into(),
10148 ..Default::default()
10149 }),
10150 ..Default::default()
10151 }),
10152 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10153 kind: Some("code-action-2".into()),
10154 edit: Some(lsp::WorkspaceEdit::new(
10155 [(
10156 uri.clone(),
10157 vec![lsp::TextEdit::new(
10158 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10159 "applied-code-action-2-edit\n".to_string(),
10160 )],
10161 )]
10162 .into_iter()
10163 .collect(),
10164 )),
10165 ..Default::default()
10166 }),
10167 ]))
10168 },
10169 );
10170
10171 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
10172 move |params, _| async move { Ok(params) }
10173 });
10174
10175 let command_lock = Arc::new(futures::lock::Mutex::new(()));
10176 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
10177 let fake = fake_server.clone();
10178 let lock = command_lock.clone();
10179 move |params, _| {
10180 assert_eq!(params.command, "the-command-for-code-action-1");
10181 let fake = fake.clone();
10182 let lock = lock.clone();
10183 async move {
10184 lock.lock().await;
10185 fake.server
10186 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
10187 label: None,
10188 edit: lsp::WorkspaceEdit {
10189 changes: Some(
10190 [(
10191 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
10192 vec![lsp::TextEdit {
10193 range: lsp::Range::new(
10194 lsp::Position::new(0, 0),
10195 lsp::Position::new(0, 0),
10196 ),
10197 new_text: "applied-code-action-1-command\n".into(),
10198 }],
10199 )]
10200 .into_iter()
10201 .collect(),
10202 ),
10203 ..Default::default()
10204 },
10205 })
10206 .await
10207 .into_response()
10208 .unwrap();
10209 Ok(Some(json!(null)))
10210 }
10211 }
10212 });
10213
10214 cx.executor().start_waiting();
10215 editor
10216 .update_in(cx, |editor, window, cx| {
10217 editor.perform_format(
10218 project.clone(),
10219 FormatTrigger::Manual,
10220 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10221 window,
10222 cx,
10223 )
10224 })
10225 .unwrap()
10226 .await;
10227 editor.update(cx, |editor, cx| {
10228 assert_eq!(
10229 editor.text(cx),
10230 r#"
10231 applied-code-action-2-edit
10232 applied-code-action-1-command
10233 applied-code-action-1-edit
10234 applied-formatting
10235 one
10236 two
10237 three
10238 "#
10239 .unindent()
10240 );
10241 });
10242
10243 editor.update_in(cx, |editor, window, cx| {
10244 editor.undo(&Default::default(), window, cx);
10245 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10246 });
10247
10248 // Perform a manual edit while waiting for an LSP command
10249 // that's being run as part of a formatting code action.
10250 let lock_guard = command_lock.lock().await;
10251 let format = editor
10252 .update_in(cx, |editor, window, cx| {
10253 editor.perform_format(
10254 project.clone(),
10255 FormatTrigger::Manual,
10256 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10257 window,
10258 cx,
10259 )
10260 })
10261 .unwrap();
10262 cx.run_until_parked();
10263 editor.update(cx, |editor, cx| {
10264 assert_eq!(
10265 editor.text(cx),
10266 r#"
10267 applied-code-action-1-edit
10268 applied-formatting
10269 one
10270 two
10271 three
10272 "#
10273 .unindent()
10274 );
10275
10276 editor.buffer.update(cx, |buffer, cx| {
10277 let ix = buffer.len(cx);
10278 buffer.edit([(ix..ix, "edited\n")], None, cx);
10279 });
10280 });
10281
10282 // Allow the LSP command to proceed. Because the buffer was edited,
10283 // the second code action will not be run.
10284 drop(lock_guard);
10285 format.await;
10286 editor.update_in(cx, |editor, window, cx| {
10287 assert_eq!(
10288 editor.text(cx),
10289 r#"
10290 applied-code-action-1-command
10291 applied-code-action-1-edit
10292 applied-formatting
10293 one
10294 two
10295 three
10296 edited
10297 "#
10298 .unindent()
10299 );
10300
10301 // The manual edit is undone first, because it is the last thing the user did
10302 // (even though the command completed afterwards).
10303 editor.undo(&Default::default(), window, cx);
10304 assert_eq!(
10305 editor.text(cx),
10306 r#"
10307 applied-code-action-1-command
10308 applied-code-action-1-edit
10309 applied-formatting
10310 one
10311 two
10312 three
10313 "#
10314 .unindent()
10315 );
10316
10317 // All the formatting (including the command, which completed after the manual edit)
10318 // is undone together.
10319 editor.undo(&Default::default(), window, cx);
10320 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10321 });
10322}
10323
10324#[gpui::test]
10325async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
10326 init_test(cx, |settings| {
10327 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(vec![
10328 Formatter::LanguageServer { name: None },
10329 ]))
10330 });
10331
10332 let fs = FakeFs::new(cx.executor());
10333 fs.insert_file(path!("/file.ts"), Default::default()).await;
10334
10335 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10336
10337 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10338 language_registry.add(Arc::new(Language::new(
10339 LanguageConfig {
10340 name: "TypeScript".into(),
10341 matcher: LanguageMatcher {
10342 path_suffixes: vec!["ts".to_string()],
10343 ..Default::default()
10344 },
10345 ..LanguageConfig::default()
10346 },
10347 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
10348 )));
10349 update_test_language_settings(cx, |settings| {
10350 settings.defaults.prettier = Some(PrettierSettings {
10351 allowed: true,
10352 ..PrettierSettings::default()
10353 });
10354 });
10355 let mut fake_servers = language_registry.register_fake_lsp(
10356 "TypeScript",
10357 FakeLspAdapter {
10358 capabilities: lsp::ServerCapabilities {
10359 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10360 ..Default::default()
10361 },
10362 ..Default::default()
10363 },
10364 );
10365
10366 let buffer = project
10367 .update(cx, |project, cx| {
10368 project.open_local_buffer(path!("/file.ts"), cx)
10369 })
10370 .await
10371 .unwrap();
10372
10373 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10374 let (editor, cx) = cx.add_window_view(|window, cx| {
10375 build_editor_with_project(project.clone(), buffer, window, cx)
10376 });
10377 editor.update_in(cx, |editor, window, cx| {
10378 editor.set_text(
10379 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10380 window,
10381 cx,
10382 )
10383 });
10384
10385 cx.executor().start_waiting();
10386 let fake_server = fake_servers.next().await.unwrap();
10387
10388 let format = editor
10389 .update_in(cx, |editor, window, cx| {
10390 editor.perform_code_action_kind(
10391 project.clone(),
10392 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10393 window,
10394 cx,
10395 )
10396 })
10397 .unwrap();
10398 fake_server
10399 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
10400 assert_eq!(
10401 params.text_document.uri,
10402 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10403 );
10404 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
10405 lsp::CodeAction {
10406 title: "Organize Imports".to_string(),
10407 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
10408 edit: Some(lsp::WorkspaceEdit {
10409 changes: Some(
10410 [(
10411 params.text_document.uri.clone(),
10412 vec![lsp::TextEdit::new(
10413 lsp::Range::new(
10414 lsp::Position::new(1, 0),
10415 lsp::Position::new(2, 0),
10416 ),
10417 "".to_string(),
10418 )],
10419 )]
10420 .into_iter()
10421 .collect(),
10422 ),
10423 ..Default::default()
10424 }),
10425 ..Default::default()
10426 },
10427 )]))
10428 })
10429 .next()
10430 .await;
10431 cx.executor().start_waiting();
10432 format.await;
10433 assert_eq!(
10434 editor.update(cx, |editor, cx| editor.text(cx)),
10435 "import { a } from 'module';\n\nconst x = a;\n"
10436 );
10437
10438 editor.update_in(cx, |editor, window, cx| {
10439 editor.set_text(
10440 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10441 window,
10442 cx,
10443 )
10444 });
10445 // Ensure we don't lock if code action hangs.
10446 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10447 move |params, _| async move {
10448 assert_eq!(
10449 params.text_document.uri,
10450 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10451 );
10452 futures::future::pending::<()>().await;
10453 unreachable!()
10454 },
10455 );
10456 let format = editor
10457 .update_in(cx, |editor, window, cx| {
10458 editor.perform_code_action_kind(
10459 project,
10460 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10461 window,
10462 cx,
10463 )
10464 })
10465 .unwrap();
10466 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
10467 cx.executor().start_waiting();
10468 format.await;
10469 assert_eq!(
10470 editor.update(cx, |editor, cx| editor.text(cx)),
10471 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
10472 );
10473}
10474
10475#[gpui::test]
10476async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
10477 init_test(cx, |_| {});
10478
10479 let mut cx = EditorLspTestContext::new_rust(
10480 lsp::ServerCapabilities {
10481 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10482 ..Default::default()
10483 },
10484 cx,
10485 )
10486 .await;
10487
10488 cx.set_state(indoc! {"
10489 one.twoˇ
10490 "});
10491
10492 // The format request takes a long time. When it completes, it inserts
10493 // a newline and an indent before the `.`
10494 cx.lsp
10495 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
10496 let executor = cx.background_executor().clone();
10497 async move {
10498 executor.timer(Duration::from_millis(100)).await;
10499 Ok(Some(vec![lsp::TextEdit {
10500 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
10501 new_text: "\n ".into(),
10502 }]))
10503 }
10504 });
10505
10506 // Submit a format request.
10507 let format_1 = cx
10508 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10509 .unwrap();
10510 cx.executor().run_until_parked();
10511
10512 // Submit a second format request.
10513 let format_2 = cx
10514 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10515 .unwrap();
10516 cx.executor().run_until_parked();
10517
10518 // Wait for both format requests to complete
10519 cx.executor().advance_clock(Duration::from_millis(200));
10520 cx.executor().start_waiting();
10521 format_1.await.unwrap();
10522 cx.executor().start_waiting();
10523 format_2.await.unwrap();
10524
10525 // The formatting edits only happens once.
10526 cx.assert_editor_state(indoc! {"
10527 one
10528 .twoˇ
10529 "});
10530}
10531
10532#[gpui::test]
10533async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
10534 init_test(cx, |settings| {
10535 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
10536 });
10537
10538 let mut cx = EditorLspTestContext::new_rust(
10539 lsp::ServerCapabilities {
10540 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10541 ..Default::default()
10542 },
10543 cx,
10544 )
10545 .await;
10546
10547 // Set up a buffer white some trailing whitespace and no trailing newline.
10548 cx.set_state(
10549 &[
10550 "one ", //
10551 "twoˇ", //
10552 "three ", //
10553 "four", //
10554 ]
10555 .join("\n"),
10556 );
10557
10558 // Submit a format request.
10559 let format = cx
10560 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10561 .unwrap();
10562
10563 // Record which buffer changes have been sent to the language server
10564 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
10565 cx.lsp
10566 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
10567 let buffer_changes = buffer_changes.clone();
10568 move |params, _| {
10569 buffer_changes.lock().extend(
10570 params
10571 .content_changes
10572 .into_iter()
10573 .map(|e| (e.range.unwrap(), e.text)),
10574 );
10575 }
10576 });
10577
10578 // Handle formatting requests to the language server.
10579 cx.lsp
10580 .set_request_handler::<lsp::request::Formatting, _, _>({
10581 let buffer_changes = buffer_changes.clone();
10582 move |_, _| {
10583 // When formatting is requested, trailing whitespace has already been stripped,
10584 // and the trailing newline has already been added.
10585 assert_eq!(
10586 &buffer_changes.lock()[1..],
10587 &[
10588 (
10589 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
10590 "".into()
10591 ),
10592 (
10593 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
10594 "".into()
10595 ),
10596 (
10597 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
10598 "\n".into()
10599 ),
10600 ]
10601 );
10602
10603 // Insert blank lines between each line of the buffer.
10604 async move {
10605 Ok(Some(vec![
10606 lsp::TextEdit {
10607 range: lsp::Range::new(
10608 lsp::Position::new(1, 0),
10609 lsp::Position::new(1, 0),
10610 ),
10611 new_text: "\n".into(),
10612 },
10613 lsp::TextEdit {
10614 range: lsp::Range::new(
10615 lsp::Position::new(2, 0),
10616 lsp::Position::new(2, 0),
10617 ),
10618 new_text: "\n".into(),
10619 },
10620 ]))
10621 }
10622 }
10623 });
10624
10625 // After formatting the buffer, the trailing whitespace is stripped,
10626 // a newline is appended, and the edits provided by the language server
10627 // have been applied.
10628 format.await.unwrap();
10629 cx.assert_editor_state(
10630 &[
10631 "one", //
10632 "", //
10633 "twoˇ", //
10634 "", //
10635 "three", //
10636 "four", //
10637 "", //
10638 ]
10639 .join("\n"),
10640 );
10641
10642 // Undoing the formatting undoes the trailing whitespace removal, the
10643 // trailing newline, and the LSP edits.
10644 cx.update_buffer(|buffer, cx| buffer.undo(cx));
10645 cx.assert_editor_state(
10646 &[
10647 "one ", //
10648 "twoˇ", //
10649 "three ", //
10650 "four", //
10651 ]
10652 .join("\n"),
10653 );
10654}
10655
10656#[gpui::test]
10657async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
10658 cx: &mut TestAppContext,
10659) {
10660 init_test(cx, |_| {});
10661
10662 cx.update(|cx| {
10663 cx.update_global::<SettingsStore, _>(|settings, cx| {
10664 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10665 settings.auto_signature_help = Some(true);
10666 });
10667 });
10668 });
10669
10670 let mut cx = EditorLspTestContext::new_rust(
10671 lsp::ServerCapabilities {
10672 signature_help_provider: Some(lsp::SignatureHelpOptions {
10673 ..Default::default()
10674 }),
10675 ..Default::default()
10676 },
10677 cx,
10678 )
10679 .await;
10680
10681 let language = Language::new(
10682 LanguageConfig {
10683 name: "Rust".into(),
10684 brackets: BracketPairConfig {
10685 pairs: vec![
10686 BracketPair {
10687 start: "{".to_string(),
10688 end: "}".to_string(),
10689 close: true,
10690 surround: true,
10691 newline: true,
10692 },
10693 BracketPair {
10694 start: "(".to_string(),
10695 end: ")".to_string(),
10696 close: true,
10697 surround: true,
10698 newline: true,
10699 },
10700 BracketPair {
10701 start: "/*".to_string(),
10702 end: " */".to_string(),
10703 close: true,
10704 surround: true,
10705 newline: true,
10706 },
10707 BracketPair {
10708 start: "[".to_string(),
10709 end: "]".to_string(),
10710 close: false,
10711 surround: false,
10712 newline: true,
10713 },
10714 BracketPair {
10715 start: "\"".to_string(),
10716 end: "\"".to_string(),
10717 close: true,
10718 surround: true,
10719 newline: false,
10720 },
10721 BracketPair {
10722 start: "<".to_string(),
10723 end: ">".to_string(),
10724 close: false,
10725 surround: true,
10726 newline: true,
10727 },
10728 ],
10729 ..Default::default()
10730 },
10731 autoclose_before: "})]".to_string(),
10732 ..Default::default()
10733 },
10734 Some(tree_sitter_rust::LANGUAGE.into()),
10735 );
10736 let language = Arc::new(language);
10737
10738 cx.language_registry().add(language.clone());
10739 cx.update_buffer(|buffer, cx| {
10740 buffer.set_language(Some(language), cx);
10741 });
10742
10743 cx.set_state(
10744 &r#"
10745 fn main() {
10746 sampleˇ
10747 }
10748 "#
10749 .unindent(),
10750 );
10751
10752 cx.update_editor(|editor, window, cx| {
10753 editor.handle_input("(", window, cx);
10754 });
10755 cx.assert_editor_state(
10756 &"
10757 fn main() {
10758 sample(ˇ)
10759 }
10760 "
10761 .unindent(),
10762 );
10763
10764 let mocked_response = lsp::SignatureHelp {
10765 signatures: vec![lsp::SignatureInformation {
10766 label: "fn sample(param1: u8, param2: u8)".to_string(),
10767 documentation: None,
10768 parameters: Some(vec![
10769 lsp::ParameterInformation {
10770 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10771 documentation: None,
10772 },
10773 lsp::ParameterInformation {
10774 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10775 documentation: None,
10776 },
10777 ]),
10778 active_parameter: None,
10779 }],
10780 active_signature: Some(0),
10781 active_parameter: Some(0),
10782 };
10783 handle_signature_help_request(&mut cx, mocked_response).await;
10784
10785 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10786 .await;
10787
10788 cx.editor(|editor, _, _| {
10789 let signature_help_state = editor.signature_help_state.popover().cloned();
10790 assert_eq!(
10791 signature_help_state.unwrap().label,
10792 "param1: u8, param2: u8"
10793 );
10794 });
10795}
10796
10797#[gpui::test]
10798async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
10799 init_test(cx, |_| {});
10800
10801 cx.update(|cx| {
10802 cx.update_global::<SettingsStore, _>(|settings, cx| {
10803 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10804 settings.auto_signature_help = Some(false);
10805 settings.show_signature_help_after_edits = Some(false);
10806 });
10807 });
10808 });
10809
10810 let mut cx = EditorLspTestContext::new_rust(
10811 lsp::ServerCapabilities {
10812 signature_help_provider: Some(lsp::SignatureHelpOptions {
10813 ..Default::default()
10814 }),
10815 ..Default::default()
10816 },
10817 cx,
10818 )
10819 .await;
10820
10821 let language = Language::new(
10822 LanguageConfig {
10823 name: "Rust".into(),
10824 brackets: BracketPairConfig {
10825 pairs: vec![
10826 BracketPair {
10827 start: "{".to_string(),
10828 end: "}".to_string(),
10829 close: true,
10830 surround: true,
10831 newline: true,
10832 },
10833 BracketPair {
10834 start: "(".to_string(),
10835 end: ")".to_string(),
10836 close: true,
10837 surround: true,
10838 newline: true,
10839 },
10840 BracketPair {
10841 start: "/*".to_string(),
10842 end: " */".to_string(),
10843 close: true,
10844 surround: true,
10845 newline: true,
10846 },
10847 BracketPair {
10848 start: "[".to_string(),
10849 end: "]".to_string(),
10850 close: false,
10851 surround: false,
10852 newline: true,
10853 },
10854 BracketPair {
10855 start: "\"".to_string(),
10856 end: "\"".to_string(),
10857 close: true,
10858 surround: true,
10859 newline: false,
10860 },
10861 BracketPair {
10862 start: "<".to_string(),
10863 end: ">".to_string(),
10864 close: false,
10865 surround: true,
10866 newline: true,
10867 },
10868 ],
10869 ..Default::default()
10870 },
10871 autoclose_before: "})]".to_string(),
10872 ..Default::default()
10873 },
10874 Some(tree_sitter_rust::LANGUAGE.into()),
10875 );
10876 let language = Arc::new(language);
10877
10878 cx.language_registry().add(language.clone());
10879 cx.update_buffer(|buffer, cx| {
10880 buffer.set_language(Some(language), cx);
10881 });
10882
10883 // Ensure that signature_help is not called when no signature help is enabled.
10884 cx.set_state(
10885 &r#"
10886 fn main() {
10887 sampleˇ
10888 }
10889 "#
10890 .unindent(),
10891 );
10892 cx.update_editor(|editor, window, cx| {
10893 editor.handle_input("(", window, cx);
10894 });
10895 cx.assert_editor_state(
10896 &"
10897 fn main() {
10898 sample(ˇ)
10899 }
10900 "
10901 .unindent(),
10902 );
10903 cx.editor(|editor, _, _| {
10904 assert!(editor.signature_help_state.task().is_none());
10905 });
10906
10907 let mocked_response = lsp::SignatureHelp {
10908 signatures: vec![lsp::SignatureInformation {
10909 label: "fn sample(param1: u8, param2: u8)".to_string(),
10910 documentation: None,
10911 parameters: Some(vec![
10912 lsp::ParameterInformation {
10913 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10914 documentation: None,
10915 },
10916 lsp::ParameterInformation {
10917 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10918 documentation: None,
10919 },
10920 ]),
10921 active_parameter: None,
10922 }],
10923 active_signature: Some(0),
10924 active_parameter: Some(0),
10925 };
10926
10927 // Ensure that signature_help is called when enabled afte edits
10928 cx.update(|_, cx| {
10929 cx.update_global::<SettingsStore, _>(|settings, cx| {
10930 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10931 settings.auto_signature_help = Some(false);
10932 settings.show_signature_help_after_edits = Some(true);
10933 });
10934 });
10935 });
10936 cx.set_state(
10937 &r#"
10938 fn main() {
10939 sampleˇ
10940 }
10941 "#
10942 .unindent(),
10943 );
10944 cx.update_editor(|editor, window, cx| {
10945 editor.handle_input("(", window, cx);
10946 });
10947 cx.assert_editor_state(
10948 &"
10949 fn main() {
10950 sample(ˇ)
10951 }
10952 "
10953 .unindent(),
10954 );
10955 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10956 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10957 .await;
10958 cx.update_editor(|editor, _, _| {
10959 let signature_help_state = editor.signature_help_state.popover().cloned();
10960 assert!(signature_help_state.is_some());
10961 assert_eq!(
10962 signature_help_state.unwrap().label,
10963 "param1: u8, param2: u8"
10964 );
10965 editor.signature_help_state = SignatureHelpState::default();
10966 });
10967
10968 // Ensure that signature_help is called when auto signature help override is enabled
10969 cx.update(|_, cx| {
10970 cx.update_global::<SettingsStore, _>(|settings, cx| {
10971 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10972 settings.auto_signature_help = Some(true);
10973 settings.show_signature_help_after_edits = Some(false);
10974 });
10975 });
10976 });
10977 cx.set_state(
10978 &r#"
10979 fn main() {
10980 sampleˇ
10981 }
10982 "#
10983 .unindent(),
10984 );
10985 cx.update_editor(|editor, window, cx| {
10986 editor.handle_input("(", window, cx);
10987 });
10988 cx.assert_editor_state(
10989 &"
10990 fn main() {
10991 sample(ˇ)
10992 }
10993 "
10994 .unindent(),
10995 );
10996 handle_signature_help_request(&mut cx, mocked_response).await;
10997 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10998 .await;
10999 cx.editor(|editor, _, _| {
11000 let signature_help_state = editor.signature_help_state.popover().cloned();
11001 assert!(signature_help_state.is_some());
11002 assert_eq!(
11003 signature_help_state.unwrap().label,
11004 "param1: u8, param2: u8"
11005 );
11006 });
11007}
11008
11009#[gpui::test]
11010async fn test_signature_help(cx: &mut TestAppContext) {
11011 init_test(cx, |_| {});
11012 cx.update(|cx| {
11013 cx.update_global::<SettingsStore, _>(|settings, cx| {
11014 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11015 settings.auto_signature_help = Some(true);
11016 });
11017 });
11018 });
11019
11020 let mut cx = EditorLspTestContext::new_rust(
11021 lsp::ServerCapabilities {
11022 signature_help_provider: Some(lsp::SignatureHelpOptions {
11023 ..Default::default()
11024 }),
11025 ..Default::default()
11026 },
11027 cx,
11028 )
11029 .await;
11030
11031 // A test that directly calls `show_signature_help`
11032 cx.update_editor(|editor, window, cx| {
11033 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11034 });
11035
11036 let mocked_response = lsp::SignatureHelp {
11037 signatures: vec![lsp::SignatureInformation {
11038 label: "fn sample(param1: u8, param2: u8)".to_string(),
11039 documentation: None,
11040 parameters: Some(vec![
11041 lsp::ParameterInformation {
11042 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11043 documentation: None,
11044 },
11045 lsp::ParameterInformation {
11046 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11047 documentation: None,
11048 },
11049 ]),
11050 active_parameter: None,
11051 }],
11052 active_signature: Some(0),
11053 active_parameter: Some(0),
11054 };
11055 handle_signature_help_request(&mut cx, mocked_response).await;
11056
11057 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11058 .await;
11059
11060 cx.editor(|editor, _, _| {
11061 let signature_help_state = editor.signature_help_state.popover().cloned();
11062 assert!(signature_help_state.is_some());
11063 assert_eq!(
11064 signature_help_state.unwrap().label,
11065 "param1: u8, param2: u8"
11066 );
11067 });
11068
11069 // When exiting outside from inside the brackets, `signature_help` is closed.
11070 cx.set_state(indoc! {"
11071 fn main() {
11072 sample(ˇ);
11073 }
11074
11075 fn sample(param1: u8, param2: u8) {}
11076 "});
11077
11078 cx.update_editor(|editor, window, cx| {
11079 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11080 s.select_ranges([0..0])
11081 });
11082 });
11083
11084 let mocked_response = lsp::SignatureHelp {
11085 signatures: Vec::new(),
11086 active_signature: None,
11087 active_parameter: None,
11088 };
11089 handle_signature_help_request(&mut cx, mocked_response).await;
11090
11091 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11092 .await;
11093
11094 cx.editor(|editor, _, _| {
11095 assert!(!editor.signature_help_state.is_shown());
11096 });
11097
11098 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
11099 cx.set_state(indoc! {"
11100 fn main() {
11101 sample(ˇ);
11102 }
11103
11104 fn sample(param1: u8, param2: u8) {}
11105 "});
11106
11107 let mocked_response = lsp::SignatureHelp {
11108 signatures: vec![lsp::SignatureInformation {
11109 label: "fn sample(param1: u8, param2: u8)".to_string(),
11110 documentation: None,
11111 parameters: Some(vec![
11112 lsp::ParameterInformation {
11113 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11114 documentation: None,
11115 },
11116 lsp::ParameterInformation {
11117 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11118 documentation: None,
11119 },
11120 ]),
11121 active_parameter: None,
11122 }],
11123 active_signature: Some(0),
11124 active_parameter: Some(0),
11125 };
11126 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11127 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11128 .await;
11129 cx.editor(|editor, _, _| {
11130 assert!(editor.signature_help_state.is_shown());
11131 });
11132
11133 // Restore the popover with more parameter input
11134 cx.set_state(indoc! {"
11135 fn main() {
11136 sample(param1, param2ˇ);
11137 }
11138
11139 fn sample(param1: u8, param2: u8) {}
11140 "});
11141
11142 let mocked_response = lsp::SignatureHelp {
11143 signatures: vec![lsp::SignatureInformation {
11144 label: "fn sample(param1: u8, param2: u8)".to_string(),
11145 documentation: None,
11146 parameters: Some(vec![
11147 lsp::ParameterInformation {
11148 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11149 documentation: None,
11150 },
11151 lsp::ParameterInformation {
11152 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11153 documentation: None,
11154 },
11155 ]),
11156 active_parameter: None,
11157 }],
11158 active_signature: Some(0),
11159 active_parameter: Some(1),
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
11165 // When selecting a range, the popover is gone.
11166 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
11167 cx.update_editor(|editor, window, cx| {
11168 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11169 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11170 })
11171 });
11172 cx.assert_editor_state(indoc! {"
11173 fn main() {
11174 sample(param1, «ˇparam2»);
11175 }
11176
11177 fn sample(param1: u8, param2: u8) {}
11178 "});
11179 cx.editor(|editor, _, _| {
11180 assert!(!editor.signature_help_state.is_shown());
11181 });
11182
11183 // When unselecting again, the popover is back if within the brackets.
11184 cx.update_editor(|editor, window, cx| {
11185 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11186 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11187 })
11188 });
11189 cx.assert_editor_state(indoc! {"
11190 fn main() {
11191 sample(param1, ˇparam2);
11192 }
11193
11194 fn sample(param1: u8, param2: u8) {}
11195 "});
11196 handle_signature_help_request(&mut cx, mocked_response).await;
11197 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11198 .await;
11199 cx.editor(|editor, _, _| {
11200 assert!(editor.signature_help_state.is_shown());
11201 });
11202
11203 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
11204 cx.update_editor(|editor, window, cx| {
11205 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11206 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
11207 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11208 })
11209 });
11210 cx.assert_editor_state(indoc! {"
11211 fn main() {
11212 sample(param1, ˇparam2);
11213 }
11214
11215 fn sample(param1: u8, param2: u8) {}
11216 "});
11217
11218 let mocked_response = lsp::SignatureHelp {
11219 signatures: vec![lsp::SignatureInformation {
11220 label: "fn sample(param1: u8, param2: u8)".to_string(),
11221 documentation: None,
11222 parameters: Some(vec![
11223 lsp::ParameterInformation {
11224 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11225 documentation: None,
11226 },
11227 lsp::ParameterInformation {
11228 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11229 documentation: None,
11230 },
11231 ]),
11232 active_parameter: None,
11233 }],
11234 active_signature: Some(0),
11235 active_parameter: Some(1),
11236 };
11237 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11238 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11239 .await;
11240 cx.update_editor(|editor, _, cx| {
11241 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
11242 });
11243 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11244 .await;
11245 cx.update_editor(|editor, window, cx| {
11246 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11247 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11248 })
11249 });
11250 cx.assert_editor_state(indoc! {"
11251 fn main() {
11252 sample(param1, «ˇparam2»);
11253 }
11254
11255 fn sample(param1: u8, param2: u8) {}
11256 "});
11257 cx.update_editor(|editor, window, cx| {
11258 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11259 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11260 })
11261 });
11262 cx.assert_editor_state(indoc! {"
11263 fn main() {
11264 sample(param1, ˇparam2);
11265 }
11266
11267 fn sample(param1: u8, param2: u8) {}
11268 "});
11269 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
11270 .await;
11271}
11272
11273#[gpui::test]
11274async fn test_completion_mode(cx: &mut TestAppContext) {
11275 init_test(cx, |_| {});
11276 let mut cx = EditorLspTestContext::new_rust(
11277 lsp::ServerCapabilities {
11278 completion_provider: Some(lsp::CompletionOptions {
11279 resolve_provider: Some(true),
11280 ..Default::default()
11281 }),
11282 ..Default::default()
11283 },
11284 cx,
11285 )
11286 .await;
11287
11288 struct Run {
11289 run_description: &'static str,
11290 initial_state: String,
11291 buffer_marked_text: String,
11292 completion_label: &'static str,
11293 completion_text: &'static str,
11294 expected_with_insert_mode: String,
11295 expected_with_replace_mode: String,
11296 expected_with_replace_subsequence_mode: String,
11297 expected_with_replace_suffix_mode: String,
11298 }
11299
11300 let runs = [
11301 Run {
11302 run_description: "Start of word matches completion text",
11303 initial_state: "before ediˇ after".into(),
11304 buffer_marked_text: "before <edi|> after".into(),
11305 completion_label: "editor",
11306 completion_text: "editor",
11307 expected_with_insert_mode: "before editorˇ after".into(),
11308 expected_with_replace_mode: "before editorˇ after".into(),
11309 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11310 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11311 },
11312 Run {
11313 run_description: "Accept same text at the middle of the word",
11314 initial_state: "before ediˇtor after".into(),
11315 buffer_marked_text: "before <edi|tor> after".into(),
11316 completion_label: "editor",
11317 completion_text: "editor",
11318 expected_with_insert_mode: "before editorˇtor after".into(),
11319 expected_with_replace_mode: "before editorˇ after".into(),
11320 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11321 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11322 },
11323 Run {
11324 run_description: "End of word matches completion text -- cursor at end",
11325 initial_state: "before torˇ after".into(),
11326 buffer_marked_text: "before <tor|> after".into(),
11327 completion_label: "editor",
11328 completion_text: "editor",
11329 expected_with_insert_mode: "before editorˇ after".into(),
11330 expected_with_replace_mode: "before editorˇ after".into(),
11331 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11332 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11333 },
11334 Run {
11335 run_description: "End of word matches completion text -- cursor at start",
11336 initial_state: "before ˇtor after".into(),
11337 buffer_marked_text: "before <|tor> after".into(),
11338 completion_label: "editor",
11339 completion_text: "editor",
11340 expected_with_insert_mode: "before editorˇtor after".into(),
11341 expected_with_replace_mode: "before editorˇ after".into(),
11342 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11343 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11344 },
11345 Run {
11346 run_description: "Prepend text containing whitespace",
11347 initial_state: "pˇfield: bool".into(),
11348 buffer_marked_text: "<p|field>: bool".into(),
11349 completion_label: "pub ",
11350 completion_text: "pub ",
11351 expected_with_insert_mode: "pub ˇfield: bool".into(),
11352 expected_with_replace_mode: "pub ˇ: bool".into(),
11353 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
11354 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
11355 },
11356 Run {
11357 run_description: "Add element to start of list",
11358 initial_state: "[element_ˇelement_2]".into(),
11359 buffer_marked_text: "[<element_|element_2>]".into(),
11360 completion_label: "element_1",
11361 completion_text: "element_1",
11362 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
11363 expected_with_replace_mode: "[element_1ˇ]".into(),
11364 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
11365 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
11366 },
11367 Run {
11368 run_description: "Add element to start of list -- first and second elements are equal",
11369 initial_state: "[elˇelement]".into(),
11370 buffer_marked_text: "[<el|element>]".into(),
11371 completion_label: "element",
11372 completion_text: "element",
11373 expected_with_insert_mode: "[elementˇelement]".into(),
11374 expected_with_replace_mode: "[elementˇ]".into(),
11375 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
11376 expected_with_replace_suffix_mode: "[elementˇ]".into(),
11377 },
11378 Run {
11379 run_description: "Ends with matching suffix",
11380 initial_state: "SubˇError".into(),
11381 buffer_marked_text: "<Sub|Error>".into(),
11382 completion_label: "SubscriptionError",
11383 completion_text: "SubscriptionError",
11384 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
11385 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11386 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11387 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
11388 },
11389 Run {
11390 run_description: "Suffix is a subsequence -- contiguous",
11391 initial_state: "SubˇErr".into(),
11392 buffer_marked_text: "<Sub|Err>".into(),
11393 completion_label: "SubscriptionError",
11394 completion_text: "SubscriptionError",
11395 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
11396 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11397 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11398 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
11399 },
11400 Run {
11401 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
11402 initial_state: "Suˇscrirr".into(),
11403 buffer_marked_text: "<Su|scrirr>".into(),
11404 completion_label: "SubscriptionError",
11405 completion_text: "SubscriptionError",
11406 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
11407 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11408 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11409 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
11410 },
11411 Run {
11412 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
11413 initial_state: "foo(indˇix)".into(),
11414 buffer_marked_text: "foo(<ind|ix>)".into(),
11415 completion_label: "node_index",
11416 completion_text: "node_index",
11417 expected_with_insert_mode: "foo(node_indexˇix)".into(),
11418 expected_with_replace_mode: "foo(node_indexˇ)".into(),
11419 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
11420 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
11421 },
11422 Run {
11423 run_description: "Replace range ends before cursor - should extend to cursor",
11424 initial_state: "before editˇo after".into(),
11425 buffer_marked_text: "before <{ed}>it|o after".into(),
11426 completion_label: "editor",
11427 completion_text: "editor",
11428 expected_with_insert_mode: "before editorˇo after".into(),
11429 expected_with_replace_mode: "before editorˇo after".into(),
11430 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
11431 expected_with_replace_suffix_mode: "before editorˇo after".into(),
11432 },
11433 Run {
11434 run_description: "Uses label for suffix matching",
11435 initial_state: "before ediˇtor after".into(),
11436 buffer_marked_text: "before <edi|tor> after".into(),
11437 completion_label: "editor",
11438 completion_text: "editor()",
11439 expected_with_insert_mode: "before editor()ˇtor after".into(),
11440 expected_with_replace_mode: "before editor()ˇ after".into(),
11441 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
11442 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
11443 },
11444 Run {
11445 run_description: "Case insensitive subsequence and suffix matching",
11446 initial_state: "before EDiˇtoR after".into(),
11447 buffer_marked_text: "before <EDi|toR> after".into(),
11448 completion_label: "editor",
11449 completion_text: "editor",
11450 expected_with_insert_mode: "before editorˇtoR after".into(),
11451 expected_with_replace_mode: "before editorˇ after".into(),
11452 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11453 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11454 },
11455 ];
11456
11457 for run in runs {
11458 let run_variations = [
11459 (LspInsertMode::Insert, run.expected_with_insert_mode),
11460 (LspInsertMode::Replace, run.expected_with_replace_mode),
11461 (
11462 LspInsertMode::ReplaceSubsequence,
11463 run.expected_with_replace_subsequence_mode,
11464 ),
11465 (
11466 LspInsertMode::ReplaceSuffix,
11467 run.expected_with_replace_suffix_mode,
11468 ),
11469 ];
11470
11471 for (lsp_insert_mode, expected_text) in run_variations {
11472 eprintln!(
11473 "run = {:?}, mode = {lsp_insert_mode:.?}",
11474 run.run_description,
11475 );
11476
11477 update_test_language_settings(&mut cx, |settings| {
11478 settings.defaults.completions = Some(CompletionSettings {
11479 lsp_insert_mode,
11480 words: WordsCompletionMode::Disabled,
11481 lsp: true,
11482 lsp_fetch_timeout_ms: 0,
11483 });
11484 });
11485
11486 cx.set_state(&run.initial_state);
11487 cx.update_editor(|editor, window, cx| {
11488 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11489 });
11490
11491 let counter = Arc::new(AtomicUsize::new(0));
11492 handle_completion_request_with_insert_and_replace(
11493 &mut cx,
11494 &run.buffer_marked_text,
11495 vec![(run.completion_label, run.completion_text)],
11496 counter.clone(),
11497 )
11498 .await;
11499 cx.condition(|editor, _| editor.context_menu_visible())
11500 .await;
11501 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11502
11503 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11504 editor
11505 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11506 .unwrap()
11507 });
11508 cx.assert_editor_state(&expected_text);
11509 handle_resolve_completion_request(&mut cx, None).await;
11510 apply_additional_edits.await.unwrap();
11511 }
11512 }
11513}
11514
11515#[gpui::test]
11516async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
11517 init_test(cx, |_| {});
11518 let mut cx = EditorLspTestContext::new_rust(
11519 lsp::ServerCapabilities {
11520 completion_provider: Some(lsp::CompletionOptions {
11521 resolve_provider: Some(true),
11522 ..Default::default()
11523 }),
11524 ..Default::default()
11525 },
11526 cx,
11527 )
11528 .await;
11529
11530 let initial_state = "SubˇError";
11531 let buffer_marked_text = "<Sub|Error>";
11532 let completion_text = "SubscriptionError";
11533 let expected_with_insert_mode = "SubscriptionErrorˇError";
11534 let expected_with_replace_mode = "SubscriptionErrorˇ";
11535
11536 update_test_language_settings(&mut cx, |settings| {
11537 settings.defaults.completions = Some(CompletionSettings {
11538 words: WordsCompletionMode::Disabled,
11539 // set the opposite here to ensure that the action is overriding the default behavior
11540 lsp_insert_mode: LspInsertMode::Insert,
11541 lsp: true,
11542 lsp_fetch_timeout_ms: 0,
11543 });
11544 });
11545
11546 cx.set_state(initial_state);
11547 cx.update_editor(|editor, window, cx| {
11548 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11549 });
11550
11551 let counter = Arc::new(AtomicUsize::new(0));
11552 handle_completion_request_with_insert_and_replace(
11553 &mut cx,
11554 &buffer_marked_text,
11555 vec![(completion_text, completion_text)],
11556 counter.clone(),
11557 )
11558 .await;
11559 cx.condition(|editor, _| editor.context_menu_visible())
11560 .await;
11561 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11562
11563 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11564 editor
11565 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11566 .unwrap()
11567 });
11568 cx.assert_editor_state(&expected_with_replace_mode);
11569 handle_resolve_completion_request(&mut cx, None).await;
11570 apply_additional_edits.await.unwrap();
11571
11572 update_test_language_settings(&mut cx, |settings| {
11573 settings.defaults.completions = Some(CompletionSettings {
11574 words: WordsCompletionMode::Disabled,
11575 // set the opposite here to ensure that the action is overriding the default behavior
11576 lsp_insert_mode: LspInsertMode::Replace,
11577 lsp: true,
11578 lsp_fetch_timeout_ms: 0,
11579 });
11580 });
11581
11582 cx.set_state(initial_state);
11583 cx.update_editor(|editor, window, cx| {
11584 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11585 });
11586 handle_completion_request_with_insert_and_replace(
11587 &mut cx,
11588 &buffer_marked_text,
11589 vec![(completion_text, completion_text)],
11590 counter.clone(),
11591 )
11592 .await;
11593 cx.condition(|editor, _| editor.context_menu_visible())
11594 .await;
11595 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11596
11597 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11598 editor
11599 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
11600 .unwrap()
11601 });
11602 cx.assert_editor_state(&expected_with_insert_mode);
11603 handle_resolve_completion_request(&mut cx, None).await;
11604 apply_additional_edits.await.unwrap();
11605}
11606
11607#[gpui::test]
11608async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
11609 init_test(cx, |_| {});
11610 let mut cx = EditorLspTestContext::new_rust(
11611 lsp::ServerCapabilities {
11612 completion_provider: Some(lsp::CompletionOptions {
11613 resolve_provider: Some(true),
11614 ..Default::default()
11615 }),
11616 ..Default::default()
11617 },
11618 cx,
11619 )
11620 .await;
11621
11622 // scenario: surrounding text matches completion text
11623 let completion_text = "to_offset";
11624 let initial_state = indoc! {"
11625 1. buf.to_offˇsuffix
11626 2. buf.to_offˇsuf
11627 3. buf.to_offˇfix
11628 4. buf.to_offˇ
11629 5. into_offˇensive
11630 6. ˇsuffix
11631 7. let ˇ //
11632 8. aaˇzz
11633 9. buf.to_off«zzzzzˇ»suffix
11634 10. buf.«ˇzzzzz»suffix
11635 11. to_off«ˇzzzzz»
11636
11637 buf.to_offˇsuffix // newest cursor
11638 "};
11639 let completion_marked_buffer = indoc! {"
11640 1. buf.to_offsuffix
11641 2. buf.to_offsuf
11642 3. buf.to_offfix
11643 4. buf.to_off
11644 5. into_offensive
11645 6. suffix
11646 7. let //
11647 8. aazz
11648 9. buf.to_offzzzzzsuffix
11649 10. buf.zzzzzsuffix
11650 11. to_offzzzzz
11651
11652 buf.<to_off|suffix> // newest cursor
11653 "};
11654 let expected = indoc! {"
11655 1. buf.to_offsetˇ
11656 2. buf.to_offsetˇsuf
11657 3. buf.to_offsetˇfix
11658 4. buf.to_offsetˇ
11659 5. into_offsetˇensive
11660 6. to_offsetˇsuffix
11661 7. let to_offsetˇ //
11662 8. aato_offsetˇzz
11663 9. buf.to_offsetˇ
11664 10. buf.to_offsetˇsuffix
11665 11. to_offsetˇ
11666
11667 buf.to_offsetˇ // newest cursor
11668 "};
11669 cx.set_state(initial_state);
11670 cx.update_editor(|editor, window, cx| {
11671 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11672 });
11673 handle_completion_request_with_insert_and_replace(
11674 &mut cx,
11675 completion_marked_buffer,
11676 vec![(completion_text, completion_text)],
11677 Arc::new(AtomicUsize::new(0)),
11678 )
11679 .await;
11680 cx.condition(|editor, _| editor.context_menu_visible())
11681 .await;
11682 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11683 editor
11684 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11685 .unwrap()
11686 });
11687 cx.assert_editor_state(expected);
11688 handle_resolve_completion_request(&mut cx, None).await;
11689 apply_additional_edits.await.unwrap();
11690
11691 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
11692 let completion_text = "foo_and_bar";
11693 let initial_state = indoc! {"
11694 1. ooanbˇ
11695 2. zooanbˇ
11696 3. ooanbˇz
11697 4. zooanbˇz
11698 5. ooanˇ
11699 6. oanbˇ
11700
11701 ooanbˇ
11702 "};
11703 let completion_marked_buffer = indoc! {"
11704 1. ooanb
11705 2. zooanb
11706 3. ooanbz
11707 4. zooanbz
11708 5. ooan
11709 6. oanb
11710
11711 <ooanb|>
11712 "};
11713 let expected = indoc! {"
11714 1. foo_and_barˇ
11715 2. zfoo_and_barˇ
11716 3. foo_and_barˇz
11717 4. zfoo_and_barˇz
11718 5. ooanfoo_and_barˇ
11719 6. oanbfoo_and_barˇ
11720
11721 foo_and_barˇ
11722 "};
11723 cx.set_state(initial_state);
11724 cx.update_editor(|editor, window, cx| {
11725 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11726 });
11727 handle_completion_request_with_insert_and_replace(
11728 &mut cx,
11729 completion_marked_buffer,
11730 vec![(completion_text, completion_text)],
11731 Arc::new(AtomicUsize::new(0)),
11732 )
11733 .await;
11734 cx.condition(|editor, _| editor.context_menu_visible())
11735 .await;
11736 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11737 editor
11738 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11739 .unwrap()
11740 });
11741 cx.assert_editor_state(expected);
11742 handle_resolve_completion_request(&mut cx, None).await;
11743 apply_additional_edits.await.unwrap();
11744
11745 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
11746 // (expects the same as if it was inserted at the end)
11747 let completion_text = "foo_and_bar";
11748 let initial_state = indoc! {"
11749 1. ooˇanb
11750 2. zooˇanb
11751 3. ooˇanbz
11752 4. zooˇanbz
11753
11754 ooˇanb
11755 "};
11756 let completion_marked_buffer = indoc! {"
11757 1. ooanb
11758 2. zooanb
11759 3. ooanbz
11760 4. zooanbz
11761
11762 <oo|anb>
11763 "};
11764 let expected = indoc! {"
11765 1. foo_and_barˇ
11766 2. zfoo_and_barˇ
11767 3. foo_and_barˇz
11768 4. zfoo_and_barˇz
11769
11770 foo_and_barˇ
11771 "};
11772 cx.set_state(initial_state);
11773 cx.update_editor(|editor, window, cx| {
11774 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11775 });
11776 handle_completion_request_with_insert_and_replace(
11777 &mut cx,
11778 completion_marked_buffer,
11779 vec![(completion_text, completion_text)],
11780 Arc::new(AtomicUsize::new(0)),
11781 )
11782 .await;
11783 cx.condition(|editor, _| editor.context_menu_visible())
11784 .await;
11785 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11786 editor
11787 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11788 .unwrap()
11789 });
11790 cx.assert_editor_state(expected);
11791 handle_resolve_completion_request(&mut cx, None).await;
11792 apply_additional_edits.await.unwrap();
11793}
11794
11795// This used to crash
11796#[gpui::test]
11797async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
11798 init_test(cx, |_| {});
11799
11800 let buffer_text = indoc! {"
11801 fn main() {
11802 10.satu;
11803
11804 //
11805 // separate cursors so they open in different excerpts (manually reproducible)
11806 //
11807
11808 10.satu20;
11809 }
11810 "};
11811 let multibuffer_text_with_selections = indoc! {"
11812 fn main() {
11813 10.satuˇ;
11814
11815 //
11816
11817 //
11818
11819 10.satuˇ20;
11820 }
11821 "};
11822 let expected_multibuffer = indoc! {"
11823 fn main() {
11824 10.saturating_sub()ˇ;
11825
11826 //
11827
11828 //
11829
11830 10.saturating_sub()ˇ;
11831 }
11832 "};
11833
11834 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
11835 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
11836
11837 let fs = FakeFs::new(cx.executor());
11838 fs.insert_tree(
11839 path!("/a"),
11840 json!({
11841 "main.rs": buffer_text,
11842 }),
11843 )
11844 .await;
11845
11846 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11847 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11848 language_registry.add(rust_lang());
11849 let mut fake_servers = language_registry.register_fake_lsp(
11850 "Rust",
11851 FakeLspAdapter {
11852 capabilities: lsp::ServerCapabilities {
11853 completion_provider: Some(lsp::CompletionOptions {
11854 resolve_provider: None,
11855 ..lsp::CompletionOptions::default()
11856 }),
11857 ..lsp::ServerCapabilities::default()
11858 },
11859 ..FakeLspAdapter::default()
11860 },
11861 );
11862 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11863 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11864 let buffer = project
11865 .update(cx, |project, cx| {
11866 project.open_local_buffer(path!("/a/main.rs"), cx)
11867 })
11868 .await
11869 .unwrap();
11870
11871 let multi_buffer = cx.new(|cx| {
11872 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
11873 multi_buffer.push_excerpts(
11874 buffer.clone(),
11875 [ExcerptRange::new(0..first_excerpt_end)],
11876 cx,
11877 );
11878 multi_buffer.push_excerpts(
11879 buffer.clone(),
11880 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
11881 cx,
11882 );
11883 multi_buffer
11884 });
11885
11886 let editor = workspace
11887 .update(cx, |_, window, cx| {
11888 cx.new(|cx| {
11889 Editor::new(
11890 EditorMode::Full {
11891 scale_ui_elements_with_buffer_font_size: false,
11892 show_active_line_background: false,
11893 sized_by_content: false,
11894 },
11895 multi_buffer.clone(),
11896 Some(project.clone()),
11897 window,
11898 cx,
11899 )
11900 })
11901 })
11902 .unwrap();
11903
11904 let pane = workspace
11905 .update(cx, |workspace, _, _| workspace.active_pane().clone())
11906 .unwrap();
11907 pane.update_in(cx, |pane, window, cx| {
11908 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
11909 });
11910
11911 let fake_server = fake_servers.next().await.unwrap();
11912
11913 editor.update_in(cx, |editor, window, cx| {
11914 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11915 s.select_ranges([
11916 Point::new(1, 11)..Point::new(1, 11),
11917 Point::new(7, 11)..Point::new(7, 11),
11918 ])
11919 });
11920
11921 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
11922 });
11923
11924 editor.update_in(cx, |editor, window, cx| {
11925 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11926 });
11927
11928 fake_server
11929 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11930 let completion_item = lsp::CompletionItem {
11931 label: "saturating_sub()".into(),
11932 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
11933 lsp::InsertReplaceEdit {
11934 new_text: "saturating_sub()".to_owned(),
11935 insert: lsp::Range::new(
11936 lsp::Position::new(7, 7),
11937 lsp::Position::new(7, 11),
11938 ),
11939 replace: lsp::Range::new(
11940 lsp::Position::new(7, 7),
11941 lsp::Position::new(7, 13),
11942 ),
11943 },
11944 )),
11945 ..lsp::CompletionItem::default()
11946 };
11947
11948 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
11949 })
11950 .next()
11951 .await
11952 .unwrap();
11953
11954 cx.condition(&editor, |editor, _| editor.context_menu_visible())
11955 .await;
11956
11957 editor
11958 .update_in(cx, |editor, window, cx| {
11959 editor
11960 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11961 .unwrap()
11962 })
11963 .await
11964 .unwrap();
11965
11966 editor.update(cx, |editor, cx| {
11967 assert_text_with_selections(editor, expected_multibuffer, cx);
11968 })
11969}
11970
11971#[gpui::test]
11972async fn test_completion(cx: &mut TestAppContext) {
11973 init_test(cx, |_| {});
11974
11975 let mut cx = EditorLspTestContext::new_rust(
11976 lsp::ServerCapabilities {
11977 completion_provider: Some(lsp::CompletionOptions {
11978 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11979 resolve_provider: Some(true),
11980 ..Default::default()
11981 }),
11982 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11983 ..Default::default()
11984 },
11985 cx,
11986 )
11987 .await;
11988 let counter = Arc::new(AtomicUsize::new(0));
11989
11990 cx.set_state(indoc! {"
11991 oneˇ
11992 two
11993 three
11994 "});
11995 cx.simulate_keystroke(".");
11996 handle_completion_request(
11997 indoc! {"
11998 one.|<>
11999 two
12000 three
12001 "},
12002 vec!["first_completion", "second_completion"],
12003 true,
12004 counter.clone(),
12005 &mut cx,
12006 )
12007 .await;
12008 cx.condition(|editor, _| editor.context_menu_visible())
12009 .await;
12010 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12011
12012 let _handler = handle_signature_help_request(
12013 &mut cx,
12014 lsp::SignatureHelp {
12015 signatures: vec![lsp::SignatureInformation {
12016 label: "test signature".to_string(),
12017 documentation: None,
12018 parameters: Some(vec![lsp::ParameterInformation {
12019 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
12020 documentation: None,
12021 }]),
12022 active_parameter: None,
12023 }],
12024 active_signature: None,
12025 active_parameter: None,
12026 },
12027 );
12028 cx.update_editor(|editor, window, cx| {
12029 assert!(
12030 !editor.signature_help_state.is_shown(),
12031 "No signature help was called for"
12032 );
12033 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12034 });
12035 cx.run_until_parked();
12036 cx.update_editor(|editor, _, _| {
12037 assert!(
12038 !editor.signature_help_state.is_shown(),
12039 "No signature help should be shown when completions menu is open"
12040 );
12041 });
12042
12043 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12044 editor.context_menu_next(&Default::default(), window, cx);
12045 editor
12046 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12047 .unwrap()
12048 });
12049 cx.assert_editor_state(indoc! {"
12050 one.second_completionˇ
12051 two
12052 three
12053 "});
12054
12055 handle_resolve_completion_request(
12056 &mut cx,
12057 Some(vec![
12058 (
12059 //This overlaps with the primary completion edit which is
12060 //misbehavior from the LSP spec, test that we filter it out
12061 indoc! {"
12062 one.second_ˇcompletion
12063 two
12064 threeˇ
12065 "},
12066 "overlapping additional edit",
12067 ),
12068 (
12069 indoc! {"
12070 one.second_completion
12071 two
12072 threeˇ
12073 "},
12074 "\nadditional edit",
12075 ),
12076 ]),
12077 )
12078 .await;
12079 apply_additional_edits.await.unwrap();
12080 cx.assert_editor_state(indoc! {"
12081 one.second_completionˇ
12082 two
12083 three
12084 additional edit
12085 "});
12086
12087 cx.set_state(indoc! {"
12088 one.second_completion
12089 twoˇ
12090 threeˇ
12091 additional edit
12092 "});
12093 cx.simulate_keystroke(" ");
12094 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12095 cx.simulate_keystroke("s");
12096 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12097
12098 cx.assert_editor_state(indoc! {"
12099 one.second_completion
12100 two sˇ
12101 three sˇ
12102 additional edit
12103 "});
12104 handle_completion_request(
12105 indoc! {"
12106 one.second_completion
12107 two s
12108 three <s|>
12109 additional edit
12110 "},
12111 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12112 true,
12113 counter.clone(),
12114 &mut cx,
12115 )
12116 .await;
12117 cx.condition(|editor, _| editor.context_menu_visible())
12118 .await;
12119 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12120
12121 cx.simulate_keystroke("i");
12122
12123 handle_completion_request(
12124 indoc! {"
12125 one.second_completion
12126 two si
12127 three <si|>
12128 additional edit
12129 "},
12130 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12131 true,
12132 counter.clone(),
12133 &mut cx,
12134 )
12135 .await;
12136 cx.condition(|editor, _| editor.context_menu_visible())
12137 .await;
12138 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12139
12140 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12141 editor
12142 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12143 .unwrap()
12144 });
12145 cx.assert_editor_state(indoc! {"
12146 one.second_completion
12147 two sixth_completionˇ
12148 three sixth_completionˇ
12149 additional edit
12150 "});
12151
12152 apply_additional_edits.await.unwrap();
12153
12154 update_test_language_settings(&mut cx, |settings| {
12155 settings.defaults.show_completions_on_input = Some(false);
12156 });
12157 cx.set_state("editorˇ");
12158 cx.simulate_keystroke(".");
12159 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12160 cx.simulate_keystrokes("c l o");
12161 cx.assert_editor_state("editor.cloˇ");
12162 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12163 cx.update_editor(|editor, window, cx| {
12164 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12165 });
12166 handle_completion_request(
12167 "editor.<clo|>",
12168 vec!["close", "clobber"],
12169 true,
12170 counter.clone(),
12171 &mut cx,
12172 )
12173 .await;
12174 cx.condition(|editor, _| editor.context_menu_visible())
12175 .await;
12176 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
12177
12178 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12179 editor
12180 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12181 .unwrap()
12182 });
12183 cx.assert_editor_state("editor.clobberˇ");
12184 handle_resolve_completion_request(&mut cx, None).await;
12185 apply_additional_edits.await.unwrap();
12186}
12187
12188#[gpui::test]
12189async fn test_completion_reuse(cx: &mut TestAppContext) {
12190 init_test(cx, |_| {});
12191
12192 let mut cx = EditorLspTestContext::new_rust(
12193 lsp::ServerCapabilities {
12194 completion_provider: Some(lsp::CompletionOptions {
12195 trigger_characters: Some(vec![".".to_string()]),
12196 ..Default::default()
12197 }),
12198 ..Default::default()
12199 },
12200 cx,
12201 )
12202 .await;
12203
12204 let counter = Arc::new(AtomicUsize::new(0));
12205 cx.set_state("objˇ");
12206 cx.simulate_keystroke(".");
12207
12208 // Initial completion request returns complete results
12209 let is_incomplete = false;
12210 handle_completion_request(
12211 "obj.|<>",
12212 vec!["a", "ab", "abc"],
12213 is_incomplete,
12214 counter.clone(),
12215 &mut cx,
12216 )
12217 .await;
12218 cx.run_until_parked();
12219 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12220 cx.assert_editor_state("obj.ˇ");
12221 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12222
12223 // Type "a" - filters existing completions
12224 cx.simulate_keystroke("a");
12225 cx.run_until_parked();
12226 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12227 cx.assert_editor_state("obj.aˇ");
12228 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12229
12230 // Type "b" - filters existing completions
12231 cx.simulate_keystroke("b");
12232 cx.run_until_parked();
12233 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12234 cx.assert_editor_state("obj.abˇ");
12235 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12236
12237 // Type "c" - filters existing completions
12238 cx.simulate_keystroke("c");
12239 cx.run_until_parked();
12240 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12241 cx.assert_editor_state("obj.abcˇ");
12242 check_displayed_completions(vec!["abc"], &mut cx);
12243
12244 // Backspace to delete "c" - filters existing completions
12245 cx.update_editor(|editor, window, cx| {
12246 editor.backspace(&Backspace, window, cx);
12247 });
12248 cx.run_until_parked();
12249 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12250 cx.assert_editor_state("obj.abˇ");
12251 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12252
12253 // Moving cursor to the left dismisses menu.
12254 cx.update_editor(|editor, window, cx| {
12255 editor.move_left(&MoveLeft, window, cx);
12256 });
12257 cx.run_until_parked();
12258 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12259 cx.assert_editor_state("obj.aˇb");
12260 cx.update_editor(|editor, _, _| {
12261 assert_eq!(editor.context_menu_visible(), false);
12262 });
12263
12264 // Type "b" - new request
12265 cx.simulate_keystroke("b");
12266 let is_incomplete = false;
12267 handle_completion_request(
12268 "obj.<ab|>a",
12269 vec!["ab", "abc"],
12270 is_incomplete,
12271 counter.clone(),
12272 &mut cx,
12273 )
12274 .await;
12275 cx.run_until_parked();
12276 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12277 cx.assert_editor_state("obj.abˇb");
12278 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12279
12280 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
12281 cx.update_editor(|editor, window, cx| {
12282 editor.backspace(&Backspace, window, cx);
12283 });
12284 let is_incomplete = false;
12285 handle_completion_request(
12286 "obj.<a|>b",
12287 vec!["a", "ab", "abc"],
12288 is_incomplete,
12289 counter.clone(),
12290 &mut cx,
12291 )
12292 .await;
12293 cx.run_until_parked();
12294 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12295 cx.assert_editor_state("obj.aˇb");
12296 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12297
12298 // Backspace to delete "a" - dismisses menu.
12299 cx.update_editor(|editor, window, cx| {
12300 editor.backspace(&Backspace, window, cx);
12301 });
12302 cx.run_until_parked();
12303 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12304 cx.assert_editor_state("obj.ˇb");
12305 cx.update_editor(|editor, _, _| {
12306 assert_eq!(editor.context_menu_visible(), false);
12307 });
12308}
12309
12310#[gpui::test]
12311async fn test_word_completion(cx: &mut TestAppContext) {
12312 let lsp_fetch_timeout_ms = 10;
12313 init_test(cx, |language_settings| {
12314 language_settings.defaults.completions = Some(CompletionSettings {
12315 words: WordsCompletionMode::Fallback,
12316 lsp: true,
12317 lsp_fetch_timeout_ms: 10,
12318 lsp_insert_mode: LspInsertMode::Insert,
12319 });
12320 });
12321
12322 let mut cx = EditorLspTestContext::new_rust(
12323 lsp::ServerCapabilities {
12324 completion_provider: Some(lsp::CompletionOptions {
12325 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12326 ..lsp::CompletionOptions::default()
12327 }),
12328 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12329 ..lsp::ServerCapabilities::default()
12330 },
12331 cx,
12332 )
12333 .await;
12334
12335 let throttle_completions = Arc::new(AtomicBool::new(false));
12336
12337 let lsp_throttle_completions = throttle_completions.clone();
12338 let _completion_requests_handler =
12339 cx.lsp
12340 .server
12341 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
12342 let lsp_throttle_completions = lsp_throttle_completions.clone();
12343 let cx = cx.clone();
12344 async move {
12345 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
12346 cx.background_executor()
12347 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
12348 .await;
12349 }
12350 Ok(Some(lsp::CompletionResponse::Array(vec![
12351 lsp::CompletionItem {
12352 label: "first".into(),
12353 ..lsp::CompletionItem::default()
12354 },
12355 lsp::CompletionItem {
12356 label: "last".into(),
12357 ..lsp::CompletionItem::default()
12358 },
12359 ])))
12360 }
12361 });
12362
12363 cx.set_state(indoc! {"
12364 oneˇ
12365 two
12366 three
12367 "});
12368 cx.simulate_keystroke(".");
12369 cx.executor().run_until_parked();
12370 cx.condition(|editor, _| editor.context_menu_visible())
12371 .await;
12372 cx.update_editor(|editor, window, cx| {
12373 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12374 {
12375 assert_eq!(
12376 completion_menu_entries(&menu),
12377 &["first", "last"],
12378 "When LSP server is fast to reply, no fallback word completions are used"
12379 );
12380 } else {
12381 panic!("expected completion menu to be open");
12382 }
12383 editor.cancel(&Cancel, window, cx);
12384 });
12385 cx.executor().run_until_parked();
12386 cx.condition(|editor, _| !editor.context_menu_visible())
12387 .await;
12388
12389 throttle_completions.store(true, atomic::Ordering::Release);
12390 cx.simulate_keystroke(".");
12391 cx.executor()
12392 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
12393 cx.executor().run_until_parked();
12394 cx.condition(|editor, _| editor.context_menu_visible())
12395 .await;
12396 cx.update_editor(|editor, _, _| {
12397 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12398 {
12399 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
12400 "When LSP server is slow, document words can be shown instead, if configured accordingly");
12401 } else {
12402 panic!("expected completion menu to be open");
12403 }
12404 });
12405}
12406
12407#[gpui::test]
12408async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
12409 init_test(cx, |language_settings| {
12410 language_settings.defaults.completions = Some(CompletionSettings {
12411 words: WordsCompletionMode::Enabled,
12412 lsp: true,
12413 lsp_fetch_timeout_ms: 0,
12414 lsp_insert_mode: LspInsertMode::Insert,
12415 });
12416 });
12417
12418 let mut cx = EditorLspTestContext::new_rust(
12419 lsp::ServerCapabilities {
12420 completion_provider: Some(lsp::CompletionOptions {
12421 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12422 ..lsp::CompletionOptions::default()
12423 }),
12424 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12425 ..lsp::ServerCapabilities::default()
12426 },
12427 cx,
12428 )
12429 .await;
12430
12431 let _completion_requests_handler =
12432 cx.lsp
12433 .server
12434 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12435 Ok(Some(lsp::CompletionResponse::Array(vec![
12436 lsp::CompletionItem {
12437 label: "first".into(),
12438 ..lsp::CompletionItem::default()
12439 },
12440 lsp::CompletionItem {
12441 label: "last".into(),
12442 ..lsp::CompletionItem::default()
12443 },
12444 ])))
12445 });
12446
12447 cx.set_state(indoc! {"ˇ
12448 first
12449 last
12450 second
12451 "});
12452 cx.simulate_keystroke(".");
12453 cx.executor().run_until_parked();
12454 cx.condition(|editor, _| editor.context_menu_visible())
12455 .await;
12456 cx.update_editor(|editor, _, _| {
12457 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12458 {
12459 assert_eq!(
12460 completion_menu_entries(&menu),
12461 &["first", "last", "second"],
12462 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
12463 );
12464 } else {
12465 panic!("expected completion menu to be open");
12466 }
12467 });
12468}
12469
12470#[gpui::test]
12471async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
12472 init_test(cx, |language_settings| {
12473 language_settings.defaults.completions = Some(CompletionSettings {
12474 words: WordsCompletionMode::Disabled,
12475 lsp: true,
12476 lsp_fetch_timeout_ms: 0,
12477 lsp_insert_mode: LspInsertMode::Insert,
12478 });
12479 });
12480
12481 let mut cx = EditorLspTestContext::new_rust(
12482 lsp::ServerCapabilities {
12483 completion_provider: Some(lsp::CompletionOptions {
12484 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12485 ..lsp::CompletionOptions::default()
12486 }),
12487 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12488 ..lsp::ServerCapabilities::default()
12489 },
12490 cx,
12491 )
12492 .await;
12493
12494 let _completion_requests_handler =
12495 cx.lsp
12496 .server
12497 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12498 panic!("LSP completions should not be queried when dealing with word completions")
12499 });
12500
12501 cx.set_state(indoc! {"ˇ
12502 first
12503 last
12504 second
12505 "});
12506 cx.update_editor(|editor, window, cx| {
12507 editor.show_word_completions(&ShowWordCompletions, window, cx);
12508 });
12509 cx.executor().run_until_parked();
12510 cx.condition(|editor, _| editor.context_menu_visible())
12511 .await;
12512 cx.update_editor(|editor, _, _| {
12513 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12514 {
12515 assert_eq!(
12516 completion_menu_entries(&menu),
12517 &["first", "last", "second"],
12518 "`ShowWordCompletions` action should show word completions"
12519 );
12520 } else {
12521 panic!("expected completion menu to be open");
12522 }
12523 });
12524
12525 cx.simulate_keystroke("l");
12526 cx.executor().run_until_parked();
12527 cx.condition(|editor, _| editor.context_menu_visible())
12528 .await;
12529 cx.update_editor(|editor, _, _| {
12530 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12531 {
12532 assert_eq!(
12533 completion_menu_entries(&menu),
12534 &["last"],
12535 "After showing word completions, further editing should filter them and not query the LSP"
12536 );
12537 } else {
12538 panic!("expected completion menu to be open");
12539 }
12540 });
12541}
12542
12543#[gpui::test]
12544async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
12545 init_test(cx, |language_settings| {
12546 language_settings.defaults.completions = Some(CompletionSettings {
12547 words: WordsCompletionMode::Fallback,
12548 lsp: false,
12549 lsp_fetch_timeout_ms: 0,
12550 lsp_insert_mode: LspInsertMode::Insert,
12551 });
12552 });
12553
12554 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12555
12556 cx.set_state(indoc! {"ˇ
12557 0_usize
12558 let
12559 33
12560 4.5f32
12561 "});
12562 cx.update_editor(|editor, window, cx| {
12563 editor.show_completions(&ShowCompletions::default(), window, cx);
12564 });
12565 cx.executor().run_until_parked();
12566 cx.condition(|editor, _| editor.context_menu_visible())
12567 .await;
12568 cx.update_editor(|editor, window, cx| {
12569 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12570 {
12571 assert_eq!(
12572 completion_menu_entries(&menu),
12573 &["let"],
12574 "With no digits in the completion query, no digits should be in the word completions"
12575 );
12576 } else {
12577 panic!("expected completion menu to be open");
12578 }
12579 editor.cancel(&Cancel, window, cx);
12580 });
12581
12582 cx.set_state(indoc! {"3ˇ
12583 0_usize
12584 let
12585 3
12586 33.35f32
12587 "});
12588 cx.update_editor(|editor, window, cx| {
12589 editor.show_completions(&ShowCompletions::default(), window, cx);
12590 });
12591 cx.executor().run_until_parked();
12592 cx.condition(|editor, _| editor.context_menu_visible())
12593 .await;
12594 cx.update_editor(|editor, _, _| {
12595 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12596 {
12597 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
12598 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
12599 } else {
12600 panic!("expected completion menu to be open");
12601 }
12602 });
12603}
12604
12605fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
12606 let position = || lsp::Position {
12607 line: params.text_document_position.position.line,
12608 character: params.text_document_position.position.character,
12609 };
12610 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12611 range: lsp::Range {
12612 start: position(),
12613 end: position(),
12614 },
12615 new_text: text.to_string(),
12616 }))
12617}
12618
12619#[gpui::test]
12620async fn test_multiline_completion(cx: &mut TestAppContext) {
12621 init_test(cx, |_| {});
12622
12623 let fs = FakeFs::new(cx.executor());
12624 fs.insert_tree(
12625 path!("/a"),
12626 json!({
12627 "main.ts": "a",
12628 }),
12629 )
12630 .await;
12631
12632 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12633 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12634 let typescript_language = Arc::new(Language::new(
12635 LanguageConfig {
12636 name: "TypeScript".into(),
12637 matcher: LanguageMatcher {
12638 path_suffixes: vec!["ts".to_string()],
12639 ..LanguageMatcher::default()
12640 },
12641 line_comments: vec!["// ".into()],
12642 ..LanguageConfig::default()
12643 },
12644 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12645 ));
12646 language_registry.add(typescript_language.clone());
12647 let mut fake_servers = language_registry.register_fake_lsp(
12648 "TypeScript",
12649 FakeLspAdapter {
12650 capabilities: lsp::ServerCapabilities {
12651 completion_provider: Some(lsp::CompletionOptions {
12652 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12653 ..lsp::CompletionOptions::default()
12654 }),
12655 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12656 ..lsp::ServerCapabilities::default()
12657 },
12658 // Emulate vtsls label generation
12659 label_for_completion: Some(Box::new(|item, _| {
12660 let text = if let Some(description) = item
12661 .label_details
12662 .as_ref()
12663 .and_then(|label_details| label_details.description.as_ref())
12664 {
12665 format!("{} {}", item.label, description)
12666 } else if let Some(detail) = &item.detail {
12667 format!("{} {}", item.label, detail)
12668 } else {
12669 item.label.clone()
12670 };
12671 let len = text.len();
12672 Some(language::CodeLabel {
12673 text,
12674 runs: Vec::new(),
12675 filter_range: 0..len,
12676 })
12677 })),
12678 ..FakeLspAdapter::default()
12679 },
12680 );
12681 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12682 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12683 let worktree_id = workspace
12684 .update(cx, |workspace, _window, cx| {
12685 workspace.project().update(cx, |project, cx| {
12686 project.worktrees(cx).next().unwrap().read(cx).id()
12687 })
12688 })
12689 .unwrap();
12690 let _buffer = project
12691 .update(cx, |project, cx| {
12692 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
12693 })
12694 .await
12695 .unwrap();
12696 let editor = workspace
12697 .update(cx, |workspace, window, cx| {
12698 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
12699 })
12700 .unwrap()
12701 .await
12702 .unwrap()
12703 .downcast::<Editor>()
12704 .unwrap();
12705 let fake_server = fake_servers.next().await.unwrap();
12706
12707 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
12708 let multiline_label_2 = "a\nb\nc\n";
12709 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
12710 let multiline_description = "d\ne\nf\n";
12711 let multiline_detail_2 = "g\nh\ni\n";
12712
12713 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
12714 move |params, _| async move {
12715 Ok(Some(lsp::CompletionResponse::Array(vec![
12716 lsp::CompletionItem {
12717 label: multiline_label.to_string(),
12718 text_edit: gen_text_edit(¶ms, "new_text_1"),
12719 ..lsp::CompletionItem::default()
12720 },
12721 lsp::CompletionItem {
12722 label: "single line label 1".to_string(),
12723 detail: Some(multiline_detail.to_string()),
12724 text_edit: gen_text_edit(¶ms, "new_text_2"),
12725 ..lsp::CompletionItem::default()
12726 },
12727 lsp::CompletionItem {
12728 label: "single line label 2".to_string(),
12729 label_details: Some(lsp::CompletionItemLabelDetails {
12730 description: Some(multiline_description.to_string()),
12731 detail: None,
12732 }),
12733 text_edit: gen_text_edit(¶ms, "new_text_2"),
12734 ..lsp::CompletionItem::default()
12735 },
12736 lsp::CompletionItem {
12737 label: multiline_label_2.to_string(),
12738 detail: Some(multiline_detail_2.to_string()),
12739 text_edit: gen_text_edit(¶ms, "new_text_3"),
12740 ..lsp::CompletionItem::default()
12741 },
12742 lsp::CompletionItem {
12743 label: "Label with many spaces and \t but without newlines".to_string(),
12744 detail: Some(
12745 "Details with many spaces and \t but without newlines".to_string(),
12746 ),
12747 text_edit: gen_text_edit(¶ms, "new_text_4"),
12748 ..lsp::CompletionItem::default()
12749 },
12750 ])))
12751 },
12752 );
12753
12754 editor.update_in(cx, |editor, window, cx| {
12755 cx.focus_self(window);
12756 editor.move_to_end(&MoveToEnd, window, cx);
12757 editor.handle_input(".", window, cx);
12758 });
12759 cx.run_until_parked();
12760 completion_handle.next().await.unwrap();
12761
12762 editor.update(cx, |editor, _| {
12763 assert!(editor.context_menu_visible());
12764 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12765 {
12766 let completion_labels = menu
12767 .completions
12768 .borrow()
12769 .iter()
12770 .map(|c| c.label.text.clone())
12771 .collect::<Vec<_>>();
12772 assert_eq!(
12773 completion_labels,
12774 &[
12775 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
12776 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
12777 "single line label 2 d e f ",
12778 "a b c g h i ",
12779 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
12780 ],
12781 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
12782 );
12783
12784 for completion in menu
12785 .completions
12786 .borrow()
12787 .iter() {
12788 assert_eq!(
12789 completion.label.filter_range,
12790 0..completion.label.text.len(),
12791 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
12792 );
12793 }
12794 } else {
12795 panic!("expected completion menu to be open");
12796 }
12797 });
12798}
12799
12800#[gpui::test]
12801async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
12802 init_test(cx, |_| {});
12803 let mut cx = EditorLspTestContext::new_rust(
12804 lsp::ServerCapabilities {
12805 completion_provider: Some(lsp::CompletionOptions {
12806 trigger_characters: Some(vec![".".to_string()]),
12807 ..Default::default()
12808 }),
12809 ..Default::default()
12810 },
12811 cx,
12812 )
12813 .await;
12814 cx.lsp
12815 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12816 Ok(Some(lsp::CompletionResponse::Array(vec![
12817 lsp::CompletionItem {
12818 label: "first".into(),
12819 ..Default::default()
12820 },
12821 lsp::CompletionItem {
12822 label: "last".into(),
12823 ..Default::default()
12824 },
12825 ])))
12826 });
12827 cx.set_state("variableˇ");
12828 cx.simulate_keystroke(".");
12829 cx.executor().run_until_parked();
12830
12831 cx.update_editor(|editor, _, _| {
12832 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12833 {
12834 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
12835 } else {
12836 panic!("expected completion menu to be open");
12837 }
12838 });
12839
12840 cx.update_editor(|editor, window, cx| {
12841 editor.move_page_down(&MovePageDown::default(), window, cx);
12842 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12843 {
12844 assert!(
12845 menu.selected_item == 1,
12846 "expected PageDown to select the last item from the context menu"
12847 );
12848 } else {
12849 panic!("expected completion menu to stay open after PageDown");
12850 }
12851 });
12852
12853 cx.update_editor(|editor, window, cx| {
12854 editor.move_page_up(&MovePageUp::default(), window, cx);
12855 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12856 {
12857 assert!(
12858 menu.selected_item == 0,
12859 "expected PageUp to select the first item from the context menu"
12860 );
12861 } else {
12862 panic!("expected completion menu to stay open after PageUp");
12863 }
12864 });
12865}
12866
12867#[gpui::test]
12868async fn test_as_is_completions(cx: &mut TestAppContext) {
12869 init_test(cx, |_| {});
12870 let mut cx = EditorLspTestContext::new_rust(
12871 lsp::ServerCapabilities {
12872 completion_provider: Some(lsp::CompletionOptions {
12873 ..Default::default()
12874 }),
12875 ..Default::default()
12876 },
12877 cx,
12878 )
12879 .await;
12880 cx.lsp
12881 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12882 Ok(Some(lsp::CompletionResponse::Array(vec![
12883 lsp::CompletionItem {
12884 label: "unsafe".into(),
12885 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12886 range: lsp::Range {
12887 start: lsp::Position {
12888 line: 1,
12889 character: 2,
12890 },
12891 end: lsp::Position {
12892 line: 1,
12893 character: 3,
12894 },
12895 },
12896 new_text: "unsafe".to_string(),
12897 })),
12898 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
12899 ..Default::default()
12900 },
12901 ])))
12902 });
12903 cx.set_state("fn a() {}\n nˇ");
12904 cx.executor().run_until_parked();
12905 cx.update_editor(|editor, window, cx| {
12906 editor.show_completions(
12907 &ShowCompletions {
12908 trigger: Some("\n".into()),
12909 },
12910 window,
12911 cx,
12912 );
12913 });
12914 cx.executor().run_until_parked();
12915
12916 cx.update_editor(|editor, window, cx| {
12917 editor.confirm_completion(&Default::default(), window, cx)
12918 });
12919 cx.executor().run_until_parked();
12920 cx.assert_editor_state("fn a() {}\n unsafeˇ");
12921}
12922
12923#[gpui::test]
12924async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
12925 init_test(cx, |_| {});
12926
12927 let mut cx = EditorLspTestContext::new_rust(
12928 lsp::ServerCapabilities {
12929 completion_provider: Some(lsp::CompletionOptions {
12930 trigger_characters: Some(vec![".".to_string()]),
12931 resolve_provider: Some(true),
12932 ..Default::default()
12933 }),
12934 ..Default::default()
12935 },
12936 cx,
12937 )
12938 .await;
12939
12940 cx.set_state("fn main() { let a = 2ˇ; }");
12941 cx.simulate_keystroke(".");
12942 let completion_item = lsp::CompletionItem {
12943 label: "Some".into(),
12944 kind: Some(lsp::CompletionItemKind::SNIPPET),
12945 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
12946 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
12947 kind: lsp::MarkupKind::Markdown,
12948 value: "```rust\nSome(2)\n```".to_string(),
12949 })),
12950 deprecated: Some(false),
12951 sort_text: Some("Some".to_string()),
12952 filter_text: Some("Some".to_string()),
12953 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
12954 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12955 range: lsp::Range {
12956 start: lsp::Position {
12957 line: 0,
12958 character: 22,
12959 },
12960 end: lsp::Position {
12961 line: 0,
12962 character: 22,
12963 },
12964 },
12965 new_text: "Some(2)".to_string(),
12966 })),
12967 additional_text_edits: Some(vec![lsp::TextEdit {
12968 range: lsp::Range {
12969 start: lsp::Position {
12970 line: 0,
12971 character: 20,
12972 },
12973 end: lsp::Position {
12974 line: 0,
12975 character: 22,
12976 },
12977 },
12978 new_text: "".to_string(),
12979 }]),
12980 ..Default::default()
12981 };
12982
12983 let closure_completion_item = completion_item.clone();
12984 let counter = Arc::new(AtomicUsize::new(0));
12985 let counter_clone = counter.clone();
12986 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
12987 let task_completion_item = closure_completion_item.clone();
12988 counter_clone.fetch_add(1, atomic::Ordering::Release);
12989 async move {
12990 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
12991 is_incomplete: true,
12992 item_defaults: None,
12993 items: vec![task_completion_item],
12994 })))
12995 }
12996 });
12997
12998 cx.condition(|editor, _| editor.context_menu_visible())
12999 .await;
13000 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
13001 assert!(request.next().await.is_some());
13002 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13003
13004 cx.simulate_keystrokes("S o m");
13005 cx.condition(|editor, _| editor.context_menu_visible())
13006 .await;
13007 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
13008 assert!(request.next().await.is_some());
13009 assert!(request.next().await.is_some());
13010 assert!(request.next().await.is_some());
13011 request.close();
13012 assert!(request.next().await.is_none());
13013 assert_eq!(
13014 counter.load(atomic::Ordering::Acquire),
13015 4,
13016 "With the completions menu open, only one LSP request should happen per input"
13017 );
13018}
13019
13020#[gpui::test]
13021async fn test_toggle_comment(cx: &mut TestAppContext) {
13022 init_test(cx, |_| {});
13023 let mut cx = EditorTestContext::new(cx).await;
13024 let language = Arc::new(Language::new(
13025 LanguageConfig {
13026 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13027 ..Default::default()
13028 },
13029 Some(tree_sitter_rust::LANGUAGE.into()),
13030 ));
13031 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13032
13033 // If multiple selections intersect a line, the line is only toggled once.
13034 cx.set_state(indoc! {"
13035 fn a() {
13036 «//b();
13037 ˇ»// «c();
13038 //ˇ» d();
13039 }
13040 "});
13041
13042 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13043
13044 cx.assert_editor_state(indoc! {"
13045 fn a() {
13046 «b();
13047 c();
13048 ˇ» d();
13049 }
13050 "});
13051
13052 // The comment prefix is inserted at the same column for every line in a
13053 // selection.
13054 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13055
13056 cx.assert_editor_state(indoc! {"
13057 fn a() {
13058 // «b();
13059 // c();
13060 ˇ»// d();
13061 }
13062 "});
13063
13064 // If a selection ends at the beginning of a line, that line is not toggled.
13065 cx.set_selections_state(indoc! {"
13066 fn a() {
13067 // b();
13068 «// c();
13069 ˇ» // d();
13070 }
13071 "});
13072
13073 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13074
13075 cx.assert_editor_state(indoc! {"
13076 fn a() {
13077 // b();
13078 «c();
13079 ˇ» // d();
13080 }
13081 "});
13082
13083 // If a selection span a single line and is empty, the line is toggled.
13084 cx.set_state(indoc! {"
13085 fn a() {
13086 a();
13087 b();
13088 ˇ
13089 }
13090 "});
13091
13092 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13093
13094 cx.assert_editor_state(indoc! {"
13095 fn a() {
13096 a();
13097 b();
13098 //•ˇ
13099 }
13100 "});
13101
13102 // If a selection span multiple lines, empty lines are not toggled.
13103 cx.set_state(indoc! {"
13104 fn a() {
13105 «a();
13106
13107 c();ˇ»
13108 }
13109 "});
13110
13111 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13112
13113 cx.assert_editor_state(indoc! {"
13114 fn a() {
13115 // «a();
13116
13117 // c();ˇ»
13118 }
13119 "});
13120
13121 // If a selection includes multiple comment prefixes, all lines are uncommented.
13122 cx.set_state(indoc! {"
13123 fn a() {
13124 «// a();
13125 /// b();
13126 //! c();ˇ»
13127 }
13128 "});
13129
13130 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13131
13132 cx.assert_editor_state(indoc! {"
13133 fn a() {
13134 «a();
13135 b();
13136 c();ˇ»
13137 }
13138 "});
13139}
13140
13141#[gpui::test]
13142async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
13143 init_test(cx, |_| {});
13144 let mut cx = EditorTestContext::new(cx).await;
13145 let language = Arc::new(Language::new(
13146 LanguageConfig {
13147 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13148 ..Default::default()
13149 },
13150 Some(tree_sitter_rust::LANGUAGE.into()),
13151 ));
13152 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13153
13154 let toggle_comments = &ToggleComments {
13155 advance_downwards: false,
13156 ignore_indent: true,
13157 };
13158
13159 // If multiple selections intersect a line, the line is only toggled once.
13160 cx.set_state(indoc! {"
13161 fn a() {
13162 // «b();
13163 // c();
13164 // ˇ» d();
13165 }
13166 "});
13167
13168 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13169
13170 cx.assert_editor_state(indoc! {"
13171 fn a() {
13172 «b();
13173 c();
13174 ˇ» d();
13175 }
13176 "});
13177
13178 // The comment prefix is inserted at the beginning of each line
13179 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13180
13181 cx.assert_editor_state(indoc! {"
13182 fn a() {
13183 // «b();
13184 // c();
13185 // ˇ» d();
13186 }
13187 "});
13188
13189 // If a selection ends at the beginning of a line, that line is not toggled.
13190 cx.set_selections_state(indoc! {"
13191 fn a() {
13192 // b();
13193 // «c();
13194 ˇ»// d();
13195 }
13196 "});
13197
13198 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13199
13200 cx.assert_editor_state(indoc! {"
13201 fn a() {
13202 // b();
13203 «c();
13204 ˇ»// d();
13205 }
13206 "});
13207
13208 // If a selection span a single line and is empty, the line is toggled.
13209 cx.set_state(indoc! {"
13210 fn a() {
13211 a();
13212 b();
13213 ˇ
13214 }
13215 "});
13216
13217 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13218
13219 cx.assert_editor_state(indoc! {"
13220 fn a() {
13221 a();
13222 b();
13223 //ˇ
13224 }
13225 "});
13226
13227 // If a selection span multiple lines, empty lines are not toggled.
13228 cx.set_state(indoc! {"
13229 fn a() {
13230 «a();
13231
13232 c();ˇ»
13233 }
13234 "});
13235
13236 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13237
13238 cx.assert_editor_state(indoc! {"
13239 fn a() {
13240 // «a();
13241
13242 // c();ˇ»
13243 }
13244 "});
13245
13246 // If a selection includes multiple comment prefixes, all lines are uncommented.
13247 cx.set_state(indoc! {"
13248 fn a() {
13249 // «a();
13250 /// b();
13251 //! c();ˇ»
13252 }
13253 "});
13254
13255 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13256
13257 cx.assert_editor_state(indoc! {"
13258 fn a() {
13259 «a();
13260 b();
13261 c();ˇ»
13262 }
13263 "});
13264}
13265
13266#[gpui::test]
13267async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
13268 init_test(cx, |_| {});
13269
13270 let language = Arc::new(Language::new(
13271 LanguageConfig {
13272 line_comments: vec!["// ".into()],
13273 ..Default::default()
13274 },
13275 Some(tree_sitter_rust::LANGUAGE.into()),
13276 ));
13277
13278 let mut cx = EditorTestContext::new(cx).await;
13279
13280 cx.language_registry().add(language.clone());
13281 cx.update_buffer(|buffer, cx| {
13282 buffer.set_language(Some(language), cx);
13283 });
13284
13285 let toggle_comments = &ToggleComments {
13286 advance_downwards: true,
13287 ignore_indent: false,
13288 };
13289
13290 // Single cursor on one line -> advance
13291 // Cursor moves horizontally 3 characters as well on non-blank line
13292 cx.set_state(indoc!(
13293 "fn a() {
13294 ˇdog();
13295 cat();
13296 }"
13297 ));
13298 cx.update_editor(|editor, window, cx| {
13299 editor.toggle_comments(toggle_comments, window, cx);
13300 });
13301 cx.assert_editor_state(indoc!(
13302 "fn a() {
13303 // dog();
13304 catˇ();
13305 }"
13306 ));
13307
13308 // Single selection on one line -> don't advance
13309 cx.set_state(indoc!(
13310 "fn a() {
13311 «dog()ˇ»;
13312 cat();
13313 }"
13314 ));
13315 cx.update_editor(|editor, window, cx| {
13316 editor.toggle_comments(toggle_comments, window, cx);
13317 });
13318 cx.assert_editor_state(indoc!(
13319 "fn a() {
13320 // «dog()ˇ»;
13321 cat();
13322 }"
13323 ));
13324
13325 // Multiple cursors on one line -> advance
13326 cx.set_state(indoc!(
13327 "fn a() {
13328 ˇdˇog();
13329 cat();
13330 }"
13331 ));
13332 cx.update_editor(|editor, window, cx| {
13333 editor.toggle_comments(toggle_comments, window, cx);
13334 });
13335 cx.assert_editor_state(indoc!(
13336 "fn a() {
13337 // dog();
13338 catˇ(ˇ);
13339 }"
13340 ));
13341
13342 // Multiple cursors on one line, with selection -> don't advance
13343 cx.set_state(indoc!(
13344 "fn a() {
13345 ˇdˇog«()ˇ»;
13346 cat();
13347 }"
13348 ));
13349 cx.update_editor(|editor, window, cx| {
13350 editor.toggle_comments(toggle_comments, window, cx);
13351 });
13352 cx.assert_editor_state(indoc!(
13353 "fn a() {
13354 // ˇdˇog«()ˇ»;
13355 cat();
13356 }"
13357 ));
13358
13359 // Single cursor on one line -> advance
13360 // Cursor moves to column 0 on blank line
13361 cx.set_state(indoc!(
13362 "fn a() {
13363 ˇdog();
13364
13365 cat();
13366 }"
13367 ));
13368 cx.update_editor(|editor, window, cx| {
13369 editor.toggle_comments(toggle_comments, window, cx);
13370 });
13371 cx.assert_editor_state(indoc!(
13372 "fn a() {
13373 // dog();
13374 ˇ
13375 cat();
13376 }"
13377 ));
13378
13379 // Single cursor on one line -> advance
13380 // Cursor starts and ends at column 0
13381 cx.set_state(indoc!(
13382 "fn a() {
13383 ˇ dog();
13384 cat();
13385 }"
13386 ));
13387 cx.update_editor(|editor, window, cx| {
13388 editor.toggle_comments(toggle_comments, window, cx);
13389 });
13390 cx.assert_editor_state(indoc!(
13391 "fn a() {
13392 // dog();
13393 ˇ cat();
13394 }"
13395 ));
13396}
13397
13398#[gpui::test]
13399async fn test_toggle_block_comment(cx: &mut TestAppContext) {
13400 init_test(cx, |_| {});
13401
13402 let mut cx = EditorTestContext::new(cx).await;
13403
13404 let html_language = Arc::new(
13405 Language::new(
13406 LanguageConfig {
13407 name: "HTML".into(),
13408 block_comment: Some(("<!-- ".into(), " -->".into())),
13409 ..Default::default()
13410 },
13411 Some(tree_sitter_html::LANGUAGE.into()),
13412 )
13413 .with_injection_query(
13414 r#"
13415 (script_element
13416 (raw_text) @injection.content
13417 (#set! injection.language "javascript"))
13418 "#,
13419 )
13420 .unwrap(),
13421 );
13422
13423 let javascript_language = Arc::new(Language::new(
13424 LanguageConfig {
13425 name: "JavaScript".into(),
13426 line_comments: vec!["// ".into()],
13427 ..Default::default()
13428 },
13429 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
13430 ));
13431
13432 cx.language_registry().add(html_language.clone());
13433 cx.language_registry().add(javascript_language.clone());
13434 cx.update_buffer(|buffer, cx| {
13435 buffer.set_language(Some(html_language), cx);
13436 });
13437
13438 // Toggle comments for empty selections
13439 cx.set_state(
13440 &r#"
13441 <p>A</p>ˇ
13442 <p>B</p>ˇ
13443 <p>C</p>ˇ
13444 "#
13445 .unindent(),
13446 );
13447 cx.update_editor(|editor, window, cx| {
13448 editor.toggle_comments(&ToggleComments::default(), window, cx)
13449 });
13450 cx.assert_editor_state(
13451 &r#"
13452 <!-- <p>A</p>ˇ -->
13453 <!-- <p>B</p>ˇ -->
13454 <!-- <p>C</p>ˇ -->
13455 "#
13456 .unindent(),
13457 );
13458 cx.update_editor(|editor, window, cx| {
13459 editor.toggle_comments(&ToggleComments::default(), window, cx)
13460 });
13461 cx.assert_editor_state(
13462 &r#"
13463 <p>A</p>ˇ
13464 <p>B</p>ˇ
13465 <p>C</p>ˇ
13466 "#
13467 .unindent(),
13468 );
13469
13470 // Toggle comments for mixture of empty and non-empty selections, where
13471 // multiple selections occupy a given line.
13472 cx.set_state(
13473 &r#"
13474 <p>A«</p>
13475 <p>ˇ»B</p>ˇ
13476 <p>C«</p>
13477 <p>ˇ»D</p>ˇ
13478 "#
13479 .unindent(),
13480 );
13481
13482 cx.update_editor(|editor, window, cx| {
13483 editor.toggle_comments(&ToggleComments::default(), window, cx)
13484 });
13485 cx.assert_editor_state(
13486 &r#"
13487 <!-- <p>A«</p>
13488 <p>ˇ»B</p>ˇ -->
13489 <!-- <p>C«</p>
13490 <p>ˇ»D</p>ˇ -->
13491 "#
13492 .unindent(),
13493 );
13494 cx.update_editor(|editor, window, cx| {
13495 editor.toggle_comments(&ToggleComments::default(), window, cx)
13496 });
13497 cx.assert_editor_state(
13498 &r#"
13499 <p>A«</p>
13500 <p>ˇ»B</p>ˇ
13501 <p>C«</p>
13502 <p>ˇ»D</p>ˇ
13503 "#
13504 .unindent(),
13505 );
13506
13507 // Toggle comments when different languages are active for different
13508 // selections.
13509 cx.set_state(
13510 &r#"
13511 ˇ<script>
13512 ˇvar x = new Y();
13513 ˇ</script>
13514 "#
13515 .unindent(),
13516 );
13517 cx.executor().run_until_parked();
13518 cx.update_editor(|editor, window, cx| {
13519 editor.toggle_comments(&ToggleComments::default(), window, cx)
13520 });
13521 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
13522 // Uncommenting and commenting from this position brings in even more wrong artifacts.
13523 cx.assert_editor_state(
13524 &r#"
13525 <!-- ˇ<script> -->
13526 // ˇvar x = new Y();
13527 <!-- ˇ</script> -->
13528 "#
13529 .unindent(),
13530 );
13531}
13532
13533#[gpui::test]
13534fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
13535 init_test(cx, |_| {});
13536
13537 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13538 let multibuffer = cx.new(|cx| {
13539 let mut multibuffer = MultiBuffer::new(ReadWrite);
13540 multibuffer.push_excerpts(
13541 buffer.clone(),
13542 [
13543 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
13544 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
13545 ],
13546 cx,
13547 );
13548 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
13549 multibuffer
13550 });
13551
13552 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
13553 editor.update_in(cx, |editor, window, cx| {
13554 assert_eq!(editor.text(cx), "aaaa\nbbbb");
13555 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13556 s.select_ranges([
13557 Point::new(0, 0)..Point::new(0, 0),
13558 Point::new(1, 0)..Point::new(1, 0),
13559 ])
13560 });
13561
13562 editor.handle_input("X", window, cx);
13563 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
13564 assert_eq!(
13565 editor.selections.ranges(cx),
13566 [
13567 Point::new(0, 1)..Point::new(0, 1),
13568 Point::new(1, 1)..Point::new(1, 1),
13569 ]
13570 );
13571
13572 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
13573 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13574 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
13575 });
13576 editor.backspace(&Default::default(), window, cx);
13577 assert_eq!(editor.text(cx), "Xa\nbbb");
13578 assert_eq!(
13579 editor.selections.ranges(cx),
13580 [Point::new(1, 0)..Point::new(1, 0)]
13581 );
13582
13583 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13584 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
13585 });
13586 editor.backspace(&Default::default(), window, cx);
13587 assert_eq!(editor.text(cx), "X\nbb");
13588 assert_eq!(
13589 editor.selections.ranges(cx),
13590 [Point::new(0, 1)..Point::new(0, 1)]
13591 );
13592 });
13593}
13594
13595#[gpui::test]
13596fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
13597 init_test(cx, |_| {});
13598
13599 let markers = vec![('[', ']').into(), ('(', ')').into()];
13600 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
13601 indoc! {"
13602 [aaaa
13603 (bbbb]
13604 cccc)",
13605 },
13606 markers.clone(),
13607 );
13608 let excerpt_ranges = markers.into_iter().map(|marker| {
13609 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
13610 ExcerptRange::new(context.clone())
13611 });
13612 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
13613 let multibuffer = cx.new(|cx| {
13614 let mut multibuffer = MultiBuffer::new(ReadWrite);
13615 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
13616 multibuffer
13617 });
13618
13619 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
13620 editor.update_in(cx, |editor, window, cx| {
13621 let (expected_text, selection_ranges) = marked_text_ranges(
13622 indoc! {"
13623 aaaa
13624 bˇbbb
13625 bˇbbˇb
13626 cccc"
13627 },
13628 true,
13629 );
13630 assert_eq!(editor.text(cx), expected_text);
13631 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13632 s.select_ranges(selection_ranges)
13633 });
13634
13635 editor.handle_input("X", window, cx);
13636
13637 let (expected_text, expected_selections) = marked_text_ranges(
13638 indoc! {"
13639 aaaa
13640 bXˇbbXb
13641 bXˇbbXˇb
13642 cccc"
13643 },
13644 false,
13645 );
13646 assert_eq!(editor.text(cx), expected_text);
13647 assert_eq!(editor.selections.ranges(cx), expected_selections);
13648
13649 editor.newline(&Newline, window, cx);
13650 let (expected_text, expected_selections) = marked_text_ranges(
13651 indoc! {"
13652 aaaa
13653 bX
13654 ˇbbX
13655 b
13656 bX
13657 ˇbbX
13658 ˇb
13659 cccc"
13660 },
13661 false,
13662 );
13663 assert_eq!(editor.text(cx), expected_text);
13664 assert_eq!(editor.selections.ranges(cx), expected_selections);
13665 });
13666}
13667
13668#[gpui::test]
13669fn test_refresh_selections(cx: &mut TestAppContext) {
13670 init_test(cx, |_| {});
13671
13672 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13673 let mut excerpt1_id = None;
13674 let multibuffer = cx.new(|cx| {
13675 let mut multibuffer = MultiBuffer::new(ReadWrite);
13676 excerpt1_id = multibuffer
13677 .push_excerpts(
13678 buffer.clone(),
13679 [
13680 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
13681 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
13682 ],
13683 cx,
13684 )
13685 .into_iter()
13686 .next();
13687 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
13688 multibuffer
13689 });
13690
13691 let editor = cx.add_window(|window, cx| {
13692 let mut editor = build_editor(multibuffer.clone(), window, cx);
13693 let snapshot = editor.snapshot(window, cx);
13694 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13695 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
13696 });
13697 editor.begin_selection(
13698 Point::new(2, 1).to_display_point(&snapshot),
13699 true,
13700 1,
13701 window,
13702 cx,
13703 );
13704 assert_eq!(
13705 editor.selections.ranges(cx),
13706 [
13707 Point::new(1, 3)..Point::new(1, 3),
13708 Point::new(2, 1)..Point::new(2, 1),
13709 ]
13710 );
13711 editor
13712 });
13713
13714 // Refreshing selections is a no-op when excerpts haven't changed.
13715 _ = editor.update(cx, |editor, window, cx| {
13716 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
13717 assert_eq!(
13718 editor.selections.ranges(cx),
13719 [
13720 Point::new(1, 3)..Point::new(1, 3),
13721 Point::new(2, 1)..Point::new(2, 1),
13722 ]
13723 );
13724 });
13725
13726 multibuffer.update(cx, |multibuffer, cx| {
13727 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
13728 });
13729 _ = editor.update(cx, |editor, window, cx| {
13730 // Removing an excerpt causes the first selection to become degenerate.
13731 assert_eq!(
13732 editor.selections.ranges(cx),
13733 [
13734 Point::new(0, 0)..Point::new(0, 0),
13735 Point::new(0, 1)..Point::new(0, 1)
13736 ]
13737 );
13738
13739 // Refreshing selections will relocate the first selection to the original buffer
13740 // location.
13741 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
13742 assert_eq!(
13743 editor.selections.ranges(cx),
13744 [
13745 Point::new(0, 1)..Point::new(0, 1),
13746 Point::new(0, 3)..Point::new(0, 3)
13747 ]
13748 );
13749 assert!(editor.selections.pending_anchor().is_some());
13750 });
13751}
13752
13753#[gpui::test]
13754fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
13755 init_test(cx, |_| {});
13756
13757 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13758 let mut excerpt1_id = None;
13759 let multibuffer = cx.new(|cx| {
13760 let mut multibuffer = MultiBuffer::new(ReadWrite);
13761 excerpt1_id = multibuffer
13762 .push_excerpts(
13763 buffer.clone(),
13764 [
13765 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
13766 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
13767 ],
13768 cx,
13769 )
13770 .into_iter()
13771 .next();
13772 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
13773 multibuffer
13774 });
13775
13776 let editor = cx.add_window(|window, cx| {
13777 let mut editor = build_editor(multibuffer.clone(), window, cx);
13778 let snapshot = editor.snapshot(window, cx);
13779 editor.begin_selection(
13780 Point::new(1, 3).to_display_point(&snapshot),
13781 false,
13782 1,
13783 window,
13784 cx,
13785 );
13786 assert_eq!(
13787 editor.selections.ranges(cx),
13788 [Point::new(1, 3)..Point::new(1, 3)]
13789 );
13790 editor
13791 });
13792
13793 multibuffer.update(cx, |multibuffer, cx| {
13794 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
13795 });
13796 _ = editor.update(cx, |editor, window, cx| {
13797 assert_eq!(
13798 editor.selections.ranges(cx),
13799 [Point::new(0, 0)..Point::new(0, 0)]
13800 );
13801
13802 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
13803 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
13804 assert_eq!(
13805 editor.selections.ranges(cx),
13806 [Point::new(0, 3)..Point::new(0, 3)]
13807 );
13808 assert!(editor.selections.pending_anchor().is_some());
13809 });
13810}
13811
13812#[gpui::test]
13813async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
13814 init_test(cx, |_| {});
13815
13816 let language = Arc::new(
13817 Language::new(
13818 LanguageConfig {
13819 brackets: BracketPairConfig {
13820 pairs: vec![
13821 BracketPair {
13822 start: "{".to_string(),
13823 end: "}".to_string(),
13824 close: true,
13825 surround: true,
13826 newline: true,
13827 },
13828 BracketPair {
13829 start: "/* ".to_string(),
13830 end: " */".to_string(),
13831 close: true,
13832 surround: true,
13833 newline: true,
13834 },
13835 ],
13836 ..Default::default()
13837 },
13838 ..Default::default()
13839 },
13840 Some(tree_sitter_rust::LANGUAGE.into()),
13841 )
13842 .with_indents_query("")
13843 .unwrap(),
13844 );
13845
13846 let text = concat!(
13847 "{ }\n", //
13848 " x\n", //
13849 " /* */\n", //
13850 "x\n", //
13851 "{{} }\n", //
13852 );
13853
13854 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
13855 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13856 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
13857 editor
13858 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
13859 .await;
13860
13861 editor.update_in(cx, |editor, window, cx| {
13862 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13863 s.select_display_ranges([
13864 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
13865 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
13866 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
13867 ])
13868 });
13869 editor.newline(&Newline, window, cx);
13870
13871 assert_eq!(
13872 editor.buffer().read(cx).read(cx).text(),
13873 concat!(
13874 "{ \n", // Suppress rustfmt
13875 "\n", //
13876 "}\n", //
13877 " x\n", //
13878 " /* \n", //
13879 " \n", //
13880 " */\n", //
13881 "x\n", //
13882 "{{} \n", //
13883 "}\n", //
13884 )
13885 );
13886 });
13887}
13888
13889#[gpui::test]
13890fn test_highlighted_ranges(cx: &mut TestAppContext) {
13891 init_test(cx, |_| {});
13892
13893 let editor = cx.add_window(|window, cx| {
13894 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
13895 build_editor(buffer.clone(), window, cx)
13896 });
13897
13898 _ = editor.update(cx, |editor, window, cx| {
13899 struct Type1;
13900 struct Type2;
13901
13902 let buffer = editor.buffer.read(cx).snapshot(cx);
13903
13904 let anchor_range =
13905 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
13906
13907 editor.highlight_background::<Type1>(
13908 &[
13909 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
13910 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
13911 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
13912 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
13913 ],
13914 |_| Hsla::red(),
13915 cx,
13916 );
13917 editor.highlight_background::<Type2>(
13918 &[
13919 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
13920 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
13921 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
13922 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
13923 ],
13924 |_| Hsla::green(),
13925 cx,
13926 );
13927
13928 let snapshot = editor.snapshot(window, cx);
13929 let mut highlighted_ranges = editor.background_highlights_in_range(
13930 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
13931 &snapshot,
13932 cx.theme(),
13933 );
13934 // Enforce a consistent ordering based on color without relying on the ordering of the
13935 // highlight's `TypeId` which is non-executor.
13936 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
13937 assert_eq!(
13938 highlighted_ranges,
13939 &[
13940 (
13941 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
13942 Hsla::red(),
13943 ),
13944 (
13945 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
13946 Hsla::red(),
13947 ),
13948 (
13949 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
13950 Hsla::green(),
13951 ),
13952 (
13953 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
13954 Hsla::green(),
13955 ),
13956 ]
13957 );
13958 assert_eq!(
13959 editor.background_highlights_in_range(
13960 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
13961 &snapshot,
13962 cx.theme(),
13963 ),
13964 &[(
13965 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
13966 Hsla::red(),
13967 )]
13968 );
13969 });
13970}
13971
13972#[gpui::test]
13973async fn test_following(cx: &mut TestAppContext) {
13974 init_test(cx, |_| {});
13975
13976 let fs = FakeFs::new(cx.executor());
13977 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
13978
13979 let buffer = project.update(cx, |project, cx| {
13980 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
13981 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
13982 });
13983 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
13984 let follower = cx.update(|cx| {
13985 cx.open_window(
13986 WindowOptions {
13987 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
13988 gpui::Point::new(px(0.), px(0.)),
13989 gpui::Point::new(px(10.), px(80.)),
13990 ))),
13991 ..Default::default()
13992 },
13993 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
13994 )
13995 .unwrap()
13996 });
13997
13998 let is_still_following = Rc::new(RefCell::new(true));
13999 let follower_edit_event_count = Rc::new(RefCell::new(0));
14000 let pending_update = Rc::new(RefCell::new(None));
14001 let leader_entity = leader.root(cx).unwrap();
14002 let follower_entity = follower.root(cx).unwrap();
14003 _ = follower.update(cx, {
14004 let update = pending_update.clone();
14005 let is_still_following = is_still_following.clone();
14006 let follower_edit_event_count = follower_edit_event_count.clone();
14007 |_, window, cx| {
14008 cx.subscribe_in(
14009 &leader_entity,
14010 window,
14011 move |_, leader, event, window, cx| {
14012 leader.read(cx).add_event_to_update_proto(
14013 event,
14014 &mut update.borrow_mut(),
14015 window,
14016 cx,
14017 );
14018 },
14019 )
14020 .detach();
14021
14022 cx.subscribe_in(
14023 &follower_entity,
14024 window,
14025 move |_, _, event: &EditorEvent, _window, _cx| {
14026 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
14027 *is_still_following.borrow_mut() = false;
14028 }
14029
14030 if let EditorEvent::BufferEdited = event {
14031 *follower_edit_event_count.borrow_mut() += 1;
14032 }
14033 },
14034 )
14035 .detach();
14036 }
14037 });
14038
14039 // Update the selections only
14040 _ = leader.update(cx, |leader, window, cx| {
14041 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14042 s.select_ranges([1..1])
14043 });
14044 });
14045 follower
14046 .update(cx, |follower, window, cx| {
14047 follower.apply_update_proto(
14048 &project,
14049 pending_update.borrow_mut().take().unwrap(),
14050 window,
14051 cx,
14052 )
14053 })
14054 .unwrap()
14055 .await
14056 .unwrap();
14057 _ = follower.update(cx, |follower, _, cx| {
14058 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
14059 });
14060 assert!(*is_still_following.borrow());
14061 assert_eq!(*follower_edit_event_count.borrow(), 0);
14062
14063 // Update the scroll position only
14064 _ = leader.update(cx, |leader, window, cx| {
14065 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14066 });
14067 follower
14068 .update(cx, |follower, window, cx| {
14069 follower.apply_update_proto(
14070 &project,
14071 pending_update.borrow_mut().take().unwrap(),
14072 window,
14073 cx,
14074 )
14075 })
14076 .unwrap()
14077 .await
14078 .unwrap();
14079 assert_eq!(
14080 follower
14081 .update(cx, |follower, _, cx| follower.scroll_position(cx))
14082 .unwrap(),
14083 gpui::Point::new(1.5, 3.5)
14084 );
14085 assert!(*is_still_following.borrow());
14086 assert_eq!(*follower_edit_event_count.borrow(), 0);
14087
14088 // Update the selections and scroll position. The follower's scroll position is updated
14089 // via autoscroll, not via the leader's exact scroll position.
14090 _ = leader.update(cx, |leader, window, cx| {
14091 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14092 s.select_ranges([0..0])
14093 });
14094 leader.request_autoscroll(Autoscroll::newest(), cx);
14095 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14096 });
14097 follower
14098 .update(cx, |follower, window, cx| {
14099 follower.apply_update_proto(
14100 &project,
14101 pending_update.borrow_mut().take().unwrap(),
14102 window,
14103 cx,
14104 )
14105 })
14106 .unwrap()
14107 .await
14108 .unwrap();
14109 _ = follower.update(cx, |follower, _, cx| {
14110 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
14111 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
14112 });
14113 assert!(*is_still_following.borrow());
14114
14115 // Creating a pending selection that precedes another selection
14116 _ = leader.update(cx, |leader, window, cx| {
14117 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14118 s.select_ranges([1..1])
14119 });
14120 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
14121 });
14122 follower
14123 .update(cx, |follower, window, cx| {
14124 follower.apply_update_proto(
14125 &project,
14126 pending_update.borrow_mut().take().unwrap(),
14127 window,
14128 cx,
14129 )
14130 })
14131 .unwrap()
14132 .await
14133 .unwrap();
14134 _ = follower.update(cx, |follower, _, cx| {
14135 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
14136 });
14137 assert!(*is_still_following.borrow());
14138
14139 // Extend the pending selection so that it surrounds another selection
14140 _ = leader.update(cx, |leader, window, cx| {
14141 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
14142 });
14143 follower
14144 .update(cx, |follower, window, cx| {
14145 follower.apply_update_proto(
14146 &project,
14147 pending_update.borrow_mut().take().unwrap(),
14148 window,
14149 cx,
14150 )
14151 })
14152 .unwrap()
14153 .await
14154 .unwrap();
14155 _ = follower.update(cx, |follower, _, cx| {
14156 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
14157 });
14158
14159 // Scrolling locally breaks the follow
14160 _ = follower.update(cx, |follower, window, cx| {
14161 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
14162 follower.set_scroll_anchor(
14163 ScrollAnchor {
14164 anchor: top_anchor,
14165 offset: gpui::Point::new(0.0, 0.5),
14166 },
14167 window,
14168 cx,
14169 );
14170 });
14171 assert!(!(*is_still_following.borrow()));
14172}
14173
14174#[gpui::test]
14175async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
14176 init_test(cx, |_| {});
14177
14178 let fs = FakeFs::new(cx.executor());
14179 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14180 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14181 let pane = workspace
14182 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14183 .unwrap();
14184
14185 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14186
14187 let leader = pane.update_in(cx, |_, window, cx| {
14188 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
14189 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
14190 });
14191
14192 // Start following the editor when it has no excerpts.
14193 let mut state_message =
14194 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14195 let workspace_entity = workspace.root(cx).unwrap();
14196 let follower_1 = cx
14197 .update_window(*workspace.deref(), |_, window, cx| {
14198 Editor::from_state_proto(
14199 workspace_entity,
14200 ViewId {
14201 creator: CollaboratorId::PeerId(PeerId::default()),
14202 id: 0,
14203 },
14204 &mut state_message,
14205 window,
14206 cx,
14207 )
14208 })
14209 .unwrap()
14210 .unwrap()
14211 .await
14212 .unwrap();
14213
14214 let update_message = Rc::new(RefCell::new(None));
14215 follower_1.update_in(cx, {
14216 let update = update_message.clone();
14217 |_, window, cx| {
14218 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
14219 leader.read(cx).add_event_to_update_proto(
14220 event,
14221 &mut update.borrow_mut(),
14222 window,
14223 cx,
14224 );
14225 })
14226 .detach();
14227 }
14228 });
14229
14230 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
14231 (
14232 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
14233 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
14234 )
14235 });
14236
14237 // Insert some excerpts.
14238 leader.update(cx, |leader, cx| {
14239 leader.buffer.update(cx, |multibuffer, cx| {
14240 multibuffer.set_excerpts_for_path(
14241 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
14242 buffer_1.clone(),
14243 vec![
14244 Point::row_range(0..3),
14245 Point::row_range(1..6),
14246 Point::row_range(12..15),
14247 ],
14248 0,
14249 cx,
14250 );
14251 multibuffer.set_excerpts_for_path(
14252 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
14253 buffer_2.clone(),
14254 vec![Point::row_range(0..6), Point::row_range(8..12)],
14255 0,
14256 cx,
14257 );
14258 });
14259 });
14260
14261 // Apply the update of adding the excerpts.
14262 follower_1
14263 .update_in(cx, |follower, window, cx| {
14264 follower.apply_update_proto(
14265 &project,
14266 update_message.borrow().clone().unwrap(),
14267 window,
14268 cx,
14269 )
14270 })
14271 .await
14272 .unwrap();
14273 assert_eq!(
14274 follower_1.update(cx, |editor, cx| editor.text(cx)),
14275 leader.update(cx, |editor, cx| editor.text(cx))
14276 );
14277 update_message.borrow_mut().take();
14278
14279 // Start following separately after it already has excerpts.
14280 let mut state_message =
14281 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14282 let workspace_entity = workspace.root(cx).unwrap();
14283 let follower_2 = cx
14284 .update_window(*workspace.deref(), |_, window, cx| {
14285 Editor::from_state_proto(
14286 workspace_entity,
14287 ViewId {
14288 creator: CollaboratorId::PeerId(PeerId::default()),
14289 id: 0,
14290 },
14291 &mut state_message,
14292 window,
14293 cx,
14294 )
14295 })
14296 .unwrap()
14297 .unwrap()
14298 .await
14299 .unwrap();
14300 assert_eq!(
14301 follower_2.update(cx, |editor, cx| editor.text(cx)),
14302 leader.update(cx, |editor, cx| editor.text(cx))
14303 );
14304
14305 // Remove some excerpts.
14306 leader.update(cx, |leader, cx| {
14307 leader.buffer.update(cx, |multibuffer, cx| {
14308 let excerpt_ids = multibuffer.excerpt_ids();
14309 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
14310 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
14311 });
14312 });
14313
14314 // Apply the update of removing the excerpts.
14315 follower_1
14316 .update_in(cx, |follower, window, cx| {
14317 follower.apply_update_proto(
14318 &project,
14319 update_message.borrow().clone().unwrap(),
14320 window,
14321 cx,
14322 )
14323 })
14324 .await
14325 .unwrap();
14326 follower_2
14327 .update_in(cx, |follower, window, cx| {
14328 follower.apply_update_proto(
14329 &project,
14330 update_message.borrow().clone().unwrap(),
14331 window,
14332 cx,
14333 )
14334 })
14335 .await
14336 .unwrap();
14337 update_message.borrow_mut().take();
14338 assert_eq!(
14339 follower_1.update(cx, |editor, cx| editor.text(cx)),
14340 leader.update(cx, |editor, cx| editor.text(cx))
14341 );
14342}
14343
14344#[gpui::test]
14345async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14346 init_test(cx, |_| {});
14347
14348 let mut cx = EditorTestContext::new(cx).await;
14349 let lsp_store =
14350 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
14351
14352 cx.set_state(indoc! {"
14353 ˇfn func(abc def: i32) -> u32 {
14354 }
14355 "});
14356
14357 cx.update(|_, cx| {
14358 lsp_store.update(cx, |lsp_store, cx| {
14359 lsp_store
14360 .update_diagnostics(
14361 LanguageServerId(0),
14362 lsp::PublishDiagnosticsParams {
14363 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
14364 version: None,
14365 diagnostics: vec![
14366 lsp::Diagnostic {
14367 range: lsp::Range::new(
14368 lsp::Position::new(0, 11),
14369 lsp::Position::new(0, 12),
14370 ),
14371 severity: Some(lsp::DiagnosticSeverity::ERROR),
14372 ..Default::default()
14373 },
14374 lsp::Diagnostic {
14375 range: lsp::Range::new(
14376 lsp::Position::new(0, 12),
14377 lsp::Position::new(0, 15),
14378 ),
14379 severity: Some(lsp::DiagnosticSeverity::ERROR),
14380 ..Default::default()
14381 },
14382 lsp::Diagnostic {
14383 range: lsp::Range::new(
14384 lsp::Position::new(0, 25),
14385 lsp::Position::new(0, 28),
14386 ),
14387 severity: Some(lsp::DiagnosticSeverity::ERROR),
14388 ..Default::default()
14389 },
14390 ],
14391 },
14392 None,
14393 DiagnosticSourceKind::Pushed,
14394 &[],
14395 cx,
14396 )
14397 .unwrap()
14398 });
14399 });
14400
14401 executor.run_until_parked();
14402
14403 cx.update_editor(|editor, window, cx| {
14404 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14405 });
14406
14407 cx.assert_editor_state(indoc! {"
14408 fn func(abc def: i32) -> ˇu32 {
14409 }
14410 "});
14411
14412 cx.update_editor(|editor, window, cx| {
14413 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14414 });
14415
14416 cx.assert_editor_state(indoc! {"
14417 fn func(abc ˇdef: i32) -> u32 {
14418 }
14419 "});
14420
14421 cx.update_editor(|editor, window, cx| {
14422 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14423 });
14424
14425 cx.assert_editor_state(indoc! {"
14426 fn func(abcˇ def: i32) -> u32 {
14427 }
14428 "});
14429
14430 cx.update_editor(|editor, window, cx| {
14431 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14432 });
14433
14434 cx.assert_editor_state(indoc! {"
14435 fn func(abc def: i32) -> ˇu32 {
14436 }
14437 "});
14438}
14439
14440#[gpui::test]
14441async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14442 init_test(cx, |_| {});
14443
14444 let mut cx = EditorTestContext::new(cx).await;
14445
14446 let diff_base = r#"
14447 use some::mod;
14448
14449 const A: u32 = 42;
14450
14451 fn main() {
14452 println!("hello");
14453
14454 println!("world");
14455 }
14456 "#
14457 .unindent();
14458
14459 // Edits are modified, removed, modified, added
14460 cx.set_state(
14461 &r#"
14462 use some::modified;
14463
14464 ˇ
14465 fn main() {
14466 println!("hello there");
14467
14468 println!("around the");
14469 println!("world");
14470 }
14471 "#
14472 .unindent(),
14473 );
14474
14475 cx.set_head_text(&diff_base);
14476 executor.run_until_parked();
14477
14478 cx.update_editor(|editor, window, cx| {
14479 //Wrap around the bottom of the buffer
14480 for _ in 0..3 {
14481 editor.go_to_next_hunk(&GoToHunk, window, cx);
14482 }
14483 });
14484
14485 cx.assert_editor_state(
14486 &r#"
14487 ˇuse some::modified;
14488
14489
14490 fn main() {
14491 println!("hello there");
14492
14493 println!("around the");
14494 println!("world");
14495 }
14496 "#
14497 .unindent(),
14498 );
14499
14500 cx.update_editor(|editor, window, cx| {
14501 //Wrap around the top of the buffer
14502 for _ in 0..2 {
14503 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14504 }
14505 });
14506
14507 cx.assert_editor_state(
14508 &r#"
14509 use some::modified;
14510
14511
14512 fn main() {
14513 ˇ println!("hello there");
14514
14515 println!("around the");
14516 println!("world");
14517 }
14518 "#
14519 .unindent(),
14520 );
14521
14522 cx.update_editor(|editor, window, cx| {
14523 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14524 });
14525
14526 cx.assert_editor_state(
14527 &r#"
14528 use some::modified;
14529
14530 ˇ
14531 fn main() {
14532 println!("hello there");
14533
14534 println!("around the");
14535 println!("world");
14536 }
14537 "#
14538 .unindent(),
14539 );
14540
14541 cx.update_editor(|editor, window, cx| {
14542 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14543 });
14544
14545 cx.assert_editor_state(
14546 &r#"
14547 ˇuse some::modified;
14548
14549
14550 fn main() {
14551 println!("hello there");
14552
14553 println!("around the");
14554 println!("world");
14555 }
14556 "#
14557 .unindent(),
14558 );
14559
14560 cx.update_editor(|editor, window, cx| {
14561 for _ in 0..2 {
14562 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14563 }
14564 });
14565
14566 cx.assert_editor_state(
14567 &r#"
14568 use some::modified;
14569
14570
14571 fn main() {
14572 ˇ println!("hello there");
14573
14574 println!("around the");
14575 println!("world");
14576 }
14577 "#
14578 .unindent(),
14579 );
14580
14581 cx.update_editor(|editor, window, cx| {
14582 editor.fold(&Fold, window, cx);
14583 });
14584
14585 cx.update_editor(|editor, window, cx| {
14586 editor.go_to_next_hunk(&GoToHunk, window, cx);
14587 });
14588
14589 cx.assert_editor_state(
14590 &r#"
14591 ˇuse some::modified;
14592
14593
14594 fn main() {
14595 println!("hello there");
14596
14597 println!("around the");
14598 println!("world");
14599 }
14600 "#
14601 .unindent(),
14602 );
14603}
14604
14605#[test]
14606fn test_split_words() {
14607 fn split(text: &str) -> Vec<&str> {
14608 split_words(text).collect()
14609 }
14610
14611 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
14612 assert_eq!(split("hello_world"), &["hello_", "world"]);
14613 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
14614 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
14615 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
14616 assert_eq!(split("helloworld"), &["helloworld"]);
14617
14618 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
14619}
14620
14621#[gpui::test]
14622async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
14623 init_test(cx, |_| {});
14624
14625 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
14626 let mut assert = |before, after| {
14627 let _state_context = cx.set_state(before);
14628 cx.run_until_parked();
14629 cx.update_editor(|editor, window, cx| {
14630 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
14631 });
14632 cx.run_until_parked();
14633 cx.assert_editor_state(after);
14634 };
14635
14636 // Outside bracket jumps to outside of matching bracket
14637 assert("console.logˇ(var);", "console.log(var)ˇ;");
14638 assert("console.log(var)ˇ;", "console.logˇ(var);");
14639
14640 // Inside bracket jumps to inside of matching bracket
14641 assert("console.log(ˇvar);", "console.log(varˇ);");
14642 assert("console.log(varˇ);", "console.log(ˇvar);");
14643
14644 // When outside a bracket and inside, favor jumping to the inside bracket
14645 assert(
14646 "console.log('foo', [1, 2, 3]ˇ);",
14647 "console.log(ˇ'foo', [1, 2, 3]);",
14648 );
14649 assert(
14650 "console.log(ˇ'foo', [1, 2, 3]);",
14651 "console.log('foo', [1, 2, 3]ˇ);",
14652 );
14653
14654 // Bias forward if two options are equally likely
14655 assert(
14656 "let result = curried_fun()ˇ();",
14657 "let result = curried_fun()()ˇ;",
14658 );
14659
14660 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
14661 assert(
14662 indoc! {"
14663 function test() {
14664 console.log('test')ˇ
14665 }"},
14666 indoc! {"
14667 function test() {
14668 console.logˇ('test')
14669 }"},
14670 );
14671}
14672
14673#[gpui::test]
14674async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
14675 init_test(cx, |_| {});
14676
14677 let fs = FakeFs::new(cx.executor());
14678 fs.insert_tree(
14679 path!("/a"),
14680 json!({
14681 "main.rs": "fn main() { let a = 5; }",
14682 "other.rs": "// Test file",
14683 }),
14684 )
14685 .await;
14686 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14687
14688 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14689 language_registry.add(Arc::new(Language::new(
14690 LanguageConfig {
14691 name: "Rust".into(),
14692 matcher: LanguageMatcher {
14693 path_suffixes: vec!["rs".to_string()],
14694 ..Default::default()
14695 },
14696 brackets: BracketPairConfig {
14697 pairs: vec![BracketPair {
14698 start: "{".to_string(),
14699 end: "}".to_string(),
14700 close: true,
14701 surround: true,
14702 newline: true,
14703 }],
14704 disabled_scopes_by_bracket_ix: Vec::new(),
14705 },
14706 ..Default::default()
14707 },
14708 Some(tree_sitter_rust::LANGUAGE.into()),
14709 )));
14710 let mut fake_servers = language_registry.register_fake_lsp(
14711 "Rust",
14712 FakeLspAdapter {
14713 capabilities: lsp::ServerCapabilities {
14714 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
14715 first_trigger_character: "{".to_string(),
14716 more_trigger_character: None,
14717 }),
14718 ..Default::default()
14719 },
14720 ..Default::default()
14721 },
14722 );
14723
14724 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14725
14726 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14727
14728 let worktree_id = workspace
14729 .update(cx, |workspace, _, cx| {
14730 workspace.project().update(cx, |project, cx| {
14731 project.worktrees(cx).next().unwrap().read(cx).id()
14732 })
14733 })
14734 .unwrap();
14735
14736 let buffer = project
14737 .update(cx, |project, cx| {
14738 project.open_local_buffer(path!("/a/main.rs"), cx)
14739 })
14740 .await
14741 .unwrap();
14742 let editor_handle = workspace
14743 .update(cx, |workspace, window, cx| {
14744 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
14745 })
14746 .unwrap()
14747 .await
14748 .unwrap()
14749 .downcast::<Editor>()
14750 .unwrap();
14751
14752 cx.executor().start_waiting();
14753 let fake_server = fake_servers.next().await.unwrap();
14754
14755 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
14756 |params, _| async move {
14757 assert_eq!(
14758 params.text_document_position.text_document.uri,
14759 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
14760 );
14761 assert_eq!(
14762 params.text_document_position.position,
14763 lsp::Position::new(0, 21),
14764 );
14765
14766 Ok(Some(vec![lsp::TextEdit {
14767 new_text: "]".to_string(),
14768 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14769 }]))
14770 },
14771 );
14772
14773 editor_handle.update_in(cx, |editor, window, cx| {
14774 window.focus(&editor.focus_handle(cx));
14775 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14776 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
14777 });
14778 editor.handle_input("{", window, cx);
14779 });
14780
14781 cx.executor().run_until_parked();
14782
14783 buffer.update(cx, |buffer, _| {
14784 assert_eq!(
14785 buffer.text(),
14786 "fn main() { let a = {5}; }",
14787 "No extra braces from on type formatting should appear in the buffer"
14788 )
14789 });
14790}
14791
14792#[gpui::test(iterations = 20, seeds(31))]
14793async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
14794 init_test(cx, |_| {});
14795
14796 let mut cx = EditorLspTestContext::new_rust(
14797 lsp::ServerCapabilities {
14798 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
14799 first_trigger_character: ".".to_string(),
14800 more_trigger_character: None,
14801 }),
14802 ..Default::default()
14803 },
14804 cx,
14805 )
14806 .await;
14807
14808 cx.update_buffer(|buffer, _| {
14809 // This causes autoindent to be async.
14810 buffer.set_sync_parse_timeout(Duration::ZERO)
14811 });
14812
14813 cx.set_state("fn c() {\n d()ˇ\n}\n");
14814 cx.simulate_keystroke("\n");
14815 cx.run_until_parked();
14816
14817 let buffer_cloned =
14818 cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap().clone());
14819 let mut request =
14820 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
14821 let buffer_cloned = buffer_cloned.clone();
14822 async move {
14823 buffer_cloned.update(&mut cx, |buffer, _| {
14824 assert_eq!(
14825 buffer.text(),
14826 "fn c() {\n d()\n .\n}\n",
14827 "OnTypeFormatting should triggered after autoindent applied"
14828 )
14829 })?;
14830
14831 Ok(Some(vec![]))
14832 }
14833 });
14834
14835 cx.simulate_keystroke(".");
14836 cx.run_until_parked();
14837
14838 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
14839 assert!(request.next().await.is_some());
14840 request.close();
14841 assert!(request.next().await.is_none());
14842}
14843
14844#[gpui::test]
14845async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
14846 init_test(cx, |_| {});
14847
14848 let fs = FakeFs::new(cx.executor());
14849 fs.insert_tree(
14850 path!("/a"),
14851 json!({
14852 "main.rs": "fn main() { let a = 5; }",
14853 "other.rs": "// Test file",
14854 }),
14855 )
14856 .await;
14857
14858 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14859
14860 let server_restarts = Arc::new(AtomicUsize::new(0));
14861 let closure_restarts = Arc::clone(&server_restarts);
14862 let language_server_name = "test language server";
14863 let language_name: LanguageName = "Rust".into();
14864
14865 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14866 language_registry.add(Arc::new(Language::new(
14867 LanguageConfig {
14868 name: language_name.clone(),
14869 matcher: LanguageMatcher {
14870 path_suffixes: vec!["rs".to_string()],
14871 ..Default::default()
14872 },
14873 ..Default::default()
14874 },
14875 Some(tree_sitter_rust::LANGUAGE.into()),
14876 )));
14877 let mut fake_servers = language_registry.register_fake_lsp(
14878 "Rust",
14879 FakeLspAdapter {
14880 name: language_server_name,
14881 initialization_options: Some(json!({
14882 "testOptionValue": true
14883 })),
14884 initializer: Some(Box::new(move |fake_server| {
14885 let task_restarts = Arc::clone(&closure_restarts);
14886 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
14887 task_restarts.fetch_add(1, atomic::Ordering::Release);
14888 futures::future::ready(Ok(()))
14889 });
14890 })),
14891 ..Default::default()
14892 },
14893 );
14894
14895 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14896 let _buffer = project
14897 .update(cx, |project, cx| {
14898 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
14899 })
14900 .await
14901 .unwrap();
14902 let _fake_server = fake_servers.next().await.unwrap();
14903 update_test_language_settings(cx, |language_settings| {
14904 language_settings.languages.0.insert(
14905 language_name.clone(),
14906 LanguageSettingsContent {
14907 tab_size: NonZeroU32::new(8),
14908 ..Default::default()
14909 },
14910 );
14911 });
14912 cx.executor().run_until_parked();
14913 assert_eq!(
14914 server_restarts.load(atomic::Ordering::Acquire),
14915 0,
14916 "Should not restart LSP server on an unrelated change"
14917 );
14918
14919 update_test_project_settings(cx, |project_settings| {
14920 project_settings.lsp.insert(
14921 "Some other server name".into(),
14922 LspSettings {
14923 binary: None,
14924 settings: None,
14925 initialization_options: Some(json!({
14926 "some other init value": false
14927 })),
14928 enable_lsp_tasks: false,
14929 },
14930 );
14931 });
14932 cx.executor().run_until_parked();
14933 assert_eq!(
14934 server_restarts.load(atomic::Ordering::Acquire),
14935 0,
14936 "Should not restart LSP server on an unrelated LSP settings change"
14937 );
14938
14939 update_test_project_settings(cx, |project_settings| {
14940 project_settings.lsp.insert(
14941 language_server_name.into(),
14942 LspSettings {
14943 binary: None,
14944 settings: None,
14945 initialization_options: Some(json!({
14946 "anotherInitValue": false
14947 })),
14948 enable_lsp_tasks: false,
14949 },
14950 );
14951 });
14952 cx.executor().run_until_parked();
14953 assert_eq!(
14954 server_restarts.load(atomic::Ordering::Acquire),
14955 1,
14956 "Should restart LSP server on a related LSP settings change"
14957 );
14958
14959 update_test_project_settings(cx, |project_settings| {
14960 project_settings.lsp.insert(
14961 language_server_name.into(),
14962 LspSettings {
14963 binary: None,
14964 settings: None,
14965 initialization_options: Some(json!({
14966 "anotherInitValue": false
14967 })),
14968 enable_lsp_tasks: false,
14969 },
14970 );
14971 });
14972 cx.executor().run_until_parked();
14973 assert_eq!(
14974 server_restarts.load(atomic::Ordering::Acquire),
14975 1,
14976 "Should not restart LSP server on a related LSP settings change that is the same"
14977 );
14978
14979 update_test_project_settings(cx, |project_settings| {
14980 project_settings.lsp.insert(
14981 language_server_name.into(),
14982 LspSettings {
14983 binary: None,
14984 settings: None,
14985 initialization_options: None,
14986 enable_lsp_tasks: false,
14987 },
14988 );
14989 });
14990 cx.executor().run_until_parked();
14991 assert_eq!(
14992 server_restarts.load(atomic::Ordering::Acquire),
14993 2,
14994 "Should restart LSP server on another related LSP settings change"
14995 );
14996}
14997
14998#[gpui::test]
14999async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
15000 init_test(cx, |_| {});
15001
15002 let mut cx = EditorLspTestContext::new_rust(
15003 lsp::ServerCapabilities {
15004 completion_provider: Some(lsp::CompletionOptions {
15005 trigger_characters: Some(vec![".".to_string()]),
15006 resolve_provider: Some(true),
15007 ..Default::default()
15008 }),
15009 ..Default::default()
15010 },
15011 cx,
15012 )
15013 .await;
15014
15015 cx.set_state("fn main() { let a = 2ˇ; }");
15016 cx.simulate_keystroke(".");
15017 let completion_item = lsp::CompletionItem {
15018 label: "some".into(),
15019 kind: Some(lsp::CompletionItemKind::SNIPPET),
15020 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15021 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15022 kind: lsp::MarkupKind::Markdown,
15023 value: "```rust\nSome(2)\n```".to_string(),
15024 })),
15025 deprecated: Some(false),
15026 sort_text: Some("fffffff2".to_string()),
15027 filter_text: Some("some".to_string()),
15028 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15029 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15030 range: lsp::Range {
15031 start: lsp::Position {
15032 line: 0,
15033 character: 22,
15034 },
15035 end: lsp::Position {
15036 line: 0,
15037 character: 22,
15038 },
15039 },
15040 new_text: "Some(2)".to_string(),
15041 })),
15042 additional_text_edits: Some(vec![lsp::TextEdit {
15043 range: lsp::Range {
15044 start: lsp::Position {
15045 line: 0,
15046 character: 20,
15047 },
15048 end: lsp::Position {
15049 line: 0,
15050 character: 22,
15051 },
15052 },
15053 new_text: "".to_string(),
15054 }]),
15055 ..Default::default()
15056 };
15057
15058 let closure_completion_item = completion_item.clone();
15059 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15060 let task_completion_item = closure_completion_item.clone();
15061 async move {
15062 Ok(Some(lsp::CompletionResponse::Array(vec![
15063 task_completion_item,
15064 ])))
15065 }
15066 });
15067
15068 request.next().await;
15069
15070 cx.condition(|editor, _| editor.context_menu_visible())
15071 .await;
15072 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15073 editor
15074 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15075 .unwrap()
15076 });
15077 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
15078
15079 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
15080 let task_completion_item = completion_item.clone();
15081 async move { Ok(task_completion_item) }
15082 })
15083 .next()
15084 .await
15085 .unwrap();
15086 apply_additional_edits.await.unwrap();
15087 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
15088}
15089
15090#[gpui::test]
15091async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
15092 init_test(cx, |_| {});
15093
15094 let mut cx = EditorLspTestContext::new_rust(
15095 lsp::ServerCapabilities {
15096 completion_provider: Some(lsp::CompletionOptions {
15097 trigger_characters: Some(vec![".".to_string()]),
15098 resolve_provider: Some(true),
15099 ..Default::default()
15100 }),
15101 ..Default::default()
15102 },
15103 cx,
15104 )
15105 .await;
15106
15107 cx.set_state("fn main() { let a = 2ˇ; }");
15108 cx.simulate_keystroke(".");
15109
15110 let item1 = lsp::CompletionItem {
15111 label: "method id()".to_string(),
15112 filter_text: Some("id".to_string()),
15113 detail: None,
15114 documentation: None,
15115 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15116 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15117 new_text: ".id".to_string(),
15118 })),
15119 ..lsp::CompletionItem::default()
15120 };
15121
15122 let item2 = lsp::CompletionItem {
15123 label: "other".to_string(),
15124 filter_text: Some("other".to_string()),
15125 detail: None,
15126 documentation: None,
15127 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15128 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15129 new_text: ".other".to_string(),
15130 })),
15131 ..lsp::CompletionItem::default()
15132 };
15133
15134 let item1 = item1.clone();
15135 cx.set_request_handler::<lsp::request::Completion, _, _>({
15136 let item1 = item1.clone();
15137 move |_, _, _| {
15138 let item1 = item1.clone();
15139 let item2 = item2.clone();
15140 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
15141 }
15142 })
15143 .next()
15144 .await;
15145
15146 cx.condition(|editor, _| editor.context_menu_visible())
15147 .await;
15148 cx.update_editor(|editor, _, _| {
15149 let context_menu = editor.context_menu.borrow_mut();
15150 let context_menu = context_menu
15151 .as_ref()
15152 .expect("Should have the context menu deployed");
15153 match context_menu {
15154 CodeContextMenu::Completions(completions_menu) => {
15155 let completions = completions_menu.completions.borrow_mut();
15156 assert_eq!(
15157 completions
15158 .iter()
15159 .map(|completion| &completion.label.text)
15160 .collect::<Vec<_>>(),
15161 vec!["method id()", "other"]
15162 )
15163 }
15164 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15165 }
15166 });
15167
15168 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
15169 let item1 = item1.clone();
15170 move |_, item_to_resolve, _| {
15171 let item1 = item1.clone();
15172 async move {
15173 if item1 == item_to_resolve {
15174 Ok(lsp::CompletionItem {
15175 label: "method id()".to_string(),
15176 filter_text: Some("id".to_string()),
15177 detail: Some("Now resolved!".to_string()),
15178 documentation: Some(lsp::Documentation::String("Docs".to_string())),
15179 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15180 range: lsp::Range::new(
15181 lsp::Position::new(0, 22),
15182 lsp::Position::new(0, 22),
15183 ),
15184 new_text: ".id".to_string(),
15185 })),
15186 ..lsp::CompletionItem::default()
15187 })
15188 } else {
15189 Ok(item_to_resolve)
15190 }
15191 }
15192 }
15193 })
15194 .next()
15195 .await
15196 .unwrap();
15197 cx.run_until_parked();
15198
15199 cx.update_editor(|editor, window, cx| {
15200 editor.context_menu_next(&Default::default(), window, cx);
15201 });
15202
15203 cx.update_editor(|editor, _, _| {
15204 let context_menu = editor.context_menu.borrow_mut();
15205 let context_menu = context_menu
15206 .as_ref()
15207 .expect("Should have the context menu deployed");
15208 match context_menu {
15209 CodeContextMenu::Completions(completions_menu) => {
15210 let completions = completions_menu.completions.borrow_mut();
15211 assert_eq!(
15212 completions
15213 .iter()
15214 .map(|completion| &completion.label.text)
15215 .collect::<Vec<_>>(),
15216 vec!["method id() Now resolved!", "other"],
15217 "Should update first completion label, but not second as the filter text did not match."
15218 );
15219 }
15220 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15221 }
15222 });
15223}
15224
15225#[gpui::test]
15226async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
15227 init_test(cx, |_| {});
15228 let mut cx = EditorLspTestContext::new_rust(
15229 lsp::ServerCapabilities {
15230 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
15231 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
15232 completion_provider: Some(lsp::CompletionOptions {
15233 resolve_provider: Some(true),
15234 ..Default::default()
15235 }),
15236 ..Default::default()
15237 },
15238 cx,
15239 )
15240 .await;
15241 cx.set_state(indoc! {"
15242 struct TestStruct {
15243 field: i32
15244 }
15245
15246 fn mainˇ() {
15247 let unused_var = 42;
15248 let test_struct = TestStruct { field: 42 };
15249 }
15250 "});
15251 let symbol_range = cx.lsp_range(indoc! {"
15252 struct TestStruct {
15253 field: i32
15254 }
15255
15256 «fn main»() {
15257 let unused_var = 42;
15258 let test_struct = TestStruct { field: 42 };
15259 }
15260 "});
15261 let mut hover_requests =
15262 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
15263 Ok(Some(lsp::Hover {
15264 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
15265 kind: lsp::MarkupKind::Markdown,
15266 value: "Function documentation".to_string(),
15267 }),
15268 range: Some(symbol_range),
15269 }))
15270 });
15271
15272 // Case 1: Test that code action menu hide hover popover
15273 cx.dispatch_action(Hover);
15274 hover_requests.next().await;
15275 cx.condition(|editor, _| editor.hover_state.visible()).await;
15276 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
15277 move |_, _, _| async move {
15278 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
15279 lsp::CodeAction {
15280 title: "Remove unused variable".to_string(),
15281 kind: Some(CodeActionKind::QUICKFIX),
15282 edit: Some(lsp::WorkspaceEdit {
15283 changes: Some(
15284 [(
15285 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
15286 vec![lsp::TextEdit {
15287 range: lsp::Range::new(
15288 lsp::Position::new(5, 4),
15289 lsp::Position::new(5, 27),
15290 ),
15291 new_text: "".to_string(),
15292 }],
15293 )]
15294 .into_iter()
15295 .collect(),
15296 ),
15297 ..Default::default()
15298 }),
15299 ..Default::default()
15300 },
15301 )]))
15302 },
15303 );
15304 cx.update_editor(|editor, window, cx| {
15305 editor.toggle_code_actions(
15306 &ToggleCodeActions {
15307 deployed_from: None,
15308 quick_launch: false,
15309 },
15310 window,
15311 cx,
15312 );
15313 });
15314 code_action_requests.next().await;
15315 cx.run_until_parked();
15316 cx.condition(|editor, _| editor.context_menu_visible())
15317 .await;
15318 cx.update_editor(|editor, _, _| {
15319 assert!(
15320 !editor.hover_state.visible(),
15321 "Hover popover should be hidden when code action menu is shown"
15322 );
15323 // Hide code actions
15324 editor.context_menu.take();
15325 });
15326
15327 // Case 2: Test that code completions hide hover popover
15328 cx.dispatch_action(Hover);
15329 hover_requests.next().await;
15330 cx.condition(|editor, _| editor.hover_state.visible()).await;
15331 let counter = Arc::new(AtomicUsize::new(0));
15332 let mut completion_requests =
15333 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15334 let counter = counter.clone();
15335 async move {
15336 counter.fetch_add(1, atomic::Ordering::Release);
15337 Ok(Some(lsp::CompletionResponse::Array(vec![
15338 lsp::CompletionItem {
15339 label: "main".into(),
15340 kind: Some(lsp::CompletionItemKind::FUNCTION),
15341 detail: Some("() -> ()".to_string()),
15342 ..Default::default()
15343 },
15344 lsp::CompletionItem {
15345 label: "TestStruct".into(),
15346 kind: Some(lsp::CompletionItemKind::STRUCT),
15347 detail: Some("struct TestStruct".to_string()),
15348 ..Default::default()
15349 },
15350 ])))
15351 }
15352 });
15353 cx.update_editor(|editor, window, cx| {
15354 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15355 });
15356 completion_requests.next().await;
15357 cx.condition(|editor, _| editor.context_menu_visible())
15358 .await;
15359 cx.update_editor(|editor, _, _| {
15360 assert!(
15361 !editor.hover_state.visible(),
15362 "Hover popover should be hidden when completion menu is shown"
15363 );
15364 });
15365}
15366
15367#[gpui::test]
15368async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
15369 init_test(cx, |_| {});
15370
15371 let mut cx = EditorLspTestContext::new_rust(
15372 lsp::ServerCapabilities {
15373 completion_provider: Some(lsp::CompletionOptions {
15374 trigger_characters: Some(vec![".".to_string()]),
15375 resolve_provider: Some(true),
15376 ..Default::default()
15377 }),
15378 ..Default::default()
15379 },
15380 cx,
15381 )
15382 .await;
15383
15384 cx.set_state("fn main() { let a = 2ˇ; }");
15385 cx.simulate_keystroke(".");
15386
15387 let unresolved_item_1 = lsp::CompletionItem {
15388 label: "id".to_string(),
15389 filter_text: Some("id".to_string()),
15390 detail: None,
15391 documentation: None,
15392 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15393 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15394 new_text: ".id".to_string(),
15395 })),
15396 ..lsp::CompletionItem::default()
15397 };
15398 let resolved_item_1 = lsp::CompletionItem {
15399 additional_text_edits: Some(vec![lsp::TextEdit {
15400 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15401 new_text: "!!".to_string(),
15402 }]),
15403 ..unresolved_item_1.clone()
15404 };
15405 let unresolved_item_2 = lsp::CompletionItem {
15406 label: "other".to_string(),
15407 filter_text: Some("other".to_string()),
15408 detail: None,
15409 documentation: None,
15410 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15411 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15412 new_text: ".other".to_string(),
15413 })),
15414 ..lsp::CompletionItem::default()
15415 };
15416 let resolved_item_2 = lsp::CompletionItem {
15417 additional_text_edits: Some(vec![lsp::TextEdit {
15418 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15419 new_text: "??".to_string(),
15420 }]),
15421 ..unresolved_item_2.clone()
15422 };
15423
15424 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
15425 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
15426 cx.lsp
15427 .server
15428 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
15429 let unresolved_item_1 = unresolved_item_1.clone();
15430 let resolved_item_1 = resolved_item_1.clone();
15431 let unresolved_item_2 = unresolved_item_2.clone();
15432 let resolved_item_2 = resolved_item_2.clone();
15433 let resolve_requests_1 = resolve_requests_1.clone();
15434 let resolve_requests_2 = resolve_requests_2.clone();
15435 move |unresolved_request, _| {
15436 let unresolved_item_1 = unresolved_item_1.clone();
15437 let resolved_item_1 = resolved_item_1.clone();
15438 let unresolved_item_2 = unresolved_item_2.clone();
15439 let resolved_item_2 = resolved_item_2.clone();
15440 let resolve_requests_1 = resolve_requests_1.clone();
15441 let resolve_requests_2 = resolve_requests_2.clone();
15442 async move {
15443 if unresolved_request == unresolved_item_1 {
15444 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
15445 Ok(resolved_item_1.clone())
15446 } else if unresolved_request == unresolved_item_2 {
15447 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
15448 Ok(resolved_item_2.clone())
15449 } else {
15450 panic!("Unexpected completion item {unresolved_request:?}")
15451 }
15452 }
15453 }
15454 })
15455 .detach();
15456
15457 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15458 let unresolved_item_1 = unresolved_item_1.clone();
15459 let unresolved_item_2 = unresolved_item_2.clone();
15460 async move {
15461 Ok(Some(lsp::CompletionResponse::Array(vec![
15462 unresolved_item_1,
15463 unresolved_item_2,
15464 ])))
15465 }
15466 })
15467 .next()
15468 .await;
15469
15470 cx.condition(|editor, _| editor.context_menu_visible())
15471 .await;
15472 cx.update_editor(|editor, _, _| {
15473 let context_menu = editor.context_menu.borrow_mut();
15474 let context_menu = context_menu
15475 .as_ref()
15476 .expect("Should have the context menu deployed");
15477 match context_menu {
15478 CodeContextMenu::Completions(completions_menu) => {
15479 let completions = completions_menu.completions.borrow_mut();
15480 assert_eq!(
15481 completions
15482 .iter()
15483 .map(|completion| &completion.label.text)
15484 .collect::<Vec<_>>(),
15485 vec!["id", "other"]
15486 )
15487 }
15488 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15489 }
15490 });
15491 cx.run_until_parked();
15492
15493 cx.update_editor(|editor, window, cx| {
15494 editor.context_menu_next(&ContextMenuNext, window, cx);
15495 });
15496 cx.run_until_parked();
15497 cx.update_editor(|editor, window, cx| {
15498 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
15499 });
15500 cx.run_until_parked();
15501 cx.update_editor(|editor, window, cx| {
15502 editor.context_menu_next(&ContextMenuNext, window, cx);
15503 });
15504 cx.run_until_parked();
15505 cx.update_editor(|editor, window, cx| {
15506 editor
15507 .compose_completion(&ComposeCompletion::default(), window, cx)
15508 .expect("No task returned")
15509 })
15510 .await
15511 .expect("Completion failed");
15512 cx.run_until_parked();
15513
15514 cx.update_editor(|editor, _, cx| {
15515 assert_eq!(
15516 resolve_requests_1.load(atomic::Ordering::Acquire),
15517 1,
15518 "Should always resolve once despite multiple selections"
15519 );
15520 assert_eq!(
15521 resolve_requests_2.load(atomic::Ordering::Acquire),
15522 1,
15523 "Should always resolve once after multiple selections and applying the completion"
15524 );
15525 assert_eq!(
15526 editor.text(cx),
15527 "fn main() { let a = ??.other; }",
15528 "Should use resolved data when applying the completion"
15529 );
15530 });
15531}
15532
15533#[gpui::test]
15534async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
15535 init_test(cx, |_| {});
15536
15537 let item_0 = lsp::CompletionItem {
15538 label: "abs".into(),
15539 insert_text: Some("abs".into()),
15540 data: Some(json!({ "very": "special"})),
15541 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
15542 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
15543 lsp::InsertReplaceEdit {
15544 new_text: "abs".to_string(),
15545 insert: lsp::Range::default(),
15546 replace: lsp::Range::default(),
15547 },
15548 )),
15549 ..lsp::CompletionItem::default()
15550 };
15551 let items = iter::once(item_0.clone())
15552 .chain((11..51).map(|i| lsp::CompletionItem {
15553 label: format!("item_{}", i),
15554 insert_text: Some(format!("item_{}", i)),
15555 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15556 ..lsp::CompletionItem::default()
15557 }))
15558 .collect::<Vec<_>>();
15559
15560 let default_commit_characters = vec!["?".to_string()];
15561 let default_data = json!({ "default": "data"});
15562 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
15563 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
15564 let default_edit_range = lsp::Range {
15565 start: lsp::Position {
15566 line: 0,
15567 character: 5,
15568 },
15569 end: lsp::Position {
15570 line: 0,
15571 character: 5,
15572 },
15573 };
15574
15575 let mut cx = EditorLspTestContext::new_rust(
15576 lsp::ServerCapabilities {
15577 completion_provider: Some(lsp::CompletionOptions {
15578 trigger_characters: Some(vec![".".to_string()]),
15579 resolve_provider: Some(true),
15580 ..Default::default()
15581 }),
15582 ..Default::default()
15583 },
15584 cx,
15585 )
15586 .await;
15587
15588 cx.set_state("fn main() { let a = 2ˇ; }");
15589 cx.simulate_keystroke(".");
15590
15591 let completion_data = default_data.clone();
15592 let completion_characters = default_commit_characters.clone();
15593 let completion_items = items.clone();
15594 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15595 let default_data = completion_data.clone();
15596 let default_commit_characters = completion_characters.clone();
15597 let items = completion_items.clone();
15598 async move {
15599 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15600 items,
15601 item_defaults: Some(lsp::CompletionListItemDefaults {
15602 data: Some(default_data.clone()),
15603 commit_characters: Some(default_commit_characters.clone()),
15604 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
15605 default_edit_range,
15606 )),
15607 insert_text_format: Some(default_insert_text_format),
15608 insert_text_mode: Some(default_insert_text_mode),
15609 }),
15610 ..lsp::CompletionList::default()
15611 })))
15612 }
15613 })
15614 .next()
15615 .await;
15616
15617 let resolved_items = Arc::new(Mutex::new(Vec::new()));
15618 cx.lsp
15619 .server
15620 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
15621 let closure_resolved_items = resolved_items.clone();
15622 move |item_to_resolve, _| {
15623 let closure_resolved_items = closure_resolved_items.clone();
15624 async move {
15625 closure_resolved_items.lock().push(item_to_resolve.clone());
15626 Ok(item_to_resolve)
15627 }
15628 }
15629 })
15630 .detach();
15631
15632 cx.condition(|editor, _| editor.context_menu_visible())
15633 .await;
15634 cx.run_until_parked();
15635 cx.update_editor(|editor, _, _| {
15636 let menu = editor.context_menu.borrow_mut();
15637 match menu.as_ref().expect("should have the completions menu") {
15638 CodeContextMenu::Completions(completions_menu) => {
15639 assert_eq!(
15640 completions_menu
15641 .entries
15642 .borrow()
15643 .iter()
15644 .map(|mat| mat.string.clone())
15645 .collect::<Vec<String>>(),
15646 items
15647 .iter()
15648 .map(|completion| completion.label.clone())
15649 .collect::<Vec<String>>()
15650 );
15651 }
15652 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
15653 }
15654 });
15655 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
15656 // with 4 from the end.
15657 assert_eq!(
15658 *resolved_items.lock(),
15659 [&items[0..16], &items[items.len() - 4..items.len()]]
15660 .concat()
15661 .iter()
15662 .cloned()
15663 .map(|mut item| {
15664 if item.data.is_none() {
15665 item.data = Some(default_data.clone());
15666 }
15667 item
15668 })
15669 .collect::<Vec<lsp::CompletionItem>>(),
15670 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
15671 );
15672 resolved_items.lock().clear();
15673
15674 cx.update_editor(|editor, window, cx| {
15675 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
15676 });
15677 cx.run_until_parked();
15678 // Completions that have already been resolved are skipped.
15679 assert_eq!(
15680 *resolved_items.lock(),
15681 items[items.len() - 17..items.len() - 4]
15682 .iter()
15683 .cloned()
15684 .map(|mut item| {
15685 if item.data.is_none() {
15686 item.data = Some(default_data.clone());
15687 }
15688 item
15689 })
15690 .collect::<Vec<lsp::CompletionItem>>()
15691 );
15692 resolved_items.lock().clear();
15693}
15694
15695#[gpui::test]
15696async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
15697 init_test(cx, |_| {});
15698
15699 let mut cx = EditorLspTestContext::new(
15700 Language::new(
15701 LanguageConfig {
15702 matcher: LanguageMatcher {
15703 path_suffixes: vec!["jsx".into()],
15704 ..Default::default()
15705 },
15706 overrides: [(
15707 "element".into(),
15708 LanguageConfigOverride {
15709 completion_query_characters: Override::Set(['-'].into_iter().collect()),
15710 ..Default::default()
15711 },
15712 )]
15713 .into_iter()
15714 .collect(),
15715 ..Default::default()
15716 },
15717 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15718 )
15719 .with_override_query("(jsx_self_closing_element) @element")
15720 .unwrap(),
15721 lsp::ServerCapabilities {
15722 completion_provider: Some(lsp::CompletionOptions {
15723 trigger_characters: Some(vec![":".to_string()]),
15724 ..Default::default()
15725 }),
15726 ..Default::default()
15727 },
15728 cx,
15729 )
15730 .await;
15731
15732 cx.lsp
15733 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15734 Ok(Some(lsp::CompletionResponse::Array(vec![
15735 lsp::CompletionItem {
15736 label: "bg-blue".into(),
15737 ..Default::default()
15738 },
15739 lsp::CompletionItem {
15740 label: "bg-red".into(),
15741 ..Default::default()
15742 },
15743 lsp::CompletionItem {
15744 label: "bg-yellow".into(),
15745 ..Default::default()
15746 },
15747 ])))
15748 });
15749
15750 cx.set_state(r#"<p class="bgˇ" />"#);
15751
15752 // Trigger completion when typing a dash, because the dash is an extra
15753 // word character in the 'element' scope, which contains the cursor.
15754 cx.simulate_keystroke("-");
15755 cx.executor().run_until_parked();
15756 cx.update_editor(|editor, _, _| {
15757 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15758 {
15759 assert_eq!(
15760 completion_menu_entries(&menu),
15761 &["bg-blue", "bg-red", "bg-yellow"]
15762 );
15763 } else {
15764 panic!("expected completion menu to be open");
15765 }
15766 });
15767
15768 cx.simulate_keystroke("l");
15769 cx.executor().run_until_parked();
15770 cx.update_editor(|editor, _, _| {
15771 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15772 {
15773 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
15774 } else {
15775 panic!("expected completion menu to be open");
15776 }
15777 });
15778
15779 // When filtering completions, consider the character after the '-' to
15780 // be the start of a subword.
15781 cx.set_state(r#"<p class="yelˇ" />"#);
15782 cx.simulate_keystroke("l");
15783 cx.executor().run_until_parked();
15784 cx.update_editor(|editor, _, _| {
15785 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15786 {
15787 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
15788 } else {
15789 panic!("expected completion menu to be open");
15790 }
15791 });
15792}
15793
15794fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
15795 let entries = menu.entries.borrow();
15796 entries.iter().map(|mat| mat.string.clone()).collect()
15797}
15798
15799#[gpui::test]
15800async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
15801 init_test(cx, |settings| {
15802 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(vec![
15803 Formatter::Prettier,
15804 ]))
15805 });
15806
15807 let fs = FakeFs::new(cx.executor());
15808 fs.insert_file(path!("/file.ts"), Default::default()).await;
15809
15810 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
15811 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15812
15813 language_registry.add(Arc::new(Language::new(
15814 LanguageConfig {
15815 name: "TypeScript".into(),
15816 matcher: LanguageMatcher {
15817 path_suffixes: vec!["ts".to_string()],
15818 ..Default::default()
15819 },
15820 ..Default::default()
15821 },
15822 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
15823 )));
15824 update_test_language_settings(cx, |settings| {
15825 settings.defaults.prettier = Some(PrettierSettings {
15826 allowed: true,
15827 ..PrettierSettings::default()
15828 });
15829 });
15830
15831 let test_plugin = "test_plugin";
15832 let _ = language_registry.register_fake_lsp(
15833 "TypeScript",
15834 FakeLspAdapter {
15835 prettier_plugins: vec![test_plugin],
15836 ..Default::default()
15837 },
15838 );
15839
15840 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
15841 let buffer = project
15842 .update(cx, |project, cx| {
15843 project.open_local_buffer(path!("/file.ts"), cx)
15844 })
15845 .await
15846 .unwrap();
15847
15848 let buffer_text = "one\ntwo\nthree\n";
15849 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
15850 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
15851 editor.update_in(cx, |editor, window, cx| {
15852 editor.set_text(buffer_text, window, cx)
15853 });
15854
15855 editor
15856 .update_in(cx, |editor, window, cx| {
15857 editor.perform_format(
15858 project.clone(),
15859 FormatTrigger::Manual,
15860 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
15861 window,
15862 cx,
15863 )
15864 })
15865 .unwrap()
15866 .await;
15867 assert_eq!(
15868 editor.update(cx, |editor, cx| editor.text(cx)),
15869 buffer_text.to_string() + prettier_format_suffix,
15870 "Test prettier formatting was not applied to the original buffer text",
15871 );
15872
15873 update_test_language_settings(cx, |settings| {
15874 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
15875 });
15876 let format = editor.update_in(cx, |editor, window, cx| {
15877 editor.perform_format(
15878 project.clone(),
15879 FormatTrigger::Manual,
15880 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
15881 window,
15882 cx,
15883 )
15884 });
15885 format.await.unwrap();
15886 assert_eq!(
15887 editor.update(cx, |editor, cx| editor.text(cx)),
15888 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
15889 "Autoformatting (via test prettier) was not applied to the original buffer text",
15890 );
15891}
15892
15893#[gpui::test]
15894async fn test_addition_reverts(cx: &mut TestAppContext) {
15895 init_test(cx, |_| {});
15896 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15897 let base_text = indoc! {r#"
15898 struct Row;
15899 struct Row1;
15900 struct Row2;
15901
15902 struct Row4;
15903 struct Row5;
15904 struct Row6;
15905
15906 struct Row8;
15907 struct Row9;
15908 struct Row10;"#};
15909
15910 // When addition hunks are not adjacent to carets, no hunk revert is performed
15911 assert_hunk_revert(
15912 indoc! {r#"struct Row;
15913 struct Row1;
15914 struct Row1.1;
15915 struct Row1.2;
15916 struct Row2;ˇ
15917
15918 struct Row4;
15919 struct Row5;
15920 struct Row6;
15921
15922 struct Row8;
15923 ˇstruct Row9;
15924 struct Row9.1;
15925 struct Row9.2;
15926 struct Row9.3;
15927 struct Row10;"#},
15928 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
15929 indoc! {r#"struct Row;
15930 struct Row1;
15931 struct Row1.1;
15932 struct Row1.2;
15933 struct Row2;ˇ
15934
15935 struct Row4;
15936 struct Row5;
15937 struct Row6;
15938
15939 struct Row8;
15940 ˇstruct Row9;
15941 struct Row9.1;
15942 struct Row9.2;
15943 struct Row9.3;
15944 struct Row10;"#},
15945 base_text,
15946 &mut cx,
15947 );
15948 // Same for selections
15949 assert_hunk_revert(
15950 indoc! {r#"struct Row;
15951 struct Row1;
15952 struct Row2;
15953 struct Row2.1;
15954 struct Row2.2;
15955 «ˇ
15956 struct Row4;
15957 struct» Row5;
15958 «struct Row6;
15959 ˇ»
15960 struct Row9.1;
15961 struct Row9.2;
15962 struct Row9.3;
15963 struct Row8;
15964 struct Row9;
15965 struct Row10;"#},
15966 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
15967 indoc! {r#"struct Row;
15968 struct Row1;
15969 struct Row2;
15970 struct Row2.1;
15971 struct Row2.2;
15972 «ˇ
15973 struct Row4;
15974 struct» Row5;
15975 «struct Row6;
15976 ˇ»
15977 struct Row9.1;
15978 struct Row9.2;
15979 struct Row9.3;
15980 struct Row8;
15981 struct Row9;
15982 struct Row10;"#},
15983 base_text,
15984 &mut cx,
15985 );
15986
15987 // When carets and selections intersect the addition hunks, those are reverted.
15988 // Adjacent carets got merged.
15989 assert_hunk_revert(
15990 indoc! {r#"struct Row;
15991 ˇ// something on the top
15992 struct Row1;
15993 struct Row2;
15994 struct Roˇw3.1;
15995 struct Row2.2;
15996 struct Row2.3;ˇ
15997
15998 struct Row4;
15999 struct ˇRow5.1;
16000 struct Row5.2;
16001 struct «Rowˇ»5.3;
16002 struct Row5;
16003 struct Row6;
16004 ˇ
16005 struct Row9.1;
16006 struct «Rowˇ»9.2;
16007 struct «ˇRow»9.3;
16008 struct Row8;
16009 struct Row9;
16010 «ˇ// something on bottom»
16011 struct Row10;"#},
16012 vec![
16013 DiffHunkStatusKind::Added,
16014 DiffHunkStatusKind::Added,
16015 DiffHunkStatusKind::Added,
16016 DiffHunkStatusKind::Added,
16017 DiffHunkStatusKind::Added,
16018 ],
16019 indoc! {r#"struct Row;
16020 ˇstruct Row1;
16021 struct Row2;
16022 ˇ
16023 struct Row4;
16024 ˇstruct Row5;
16025 struct Row6;
16026 ˇ
16027 ˇstruct Row8;
16028 struct Row9;
16029 ˇstruct Row10;"#},
16030 base_text,
16031 &mut cx,
16032 );
16033}
16034
16035#[gpui::test]
16036async fn test_modification_reverts(cx: &mut TestAppContext) {
16037 init_test(cx, |_| {});
16038 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16039 let base_text = indoc! {r#"
16040 struct Row;
16041 struct Row1;
16042 struct Row2;
16043
16044 struct Row4;
16045 struct Row5;
16046 struct Row6;
16047
16048 struct Row8;
16049 struct Row9;
16050 struct Row10;"#};
16051
16052 // Modification hunks behave the same as the addition ones.
16053 assert_hunk_revert(
16054 indoc! {r#"struct Row;
16055 struct Row1;
16056 struct Row33;
16057 ˇ
16058 struct Row4;
16059 struct Row5;
16060 struct Row6;
16061 ˇ
16062 struct Row99;
16063 struct Row9;
16064 struct Row10;"#},
16065 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16066 indoc! {r#"struct Row;
16067 struct Row1;
16068 struct Row33;
16069 ˇ
16070 struct Row4;
16071 struct Row5;
16072 struct Row6;
16073 ˇ
16074 struct Row99;
16075 struct Row9;
16076 struct Row10;"#},
16077 base_text,
16078 &mut cx,
16079 );
16080 assert_hunk_revert(
16081 indoc! {r#"struct Row;
16082 struct Row1;
16083 struct Row33;
16084 «ˇ
16085 struct Row4;
16086 struct» Row5;
16087 «struct Row6;
16088 ˇ»
16089 struct Row99;
16090 struct Row9;
16091 struct Row10;"#},
16092 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16093 indoc! {r#"struct Row;
16094 struct Row1;
16095 struct Row33;
16096 «ˇ
16097 struct Row4;
16098 struct» Row5;
16099 «struct Row6;
16100 ˇ»
16101 struct Row99;
16102 struct Row9;
16103 struct Row10;"#},
16104 base_text,
16105 &mut cx,
16106 );
16107
16108 assert_hunk_revert(
16109 indoc! {r#"ˇstruct Row1.1;
16110 struct Row1;
16111 «ˇstr»uct Row22;
16112
16113 struct ˇRow44;
16114 struct Row5;
16115 struct «Rˇ»ow66;ˇ
16116
16117 «struˇ»ct Row88;
16118 struct Row9;
16119 struct Row1011;ˇ"#},
16120 vec![
16121 DiffHunkStatusKind::Modified,
16122 DiffHunkStatusKind::Modified,
16123 DiffHunkStatusKind::Modified,
16124 DiffHunkStatusKind::Modified,
16125 DiffHunkStatusKind::Modified,
16126 DiffHunkStatusKind::Modified,
16127 ],
16128 indoc! {r#"struct Row;
16129 ˇstruct Row1;
16130 struct Row2;
16131 ˇ
16132 struct Row4;
16133 ˇstruct Row5;
16134 struct Row6;
16135 ˇ
16136 struct Row8;
16137 ˇstruct Row9;
16138 struct Row10;ˇ"#},
16139 base_text,
16140 &mut cx,
16141 );
16142}
16143
16144#[gpui::test]
16145async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
16146 init_test(cx, |_| {});
16147 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16148 let base_text = indoc! {r#"
16149 one
16150
16151 two
16152 three
16153 "#};
16154
16155 cx.set_head_text(base_text);
16156 cx.set_state("\nˇ\n");
16157 cx.executor().run_until_parked();
16158 cx.update_editor(|editor, _window, cx| {
16159 editor.expand_selected_diff_hunks(cx);
16160 });
16161 cx.executor().run_until_parked();
16162 cx.update_editor(|editor, window, cx| {
16163 editor.backspace(&Default::default(), window, cx);
16164 });
16165 cx.run_until_parked();
16166 cx.assert_state_with_diff(
16167 indoc! {r#"
16168
16169 - two
16170 - threeˇ
16171 +
16172 "#}
16173 .to_string(),
16174 );
16175}
16176
16177#[gpui::test]
16178async fn test_deletion_reverts(cx: &mut TestAppContext) {
16179 init_test(cx, |_| {});
16180 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16181 let base_text = indoc! {r#"struct Row;
16182struct Row1;
16183struct Row2;
16184
16185struct Row4;
16186struct Row5;
16187struct Row6;
16188
16189struct Row8;
16190struct Row9;
16191struct Row10;"#};
16192
16193 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
16194 assert_hunk_revert(
16195 indoc! {r#"struct Row;
16196 struct Row2;
16197
16198 ˇstruct Row4;
16199 struct Row5;
16200 struct Row6;
16201 ˇ
16202 struct Row8;
16203 struct Row10;"#},
16204 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16205 indoc! {r#"struct Row;
16206 struct Row2;
16207
16208 ˇstruct Row4;
16209 struct Row5;
16210 struct Row6;
16211 ˇ
16212 struct Row8;
16213 struct Row10;"#},
16214 base_text,
16215 &mut cx,
16216 );
16217 assert_hunk_revert(
16218 indoc! {r#"struct Row;
16219 struct Row2;
16220
16221 «ˇstruct Row4;
16222 struct» Row5;
16223 «struct Row6;
16224 ˇ»
16225 struct Row8;
16226 struct Row10;"#},
16227 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16228 indoc! {r#"struct Row;
16229 struct Row2;
16230
16231 «ˇstruct Row4;
16232 struct» Row5;
16233 «struct Row6;
16234 ˇ»
16235 struct Row8;
16236 struct Row10;"#},
16237 base_text,
16238 &mut cx,
16239 );
16240
16241 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
16242 assert_hunk_revert(
16243 indoc! {r#"struct Row;
16244 ˇstruct Row2;
16245
16246 struct Row4;
16247 struct Row5;
16248 struct Row6;
16249
16250 struct Row8;ˇ
16251 struct Row10;"#},
16252 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16253 indoc! {r#"struct Row;
16254 struct Row1;
16255 ˇstruct Row2;
16256
16257 struct Row4;
16258 struct Row5;
16259 struct Row6;
16260
16261 struct Row8;ˇ
16262 struct Row9;
16263 struct Row10;"#},
16264 base_text,
16265 &mut cx,
16266 );
16267 assert_hunk_revert(
16268 indoc! {r#"struct Row;
16269 struct Row2«ˇ;
16270 struct Row4;
16271 struct» Row5;
16272 «struct Row6;
16273
16274 struct Row8;ˇ»
16275 struct Row10;"#},
16276 vec![
16277 DiffHunkStatusKind::Deleted,
16278 DiffHunkStatusKind::Deleted,
16279 DiffHunkStatusKind::Deleted,
16280 ],
16281 indoc! {r#"struct Row;
16282 struct Row1;
16283 struct Row2«ˇ;
16284
16285 struct Row4;
16286 struct» Row5;
16287 «struct Row6;
16288
16289 struct Row8;ˇ»
16290 struct Row9;
16291 struct Row10;"#},
16292 base_text,
16293 &mut cx,
16294 );
16295}
16296
16297#[gpui::test]
16298async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
16299 init_test(cx, |_| {});
16300
16301 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
16302 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
16303 let base_text_3 =
16304 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
16305
16306 let text_1 = edit_first_char_of_every_line(base_text_1);
16307 let text_2 = edit_first_char_of_every_line(base_text_2);
16308 let text_3 = edit_first_char_of_every_line(base_text_3);
16309
16310 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
16311 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
16312 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
16313
16314 let multibuffer = cx.new(|cx| {
16315 let mut multibuffer = MultiBuffer::new(ReadWrite);
16316 multibuffer.push_excerpts(
16317 buffer_1.clone(),
16318 [
16319 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16320 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16321 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16322 ],
16323 cx,
16324 );
16325 multibuffer.push_excerpts(
16326 buffer_2.clone(),
16327 [
16328 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16329 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16330 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16331 ],
16332 cx,
16333 );
16334 multibuffer.push_excerpts(
16335 buffer_3.clone(),
16336 [
16337 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16338 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16339 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16340 ],
16341 cx,
16342 );
16343 multibuffer
16344 });
16345
16346 let fs = FakeFs::new(cx.executor());
16347 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
16348 let (editor, cx) = cx
16349 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
16350 editor.update_in(cx, |editor, _window, cx| {
16351 for (buffer, diff_base) in [
16352 (buffer_1.clone(), base_text_1),
16353 (buffer_2.clone(), base_text_2),
16354 (buffer_3.clone(), base_text_3),
16355 ] {
16356 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
16357 editor
16358 .buffer
16359 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
16360 }
16361 });
16362 cx.executor().run_until_parked();
16363
16364 editor.update_in(cx, |editor, window, cx| {
16365 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}");
16366 editor.select_all(&SelectAll, window, cx);
16367 editor.git_restore(&Default::default(), window, cx);
16368 });
16369 cx.executor().run_until_parked();
16370
16371 // When all ranges are selected, all buffer hunks are reverted.
16372 editor.update(cx, |editor, cx| {
16373 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");
16374 });
16375 buffer_1.update(cx, |buffer, _| {
16376 assert_eq!(buffer.text(), base_text_1);
16377 });
16378 buffer_2.update(cx, |buffer, _| {
16379 assert_eq!(buffer.text(), base_text_2);
16380 });
16381 buffer_3.update(cx, |buffer, _| {
16382 assert_eq!(buffer.text(), base_text_3);
16383 });
16384
16385 editor.update_in(cx, |editor, window, cx| {
16386 editor.undo(&Default::default(), window, cx);
16387 });
16388
16389 editor.update_in(cx, |editor, window, cx| {
16390 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16391 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
16392 });
16393 editor.git_restore(&Default::default(), window, cx);
16394 });
16395
16396 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
16397 // but not affect buffer_2 and its related excerpts.
16398 editor.update(cx, |editor, cx| {
16399 assert_eq!(
16400 editor.text(cx),
16401 "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}"
16402 );
16403 });
16404 buffer_1.update(cx, |buffer, _| {
16405 assert_eq!(buffer.text(), base_text_1);
16406 });
16407 buffer_2.update(cx, |buffer, _| {
16408 assert_eq!(
16409 buffer.text(),
16410 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
16411 );
16412 });
16413 buffer_3.update(cx, |buffer, _| {
16414 assert_eq!(
16415 buffer.text(),
16416 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
16417 );
16418 });
16419
16420 fn edit_first_char_of_every_line(text: &str) -> String {
16421 text.split('\n')
16422 .map(|line| format!("X{}", &line[1..]))
16423 .collect::<Vec<_>>()
16424 .join("\n")
16425 }
16426}
16427
16428#[gpui::test]
16429async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
16430 init_test(cx, |_| {});
16431
16432 let cols = 4;
16433 let rows = 10;
16434 let sample_text_1 = sample_text(rows, cols, 'a');
16435 assert_eq!(
16436 sample_text_1,
16437 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
16438 );
16439 let sample_text_2 = sample_text(rows, cols, 'l');
16440 assert_eq!(
16441 sample_text_2,
16442 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
16443 );
16444 let sample_text_3 = sample_text(rows, cols, 'v');
16445 assert_eq!(
16446 sample_text_3,
16447 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
16448 );
16449
16450 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
16451 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
16452 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
16453
16454 let multi_buffer = cx.new(|cx| {
16455 let mut multibuffer = MultiBuffer::new(ReadWrite);
16456 multibuffer.push_excerpts(
16457 buffer_1.clone(),
16458 [
16459 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16460 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16461 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16462 ],
16463 cx,
16464 );
16465 multibuffer.push_excerpts(
16466 buffer_2.clone(),
16467 [
16468 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16469 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16470 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16471 ],
16472 cx,
16473 );
16474 multibuffer.push_excerpts(
16475 buffer_3.clone(),
16476 [
16477 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16478 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16479 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16480 ],
16481 cx,
16482 );
16483 multibuffer
16484 });
16485
16486 let fs = FakeFs::new(cx.executor());
16487 fs.insert_tree(
16488 "/a",
16489 json!({
16490 "main.rs": sample_text_1,
16491 "other.rs": sample_text_2,
16492 "lib.rs": sample_text_3,
16493 }),
16494 )
16495 .await;
16496 let project = Project::test(fs, ["/a".as_ref()], cx).await;
16497 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16498 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16499 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16500 Editor::new(
16501 EditorMode::full(),
16502 multi_buffer,
16503 Some(project.clone()),
16504 window,
16505 cx,
16506 )
16507 });
16508 let multibuffer_item_id = workspace
16509 .update(cx, |workspace, window, cx| {
16510 assert!(
16511 workspace.active_item(cx).is_none(),
16512 "active item should be None before the first item is added"
16513 );
16514 workspace.add_item_to_active_pane(
16515 Box::new(multi_buffer_editor.clone()),
16516 None,
16517 true,
16518 window,
16519 cx,
16520 );
16521 let active_item = workspace
16522 .active_item(cx)
16523 .expect("should have an active item after adding the multi buffer");
16524 assert!(
16525 !active_item.is_singleton(cx),
16526 "A multi buffer was expected to active after adding"
16527 );
16528 active_item.item_id()
16529 })
16530 .unwrap();
16531 cx.executor().run_until_parked();
16532
16533 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16534 editor.change_selections(
16535 SelectionEffects::scroll(Autoscroll::Next),
16536 window,
16537 cx,
16538 |s| s.select_ranges(Some(1..2)),
16539 );
16540 editor.open_excerpts(&OpenExcerpts, window, cx);
16541 });
16542 cx.executor().run_until_parked();
16543 let first_item_id = workspace
16544 .update(cx, |workspace, window, cx| {
16545 let active_item = workspace
16546 .active_item(cx)
16547 .expect("should have an active item after navigating into the 1st buffer");
16548 let first_item_id = active_item.item_id();
16549 assert_ne!(
16550 first_item_id, multibuffer_item_id,
16551 "Should navigate into the 1st buffer and activate it"
16552 );
16553 assert!(
16554 active_item.is_singleton(cx),
16555 "New active item should be a singleton buffer"
16556 );
16557 assert_eq!(
16558 active_item
16559 .act_as::<Editor>(cx)
16560 .expect("should have navigated into an editor for the 1st buffer")
16561 .read(cx)
16562 .text(cx),
16563 sample_text_1
16564 );
16565
16566 workspace
16567 .go_back(workspace.active_pane().downgrade(), window, cx)
16568 .detach_and_log_err(cx);
16569
16570 first_item_id
16571 })
16572 .unwrap();
16573 cx.executor().run_until_parked();
16574 workspace
16575 .update(cx, |workspace, _, cx| {
16576 let active_item = workspace
16577 .active_item(cx)
16578 .expect("should have an active item after navigating back");
16579 assert_eq!(
16580 active_item.item_id(),
16581 multibuffer_item_id,
16582 "Should navigate back to the multi buffer"
16583 );
16584 assert!(!active_item.is_singleton(cx));
16585 })
16586 .unwrap();
16587
16588 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16589 editor.change_selections(
16590 SelectionEffects::scroll(Autoscroll::Next),
16591 window,
16592 cx,
16593 |s| s.select_ranges(Some(39..40)),
16594 );
16595 editor.open_excerpts(&OpenExcerpts, window, cx);
16596 });
16597 cx.executor().run_until_parked();
16598 let second_item_id = workspace
16599 .update(cx, |workspace, window, cx| {
16600 let active_item = workspace
16601 .active_item(cx)
16602 .expect("should have an active item after navigating into the 2nd buffer");
16603 let second_item_id = active_item.item_id();
16604 assert_ne!(
16605 second_item_id, multibuffer_item_id,
16606 "Should navigate away from the multibuffer"
16607 );
16608 assert_ne!(
16609 second_item_id, first_item_id,
16610 "Should navigate into the 2nd buffer and activate it"
16611 );
16612 assert!(
16613 active_item.is_singleton(cx),
16614 "New active item should be a singleton buffer"
16615 );
16616 assert_eq!(
16617 active_item
16618 .act_as::<Editor>(cx)
16619 .expect("should have navigated into an editor")
16620 .read(cx)
16621 .text(cx),
16622 sample_text_2
16623 );
16624
16625 workspace
16626 .go_back(workspace.active_pane().downgrade(), window, cx)
16627 .detach_and_log_err(cx);
16628
16629 second_item_id
16630 })
16631 .unwrap();
16632 cx.executor().run_until_parked();
16633 workspace
16634 .update(cx, |workspace, _, cx| {
16635 let active_item = workspace
16636 .active_item(cx)
16637 .expect("should have an active item after navigating back from the 2nd buffer");
16638 assert_eq!(
16639 active_item.item_id(),
16640 multibuffer_item_id,
16641 "Should navigate back from the 2nd buffer to the multi buffer"
16642 );
16643 assert!(!active_item.is_singleton(cx));
16644 })
16645 .unwrap();
16646
16647 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16648 editor.change_selections(
16649 SelectionEffects::scroll(Autoscroll::Next),
16650 window,
16651 cx,
16652 |s| s.select_ranges(Some(70..70)),
16653 );
16654 editor.open_excerpts(&OpenExcerpts, window, cx);
16655 });
16656 cx.executor().run_until_parked();
16657 workspace
16658 .update(cx, |workspace, window, cx| {
16659 let active_item = workspace
16660 .active_item(cx)
16661 .expect("should have an active item after navigating into the 3rd buffer");
16662 let third_item_id = active_item.item_id();
16663 assert_ne!(
16664 third_item_id, multibuffer_item_id,
16665 "Should navigate into the 3rd buffer and activate it"
16666 );
16667 assert_ne!(third_item_id, first_item_id);
16668 assert_ne!(third_item_id, second_item_id);
16669 assert!(
16670 active_item.is_singleton(cx),
16671 "New active item should be a singleton buffer"
16672 );
16673 assert_eq!(
16674 active_item
16675 .act_as::<Editor>(cx)
16676 .expect("should have navigated into an editor")
16677 .read(cx)
16678 .text(cx),
16679 sample_text_3
16680 );
16681
16682 workspace
16683 .go_back(workspace.active_pane().downgrade(), window, cx)
16684 .detach_and_log_err(cx);
16685 })
16686 .unwrap();
16687 cx.executor().run_until_parked();
16688 workspace
16689 .update(cx, |workspace, _, cx| {
16690 let active_item = workspace
16691 .active_item(cx)
16692 .expect("should have an active item after navigating back from the 3rd buffer");
16693 assert_eq!(
16694 active_item.item_id(),
16695 multibuffer_item_id,
16696 "Should navigate back from the 3rd buffer to the multi buffer"
16697 );
16698 assert!(!active_item.is_singleton(cx));
16699 })
16700 .unwrap();
16701}
16702
16703#[gpui::test]
16704async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16705 init_test(cx, |_| {});
16706
16707 let mut cx = EditorTestContext::new(cx).await;
16708
16709 let diff_base = r#"
16710 use some::mod;
16711
16712 const A: u32 = 42;
16713
16714 fn main() {
16715 println!("hello");
16716
16717 println!("world");
16718 }
16719 "#
16720 .unindent();
16721
16722 cx.set_state(
16723 &r#"
16724 use some::modified;
16725
16726 ˇ
16727 fn main() {
16728 println!("hello there");
16729
16730 println!("around the");
16731 println!("world");
16732 }
16733 "#
16734 .unindent(),
16735 );
16736
16737 cx.set_head_text(&diff_base);
16738 executor.run_until_parked();
16739
16740 cx.update_editor(|editor, window, cx| {
16741 editor.go_to_next_hunk(&GoToHunk, window, cx);
16742 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16743 });
16744 executor.run_until_parked();
16745 cx.assert_state_with_diff(
16746 r#"
16747 use some::modified;
16748
16749
16750 fn main() {
16751 - println!("hello");
16752 + ˇ println!("hello there");
16753
16754 println!("around the");
16755 println!("world");
16756 }
16757 "#
16758 .unindent(),
16759 );
16760
16761 cx.update_editor(|editor, window, cx| {
16762 for _ in 0..2 {
16763 editor.go_to_next_hunk(&GoToHunk, window, cx);
16764 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16765 }
16766 });
16767 executor.run_until_parked();
16768 cx.assert_state_with_diff(
16769 r#"
16770 - use some::mod;
16771 + ˇuse some::modified;
16772
16773
16774 fn main() {
16775 - println!("hello");
16776 + println!("hello there");
16777
16778 + println!("around the");
16779 println!("world");
16780 }
16781 "#
16782 .unindent(),
16783 );
16784
16785 cx.update_editor(|editor, window, cx| {
16786 editor.go_to_next_hunk(&GoToHunk, window, cx);
16787 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16788 });
16789 executor.run_until_parked();
16790 cx.assert_state_with_diff(
16791 r#"
16792 - use some::mod;
16793 + use some::modified;
16794
16795 - const A: u32 = 42;
16796 ˇ
16797 fn main() {
16798 - println!("hello");
16799 + println!("hello there");
16800
16801 + println!("around the");
16802 println!("world");
16803 }
16804 "#
16805 .unindent(),
16806 );
16807
16808 cx.update_editor(|editor, window, cx| {
16809 editor.cancel(&Cancel, window, cx);
16810 });
16811
16812 cx.assert_state_with_diff(
16813 r#"
16814 use some::modified;
16815
16816 ˇ
16817 fn main() {
16818 println!("hello there");
16819
16820 println!("around the");
16821 println!("world");
16822 }
16823 "#
16824 .unindent(),
16825 );
16826}
16827
16828#[gpui::test]
16829async fn test_diff_base_change_with_expanded_diff_hunks(
16830 executor: BackgroundExecutor,
16831 cx: &mut TestAppContext,
16832) {
16833 init_test(cx, |_| {});
16834
16835 let mut cx = EditorTestContext::new(cx).await;
16836
16837 let diff_base = r#"
16838 use some::mod1;
16839 use some::mod2;
16840
16841 const A: u32 = 42;
16842 const B: u32 = 42;
16843 const C: u32 = 42;
16844
16845 fn main() {
16846 println!("hello");
16847
16848 println!("world");
16849 }
16850 "#
16851 .unindent();
16852
16853 cx.set_state(
16854 &r#"
16855 use some::mod2;
16856
16857 const A: u32 = 42;
16858 const C: u32 = 42;
16859
16860 fn main(ˇ) {
16861 //println!("hello");
16862
16863 println!("world");
16864 //
16865 //
16866 }
16867 "#
16868 .unindent(),
16869 );
16870
16871 cx.set_head_text(&diff_base);
16872 executor.run_until_parked();
16873
16874 cx.update_editor(|editor, window, cx| {
16875 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16876 });
16877 executor.run_until_parked();
16878 cx.assert_state_with_diff(
16879 r#"
16880 - use some::mod1;
16881 use some::mod2;
16882
16883 const A: u32 = 42;
16884 - const B: u32 = 42;
16885 const C: u32 = 42;
16886
16887 fn main(ˇ) {
16888 - println!("hello");
16889 + //println!("hello");
16890
16891 println!("world");
16892 + //
16893 + //
16894 }
16895 "#
16896 .unindent(),
16897 );
16898
16899 cx.set_head_text("new diff base!");
16900 executor.run_until_parked();
16901 cx.assert_state_with_diff(
16902 r#"
16903 - new diff base!
16904 + use some::mod2;
16905 +
16906 + const A: u32 = 42;
16907 + const C: u32 = 42;
16908 +
16909 + fn main(ˇ) {
16910 + //println!("hello");
16911 +
16912 + println!("world");
16913 + //
16914 + //
16915 + }
16916 "#
16917 .unindent(),
16918 );
16919}
16920
16921#[gpui::test]
16922async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
16923 init_test(cx, |_| {});
16924
16925 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
16926 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
16927 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
16928 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
16929 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
16930 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
16931
16932 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
16933 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
16934 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
16935
16936 let multi_buffer = cx.new(|cx| {
16937 let mut multibuffer = MultiBuffer::new(ReadWrite);
16938 multibuffer.push_excerpts(
16939 buffer_1.clone(),
16940 [
16941 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16942 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16943 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16944 ],
16945 cx,
16946 );
16947 multibuffer.push_excerpts(
16948 buffer_2.clone(),
16949 [
16950 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16951 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16952 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16953 ],
16954 cx,
16955 );
16956 multibuffer.push_excerpts(
16957 buffer_3.clone(),
16958 [
16959 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16960 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16961 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16962 ],
16963 cx,
16964 );
16965 multibuffer
16966 });
16967
16968 let editor =
16969 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
16970 editor
16971 .update(cx, |editor, _window, cx| {
16972 for (buffer, diff_base) in [
16973 (buffer_1.clone(), file_1_old),
16974 (buffer_2.clone(), file_2_old),
16975 (buffer_3.clone(), file_3_old),
16976 ] {
16977 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
16978 editor
16979 .buffer
16980 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
16981 }
16982 })
16983 .unwrap();
16984
16985 let mut cx = EditorTestContext::for_editor(editor, cx).await;
16986 cx.run_until_parked();
16987
16988 cx.assert_editor_state(
16989 &"
16990 ˇaaa
16991 ccc
16992 ddd
16993
16994 ggg
16995 hhh
16996
16997
16998 lll
16999 mmm
17000 NNN
17001
17002 qqq
17003 rrr
17004
17005 uuu
17006 111
17007 222
17008 333
17009
17010 666
17011 777
17012
17013 000
17014 !!!"
17015 .unindent(),
17016 );
17017
17018 cx.update_editor(|editor, window, cx| {
17019 editor.select_all(&SelectAll, window, cx);
17020 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17021 });
17022 cx.executor().run_until_parked();
17023
17024 cx.assert_state_with_diff(
17025 "
17026 «aaa
17027 - bbb
17028 ccc
17029 ddd
17030
17031 ggg
17032 hhh
17033
17034
17035 lll
17036 mmm
17037 - nnn
17038 + NNN
17039
17040 qqq
17041 rrr
17042
17043 uuu
17044 111
17045 222
17046 333
17047
17048 + 666
17049 777
17050
17051 000
17052 !!!ˇ»"
17053 .unindent(),
17054 );
17055}
17056
17057#[gpui::test]
17058async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
17059 init_test(cx, |_| {});
17060
17061 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
17062 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
17063
17064 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
17065 let multi_buffer = cx.new(|cx| {
17066 let mut multibuffer = MultiBuffer::new(ReadWrite);
17067 multibuffer.push_excerpts(
17068 buffer.clone(),
17069 [
17070 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
17071 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
17072 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
17073 ],
17074 cx,
17075 );
17076 multibuffer
17077 });
17078
17079 let editor =
17080 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17081 editor
17082 .update(cx, |editor, _window, cx| {
17083 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
17084 editor
17085 .buffer
17086 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
17087 })
17088 .unwrap();
17089
17090 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17091 cx.run_until_parked();
17092
17093 cx.update_editor(|editor, window, cx| {
17094 editor.expand_all_diff_hunks(&Default::default(), window, cx)
17095 });
17096 cx.executor().run_until_parked();
17097
17098 // When the start of a hunk coincides with the start of its excerpt,
17099 // the hunk is expanded. When the start of a a hunk is earlier than
17100 // the start of its excerpt, the hunk is not expanded.
17101 cx.assert_state_with_diff(
17102 "
17103 ˇaaa
17104 - bbb
17105 + BBB
17106
17107 - ddd
17108 - eee
17109 + DDD
17110 + EEE
17111 fff
17112
17113 iii
17114 "
17115 .unindent(),
17116 );
17117}
17118
17119#[gpui::test]
17120async fn test_edits_around_expanded_insertion_hunks(
17121 executor: BackgroundExecutor,
17122 cx: &mut TestAppContext,
17123) {
17124 init_test(cx, |_| {});
17125
17126 let mut cx = EditorTestContext::new(cx).await;
17127
17128 let diff_base = r#"
17129 use some::mod1;
17130 use some::mod2;
17131
17132 const A: u32 = 42;
17133
17134 fn main() {
17135 println!("hello");
17136
17137 println!("world");
17138 }
17139 "#
17140 .unindent();
17141 executor.run_until_parked();
17142 cx.set_state(
17143 &r#"
17144 use some::mod1;
17145 use some::mod2;
17146
17147 const A: u32 = 42;
17148 const B: u32 = 42;
17149 const C: u32 = 42;
17150 ˇ
17151
17152 fn main() {
17153 println!("hello");
17154
17155 println!("world");
17156 }
17157 "#
17158 .unindent(),
17159 );
17160
17161 cx.set_head_text(&diff_base);
17162 executor.run_until_parked();
17163
17164 cx.update_editor(|editor, window, cx| {
17165 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17166 });
17167 executor.run_until_parked();
17168
17169 cx.assert_state_with_diff(
17170 r#"
17171 use some::mod1;
17172 use some::mod2;
17173
17174 const A: u32 = 42;
17175 + const B: u32 = 42;
17176 + const C: u32 = 42;
17177 + ˇ
17178
17179 fn main() {
17180 println!("hello");
17181
17182 println!("world");
17183 }
17184 "#
17185 .unindent(),
17186 );
17187
17188 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
17189 executor.run_until_parked();
17190
17191 cx.assert_state_with_diff(
17192 r#"
17193 use some::mod1;
17194 use some::mod2;
17195
17196 const A: u32 = 42;
17197 + const B: u32 = 42;
17198 + const C: u32 = 42;
17199 + const D: u32 = 42;
17200 + ˇ
17201
17202 fn main() {
17203 println!("hello");
17204
17205 println!("world");
17206 }
17207 "#
17208 .unindent(),
17209 );
17210
17211 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
17212 executor.run_until_parked();
17213
17214 cx.assert_state_with_diff(
17215 r#"
17216 use some::mod1;
17217 use some::mod2;
17218
17219 const A: u32 = 42;
17220 + const B: u32 = 42;
17221 + const C: u32 = 42;
17222 + const D: u32 = 42;
17223 + const E: u32 = 42;
17224 + ˇ
17225
17226 fn main() {
17227 println!("hello");
17228
17229 println!("world");
17230 }
17231 "#
17232 .unindent(),
17233 );
17234
17235 cx.update_editor(|editor, window, cx| {
17236 editor.delete_line(&DeleteLine, window, cx);
17237 });
17238 executor.run_until_parked();
17239
17240 cx.assert_state_with_diff(
17241 r#"
17242 use some::mod1;
17243 use some::mod2;
17244
17245 const A: u32 = 42;
17246 + const B: u32 = 42;
17247 + const C: u32 = 42;
17248 + const D: u32 = 42;
17249 + const E: u32 = 42;
17250 ˇ
17251 fn main() {
17252 println!("hello");
17253
17254 println!("world");
17255 }
17256 "#
17257 .unindent(),
17258 );
17259
17260 cx.update_editor(|editor, window, cx| {
17261 editor.move_up(&MoveUp, window, cx);
17262 editor.delete_line(&DeleteLine, window, cx);
17263 editor.move_up(&MoveUp, window, cx);
17264 editor.delete_line(&DeleteLine, window, cx);
17265 editor.move_up(&MoveUp, window, cx);
17266 editor.delete_line(&DeleteLine, window, cx);
17267 });
17268 executor.run_until_parked();
17269 cx.assert_state_with_diff(
17270 r#"
17271 use some::mod1;
17272 use some::mod2;
17273
17274 const A: u32 = 42;
17275 + const B: u32 = 42;
17276 ˇ
17277 fn main() {
17278 println!("hello");
17279
17280 println!("world");
17281 }
17282 "#
17283 .unindent(),
17284 );
17285
17286 cx.update_editor(|editor, window, cx| {
17287 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
17288 editor.delete_line(&DeleteLine, window, cx);
17289 });
17290 executor.run_until_parked();
17291 cx.assert_state_with_diff(
17292 r#"
17293 ˇ
17294 fn main() {
17295 println!("hello");
17296
17297 println!("world");
17298 }
17299 "#
17300 .unindent(),
17301 );
17302}
17303
17304#[gpui::test]
17305async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
17306 init_test(cx, |_| {});
17307
17308 let mut cx = EditorTestContext::new(cx).await;
17309 cx.set_head_text(indoc! { "
17310 one
17311 two
17312 three
17313 four
17314 five
17315 "
17316 });
17317 cx.set_state(indoc! { "
17318 one
17319 ˇthree
17320 five
17321 "});
17322 cx.run_until_parked();
17323 cx.update_editor(|editor, window, cx| {
17324 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17325 });
17326 cx.assert_state_with_diff(
17327 indoc! { "
17328 one
17329 - two
17330 ˇthree
17331 - four
17332 five
17333 "}
17334 .to_string(),
17335 );
17336 cx.update_editor(|editor, window, cx| {
17337 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17338 });
17339
17340 cx.assert_state_with_diff(
17341 indoc! { "
17342 one
17343 ˇthree
17344 five
17345 "}
17346 .to_string(),
17347 );
17348
17349 cx.set_state(indoc! { "
17350 one
17351 ˇTWO
17352 three
17353 four
17354 five
17355 "});
17356 cx.run_until_parked();
17357 cx.update_editor(|editor, window, cx| {
17358 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17359 });
17360
17361 cx.assert_state_with_diff(
17362 indoc! { "
17363 one
17364 - two
17365 + ˇTWO
17366 three
17367 four
17368 five
17369 "}
17370 .to_string(),
17371 );
17372 cx.update_editor(|editor, window, cx| {
17373 editor.move_up(&Default::default(), window, cx);
17374 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17375 });
17376 cx.assert_state_with_diff(
17377 indoc! { "
17378 one
17379 ˇTWO
17380 three
17381 four
17382 five
17383 "}
17384 .to_string(),
17385 );
17386}
17387
17388#[gpui::test]
17389async fn test_edits_around_expanded_deletion_hunks(
17390 executor: BackgroundExecutor,
17391 cx: &mut TestAppContext,
17392) {
17393 init_test(cx, |_| {});
17394
17395 let mut cx = EditorTestContext::new(cx).await;
17396
17397 let diff_base = r#"
17398 use some::mod1;
17399 use some::mod2;
17400
17401 const A: u32 = 42;
17402 const B: u32 = 42;
17403 const C: u32 = 42;
17404
17405
17406 fn main() {
17407 println!("hello");
17408
17409 println!("world");
17410 }
17411 "#
17412 .unindent();
17413 executor.run_until_parked();
17414 cx.set_state(
17415 &r#"
17416 use some::mod1;
17417 use some::mod2;
17418
17419 ˇconst B: u32 = 42;
17420 const C: u32 = 42;
17421
17422
17423 fn main() {
17424 println!("hello");
17425
17426 println!("world");
17427 }
17428 "#
17429 .unindent(),
17430 );
17431
17432 cx.set_head_text(&diff_base);
17433 executor.run_until_parked();
17434
17435 cx.update_editor(|editor, window, cx| {
17436 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17437 });
17438 executor.run_until_parked();
17439
17440 cx.assert_state_with_diff(
17441 r#"
17442 use some::mod1;
17443 use some::mod2;
17444
17445 - const A: u32 = 42;
17446 ˇconst B: u32 = 42;
17447 const C: u32 = 42;
17448
17449
17450 fn main() {
17451 println!("hello");
17452
17453 println!("world");
17454 }
17455 "#
17456 .unindent(),
17457 );
17458
17459 cx.update_editor(|editor, window, cx| {
17460 editor.delete_line(&DeleteLine, window, cx);
17461 });
17462 executor.run_until_parked();
17463 cx.assert_state_with_diff(
17464 r#"
17465 use some::mod1;
17466 use some::mod2;
17467
17468 - const A: u32 = 42;
17469 - const B: u32 = 42;
17470 ˇconst C: u32 = 42;
17471
17472
17473 fn main() {
17474 println!("hello");
17475
17476 println!("world");
17477 }
17478 "#
17479 .unindent(),
17480 );
17481
17482 cx.update_editor(|editor, window, cx| {
17483 editor.delete_line(&DeleteLine, window, cx);
17484 });
17485 executor.run_until_parked();
17486 cx.assert_state_with_diff(
17487 r#"
17488 use some::mod1;
17489 use some::mod2;
17490
17491 - const A: u32 = 42;
17492 - const B: u32 = 42;
17493 - const C: u32 = 42;
17494 ˇ
17495
17496 fn main() {
17497 println!("hello");
17498
17499 println!("world");
17500 }
17501 "#
17502 .unindent(),
17503 );
17504
17505 cx.update_editor(|editor, window, cx| {
17506 editor.handle_input("replacement", window, cx);
17507 });
17508 executor.run_until_parked();
17509 cx.assert_state_with_diff(
17510 r#"
17511 use some::mod1;
17512 use some::mod2;
17513
17514 - const A: u32 = 42;
17515 - const B: u32 = 42;
17516 - const C: u32 = 42;
17517 -
17518 + replacementˇ
17519
17520 fn main() {
17521 println!("hello");
17522
17523 println!("world");
17524 }
17525 "#
17526 .unindent(),
17527 );
17528}
17529
17530#[gpui::test]
17531async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17532 init_test(cx, |_| {});
17533
17534 let mut cx = EditorTestContext::new(cx).await;
17535
17536 let base_text = r#"
17537 one
17538 two
17539 three
17540 four
17541 five
17542 "#
17543 .unindent();
17544 executor.run_until_parked();
17545 cx.set_state(
17546 &r#"
17547 one
17548 two
17549 fˇour
17550 five
17551 "#
17552 .unindent(),
17553 );
17554
17555 cx.set_head_text(&base_text);
17556 executor.run_until_parked();
17557
17558 cx.update_editor(|editor, window, cx| {
17559 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17560 });
17561 executor.run_until_parked();
17562
17563 cx.assert_state_with_diff(
17564 r#"
17565 one
17566 two
17567 - three
17568 fˇour
17569 five
17570 "#
17571 .unindent(),
17572 );
17573
17574 cx.update_editor(|editor, window, cx| {
17575 editor.backspace(&Backspace, window, cx);
17576 editor.backspace(&Backspace, window, cx);
17577 });
17578 executor.run_until_parked();
17579 cx.assert_state_with_diff(
17580 r#"
17581 one
17582 two
17583 - threeˇ
17584 - four
17585 + our
17586 five
17587 "#
17588 .unindent(),
17589 );
17590}
17591
17592#[gpui::test]
17593async fn test_edit_after_expanded_modification_hunk(
17594 executor: BackgroundExecutor,
17595 cx: &mut TestAppContext,
17596) {
17597 init_test(cx, |_| {});
17598
17599 let mut cx = EditorTestContext::new(cx).await;
17600
17601 let diff_base = r#"
17602 use some::mod1;
17603 use some::mod2;
17604
17605 const A: u32 = 42;
17606 const B: u32 = 42;
17607 const C: u32 = 42;
17608 const D: u32 = 42;
17609
17610
17611 fn main() {
17612 println!("hello");
17613
17614 println!("world");
17615 }"#
17616 .unindent();
17617
17618 cx.set_state(
17619 &r#"
17620 use some::mod1;
17621 use some::mod2;
17622
17623 const A: u32 = 42;
17624 const B: u32 = 42;
17625 const C: u32 = 43ˇ
17626 const D: u32 = 42;
17627
17628
17629 fn main() {
17630 println!("hello");
17631
17632 println!("world");
17633 }"#
17634 .unindent(),
17635 );
17636
17637 cx.set_head_text(&diff_base);
17638 executor.run_until_parked();
17639 cx.update_editor(|editor, window, cx| {
17640 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17641 });
17642 executor.run_until_parked();
17643
17644 cx.assert_state_with_diff(
17645 r#"
17646 use some::mod1;
17647 use some::mod2;
17648
17649 const A: u32 = 42;
17650 const B: u32 = 42;
17651 - const C: u32 = 42;
17652 + const C: u32 = 43ˇ
17653 const D: u32 = 42;
17654
17655
17656 fn main() {
17657 println!("hello");
17658
17659 println!("world");
17660 }"#
17661 .unindent(),
17662 );
17663
17664 cx.update_editor(|editor, window, cx| {
17665 editor.handle_input("\nnew_line\n", window, cx);
17666 });
17667 executor.run_until_parked();
17668
17669 cx.assert_state_with_diff(
17670 r#"
17671 use some::mod1;
17672 use some::mod2;
17673
17674 const A: u32 = 42;
17675 const B: u32 = 42;
17676 - const C: u32 = 42;
17677 + const C: u32 = 43
17678 + new_line
17679 + ˇ
17680 const D: u32 = 42;
17681
17682
17683 fn main() {
17684 println!("hello");
17685
17686 println!("world");
17687 }"#
17688 .unindent(),
17689 );
17690}
17691
17692#[gpui::test]
17693async fn test_stage_and_unstage_added_file_hunk(
17694 executor: BackgroundExecutor,
17695 cx: &mut TestAppContext,
17696) {
17697 init_test(cx, |_| {});
17698
17699 let mut cx = EditorTestContext::new(cx).await;
17700 cx.update_editor(|editor, _, cx| {
17701 editor.set_expand_all_diff_hunks(cx);
17702 });
17703
17704 let working_copy = r#"
17705 ˇfn main() {
17706 println!("hello, world!");
17707 }
17708 "#
17709 .unindent();
17710
17711 cx.set_state(&working_copy);
17712 executor.run_until_parked();
17713
17714 cx.assert_state_with_diff(
17715 r#"
17716 + ˇfn main() {
17717 + println!("hello, world!");
17718 + }
17719 "#
17720 .unindent(),
17721 );
17722 cx.assert_index_text(None);
17723
17724 cx.update_editor(|editor, window, cx| {
17725 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17726 });
17727 executor.run_until_parked();
17728 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
17729 cx.assert_state_with_diff(
17730 r#"
17731 + ˇfn main() {
17732 + println!("hello, world!");
17733 + }
17734 "#
17735 .unindent(),
17736 );
17737
17738 cx.update_editor(|editor, window, cx| {
17739 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17740 });
17741 executor.run_until_parked();
17742 cx.assert_index_text(None);
17743}
17744
17745async fn setup_indent_guides_editor(
17746 text: &str,
17747 cx: &mut TestAppContext,
17748) -> (BufferId, EditorTestContext) {
17749 init_test(cx, |_| {});
17750
17751 let mut cx = EditorTestContext::new(cx).await;
17752
17753 let buffer_id = cx.update_editor(|editor, window, cx| {
17754 editor.set_text(text, window, cx);
17755 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
17756
17757 buffer_ids[0]
17758 });
17759
17760 (buffer_id, cx)
17761}
17762
17763fn assert_indent_guides(
17764 range: Range<u32>,
17765 expected: Vec<IndentGuide>,
17766 active_indices: Option<Vec<usize>>,
17767 cx: &mut EditorTestContext,
17768) {
17769 let indent_guides = cx.update_editor(|editor, window, cx| {
17770 let snapshot = editor.snapshot(window, cx).display_snapshot;
17771 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
17772 editor,
17773 MultiBufferRow(range.start)..MultiBufferRow(range.end),
17774 true,
17775 &snapshot,
17776 cx,
17777 );
17778
17779 indent_guides.sort_by(|a, b| {
17780 a.depth.cmp(&b.depth).then(
17781 a.start_row
17782 .cmp(&b.start_row)
17783 .then(a.end_row.cmp(&b.end_row)),
17784 )
17785 });
17786 indent_guides
17787 });
17788
17789 if let Some(expected) = active_indices {
17790 let active_indices = cx.update_editor(|editor, window, cx| {
17791 let snapshot = editor.snapshot(window, cx).display_snapshot;
17792 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
17793 });
17794
17795 assert_eq!(
17796 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
17797 expected,
17798 "Active indent guide indices do not match"
17799 );
17800 }
17801
17802 assert_eq!(indent_guides, expected, "Indent guides do not match");
17803}
17804
17805fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
17806 IndentGuide {
17807 buffer_id,
17808 start_row: MultiBufferRow(start_row),
17809 end_row: MultiBufferRow(end_row),
17810 depth,
17811 tab_size: 4,
17812 settings: IndentGuideSettings {
17813 enabled: true,
17814 line_width: 1,
17815 active_line_width: 1,
17816 ..Default::default()
17817 },
17818 }
17819}
17820
17821#[gpui::test]
17822async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
17823 let (buffer_id, mut cx) = setup_indent_guides_editor(
17824 &"
17825 fn main() {
17826 let a = 1;
17827 }"
17828 .unindent(),
17829 cx,
17830 )
17831 .await;
17832
17833 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
17834}
17835
17836#[gpui::test]
17837async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
17838 let (buffer_id, mut cx) = setup_indent_guides_editor(
17839 &"
17840 fn main() {
17841 let a = 1;
17842 let b = 2;
17843 }"
17844 .unindent(),
17845 cx,
17846 )
17847 .await;
17848
17849 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
17850}
17851
17852#[gpui::test]
17853async fn test_indent_guide_nested(cx: &mut TestAppContext) {
17854 let (buffer_id, mut cx) = setup_indent_guides_editor(
17855 &"
17856 fn main() {
17857 let a = 1;
17858 if a == 3 {
17859 let b = 2;
17860 } else {
17861 let c = 3;
17862 }
17863 }"
17864 .unindent(),
17865 cx,
17866 )
17867 .await;
17868
17869 assert_indent_guides(
17870 0..8,
17871 vec![
17872 indent_guide(buffer_id, 1, 6, 0),
17873 indent_guide(buffer_id, 3, 3, 1),
17874 indent_guide(buffer_id, 5, 5, 1),
17875 ],
17876 None,
17877 &mut cx,
17878 );
17879}
17880
17881#[gpui::test]
17882async fn test_indent_guide_tab(cx: &mut TestAppContext) {
17883 let (buffer_id, mut cx) = setup_indent_guides_editor(
17884 &"
17885 fn main() {
17886 let a = 1;
17887 let b = 2;
17888 let c = 3;
17889 }"
17890 .unindent(),
17891 cx,
17892 )
17893 .await;
17894
17895 assert_indent_guides(
17896 0..5,
17897 vec![
17898 indent_guide(buffer_id, 1, 3, 0),
17899 indent_guide(buffer_id, 2, 2, 1),
17900 ],
17901 None,
17902 &mut cx,
17903 );
17904}
17905
17906#[gpui::test]
17907async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
17908 let (buffer_id, mut cx) = setup_indent_guides_editor(
17909 &"
17910 fn main() {
17911 let a = 1;
17912
17913 let c = 3;
17914 }"
17915 .unindent(),
17916 cx,
17917 )
17918 .await;
17919
17920 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
17921}
17922
17923#[gpui::test]
17924async fn test_indent_guide_complex(cx: &mut TestAppContext) {
17925 let (buffer_id, mut cx) = setup_indent_guides_editor(
17926 &"
17927 fn main() {
17928 let a = 1;
17929
17930 let c = 3;
17931
17932 if a == 3 {
17933 let b = 2;
17934 } else {
17935 let c = 3;
17936 }
17937 }"
17938 .unindent(),
17939 cx,
17940 )
17941 .await;
17942
17943 assert_indent_guides(
17944 0..11,
17945 vec![
17946 indent_guide(buffer_id, 1, 9, 0),
17947 indent_guide(buffer_id, 6, 6, 1),
17948 indent_guide(buffer_id, 8, 8, 1),
17949 ],
17950 None,
17951 &mut cx,
17952 );
17953}
17954
17955#[gpui::test]
17956async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
17957 let (buffer_id, mut cx) = setup_indent_guides_editor(
17958 &"
17959 fn main() {
17960 let a = 1;
17961
17962 let c = 3;
17963
17964 if a == 3 {
17965 let b = 2;
17966 } else {
17967 let c = 3;
17968 }
17969 }"
17970 .unindent(),
17971 cx,
17972 )
17973 .await;
17974
17975 assert_indent_guides(
17976 1..11,
17977 vec![
17978 indent_guide(buffer_id, 1, 9, 0),
17979 indent_guide(buffer_id, 6, 6, 1),
17980 indent_guide(buffer_id, 8, 8, 1),
17981 ],
17982 None,
17983 &mut cx,
17984 );
17985}
17986
17987#[gpui::test]
17988async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
17989 let (buffer_id, mut cx) = setup_indent_guides_editor(
17990 &"
17991 fn main() {
17992 let a = 1;
17993
17994 let c = 3;
17995
17996 if a == 3 {
17997 let b = 2;
17998 } else {
17999 let c = 3;
18000 }
18001 }"
18002 .unindent(),
18003 cx,
18004 )
18005 .await;
18006
18007 assert_indent_guides(
18008 1..10,
18009 vec![
18010 indent_guide(buffer_id, 1, 9, 0),
18011 indent_guide(buffer_id, 6, 6, 1),
18012 indent_guide(buffer_id, 8, 8, 1),
18013 ],
18014 None,
18015 &mut cx,
18016 );
18017}
18018
18019#[gpui::test]
18020async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
18021 let (buffer_id, mut cx) = setup_indent_guides_editor(
18022 &"
18023 fn main() {
18024 if a {
18025 b(
18026 c,
18027 d,
18028 )
18029 } else {
18030 e(
18031 f
18032 )
18033 }
18034 }"
18035 .unindent(),
18036 cx,
18037 )
18038 .await;
18039
18040 assert_indent_guides(
18041 0..11,
18042 vec![
18043 indent_guide(buffer_id, 1, 10, 0),
18044 indent_guide(buffer_id, 2, 5, 1),
18045 indent_guide(buffer_id, 7, 9, 1),
18046 indent_guide(buffer_id, 3, 4, 2),
18047 indent_guide(buffer_id, 8, 8, 2),
18048 ],
18049 None,
18050 &mut cx,
18051 );
18052
18053 cx.update_editor(|editor, window, cx| {
18054 editor.fold_at(MultiBufferRow(2), window, cx);
18055 assert_eq!(
18056 editor.display_text(cx),
18057 "
18058 fn main() {
18059 if a {
18060 b(⋯
18061 )
18062 } else {
18063 e(
18064 f
18065 )
18066 }
18067 }"
18068 .unindent()
18069 );
18070 });
18071
18072 assert_indent_guides(
18073 0..11,
18074 vec![
18075 indent_guide(buffer_id, 1, 10, 0),
18076 indent_guide(buffer_id, 2, 5, 1),
18077 indent_guide(buffer_id, 7, 9, 1),
18078 indent_guide(buffer_id, 8, 8, 2),
18079 ],
18080 None,
18081 &mut cx,
18082 );
18083}
18084
18085#[gpui::test]
18086async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
18087 let (buffer_id, mut cx) = setup_indent_guides_editor(
18088 &"
18089 block1
18090 block2
18091 block3
18092 block4
18093 block2
18094 block1
18095 block1"
18096 .unindent(),
18097 cx,
18098 )
18099 .await;
18100
18101 assert_indent_guides(
18102 1..10,
18103 vec![
18104 indent_guide(buffer_id, 1, 4, 0),
18105 indent_guide(buffer_id, 2, 3, 1),
18106 indent_guide(buffer_id, 3, 3, 2),
18107 ],
18108 None,
18109 &mut cx,
18110 );
18111}
18112
18113#[gpui::test]
18114async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
18115 let (buffer_id, mut cx) = setup_indent_guides_editor(
18116 &"
18117 block1
18118 block2
18119 block3
18120
18121 block1
18122 block1"
18123 .unindent(),
18124 cx,
18125 )
18126 .await;
18127
18128 assert_indent_guides(
18129 0..6,
18130 vec![
18131 indent_guide(buffer_id, 1, 2, 0),
18132 indent_guide(buffer_id, 2, 2, 1),
18133 ],
18134 None,
18135 &mut cx,
18136 );
18137}
18138
18139#[gpui::test]
18140async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
18141 let (buffer_id, mut cx) = setup_indent_guides_editor(
18142 &"
18143 function component() {
18144 \treturn (
18145 \t\t\t
18146 \t\t<div>
18147 \t\t\t<abc></abc>
18148 \t\t</div>
18149 \t)
18150 }"
18151 .unindent(),
18152 cx,
18153 )
18154 .await;
18155
18156 assert_indent_guides(
18157 0..8,
18158 vec![
18159 indent_guide(buffer_id, 1, 6, 0),
18160 indent_guide(buffer_id, 2, 5, 1),
18161 indent_guide(buffer_id, 4, 4, 2),
18162 ],
18163 None,
18164 &mut cx,
18165 );
18166}
18167
18168#[gpui::test]
18169async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
18170 let (buffer_id, mut cx) = setup_indent_guides_editor(
18171 &"
18172 function component() {
18173 \treturn (
18174 \t
18175 \t\t<div>
18176 \t\t\t<abc></abc>
18177 \t\t</div>
18178 \t)
18179 }"
18180 .unindent(),
18181 cx,
18182 )
18183 .await;
18184
18185 assert_indent_guides(
18186 0..8,
18187 vec![
18188 indent_guide(buffer_id, 1, 6, 0),
18189 indent_guide(buffer_id, 2, 5, 1),
18190 indent_guide(buffer_id, 4, 4, 2),
18191 ],
18192 None,
18193 &mut cx,
18194 );
18195}
18196
18197#[gpui::test]
18198async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
18199 let (buffer_id, mut cx) = setup_indent_guides_editor(
18200 &"
18201 block1
18202
18203
18204
18205 block2
18206 "
18207 .unindent(),
18208 cx,
18209 )
18210 .await;
18211
18212 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18213}
18214
18215#[gpui::test]
18216async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
18217 let (buffer_id, mut cx) = setup_indent_guides_editor(
18218 &"
18219 def a:
18220 \tb = 3
18221 \tif True:
18222 \t\tc = 4
18223 \t\td = 5
18224 \tprint(b)
18225 "
18226 .unindent(),
18227 cx,
18228 )
18229 .await;
18230
18231 assert_indent_guides(
18232 0..6,
18233 vec![
18234 indent_guide(buffer_id, 1, 5, 0),
18235 indent_guide(buffer_id, 3, 4, 1),
18236 ],
18237 None,
18238 &mut cx,
18239 );
18240}
18241
18242#[gpui::test]
18243async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
18244 let (buffer_id, mut cx) = setup_indent_guides_editor(
18245 &"
18246 fn main() {
18247 let a = 1;
18248 }"
18249 .unindent(),
18250 cx,
18251 )
18252 .await;
18253
18254 cx.update_editor(|editor, window, cx| {
18255 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18256 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18257 });
18258 });
18259
18260 assert_indent_guides(
18261 0..3,
18262 vec![indent_guide(buffer_id, 1, 1, 0)],
18263 Some(vec![0]),
18264 &mut cx,
18265 );
18266}
18267
18268#[gpui::test]
18269async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
18270 let (buffer_id, mut cx) = setup_indent_guides_editor(
18271 &"
18272 fn main() {
18273 if 1 == 2 {
18274 let a = 1;
18275 }
18276 }"
18277 .unindent(),
18278 cx,
18279 )
18280 .await;
18281
18282 cx.update_editor(|editor, window, cx| {
18283 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18284 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18285 });
18286 });
18287
18288 assert_indent_guides(
18289 0..4,
18290 vec![
18291 indent_guide(buffer_id, 1, 3, 0),
18292 indent_guide(buffer_id, 2, 2, 1),
18293 ],
18294 Some(vec![1]),
18295 &mut cx,
18296 );
18297
18298 cx.update_editor(|editor, window, cx| {
18299 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18300 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18301 });
18302 });
18303
18304 assert_indent_guides(
18305 0..4,
18306 vec![
18307 indent_guide(buffer_id, 1, 3, 0),
18308 indent_guide(buffer_id, 2, 2, 1),
18309 ],
18310 Some(vec![1]),
18311 &mut cx,
18312 );
18313
18314 cx.update_editor(|editor, window, cx| {
18315 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18316 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
18317 });
18318 });
18319
18320 assert_indent_guides(
18321 0..4,
18322 vec![
18323 indent_guide(buffer_id, 1, 3, 0),
18324 indent_guide(buffer_id, 2, 2, 1),
18325 ],
18326 Some(vec![0]),
18327 &mut cx,
18328 );
18329}
18330
18331#[gpui::test]
18332async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
18333 let (buffer_id, mut cx) = setup_indent_guides_editor(
18334 &"
18335 fn main() {
18336 let a = 1;
18337
18338 let b = 2;
18339 }"
18340 .unindent(),
18341 cx,
18342 )
18343 .await;
18344
18345 cx.update_editor(|editor, window, cx| {
18346 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18347 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18348 });
18349 });
18350
18351 assert_indent_guides(
18352 0..5,
18353 vec![indent_guide(buffer_id, 1, 3, 0)],
18354 Some(vec![0]),
18355 &mut cx,
18356 );
18357}
18358
18359#[gpui::test]
18360async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
18361 let (buffer_id, mut cx) = setup_indent_guides_editor(
18362 &"
18363 def m:
18364 a = 1
18365 pass"
18366 .unindent(),
18367 cx,
18368 )
18369 .await;
18370
18371 cx.update_editor(|editor, window, cx| {
18372 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18373 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18374 });
18375 });
18376
18377 assert_indent_guides(
18378 0..3,
18379 vec![indent_guide(buffer_id, 1, 2, 0)],
18380 Some(vec![0]),
18381 &mut cx,
18382 );
18383}
18384
18385#[gpui::test]
18386async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
18387 init_test(cx, |_| {});
18388 let mut cx = EditorTestContext::new(cx).await;
18389 let text = indoc! {
18390 "
18391 impl A {
18392 fn b() {
18393 0;
18394 3;
18395 5;
18396 6;
18397 7;
18398 }
18399 }
18400 "
18401 };
18402 let base_text = indoc! {
18403 "
18404 impl A {
18405 fn b() {
18406 0;
18407 1;
18408 2;
18409 3;
18410 4;
18411 }
18412 fn c() {
18413 5;
18414 6;
18415 7;
18416 }
18417 }
18418 "
18419 };
18420
18421 cx.update_editor(|editor, window, cx| {
18422 editor.set_text(text, window, cx);
18423
18424 editor.buffer().update(cx, |multibuffer, cx| {
18425 let buffer = multibuffer.as_singleton().unwrap();
18426 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
18427
18428 multibuffer.set_all_diff_hunks_expanded(cx);
18429 multibuffer.add_diff(diff, cx);
18430
18431 buffer.read(cx).remote_id()
18432 })
18433 });
18434 cx.run_until_parked();
18435
18436 cx.assert_state_with_diff(
18437 indoc! { "
18438 impl A {
18439 fn b() {
18440 0;
18441 - 1;
18442 - 2;
18443 3;
18444 - 4;
18445 - }
18446 - fn c() {
18447 5;
18448 6;
18449 7;
18450 }
18451 }
18452 ˇ"
18453 }
18454 .to_string(),
18455 );
18456
18457 let mut actual_guides = cx.update_editor(|editor, window, cx| {
18458 editor
18459 .snapshot(window, cx)
18460 .buffer_snapshot
18461 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
18462 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
18463 .collect::<Vec<_>>()
18464 });
18465 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
18466 assert_eq!(
18467 actual_guides,
18468 vec![
18469 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
18470 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
18471 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
18472 ]
18473 );
18474}
18475
18476#[gpui::test]
18477async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18478 init_test(cx, |_| {});
18479 let mut cx = EditorTestContext::new(cx).await;
18480
18481 let diff_base = r#"
18482 a
18483 b
18484 c
18485 "#
18486 .unindent();
18487
18488 cx.set_state(
18489 &r#"
18490 ˇA
18491 b
18492 C
18493 "#
18494 .unindent(),
18495 );
18496 cx.set_head_text(&diff_base);
18497 cx.update_editor(|editor, window, cx| {
18498 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18499 });
18500 executor.run_until_parked();
18501
18502 let both_hunks_expanded = r#"
18503 - a
18504 + ˇA
18505 b
18506 - c
18507 + C
18508 "#
18509 .unindent();
18510
18511 cx.assert_state_with_diff(both_hunks_expanded.clone());
18512
18513 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18514 let snapshot = editor.snapshot(window, cx);
18515 let hunks = editor
18516 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18517 .collect::<Vec<_>>();
18518 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18519 let buffer_id = hunks[0].buffer_id;
18520 hunks
18521 .into_iter()
18522 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18523 .collect::<Vec<_>>()
18524 });
18525 assert_eq!(hunk_ranges.len(), 2);
18526
18527 cx.update_editor(|editor, _, cx| {
18528 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18529 });
18530 executor.run_until_parked();
18531
18532 let second_hunk_expanded = r#"
18533 ˇA
18534 b
18535 - c
18536 + C
18537 "#
18538 .unindent();
18539
18540 cx.assert_state_with_diff(second_hunk_expanded);
18541
18542 cx.update_editor(|editor, _, cx| {
18543 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18544 });
18545 executor.run_until_parked();
18546
18547 cx.assert_state_with_diff(both_hunks_expanded.clone());
18548
18549 cx.update_editor(|editor, _, cx| {
18550 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18551 });
18552 executor.run_until_parked();
18553
18554 let first_hunk_expanded = r#"
18555 - a
18556 + ˇA
18557 b
18558 C
18559 "#
18560 .unindent();
18561
18562 cx.assert_state_with_diff(first_hunk_expanded);
18563
18564 cx.update_editor(|editor, _, cx| {
18565 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18566 });
18567 executor.run_until_parked();
18568
18569 cx.assert_state_with_diff(both_hunks_expanded);
18570
18571 cx.set_state(
18572 &r#"
18573 ˇA
18574 b
18575 "#
18576 .unindent(),
18577 );
18578 cx.run_until_parked();
18579
18580 // TODO this cursor position seems bad
18581 cx.assert_state_with_diff(
18582 r#"
18583 - ˇa
18584 + A
18585 b
18586 "#
18587 .unindent(),
18588 );
18589
18590 cx.update_editor(|editor, window, cx| {
18591 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18592 });
18593
18594 cx.assert_state_with_diff(
18595 r#"
18596 - ˇa
18597 + A
18598 b
18599 - c
18600 "#
18601 .unindent(),
18602 );
18603
18604 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18605 let snapshot = editor.snapshot(window, cx);
18606 let hunks = editor
18607 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18608 .collect::<Vec<_>>();
18609 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18610 let buffer_id = hunks[0].buffer_id;
18611 hunks
18612 .into_iter()
18613 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18614 .collect::<Vec<_>>()
18615 });
18616 assert_eq!(hunk_ranges.len(), 2);
18617
18618 cx.update_editor(|editor, _, cx| {
18619 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18620 });
18621 executor.run_until_parked();
18622
18623 cx.assert_state_with_diff(
18624 r#"
18625 - ˇa
18626 + A
18627 b
18628 "#
18629 .unindent(),
18630 );
18631}
18632
18633#[gpui::test]
18634async fn test_toggle_deletion_hunk_at_start_of_file(
18635 executor: BackgroundExecutor,
18636 cx: &mut TestAppContext,
18637) {
18638 init_test(cx, |_| {});
18639 let mut cx = EditorTestContext::new(cx).await;
18640
18641 let diff_base = r#"
18642 a
18643 b
18644 c
18645 "#
18646 .unindent();
18647
18648 cx.set_state(
18649 &r#"
18650 ˇb
18651 c
18652 "#
18653 .unindent(),
18654 );
18655 cx.set_head_text(&diff_base);
18656 cx.update_editor(|editor, window, cx| {
18657 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18658 });
18659 executor.run_until_parked();
18660
18661 let hunk_expanded = r#"
18662 - a
18663 ˇb
18664 c
18665 "#
18666 .unindent();
18667
18668 cx.assert_state_with_diff(hunk_expanded.clone());
18669
18670 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18671 let snapshot = editor.snapshot(window, cx);
18672 let hunks = editor
18673 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18674 .collect::<Vec<_>>();
18675 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18676 let buffer_id = hunks[0].buffer_id;
18677 hunks
18678 .into_iter()
18679 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18680 .collect::<Vec<_>>()
18681 });
18682 assert_eq!(hunk_ranges.len(), 1);
18683
18684 cx.update_editor(|editor, _, cx| {
18685 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18686 });
18687 executor.run_until_parked();
18688
18689 let hunk_collapsed = r#"
18690 ˇb
18691 c
18692 "#
18693 .unindent();
18694
18695 cx.assert_state_with_diff(hunk_collapsed);
18696
18697 cx.update_editor(|editor, _, cx| {
18698 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18699 });
18700 executor.run_until_parked();
18701
18702 cx.assert_state_with_diff(hunk_expanded.clone());
18703}
18704
18705#[gpui::test]
18706async fn test_display_diff_hunks(cx: &mut TestAppContext) {
18707 init_test(cx, |_| {});
18708
18709 let fs = FakeFs::new(cx.executor());
18710 fs.insert_tree(
18711 path!("/test"),
18712 json!({
18713 ".git": {},
18714 "file-1": "ONE\n",
18715 "file-2": "TWO\n",
18716 "file-3": "THREE\n",
18717 }),
18718 )
18719 .await;
18720
18721 fs.set_head_for_repo(
18722 path!("/test/.git").as_ref(),
18723 &[
18724 ("file-1".into(), "one\n".into()),
18725 ("file-2".into(), "two\n".into()),
18726 ("file-3".into(), "three\n".into()),
18727 ],
18728 "deadbeef",
18729 );
18730
18731 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
18732 let mut buffers = vec![];
18733 for i in 1..=3 {
18734 let buffer = project
18735 .update(cx, |project, cx| {
18736 let path = format!(path!("/test/file-{}"), i);
18737 project.open_local_buffer(path, cx)
18738 })
18739 .await
18740 .unwrap();
18741 buffers.push(buffer);
18742 }
18743
18744 let multibuffer = cx.new(|cx| {
18745 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
18746 multibuffer.set_all_diff_hunks_expanded(cx);
18747 for buffer in &buffers {
18748 let snapshot = buffer.read(cx).snapshot();
18749 multibuffer.set_excerpts_for_path(
18750 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
18751 buffer.clone(),
18752 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
18753 DEFAULT_MULTIBUFFER_CONTEXT,
18754 cx,
18755 );
18756 }
18757 multibuffer
18758 });
18759
18760 let editor = cx.add_window(|window, cx| {
18761 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
18762 });
18763 cx.run_until_parked();
18764
18765 let snapshot = editor
18766 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18767 .unwrap();
18768 let hunks = snapshot
18769 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
18770 .map(|hunk| match hunk {
18771 DisplayDiffHunk::Unfolded {
18772 display_row_range, ..
18773 } => display_row_range,
18774 DisplayDiffHunk::Folded { .. } => unreachable!(),
18775 })
18776 .collect::<Vec<_>>();
18777 assert_eq!(
18778 hunks,
18779 [
18780 DisplayRow(2)..DisplayRow(4),
18781 DisplayRow(7)..DisplayRow(9),
18782 DisplayRow(12)..DisplayRow(14),
18783 ]
18784 );
18785}
18786
18787#[gpui::test]
18788async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
18789 init_test(cx, |_| {});
18790
18791 let mut cx = EditorTestContext::new(cx).await;
18792 cx.set_head_text(indoc! { "
18793 one
18794 two
18795 three
18796 four
18797 five
18798 "
18799 });
18800 cx.set_index_text(indoc! { "
18801 one
18802 two
18803 three
18804 four
18805 five
18806 "
18807 });
18808 cx.set_state(indoc! {"
18809 one
18810 TWO
18811 ˇTHREE
18812 FOUR
18813 five
18814 "});
18815 cx.run_until_parked();
18816 cx.update_editor(|editor, window, cx| {
18817 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18818 });
18819 cx.run_until_parked();
18820 cx.assert_index_text(Some(indoc! {"
18821 one
18822 TWO
18823 THREE
18824 FOUR
18825 five
18826 "}));
18827 cx.set_state(indoc! { "
18828 one
18829 TWO
18830 ˇTHREE-HUNDRED
18831 FOUR
18832 five
18833 "});
18834 cx.run_until_parked();
18835 cx.update_editor(|editor, window, cx| {
18836 let snapshot = editor.snapshot(window, cx);
18837 let hunks = editor
18838 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18839 .collect::<Vec<_>>();
18840 assert_eq!(hunks.len(), 1);
18841 assert_eq!(
18842 hunks[0].status(),
18843 DiffHunkStatus {
18844 kind: DiffHunkStatusKind::Modified,
18845 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
18846 }
18847 );
18848
18849 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18850 });
18851 cx.run_until_parked();
18852 cx.assert_index_text(Some(indoc! {"
18853 one
18854 TWO
18855 THREE-HUNDRED
18856 FOUR
18857 five
18858 "}));
18859}
18860
18861#[gpui::test]
18862fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
18863 init_test(cx, |_| {});
18864
18865 let editor = cx.add_window(|window, cx| {
18866 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
18867 build_editor(buffer, window, cx)
18868 });
18869
18870 let render_args = Arc::new(Mutex::new(None));
18871 let snapshot = editor
18872 .update(cx, |editor, window, cx| {
18873 let snapshot = editor.buffer().read(cx).snapshot(cx);
18874 let range =
18875 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
18876
18877 struct RenderArgs {
18878 row: MultiBufferRow,
18879 folded: bool,
18880 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
18881 }
18882
18883 let crease = Crease::inline(
18884 range,
18885 FoldPlaceholder::test(),
18886 {
18887 let toggle_callback = render_args.clone();
18888 move |row, folded, callback, _window, _cx| {
18889 *toggle_callback.lock() = Some(RenderArgs {
18890 row,
18891 folded,
18892 callback,
18893 });
18894 div()
18895 }
18896 },
18897 |_row, _folded, _window, _cx| div(),
18898 );
18899
18900 editor.insert_creases(Some(crease), cx);
18901 let snapshot = editor.snapshot(window, cx);
18902 let _div = snapshot.render_crease_toggle(
18903 MultiBufferRow(1),
18904 false,
18905 cx.entity().clone(),
18906 window,
18907 cx,
18908 );
18909 snapshot
18910 })
18911 .unwrap();
18912
18913 let render_args = render_args.lock().take().unwrap();
18914 assert_eq!(render_args.row, MultiBufferRow(1));
18915 assert!(!render_args.folded);
18916 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
18917
18918 cx.update_window(*editor, |_, window, cx| {
18919 (render_args.callback)(true, window, cx)
18920 })
18921 .unwrap();
18922 let snapshot = editor
18923 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18924 .unwrap();
18925 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
18926
18927 cx.update_window(*editor, |_, window, cx| {
18928 (render_args.callback)(false, window, cx)
18929 })
18930 .unwrap();
18931 let snapshot = editor
18932 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18933 .unwrap();
18934 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
18935}
18936
18937#[gpui::test]
18938async fn test_input_text(cx: &mut TestAppContext) {
18939 init_test(cx, |_| {});
18940 let mut cx = EditorTestContext::new(cx).await;
18941
18942 cx.set_state(
18943 &r#"ˇone
18944 two
18945
18946 three
18947 fourˇ
18948 five
18949
18950 siˇx"#
18951 .unindent(),
18952 );
18953
18954 cx.dispatch_action(HandleInput(String::new()));
18955 cx.assert_editor_state(
18956 &r#"ˇone
18957 two
18958
18959 three
18960 fourˇ
18961 five
18962
18963 siˇx"#
18964 .unindent(),
18965 );
18966
18967 cx.dispatch_action(HandleInput("AAAA".to_string()));
18968 cx.assert_editor_state(
18969 &r#"AAAAˇone
18970 two
18971
18972 three
18973 fourAAAAˇ
18974 five
18975
18976 siAAAAˇx"#
18977 .unindent(),
18978 );
18979}
18980
18981#[gpui::test]
18982async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
18983 init_test(cx, |_| {});
18984
18985 let mut cx = EditorTestContext::new(cx).await;
18986 cx.set_state(
18987 r#"let foo = 1;
18988let foo = 2;
18989let foo = 3;
18990let fooˇ = 4;
18991let foo = 5;
18992let foo = 6;
18993let foo = 7;
18994let foo = 8;
18995let foo = 9;
18996let foo = 10;
18997let foo = 11;
18998let foo = 12;
18999let foo = 13;
19000let foo = 14;
19001let foo = 15;"#,
19002 );
19003
19004 cx.update_editor(|e, window, cx| {
19005 assert_eq!(
19006 e.next_scroll_position,
19007 NextScrollCursorCenterTopBottom::Center,
19008 "Default next scroll direction is center",
19009 );
19010
19011 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19012 assert_eq!(
19013 e.next_scroll_position,
19014 NextScrollCursorCenterTopBottom::Top,
19015 "After center, next scroll direction should be top",
19016 );
19017
19018 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19019 assert_eq!(
19020 e.next_scroll_position,
19021 NextScrollCursorCenterTopBottom::Bottom,
19022 "After top, next scroll direction should be bottom",
19023 );
19024
19025 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19026 assert_eq!(
19027 e.next_scroll_position,
19028 NextScrollCursorCenterTopBottom::Center,
19029 "After bottom, scrolling should start over",
19030 );
19031
19032 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19033 assert_eq!(
19034 e.next_scroll_position,
19035 NextScrollCursorCenterTopBottom::Top,
19036 "Scrolling continues if retriggered fast enough"
19037 );
19038 });
19039
19040 cx.executor()
19041 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
19042 cx.executor().run_until_parked();
19043 cx.update_editor(|e, _, _| {
19044 assert_eq!(
19045 e.next_scroll_position,
19046 NextScrollCursorCenterTopBottom::Center,
19047 "If scrolling is not triggered fast enough, it should reset"
19048 );
19049 });
19050}
19051
19052#[gpui::test]
19053async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
19054 init_test(cx, |_| {});
19055 let mut cx = EditorLspTestContext::new_rust(
19056 lsp::ServerCapabilities {
19057 definition_provider: Some(lsp::OneOf::Left(true)),
19058 references_provider: Some(lsp::OneOf::Left(true)),
19059 ..lsp::ServerCapabilities::default()
19060 },
19061 cx,
19062 )
19063 .await;
19064
19065 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
19066 let go_to_definition = cx
19067 .lsp
19068 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19069 move |params, _| async move {
19070 if empty_go_to_definition {
19071 Ok(None)
19072 } else {
19073 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
19074 uri: params.text_document_position_params.text_document.uri,
19075 range: lsp::Range::new(
19076 lsp::Position::new(4, 3),
19077 lsp::Position::new(4, 6),
19078 ),
19079 })))
19080 }
19081 },
19082 );
19083 let references = cx
19084 .lsp
19085 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
19086 Ok(Some(vec![lsp::Location {
19087 uri: params.text_document_position.text_document.uri,
19088 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
19089 }]))
19090 });
19091 (go_to_definition, references)
19092 };
19093
19094 cx.set_state(
19095 &r#"fn one() {
19096 let mut a = ˇtwo();
19097 }
19098
19099 fn two() {}"#
19100 .unindent(),
19101 );
19102 set_up_lsp_handlers(false, &mut cx);
19103 let navigated = cx
19104 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19105 .await
19106 .expect("Failed to navigate to definition");
19107 assert_eq!(
19108 navigated,
19109 Navigated::Yes,
19110 "Should have navigated to definition from the GetDefinition response"
19111 );
19112 cx.assert_editor_state(
19113 &r#"fn one() {
19114 let mut a = two();
19115 }
19116
19117 fn «twoˇ»() {}"#
19118 .unindent(),
19119 );
19120
19121 let editors = cx.update_workspace(|workspace, _, cx| {
19122 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19123 });
19124 cx.update_editor(|_, _, test_editor_cx| {
19125 assert_eq!(
19126 editors.len(),
19127 1,
19128 "Initially, only one, test, editor should be open in the workspace"
19129 );
19130 assert_eq!(
19131 test_editor_cx.entity(),
19132 editors.last().expect("Asserted len is 1").clone()
19133 );
19134 });
19135
19136 set_up_lsp_handlers(true, &mut cx);
19137 let navigated = cx
19138 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19139 .await
19140 .expect("Failed to navigate to lookup references");
19141 assert_eq!(
19142 navigated,
19143 Navigated::Yes,
19144 "Should have navigated to references as a fallback after empty GoToDefinition response"
19145 );
19146 // We should not change the selections in the existing file,
19147 // if opening another milti buffer with the references
19148 cx.assert_editor_state(
19149 &r#"fn one() {
19150 let mut a = two();
19151 }
19152
19153 fn «twoˇ»() {}"#
19154 .unindent(),
19155 );
19156 let editors = cx.update_workspace(|workspace, _, cx| {
19157 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19158 });
19159 cx.update_editor(|_, _, test_editor_cx| {
19160 assert_eq!(
19161 editors.len(),
19162 2,
19163 "After falling back to references search, we open a new editor with the results"
19164 );
19165 let references_fallback_text = editors
19166 .into_iter()
19167 .find(|new_editor| *new_editor != test_editor_cx.entity())
19168 .expect("Should have one non-test editor now")
19169 .read(test_editor_cx)
19170 .text(test_editor_cx);
19171 assert_eq!(
19172 references_fallback_text, "fn one() {\n let mut a = two();\n}",
19173 "Should use the range from the references response and not the GoToDefinition one"
19174 );
19175 });
19176}
19177
19178#[gpui::test]
19179async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
19180 init_test(cx, |_| {});
19181 cx.update(|cx| {
19182 let mut editor_settings = EditorSettings::get_global(cx).clone();
19183 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
19184 EditorSettings::override_global(editor_settings, cx);
19185 });
19186 let mut cx = EditorLspTestContext::new_rust(
19187 lsp::ServerCapabilities {
19188 definition_provider: Some(lsp::OneOf::Left(true)),
19189 references_provider: Some(lsp::OneOf::Left(true)),
19190 ..lsp::ServerCapabilities::default()
19191 },
19192 cx,
19193 )
19194 .await;
19195 let original_state = r#"fn one() {
19196 let mut a = ˇtwo();
19197 }
19198
19199 fn two() {}"#
19200 .unindent();
19201 cx.set_state(&original_state);
19202
19203 let mut go_to_definition = cx
19204 .lsp
19205 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19206 move |_, _| async move { Ok(None) },
19207 );
19208 let _references = cx
19209 .lsp
19210 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
19211 panic!("Should not call for references with no go to definition fallback")
19212 });
19213
19214 let navigated = cx
19215 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19216 .await
19217 .expect("Failed to navigate to lookup references");
19218 go_to_definition
19219 .next()
19220 .await
19221 .expect("Should have called the go_to_definition handler");
19222
19223 assert_eq!(
19224 navigated,
19225 Navigated::No,
19226 "Should have navigated to references as a fallback after empty GoToDefinition response"
19227 );
19228 cx.assert_editor_state(&original_state);
19229 let editors = cx.update_workspace(|workspace, _, cx| {
19230 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19231 });
19232 cx.update_editor(|_, _, _| {
19233 assert_eq!(
19234 editors.len(),
19235 1,
19236 "After unsuccessful fallback, no other editor should have been opened"
19237 );
19238 });
19239}
19240
19241#[gpui::test]
19242async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
19243 init_test(cx, |_| {});
19244
19245 let language = Arc::new(Language::new(
19246 LanguageConfig::default(),
19247 Some(tree_sitter_rust::LANGUAGE.into()),
19248 ));
19249
19250 let text = r#"
19251 #[cfg(test)]
19252 mod tests() {
19253 #[test]
19254 fn runnable_1() {
19255 let a = 1;
19256 }
19257
19258 #[test]
19259 fn runnable_2() {
19260 let a = 1;
19261 let b = 2;
19262 }
19263 }
19264 "#
19265 .unindent();
19266
19267 let fs = FakeFs::new(cx.executor());
19268 fs.insert_file("/file.rs", Default::default()).await;
19269
19270 let project = Project::test(fs, ["/a".as_ref()], cx).await;
19271 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19272 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19273 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
19274 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
19275
19276 let editor = cx.new_window_entity(|window, cx| {
19277 Editor::new(
19278 EditorMode::full(),
19279 multi_buffer,
19280 Some(project.clone()),
19281 window,
19282 cx,
19283 )
19284 });
19285
19286 editor.update_in(cx, |editor, window, cx| {
19287 let snapshot = editor.buffer().read(cx).snapshot(cx);
19288 editor.tasks.insert(
19289 (buffer.read(cx).remote_id(), 3),
19290 RunnableTasks {
19291 templates: vec![],
19292 offset: snapshot.anchor_before(43),
19293 column: 0,
19294 extra_variables: HashMap::default(),
19295 context_range: BufferOffset(43)..BufferOffset(85),
19296 },
19297 );
19298 editor.tasks.insert(
19299 (buffer.read(cx).remote_id(), 8),
19300 RunnableTasks {
19301 templates: vec![],
19302 offset: snapshot.anchor_before(86),
19303 column: 0,
19304 extra_variables: HashMap::default(),
19305 context_range: BufferOffset(86)..BufferOffset(191),
19306 },
19307 );
19308
19309 // Test finding task when cursor is inside function body
19310 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19311 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
19312 });
19313 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19314 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
19315
19316 // Test finding task when cursor is on function name
19317 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19318 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
19319 });
19320 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19321 assert_eq!(row, 8, "Should find task when cursor is on function name");
19322 });
19323}
19324
19325#[gpui::test]
19326async fn test_folding_buffers(cx: &mut TestAppContext) {
19327 init_test(cx, |_| {});
19328
19329 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
19330 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
19331 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
19332
19333 let fs = FakeFs::new(cx.executor());
19334 fs.insert_tree(
19335 path!("/a"),
19336 json!({
19337 "first.rs": sample_text_1,
19338 "second.rs": sample_text_2,
19339 "third.rs": sample_text_3,
19340 }),
19341 )
19342 .await;
19343 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19344 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19345 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19346 let worktree = project.update(cx, |project, cx| {
19347 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19348 assert_eq!(worktrees.len(), 1);
19349 worktrees.pop().unwrap()
19350 });
19351 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19352
19353 let buffer_1 = project
19354 .update(cx, |project, cx| {
19355 project.open_buffer((worktree_id, "first.rs"), cx)
19356 })
19357 .await
19358 .unwrap();
19359 let buffer_2 = project
19360 .update(cx, |project, cx| {
19361 project.open_buffer((worktree_id, "second.rs"), cx)
19362 })
19363 .await
19364 .unwrap();
19365 let buffer_3 = project
19366 .update(cx, |project, cx| {
19367 project.open_buffer((worktree_id, "third.rs"), cx)
19368 })
19369 .await
19370 .unwrap();
19371
19372 let multi_buffer = cx.new(|cx| {
19373 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19374 multi_buffer.push_excerpts(
19375 buffer_1.clone(),
19376 [
19377 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19378 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19379 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19380 ],
19381 cx,
19382 );
19383 multi_buffer.push_excerpts(
19384 buffer_2.clone(),
19385 [
19386 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19387 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19388 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19389 ],
19390 cx,
19391 );
19392 multi_buffer.push_excerpts(
19393 buffer_3.clone(),
19394 [
19395 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19396 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19397 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19398 ],
19399 cx,
19400 );
19401 multi_buffer
19402 });
19403 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19404 Editor::new(
19405 EditorMode::full(),
19406 multi_buffer.clone(),
19407 Some(project.clone()),
19408 window,
19409 cx,
19410 )
19411 });
19412
19413 assert_eq!(
19414 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19415 "\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",
19416 );
19417
19418 multi_buffer_editor.update(cx, |editor, cx| {
19419 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
19420 });
19421 assert_eq!(
19422 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19423 "\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",
19424 "After folding the first buffer, its text should not be displayed"
19425 );
19426
19427 multi_buffer_editor.update(cx, |editor, cx| {
19428 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
19429 });
19430 assert_eq!(
19431 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19432 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
19433 "After folding the second buffer, its text should not be displayed"
19434 );
19435
19436 multi_buffer_editor.update(cx, |editor, cx| {
19437 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
19438 });
19439 assert_eq!(
19440 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19441 "\n\n\n\n\n",
19442 "After folding the third buffer, its text should not be displayed"
19443 );
19444
19445 // Emulate selection inside the fold logic, that should work
19446 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19447 editor
19448 .snapshot(window, cx)
19449 .next_line_boundary(Point::new(0, 4));
19450 });
19451
19452 multi_buffer_editor.update(cx, |editor, cx| {
19453 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
19454 });
19455 assert_eq!(
19456 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19457 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19458 "After unfolding the second buffer, its text should be displayed"
19459 );
19460
19461 // Typing inside of buffer 1 causes that buffer to be unfolded.
19462 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19463 assert_eq!(
19464 multi_buffer
19465 .read(cx)
19466 .snapshot(cx)
19467 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
19468 .collect::<String>(),
19469 "bbbb"
19470 );
19471 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
19472 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
19473 });
19474 editor.handle_input("B", window, cx);
19475 });
19476
19477 assert_eq!(
19478 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19479 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19480 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
19481 );
19482
19483 multi_buffer_editor.update(cx, |editor, cx| {
19484 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
19485 });
19486 assert_eq!(
19487 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19488 "\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",
19489 "After unfolding the all buffers, all original text should be displayed"
19490 );
19491}
19492
19493#[gpui::test]
19494async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
19495 init_test(cx, |_| {});
19496
19497 let sample_text_1 = "1111\n2222\n3333".to_string();
19498 let sample_text_2 = "4444\n5555\n6666".to_string();
19499 let sample_text_3 = "7777\n8888\n9999".to_string();
19500
19501 let fs = FakeFs::new(cx.executor());
19502 fs.insert_tree(
19503 path!("/a"),
19504 json!({
19505 "first.rs": sample_text_1,
19506 "second.rs": sample_text_2,
19507 "third.rs": sample_text_3,
19508 }),
19509 )
19510 .await;
19511 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19512 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19513 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19514 let worktree = project.update(cx, |project, cx| {
19515 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19516 assert_eq!(worktrees.len(), 1);
19517 worktrees.pop().unwrap()
19518 });
19519 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19520
19521 let buffer_1 = project
19522 .update(cx, |project, cx| {
19523 project.open_buffer((worktree_id, "first.rs"), cx)
19524 })
19525 .await
19526 .unwrap();
19527 let buffer_2 = project
19528 .update(cx, |project, cx| {
19529 project.open_buffer((worktree_id, "second.rs"), cx)
19530 })
19531 .await
19532 .unwrap();
19533 let buffer_3 = project
19534 .update(cx, |project, cx| {
19535 project.open_buffer((worktree_id, "third.rs"), cx)
19536 })
19537 .await
19538 .unwrap();
19539
19540 let multi_buffer = cx.new(|cx| {
19541 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19542 multi_buffer.push_excerpts(
19543 buffer_1.clone(),
19544 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19545 cx,
19546 );
19547 multi_buffer.push_excerpts(
19548 buffer_2.clone(),
19549 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19550 cx,
19551 );
19552 multi_buffer.push_excerpts(
19553 buffer_3.clone(),
19554 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19555 cx,
19556 );
19557 multi_buffer
19558 });
19559
19560 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19561 Editor::new(
19562 EditorMode::full(),
19563 multi_buffer,
19564 Some(project.clone()),
19565 window,
19566 cx,
19567 )
19568 });
19569
19570 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
19571 assert_eq!(
19572 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19573 full_text,
19574 );
19575
19576 multi_buffer_editor.update(cx, |editor, cx| {
19577 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
19578 });
19579 assert_eq!(
19580 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19581 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
19582 "After folding the first buffer, its text should not be displayed"
19583 );
19584
19585 multi_buffer_editor.update(cx, |editor, cx| {
19586 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
19587 });
19588
19589 assert_eq!(
19590 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19591 "\n\n\n\n\n\n7777\n8888\n9999",
19592 "After folding the second buffer, its text should not be displayed"
19593 );
19594
19595 multi_buffer_editor.update(cx, |editor, cx| {
19596 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
19597 });
19598 assert_eq!(
19599 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19600 "\n\n\n\n\n",
19601 "After folding the third buffer, its text should not be displayed"
19602 );
19603
19604 multi_buffer_editor.update(cx, |editor, cx| {
19605 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
19606 });
19607 assert_eq!(
19608 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19609 "\n\n\n\n4444\n5555\n6666\n\n",
19610 "After unfolding the second buffer, its text should be displayed"
19611 );
19612
19613 multi_buffer_editor.update(cx, |editor, cx| {
19614 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
19615 });
19616 assert_eq!(
19617 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19618 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
19619 "After unfolding the first buffer, its text should be displayed"
19620 );
19621
19622 multi_buffer_editor.update(cx, |editor, cx| {
19623 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
19624 });
19625 assert_eq!(
19626 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19627 full_text,
19628 "After unfolding all buffers, all original text should be displayed"
19629 );
19630}
19631
19632#[gpui::test]
19633async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
19634 init_test(cx, |_| {});
19635
19636 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
19637
19638 let fs = FakeFs::new(cx.executor());
19639 fs.insert_tree(
19640 path!("/a"),
19641 json!({
19642 "main.rs": sample_text,
19643 }),
19644 )
19645 .await;
19646 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19647 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19648 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19649 let worktree = project.update(cx, |project, cx| {
19650 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19651 assert_eq!(worktrees.len(), 1);
19652 worktrees.pop().unwrap()
19653 });
19654 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19655
19656 let buffer_1 = project
19657 .update(cx, |project, cx| {
19658 project.open_buffer((worktree_id, "main.rs"), cx)
19659 })
19660 .await
19661 .unwrap();
19662
19663 let multi_buffer = cx.new(|cx| {
19664 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19665 multi_buffer.push_excerpts(
19666 buffer_1.clone(),
19667 [ExcerptRange::new(
19668 Point::new(0, 0)
19669 ..Point::new(
19670 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
19671 0,
19672 ),
19673 )],
19674 cx,
19675 );
19676 multi_buffer
19677 });
19678 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19679 Editor::new(
19680 EditorMode::full(),
19681 multi_buffer,
19682 Some(project.clone()),
19683 window,
19684 cx,
19685 )
19686 });
19687
19688 let selection_range = Point::new(1, 0)..Point::new(2, 0);
19689 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19690 enum TestHighlight {}
19691 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
19692 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
19693 editor.highlight_text::<TestHighlight>(
19694 vec![highlight_range.clone()],
19695 HighlightStyle::color(Hsla::green()),
19696 cx,
19697 );
19698 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19699 s.select_ranges(Some(highlight_range))
19700 });
19701 });
19702
19703 let full_text = format!("\n\n{sample_text}");
19704 assert_eq!(
19705 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19706 full_text,
19707 );
19708}
19709
19710#[gpui::test]
19711async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
19712 init_test(cx, |_| {});
19713 cx.update(|cx| {
19714 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
19715 "keymaps/default-linux.json",
19716 cx,
19717 )
19718 .unwrap();
19719 cx.bind_keys(default_key_bindings);
19720 });
19721
19722 let (editor, cx) = cx.add_window_view(|window, cx| {
19723 let multi_buffer = MultiBuffer::build_multi(
19724 [
19725 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
19726 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
19727 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
19728 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
19729 ],
19730 cx,
19731 );
19732 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
19733
19734 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
19735 // fold all but the second buffer, so that we test navigating between two
19736 // adjacent folded buffers, as well as folded buffers at the start and
19737 // end the multibuffer
19738 editor.fold_buffer(buffer_ids[0], cx);
19739 editor.fold_buffer(buffer_ids[2], cx);
19740 editor.fold_buffer(buffer_ids[3], cx);
19741
19742 editor
19743 });
19744 cx.simulate_resize(size(px(1000.), px(1000.)));
19745
19746 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
19747 cx.assert_excerpts_with_selections(indoc! {"
19748 [EXCERPT]
19749 ˇ[FOLDED]
19750 [EXCERPT]
19751 a1
19752 b1
19753 [EXCERPT]
19754 [FOLDED]
19755 [EXCERPT]
19756 [FOLDED]
19757 "
19758 });
19759 cx.simulate_keystroke("down");
19760 cx.assert_excerpts_with_selections(indoc! {"
19761 [EXCERPT]
19762 [FOLDED]
19763 [EXCERPT]
19764 ˇa1
19765 b1
19766 [EXCERPT]
19767 [FOLDED]
19768 [EXCERPT]
19769 [FOLDED]
19770 "
19771 });
19772 cx.simulate_keystroke("down");
19773 cx.assert_excerpts_with_selections(indoc! {"
19774 [EXCERPT]
19775 [FOLDED]
19776 [EXCERPT]
19777 a1
19778 ˇb1
19779 [EXCERPT]
19780 [FOLDED]
19781 [EXCERPT]
19782 [FOLDED]
19783 "
19784 });
19785 cx.simulate_keystroke("down");
19786 cx.assert_excerpts_with_selections(indoc! {"
19787 [EXCERPT]
19788 [FOLDED]
19789 [EXCERPT]
19790 a1
19791 b1
19792 ˇ[EXCERPT]
19793 [FOLDED]
19794 [EXCERPT]
19795 [FOLDED]
19796 "
19797 });
19798 cx.simulate_keystroke("down");
19799 cx.assert_excerpts_with_selections(indoc! {"
19800 [EXCERPT]
19801 [FOLDED]
19802 [EXCERPT]
19803 a1
19804 b1
19805 [EXCERPT]
19806 ˇ[FOLDED]
19807 [EXCERPT]
19808 [FOLDED]
19809 "
19810 });
19811 for _ in 0..5 {
19812 cx.simulate_keystroke("down");
19813 cx.assert_excerpts_with_selections(indoc! {"
19814 [EXCERPT]
19815 [FOLDED]
19816 [EXCERPT]
19817 a1
19818 b1
19819 [EXCERPT]
19820 [FOLDED]
19821 [EXCERPT]
19822 ˇ[FOLDED]
19823 "
19824 });
19825 }
19826
19827 cx.simulate_keystroke("up");
19828 cx.assert_excerpts_with_selections(indoc! {"
19829 [EXCERPT]
19830 [FOLDED]
19831 [EXCERPT]
19832 a1
19833 b1
19834 [EXCERPT]
19835 ˇ[FOLDED]
19836 [EXCERPT]
19837 [FOLDED]
19838 "
19839 });
19840 cx.simulate_keystroke("up");
19841 cx.assert_excerpts_with_selections(indoc! {"
19842 [EXCERPT]
19843 [FOLDED]
19844 [EXCERPT]
19845 a1
19846 b1
19847 ˇ[EXCERPT]
19848 [FOLDED]
19849 [EXCERPT]
19850 [FOLDED]
19851 "
19852 });
19853 cx.simulate_keystroke("up");
19854 cx.assert_excerpts_with_selections(indoc! {"
19855 [EXCERPT]
19856 [FOLDED]
19857 [EXCERPT]
19858 a1
19859 ˇb1
19860 [EXCERPT]
19861 [FOLDED]
19862 [EXCERPT]
19863 [FOLDED]
19864 "
19865 });
19866 cx.simulate_keystroke("up");
19867 cx.assert_excerpts_with_selections(indoc! {"
19868 [EXCERPT]
19869 [FOLDED]
19870 [EXCERPT]
19871 ˇa1
19872 b1
19873 [EXCERPT]
19874 [FOLDED]
19875 [EXCERPT]
19876 [FOLDED]
19877 "
19878 });
19879 for _ in 0..5 {
19880 cx.simulate_keystroke("up");
19881 cx.assert_excerpts_with_selections(indoc! {"
19882 [EXCERPT]
19883 ˇ[FOLDED]
19884 [EXCERPT]
19885 a1
19886 b1
19887 [EXCERPT]
19888 [FOLDED]
19889 [EXCERPT]
19890 [FOLDED]
19891 "
19892 });
19893 }
19894}
19895
19896#[gpui::test]
19897async fn test_inline_completion_text(cx: &mut TestAppContext) {
19898 init_test(cx, |_| {});
19899
19900 // Simple insertion
19901 assert_highlighted_edits(
19902 "Hello, world!",
19903 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
19904 true,
19905 cx,
19906 |highlighted_edits, cx| {
19907 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
19908 assert_eq!(highlighted_edits.highlights.len(), 1);
19909 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
19910 assert_eq!(
19911 highlighted_edits.highlights[0].1.background_color,
19912 Some(cx.theme().status().created_background)
19913 );
19914 },
19915 )
19916 .await;
19917
19918 // Replacement
19919 assert_highlighted_edits(
19920 "This is a test.",
19921 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
19922 false,
19923 cx,
19924 |highlighted_edits, cx| {
19925 assert_eq!(highlighted_edits.text, "That is a test.");
19926 assert_eq!(highlighted_edits.highlights.len(), 1);
19927 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
19928 assert_eq!(
19929 highlighted_edits.highlights[0].1.background_color,
19930 Some(cx.theme().status().created_background)
19931 );
19932 },
19933 )
19934 .await;
19935
19936 // Multiple edits
19937 assert_highlighted_edits(
19938 "Hello, world!",
19939 vec![
19940 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
19941 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
19942 ],
19943 false,
19944 cx,
19945 |highlighted_edits, cx| {
19946 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
19947 assert_eq!(highlighted_edits.highlights.len(), 2);
19948 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
19949 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
19950 assert_eq!(
19951 highlighted_edits.highlights[0].1.background_color,
19952 Some(cx.theme().status().created_background)
19953 );
19954 assert_eq!(
19955 highlighted_edits.highlights[1].1.background_color,
19956 Some(cx.theme().status().created_background)
19957 );
19958 },
19959 )
19960 .await;
19961
19962 // Multiple lines with edits
19963 assert_highlighted_edits(
19964 "First line\nSecond line\nThird line\nFourth line",
19965 vec![
19966 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
19967 (
19968 Point::new(2, 0)..Point::new(2, 10),
19969 "New third line".to_string(),
19970 ),
19971 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
19972 ],
19973 false,
19974 cx,
19975 |highlighted_edits, cx| {
19976 assert_eq!(
19977 highlighted_edits.text,
19978 "Second modified\nNew third line\nFourth updated line"
19979 );
19980 assert_eq!(highlighted_edits.highlights.len(), 3);
19981 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
19982 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
19983 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
19984 for highlight in &highlighted_edits.highlights {
19985 assert_eq!(
19986 highlight.1.background_color,
19987 Some(cx.theme().status().created_background)
19988 );
19989 }
19990 },
19991 )
19992 .await;
19993}
19994
19995#[gpui::test]
19996async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
19997 init_test(cx, |_| {});
19998
19999 // Deletion
20000 assert_highlighted_edits(
20001 "Hello, world!",
20002 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
20003 true,
20004 cx,
20005 |highlighted_edits, cx| {
20006 assert_eq!(highlighted_edits.text, "Hello, world!");
20007 assert_eq!(highlighted_edits.highlights.len(), 1);
20008 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
20009 assert_eq!(
20010 highlighted_edits.highlights[0].1.background_color,
20011 Some(cx.theme().status().deleted_background)
20012 );
20013 },
20014 )
20015 .await;
20016
20017 // Insertion
20018 assert_highlighted_edits(
20019 "Hello, world!",
20020 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
20021 true,
20022 cx,
20023 |highlighted_edits, cx| {
20024 assert_eq!(highlighted_edits.highlights.len(), 1);
20025 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
20026 assert_eq!(
20027 highlighted_edits.highlights[0].1.background_color,
20028 Some(cx.theme().status().created_background)
20029 );
20030 },
20031 )
20032 .await;
20033}
20034
20035async fn assert_highlighted_edits(
20036 text: &str,
20037 edits: Vec<(Range<Point>, String)>,
20038 include_deletions: bool,
20039 cx: &mut TestAppContext,
20040 assertion_fn: impl Fn(HighlightedText, &App),
20041) {
20042 let window = cx.add_window(|window, cx| {
20043 let buffer = MultiBuffer::build_simple(text, cx);
20044 Editor::new(EditorMode::full(), buffer, None, window, cx)
20045 });
20046 let cx = &mut VisualTestContext::from_window(*window, cx);
20047
20048 let (buffer, snapshot) = window
20049 .update(cx, |editor, _window, cx| {
20050 (
20051 editor.buffer().clone(),
20052 editor.buffer().read(cx).snapshot(cx),
20053 )
20054 })
20055 .unwrap();
20056
20057 let edits = edits
20058 .into_iter()
20059 .map(|(range, edit)| {
20060 (
20061 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
20062 edit,
20063 )
20064 })
20065 .collect::<Vec<_>>();
20066
20067 let text_anchor_edits = edits
20068 .clone()
20069 .into_iter()
20070 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
20071 .collect::<Vec<_>>();
20072
20073 let edit_preview = window
20074 .update(cx, |_, _window, cx| {
20075 buffer
20076 .read(cx)
20077 .as_singleton()
20078 .unwrap()
20079 .read(cx)
20080 .preview_edits(text_anchor_edits.into(), cx)
20081 })
20082 .unwrap()
20083 .await;
20084
20085 cx.update(|_window, cx| {
20086 let highlighted_edits = inline_completion_edit_text(
20087 &snapshot.as_singleton().unwrap().2,
20088 &edits,
20089 &edit_preview,
20090 include_deletions,
20091 cx,
20092 );
20093 assertion_fn(highlighted_edits, cx)
20094 });
20095}
20096
20097#[track_caller]
20098fn assert_breakpoint(
20099 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
20100 path: &Arc<Path>,
20101 expected: Vec<(u32, Breakpoint)>,
20102) {
20103 if expected.len() == 0usize {
20104 assert!(!breakpoints.contains_key(path), "{}", path.display());
20105 } else {
20106 let mut breakpoint = breakpoints
20107 .get(path)
20108 .unwrap()
20109 .into_iter()
20110 .map(|breakpoint| {
20111 (
20112 breakpoint.row,
20113 Breakpoint {
20114 message: breakpoint.message.clone(),
20115 state: breakpoint.state,
20116 condition: breakpoint.condition.clone(),
20117 hit_condition: breakpoint.hit_condition.clone(),
20118 },
20119 )
20120 })
20121 .collect::<Vec<_>>();
20122
20123 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
20124
20125 assert_eq!(expected, breakpoint);
20126 }
20127}
20128
20129fn add_log_breakpoint_at_cursor(
20130 editor: &mut Editor,
20131 log_message: &str,
20132 window: &mut Window,
20133 cx: &mut Context<Editor>,
20134) {
20135 let (anchor, bp) = editor
20136 .breakpoints_at_cursors(window, cx)
20137 .first()
20138 .and_then(|(anchor, bp)| {
20139 if let Some(bp) = bp {
20140 Some((*anchor, bp.clone()))
20141 } else {
20142 None
20143 }
20144 })
20145 .unwrap_or_else(|| {
20146 let cursor_position: Point = editor.selections.newest(cx).head();
20147
20148 let breakpoint_position = editor
20149 .snapshot(window, cx)
20150 .display_snapshot
20151 .buffer_snapshot
20152 .anchor_before(Point::new(cursor_position.row, 0));
20153
20154 (breakpoint_position, Breakpoint::new_log(&log_message))
20155 });
20156
20157 editor.edit_breakpoint_at_anchor(
20158 anchor,
20159 bp,
20160 BreakpointEditAction::EditLogMessage(log_message.into()),
20161 cx,
20162 );
20163}
20164
20165#[gpui::test]
20166async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
20167 init_test(cx, |_| {});
20168
20169 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20170 let fs = FakeFs::new(cx.executor());
20171 fs.insert_tree(
20172 path!("/a"),
20173 json!({
20174 "main.rs": sample_text,
20175 }),
20176 )
20177 .await;
20178 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20179 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20180 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20181
20182 let fs = FakeFs::new(cx.executor());
20183 fs.insert_tree(
20184 path!("/a"),
20185 json!({
20186 "main.rs": sample_text,
20187 }),
20188 )
20189 .await;
20190 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20191 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20192 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20193 let worktree_id = workspace
20194 .update(cx, |workspace, _window, cx| {
20195 workspace.project().update(cx, |project, cx| {
20196 project.worktrees(cx).next().unwrap().read(cx).id()
20197 })
20198 })
20199 .unwrap();
20200
20201 let buffer = project
20202 .update(cx, |project, cx| {
20203 project.open_buffer((worktree_id, "main.rs"), cx)
20204 })
20205 .await
20206 .unwrap();
20207
20208 let (editor, cx) = cx.add_window_view(|window, cx| {
20209 Editor::new(
20210 EditorMode::full(),
20211 MultiBuffer::build_from_buffer(buffer, cx),
20212 Some(project.clone()),
20213 window,
20214 cx,
20215 )
20216 });
20217
20218 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20219 let abs_path = project.read_with(cx, |project, cx| {
20220 project
20221 .absolute_path(&project_path, cx)
20222 .map(|path_buf| Arc::from(path_buf.to_owned()))
20223 .unwrap()
20224 });
20225
20226 // assert we can add breakpoint on the first line
20227 editor.update_in(cx, |editor, window, cx| {
20228 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20229 editor.move_to_end(&MoveToEnd, window, cx);
20230 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20231 });
20232
20233 let breakpoints = editor.update(cx, |editor, cx| {
20234 editor
20235 .breakpoint_store()
20236 .as_ref()
20237 .unwrap()
20238 .read(cx)
20239 .all_source_breakpoints(cx)
20240 .clone()
20241 });
20242
20243 assert_eq!(1, breakpoints.len());
20244 assert_breakpoint(
20245 &breakpoints,
20246 &abs_path,
20247 vec![
20248 (0, Breakpoint::new_standard()),
20249 (3, Breakpoint::new_standard()),
20250 ],
20251 );
20252
20253 editor.update_in(cx, |editor, window, cx| {
20254 editor.move_to_beginning(&MoveToBeginning, window, cx);
20255 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20256 });
20257
20258 let breakpoints = editor.update(cx, |editor, cx| {
20259 editor
20260 .breakpoint_store()
20261 .as_ref()
20262 .unwrap()
20263 .read(cx)
20264 .all_source_breakpoints(cx)
20265 .clone()
20266 });
20267
20268 assert_eq!(1, breakpoints.len());
20269 assert_breakpoint(
20270 &breakpoints,
20271 &abs_path,
20272 vec![(3, Breakpoint::new_standard())],
20273 );
20274
20275 editor.update_in(cx, |editor, window, cx| {
20276 editor.move_to_end(&MoveToEnd, window, cx);
20277 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20278 });
20279
20280 let breakpoints = editor.update(cx, |editor, cx| {
20281 editor
20282 .breakpoint_store()
20283 .as_ref()
20284 .unwrap()
20285 .read(cx)
20286 .all_source_breakpoints(cx)
20287 .clone()
20288 });
20289
20290 assert_eq!(0, breakpoints.len());
20291 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20292}
20293
20294#[gpui::test]
20295async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
20296 init_test(cx, |_| {});
20297
20298 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20299
20300 let fs = FakeFs::new(cx.executor());
20301 fs.insert_tree(
20302 path!("/a"),
20303 json!({
20304 "main.rs": sample_text,
20305 }),
20306 )
20307 .await;
20308 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20309 let (workspace, cx) =
20310 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20311
20312 let worktree_id = workspace.update(cx, |workspace, cx| {
20313 workspace.project().update(cx, |project, cx| {
20314 project.worktrees(cx).next().unwrap().read(cx).id()
20315 })
20316 });
20317
20318 let buffer = project
20319 .update(cx, |project, cx| {
20320 project.open_buffer((worktree_id, "main.rs"), cx)
20321 })
20322 .await
20323 .unwrap();
20324
20325 let (editor, cx) = cx.add_window_view(|window, cx| {
20326 Editor::new(
20327 EditorMode::full(),
20328 MultiBuffer::build_from_buffer(buffer, cx),
20329 Some(project.clone()),
20330 window,
20331 cx,
20332 )
20333 });
20334
20335 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20336 let abs_path = project.read_with(cx, |project, cx| {
20337 project
20338 .absolute_path(&project_path, cx)
20339 .map(|path_buf| Arc::from(path_buf.to_owned()))
20340 .unwrap()
20341 });
20342
20343 editor.update_in(cx, |editor, window, cx| {
20344 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20345 });
20346
20347 let breakpoints = editor.update(cx, |editor, cx| {
20348 editor
20349 .breakpoint_store()
20350 .as_ref()
20351 .unwrap()
20352 .read(cx)
20353 .all_source_breakpoints(cx)
20354 .clone()
20355 });
20356
20357 assert_breakpoint(
20358 &breakpoints,
20359 &abs_path,
20360 vec![(0, Breakpoint::new_log("hello world"))],
20361 );
20362
20363 // Removing a log message from a log breakpoint should remove it
20364 editor.update_in(cx, |editor, window, cx| {
20365 add_log_breakpoint_at_cursor(editor, "", window, cx);
20366 });
20367
20368 let breakpoints = editor.update(cx, |editor, cx| {
20369 editor
20370 .breakpoint_store()
20371 .as_ref()
20372 .unwrap()
20373 .read(cx)
20374 .all_source_breakpoints(cx)
20375 .clone()
20376 });
20377
20378 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20379
20380 editor.update_in(cx, |editor, window, cx| {
20381 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20382 editor.move_to_end(&MoveToEnd, window, cx);
20383 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20384 // Not adding a log message to a standard breakpoint shouldn't remove it
20385 add_log_breakpoint_at_cursor(editor, "", window, cx);
20386 });
20387
20388 let breakpoints = editor.update(cx, |editor, cx| {
20389 editor
20390 .breakpoint_store()
20391 .as_ref()
20392 .unwrap()
20393 .read(cx)
20394 .all_source_breakpoints(cx)
20395 .clone()
20396 });
20397
20398 assert_breakpoint(
20399 &breakpoints,
20400 &abs_path,
20401 vec![
20402 (0, Breakpoint::new_standard()),
20403 (3, Breakpoint::new_standard()),
20404 ],
20405 );
20406
20407 editor.update_in(cx, |editor, window, cx| {
20408 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20409 });
20410
20411 let breakpoints = editor.update(cx, |editor, cx| {
20412 editor
20413 .breakpoint_store()
20414 .as_ref()
20415 .unwrap()
20416 .read(cx)
20417 .all_source_breakpoints(cx)
20418 .clone()
20419 });
20420
20421 assert_breakpoint(
20422 &breakpoints,
20423 &abs_path,
20424 vec![
20425 (0, Breakpoint::new_standard()),
20426 (3, Breakpoint::new_log("hello world")),
20427 ],
20428 );
20429
20430 editor.update_in(cx, |editor, window, cx| {
20431 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
20432 });
20433
20434 let breakpoints = editor.update(cx, |editor, cx| {
20435 editor
20436 .breakpoint_store()
20437 .as_ref()
20438 .unwrap()
20439 .read(cx)
20440 .all_source_breakpoints(cx)
20441 .clone()
20442 });
20443
20444 assert_breakpoint(
20445 &breakpoints,
20446 &abs_path,
20447 vec![
20448 (0, Breakpoint::new_standard()),
20449 (3, Breakpoint::new_log("hello Earth!!")),
20450 ],
20451 );
20452}
20453
20454/// This also tests that Editor::breakpoint_at_cursor_head is working properly
20455/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
20456/// or when breakpoints were placed out of order. This tests for a regression too
20457#[gpui::test]
20458async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
20459 init_test(cx, |_| {});
20460
20461 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20462 let fs = FakeFs::new(cx.executor());
20463 fs.insert_tree(
20464 path!("/a"),
20465 json!({
20466 "main.rs": sample_text,
20467 }),
20468 )
20469 .await;
20470 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20471 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20472 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20473
20474 let fs = FakeFs::new(cx.executor());
20475 fs.insert_tree(
20476 path!("/a"),
20477 json!({
20478 "main.rs": sample_text,
20479 }),
20480 )
20481 .await;
20482 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20483 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20484 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20485 let worktree_id = workspace
20486 .update(cx, |workspace, _window, cx| {
20487 workspace.project().update(cx, |project, cx| {
20488 project.worktrees(cx).next().unwrap().read(cx).id()
20489 })
20490 })
20491 .unwrap();
20492
20493 let buffer = project
20494 .update(cx, |project, cx| {
20495 project.open_buffer((worktree_id, "main.rs"), cx)
20496 })
20497 .await
20498 .unwrap();
20499
20500 let (editor, cx) = cx.add_window_view(|window, cx| {
20501 Editor::new(
20502 EditorMode::full(),
20503 MultiBuffer::build_from_buffer(buffer, cx),
20504 Some(project.clone()),
20505 window,
20506 cx,
20507 )
20508 });
20509
20510 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20511 let abs_path = project.read_with(cx, |project, cx| {
20512 project
20513 .absolute_path(&project_path, cx)
20514 .map(|path_buf| Arc::from(path_buf.to_owned()))
20515 .unwrap()
20516 });
20517
20518 // assert we can add breakpoint on the first line
20519 editor.update_in(cx, |editor, window, cx| {
20520 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20521 editor.move_to_end(&MoveToEnd, window, cx);
20522 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20523 editor.move_up(&MoveUp, window, cx);
20524 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20525 });
20526
20527 let breakpoints = editor.update(cx, |editor, cx| {
20528 editor
20529 .breakpoint_store()
20530 .as_ref()
20531 .unwrap()
20532 .read(cx)
20533 .all_source_breakpoints(cx)
20534 .clone()
20535 });
20536
20537 assert_eq!(1, breakpoints.len());
20538 assert_breakpoint(
20539 &breakpoints,
20540 &abs_path,
20541 vec![
20542 (0, Breakpoint::new_standard()),
20543 (2, Breakpoint::new_standard()),
20544 (3, Breakpoint::new_standard()),
20545 ],
20546 );
20547
20548 editor.update_in(cx, |editor, window, cx| {
20549 editor.move_to_beginning(&MoveToBeginning, window, cx);
20550 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20551 editor.move_to_end(&MoveToEnd, window, cx);
20552 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20553 // Disabling a breakpoint that doesn't exist should do nothing
20554 editor.move_up(&MoveUp, window, cx);
20555 editor.move_up(&MoveUp, window, cx);
20556 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20557 });
20558
20559 let breakpoints = editor.update(cx, |editor, cx| {
20560 editor
20561 .breakpoint_store()
20562 .as_ref()
20563 .unwrap()
20564 .read(cx)
20565 .all_source_breakpoints(cx)
20566 .clone()
20567 });
20568
20569 let disable_breakpoint = {
20570 let mut bp = Breakpoint::new_standard();
20571 bp.state = BreakpointState::Disabled;
20572 bp
20573 };
20574
20575 assert_eq!(1, breakpoints.len());
20576 assert_breakpoint(
20577 &breakpoints,
20578 &abs_path,
20579 vec![
20580 (0, disable_breakpoint.clone()),
20581 (2, Breakpoint::new_standard()),
20582 (3, disable_breakpoint.clone()),
20583 ],
20584 );
20585
20586 editor.update_in(cx, |editor, window, cx| {
20587 editor.move_to_beginning(&MoveToBeginning, window, cx);
20588 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
20589 editor.move_to_end(&MoveToEnd, window, cx);
20590 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
20591 editor.move_up(&MoveUp, window, cx);
20592 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20593 });
20594
20595 let breakpoints = editor.update(cx, |editor, cx| {
20596 editor
20597 .breakpoint_store()
20598 .as_ref()
20599 .unwrap()
20600 .read(cx)
20601 .all_source_breakpoints(cx)
20602 .clone()
20603 });
20604
20605 assert_eq!(1, breakpoints.len());
20606 assert_breakpoint(
20607 &breakpoints,
20608 &abs_path,
20609 vec![
20610 (0, Breakpoint::new_standard()),
20611 (2, disable_breakpoint),
20612 (3, Breakpoint::new_standard()),
20613 ],
20614 );
20615}
20616
20617#[gpui::test]
20618async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
20619 init_test(cx, |_| {});
20620 let capabilities = lsp::ServerCapabilities {
20621 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
20622 prepare_provider: Some(true),
20623 work_done_progress_options: Default::default(),
20624 })),
20625 ..Default::default()
20626 };
20627 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
20628
20629 cx.set_state(indoc! {"
20630 struct Fˇoo {}
20631 "});
20632
20633 cx.update_editor(|editor, _, cx| {
20634 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
20635 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
20636 editor.highlight_background::<DocumentHighlightRead>(
20637 &[highlight_range],
20638 |theme| theme.colors().editor_document_highlight_read_background,
20639 cx,
20640 );
20641 });
20642
20643 let mut prepare_rename_handler = cx
20644 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
20645 move |_, _, _| async move {
20646 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
20647 start: lsp::Position {
20648 line: 0,
20649 character: 7,
20650 },
20651 end: lsp::Position {
20652 line: 0,
20653 character: 10,
20654 },
20655 })))
20656 },
20657 );
20658 let prepare_rename_task = cx
20659 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
20660 .expect("Prepare rename was not started");
20661 prepare_rename_handler.next().await.unwrap();
20662 prepare_rename_task.await.expect("Prepare rename failed");
20663
20664 let mut rename_handler =
20665 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
20666 let edit = lsp::TextEdit {
20667 range: lsp::Range {
20668 start: lsp::Position {
20669 line: 0,
20670 character: 7,
20671 },
20672 end: lsp::Position {
20673 line: 0,
20674 character: 10,
20675 },
20676 },
20677 new_text: "FooRenamed".to_string(),
20678 };
20679 Ok(Some(lsp::WorkspaceEdit::new(
20680 // Specify the same edit twice
20681 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
20682 )))
20683 });
20684 let rename_task = cx
20685 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
20686 .expect("Confirm rename was not started");
20687 rename_handler.next().await.unwrap();
20688 rename_task.await.expect("Confirm rename failed");
20689 cx.run_until_parked();
20690
20691 // Despite two edits, only one is actually applied as those are identical
20692 cx.assert_editor_state(indoc! {"
20693 struct FooRenamedˇ {}
20694 "});
20695}
20696
20697#[gpui::test]
20698async fn test_rename_without_prepare(cx: &mut TestAppContext) {
20699 init_test(cx, |_| {});
20700 // These capabilities indicate that the server does not support prepare rename.
20701 let capabilities = lsp::ServerCapabilities {
20702 rename_provider: Some(lsp::OneOf::Left(true)),
20703 ..Default::default()
20704 };
20705 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
20706
20707 cx.set_state(indoc! {"
20708 struct Fˇoo {}
20709 "});
20710
20711 cx.update_editor(|editor, _window, cx| {
20712 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
20713 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
20714 editor.highlight_background::<DocumentHighlightRead>(
20715 &[highlight_range],
20716 |theme| theme.colors().editor_document_highlight_read_background,
20717 cx,
20718 );
20719 });
20720
20721 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
20722 .expect("Prepare rename was not started")
20723 .await
20724 .expect("Prepare rename failed");
20725
20726 let mut rename_handler =
20727 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
20728 let edit = lsp::TextEdit {
20729 range: lsp::Range {
20730 start: lsp::Position {
20731 line: 0,
20732 character: 7,
20733 },
20734 end: lsp::Position {
20735 line: 0,
20736 character: 10,
20737 },
20738 },
20739 new_text: "FooRenamed".to_string(),
20740 };
20741 Ok(Some(lsp::WorkspaceEdit::new(
20742 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
20743 )))
20744 });
20745 let rename_task = cx
20746 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
20747 .expect("Confirm rename was not started");
20748 rename_handler.next().await.unwrap();
20749 rename_task.await.expect("Confirm rename failed");
20750 cx.run_until_parked();
20751
20752 // Correct range is renamed, as `surrounding_word` is used to find it.
20753 cx.assert_editor_state(indoc! {"
20754 struct FooRenamedˇ {}
20755 "});
20756}
20757
20758#[gpui::test]
20759async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
20760 init_test(cx, |_| {});
20761 let mut cx = EditorTestContext::new(cx).await;
20762
20763 let language = Arc::new(
20764 Language::new(
20765 LanguageConfig::default(),
20766 Some(tree_sitter_html::LANGUAGE.into()),
20767 )
20768 .with_brackets_query(
20769 r#"
20770 ("<" @open "/>" @close)
20771 ("</" @open ">" @close)
20772 ("<" @open ">" @close)
20773 ("\"" @open "\"" @close)
20774 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
20775 "#,
20776 )
20777 .unwrap(),
20778 );
20779 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20780
20781 cx.set_state(indoc! {"
20782 <span>ˇ</span>
20783 "});
20784 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
20785 cx.assert_editor_state(indoc! {"
20786 <span>
20787 ˇ
20788 </span>
20789 "});
20790
20791 cx.set_state(indoc! {"
20792 <span><span></span>ˇ</span>
20793 "});
20794 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
20795 cx.assert_editor_state(indoc! {"
20796 <span><span></span>
20797 ˇ</span>
20798 "});
20799
20800 cx.set_state(indoc! {"
20801 <span>ˇ
20802 </span>
20803 "});
20804 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
20805 cx.assert_editor_state(indoc! {"
20806 <span>
20807 ˇ
20808 </span>
20809 "});
20810}
20811
20812#[gpui::test(iterations = 10)]
20813async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
20814 init_test(cx, |_| {});
20815
20816 let fs = FakeFs::new(cx.executor());
20817 fs.insert_tree(
20818 path!("/dir"),
20819 json!({
20820 "a.ts": "a",
20821 }),
20822 )
20823 .await;
20824
20825 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
20826 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20827 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20828
20829 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20830 language_registry.add(Arc::new(Language::new(
20831 LanguageConfig {
20832 name: "TypeScript".into(),
20833 matcher: LanguageMatcher {
20834 path_suffixes: vec!["ts".to_string()],
20835 ..Default::default()
20836 },
20837 ..Default::default()
20838 },
20839 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
20840 )));
20841 let mut fake_language_servers = language_registry.register_fake_lsp(
20842 "TypeScript",
20843 FakeLspAdapter {
20844 capabilities: lsp::ServerCapabilities {
20845 code_lens_provider: Some(lsp::CodeLensOptions {
20846 resolve_provider: Some(true),
20847 }),
20848 execute_command_provider: Some(lsp::ExecuteCommandOptions {
20849 commands: vec!["_the/command".to_string()],
20850 ..lsp::ExecuteCommandOptions::default()
20851 }),
20852 ..lsp::ServerCapabilities::default()
20853 },
20854 ..FakeLspAdapter::default()
20855 },
20856 );
20857
20858 let (buffer, _handle) = project
20859 .update(cx, |p, cx| {
20860 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
20861 })
20862 .await
20863 .unwrap();
20864 cx.executor().run_until_parked();
20865
20866 let fake_server = fake_language_servers.next().await.unwrap();
20867
20868 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
20869 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
20870 drop(buffer_snapshot);
20871 let actions = cx
20872 .update_window(*workspace, |_, window, cx| {
20873 project.code_actions(&buffer, anchor..anchor, window, cx)
20874 })
20875 .unwrap();
20876
20877 fake_server
20878 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
20879 Ok(Some(vec![
20880 lsp::CodeLens {
20881 range: lsp::Range::default(),
20882 command: Some(lsp::Command {
20883 title: "Code lens command".to_owned(),
20884 command: "_the/command".to_owned(),
20885 arguments: None,
20886 }),
20887 data: None,
20888 },
20889 lsp::CodeLens {
20890 range: lsp::Range::default(),
20891 command: Some(lsp::Command {
20892 title: "Command not in capabilities".to_owned(),
20893 command: "not in capabilities".to_owned(),
20894 arguments: None,
20895 }),
20896 data: None,
20897 },
20898 lsp::CodeLens {
20899 range: lsp::Range {
20900 start: lsp::Position {
20901 line: 1,
20902 character: 1,
20903 },
20904 end: lsp::Position {
20905 line: 1,
20906 character: 1,
20907 },
20908 },
20909 command: Some(lsp::Command {
20910 title: "Command not in range".to_owned(),
20911 command: "_the/command".to_owned(),
20912 arguments: None,
20913 }),
20914 data: None,
20915 },
20916 ]))
20917 })
20918 .next()
20919 .await;
20920
20921 let actions = actions.await.unwrap();
20922 assert_eq!(
20923 actions.len(),
20924 1,
20925 "Should have only one valid action for the 0..0 range"
20926 );
20927 let action = actions[0].clone();
20928 let apply = project.update(cx, |project, cx| {
20929 project.apply_code_action(buffer.clone(), action, true, cx)
20930 });
20931
20932 // Resolving the code action does not populate its edits. In absence of
20933 // edits, we must execute the given command.
20934 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
20935 |mut lens, _| async move {
20936 let lens_command = lens.command.as_mut().expect("should have a command");
20937 assert_eq!(lens_command.title, "Code lens command");
20938 lens_command.arguments = Some(vec![json!("the-argument")]);
20939 Ok(lens)
20940 },
20941 );
20942
20943 // While executing the command, the language server sends the editor
20944 // a `workspaceEdit` request.
20945 fake_server
20946 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
20947 let fake = fake_server.clone();
20948 move |params, _| {
20949 assert_eq!(params.command, "_the/command");
20950 let fake = fake.clone();
20951 async move {
20952 fake.server
20953 .request::<lsp::request::ApplyWorkspaceEdit>(
20954 lsp::ApplyWorkspaceEditParams {
20955 label: None,
20956 edit: lsp::WorkspaceEdit {
20957 changes: Some(
20958 [(
20959 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
20960 vec![lsp::TextEdit {
20961 range: lsp::Range::new(
20962 lsp::Position::new(0, 0),
20963 lsp::Position::new(0, 0),
20964 ),
20965 new_text: "X".into(),
20966 }],
20967 )]
20968 .into_iter()
20969 .collect(),
20970 ),
20971 ..Default::default()
20972 },
20973 },
20974 )
20975 .await
20976 .into_response()
20977 .unwrap();
20978 Ok(Some(json!(null)))
20979 }
20980 }
20981 })
20982 .next()
20983 .await;
20984
20985 // Applying the code lens command returns a project transaction containing the edits
20986 // sent by the language server in its `workspaceEdit` request.
20987 let transaction = apply.await.unwrap();
20988 assert!(transaction.0.contains_key(&buffer));
20989 buffer.update(cx, |buffer, cx| {
20990 assert_eq!(buffer.text(), "Xa");
20991 buffer.undo(cx);
20992 assert_eq!(buffer.text(), "a");
20993 });
20994}
20995
20996#[gpui::test]
20997async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
20998 init_test(cx, |_| {});
20999
21000 let fs = FakeFs::new(cx.executor());
21001 let main_text = r#"fn main() {
21002println!("1");
21003println!("2");
21004println!("3");
21005println!("4");
21006println!("5");
21007}"#;
21008 let lib_text = "mod foo {}";
21009 fs.insert_tree(
21010 path!("/a"),
21011 json!({
21012 "lib.rs": lib_text,
21013 "main.rs": main_text,
21014 }),
21015 )
21016 .await;
21017
21018 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21019 let (workspace, cx) =
21020 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21021 let worktree_id = workspace.update(cx, |workspace, cx| {
21022 workspace.project().update(cx, |project, cx| {
21023 project.worktrees(cx).next().unwrap().read(cx).id()
21024 })
21025 });
21026
21027 let expected_ranges = vec![
21028 Point::new(0, 0)..Point::new(0, 0),
21029 Point::new(1, 0)..Point::new(1, 1),
21030 Point::new(2, 0)..Point::new(2, 2),
21031 Point::new(3, 0)..Point::new(3, 3),
21032 ];
21033
21034 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21035 let editor_1 = workspace
21036 .update_in(cx, |workspace, window, cx| {
21037 workspace.open_path(
21038 (worktree_id, "main.rs"),
21039 Some(pane_1.downgrade()),
21040 true,
21041 window,
21042 cx,
21043 )
21044 })
21045 .unwrap()
21046 .await
21047 .downcast::<Editor>()
21048 .unwrap();
21049 pane_1.update(cx, |pane, cx| {
21050 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21051 open_editor.update(cx, |editor, cx| {
21052 assert_eq!(
21053 editor.display_text(cx),
21054 main_text,
21055 "Original main.rs text on initial open",
21056 );
21057 assert_eq!(
21058 editor
21059 .selections
21060 .all::<Point>(cx)
21061 .into_iter()
21062 .map(|s| s.range())
21063 .collect::<Vec<_>>(),
21064 vec![Point::zero()..Point::zero()],
21065 "Default selections on initial open",
21066 );
21067 })
21068 });
21069 editor_1.update_in(cx, |editor, window, cx| {
21070 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21071 s.select_ranges(expected_ranges.clone());
21072 });
21073 });
21074
21075 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
21076 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
21077 });
21078 let editor_2 = workspace
21079 .update_in(cx, |workspace, window, cx| {
21080 workspace.open_path(
21081 (worktree_id, "main.rs"),
21082 Some(pane_2.downgrade()),
21083 true,
21084 window,
21085 cx,
21086 )
21087 })
21088 .unwrap()
21089 .await
21090 .downcast::<Editor>()
21091 .unwrap();
21092 pane_2.update(cx, |pane, cx| {
21093 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21094 open_editor.update(cx, |editor, cx| {
21095 assert_eq!(
21096 editor.display_text(cx),
21097 main_text,
21098 "Original main.rs text on initial open in another panel",
21099 );
21100 assert_eq!(
21101 editor
21102 .selections
21103 .all::<Point>(cx)
21104 .into_iter()
21105 .map(|s| s.range())
21106 .collect::<Vec<_>>(),
21107 vec![Point::zero()..Point::zero()],
21108 "Default selections on initial open in another panel",
21109 );
21110 })
21111 });
21112
21113 editor_2.update_in(cx, |editor, window, cx| {
21114 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
21115 });
21116
21117 let _other_editor_1 = workspace
21118 .update_in(cx, |workspace, window, cx| {
21119 workspace.open_path(
21120 (worktree_id, "lib.rs"),
21121 Some(pane_1.downgrade()),
21122 true,
21123 window,
21124 cx,
21125 )
21126 })
21127 .unwrap()
21128 .await
21129 .downcast::<Editor>()
21130 .unwrap();
21131 pane_1
21132 .update_in(cx, |pane, window, cx| {
21133 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
21134 })
21135 .await
21136 .unwrap();
21137 drop(editor_1);
21138 pane_1.update(cx, |pane, cx| {
21139 pane.active_item()
21140 .unwrap()
21141 .downcast::<Editor>()
21142 .unwrap()
21143 .update(cx, |editor, cx| {
21144 assert_eq!(
21145 editor.display_text(cx),
21146 lib_text,
21147 "Other file should be open and active",
21148 );
21149 });
21150 assert_eq!(pane.items().count(), 1, "No other editors should be open");
21151 });
21152
21153 let _other_editor_2 = workspace
21154 .update_in(cx, |workspace, window, cx| {
21155 workspace.open_path(
21156 (worktree_id, "lib.rs"),
21157 Some(pane_2.downgrade()),
21158 true,
21159 window,
21160 cx,
21161 )
21162 })
21163 .unwrap()
21164 .await
21165 .downcast::<Editor>()
21166 .unwrap();
21167 pane_2
21168 .update_in(cx, |pane, window, cx| {
21169 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
21170 })
21171 .await
21172 .unwrap();
21173 drop(editor_2);
21174 pane_2.update(cx, |pane, cx| {
21175 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21176 open_editor.update(cx, |editor, cx| {
21177 assert_eq!(
21178 editor.display_text(cx),
21179 lib_text,
21180 "Other file should be open and active in another panel too",
21181 );
21182 });
21183 assert_eq!(
21184 pane.items().count(),
21185 1,
21186 "No other editors should be open in another pane",
21187 );
21188 });
21189
21190 let _editor_1_reopened = workspace
21191 .update_in(cx, |workspace, window, cx| {
21192 workspace.open_path(
21193 (worktree_id, "main.rs"),
21194 Some(pane_1.downgrade()),
21195 true,
21196 window,
21197 cx,
21198 )
21199 })
21200 .unwrap()
21201 .await
21202 .downcast::<Editor>()
21203 .unwrap();
21204 let _editor_2_reopened = workspace
21205 .update_in(cx, |workspace, window, cx| {
21206 workspace.open_path(
21207 (worktree_id, "main.rs"),
21208 Some(pane_2.downgrade()),
21209 true,
21210 window,
21211 cx,
21212 )
21213 })
21214 .unwrap()
21215 .await
21216 .downcast::<Editor>()
21217 .unwrap();
21218 pane_1.update(cx, |pane, cx| {
21219 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21220 open_editor.update(cx, |editor, cx| {
21221 assert_eq!(
21222 editor.display_text(cx),
21223 main_text,
21224 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
21225 );
21226 assert_eq!(
21227 editor
21228 .selections
21229 .all::<Point>(cx)
21230 .into_iter()
21231 .map(|s| s.range())
21232 .collect::<Vec<_>>(),
21233 expected_ranges,
21234 "Previous editor in the 1st panel had selections and should get them restored on reopen",
21235 );
21236 })
21237 });
21238 pane_2.update(cx, |pane, cx| {
21239 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21240 open_editor.update(cx, |editor, cx| {
21241 assert_eq!(
21242 editor.display_text(cx),
21243 r#"fn main() {
21244⋯rintln!("1");
21245⋯intln!("2");
21246⋯ntln!("3");
21247println!("4");
21248println!("5");
21249}"#,
21250 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
21251 );
21252 assert_eq!(
21253 editor
21254 .selections
21255 .all::<Point>(cx)
21256 .into_iter()
21257 .map(|s| s.range())
21258 .collect::<Vec<_>>(),
21259 vec![Point::zero()..Point::zero()],
21260 "Previous editor in the 2nd pane had no selections changed hence should restore none",
21261 );
21262 })
21263 });
21264}
21265
21266#[gpui::test]
21267async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
21268 init_test(cx, |_| {});
21269
21270 let fs = FakeFs::new(cx.executor());
21271 let main_text = r#"fn main() {
21272println!("1");
21273println!("2");
21274println!("3");
21275println!("4");
21276println!("5");
21277}"#;
21278 let lib_text = "mod foo {}";
21279 fs.insert_tree(
21280 path!("/a"),
21281 json!({
21282 "lib.rs": lib_text,
21283 "main.rs": main_text,
21284 }),
21285 )
21286 .await;
21287
21288 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21289 let (workspace, cx) =
21290 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21291 let worktree_id = workspace.update(cx, |workspace, cx| {
21292 workspace.project().update(cx, |project, cx| {
21293 project.worktrees(cx).next().unwrap().read(cx).id()
21294 })
21295 });
21296
21297 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21298 let editor = workspace
21299 .update_in(cx, |workspace, window, cx| {
21300 workspace.open_path(
21301 (worktree_id, "main.rs"),
21302 Some(pane.downgrade()),
21303 true,
21304 window,
21305 cx,
21306 )
21307 })
21308 .unwrap()
21309 .await
21310 .downcast::<Editor>()
21311 .unwrap();
21312 pane.update(cx, |pane, cx| {
21313 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21314 open_editor.update(cx, |editor, cx| {
21315 assert_eq!(
21316 editor.display_text(cx),
21317 main_text,
21318 "Original main.rs text on initial open",
21319 );
21320 })
21321 });
21322 editor.update_in(cx, |editor, window, cx| {
21323 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
21324 });
21325
21326 cx.update_global(|store: &mut SettingsStore, cx| {
21327 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21328 s.restore_on_file_reopen = Some(false);
21329 });
21330 });
21331 editor.update_in(cx, |editor, window, cx| {
21332 editor.fold_ranges(
21333 vec![
21334 Point::new(1, 0)..Point::new(1, 1),
21335 Point::new(2, 0)..Point::new(2, 2),
21336 Point::new(3, 0)..Point::new(3, 3),
21337 ],
21338 false,
21339 window,
21340 cx,
21341 );
21342 });
21343 pane.update_in(cx, |pane, window, cx| {
21344 pane.close_all_items(&CloseAllItems::default(), window, cx)
21345 })
21346 .await
21347 .unwrap();
21348 pane.update(cx, |pane, _| {
21349 assert!(pane.active_item().is_none());
21350 });
21351 cx.update_global(|store: &mut SettingsStore, cx| {
21352 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21353 s.restore_on_file_reopen = Some(true);
21354 });
21355 });
21356
21357 let _editor_reopened = workspace
21358 .update_in(cx, |workspace, window, cx| {
21359 workspace.open_path(
21360 (worktree_id, "main.rs"),
21361 Some(pane.downgrade()),
21362 true,
21363 window,
21364 cx,
21365 )
21366 })
21367 .unwrap()
21368 .await
21369 .downcast::<Editor>()
21370 .unwrap();
21371 pane.update(cx, |pane, cx| {
21372 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21373 open_editor.update(cx, |editor, cx| {
21374 assert_eq!(
21375 editor.display_text(cx),
21376 main_text,
21377 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
21378 );
21379 })
21380 });
21381}
21382
21383#[gpui::test]
21384async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
21385 struct EmptyModalView {
21386 focus_handle: gpui::FocusHandle,
21387 }
21388 impl EventEmitter<DismissEvent> for EmptyModalView {}
21389 impl Render for EmptyModalView {
21390 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
21391 div()
21392 }
21393 }
21394 impl Focusable for EmptyModalView {
21395 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
21396 self.focus_handle.clone()
21397 }
21398 }
21399 impl workspace::ModalView for EmptyModalView {}
21400 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
21401 EmptyModalView {
21402 focus_handle: cx.focus_handle(),
21403 }
21404 }
21405
21406 init_test(cx, |_| {});
21407
21408 let fs = FakeFs::new(cx.executor());
21409 let project = Project::test(fs, [], cx).await;
21410 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21411 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
21412 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21413 let editor = cx.new_window_entity(|window, cx| {
21414 Editor::new(
21415 EditorMode::full(),
21416 buffer,
21417 Some(project.clone()),
21418 window,
21419 cx,
21420 )
21421 });
21422 workspace
21423 .update(cx, |workspace, window, cx| {
21424 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
21425 })
21426 .unwrap();
21427 editor.update_in(cx, |editor, window, cx| {
21428 editor.open_context_menu(&OpenContextMenu, window, cx);
21429 assert!(editor.mouse_context_menu.is_some());
21430 });
21431 workspace
21432 .update(cx, |workspace, window, cx| {
21433 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
21434 })
21435 .unwrap();
21436 cx.read(|cx| {
21437 assert!(editor.read(cx).mouse_context_menu.is_none());
21438 });
21439}
21440
21441#[gpui::test]
21442async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
21443 init_test(cx, |_| {});
21444
21445 let fs = FakeFs::new(cx.executor());
21446 fs.insert_file(path!("/file.html"), Default::default())
21447 .await;
21448
21449 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
21450
21451 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21452 let html_language = Arc::new(Language::new(
21453 LanguageConfig {
21454 name: "HTML".into(),
21455 matcher: LanguageMatcher {
21456 path_suffixes: vec!["html".to_string()],
21457 ..LanguageMatcher::default()
21458 },
21459 brackets: BracketPairConfig {
21460 pairs: vec![BracketPair {
21461 start: "<".into(),
21462 end: ">".into(),
21463 close: true,
21464 ..Default::default()
21465 }],
21466 ..Default::default()
21467 },
21468 ..Default::default()
21469 },
21470 Some(tree_sitter_html::LANGUAGE.into()),
21471 ));
21472 language_registry.add(html_language);
21473 let mut fake_servers = language_registry.register_fake_lsp(
21474 "HTML",
21475 FakeLspAdapter {
21476 capabilities: lsp::ServerCapabilities {
21477 completion_provider: Some(lsp::CompletionOptions {
21478 resolve_provider: Some(true),
21479 ..Default::default()
21480 }),
21481 ..Default::default()
21482 },
21483 ..Default::default()
21484 },
21485 );
21486
21487 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21488 let cx = &mut VisualTestContext::from_window(*workspace, cx);
21489
21490 let worktree_id = workspace
21491 .update(cx, |workspace, _window, cx| {
21492 workspace.project().update(cx, |project, cx| {
21493 project.worktrees(cx).next().unwrap().read(cx).id()
21494 })
21495 })
21496 .unwrap();
21497 project
21498 .update(cx, |project, cx| {
21499 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
21500 })
21501 .await
21502 .unwrap();
21503 let editor = workspace
21504 .update(cx, |workspace, window, cx| {
21505 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
21506 })
21507 .unwrap()
21508 .await
21509 .unwrap()
21510 .downcast::<Editor>()
21511 .unwrap();
21512
21513 let fake_server = fake_servers.next().await.unwrap();
21514 editor.update_in(cx, |editor, window, cx| {
21515 editor.set_text("<ad></ad>", window, cx);
21516 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
21517 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
21518 });
21519 let Some((buffer, _)) = editor
21520 .buffer
21521 .read(cx)
21522 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
21523 else {
21524 panic!("Failed to get buffer for selection position");
21525 };
21526 let buffer = buffer.read(cx);
21527 let buffer_id = buffer.remote_id();
21528 let opening_range =
21529 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
21530 let closing_range =
21531 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
21532 let mut linked_ranges = HashMap::default();
21533 linked_ranges.insert(
21534 buffer_id,
21535 vec![(opening_range.clone(), vec![closing_range.clone()])],
21536 );
21537 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
21538 });
21539 let mut completion_handle =
21540 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
21541 Ok(Some(lsp::CompletionResponse::Array(vec![
21542 lsp::CompletionItem {
21543 label: "head".to_string(),
21544 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
21545 lsp::InsertReplaceEdit {
21546 new_text: "head".to_string(),
21547 insert: lsp::Range::new(
21548 lsp::Position::new(0, 1),
21549 lsp::Position::new(0, 3),
21550 ),
21551 replace: lsp::Range::new(
21552 lsp::Position::new(0, 1),
21553 lsp::Position::new(0, 3),
21554 ),
21555 },
21556 )),
21557 ..Default::default()
21558 },
21559 ])))
21560 });
21561 editor.update_in(cx, |editor, window, cx| {
21562 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
21563 });
21564 cx.run_until_parked();
21565 completion_handle.next().await.unwrap();
21566 editor.update(cx, |editor, _| {
21567 assert!(
21568 editor.context_menu_visible(),
21569 "Completion menu should be visible"
21570 );
21571 });
21572 editor.update_in(cx, |editor, window, cx| {
21573 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
21574 });
21575 cx.executor().run_until_parked();
21576 editor.update(cx, |editor, cx| {
21577 assert_eq!(editor.text(cx), "<head></head>");
21578 });
21579}
21580
21581#[gpui::test]
21582async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
21583 init_test(cx, |_| {});
21584
21585 let fs = FakeFs::new(cx.executor());
21586 fs.insert_tree(
21587 path!("/root"),
21588 json!({
21589 "a": {
21590 "main.rs": "fn main() {}",
21591 },
21592 "foo": {
21593 "bar": {
21594 "external_file.rs": "pub mod external {}",
21595 }
21596 }
21597 }),
21598 )
21599 .await;
21600
21601 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
21602 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21603 language_registry.add(rust_lang());
21604 let _fake_servers = language_registry.register_fake_lsp(
21605 "Rust",
21606 FakeLspAdapter {
21607 ..FakeLspAdapter::default()
21608 },
21609 );
21610 let (workspace, cx) =
21611 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21612 let worktree_id = workspace.update(cx, |workspace, cx| {
21613 workspace.project().update(cx, |project, cx| {
21614 project.worktrees(cx).next().unwrap().read(cx).id()
21615 })
21616 });
21617
21618 let assert_language_servers_count =
21619 |expected: usize, context: &str, cx: &mut VisualTestContext| {
21620 project.update(cx, |project, cx| {
21621 let current = project
21622 .lsp_store()
21623 .read(cx)
21624 .as_local()
21625 .unwrap()
21626 .language_servers
21627 .len();
21628 assert_eq!(expected, current, "{context}");
21629 });
21630 };
21631
21632 assert_language_servers_count(
21633 0,
21634 "No servers should be running before any file is open",
21635 cx,
21636 );
21637 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21638 let main_editor = workspace
21639 .update_in(cx, |workspace, window, cx| {
21640 workspace.open_path(
21641 (worktree_id, "main.rs"),
21642 Some(pane.downgrade()),
21643 true,
21644 window,
21645 cx,
21646 )
21647 })
21648 .unwrap()
21649 .await
21650 .downcast::<Editor>()
21651 .unwrap();
21652 pane.update(cx, |pane, cx| {
21653 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21654 open_editor.update(cx, |editor, cx| {
21655 assert_eq!(
21656 editor.display_text(cx),
21657 "fn main() {}",
21658 "Original main.rs text on initial open",
21659 );
21660 });
21661 assert_eq!(open_editor, main_editor);
21662 });
21663 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
21664
21665 let external_editor = workspace
21666 .update_in(cx, |workspace, window, cx| {
21667 workspace.open_abs_path(
21668 PathBuf::from("/root/foo/bar/external_file.rs"),
21669 OpenOptions::default(),
21670 window,
21671 cx,
21672 )
21673 })
21674 .await
21675 .expect("opening external file")
21676 .downcast::<Editor>()
21677 .expect("downcasted external file's open element to editor");
21678 pane.update(cx, |pane, cx| {
21679 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21680 open_editor.update(cx, |editor, cx| {
21681 assert_eq!(
21682 editor.display_text(cx),
21683 "pub mod external {}",
21684 "External file is open now",
21685 );
21686 });
21687 assert_eq!(open_editor, external_editor);
21688 });
21689 assert_language_servers_count(
21690 1,
21691 "Second, external, *.rs file should join the existing server",
21692 cx,
21693 );
21694
21695 pane.update_in(cx, |pane, window, cx| {
21696 pane.close_active_item(&CloseActiveItem::default(), window, cx)
21697 })
21698 .await
21699 .unwrap();
21700 pane.update_in(cx, |pane, window, cx| {
21701 pane.navigate_backward(window, cx);
21702 });
21703 cx.run_until_parked();
21704 pane.update(cx, |pane, cx| {
21705 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21706 open_editor.update(cx, |editor, cx| {
21707 assert_eq!(
21708 editor.display_text(cx),
21709 "pub mod external {}",
21710 "External file is open now",
21711 );
21712 });
21713 });
21714 assert_language_servers_count(
21715 1,
21716 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
21717 cx,
21718 );
21719
21720 cx.update(|_, cx| {
21721 workspace::reload(&workspace::Reload::default(), cx);
21722 });
21723 assert_language_servers_count(
21724 1,
21725 "After reloading the worktree with local and external files opened, only one project should be started",
21726 cx,
21727 );
21728}
21729
21730#[gpui::test]
21731async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
21732 init_test(cx, |_| {});
21733
21734 let mut cx = EditorTestContext::new(cx).await;
21735 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
21736 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21737
21738 // test cursor move to start of each line on tab
21739 // for `if`, `elif`, `else`, `while`, `with` and `for`
21740 cx.set_state(indoc! {"
21741 def main():
21742 ˇ for item in items:
21743 ˇ while item.active:
21744 ˇ if item.value > 10:
21745 ˇ continue
21746 ˇ elif item.value < 0:
21747 ˇ break
21748 ˇ else:
21749 ˇ with item.context() as ctx:
21750 ˇ yield count
21751 ˇ else:
21752 ˇ log('while else')
21753 ˇ else:
21754 ˇ log('for else')
21755 "});
21756 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21757 cx.assert_editor_state(indoc! {"
21758 def main():
21759 ˇfor item in items:
21760 ˇwhile item.active:
21761 ˇif item.value > 10:
21762 ˇcontinue
21763 ˇelif item.value < 0:
21764 ˇbreak
21765 ˇelse:
21766 ˇwith item.context() as ctx:
21767 ˇyield count
21768 ˇelse:
21769 ˇlog('while else')
21770 ˇelse:
21771 ˇlog('for else')
21772 "});
21773 // test relative indent is preserved when tab
21774 // for `if`, `elif`, `else`, `while`, `with` and `for`
21775 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21776 cx.assert_editor_state(indoc! {"
21777 def main():
21778 ˇfor item in items:
21779 ˇwhile item.active:
21780 ˇif item.value > 10:
21781 ˇcontinue
21782 ˇelif item.value < 0:
21783 ˇbreak
21784 ˇelse:
21785 ˇwith item.context() as ctx:
21786 ˇyield count
21787 ˇelse:
21788 ˇlog('while else')
21789 ˇelse:
21790 ˇlog('for else')
21791 "});
21792
21793 // test cursor move to start of each line on tab
21794 // for `try`, `except`, `else`, `finally`, `match` and `def`
21795 cx.set_state(indoc! {"
21796 def main():
21797 ˇ try:
21798 ˇ fetch()
21799 ˇ except ValueError:
21800 ˇ handle_error()
21801 ˇ else:
21802 ˇ match value:
21803 ˇ case _:
21804 ˇ finally:
21805 ˇ def status():
21806 ˇ return 0
21807 "});
21808 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21809 cx.assert_editor_state(indoc! {"
21810 def main():
21811 ˇtry:
21812 ˇfetch()
21813 ˇexcept ValueError:
21814 ˇhandle_error()
21815 ˇelse:
21816 ˇmatch value:
21817 ˇcase _:
21818 ˇfinally:
21819 ˇdef status():
21820 ˇreturn 0
21821 "});
21822 // test relative indent is preserved when tab
21823 // for `try`, `except`, `else`, `finally`, `match` and `def`
21824 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21825 cx.assert_editor_state(indoc! {"
21826 def main():
21827 ˇtry:
21828 ˇfetch()
21829 ˇexcept ValueError:
21830 ˇhandle_error()
21831 ˇelse:
21832 ˇmatch value:
21833 ˇcase _:
21834 ˇfinally:
21835 ˇdef status():
21836 ˇreturn 0
21837 "});
21838}
21839
21840#[gpui::test]
21841async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
21842 init_test(cx, |_| {});
21843
21844 let mut cx = EditorTestContext::new(cx).await;
21845 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
21846 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21847
21848 // test `else` auto outdents when typed inside `if` block
21849 cx.set_state(indoc! {"
21850 def main():
21851 if i == 2:
21852 return
21853 ˇ
21854 "});
21855 cx.update_editor(|editor, window, cx| {
21856 editor.handle_input("else:", window, cx);
21857 });
21858 cx.assert_editor_state(indoc! {"
21859 def main():
21860 if i == 2:
21861 return
21862 else:ˇ
21863 "});
21864
21865 // test `except` auto outdents when typed inside `try` block
21866 cx.set_state(indoc! {"
21867 def main():
21868 try:
21869 i = 2
21870 ˇ
21871 "});
21872 cx.update_editor(|editor, window, cx| {
21873 editor.handle_input("except:", window, cx);
21874 });
21875 cx.assert_editor_state(indoc! {"
21876 def main():
21877 try:
21878 i = 2
21879 except:ˇ
21880 "});
21881
21882 // test `else` auto outdents when typed inside `except` block
21883 cx.set_state(indoc! {"
21884 def main():
21885 try:
21886 i = 2
21887 except:
21888 j = 2
21889 ˇ
21890 "});
21891 cx.update_editor(|editor, window, cx| {
21892 editor.handle_input("else:", window, cx);
21893 });
21894 cx.assert_editor_state(indoc! {"
21895 def main():
21896 try:
21897 i = 2
21898 except:
21899 j = 2
21900 else:ˇ
21901 "});
21902
21903 // test `finally` auto outdents when typed inside `else` block
21904 cx.set_state(indoc! {"
21905 def main():
21906 try:
21907 i = 2
21908 except:
21909 j = 2
21910 else:
21911 k = 2
21912 ˇ
21913 "});
21914 cx.update_editor(|editor, window, cx| {
21915 editor.handle_input("finally:", window, cx);
21916 });
21917 cx.assert_editor_state(indoc! {"
21918 def main():
21919 try:
21920 i = 2
21921 except:
21922 j = 2
21923 else:
21924 k = 2
21925 finally:ˇ
21926 "});
21927
21928 // test `else` does not outdents when typed inside `except` block right after for block
21929 cx.set_state(indoc! {"
21930 def main():
21931 try:
21932 i = 2
21933 except:
21934 for i in range(n):
21935 pass
21936 ˇ
21937 "});
21938 cx.update_editor(|editor, window, cx| {
21939 editor.handle_input("else:", window, cx);
21940 });
21941 cx.assert_editor_state(indoc! {"
21942 def main():
21943 try:
21944 i = 2
21945 except:
21946 for i in range(n):
21947 pass
21948 else:ˇ
21949 "});
21950
21951 // test `finally` auto outdents when typed inside `else` block right after for block
21952 cx.set_state(indoc! {"
21953 def main():
21954 try:
21955 i = 2
21956 except:
21957 j = 2
21958 else:
21959 for i in range(n):
21960 pass
21961 ˇ
21962 "});
21963 cx.update_editor(|editor, window, cx| {
21964 editor.handle_input("finally:", window, cx);
21965 });
21966 cx.assert_editor_state(indoc! {"
21967 def main():
21968 try:
21969 i = 2
21970 except:
21971 j = 2
21972 else:
21973 for i in range(n):
21974 pass
21975 finally:ˇ
21976 "});
21977
21978 // test `except` outdents to inner "try" block
21979 cx.set_state(indoc! {"
21980 def main():
21981 try:
21982 i = 2
21983 if i == 2:
21984 try:
21985 i = 3
21986 ˇ
21987 "});
21988 cx.update_editor(|editor, window, cx| {
21989 editor.handle_input("except:", window, cx);
21990 });
21991 cx.assert_editor_state(indoc! {"
21992 def main():
21993 try:
21994 i = 2
21995 if i == 2:
21996 try:
21997 i = 3
21998 except:ˇ
21999 "});
22000
22001 // test `except` outdents to outer "try" block
22002 cx.set_state(indoc! {"
22003 def main():
22004 try:
22005 i = 2
22006 if i == 2:
22007 try:
22008 i = 3
22009 ˇ
22010 "});
22011 cx.update_editor(|editor, window, cx| {
22012 editor.handle_input("except:", window, cx);
22013 });
22014 cx.assert_editor_state(indoc! {"
22015 def main():
22016 try:
22017 i = 2
22018 if i == 2:
22019 try:
22020 i = 3
22021 except:ˇ
22022 "});
22023
22024 // test `else` stays at correct indent when typed after `for` block
22025 cx.set_state(indoc! {"
22026 def main():
22027 for i in range(10):
22028 if i == 3:
22029 break
22030 ˇ
22031 "});
22032 cx.update_editor(|editor, window, cx| {
22033 editor.handle_input("else:", window, cx);
22034 });
22035 cx.assert_editor_state(indoc! {"
22036 def main():
22037 for i in range(10):
22038 if i == 3:
22039 break
22040 else:ˇ
22041 "});
22042
22043 // test does not outdent on typing after line with square brackets
22044 cx.set_state(indoc! {"
22045 def f() -> list[str]:
22046 ˇ
22047 "});
22048 cx.update_editor(|editor, window, cx| {
22049 editor.handle_input("a", window, cx);
22050 });
22051 cx.assert_editor_state(indoc! {"
22052 def f() -> list[str]:
22053 aˇ
22054 "});
22055}
22056
22057#[gpui::test]
22058async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
22059 init_test(cx, |_| {});
22060 update_test_language_settings(cx, |settings| {
22061 settings.defaults.extend_comment_on_newline = Some(false);
22062 });
22063 let mut cx = EditorTestContext::new(cx).await;
22064 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22065 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22066
22067 // test correct indent after newline on comment
22068 cx.set_state(indoc! {"
22069 # COMMENT:ˇ
22070 "});
22071 cx.update_editor(|editor, window, cx| {
22072 editor.newline(&Newline, window, cx);
22073 });
22074 cx.assert_editor_state(indoc! {"
22075 # COMMENT:
22076 ˇ
22077 "});
22078
22079 // test correct indent after newline in brackets
22080 cx.set_state(indoc! {"
22081 {ˇ}
22082 "});
22083 cx.update_editor(|editor, window, cx| {
22084 editor.newline(&Newline, window, cx);
22085 });
22086 cx.run_until_parked();
22087 cx.assert_editor_state(indoc! {"
22088 {
22089 ˇ
22090 }
22091 "});
22092
22093 cx.set_state(indoc! {"
22094 (ˇ)
22095 "});
22096 cx.update_editor(|editor, window, cx| {
22097 editor.newline(&Newline, window, cx);
22098 });
22099 cx.run_until_parked();
22100 cx.assert_editor_state(indoc! {"
22101 (
22102 ˇ
22103 )
22104 "});
22105
22106 // do not indent after empty lists or dictionaries
22107 cx.set_state(indoc! {"
22108 a = []ˇ
22109 "});
22110 cx.update_editor(|editor, window, cx| {
22111 editor.newline(&Newline, window, cx);
22112 });
22113 cx.run_until_parked();
22114 cx.assert_editor_state(indoc! {"
22115 a = []
22116 ˇ
22117 "});
22118}
22119
22120fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
22121 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
22122 point..point
22123}
22124
22125#[track_caller]
22126fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
22127 let (text, ranges) = marked_text_ranges(marked_text, true);
22128 assert_eq!(editor.text(cx), text);
22129 assert_eq!(
22130 editor.selections.ranges(cx),
22131 ranges,
22132 "Assert selections are {}",
22133 marked_text
22134 );
22135}
22136
22137pub fn handle_signature_help_request(
22138 cx: &mut EditorLspTestContext,
22139 mocked_response: lsp::SignatureHelp,
22140) -> impl Future<Output = ()> + use<> {
22141 let mut request =
22142 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
22143 let mocked_response = mocked_response.clone();
22144 async move { Ok(Some(mocked_response)) }
22145 });
22146
22147 async move {
22148 request.next().await;
22149 }
22150}
22151
22152#[track_caller]
22153pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
22154 cx.update_editor(|editor, _, _| {
22155 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
22156 let entries = menu.entries.borrow();
22157 let entries = entries
22158 .iter()
22159 .map(|entry| entry.string.as_str())
22160 .collect::<Vec<_>>();
22161 assert_eq!(entries, expected);
22162 } else {
22163 panic!("Expected completions menu");
22164 }
22165 });
22166}
22167
22168/// Handle completion request passing a marked string specifying where the completion
22169/// should be triggered from using '|' character, what range should be replaced, and what completions
22170/// should be returned using '<' and '>' to delimit the range.
22171///
22172/// Also see `handle_completion_request_with_insert_and_replace`.
22173#[track_caller]
22174pub fn handle_completion_request(
22175 marked_string: &str,
22176 completions: Vec<&'static str>,
22177 is_incomplete: bool,
22178 counter: Arc<AtomicUsize>,
22179 cx: &mut EditorLspTestContext,
22180) -> impl Future<Output = ()> {
22181 let complete_from_marker: TextRangeMarker = '|'.into();
22182 let replace_range_marker: TextRangeMarker = ('<', '>').into();
22183 let (_, mut marked_ranges) = marked_text_ranges_by(
22184 marked_string,
22185 vec![complete_from_marker.clone(), replace_range_marker.clone()],
22186 );
22187
22188 let complete_from_position =
22189 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
22190 let replace_range =
22191 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
22192
22193 let mut request =
22194 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
22195 let completions = completions.clone();
22196 counter.fetch_add(1, atomic::Ordering::Release);
22197 async move {
22198 assert_eq!(params.text_document_position.text_document.uri, url.clone());
22199 assert_eq!(
22200 params.text_document_position.position,
22201 complete_from_position
22202 );
22203 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
22204 is_incomplete: is_incomplete,
22205 item_defaults: None,
22206 items: completions
22207 .iter()
22208 .map(|completion_text| lsp::CompletionItem {
22209 label: completion_text.to_string(),
22210 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
22211 range: replace_range,
22212 new_text: completion_text.to_string(),
22213 })),
22214 ..Default::default()
22215 })
22216 .collect(),
22217 })))
22218 }
22219 });
22220
22221 async move {
22222 request.next().await;
22223 }
22224}
22225
22226/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
22227/// given instead, which also contains an `insert` range.
22228///
22229/// This function uses markers to define ranges:
22230/// - `|` marks the cursor position
22231/// - `<>` marks the replace range
22232/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
22233pub fn handle_completion_request_with_insert_and_replace(
22234 cx: &mut EditorLspTestContext,
22235 marked_string: &str,
22236 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
22237 counter: Arc<AtomicUsize>,
22238) -> impl Future<Output = ()> {
22239 let complete_from_marker: TextRangeMarker = '|'.into();
22240 let replace_range_marker: TextRangeMarker = ('<', '>').into();
22241 let insert_range_marker: TextRangeMarker = ('{', '}').into();
22242
22243 let (_, mut marked_ranges) = marked_text_ranges_by(
22244 marked_string,
22245 vec![
22246 complete_from_marker.clone(),
22247 replace_range_marker.clone(),
22248 insert_range_marker.clone(),
22249 ],
22250 );
22251
22252 let complete_from_position =
22253 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
22254 let replace_range =
22255 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
22256
22257 let insert_range = match marked_ranges.remove(&insert_range_marker) {
22258 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
22259 _ => lsp::Range {
22260 start: replace_range.start,
22261 end: complete_from_position,
22262 },
22263 };
22264
22265 let mut request =
22266 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
22267 let completions = completions.clone();
22268 counter.fetch_add(1, atomic::Ordering::Release);
22269 async move {
22270 assert_eq!(params.text_document_position.text_document.uri, url.clone());
22271 assert_eq!(
22272 params.text_document_position.position, complete_from_position,
22273 "marker `|` position doesn't match",
22274 );
22275 Ok(Some(lsp::CompletionResponse::Array(
22276 completions
22277 .iter()
22278 .map(|(label, new_text)| lsp::CompletionItem {
22279 label: label.to_string(),
22280 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
22281 lsp::InsertReplaceEdit {
22282 insert: insert_range,
22283 replace: replace_range,
22284 new_text: new_text.to_string(),
22285 },
22286 )),
22287 ..Default::default()
22288 })
22289 .collect(),
22290 )))
22291 }
22292 });
22293
22294 async move {
22295 request.next().await;
22296 }
22297}
22298
22299fn handle_resolve_completion_request(
22300 cx: &mut EditorLspTestContext,
22301 edits: Option<Vec<(&'static str, &'static str)>>,
22302) -> impl Future<Output = ()> {
22303 let edits = edits.map(|edits| {
22304 edits
22305 .iter()
22306 .map(|(marked_string, new_text)| {
22307 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
22308 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
22309 lsp::TextEdit::new(replace_range, new_text.to_string())
22310 })
22311 .collect::<Vec<_>>()
22312 });
22313
22314 let mut request =
22315 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
22316 let edits = edits.clone();
22317 async move {
22318 Ok(lsp::CompletionItem {
22319 additional_text_edits: edits,
22320 ..Default::default()
22321 })
22322 }
22323 });
22324
22325 async move {
22326 request.next().await;
22327 }
22328}
22329
22330pub(crate) fn update_test_language_settings(
22331 cx: &mut TestAppContext,
22332 f: impl Fn(&mut AllLanguageSettingsContent),
22333) {
22334 cx.update(|cx| {
22335 SettingsStore::update_global(cx, |store, cx| {
22336 store.update_user_settings::<AllLanguageSettings>(cx, f);
22337 });
22338 });
22339}
22340
22341pub(crate) fn update_test_project_settings(
22342 cx: &mut TestAppContext,
22343 f: impl Fn(&mut ProjectSettings),
22344) {
22345 cx.update(|cx| {
22346 SettingsStore::update_global(cx, |store, cx| {
22347 store.update_user_settings::<ProjectSettings>(cx, f);
22348 });
22349 });
22350}
22351
22352pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
22353 cx.update(|cx| {
22354 assets::Assets.load_test_fonts(cx);
22355 let store = SettingsStore::test(cx);
22356 cx.set_global(store);
22357 theme::init(theme::LoadThemes::JustBase, cx);
22358 release_channel::init(SemanticVersion::default(), cx);
22359 client::init_settings(cx);
22360 language::init(cx);
22361 Project::init_settings(cx);
22362 workspace::init_settings(cx);
22363 crate::init(cx);
22364 });
22365
22366 update_test_language_settings(cx, f);
22367}
22368
22369#[track_caller]
22370fn assert_hunk_revert(
22371 not_reverted_text_with_selections: &str,
22372 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
22373 expected_reverted_text_with_selections: &str,
22374 base_text: &str,
22375 cx: &mut EditorLspTestContext,
22376) {
22377 cx.set_state(not_reverted_text_with_selections);
22378 cx.set_head_text(base_text);
22379 cx.executor().run_until_parked();
22380
22381 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
22382 let snapshot = editor.snapshot(window, cx);
22383 let reverted_hunk_statuses = snapshot
22384 .buffer_snapshot
22385 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
22386 .map(|hunk| hunk.status().kind)
22387 .collect::<Vec<_>>();
22388
22389 editor.git_restore(&Default::default(), window, cx);
22390 reverted_hunk_statuses
22391 });
22392 cx.executor().run_until_parked();
22393 cx.assert_editor_state(expected_reverted_text_with_selections);
22394 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
22395}
22396
22397#[gpui::test(iterations = 10)]
22398async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
22399 init_test(cx, |_| {});
22400
22401 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
22402 let counter = diagnostic_requests.clone();
22403
22404 let fs = FakeFs::new(cx.executor());
22405 fs.insert_tree(
22406 path!("/a"),
22407 json!({
22408 "first.rs": "fn main() { let a = 5; }",
22409 "second.rs": "// Test file",
22410 }),
22411 )
22412 .await;
22413
22414 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22415 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22416 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22417
22418 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22419 language_registry.add(rust_lang());
22420 let mut fake_servers = language_registry.register_fake_lsp(
22421 "Rust",
22422 FakeLspAdapter {
22423 capabilities: lsp::ServerCapabilities {
22424 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
22425 lsp::DiagnosticOptions {
22426 identifier: None,
22427 inter_file_dependencies: true,
22428 workspace_diagnostics: true,
22429 work_done_progress_options: Default::default(),
22430 },
22431 )),
22432 ..Default::default()
22433 },
22434 ..Default::default()
22435 },
22436 );
22437
22438 let editor = workspace
22439 .update(cx, |workspace, window, cx| {
22440 workspace.open_abs_path(
22441 PathBuf::from(path!("/a/first.rs")),
22442 OpenOptions::default(),
22443 window,
22444 cx,
22445 )
22446 })
22447 .unwrap()
22448 .await
22449 .unwrap()
22450 .downcast::<Editor>()
22451 .unwrap();
22452 let fake_server = fake_servers.next().await.unwrap();
22453 let server_id = fake_server.server.server_id();
22454 let mut first_request = fake_server
22455 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
22456 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
22457 let result_id = Some(new_result_id.to_string());
22458 assert_eq!(
22459 params.text_document.uri,
22460 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
22461 );
22462 async move {
22463 Ok(lsp::DocumentDiagnosticReportResult::Report(
22464 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
22465 related_documents: None,
22466 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
22467 items: Vec::new(),
22468 result_id,
22469 },
22470 }),
22471 ))
22472 }
22473 });
22474
22475 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
22476 project.update(cx, |project, cx| {
22477 let buffer_id = editor
22478 .read(cx)
22479 .buffer()
22480 .read(cx)
22481 .as_singleton()
22482 .expect("created a singleton buffer")
22483 .read(cx)
22484 .remote_id();
22485 let buffer_result_id = project
22486 .lsp_store()
22487 .read(cx)
22488 .result_id(server_id, buffer_id, cx);
22489 assert_eq!(expected, buffer_result_id);
22490 });
22491 };
22492
22493 ensure_result_id(None, cx);
22494 cx.executor().advance_clock(Duration::from_millis(60));
22495 cx.executor().run_until_parked();
22496 assert_eq!(
22497 diagnostic_requests.load(atomic::Ordering::Acquire),
22498 1,
22499 "Opening file should trigger diagnostic request"
22500 );
22501 first_request
22502 .next()
22503 .await
22504 .expect("should have sent the first diagnostics pull request");
22505 ensure_result_id(Some("1".to_string()), cx);
22506
22507 // Editing should trigger diagnostics
22508 editor.update_in(cx, |editor, window, cx| {
22509 editor.handle_input("2", window, cx)
22510 });
22511 cx.executor().advance_clock(Duration::from_millis(60));
22512 cx.executor().run_until_parked();
22513 assert_eq!(
22514 diagnostic_requests.load(atomic::Ordering::Acquire),
22515 2,
22516 "Editing should trigger diagnostic request"
22517 );
22518 ensure_result_id(Some("2".to_string()), cx);
22519
22520 // Moving cursor should not trigger diagnostic request
22521 editor.update_in(cx, |editor, window, cx| {
22522 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22523 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
22524 });
22525 });
22526 cx.executor().advance_clock(Duration::from_millis(60));
22527 cx.executor().run_until_parked();
22528 assert_eq!(
22529 diagnostic_requests.load(atomic::Ordering::Acquire),
22530 2,
22531 "Cursor movement should not trigger diagnostic request"
22532 );
22533 ensure_result_id(Some("2".to_string()), cx);
22534 // Multiple rapid edits should be debounced
22535 for _ in 0..5 {
22536 editor.update_in(cx, |editor, window, cx| {
22537 editor.handle_input("x", window, cx)
22538 });
22539 }
22540 cx.executor().advance_clock(Duration::from_millis(60));
22541 cx.executor().run_until_parked();
22542
22543 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
22544 assert!(
22545 final_requests <= 4,
22546 "Multiple rapid edits should be debounced (got {final_requests} requests)",
22547 );
22548 ensure_result_id(Some(final_requests.to_string()), cx);
22549}
22550
22551#[gpui::test]
22552async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
22553 // Regression test for issue #11671
22554 // Previously, adding a cursor after moving multiple cursors would reset
22555 // the cursor count instead of adding to the existing cursors.
22556 init_test(cx, |_| {});
22557 let mut cx = EditorTestContext::new(cx).await;
22558
22559 // Create a simple buffer with cursor at start
22560 cx.set_state(indoc! {"
22561 ˇaaaa
22562 bbbb
22563 cccc
22564 dddd
22565 eeee
22566 ffff
22567 gggg
22568 hhhh"});
22569
22570 // Add 2 cursors below (so we have 3 total)
22571 cx.update_editor(|editor, window, cx| {
22572 editor.add_selection_below(&Default::default(), window, cx);
22573 editor.add_selection_below(&Default::default(), window, cx);
22574 });
22575
22576 // Verify we have 3 cursors
22577 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
22578 assert_eq!(
22579 initial_count, 3,
22580 "Should have 3 cursors after adding 2 below"
22581 );
22582
22583 // Move down one line
22584 cx.update_editor(|editor, window, cx| {
22585 editor.move_down(&MoveDown, window, cx);
22586 });
22587
22588 // Add another cursor below
22589 cx.update_editor(|editor, window, cx| {
22590 editor.add_selection_below(&Default::default(), window, cx);
22591 });
22592
22593 // Should now have 4 cursors (3 original + 1 new)
22594 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
22595 assert_eq!(
22596 final_count, 4,
22597 "Should have 4 cursors after moving and adding another"
22598 );
22599}
22600
22601#[gpui::test(iterations = 10)]
22602async fn test_document_colors(cx: &mut TestAppContext) {
22603 let expected_color = Rgba {
22604 r: 0.33,
22605 g: 0.33,
22606 b: 0.33,
22607 a: 0.33,
22608 };
22609
22610 init_test(cx, |_| {});
22611
22612 let fs = FakeFs::new(cx.executor());
22613 fs.insert_tree(
22614 path!("/a"),
22615 json!({
22616 "first.rs": "fn main() { let a = 5; }",
22617 }),
22618 )
22619 .await;
22620
22621 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22622 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22623 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22624
22625 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22626 language_registry.add(rust_lang());
22627 let mut fake_servers = language_registry.register_fake_lsp(
22628 "Rust",
22629 FakeLspAdapter {
22630 capabilities: lsp::ServerCapabilities {
22631 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
22632 ..lsp::ServerCapabilities::default()
22633 },
22634 name: "rust-analyzer",
22635 ..FakeLspAdapter::default()
22636 },
22637 );
22638 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
22639 "Rust",
22640 FakeLspAdapter {
22641 capabilities: lsp::ServerCapabilities {
22642 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
22643 ..lsp::ServerCapabilities::default()
22644 },
22645 name: "not-rust-analyzer",
22646 ..FakeLspAdapter::default()
22647 },
22648 );
22649
22650 let editor = workspace
22651 .update(cx, |workspace, window, cx| {
22652 workspace.open_abs_path(
22653 PathBuf::from(path!("/a/first.rs")),
22654 OpenOptions::default(),
22655 window,
22656 cx,
22657 )
22658 })
22659 .unwrap()
22660 .await
22661 .unwrap()
22662 .downcast::<Editor>()
22663 .unwrap();
22664 let fake_language_server = fake_servers.next().await.unwrap();
22665 let fake_language_server_without_capabilities =
22666 fake_servers_without_capabilities.next().await.unwrap();
22667 let requests_made = Arc::new(AtomicUsize::new(0));
22668 let closure_requests_made = Arc::clone(&requests_made);
22669 let mut color_request_handle = fake_language_server
22670 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
22671 let requests_made = Arc::clone(&closure_requests_made);
22672 async move {
22673 assert_eq!(
22674 params.text_document.uri,
22675 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
22676 );
22677 requests_made.fetch_add(1, atomic::Ordering::Release);
22678 Ok(vec![
22679 lsp::ColorInformation {
22680 range: lsp::Range {
22681 start: lsp::Position {
22682 line: 0,
22683 character: 0,
22684 },
22685 end: lsp::Position {
22686 line: 0,
22687 character: 1,
22688 },
22689 },
22690 color: lsp::Color {
22691 red: 0.33,
22692 green: 0.33,
22693 blue: 0.33,
22694 alpha: 0.33,
22695 },
22696 },
22697 lsp::ColorInformation {
22698 range: lsp::Range {
22699 start: lsp::Position {
22700 line: 0,
22701 character: 0,
22702 },
22703 end: lsp::Position {
22704 line: 0,
22705 character: 1,
22706 },
22707 },
22708 color: lsp::Color {
22709 red: 0.33,
22710 green: 0.33,
22711 blue: 0.33,
22712 alpha: 0.33,
22713 },
22714 },
22715 ])
22716 }
22717 });
22718
22719 let _handle = fake_language_server_without_capabilities
22720 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
22721 panic!("Should not be called");
22722 });
22723 cx.executor().advance_clock(Duration::from_millis(100));
22724 color_request_handle.next().await.unwrap();
22725 cx.run_until_parked();
22726 assert_eq!(
22727 1,
22728 requests_made.load(atomic::Ordering::Acquire),
22729 "Should query for colors once per editor open"
22730 );
22731 editor.update_in(cx, |editor, _, cx| {
22732 assert_eq!(
22733 vec![expected_color],
22734 extract_color_inlays(editor, cx),
22735 "Should have an initial inlay"
22736 );
22737 });
22738
22739 // opening another file in a split should not influence the LSP query counter
22740 workspace
22741 .update(cx, |workspace, window, cx| {
22742 assert_eq!(
22743 workspace.panes().len(),
22744 1,
22745 "Should have one pane with one editor"
22746 );
22747 workspace.move_item_to_pane_in_direction(
22748 &MoveItemToPaneInDirection {
22749 direction: SplitDirection::Right,
22750 focus: false,
22751 clone: true,
22752 },
22753 window,
22754 cx,
22755 );
22756 })
22757 .unwrap();
22758 cx.run_until_parked();
22759 workspace
22760 .update(cx, |workspace, _, cx| {
22761 let panes = workspace.panes();
22762 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
22763 for pane in panes {
22764 let editor = pane
22765 .read(cx)
22766 .active_item()
22767 .and_then(|item| item.downcast::<Editor>())
22768 .expect("Should have opened an editor in each split");
22769 let editor_file = editor
22770 .read(cx)
22771 .buffer()
22772 .read(cx)
22773 .as_singleton()
22774 .expect("test deals with singleton buffers")
22775 .read(cx)
22776 .file()
22777 .expect("test buffese should have a file")
22778 .path();
22779 assert_eq!(
22780 editor_file.as_ref(),
22781 Path::new("first.rs"),
22782 "Both editors should be opened for the same file"
22783 )
22784 }
22785 })
22786 .unwrap();
22787
22788 cx.executor().advance_clock(Duration::from_millis(500));
22789 let save = editor.update_in(cx, |editor, window, cx| {
22790 editor.move_to_end(&MoveToEnd, window, cx);
22791 editor.handle_input("dirty", window, cx);
22792 editor.save(
22793 SaveOptions {
22794 format: true,
22795 autosave: true,
22796 },
22797 project.clone(),
22798 window,
22799 cx,
22800 )
22801 });
22802 save.await.unwrap();
22803
22804 color_request_handle.next().await.unwrap();
22805 cx.run_until_parked();
22806 assert_eq!(
22807 3,
22808 requests_made.load(atomic::Ordering::Acquire),
22809 "Should query for colors once per save and once per formatting after save"
22810 );
22811
22812 drop(editor);
22813 let close = workspace
22814 .update(cx, |workspace, window, cx| {
22815 workspace.active_pane().update(cx, |pane, cx| {
22816 pane.close_active_item(&CloseActiveItem::default(), window, cx)
22817 })
22818 })
22819 .unwrap();
22820 close.await.unwrap();
22821 let close = workspace
22822 .update(cx, |workspace, window, cx| {
22823 workspace.active_pane().update(cx, |pane, cx| {
22824 pane.close_active_item(&CloseActiveItem::default(), window, cx)
22825 })
22826 })
22827 .unwrap();
22828 close.await.unwrap();
22829 assert_eq!(
22830 3,
22831 requests_made.load(atomic::Ordering::Acquire),
22832 "After saving and closing all editors, no extra requests should be made"
22833 );
22834 workspace
22835 .update(cx, |workspace, _, cx| {
22836 assert!(
22837 workspace.active_item(cx).is_none(),
22838 "Should close all editors"
22839 )
22840 })
22841 .unwrap();
22842
22843 workspace
22844 .update(cx, |workspace, window, cx| {
22845 workspace.active_pane().update(cx, |pane, cx| {
22846 pane.navigate_backward(window, cx);
22847 })
22848 })
22849 .unwrap();
22850 cx.executor().advance_clock(Duration::from_millis(100));
22851 cx.run_until_parked();
22852 let editor = workspace
22853 .update(cx, |workspace, _, cx| {
22854 workspace
22855 .active_item(cx)
22856 .expect("Should have reopened the editor again after navigating back")
22857 .downcast::<Editor>()
22858 .expect("Should be an editor")
22859 })
22860 .unwrap();
22861 color_request_handle.next().await.unwrap();
22862 assert_eq!(
22863 3,
22864 requests_made.load(atomic::Ordering::Acquire),
22865 "Cache should be reused on buffer close and reopen"
22866 );
22867 editor.update(cx, |editor, cx| {
22868 assert_eq!(
22869 vec![expected_color],
22870 extract_color_inlays(editor, cx),
22871 "Should have an initial inlay"
22872 );
22873 });
22874}
22875
22876#[gpui::test]
22877async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
22878 init_test(cx, |_| {});
22879 let (editor, cx) = cx.add_window_view(Editor::single_line);
22880 editor.update_in(cx, |editor, window, cx| {
22881 editor.set_text("oops\n\nwow\n", window, cx)
22882 });
22883 cx.run_until_parked();
22884 editor.update(cx, |editor, cx| {
22885 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
22886 });
22887 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
22888 cx.run_until_parked();
22889 editor.update(cx, |editor, cx| {
22890 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
22891 });
22892}
22893
22894#[track_caller]
22895fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
22896 editor
22897 .all_inlays(cx)
22898 .into_iter()
22899 .filter_map(|inlay| inlay.get_color())
22900 .map(Rgba::from)
22901 .collect()
22902}