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, FormatterList, IndentGuideSettings};
34use lsp::CompletionParams;
35use multi_buffer::{IndentGuide, PathKey};
36use parking_lot::Mutex;
37use pretty_assertions::{assert_eq, assert_ne};
38use project::{
39 FakeFs,
40 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
41 project_settings::{LspSettings, ProjectSettings},
42};
43use serde_json::{self, json};
44use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
45use std::{
46 iter,
47 sync::atomic::{self, AtomicUsize},
48};
49use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
50use text::ToPoint as _;
51use unindent::Unindent;
52use util::{
53 assert_set_eq, path,
54 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
55 uri,
56};
57use workspace::{
58 CloseActiveItem, CloseAllItems, CloseInactiveItems, 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.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.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.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.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(
9937 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
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 =
10066 Some(language_settings::SelectedFormatter::List(FormatterList(
10067 vec![
10068 Formatter::LanguageServer { name: None },
10069 Formatter::CodeActions(
10070 [
10071 ("code-action-1".into(), true),
10072 ("code-action-2".into(), true),
10073 ]
10074 .into_iter()
10075 .collect(),
10076 ),
10077 ]
10078 .into(),
10079 )))
10080 });
10081
10082 let fs = FakeFs::new(cx.executor());
10083 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
10084 .await;
10085
10086 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10087 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10088 language_registry.add(rust_lang());
10089
10090 let mut fake_servers = language_registry.register_fake_lsp(
10091 "Rust",
10092 FakeLspAdapter {
10093 capabilities: lsp::ServerCapabilities {
10094 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10095 execute_command_provider: Some(lsp::ExecuteCommandOptions {
10096 commands: vec!["the-command-for-code-action-1".into()],
10097 ..Default::default()
10098 }),
10099 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10100 ..Default::default()
10101 },
10102 ..Default::default()
10103 },
10104 );
10105
10106 let buffer = project
10107 .update(cx, |project, cx| {
10108 project.open_local_buffer(path!("/file.rs"), cx)
10109 })
10110 .await
10111 .unwrap();
10112
10113 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10114 let (editor, cx) = cx.add_window_view(|window, cx| {
10115 build_editor_with_project(project.clone(), buffer, window, cx)
10116 });
10117
10118 cx.executor().start_waiting();
10119
10120 let fake_server = fake_servers.next().await.unwrap();
10121 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10122 move |_params, _| async move {
10123 Ok(Some(vec![lsp::TextEdit::new(
10124 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10125 "applied-formatting\n".to_string(),
10126 )]))
10127 },
10128 );
10129 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10130 move |params, _| async move {
10131 assert_eq!(
10132 params.context.only,
10133 Some(vec!["code-action-1".into(), "code-action-2".into()])
10134 );
10135 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
10136 Ok(Some(vec![
10137 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10138 kind: Some("code-action-1".into()),
10139 edit: Some(lsp::WorkspaceEdit::new(
10140 [(
10141 uri.clone(),
10142 vec![lsp::TextEdit::new(
10143 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10144 "applied-code-action-1-edit\n".to_string(),
10145 )],
10146 )]
10147 .into_iter()
10148 .collect(),
10149 )),
10150 command: Some(lsp::Command {
10151 command: "the-command-for-code-action-1".into(),
10152 ..Default::default()
10153 }),
10154 ..Default::default()
10155 }),
10156 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10157 kind: Some("code-action-2".into()),
10158 edit: Some(lsp::WorkspaceEdit::new(
10159 [(
10160 uri.clone(),
10161 vec![lsp::TextEdit::new(
10162 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10163 "applied-code-action-2-edit\n".to_string(),
10164 )],
10165 )]
10166 .into_iter()
10167 .collect(),
10168 )),
10169 ..Default::default()
10170 }),
10171 ]))
10172 },
10173 );
10174
10175 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
10176 move |params, _| async move { Ok(params) }
10177 });
10178
10179 let command_lock = Arc::new(futures::lock::Mutex::new(()));
10180 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
10181 let fake = fake_server.clone();
10182 let lock = command_lock.clone();
10183 move |params, _| {
10184 assert_eq!(params.command, "the-command-for-code-action-1");
10185 let fake = fake.clone();
10186 let lock = lock.clone();
10187 async move {
10188 lock.lock().await;
10189 fake.server
10190 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
10191 label: None,
10192 edit: lsp::WorkspaceEdit {
10193 changes: Some(
10194 [(
10195 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
10196 vec![lsp::TextEdit {
10197 range: lsp::Range::new(
10198 lsp::Position::new(0, 0),
10199 lsp::Position::new(0, 0),
10200 ),
10201 new_text: "applied-code-action-1-command\n".into(),
10202 }],
10203 )]
10204 .into_iter()
10205 .collect(),
10206 ),
10207 ..Default::default()
10208 },
10209 })
10210 .await
10211 .into_response()
10212 .unwrap();
10213 Ok(Some(json!(null)))
10214 }
10215 }
10216 });
10217
10218 cx.executor().start_waiting();
10219 editor
10220 .update_in(cx, |editor, window, cx| {
10221 editor.perform_format(
10222 project.clone(),
10223 FormatTrigger::Manual,
10224 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10225 window,
10226 cx,
10227 )
10228 })
10229 .unwrap()
10230 .await;
10231 editor.update(cx, |editor, cx| {
10232 assert_eq!(
10233 editor.text(cx),
10234 r#"
10235 applied-code-action-2-edit
10236 applied-code-action-1-command
10237 applied-code-action-1-edit
10238 applied-formatting
10239 one
10240 two
10241 three
10242 "#
10243 .unindent()
10244 );
10245 });
10246
10247 editor.update_in(cx, |editor, window, cx| {
10248 editor.undo(&Default::default(), window, cx);
10249 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10250 });
10251
10252 // Perform a manual edit while waiting for an LSP command
10253 // that's being run as part of a formatting code action.
10254 let lock_guard = command_lock.lock().await;
10255 let format = editor
10256 .update_in(cx, |editor, window, cx| {
10257 editor.perform_format(
10258 project.clone(),
10259 FormatTrigger::Manual,
10260 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10261 window,
10262 cx,
10263 )
10264 })
10265 .unwrap();
10266 cx.run_until_parked();
10267 editor.update(cx, |editor, cx| {
10268 assert_eq!(
10269 editor.text(cx),
10270 r#"
10271 applied-code-action-1-edit
10272 applied-formatting
10273 one
10274 two
10275 three
10276 "#
10277 .unindent()
10278 );
10279
10280 editor.buffer.update(cx, |buffer, cx| {
10281 let ix = buffer.len(cx);
10282 buffer.edit([(ix..ix, "edited\n")], None, cx);
10283 });
10284 });
10285
10286 // Allow the LSP command to proceed. Because the buffer was edited,
10287 // the second code action will not be run.
10288 drop(lock_guard);
10289 format.await;
10290 editor.update_in(cx, |editor, window, cx| {
10291 assert_eq!(
10292 editor.text(cx),
10293 r#"
10294 applied-code-action-1-command
10295 applied-code-action-1-edit
10296 applied-formatting
10297 one
10298 two
10299 three
10300 edited
10301 "#
10302 .unindent()
10303 );
10304
10305 // The manual edit is undone first, because it is the last thing the user did
10306 // (even though the command completed afterwards).
10307 editor.undo(&Default::default(), window, cx);
10308 assert_eq!(
10309 editor.text(cx),
10310 r#"
10311 applied-code-action-1-command
10312 applied-code-action-1-edit
10313 applied-formatting
10314 one
10315 two
10316 three
10317 "#
10318 .unindent()
10319 );
10320
10321 // All the formatting (including the command, which completed after the manual edit)
10322 // is undone together.
10323 editor.undo(&Default::default(), window, cx);
10324 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10325 });
10326}
10327
10328#[gpui::test]
10329async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
10330 init_test(cx, |settings| {
10331 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
10332 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
10333 ))
10334 });
10335
10336 let fs = FakeFs::new(cx.executor());
10337 fs.insert_file(path!("/file.ts"), Default::default()).await;
10338
10339 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10340
10341 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10342 language_registry.add(Arc::new(Language::new(
10343 LanguageConfig {
10344 name: "TypeScript".into(),
10345 matcher: LanguageMatcher {
10346 path_suffixes: vec!["ts".to_string()],
10347 ..Default::default()
10348 },
10349 ..LanguageConfig::default()
10350 },
10351 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
10352 )));
10353 update_test_language_settings(cx, |settings| {
10354 settings.defaults.prettier = Some(PrettierSettings {
10355 allowed: true,
10356 ..PrettierSettings::default()
10357 });
10358 });
10359 let mut fake_servers = language_registry.register_fake_lsp(
10360 "TypeScript",
10361 FakeLspAdapter {
10362 capabilities: lsp::ServerCapabilities {
10363 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10364 ..Default::default()
10365 },
10366 ..Default::default()
10367 },
10368 );
10369
10370 let buffer = project
10371 .update(cx, |project, cx| {
10372 project.open_local_buffer(path!("/file.ts"), cx)
10373 })
10374 .await
10375 .unwrap();
10376
10377 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10378 let (editor, cx) = cx.add_window_view(|window, cx| {
10379 build_editor_with_project(project.clone(), buffer, window, cx)
10380 });
10381 editor.update_in(cx, |editor, window, cx| {
10382 editor.set_text(
10383 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10384 window,
10385 cx,
10386 )
10387 });
10388
10389 cx.executor().start_waiting();
10390 let fake_server = fake_servers.next().await.unwrap();
10391
10392 let format = editor
10393 .update_in(cx, |editor, window, cx| {
10394 editor.perform_code_action_kind(
10395 project.clone(),
10396 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10397 window,
10398 cx,
10399 )
10400 })
10401 .unwrap();
10402 fake_server
10403 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
10404 assert_eq!(
10405 params.text_document.uri,
10406 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10407 );
10408 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
10409 lsp::CodeAction {
10410 title: "Organize Imports".to_string(),
10411 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
10412 edit: Some(lsp::WorkspaceEdit {
10413 changes: Some(
10414 [(
10415 params.text_document.uri.clone(),
10416 vec![lsp::TextEdit::new(
10417 lsp::Range::new(
10418 lsp::Position::new(1, 0),
10419 lsp::Position::new(2, 0),
10420 ),
10421 "".to_string(),
10422 )],
10423 )]
10424 .into_iter()
10425 .collect(),
10426 ),
10427 ..Default::default()
10428 }),
10429 ..Default::default()
10430 },
10431 )]))
10432 })
10433 .next()
10434 .await;
10435 cx.executor().start_waiting();
10436 format.await;
10437 assert_eq!(
10438 editor.update(cx, |editor, cx| editor.text(cx)),
10439 "import { a } from 'module';\n\nconst x = a;\n"
10440 );
10441
10442 editor.update_in(cx, |editor, window, cx| {
10443 editor.set_text(
10444 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10445 window,
10446 cx,
10447 )
10448 });
10449 // Ensure we don't lock if code action hangs.
10450 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10451 move |params, _| async move {
10452 assert_eq!(
10453 params.text_document.uri,
10454 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10455 );
10456 futures::future::pending::<()>().await;
10457 unreachable!()
10458 },
10459 );
10460 let format = editor
10461 .update_in(cx, |editor, window, cx| {
10462 editor.perform_code_action_kind(
10463 project,
10464 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10465 window,
10466 cx,
10467 )
10468 })
10469 .unwrap();
10470 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
10471 cx.executor().start_waiting();
10472 format.await;
10473 assert_eq!(
10474 editor.update(cx, |editor, cx| editor.text(cx)),
10475 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
10476 );
10477}
10478
10479#[gpui::test]
10480async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
10481 init_test(cx, |_| {});
10482
10483 let mut cx = EditorLspTestContext::new_rust(
10484 lsp::ServerCapabilities {
10485 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10486 ..Default::default()
10487 },
10488 cx,
10489 )
10490 .await;
10491
10492 cx.set_state(indoc! {"
10493 one.twoˇ
10494 "});
10495
10496 // The format request takes a long time. When it completes, it inserts
10497 // a newline and an indent before the `.`
10498 cx.lsp
10499 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
10500 let executor = cx.background_executor().clone();
10501 async move {
10502 executor.timer(Duration::from_millis(100)).await;
10503 Ok(Some(vec![lsp::TextEdit {
10504 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
10505 new_text: "\n ".into(),
10506 }]))
10507 }
10508 });
10509
10510 // Submit a format request.
10511 let format_1 = cx
10512 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10513 .unwrap();
10514 cx.executor().run_until_parked();
10515
10516 // Submit a second format request.
10517 let format_2 = cx
10518 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10519 .unwrap();
10520 cx.executor().run_until_parked();
10521
10522 // Wait for both format requests to complete
10523 cx.executor().advance_clock(Duration::from_millis(200));
10524 cx.executor().start_waiting();
10525 format_1.await.unwrap();
10526 cx.executor().start_waiting();
10527 format_2.await.unwrap();
10528
10529 // The formatting edits only happens once.
10530 cx.assert_editor_state(indoc! {"
10531 one
10532 .twoˇ
10533 "});
10534}
10535
10536#[gpui::test]
10537async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
10538 init_test(cx, |settings| {
10539 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
10540 });
10541
10542 let mut cx = EditorLspTestContext::new_rust(
10543 lsp::ServerCapabilities {
10544 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10545 ..Default::default()
10546 },
10547 cx,
10548 )
10549 .await;
10550
10551 // Set up a buffer white some trailing whitespace and no trailing newline.
10552 cx.set_state(
10553 &[
10554 "one ", //
10555 "twoˇ", //
10556 "three ", //
10557 "four", //
10558 ]
10559 .join("\n"),
10560 );
10561
10562 // Submit a format request.
10563 let format = cx
10564 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10565 .unwrap();
10566
10567 // Record which buffer changes have been sent to the language server
10568 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
10569 cx.lsp
10570 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
10571 let buffer_changes = buffer_changes.clone();
10572 move |params, _| {
10573 buffer_changes.lock().extend(
10574 params
10575 .content_changes
10576 .into_iter()
10577 .map(|e| (e.range.unwrap(), e.text)),
10578 );
10579 }
10580 });
10581
10582 // Handle formatting requests to the language server.
10583 cx.lsp
10584 .set_request_handler::<lsp::request::Formatting, _, _>({
10585 let buffer_changes = buffer_changes.clone();
10586 move |_, _| {
10587 // When formatting is requested, trailing whitespace has already been stripped,
10588 // and the trailing newline has already been added.
10589 assert_eq!(
10590 &buffer_changes.lock()[1..],
10591 &[
10592 (
10593 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
10594 "".into()
10595 ),
10596 (
10597 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
10598 "".into()
10599 ),
10600 (
10601 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
10602 "\n".into()
10603 ),
10604 ]
10605 );
10606
10607 // Insert blank lines between each line of the buffer.
10608 async move {
10609 Ok(Some(vec![
10610 lsp::TextEdit {
10611 range: lsp::Range::new(
10612 lsp::Position::new(1, 0),
10613 lsp::Position::new(1, 0),
10614 ),
10615 new_text: "\n".into(),
10616 },
10617 lsp::TextEdit {
10618 range: lsp::Range::new(
10619 lsp::Position::new(2, 0),
10620 lsp::Position::new(2, 0),
10621 ),
10622 new_text: "\n".into(),
10623 },
10624 ]))
10625 }
10626 }
10627 });
10628
10629 // After formatting the buffer, the trailing whitespace is stripped,
10630 // a newline is appended, and the edits provided by the language server
10631 // have been applied.
10632 format.await.unwrap();
10633 cx.assert_editor_state(
10634 &[
10635 "one", //
10636 "", //
10637 "twoˇ", //
10638 "", //
10639 "three", //
10640 "four", //
10641 "", //
10642 ]
10643 .join("\n"),
10644 );
10645
10646 // Undoing the formatting undoes the trailing whitespace removal, the
10647 // trailing newline, and the LSP edits.
10648 cx.update_buffer(|buffer, cx| buffer.undo(cx));
10649 cx.assert_editor_state(
10650 &[
10651 "one ", //
10652 "twoˇ", //
10653 "three ", //
10654 "four", //
10655 ]
10656 .join("\n"),
10657 );
10658}
10659
10660#[gpui::test]
10661async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
10662 cx: &mut TestAppContext,
10663) {
10664 init_test(cx, |_| {});
10665
10666 cx.update(|cx| {
10667 cx.update_global::<SettingsStore, _>(|settings, cx| {
10668 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10669 settings.auto_signature_help = Some(true);
10670 });
10671 });
10672 });
10673
10674 let mut cx = EditorLspTestContext::new_rust(
10675 lsp::ServerCapabilities {
10676 signature_help_provider: Some(lsp::SignatureHelpOptions {
10677 ..Default::default()
10678 }),
10679 ..Default::default()
10680 },
10681 cx,
10682 )
10683 .await;
10684
10685 let language = Language::new(
10686 LanguageConfig {
10687 name: "Rust".into(),
10688 brackets: BracketPairConfig {
10689 pairs: vec![
10690 BracketPair {
10691 start: "{".to_string(),
10692 end: "}".to_string(),
10693 close: true,
10694 surround: true,
10695 newline: true,
10696 },
10697 BracketPair {
10698 start: "(".to_string(),
10699 end: ")".to_string(),
10700 close: true,
10701 surround: true,
10702 newline: true,
10703 },
10704 BracketPair {
10705 start: "/*".to_string(),
10706 end: " */".to_string(),
10707 close: true,
10708 surround: true,
10709 newline: true,
10710 },
10711 BracketPair {
10712 start: "[".to_string(),
10713 end: "]".to_string(),
10714 close: false,
10715 surround: false,
10716 newline: true,
10717 },
10718 BracketPair {
10719 start: "\"".to_string(),
10720 end: "\"".to_string(),
10721 close: true,
10722 surround: true,
10723 newline: false,
10724 },
10725 BracketPair {
10726 start: "<".to_string(),
10727 end: ">".to_string(),
10728 close: false,
10729 surround: true,
10730 newline: true,
10731 },
10732 ],
10733 ..Default::default()
10734 },
10735 autoclose_before: "})]".to_string(),
10736 ..Default::default()
10737 },
10738 Some(tree_sitter_rust::LANGUAGE.into()),
10739 );
10740 let language = Arc::new(language);
10741
10742 cx.language_registry().add(language.clone());
10743 cx.update_buffer(|buffer, cx| {
10744 buffer.set_language(Some(language), cx);
10745 });
10746
10747 cx.set_state(
10748 &r#"
10749 fn main() {
10750 sampleˇ
10751 }
10752 "#
10753 .unindent(),
10754 );
10755
10756 cx.update_editor(|editor, window, cx| {
10757 editor.handle_input("(", window, cx);
10758 });
10759 cx.assert_editor_state(
10760 &"
10761 fn main() {
10762 sample(ˇ)
10763 }
10764 "
10765 .unindent(),
10766 );
10767
10768 let mocked_response = lsp::SignatureHelp {
10769 signatures: vec![lsp::SignatureInformation {
10770 label: "fn sample(param1: u8, param2: u8)".to_string(),
10771 documentation: None,
10772 parameters: Some(vec![
10773 lsp::ParameterInformation {
10774 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10775 documentation: None,
10776 },
10777 lsp::ParameterInformation {
10778 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10779 documentation: None,
10780 },
10781 ]),
10782 active_parameter: None,
10783 }],
10784 active_signature: Some(0),
10785 active_parameter: Some(0),
10786 };
10787 handle_signature_help_request(&mut cx, mocked_response).await;
10788
10789 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10790 .await;
10791
10792 cx.editor(|editor, _, _| {
10793 let signature_help_state = editor.signature_help_state.popover().cloned();
10794 assert_eq!(
10795 signature_help_state.unwrap().label,
10796 "param1: u8, param2: u8"
10797 );
10798 });
10799}
10800
10801#[gpui::test]
10802async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
10803 init_test(cx, |_| {});
10804
10805 cx.update(|cx| {
10806 cx.update_global::<SettingsStore, _>(|settings, cx| {
10807 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10808 settings.auto_signature_help = Some(false);
10809 settings.show_signature_help_after_edits = Some(false);
10810 });
10811 });
10812 });
10813
10814 let mut cx = EditorLspTestContext::new_rust(
10815 lsp::ServerCapabilities {
10816 signature_help_provider: Some(lsp::SignatureHelpOptions {
10817 ..Default::default()
10818 }),
10819 ..Default::default()
10820 },
10821 cx,
10822 )
10823 .await;
10824
10825 let language = Language::new(
10826 LanguageConfig {
10827 name: "Rust".into(),
10828 brackets: BracketPairConfig {
10829 pairs: vec![
10830 BracketPair {
10831 start: "{".to_string(),
10832 end: "}".to_string(),
10833 close: true,
10834 surround: true,
10835 newline: true,
10836 },
10837 BracketPair {
10838 start: "(".to_string(),
10839 end: ")".to_string(),
10840 close: true,
10841 surround: true,
10842 newline: true,
10843 },
10844 BracketPair {
10845 start: "/*".to_string(),
10846 end: " */".to_string(),
10847 close: true,
10848 surround: true,
10849 newline: true,
10850 },
10851 BracketPair {
10852 start: "[".to_string(),
10853 end: "]".to_string(),
10854 close: false,
10855 surround: false,
10856 newline: true,
10857 },
10858 BracketPair {
10859 start: "\"".to_string(),
10860 end: "\"".to_string(),
10861 close: true,
10862 surround: true,
10863 newline: false,
10864 },
10865 BracketPair {
10866 start: "<".to_string(),
10867 end: ">".to_string(),
10868 close: false,
10869 surround: true,
10870 newline: true,
10871 },
10872 ],
10873 ..Default::default()
10874 },
10875 autoclose_before: "})]".to_string(),
10876 ..Default::default()
10877 },
10878 Some(tree_sitter_rust::LANGUAGE.into()),
10879 );
10880 let language = Arc::new(language);
10881
10882 cx.language_registry().add(language.clone());
10883 cx.update_buffer(|buffer, cx| {
10884 buffer.set_language(Some(language), cx);
10885 });
10886
10887 // Ensure that signature_help is not called when no signature help is enabled.
10888 cx.set_state(
10889 &r#"
10890 fn main() {
10891 sampleˇ
10892 }
10893 "#
10894 .unindent(),
10895 );
10896 cx.update_editor(|editor, window, cx| {
10897 editor.handle_input("(", window, cx);
10898 });
10899 cx.assert_editor_state(
10900 &"
10901 fn main() {
10902 sample(ˇ)
10903 }
10904 "
10905 .unindent(),
10906 );
10907 cx.editor(|editor, _, _| {
10908 assert!(editor.signature_help_state.task().is_none());
10909 });
10910
10911 let mocked_response = lsp::SignatureHelp {
10912 signatures: vec![lsp::SignatureInformation {
10913 label: "fn sample(param1: u8, param2: u8)".to_string(),
10914 documentation: None,
10915 parameters: Some(vec![
10916 lsp::ParameterInformation {
10917 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10918 documentation: None,
10919 },
10920 lsp::ParameterInformation {
10921 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10922 documentation: None,
10923 },
10924 ]),
10925 active_parameter: None,
10926 }],
10927 active_signature: Some(0),
10928 active_parameter: Some(0),
10929 };
10930
10931 // Ensure that signature_help is called when enabled afte edits
10932 cx.update(|_, cx| {
10933 cx.update_global::<SettingsStore, _>(|settings, cx| {
10934 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10935 settings.auto_signature_help = Some(false);
10936 settings.show_signature_help_after_edits = Some(true);
10937 });
10938 });
10939 });
10940 cx.set_state(
10941 &r#"
10942 fn main() {
10943 sampleˇ
10944 }
10945 "#
10946 .unindent(),
10947 );
10948 cx.update_editor(|editor, window, cx| {
10949 editor.handle_input("(", window, cx);
10950 });
10951 cx.assert_editor_state(
10952 &"
10953 fn main() {
10954 sample(ˇ)
10955 }
10956 "
10957 .unindent(),
10958 );
10959 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10960 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10961 .await;
10962 cx.update_editor(|editor, _, _| {
10963 let signature_help_state = editor.signature_help_state.popover().cloned();
10964 assert!(signature_help_state.is_some());
10965 assert_eq!(
10966 signature_help_state.unwrap().label,
10967 "param1: u8, param2: u8"
10968 );
10969 editor.signature_help_state = SignatureHelpState::default();
10970 });
10971
10972 // Ensure that signature_help is called when auto signature help override is enabled
10973 cx.update(|_, cx| {
10974 cx.update_global::<SettingsStore, _>(|settings, cx| {
10975 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10976 settings.auto_signature_help = Some(true);
10977 settings.show_signature_help_after_edits = Some(false);
10978 });
10979 });
10980 });
10981 cx.set_state(
10982 &r#"
10983 fn main() {
10984 sampleˇ
10985 }
10986 "#
10987 .unindent(),
10988 );
10989 cx.update_editor(|editor, window, cx| {
10990 editor.handle_input("(", window, cx);
10991 });
10992 cx.assert_editor_state(
10993 &"
10994 fn main() {
10995 sample(ˇ)
10996 }
10997 "
10998 .unindent(),
10999 );
11000 handle_signature_help_request(&mut cx, mocked_response).await;
11001 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11002 .await;
11003 cx.editor(|editor, _, _| {
11004 let signature_help_state = editor.signature_help_state.popover().cloned();
11005 assert!(signature_help_state.is_some());
11006 assert_eq!(
11007 signature_help_state.unwrap().label,
11008 "param1: u8, param2: u8"
11009 );
11010 });
11011}
11012
11013#[gpui::test]
11014async fn test_signature_help(cx: &mut TestAppContext) {
11015 init_test(cx, |_| {});
11016 cx.update(|cx| {
11017 cx.update_global::<SettingsStore, _>(|settings, cx| {
11018 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11019 settings.auto_signature_help = Some(true);
11020 });
11021 });
11022 });
11023
11024 let mut cx = EditorLspTestContext::new_rust(
11025 lsp::ServerCapabilities {
11026 signature_help_provider: Some(lsp::SignatureHelpOptions {
11027 ..Default::default()
11028 }),
11029 ..Default::default()
11030 },
11031 cx,
11032 )
11033 .await;
11034
11035 // A test that directly calls `show_signature_help`
11036 cx.update_editor(|editor, window, cx| {
11037 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11038 });
11039
11040 let mocked_response = lsp::SignatureHelp {
11041 signatures: vec![lsp::SignatureInformation {
11042 label: "fn sample(param1: u8, param2: u8)".to_string(),
11043 documentation: None,
11044 parameters: Some(vec![
11045 lsp::ParameterInformation {
11046 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11047 documentation: None,
11048 },
11049 lsp::ParameterInformation {
11050 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11051 documentation: None,
11052 },
11053 ]),
11054 active_parameter: None,
11055 }],
11056 active_signature: Some(0),
11057 active_parameter: Some(0),
11058 };
11059 handle_signature_help_request(&mut cx, mocked_response).await;
11060
11061 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11062 .await;
11063
11064 cx.editor(|editor, _, _| {
11065 let signature_help_state = editor.signature_help_state.popover().cloned();
11066 assert!(signature_help_state.is_some());
11067 assert_eq!(
11068 signature_help_state.unwrap().label,
11069 "param1: u8, param2: u8"
11070 );
11071 });
11072
11073 // When exiting outside from inside the brackets, `signature_help` is closed.
11074 cx.set_state(indoc! {"
11075 fn main() {
11076 sample(ˇ);
11077 }
11078
11079 fn sample(param1: u8, param2: u8) {}
11080 "});
11081
11082 cx.update_editor(|editor, window, cx| {
11083 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11084 s.select_ranges([0..0])
11085 });
11086 });
11087
11088 let mocked_response = lsp::SignatureHelp {
11089 signatures: Vec::new(),
11090 active_signature: None,
11091 active_parameter: None,
11092 };
11093 handle_signature_help_request(&mut cx, mocked_response).await;
11094
11095 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11096 .await;
11097
11098 cx.editor(|editor, _, _| {
11099 assert!(!editor.signature_help_state.is_shown());
11100 });
11101
11102 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
11103 cx.set_state(indoc! {"
11104 fn main() {
11105 sample(ˇ);
11106 }
11107
11108 fn sample(param1: u8, param2: u8) {}
11109 "});
11110
11111 let mocked_response = lsp::SignatureHelp {
11112 signatures: vec![lsp::SignatureInformation {
11113 label: "fn sample(param1: u8, param2: u8)".to_string(),
11114 documentation: None,
11115 parameters: Some(vec![
11116 lsp::ParameterInformation {
11117 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11118 documentation: None,
11119 },
11120 lsp::ParameterInformation {
11121 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11122 documentation: None,
11123 },
11124 ]),
11125 active_parameter: None,
11126 }],
11127 active_signature: Some(0),
11128 active_parameter: Some(0),
11129 };
11130 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11131 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11132 .await;
11133 cx.editor(|editor, _, _| {
11134 assert!(editor.signature_help_state.is_shown());
11135 });
11136
11137 // Restore the popover with more parameter input
11138 cx.set_state(indoc! {"
11139 fn main() {
11140 sample(param1, param2ˇ);
11141 }
11142
11143 fn sample(param1: u8, param2: u8) {}
11144 "});
11145
11146 let mocked_response = lsp::SignatureHelp {
11147 signatures: vec![lsp::SignatureInformation {
11148 label: "fn sample(param1: u8, param2: u8)".to_string(),
11149 documentation: None,
11150 parameters: Some(vec![
11151 lsp::ParameterInformation {
11152 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11153 documentation: None,
11154 },
11155 lsp::ParameterInformation {
11156 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11157 documentation: None,
11158 },
11159 ]),
11160 active_parameter: None,
11161 }],
11162 active_signature: Some(0),
11163 active_parameter: Some(1),
11164 };
11165 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11166 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11167 .await;
11168
11169 // When selecting a range, the popover is gone.
11170 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
11171 cx.update_editor(|editor, window, cx| {
11172 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11173 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11174 })
11175 });
11176 cx.assert_editor_state(indoc! {"
11177 fn main() {
11178 sample(param1, «ˇparam2»);
11179 }
11180
11181 fn sample(param1: u8, param2: u8) {}
11182 "});
11183 cx.editor(|editor, _, _| {
11184 assert!(!editor.signature_help_state.is_shown());
11185 });
11186
11187 // When unselecting again, the popover is back if within the brackets.
11188 cx.update_editor(|editor, window, cx| {
11189 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11190 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11191 })
11192 });
11193 cx.assert_editor_state(indoc! {"
11194 fn main() {
11195 sample(param1, ˇparam2);
11196 }
11197
11198 fn sample(param1: u8, param2: u8) {}
11199 "});
11200 handle_signature_help_request(&mut cx, mocked_response).await;
11201 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11202 .await;
11203 cx.editor(|editor, _, _| {
11204 assert!(editor.signature_help_state.is_shown());
11205 });
11206
11207 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
11208 cx.update_editor(|editor, window, cx| {
11209 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11210 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
11211 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11212 })
11213 });
11214 cx.assert_editor_state(indoc! {"
11215 fn main() {
11216 sample(param1, ˇparam2);
11217 }
11218
11219 fn sample(param1: u8, param2: u8) {}
11220 "});
11221
11222 let mocked_response = lsp::SignatureHelp {
11223 signatures: vec![lsp::SignatureInformation {
11224 label: "fn sample(param1: u8, param2: u8)".to_string(),
11225 documentation: None,
11226 parameters: Some(vec![
11227 lsp::ParameterInformation {
11228 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11229 documentation: None,
11230 },
11231 lsp::ParameterInformation {
11232 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11233 documentation: None,
11234 },
11235 ]),
11236 active_parameter: None,
11237 }],
11238 active_signature: Some(0),
11239 active_parameter: Some(1),
11240 };
11241 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11242 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11243 .await;
11244 cx.update_editor(|editor, _, cx| {
11245 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
11246 });
11247 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11248 .await;
11249 cx.update_editor(|editor, window, cx| {
11250 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11251 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11252 })
11253 });
11254 cx.assert_editor_state(indoc! {"
11255 fn main() {
11256 sample(param1, «ˇparam2»);
11257 }
11258
11259 fn sample(param1: u8, param2: u8) {}
11260 "});
11261 cx.update_editor(|editor, window, cx| {
11262 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11263 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11264 })
11265 });
11266 cx.assert_editor_state(indoc! {"
11267 fn main() {
11268 sample(param1, ˇparam2);
11269 }
11270
11271 fn sample(param1: u8, param2: u8) {}
11272 "});
11273 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
11274 .await;
11275}
11276
11277#[gpui::test]
11278async fn test_completion_mode(cx: &mut TestAppContext) {
11279 init_test(cx, |_| {});
11280 let mut cx = EditorLspTestContext::new_rust(
11281 lsp::ServerCapabilities {
11282 completion_provider: Some(lsp::CompletionOptions {
11283 resolve_provider: Some(true),
11284 ..Default::default()
11285 }),
11286 ..Default::default()
11287 },
11288 cx,
11289 )
11290 .await;
11291
11292 struct Run {
11293 run_description: &'static str,
11294 initial_state: String,
11295 buffer_marked_text: String,
11296 completion_label: &'static str,
11297 completion_text: &'static str,
11298 expected_with_insert_mode: String,
11299 expected_with_replace_mode: String,
11300 expected_with_replace_subsequence_mode: String,
11301 expected_with_replace_suffix_mode: String,
11302 }
11303
11304 let runs = [
11305 Run {
11306 run_description: "Start of word matches completion text",
11307 initial_state: "before ediˇ after".into(),
11308 buffer_marked_text: "before <edi|> after".into(),
11309 completion_label: "editor",
11310 completion_text: "editor",
11311 expected_with_insert_mode: "before editorˇ after".into(),
11312 expected_with_replace_mode: "before editorˇ after".into(),
11313 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11314 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11315 },
11316 Run {
11317 run_description: "Accept same text at the middle of the word",
11318 initial_state: "before ediˇtor after".into(),
11319 buffer_marked_text: "before <edi|tor> after".into(),
11320 completion_label: "editor",
11321 completion_text: "editor",
11322 expected_with_insert_mode: "before editorˇtor after".into(),
11323 expected_with_replace_mode: "before editorˇ after".into(),
11324 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11325 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11326 },
11327 Run {
11328 run_description: "End of word matches completion text -- cursor at end",
11329 initial_state: "before torˇ after".into(),
11330 buffer_marked_text: "before <tor|> after".into(),
11331 completion_label: "editor",
11332 completion_text: "editor",
11333 expected_with_insert_mode: "before editorˇ after".into(),
11334 expected_with_replace_mode: "before editorˇ after".into(),
11335 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11336 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11337 },
11338 Run {
11339 run_description: "End of word matches completion text -- cursor at start",
11340 initial_state: "before ˇtor after".into(),
11341 buffer_marked_text: "before <|tor> after".into(),
11342 completion_label: "editor",
11343 completion_text: "editor",
11344 expected_with_insert_mode: "before editorˇtor after".into(),
11345 expected_with_replace_mode: "before editorˇ after".into(),
11346 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11347 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11348 },
11349 Run {
11350 run_description: "Prepend text containing whitespace",
11351 initial_state: "pˇfield: bool".into(),
11352 buffer_marked_text: "<p|field>: bool".into(),
11353 completion_label: "pub ",
11354 completion_text: "pub ",
11355 expected_with_insert_mode: "pub ˇfield: bool".into(),
11356 expected_with_replace_mode: "pub ˇ: bool".into(),
11357 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
11358 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
11359 },
11360 Run {
11361 run_description: "Add element to start of list",
11362 initial_state: "[element_ˇelement_2]".into(),
11363 buffer_marked_text: "[<element_|element_2>]".into(),
11364 completion_label: "element_1",
11365 completion_text: "element_1",
11366 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
11367 expected_with_replace_mode: "[element_1ˇ]".into(),
11368 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
11369 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
11370 },
11371 Run {
11372 run_description: "Add element to start of list -- first and second elements are equal",
11373 initial_state: "[elˇelement]".into(),
11374 buffer_marked_text: "[<el|element>]".into(),
11375 completion_label: "element",
11376 completion_text: "element",
11377 expected_with_insert_mode: "[elementˇelement]".into(),
11378 expected_with_replace_mode: "[elementˇ]".into(),
11379 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
11380 expected_with_replace_suffix_mode: "[elementˇ]".into(),
11381 },
11382 Run {
11383 run_description: "Ends with matching suffix",
11384 initial_state: "SubˇError".into(),
11385 buffer_marked_text: "<Sub|Error>".into(),
11386 completion_label: "SubscriptionError",
11387 completion_text: "SubscriptionError",
11388 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
11389 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11390 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11391 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
11392 },
11393 Run {
11394 run_description: "Suffix is a subsequence -- contiguous",
11395 initial_state: "SubˇErr".into(),
11396 buffer_marked_text: "<Sub|Err>".into(),
11397 completion_label: "SubscriptionError",
11398 completion_text: "SubscriptionError",
11399 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
11400 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11401 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11402 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
11403 },
11404 Run {
11405 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
11406 initial_state: "Suˇscrirr".into(),
11407 buffer_marked_text: "<Su|scrirr>".into(),
11408 completion_label: "SubscriptionError",
11409 completion_text: "SubscriptionError",
11410 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
11411 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11412 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11413 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
11414 },
11415 Run {
11416 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
11417 initial_state: "foo(indˇix)".into(),
11418 buffer_marked_text: "foo(<ind|ix>)".into(),
11419 completion_label: "node_index",
11420 completion_text: "node_index",
11421 expected_with_insert_mode: "foo(node_indexˇix)".into(),
11422 expected_with_replace_mode: "foo(node_indexˇ)".into(),
11423 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
11424 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
11425 },
11426 Run {
11427 run_description: "Replace range ends before cursor - should extend to cursor",
11428 initial_state: "before editˇo after".into(),
11429 buffer_marked_text: "before <{ed}>it|o after".into(),
11430 completion_label: "editor",
11431 completion_text: "editor",
11432 expected_with_insert_mode: "before editorˇo after".into(),
11433 expected_with_replace_mode: "before editorˇo after".into(),
11434 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
11435 expected_with_replace_suffix_mode: "before editorˇo after".into(),
11436 },
11437 Run {
11438 run_description: "Uses label for suffix matching",
11439 initial_state: "before ediˇtor after".into(),
11440 buffer_marked_text: "before <edi|tor> after".into(),
11441 completion_label: "editor",
11442 completion_text: "editor()",
11443 expected_with_insert_mode: "before editor()ˇtor after".into(),
11444 expected_with_replace_mode: "before editor()ˇ after".into(),
11445 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
11446 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
11447 },
11448 Run {
11449 run_description: "Case insensitive subsequence and suffix matching",
11450 initial_state: "before EDiˇtoR after".into(),
11451 buffer_marked_text: "before <EDi|toR> after".into(),
11452 completion_label: "editor",
11453 completion_text: "editor",
11454 expected_with_insert_mode: "before editorˇtoR after".into(),
11455 expected_with_replace_mode: "before editorˇ after".into(),
11456 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11457 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11458 },
11459 ];
11460
11461 for run in runs {
11462 let run_variations = [
11463 (LspInsertMode::Insert, run.expected_with_insert_mode),
11464 (LspInsertMode::Replace, run.expected_with_replace_mode),
11465 (
11466 LspInsertMode::ReplaceSubsequence,
11467 run.expected_with_replace_subsequence_mode,
11468 ),
11469 (
11470 LspInsertMode::ReplaceSuffix,
11471 run.expected_with_replace_suffix_mode,
11472 ),
11473 ];
11474
11475 for (lsp_insert_mode, expected_text) in run_variations {
11476 eprintln!(
11477 "run = {:?}, mode = {lsp_insert_mode:.?}",
11478 run.run_description,
11479 );
11480
11481 update_test_language_settings(&mut cx, |settings| {
11482 settings.defaults.completions = Some(CompletionSettings {
11483 lsp_insert_mode,
11484 words: WordsCompletionMode::Disabled,
11485 lsp: true,
11486 lsp_fetch_timeout_ms: 0,
11487 });
11488 });
11489
11490 cx.set_state(&run.initial_state);
11491 cx.update_editor(|editor, window, cx| {
11492 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11493 });
11494
11495 let counter = Arc::new(AtomicUsize::new(0));
11496 handle_completion_request_with_insert_and_replace(
11497 &mut cx,
11498 &run.buffer_marked_text,
11499 vec![(run.completion_label, run.completion_text)],
11500 counter.clone(),
11501 )
11502 .await;
11503 cx.condition(|editor, _| editor.context_menu_visible())
11504 .await;
11505 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11506
11507 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11508 editor
11509 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11510 .unwrap()
11511 });
11512 cx.assert_editor_state(&expected_text);
11513 handle_resolve_completion_request(&mut cx, None).await;
11514 apply_additional_edits.await.unwrap();
11515 }
11516 }
11517}
11518
11519#[gpui::test]
11520async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
11521 init_test(cx, |_| {});
11522 let mut cx = EditorLspTestContext::new_rust(
11523 lsp::ServerCapabilities {
11524 completion_provider: Some(lsp::CompletionOptions {
11525 resolve_provider: Some(true),
11526 ..Default::default()
11527 }),
11528 ..Default::default()
11529 },
11530 cx,
11531 )
11532 .await;
11533
11534 let initial_state = "SubˇError";
11535 let buffer_marked_text = "<Sub|Error>";
11536 let completion_text = "SubscriptionError";
11537 let expected_with_insert_mode = "SubscriptionErrorˇError";
11538 let expected_with_replace_mode = "SubscriptionErrorˇ";
11539
11540 update_test_language_settings(&mut cx, |settings| {
11541 settings.defaults.completions = Some(CompletionSettings {
11542 words: WordsCompletionMode::Disabled,
11543 // set the opposite here to ensure that the action is overriding the default behavior
11544 lsp_insert_mode: LspInsertMode::Insert,
11545 lsp: true,
11546 lsp_fetch_timeout_ms: 0,
11547 });
11548 });
11549
11550 cx.set_state(initial_state);
11551 cx.update_editor(|editor, window, cx| {
11552 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11553 });
11554
11555 let counter = Arc::new(AtomicUsize::new(0));
11556 handle_completion_request_with_insert_and_replace(
11557 &mut cx,
11558 &buffer_marked_text,
11559 vec![(completion_text, completion_text)],
11560 counter.clone(),
11561 )
11562 .await;
11563 cx.condition(|editor, _| editor.context_menu_visible())
11564 .await;
11565 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11566
11567 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11568 editor
11569 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11570 .unwrap()
11571 });
11572 cx.assert_editor_state(&expected_with_replace_mode);
11573 handle_resolve_completion_request(&mut cx, None).await;
11574 apply_additional_edits.await.unwrap();
11575
11576 update_test_language_settings(&mut cx, |settings| {
11577 settings.defaults.completions = Some(CompletionSettings {
11578 words: WordsCompletionMode::Disabled,
11579 // set the opposite here to ensure that the action is overriding the default behavior
11580 lsp_insert_mode: LspInsertMode::Replace,
11581 lsp: true,
11582 lsp_fetch_timeout_ms: 0,
11583 });
11584 });
11585
11586 cx.set_state(initial_state);
11587 cx.update_editor(|editor, window, cx| {
11588 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11589 });
11590 handle_completion_request_with_insert_and_replace(
11591 &mut cx,
11592 &buffer_marked_text,
11593 vec![(completion_text, completion_text)],
11594 counter.clone(),
11595 )
11596 .await;
11597 cx.condition(|editor, _| editor.context_menu_visible())
11598 .await;
11599 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11600
11601 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11602 editor
11603 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
11604 .unwrap()
11605 });
11606 cx.assert_editor_state(&expected_with_insert_mode);
11607 handle_resolve_completion_request(&mut cx, None).await;
11608 apply_additional_edits.await.unwrap();
11609}
11610
11611#[gpui::test]
11612async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
11613 init_test(cx, |_| {});
11614 let mut cx = EditorLspTestContext::new_rust(
11615 lsp::ServerCapabilities {
11616 completion_provider: Some(lsp::CompletionOptions {
11617 resolve_provider: Some(true),
11618 ..Default::default()
11619 }),
11620 ..Default::default()
11621 },
11622 cx,
11623 )
11624 .await;
11625
11626 // scenario: surrounding text matches completion text
11627 let completion_text = "to_offset";
11628 let initial_state = indoc! {"
11629 1. buf.to_offˇsuffix
11630 2. buf.to_offˇsuf
11631 3. buf.to_offˇfix
11632 4. buf.to_offˇ
11633 5. into_offˇensive
11634 6. ˇsuffix
11635 7. let ˇ //
11636 8. aaˇzz
11637 9. buf.to_off«zzzzzˇ»suffix
11638 10. buf.«ˇzzzzz»suffix
11639 11. to_off«ˇzzzzz»
11640
11641 buf.to_offˇsuffix // newest cursor
11642 "};
11643 let completion_marked_buffer = indoc! {"
11644 1. buf.to_offsuffix
11645 2. buf.to_offsuf
11646 3. buf.to_offfix
11647 4. buf.to_off
11648 5. into_offensive
11649 6. suffix
11650 7. let //
11651 8. aazz
11652 9. buf.to_offzzzzzsuffix
11653 10. buf.zzzzzsuffix
11654 11. to_offzzzzz
11655
11656 buf.<to_off|suffix> // newest cursor
11657 "};
11658 let expected = indoc! {"
11659 1. buf.to_offsetˇ
11660 2. buf.to_offsetˇsuf
11661 3. buf.to_offsetˇfix
11662 4. buf.to_offsetˇ
11663 5. into_offsetˇensive
11664 6. to_offsetˇsuffix
11665 7. let to_offsetˇ //
11666 8. aato_offsetˇzz
11667 9. buf.to_offsetˇ
11668 10. buf.to_offsetˇsuffix
11669 11. to_offsetˇ
11670
11671 buf.to_offsetˇ // newest cursor
11672 "};
11673 cx.set_state(initial_state);
11674 cx.update_editor(|editor, window, cx| {
11675 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11676 });
11677 handle_completion_request_with_insert_and_replace(
11678 &mut cx,
11679 completion_marked_buffer,
11680 vec![(completion_text, completion_text)],
11681 Arc::new(AtomicUsize::new(0)),
11682 )
11683 .await;
11684 cx.condition(|editor, _| editor.context_menu_visible())
11685 .await;
11686 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11687 editor
11688 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11689 .unwrap()
11690 });
11691 cx.assert_editor_state(expected);
11692 handle_resolve_completion_request(&mut cx, None).await;
11693 apply_additional_edits.await.unwrap();
11694
11695 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
11696 let completion_text = "foo_and_bar";
11697 let initial_state = indoc! {"
11698 1. ooanbˇ
11699 2. zooanbˇ
11700 3. ooanbˇz
11701 4. zooanbˇz
11702 5. ooanˇ
11703 6. oanbˇ
11704
11705 ooanbˇ
11706 "};
11707 let completion_marked_buffer = indoc! {"
11708 1. ooanb
11709 2. zooanb
11710 3. ooanbz
11711 4. zooanbz
11712 5. ooan
11713 6. oanb
11714
11715 <ooanb|>
11716 "};
11717 let expected = indoc! {"
11718 1. foo_and_barˇ
11719 2. zfoo_and_barˇ
11720 3. foo_and_barˇz
11721 4. zfoo_and_barˇz
11722 5. ooanfoo_and_barˇ
11723 6. oanbfoo_and_barˇ
11724
11725 foo_and_barˇ
11726 "};
11727 cx.set_state(initial_state);
11728 cx.update_editor(|editor, window, cx| {
11729 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11730 });
11731 handle_completion_request_with_insert_and_replace(
11732 &mut cx,
11733 completion_marked_buffer,
11734 vec![(completion_text, completion_text)],
11735 Arc::new(AtomicUsize::new(0)),
11736 )
11737 .await;
11738 cx.condition(|editor, _| editor.context_menu_visible())
11739 .await;
11740 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11741 editor
11742 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11743 .unwrap()
11744 });
11745 cx.assert_editor_state(expected);
11746 handle_resolve_completion_request(&mut cx, None).await;
11747 apply_additional_edits.await.unwrap();
11748
11749 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
11750 // (expects the same as if it was inserted at the end)
11751 let completion_text = "foo_and_bar";
11752 let initial_state = indoc! {"
11753 1. ooˇanb
11754 2. zooˇanb
11755 3. ooˇanbz
11756 4. zooˇanbz
11757
11758 ooˇanb
11759 "};
11760 let completion_marked_buffer = indoc! {"
11761 1. ooanb
11762 2. zooanb
11763 3. ooanbz
11764 4. zooanbz
11765
11766 <oo|anb>
11767 "};
11768 let expected = indoc! {"
11769 1. foo_and_barˇ
11770 2. zfoo_and_barˇ
11771 3. foo_and_barˇz
11772 4. zfoo_and_barˇz
11773
11774 foo_and_barˇ
11775 "};
11776 cx.set_state(initial_state);
11777 cx.update_editor(|editor, window, cx| {
11778 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11779 });
11780 handle_completion_request_with_insert_and_replace(
11781 &mut cx,
11782 completion_marked_buffer,
11783 vec![(completion_text, completion_text)],
11784 Arc::new(AtomicUsize::new(0)),
11785 )
11786 .await;
11787 cx.condition(|editor, _| editor.context_menu_visible())
11788 .await;
11789 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11790 editor
11791 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11792 .unwrap()
11793 });
11794 cx.assert_editor_state(expected);
11795 handle_resolve_completion_request(&mut cx, None).await;
11796 apply_additional_edits.await.unwrap();
11797}
11798
11799// This used to crash
11800#[gpui::test]
11801async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
11802 init_test(cx, |_| {});
11803
11804 let buffer_text = indoc! {"
11805 fn main() {
11806 10.satu;
11807
11808 //
11809 // separate cursors so they open in different excerpts (manually reproducible)
11810 //
11811
11812 10.satu20;
11813 }
11814 "};
11815 let multibuffer_text_with_selections = indoc! {"
11816 fn main() {
11817 10.satuˇ;
11818
11819 //
11820
11821 //
11822
11823 10.satuˇ20;
11824 }
11825 "};
11826 let expected_multibuffer = indoc! {"
11827 fn main() {
11828 10.saturating_sub()ˇ;
11829
11830 //
11831
11832 //
11833
11834 10.saturating_sub()ˇ;
11835 }
11836 "};
11837
11838 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
11839 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
11840
11841 let fs = FakeFs::new(cx.executor());
11842 fs.insert_tree(
11843 path!("/a"),
11844 json!({
11845 "main.rs": buffer_text,
11846 }),
11847 )
11848 .await;
11849
11850 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11851 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11852 language_registry.add(rust_lang());
11853 let mut fake_servers = language_registry.register_fake_lsp(
11854 "Rust",
11855 FakeLspAdapter {
11856 capabilities: lsp::ServerCapabilities {
11857 completion_provider: Some(lsp::CompletionOptions {
11858 resolve_provider: None,
11859 ..lsp::CompletionOptions::default()
11860 }),
11861 ..lsp::ServerCapabilities::default()
11862 },
11863 ..FakeLspAdapter::default()
11864 },
11865 );
11866 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11867 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11868 let buffer = project
11869 .update(cx, |project, cx| {
11870 project.open_local_buffer(path!("/a/main.rs"), cx)
11871 })
11872 .await
11873 .unwrap();
11874
11875 let multi_buffer = cx.new(|cx| {
11876 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
11877 multi_buffer.push_excerpts(
11878 buffer.clone(),
11879 [ExcerptRange::new(0..first_excerpt_end)],
11880 cx,
11881 );
11882 multi_buffer.push_excerpts(
11883 buffer.clone(),
11884 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
11885 cx,
11886 );
11887 multi_buffer
11888 });
11889
11890 let editor = workspace
11891 .update(cx, |_, window, cx| {
11892 cx.new(|cx| {
11893 Editor::new(
11894 EditorMode::Full {
11895 scale_ui_elements_with_buffer_font_size: false,
11896 show_active_line_background: false,
11897 sized_by_content: false,
11898 },
11899 multi_buffer.clone(),
11900 Some(project.clone()),
11901 window,
11902 cx,
11903 )
11904 })
11905 })
11906 .unwrap();
11907
11908 let pane = workspace
11909 .update(cx, |workspace, _, _| workspace.active_pane().clone())
11910 .unwrap();
11911 pane.update_in(cx, |pane, window, cx| {
11912 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
11913 });
11914
11915 let fake_server = fake_servers.next().await.unwrap();
11916
11917 editor.update_in(cx, |editor, window, cx| {
11918 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11919 s.select_ranges([
11920 Point::new(1, 11)..Point::new(1, 11),
11921 Point::new(7, 11)..Point::new(7, 11),
11922 ])
11923 });
11924
11925 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
11926 });
11927
11928 editor.update_in(cx, |editor, window, cx| {
11929 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11930 });
11931
11932 fake_server
11933 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11934 let completion_item = lsp::CompletionItem {
11935 label: "saturating_sub()".into(),
11936 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
11937 lsp::InsertReplaceEdit {
11938 new_text: "saturating_sub()".to_owned(),
11939 insert: lsp::Range::new(
11940 lsp::Position::new(7, 7),
11941 lsp::Position::new(7, 11),
11942 ),
11943 replace: lsp::Range::new(
11944 lsp::Position::new(7, 7),
11945 lsp::Position::new(7, 13),
11946 ),
11947 },
11948 )),
11949 ..lsp::CompletionItem::default()
11950 };
11951
11952 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
11953 })
11954 .next()
11955 .await
11956 .unwrap();
11957
11958 cx.condition(&editor, |editor, _| editor.context_menu_visible())
11959 .await;
11960
11961 editor
11962 .update_in(cx, |editor, window, cx| {
11963 editor
11964 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11965 .unwrap()
11966 })
11967 .await
11968 .unwrap();
11969
11970 editor.update(cx, |editor, cx| {
11971 assert_text_with_selections(editor, expected_multibuffer, cx);
11972 })
11973}
11974
11975#[gpui::test]
11976async fn test_completion(cx: &mut TestAppContext) {
11977 init_test(cx, |_| {});
11978
11979 let mut cx = EditorLspTestContext::new_rust(
11980 lsp::ServerCapabilities {
11981 completion_provider: Some(lsp::CompletionOptions {
11982 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11983 resolve_provider: Some(true),
11984 ..Default::default()
11985 }),
11986 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11987 ..Default::default()
11988 },
11989 cx,
11990 )
11991 .await;
11992 let counter = Arc::new(AtomicUsize::new(0));
11993
11994 cx.set_state(indoc! {"
11995 oneˇ
11996 two
11997 three
11998 "});
11999 cx.simulate_keystroke(".");
12000 handle_completion_request(
12001 indoc! {"
12002 one.|<>
12003 two
12004 three
12005 "},
12006 vec!["first_completion", "second_completion"],
12007 true,
12008 counter.clone(),
12009 &mut cx,
12010 )
12011 .await;
12012 cx.condition(|editor, _| editor.context_menu_visible())
12013 .await;
12014 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12015
12016 let _handler = handle_signature_help_request(
12017 &mut cx,
12018 lsp::SignatureHelp {
12019 signatures: vec![lsp::SignatureInformation {
12020 label: "test signature".to_string(),
12021 documentation: None,
12022 parameters: Some(vec![lsp::ParameterInformation {
12023 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
12024 documentation: None,
12025 }]),
12026 active_parameter: None,
12027 }],
12028 active_signature: None,
12029 active_parameter: None,
12030 },
12031 );
12032 cx.update_editor(|editor, window, cx| {
12033 assert!(
12034 !editor.signature_help_state.is_shown(),
12035 "No signature help was called for"
12036 );
12037 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12038 });
12039 cx.run_until_parked();
12040 cx.update_editor(|editor, _, _| {
12041 assert!(
12042 !editor.signature_help_state.is_shown(),
12043 "No signature help should be shown when completions menu is open"
12044 );
12045 });
12046
12047 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12048 editor.context_menu_next(&Default::default(), window, cx);
12049 editor
12050 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12051 .unwrap()
12052 });
12053 cx.assert_editor_state(indoc! {"
12054 one.second_completionˇ
12055 two
12056 three
12057 "});
12058
12059 handle_resolve_completion_request(
12060 &mut cx,
12061 Some(vec![
12062 (
12063 //This overlaps with the primary completion edit which is
12064 //misbehavior from the LSP spec, test that we filter it out
12065 indoc! {"
12066 one.second_ˇcompletion
12067 two
12068 threeˇ
12069 "},
12070 "overlapping additional edit",
12071 ),
12072 (
12073 indoc! {"
12074 one.second_completion
12075 two
12076 threeˇ
12077 "},
12078 "\nadditional edit",
12079 ),
12080 ]),
12081 )
12082 .await;
12083 apply_additional_edits.await.unwrap();
12084 cx.assert_editor_state(indoc! {"
12085 one.second_completionˇ
12086 two
12087 three
12088 additional edit
12089 "});
12090
12091 cx.set_state(indoc! {"
12092 one.second_completion
12093 twoˇ
12094 threeˇ
12095 additional edit
12096 "});
12097 cx.simulate_keystroke(" ");
12098 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12099 cx.simulate_keystroke("s");
12100 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12101
12102 cx.assert_editor_state(indoc! {"
12103 one.second_completion
12104 two sˇ
12105 three sˇ
12106 additional edit
12107 "});
12108 handle_completion_request(
12109 indoc! {"
12110 one.second_completion
12111 two s
12112 three <s|>
12113 additional edit
12114 "},
12115 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12116 true,
12117 counter.clone(),
12118 &mut cx,
12119 )
12120 .await;
12121 cx.condition(|editor, _| editor.context_menu_visible())
12122 .await;
12123 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12124
12125 cx.simulate_keystroke("i");
12126
12127 handle_completion_request(
12128 indoc! {"
12129 one.second_completion
12130 two si
12131 three <si|>
12132 additional edit
12133 "},
12134 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12135 true,
12136 counter.clone(),
12137 &mut cx,
12138 )
12139 .await;
12140 cx.condition(|editor, _| editor.context_menu_visible())
12141 .await;
12142 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12143
12144 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12145 editor
12146 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12147 .unwrap()
12148 });
12149 cx.assert_editor_state(indoc! {"
12150 one.second_completion
12151 two sixth_completionˇ
12152 three sixth_completionˇ
12153 additional edit
12154 "});
12155
12156 apply_additional_edits.await.unwrap();
12157
12158 update_test_language_settings(&mut cx, |settings| {
12159 settings.defaults.show_completions_on_input = Some(false);
12160 });
12161 cx.set_state("editorˇ");
12162 cx.simulate_keystroke(".");
12163 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12164 cx.simulate_keystrokes("c l o");
12165 cx.assert_editor_state("editor.cloˇ");
12166 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12167 cx.update_editor(|editor, window, cx| {
12168 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12169 });
12170 handle_completion_request(
12171 "editor.<clo|>",
12172 vec!["close", "clobber"],
12173 true,
12174 counter.clone(),
12175 &mut cx,
12176 )
12177 .await;
12178 cx.condition(|editor, _| editor.context_menu_visible())
12179 .await;
12180 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
12181
12182 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12183 editor
12184 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12185 .unwrap()
12186 });
12187 cx.assert_editor_state("editor.clobberˇ");
12188 handle_resolve_completion_request(&mut cx, None).await;
12189 apply_additional_edits.await.unwrap();
12190}
12191
12192#[gpui::test]
12193async fn test_completion_reuse(cx: &mut TestAppContext) {
12194 init_test(cx, |_| {});
12195
12196 let mut cx = EditorLspTestContext::new_rust(
12197 lsp::ServerCapabilities {
12198 completion_provider: Some(lsp::CompletionOptions {
12199 trigger_characters: Some(vec![".".to_string()]),
12200 ..Default::default()
12201 }),
12202 ..Default::default()
12203 },
12204 cx,
12205 )
12206 .await;
12207
12208 let counter = Arc::new(AtomicUsize::new(0));
12209 cx.set_state("objˇ");
12210 cx.simulate_keystroke(".");
12211
12212 // Initial completion request returns complete results
12213 let is_incomplete = false;
12214 handle_completion_request(
12215 "obj.|<>",
12216 vec!["a", "ab", "abc"],
12217 is_incomplete,
12218 counter.clone(),
12219 &mut cx,
12220 )
12221 .await;
12222 cx.run_until_parked();
12223 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12224 cx.assert_editor_state("obj.ˇ");
12225 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12226
12227 // Type "a" - filters existing completions
12228 cx.simulate_keystroke("a");
12229 cx.run_until_parked();
12230 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12231 cx.assert_editor_state("obj.aˇ");
12232 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12233
12234 // Type "b" - filters existing completions
12235 cx.simulate_keystroke("b");
12236 cx.run_until_parked();
12237 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12238 cx.assert_editor_state("obj.abˇ");
12239 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12240
12241 // Type "c" - filters existing completions
12242 cx.simulate_keystroke("c");
12243 cx.run_until_parked();
12244 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12245 cx.assert_editor_state("obj.abcˇ");
12246 check_displayed_completions(vec!["abc"], &mut cx);
12247
12248 // Backspace to delete "c" - filters existing completions
12249 cx.update_editor(|editor, window, cx| {
12250 editor.backspace(&Backspace, window, cx);
12251 });
12252 cx.run_until_parked();
12253 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12254 cx.assert_editor_state("obj.abˇ");
12255 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12256
12257 // Moving cursor to the left dismisses menu.
12258 cx.update_editor(|editor, window, cx| {
12259 editor.move_left(&MoveLeft, window, cx);
12260 });
12261 cx.run_until_parked();
12262 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12263 cx.assert_editor_state("obj.aˇb");
12264 cx.update_editor(|editor, _, _| {
12265 assert_eq!(editor.context_menu_visible(), false);
12266 });
12267
12268 // Type "b" - new request
12269 cx.simulate_keystroke("b");
12270 let is_incomplete = false;
12271 handle_completion_request(
12272 "obj.<ab|>a",
12273 vec!["ab", "abc"],
12274 is_incomplete,
12275 counter.clone(),
12276 &mut cx,
12277 )
12278 .await;
12279 cx.run_until_parked();
12280 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12281 cx.assert_editor_state("obj.abˇb");
12282 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12283
12284 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
12285 cx.update_editor(|editor, window, cx| {
12286 editor.backspace(&Backspace, window, cx);
12287 });
12288 let is_incomplete = false;
12289 handle_completion_request(
12290 "obj.<a|>b",
12291 vec!["a", "ab", "abc"],
12292 is_incomplete,
12293 counter.clone(),
12294 &mut cx,
12295 )
12296 .await;
12297 cx.run_until_parked();
12298 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12299 cx.assert_editor_state("obj.aˇb");
12300 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12301
12302 // Backspace to delete "a" - dismisses menu.
12303 cx.update_editor(|editor, window, cx| {
12304 editor.backspace(&Backspace, window, cx);
12305 });
12306 cx.run_until_parked();
12307 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12308 cx.assert_editor_state("obj.ˇb");
12309 cx.update_editor(|editor, _, _| {
12310 assert_eq!(editor.context_menu_visible(), false);
12311 });
12312}
12313
12314#[gpui::test]
12315async fn test_word_completion(cx: &mut TestAppContext) {
12316 let lsp_fetch_timeout_ms = 10;
12317 init_test(cx, |language_settings| {
12318 language_settings.defaults.completions = Some(CompletionSettings {
12319 words: WordsCompletionMode::Fallback,
12320 lsp: true,
12321 lsp_fetch_timeout_ms: 10,
12322 lsp_insert_mode: LspInsertMode::Insert,
12323 });
12324 });
12325
12326 let mut cx = EditorLspTestContext::new_rust(
12327 lsp::ServerCapabilities {
12328 completion_provider: Some(lsp::CompletionOptions {
12329 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12330 ..lsp::CompletionOptions::default()
12331 }),
12332 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12333 ..lsp::ServerCapabilities::default()
12334 },
12335 cx,
12336 )
12337 .await;
12338
12339 let throttle_completions = Arc::new(AtomicBool::new(false));
12340
12341 let lsp_throttle_completions = throttle_completions.clone();
12342 let _completion_requests_handler =
12343 cx.lsp
12344 .server
12345 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
12346 let lsp_throttle_completions = lsp_throttle_completions.clone();
12347 let cx = cx.clone();
12348 async move {
12349 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
12350 cx.background_executor()
12351 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
12352 .await;
12353 }
12354 Ok(Some(lsp::CompletionResponse::Array(vec![
12355 lsp::CompletionItem {
12356 label: "first".into(),
12357 ..lsp::CompletionItem::default()
12358 },
12359 lsp::CompletionItem {
12360 label: "last".into(),
12361 ..lsp::CompletionItem::default()
12362 },
12363 ])))
12364 }
12365 });
12366
12367 cx.set_state(indoc! {"
12368 oneˇ
12369 two
12370 three
12371 "});
12372 cx.simulate_keystroke(".");
12373 cx.executor().run_until_parked();
12374 cx.condition(|editor, _| editor.context_menu_visible())
12375 .await;
12376 cx.update_editor(|editor, window, cx| {
12377 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12378 {
12379 assert_eq!(
12380 completion_menu_entries(&menu),
12381 &["first", "last"],
12382 "When LSP server is fast to reply, no fallback word completions are used"
12383 );
12384 } else {
12385 panic!("expected completion menu to be open");
12386 }
12387 editor.cancel(&Cancel, window, cx);
12388 });
12389 cx.executor().run_until_parked();
12390 cx.condition(|editor, _| !editor.context_menu_visible())
12391 .await;
12392
12393 throttle_completions.store(true, atomic::Ordering::Release);
12394 cx.simulate_keystroke(".");
12395 cx.executor()
12396 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
12397 cx.executor().run_until_parked();
12398 cx.condition(|editor, _| editor.context_menu_visible())
12399 .await;
12400 cx.update_editor(|editor, _, _| {
12401 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12402 {
12403 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
12404 "When LSP server is slow, document words can be shown instead, if configured accordingly");
12405 } else {
12406 panic!("expected completion menu to be open");
12407 }
12408 });
12409}
12410
12411#[gpui::test]
12412async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
12413 init_test(cx, |language_settings| {
12414 language_settings.defaults.completions = Some(CompletionSettings {
12415 words: WordsCompletionMode::Enabled,
12416 lsp: true,
12417 lsp_fetch_timeout_ms: 0,
12418 lsp_insert_mode: LspInsertMode::Insert,
12419 });
12420 });
12421
12422 let mut cx = EditorLspTestContext::new_rust(
12423 lsp::ServerCapabilities {
12424 completion_provider: Some(lsp::CompletionOptions {
12425 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12426 ..lsp::CompletionOptions::default()
12427 }),
12428 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12429 ..lsp::ServerCapabilities::default()
12430 },
12431 cx,
12432 )
12433 .await;
12434
12435 let _completion_requests_handler =
12436 cx.lsp
12437 .server
12438 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12439 Ok(Some(lsp::CompletionResponse::Array(vec![
12440 lsp::CompletionItem {
12441 label: "first".into(),
12442 ..lsp::CompletionItem::default()
12443 },
12444 lsp::CompletionItem {
12445 label: "last".into(),
12446 ..lsp::CompletionItem::default()
12447 },
12448 ])))
12449 });
12450
12451 cx.set_state(indoc! {"ˇ
12452 first
12453 last
12454 second
12455 "});
12456 cx.simulate_keystroke(".");
12457 cx.executor().run_until_parked();
12458 cx.condition(|editor, _| editor.context_menu_visible())
12459 .await;
12460 cx.update_editor(|editor, _, _| {
12461 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12462 {
12463 assert_eq!(
12464 completion_menu_entries(&menu),
12465 &["first", "last", "second"],
12466 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
12467 );
12468 } else {
12469 panic!("expected completion menu to be open");
12470 }
12471 });
12472}
12473
12474#[gpui::test]
12475async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
12476 init_test(cx, |language_settings| {
12477 language_settings.defaults.completions = Some(CompletionSettings {
12478 words: WordsCompletionMode::Disabled,
12479 lsp: true,
12480 lsp_fetch_timeout_ms: 0,
12481 lsp_insert_mode: LspInsertMode::Insert,
12482 });
12483 });
12484
12485 let mut cx = EditorLspTestContext::new_rust(
12486 lsp::ServerCapabilities {
12487 completion_provider: Some(lsp::CompletionOptions {
12488 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12489 ..lsp::CompletionOptions::default()
12490 }),
12491 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12492 ..lsp::ServerCapabilities::default()
12493 },
12494 cx,
12495 )
12496 .await;
12497
12498 let _completion_requests_handler =
12499 cx.lsp
12500 .server
12501 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12502 panic!("LSP completions should not be queried when dealing with word completions")
12503 });
12504
12505 cx.set_state(indoc! {"ˇ
12506 first
12507 last
12508 second
12509 "});
12510 cx.update_editor(|editor, window, cx| {
12511 editor.show_word_completions(&ShowWordCompletions, window, cx);
12512 });
12513 cx.executor().run_until_parked();
12514 cx.condition(|editor, _| editor.context_menu_visible())
12515 .await;
12516 cx.update_editor(|editor, _, _| {
12517 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12518 {
12519 assert_eq!(
12520 completion_menu_entries(&menu),
12521 &["first", "last", "second"],
12522 "`ShowWordCompletions` action should show word completions"
12523 );
12524 } else {
12525 panic!("expected completion menu to be open");
12526 }
12527 });
12528
12529 cx.simulate_keystroke("l");
12530 cx.executor().run_until_parked();
12531 cx.condition(|editor, _| editor.context_menu_visible())
12532 .await;
12533 cx.update_editor(|editor, _, _| {
12534 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12535 {
12536 assert_eq!(
12537 completion_menu_entries(&menu),
12538 &["last"],
12539 "After showing word completions, further editing should filter them and not query the LSP"
12540 );
12541 } else {
12542 panic!("expected completion menu to be open");
12543 }
12544 });
12545}
12546
12547#[gpui::test]
12548async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
12549 init_test(cx, |language_settings| {
12550 language_settings.defaults.completions = Some(CompletionSettings {
12551 words: WordsCompletionMode::Fallback,
12552 lsp: false,
12553 lsp_fetch_timeout_ms: 0,
12554 lsp_insert_mode: LspInsertMode::Insert,
12555 });
12556 });
12557
12558 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12559
12560 cx.set_state(indoc! {"ˇ
12561 0_usize
12562 let
12563 33
12564 4.5f32
12565 "});
12566 cx.update_editor(|editor, window, cx| {
12567 editor.show_completions(&ShowCompletions::default(), window, cx);
12568 });
12569 cx.executor().run_until_parked();
12570 cx.condition(|editor, _| editor.context_menu_visible())
12571 .await;
12572 cx.update_editor(|editor, window, cx| {
12573 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12574 {
12575 assert_eq!(
12576 completion_menu_entries(&menu),
12577 &["let"],
12578 "With no digits in the completion query, no digits should be in the word completions"
12579 );
12580 } else {
12581 panic!("expected completion menu to be open");
12582 }
12583 editor.cancel(&Cancel, window, cx);
12584 });
12585
12586 cx.set_state(indoc! {"3ˇ
12587 0_usize
12588 let
12589 3
12590 33.35f32
12591 "});
12592 cx.update_editor(|editor, window, cx| {
12593 editor.show_completions(&ShowCompletions::default(), window, cx);
12594 });
12595 cx.executor().run_until_parked();
12596 cx.condition(|editor, _| editor.context_menu_visible())
12597 .await;
12598 cx.update_editor(|editor, _, _| {
12599 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12600 {
12601 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
12602 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
12603 } else {
12604 panic!("expected completion menu to be open");
12605 }
12606 });
12607}
12608
12609fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
12610 let position = || lsp::Position {
12611 line: params.text_document_position.position.line,
12612 character: params.text_document_position.position.character,
12613 };
12614 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12615 range: lsp::Range {
12616 start: position(),
12617 end: position(),
12618 },
12619 new_text: text.to_string(),
12620 }))
12621}
12622
12623#[gpui::test]
12624async fn test_multiline_completion(cx: &mut TestAppContext) {
12625 init_test(cx, |_| {});
12626
12627 let fs = FakeFs::new(cx.executor());
12628 fs.insert_tree(
12629 path!("/a"),
12630 json!({
12631 "main.ts": "a",
12632 }),
12633 )
12634 .await;
12635
12636 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12637 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12638 let typescript_language = Arc::new(Language::new(
12639 LanguageConfig {
12640 name: "TypeScript".into(),
12641 matcher: LanguageMatcher {
12642 path_suffixes: vec!["ts".to_string()],
12643 ..LanguageMatcher::default()
12644 },
12645 line_comments: vec!["// ".into()],
12646 ..LanguageConfig::default()
12647 },
12648 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12649 ));
12650 language_registry.add(typescript_language.clone());
12651 let mut fake_servers = language_registry.register_fake_lsp(
12652 "TypeScript",
12653 FakeLspAdapter {
12654 capabilities: lsp::ServerCapabilities {
12655 completion_provider: Some(lsp::CompletionOptions {
12656 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12657 ..lsp::CompletionOptions::default()
12658 }),
12659 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12660 ..lsp::ServerCapabilities::default()
12661 },
12662 // Emulate vtsls label generation
12663 label_for_completion: Some(Box::new(|item, _| {
12664 let text = if let Some(description) = item
12665 .label_details
12666 .as_ref()
12667 .and_then(|label_details| label_details.description.as_ref())
12668 {
12669 format!("{} {}", item.label, description)
12670 } else if let Some(detail) = &item.detail {
12671 format!("{} {}", item.label, detail)
12672 } else {
12673 item.label.clone()
12674 };
12675 let len = text.len();
12676 Some(language::CodeLabel {
12677 text,
12678 runs: Vec::new(),
12679 filter_range: 0..len,
12680 })
12681 })),
12682 ..FakeLspAdapter::default()
12683 },
12684 );
12685 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12686 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12687 let worktree_id = workspace
12688 .update(cx, |workspace, _window, cx| {
12689 workspace.project().update(cx, |project, cx| {
12690 project.worktrees(cx).next().unwrap().read(cx).id()
12691 })
12692 })
12693 .unwrap();
12694 let _buffer = project
12695 .update(cx, |project, cx| {
12696 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
12697 })
12698 .await
12699 .unwrap();
12700 let editor = workspace
12701 .update(cx, |workspace, window, cx| {
12702 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
12703 })
12704 .unwrap()
12705 .await
12706 .unwrap()
12707 .downcast::<Editor>()
12708 .unwrap();
12709 let fake_server = fake_servers.next().await.unwrap();
12710
12711 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
12712 let multiline_label_2 = "a\nb\nc\n";
12713 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
12714 let multiline_description = "d\ne\nf\n";
12715 let multiline_detail_2 = "g\nh\ni\n";
12716
12717 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
12718 move |params, _| async move {
12719 Ok(Some(lsp::CompletionResponse::Array(vec![
12720 lsp::CompletionItem {
12721 label: multiline_label.to_string(),
12722 text_edit: gen_text_edit(¶ms, "new_text_1"),
12723 ..lsp::CompletionItem::default()
12724 },
12725 lsp::CompletionItem {
12726 label: "single line label 1".to_string(),
12727 detail: Some(multiline_detail.to_string()),
12728 text_edit: gen_text_edit(¶ms, "new_text_2"),
12729 ..lsp::CompletionItem::default()
12730 },
12731 lsp::CompletionItem {
12732 label: "single line label 2".to_string(),
12733 label_details: Some(lsp::CompletionItemLabelDetails {
12734 description: Some(multiline_description.to_string()),
12735 detail: None,
12736 }),
12737 text_edit: gen_text_edit(¶ms, "new_text_2"),
12738 ..lsp::CompletionItem::default()
12739 },
12740 lsp::CompletionItem {
12741 label: multiline_label_2.to_string(),
12742 detail: Some(multiline_detail_2.to_string()),
12743 text_edit: gen_text_edit(¶ms, "new_text_3"),
12744 ..lsp::CompletionItem::default()
12745 },
12746 lsp::CompletionItem {
12747 label: "Label with many spaces and \t but without newlines".to_string(),
12748 detail: Some(
12749 "Details with many spaces and \t but without newlines".to_string(),
12750 ),
12751 text_edit: gen_text_edit(¶ms, "new_text_4"),
12752 ..lsp::CompletionItem::default()
12753 },
12754 ])))
12755 },
12756 );
12757
12758 editor.update_in(cx, |editor, window, cx| {
12759 cx.focus_self(window);
12760 editor.move_to_end(&MoveToEnd, window, cx);
12761 editor.handle_input(".", window, cx);
12762 });
12763 cx.run_until_parked();
12764 completion_handle.next().await.unwrap();
12765
12766 editor.update(cx, |editor, _| {
12767 assert!(editor.context_menu_visible());
12768 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12769 {
12770 let completion_labels = menu
12771 .completions
12772 .borrow()
12773 .iter()
12774 .map(|c| c.label.text.clone())
12775 .collect::<Vec<_>>();
12776 assert_eq!(
12777 completion_labels,
12778 &[
12779 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
12780 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
12781 "single line label 2 d e f ",
12782 "a b c g h i ",
12783 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
12784 ],
12785 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
12786 );
12787
12788 for completion in menu
12789 .completions
12790 .borrow()
12791 .iter() {
12792 assert_eq!(
12793 completion.label.filter_range,
12794 0..completion.label.text.len(),
12795 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
12796 );
12797 }
12798 } else {
12799 panic!("expected completion menu to be open");
12800 }
12801 });
12802}
12803
12804#[gpui::test]
12805async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
12806 init_test(cx, |_| {});
12807 let mut cx = EditorLspTestContext::new_rust(
12808 lsp::ServerCapabilities {
12809 completion_provider: Some(lsp::CompletionOptions {
12810 trigger_characters: Some(vec![".".to_string()]),
12811 ..Default::default()
12812 }),
12813 ..Default::default()
12814 },
12815 cx,
12816 )
12817 .await;
12818 cx.lsp
12819 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12820 Ok(Some(lsp::CompletionResponse::Array(vec![
12821 lsp::CompletionItem {
12822 label: "first".into(),
12823 ..Default::default()
12824 },
12825 lsp::CompletionItem {
12826 label: "last".into(),
12827 ..Default::default()
12828 },
12829 ])))
12830 });
12831 cx.set_state("variableˇ");
12832 cx.simulate_keystroke(".");
12833 cx.executor().run_until_parked();
12834
12835 cx.update_editor(|editor, _, _| {
12836 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12837 {
12838 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
12839 } else {
12840 panic!("expected completion menu to be open");
12841 }
12842 });
12843
12844 cx.update_editor(|editor, window, cx| {
12845 editor.move_page_down(&MovePageDown::default(), window, cx);
12846 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12847 {
12848 assert!(
12849 menu.selected_item == 1,
12850 "expected PageDown to select the last item from the context menu"
12851 );
12852 } else {
12853 panic!("expected completion menu to stay open after PageDown");
12854 }
12855 });
12856
12857 cx.update_editor(|editor, window, cx| {
12858 editor.move_page_up(&MovePageUp::default(), window, cx);
12859 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12860 {
12861 assert!(
12862 menu.selected_item == 0,
12863 "expected PageUp to select the first item from the context menu"
12864 );
12865 } else {
12866 panic!("expected completion menu to stay open after PageUp");
12867 }
12868 });
12869}
12870
12871#[gpui::test]
12872async fn test_as_is_completions(cx: &mut TestAppContext) {
12873 init_test(cx, |_| {});
12874 let mut cx = EditorLspTestContext::new_rust(
12875 lsp::ServerCapabilities {
12876 completion_provider: Some(lsp::CompletionOptions {
12877 ..Default::default()
12878 }),
12879 ..Default::default()
12880 },
12881 cx,
12882 )
12883 .await;
12884 cx.lsp
12885 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12886 Ok(Some(lsp::CompletionResponse::Array(vec![
12887 lsp::CompletionItem {
12888 label: "unsafe".into(),
12889 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12890 range: lsp::Range {
12891 start: lsp::Position {
12892 line: 1,
12893 character: 2,
12894 },
12895 end: lsp::Position {
12896 line: 1,
12897 character: 3,
12898 },
12899 },
12900 new_text: "unsafe".to_string(),
12901 })),
12902 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
12903 ..Default::default()
12904 },
12905 ])))
12906 });
12907 cx.set_state("fn a() {}\n nˇ");
12908 cx.executor().run_until_parked();
12909 cx.update_editor(|editor, window, cx| {
12910 editor.show_completions(
12911 &ShowCompletions {
12912 trigger: Some("\n".into()),
12913 },
12914 window,
12915 cx,
12916 );
12917 });
12918 cx.executor().run_until_parked();
12919
12920 cx.update_editor(|editor, window, cx| {
12921 editor.confirm_completion(&Default::default(), window, cx)
12922 });
12923 cx.executor().run_until_parked();
12924 cx.assert_editor_state("fn a() {}\n unsafeˇ");
12925}
12926
12927#[gpui::test]
12928async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
12929 init_test(cx, |_| {});
12930
12931 let mut cx = EditorLspTestContext::new_rust(
12932 lsp::ServerCapabilities {
12933 completion_provider: Some(lsp::CompletionOptions {
12934 trigger_characters: Some(vec![".".to_string()]),
12935 resolve_provider: Some(true),
12936 ..Default::default()
12937 }),
12938 ..Default::default()
12939 },
12940 cx,
12941 )
12942 .await;
12943
12944 cx.set_state("fn main() { let a = 2ˇ; }");
12945 cx.simulate_keystroke(".");
12946 let completion_item = lsp::CompletionItem {
12947 label: "Some".into(),
12948 kind: Some(lsp::CompletionItemKind::SNIPPET),
12949 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
12950 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
12951 kind: lsp::MarkupKind::Markdown,
12952 value: "```rust\nSome(2)\n```".to_string(),
12953 })),
12954 deprecated: Some(false),
12955 sort_text: Some("Some".to_string()),
12956 filter_text: Some("Some".to_string()),
12957 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
12958 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12959 range: lsp::Range {
12960 start: lsp::Position {
12961 line: 0,
12962 character: 22,
12963 },
12964 end: lsp::Position {
12965 line: 0,
12966 character: 22,
12967 },
12968 },
12969 new_text: "Some(2)".to_string(),
12970 })),
12971 additional_text_edits: Some(vec![lsp::TextEdit {
12972 range: lsp::Range {
12973 start: lsp::Position {
12974 line: 0,
12975 character: 20,
12976 },
12977 end: lsp::Position {
12978 line: 0,
12979 character: 22,
12980 },
12981 },
12982 new_text: "".to_string(),
12983 }]),
12984 ..Default::default()
12985 };
12986
12987 let closure_completion_item = completion_item.clone();
12988 let counter = Arc::new(AtomicUsize::new(0));
12989 let counter_clone = counter.clone();
12990 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
12991 let task_completion_item = closure_completion_item.clone();
12992 counter_clone.fetch_add(1, atomic::Ordering::Release);
12993 async move {
12994 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
12995 is_incomplete: true,
12996 item_defaults: None,
12997 items: vec![task_completion_item],
12998 })))
12999 }
13000 });
13001
13002 cx.condition(|editor, _| editor.context_menu_visible())
13003 .await;
13004 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
13005 assert!(request.next().await.is_some());
13006 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13007
13008 cx.simulate_keystrokes("S o m");
13009 cx.condition(|editor, _| editor.context_menu_visible())
13010 .await;
13011 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
13012 assert!(request.next().await.is_some());
13013 assert!(request.next().await.is_some());
13014 assert!(request.next().await.is_some());
13015 request.close();
13016 assert!(request.next().await.is_none());
13017 assert_eq!(
13018 counter.load(atomic::Ordering::Acquire),
13019 4,
13020 "With the completions menu open, only one LSP request should happen per input"
13021 );
13022}
13023
13024#[gpui::test]
13025async fn test_toggle_comment(cx: &mut TestAppContext) {
13026 init_test(cx, |_| {});
13027 let mut cx = EditorTestContext::new(cx).await;
13028 let language = Arc::new(Language::new(
13029 LanguageConfig {
13030 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13031 ..Default::default()
13032 },
13033 Some(tree_sitter_rust::LANGUAGE.into()),
13034 ));
13035 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13036
13037 // If multiple selections intersect a line, the line is only toggled once.
13038 cx.set_state(indoc! {"
13039 fn a() {
13040 «//b();
13041 ˇ»// «c();
13042 //ˇ» d();
13043 }
13044 "});
13045
13046 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13047
13048 cx.assert_editor_state(indoc! {"
13049 fn a() {
13050 «b();
13051 c();
13052 ˇ» d();
13053 }
13054 "});
13055
13056 // The comment prefix is inserted at the same column for every line in a
13057 // selection.
13058 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13059
13060 cx.assert_editor_state(indoc! {"
13061 fn a() {
13062 // «b();
13063 // c();
13064 ˇ»// d();
13065 }
13066 "});
13067
13068 // If a selection ends at the beginning of a line, that line is not toggled.
13069 cx.set_selections_state(indoc! {"
13070 fn a() {
13071 // b();
13072 «// c();
13073 ˇ» // d();
13074 }
13075 "});
13076
13077 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13078
13079 cx.assert_editor_state(indoc! {"
13080 fn a() {
13081 // b();
13082 «c();
13083 ˇ» // d();
13084 }
13085 "});
13086
13087 // If a selection span a single line and is empty, the line is toggled.
13088 cx.set_state(indoc! {"
13089 fn a() {
13090 a();
13091 b();
13092 ˇ
13093 }
13094 "});
13095
13096 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13097
13098 cx.assert_editor_state(indoc! {"
13099 fn a() {
13100 a();
13101 b();
13102 //•ˇ
13103 }
13104 "});
13105
13106 // If a selection span multiple lines, empty lines are not toggled.
13107 cx.set_state(indoc! {"
13108 fn a() {
13109 «a();
13110
13111 c();ˇ»
13112 }
13113 "});
13114
13115 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13116
13117 cx.assert_editor_state(indoc! {"
13118 fn a() {
13119 // «a();
13120
13121 // c();ˇ»
13122 }
13123 "});
13124
13125 // If a selection includes multiple comment prefixes, all lines are uncommented.
13126 cx.set_state(indoc! {"
13127 fn a() {
13128 «// a();
13129 /// b();
13130 //! c();ˇ»
13131 }
13132 "});
13133
13134 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13135
13136 cx.assert_editor_state(indoc! {"
13137 fn a() {
13138 «a();
13139 b();
13140 c();ˇ»
13141 }
13142 "});
13143}
13144
13145#[gpui::test]
13146async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
13147 init_test(cx, |_| {});
13148 let mut cx = EditorTestContext::new(cx).await;
13149 let language = Arc::new(Language::new(
13150 LanguageConfig {
13151 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13152 ..Default::default()
13153 },
13154 Some(tree_sitter_rust::LANGUAGE.into()),
13155 ));
13156 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13157
13158 let toggle_comments = &ToggleComments {
13159 advance_downwards: false,
13160 ignore_indent: true,
13161 };
13162
13163 // If multiple selections intersect a line, the line is only toggled once.
13164 cx.set_state(indoc! {"
13165 fn a() {
13166 // «b();
13167 // c();
13168 // ˇ» d();
13169 }
13170 "});
13171
13172 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13173
13174 cx.assert_editor_state(indoc! {"
13175 fn a() {
13176 «b();
13177 c();
13178 ˇ» d();
13179 }
13180 "});
13181
13182 // The comment prefix is inserted at the beginning of each line
13183 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13184
13185 cx.assert_editor_state(indoc! {"
13186 fn a() {
13187 // «b();
13188 // c();
13189 // ˇ» d();
13190 }
13191 "});
13192
13193 // If a selection ends at the beginning of a line, that line is not toggled.
13194 cx.set_selections_state(indoc! {"
13195 fn a() {
13196 // b();
13197 // «c();
13198 ˇ»// d();
13199 }
13200 "});
13201
13202 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13203
13204 cx.assert_editor_state(indoc! {"
13205 fn a() {
13206 // b();
13207 «c();
13208 ˇ»// d();
13209 }
13210 "});
13211
13212 // If a selection span a single line and is empty, the line is toggled.
13213 cx.set_state(indoc! {"
13214 fn a() {
13215 a();
13216 b();
13217 ˇ
13218 }
13219 "});
13220
13221 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13222
13223 cx.assert_editor_state(indoc! {"
13224 fn a() {
13225 a();
13226 b();
13227 //ˇ
13228 }
13229 "});
13230
13231 // If a selection span multiple lines, empty lines are not toggled.
13232 cx.set_state(indoc! {"
13233 fn a() {
13234 «a();
13235
13236 c();ˇ»
13237 }
13238 "});
13239
13240 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13241
13242 cx.assert_editor_state(indoc! {"
13243 fn a() {
13244 // «a();
13245
13246 // c();ˇ»
13247 }
13248 "});
13249
13250 // If a selection includes multiple comment prefixes, all lines are uncommented.
13251 cx.set_state(indoc! {"
13252 fn a() {
13253 // «a();
13254 /// b();
13255 //! c();ˇ»
13256 }
13257 "});
13258
13259 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13260
13261 cx.assert_editor_state(indoc! {"
13262 fn a() {
13263 «a();
13264 b();
13265 c();ˇ»
13266 }
13267 "});
13268}
13269
13270#[gpui::test]
13271async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
13272 init_test(cx, |_| {});
13273
13274 let language = Arc::new(Language::new(
13275 LanguageConfig {
13276 line_comments: vec!["// ".into()],
13277 ..Default::default()
13278 },
13279 Some(tree_sitter_rust::LANGUAGE.into()),
13280 ));
13281
13282 let mut cx = EditorTestContext::new(cx).await;
13283
13284 cx.language_registry().add(language.clone());
13285 cx.update_buffer(|buffer, cx| {
13286 buffer.set_language(Some(language), cx);
13287 });
13288
13289 let toggle_comments = &ToggleComments {
13290 advance_downwards: true,
13291 ignore_indent: false,
13292 };
13293
13294 // Single cursor on one line -> advance
13295 // Cursor moves horizontally 3 characters as well on non-blank line
13296 cx.set_state(indoc!(
13297 "fn a() {
13298 ˇdog();
13299 cat();
13300 }"
13301 ));
13302 cx.update_editor(|editor, window, cx| {
13303 editor.toggle_comments(toggle_comments, window, cx);
13304 });
13305 cx.assert_editor_state(indoc!(
13306 "fn a() {
13307 // dog();
13308 catˇ();
13309 }"
13310 ));
13311
13312 // Single selection on one line -> don't advance
13313 cx.set_state(indoc!(
13314 "fn a() {
13315 «dog()ˇ»;
13316 cat();
13317 }"
13318 ));
13319 cx.update_editor(|editor, window, cx| {
13320 editor.toggle_comments(toggle_comments, window, cx);
13321 });
13322 cx.assert_editor_state(indoc!(
13323 "fn a() {
13324 // «dog()ˇ»;
13325 cat();
13326 }"
13327 ));
13328
13329 // Multiple cursors on one line -> advance
13330 cx.set_state(indoc!(
13331 "fn a() {
13332 ˇdˇog();
13333 cat();
13334 }"
13335 ));
13336 cx.update_editor(|editor, window, cx| {
13337 editor.toggle_comments(toggle_comments, window, cx);
13338 });
13339 cx.assert_editor_state(indoc!(
13340 "fn a() {
13341 // dog();
13342 catˇ(ˇ);
13343 }"
13344 ));
13345
13346 // Multiple cursors on one line, with selection -> don't advance
13347 cx.set_state(indoc!(
13348 "fn a() {
13349 ˇdˇog«()ˇ»;
13350 cat();
13351 }"
13352 ));
13353 cx.update_editor(|editor, window, cx| {
13354 editor.toggle_comments(toggle_comments, window, cx);
13355 });
13356 cx.assert_editor_state(indoc!(
13357 "fn a() {
13358 // ˇdˇog«()ˇ»;
13359 cat();
13360 }"
13361 ));
13362
13363 // Single cursor on one line -> advance
13364 // Cursor moves to column 0 on blank line
13365 cx.set_state(indoc!(
13366 "fn a() {
13367 ˇdog();
13368
13369 cat();
13370 }"
13371 ));
13372 cx.update_editor(|editor, window, cx| {
13373 editor.toggle_comments(toggle_comments, window, cx);
13374 });
13375 cx.assert_editor_state(indoc!(
13376 "fn a() {
13377 // dog();
13378 ˇ
13379 cat();
13380 }"
13381 ));
13382
13383 // Single cursor on one line -> advance
13384 // Cursor starts and ends at column 0
13385 cx.set_state(indoc!(
13386 "fn a() {
13387 ˇ dog();
13388 cat();
13389 }"
13390 ));
13391 cx.update_editor(|editor, window, cx| {
13392 editor.toggle_comments(toggle_comments, window, cx);
13393 });
13394 cx.assert_editor_state(indoc!(
13395 "fn a() {
13396 // dog();
13397 ˇ cat();
13398 }"
13399 ));
13400}
13401
13402#[gpui::test]
13403async fn test_toggle_block_comment(cx: &mut TestAppContext) {
13404 init_test(cx, |_| {});
13405
13406 let mut cx = EditorTestContext::new(cx).await;
13407
13408 let html_language = Arc::new(
13409 Language::new(
13410 LanguageConfig {
13411 name: "HTML".into(),
13412 block_comment: Some(("<!-- ".into(), " -->".into())),
13413 ..Default::default()
13414 },
13415 Some(tree_sitter_html::LANGUAGE.into()),
13416 )
13417 .with_injection_query(
13418 r#"
13419 (script_element
13420 (raw_text) @injection.content
13421 (#set! injection.language "javascript"))
13422 "#,
13423 )
13424 .unwrap(),
13425 );
13426
13427 let javascript_language = Arc::new(Language::new(
13428 LanguageConfig {
13429 name: "JavaScript".into(),
13430 line_comments: vec!["// ".into()],
13431 ..Default::default()
13432 },
13433 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
13434 ));
13435
13436 cx.language_registry().add(html_language.clone());
13437 cx.language_registry().add(javascript_language.clone());
13438 cx.update_buffer(|buffer, cx| {
13439 buffer.set_language(Some(html_language), cx);
13440 });
13441
13442 // Toggle comments for empty selections
13443 cx.set_state(
13444 &r#"
13445 <p>A</p>ˇ
13446 <p>B</p>ˇ
13447 <p>C</p>ˇ
13448 "#
13449 .unindent(),
13450 );
13451 cx.update_editor(|editor, window, cx| {
13452 editor.toggle_comments(&ToggleComments::default(), window, cx)
13453 });
13454 cx.assert_editor_state(
13455 &r#"
13456 <!-- <p>A</p>ˇ -->
13457 <!-- <p>B</p>ˇ -->
13458 <!-- <p>C</p>ˇ -->
13459 "#
13460 .unindent(),
13461 );
13462 cx.update_editor(|editor, window, cx| {
13463 editor.toggle_comments(&ToggleComments::default(), window, cx)
13464 });
13465 cx.assert_editor_state(
13466 &r#"
13467 <p>A</p>ˇ
13468 <p>B</p>ˇ
13469 <p>C</p>ˇ
13470 "#
13471 .unindent(),
13472 );
13473
13474 // Toggle comments for mixture of empty and non-empty selections, where
13475 // multiple selections occupy a given line.
13476 cx.set_state(
13477 &r#"
13478 <p>A«</p>
13479 <p>ˇ»B</p>ˇ
13480 <p>C«</p>
13481 <p>ˇ»D</p>ˇ
13482 "#
13483 .unindent(),
13484 );
13485
13486 cx.update_editor(|editor, window, cx| {
13487 editor.toggle_comments(&ToggleComments::default(), window, cx)
13488 });
13489 cx.assert_editor_state(
13490 &r#"
13491 <!-- <p>A«</p>
13492 <p>ˇ»B</p>ˇ -->
13493 <!-- <p>C«</p>
13494 <p>ˇ»D</p>ˇ -->
13495 "#
13496 .unindent(),
13497 );
13498 cx.update_editor(|editor, window, cx| {
13499 editor.toggle_comments(&ToggleComments::default(), window, cx)
13500 });
13501 cx.assert_editor_state(
13502 &r#"
13503 <p>A«</p>
13504 <p>ˇ»B</p>ˇ
13505 <p>C«</p>
13506 <p>ˇ»D</p>ˇ
13507 "#
13508 .unindent(),
13509 );
13510
13511 // Toggle comments when different languages are active for different
13512 // selections.
13513 cx.set_state(
13514 &r#"
13515 ˇ<script>
13516 ˇvar x = new Y();
13517 ˇ</script>
13518 "#
13519 .unindent(),
13520 );
13521 cx.executor().run_until_parked();
13522 cx.update_editor(|editor, window, cx| {
13523 editor.toggle_comments(&ToggleComments::default(), window, cx)
13524 });
13525 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
13526 // Uncommenting and commenting from this position brings in even more wrong artifacts.
13527 cx.assert_editor_state(
13528 &r#"
13529 <!-- ˇ<script> -->
13530 // ˇvar x = new Y();
13531 <!-- ˇ</script> -->
13532 "#
13533 .unindent(),
13534 );
13535}
13536
13537#[gpui::test]
13538fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
13539 init_test(cx, |_| {});
13540
13541 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13542 let multibuffer = cx.new(|cx| {
13543 let mut multibuffer = MultiBuffer::new(ReadWrite);
13544 multibuffer.push_excerpts(
13545 buffer.clone(),
13546 [
13547 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
13548 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
13549 ],
13550 cx,
13551 );
13552 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
13553 multibuffer
13554 });
13555
13556 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
13557 editor.update_in(cx, |editor, window, cx| {
13558 assert_eq!(editor.text(cx), "aaaa\nbbbb");
13559 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13560 s.select_ranges([
13561 Point::new(0, 0)..Point::new(0, 0),
13562 Point::new(1, 0)..Point::new(1, 0),
13563 ])
13564 });
13565
13566 editor.handle_input("X", window, cx);
13567 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
13568 assert_eq!(
13569 editor.selections.ranges(cx),
13570 [
13571 Point::new(0, 1)..Point::new(0, 1),
13572 Point::new(1, 1)..Point::new(1, 1),
13573 ]
13574 );
13575
13576 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
13577 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13578 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
13579 });
13580 editor.backspace(&Default::default(), window, cx);
13581 assert_eq!(editor.text(cx), "Xa\nbbb");
13582 assert_eq!(
13583 editor.selections.ranges(cx),
13584 [Point::new(1, 0)..Point::new(1, 0)]
13585 );
13586
13587 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13588 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
13589 });
13590 editor.backspace(&Default::default(), window, cx);
13591 assert_eq!(editor.text(cx), "X\nbb");
13592 assert_eq!(
13593 editor.selections.ranges(cx),
13594 [Point::new(0, 1)..Point::new(0, 1)]
13595 );
13596 });
13597}
13598
13599#[gpui::test]
13600fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
13601 init_test(cx, |_| {});
13602
13603 let markers = vec![('[', ']').into(), ('(', ')').into()];
13604 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
13605 indoc! {"
13606 [aaaa
13607 (bbbb]
13608 cccc)",
13609 },
13610 markers.clone(),
13611 );
13612 let excerpt_ranges = markers.into_iter().map(|marker| {
13613 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
13614 ExcerptRange::new(context.clone())
13615 });
13616 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
13617 let multibuffer = cx.new(|cx| {
13618 let mut multibuffer = MultiBuffer::new(ReadWrite);
13619 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
13620 multibuffer
13621 });
13622
13623 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
13624 editor.update_in(cx, |editor, window, cx| {
13625 let (expected_text, selection_ranges) = marked_text_ranges(
13626 indoc! {"
13627 aaaa
13628 bˇbbb
13629 bˇbbˇb
13630 cccc"
13631 },
13632 true,
13633 );
13634 assert_eq!(editor.text(cx), expected_text);
13635 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13636 s.select_ranges(selection_ranges)
13637 });
13638
13639 editor.handle_input("X", window, cx);
13640
13641 let (expected_text, expected_selections) = marked_text_ranges(
13642 indoc! {"
13643 aaaa
13644 bXˇbbXb
13645 bXˇbbXˇb
13646 cccc"
13647 },
13648 false,
13649 );
13650 assert_eq!(editor.text(cx), expected_text);
13651 assert_eq!(editor.selections.ranges(cx), expected_selections);
13652
13653 editor.newline(&Newline, window, cx);
13654 let (expected_text, expected_selections) = marked_text_ranges(
13655 indoc! {"
13656 aaaa
13657 bX
13658 ˇbbX
13659 b
13660 bX
13661 ˇbbX
13662 ˇb
13663 cccc"
13664 },
13665 false,
13666 );
13667 assert_eq!(editor.text(cx), expected_text);
13668 assert_eq!(editor.selections.ranges(cx), expected_selections);
13669 });
13670}
13671
13672#[gpui::test]
13673fn test_refresh_selections(cx: &mut TestAppContext) {
13674 init_test(cx, |_| {});
13675
13676 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13677 let mut excerpt1_id = None;
13678 let multibuffer = cx.new(|cx| {
13679 let mut multibuffer = MultiBuffer::new(ReadWrite);
13680 excerpt1_id = multibuffer
13681 .push_excerpts(
13682 buffer.clone(),
13683 [
13684 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
13685 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
13686 ],
13687 cx,
13688 )
13689 .into_iter()
13690 .next();
13691 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
13692 multibuffer
13693 });
13694
13695 let editor = cx.add_window(|window, cx| {
13696 let mut editor = build_editor(multibuffer.clone(), window, cx);
13697 let snapshot = editor.snapshot(window, cx);
13698 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13699 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
13700 });
13701 editor.begin_selection(
13702 Point::new(2, 1).to_display_point(&snapshot),
13703 true,
13704 1,
13705 window,
13706 cx,
13707 );
13708 assert_eq!(
13709 editor.selections.ranges(cx),
13710 [
13711 Point::new(1, 3)..Point::new(1, 3),
13712 Point::new(2, 1)..Point::new(2, 1),
13713 ]
13714 );
13715 editor
13716 });
13717
13718 // Refreshing selections is a no-op when excerpts haven't changed.
13719 _ = editor.update(cx, |editor, window, cx| {
13720 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
13721 assert_eq!(
13722 editor.selections.ranges(cx),
13723 [
13724 Point::new(1, 3)..Point::new(1, 3),
13725 Point::new(2, 1)..Point::new(2, 1),
13726 ]
13727 );
13728 });
13729
13730 multibuffer.update(cx, |multibuffer, cx| {
13731 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
13732 });
13733 _ = editor.update(cx, |editor, window, cx| {
13734 // Removing an excerpt causes the first selection to become degenerate.
13735 assert_eq!(
13736 editor.selections.ranges(cx),
13737 [
13738 Point::new(0, 0)..Point::new(0, 0),
13739 Point::new(0, 1)..Point::new(0, 1)
13740 ]
13741 );
13742
13743 // Refreshing selections will relocate the first selection to the original buffer
13744 // location.
13745 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
13746 assert_eq!(
13747 editor.selections.ranges(cx),
13748 [
13749 Point::new(0, 1)..Point::new(0, 1),
13750 Point::new(0, 3)..Point::new(0, 3)
13751 ]
13752 );
13753 assert!(editor.selections.pending_anchor().is_some());
13754 });
13755}
13756
13757#[gpui::test]
13758fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
13759 init_test(cx, |_| {});
13760
13761 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13762 let mut excerpt1_id = None;
13763 let multibuffer = cx.new(|cx| {
13764 let mut multibuffer = MultiBuffer::new(ReadWrite);
13765 excerpt1_id = multibuffer
13766 .push_excerpts(
13767 buffer.clone(),
13768 [
13769 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
13770 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
13771 ],
13772 cx,
13773 )
13774 .into_iter()
13775 .next();
13776 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
13777 multibuffer
13778 });
13779
13780 let editor = cx.add_window(|window, cx| {
13781 let mut editor = build_editor(multibuffer.clone(), window, cx);
13782 let snapshot = editor.snapshot(window, cx);
13783 editor.begin_selection(
13784 Point::new(1, 3).to_display_point(&snapshot),
13785 false,
13786 1,
13787 window,
13788 cx,
13789 );
13790 assert_eq!(
13791 editor.selections.ranges(cx),
13792 [Point::new(1, 3)..Point::new(1, 3)]
13793 );
13794 editor
13795 });
13796
13797 multibuffer.update(cx, |multibuffer, cx| {
13798 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
13799 });
13800 _ = editor.update(cx, |editor, window, cx| {
13801 assert_eq!(
13802 editor.selections.ranges(cx),
13803 [Point::new(0, 0)..Point::new(0, 0)]
13804 );
13805
13806 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
13807 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
13808 assert_eq!(
13809 editor.selections.ranges(cx),
13810 [Point::new(0, 3)..Point::new(0, 3)]
13811 );
13812 assert!(editor.selections.pending_anchor().is_some());
13813 });
13814}
13815
13816#[gpui::test]
13817async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
13818 init_test(cx, |_| {});
13819
13820 let language = Arc::new(
13821 Language::new(
13822 LanguageConfig {
13823 brackets: BracketPairConfig {
13824 pairs: vec![
13825 BracketPair {
13826 start: "{".to_string(),
13827 end: "}".to_string(),
13828 close: true,
13829 surround: true,
13830 newline: true,
13831 },
13832 BracketPair {
13833 start: "/* ".to_string(),
13834 end: " */".to_string(),
13835 close: true,
13836 surround: true,
13837 newline: true,
13838 },
13839 ],
13840 ..Default::default()
13841 },
13842 ..Default::default()
13843 },
13844 Some(tree_sitter_rust::LANGUAGE.into()),
13845 )
13846 .with_indents_query("")
13847 .unwrap(),
13848 );
13849
13850 let text = concat!(
13851 "{ }\n", //
13852 " x\n", //
13853 " /* */\n", //
13854 "x\n", //
13855 "{{} }\n", //
13856 );
13857
13858 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
13859 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13860 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
13861 editor
13862 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
13863 .await;
13864
13865 editor.update_in(cx, |editor, window, cx| {
13866 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13867 s.select_display_ranges([
13868 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
13869 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
13870 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
13871 ])
13872 });
13873 editor.newline(&Newline, window, cx);
13874
13875 assert_eq!(
13876 editor.buffer().read(cx).read(cx).text(),
13877 concat!(
13878 "{ \n", // Suppress rustfmt
13879 "\n", //
13880 "}\n", //
13881 " x\n", //
13882 " /* \n", //
13883 " \n", //
13884 " */\n", //
13885 "x\n", //
13886 "{{} \n", //
13887 "}\n", //
13888 )
13889 );
13890 });
13891}
13892
13893#[gpui::test]
13894fn test_highlighted_ranges(cx: &mut TestAppContext) {
13895 init_test(cx, |_| {});
13896
13897 let editor = cx.add_window(|window, cx| {
13898 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
13899 build_editor(buffer.clone(), window, cx)
13900 });
13901
13902 _ = editor.update(cx, |editor, window, cx| {
13903 struct Type1;
13904 struct Type2;
13905
13906 let buffer = editor.buffer.read(cx).snapshot(cx);
13907
13908 let anchor_range =
13909 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
13910
13911 editor.highlight_background::<Type1>(
13912 &[
13913 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
13914 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
13915 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
13916 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
13917 ],
13918 |_| Hsla::red(),
13919 cx,
13920 );
13921 editor.highlight_background::<Type2>(
13922 &[
13923 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
13924 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
13925 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
13926 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
13927 ],
13928 |_| Hsla::green(),
13929 cx,
13930 );
13931
13932 let snapshot = editor.snapshot(window, cx);
13933 let mut highlighted_ranges = editor.background_highlights_in_range(
13934 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
13935 &snapshot,
13936 cx.theme(),
13937 );
13938 // Enforce a consistent ordering based on color without relying on the ordering of the
13939 // highlight's `TypeId` which is non-executor.
13940 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
13941 assert_eq!(
13942 highlighted_ranges,
13943 &[
13944 (
13945 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
13946 Hsla::red(),
13947 ),
13948 (
13949 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
13950 Hsla::red(),
13951 ),
13952 (
13953 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
13954 Hsla::green(),
13955 ),
13956 (
13957 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
13958 Hsla::green(),
13959 ),
13960 ]
13961 );
13962 assert_eq!(
13963 editor.background_highlights_in_range(
13964 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
13965 &snapshot,
13966 cx.theme(),
13967 ),
13968 &[(
13969 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
13970 Hsla::red(),
13971 )]
13972 );
13973 });
13974}
13975
13976#[gpui::test]
13977async fn test_following(cx: &mut TestAppContext) {
13978 init_test(cx, |_| {});
13979
13980 let fs = FakeFs::new(cx.executor());
13981 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
13982
13983 let buffer = project.update(cx, |project, cx| {
13984 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
13985 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
13986 });
13987 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
13988 let follower = cx.update(|cx| {
13989 cx.open_window(
13990 WindowOptions {
13991 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
13992 gpui::Point::new(px(0.), px(0.)),
13993 gpui::Point::new(px(10.), px(80.)),
13994 ))),
13995 ..Default::default()
13996 },
13997 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
13998 )
13999 .unwrap()
14000 });
14001
14002 let is_still_following = Rc::new(RefCell::new(true));
14003 let follower_edit_event_count = Rc::new(RefCell::new(0));
14004 let pending_update = Rc::new(RefCell::new(None));
14005 let leader_entity = leader.root(cx).unwrap();
14006 let follower_entity = follower.root(cx).unwrap();
14007 _ = follower.update(cx, {
14008 let update = pending_update.clone();
14009 let is_still_following = is_still_following.clone();
14010 let follower_edit_event_count = follower_edit_event_count.clone();
14011 |_, window, cx| {
14012 cx.subscribe_in(
14013 &leader_entity,
14014 window,
14015 move |_, leader, event, window, cx| {
14016 leader.read(cx).add_event_to_update_proto(
14017 event,
14018 &mut update.borrow_mut(),
14019 window,
14020 cx,
14021 );
14022 },
14023 )
14024 .detach();
14025
14026 cx.subscribe_in(
14027 &follower_entity,
14028 window,
14029 move |_, _, event: &EditorEvent, _window, _cx| {
14030 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
14031 *is_still_following.borrow_mut() = false;
14032 }
14033
14034 if let EditorEvent::BufferEdited = event {
14035 *follower_edit_event_count.borrow_mut() += 1;
14036 }
14037 },
14038 )
14039 .detach();
14040 }
14041 });
14042
14043 // Update the selections only
14044 _ = leader.update(cx, |leader, window, cx| {
14045 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14046 s.select_ranges([1..1])
14047 });
14048 });
14049 follower
14050 .update(cx, |follower, window, cx| {
14051 follower.apply_update_proto(
14052 &project,
14053 pending_update.borrow_mut().take().unwrap(),
14054 window,
14055 cx,
14056 )
14057 })
14058 .unwrap()
14059 .await
14060 .unwrap();
14061 _ = follower.update(cx, |follower, _, cx| {
14062 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
14063 });
14064 assert!(*is_still_following.borrow());
14065 assert_eq!(*follower_edit_event_count.borrow(), 0);
14066
14067 // Update the scroll position only
14068 _ = leader.update(cx, |leader, window, cx| {
14069 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14070 });
14071 follower
14072 .update(cx, |follower, window, cx| {
14073 follower.apply_update_proto(
14074 &project,
14075 pending_update.borrow_mut().take().unwrap(),
14076 window,
14077 cx,
14078 )
14079 })
14080 .unwrap()
14081 .await
14082 .unwrap();
14083 assert_eq!(
14084 follower
14085 .update(cx, |follower, _, cx| follower.scroll_position(cx))
14086 .unwrap(),
14087 gpui::Point::new(1.5, 3.5)
14088 );
14089 assert!(*is_still_following.borrow());
14090 assert_eq!(*follower_edit_event_count.borrow(), 0);
14091
14092 // Update the selections and scroll position. The follower's scroll position is updated
14093 // via autoscroll, not via the leader's exact scroll position.
14094 _ = leader.update(cx, |leader, window, cx| {
14095 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14096 s.select_ranges([0..0])
14097 });
14098 leader.request_autoscroll(Autoscroll::newest(), cx);
14099 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14100 });
14101 follower
14102 .update(cx, |follower, window, cx| {
14103 follower.apply_update_proto(
14104 &project,
14105 pending_update.borrow_mut().take().unwrap(),
14106 window,
14107 cx,
14108 )
14109 })
14110 .unwrap()
14111 .await
14112 .unwrap();
14113 _ = follower.update(cx, |follower, _, cx| {
14114 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
14115 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
14116 });
14117 assert!(*is_still_following.borrow());
14118
14119 // Creating a pending selection that precedes another selection
14120 _ = leader.update(cx, |leader, window, cx| {
14121 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14122 s.select_ranges([1..1])
14123 });
14124 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
14125 });
14126 follower
14127 .update(cx, |follower, window, cx| {
14128 follower.apply_update_proto(
14129 &project,
14130 pending_update.borrow_mut().take().unwrap(),
14131 window,
14132 cx,
14133 )
14134 })
14135 .unwrap()
14136 .await
14137 .unwrap();
14138 _ = follower.update(cx, |follower, _, cx| {
14139 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
14140 });
14141 assert!(*is_still_following.borrow());
14142
14143 // Extend the pending selection so that it surrounds another selection
14144 _ = leader.update(cx, |leader, window, cx| {
14145 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
14146 });
14147 follower
14148 .update(cx, |follower, window, cx| {
14149 follower.apply_update_proto(
14150 &project,
14151 pending_update.borrow_mut().take().unwrap(),
14152 window,
14153 cx,
14154 )
14155 })
14156 .unwrap()
14157 .await
14158 .unwrap();
14159 _ = follower.update(cx, |follower, _, cx| {
14160 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
14161 });
14162
14163 // Scrolling locally breaks the follow
14164 _ = follower.update(cx, |follower, window, cx| {
14165 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
14166 follower.set_scroll_anchor(
14167 ScrollAnchor {
14168 anchor: top_anchor,
14169 offset: gpui::Point::new(0.0, 0.5),
14170 },
14171 window,
14172 cx,
14173 );
14174 });
14175 assert!(!(*is_still_following.borrow()));
14176}
14177
14178#[gpui::test]
14179async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
14180 init_test(cx, |_| {});
14181
14182 let fs = FakeFs::new(cx.executor());
14183 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14184 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14185 let pane = workspace
14186 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14187 .unwrap();
14188
14189 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14190
14191 let leader = pane.update_in(cx, |_, window, cx| {
14192 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
14193 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
14194 });
14195
14196 // Start following the editor when it has no excerpts.
14197 let mut state_message =
14198 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14199 let workspace_entity = workspace.root(cx).unwrap();
14200 let follower_1 = cx
14201 .update_window(*workspace.deref(), |_, window, cx| {
14202 Editor::from_state_proto(
14203 workspace_entity,
14204 ViewId {
14205 creator: CollaboratorId::PeerId(PeerId::default()),
14206 id: 0,
14207 },
14208 &mut state_message,
14209 window,
14210 cx,
14211 )
14212 })
14213 .unwrap()
14214 .unwrap()
14215 .await
14216 .unwrap();
14217
14218 let update_message = Rc::new(RefCell::new(None));
14219 follower_1.update_in(cx, {
14220 let update = update_message.clone();
14221 |_, window, cx| {
14222 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
14223 leader.read(cx).add_event_to_update_proto(
14224 event,
14225 &mut update.borrow_mut(),
14226 window,
14227 cx,
14228 );
14229 })
14230 .detach();
14231 }
14232 });
14233
14234 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
14235 (
14236 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
14237 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
14238 )
14239 });
14240
14241 // Insert some excerpts.
14242 leader.update(cx, |leader, cx| {
14243 leader.buffer.update(cx, |multibuffer, cx| {
14244 multibuffer.set_excerpts_for_path(
14245 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
14246 buffer_1.clone(),
14247 vec![
14248 Point::row_range(0..3),
14249 Point::row_range(1..6),
14250 Point::row_range(12..15),
14251 ],
14252 0,
14253 cx,
14254 );
14255 multibuffer.set_excerpts_for_path(
14256 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
14257 buffer_2.clone(),
14258 vec![Point::row_range(0..6), Point::row_range(8..12)],
14259 0,
14260 cx,
14261 );
14262 });
14263 });
14264
14265 // Apply the update of adding the excerpts.
14266 follower_1
14267 .update_in(cx, |follower, window, cx| {
14268 follower.apply_update_proto(
14269 &project,
14270 update_message.borrow().clone().unwrap(),
14271 window,
14272 cx,
14273 )
14274 })
14275 .await
14276 .unwrap();
14277 assert_eq!(
14278 follower_1.update(cx, |editor, cx| editor.text(cx)),
14279 leader.update(cx, |editor, cx| editor.text(cx))
14280 );
14281 update_message.borrow_mut().take();
14282
14283 // Start following separately after it already has excerpts.
14284 let mut state_message =
14285 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14286 let workspace_entity = workspace.root(cx).unwrap();
14287 let follower_2 = cx
14288 .update_window(*workspace.deref(), |_, window, cx| {
14289 Editor::from_state_proto(
14290 workspace_entity,
14291 ViewId {
14292 creator: CollaboratorId::PeerId(PeerId::default()),
14293 id: 0,
14294 },
14295 &mut state_message,
14296 window,
14297 cx,
14298 )
14299 })
14300 .unwrap()
14301 .unwrap()
14302 .await
14303 .unwrap();
14304 assert_eq!(
14305 follower_2.update(cx, |editor, cx| editor.text(cx)),
14306 leader.update(cx, |editor, cx| editor.text(cx))
14307 );
14308
14309 // Remove some excerpts.
14310 leader.update(cx, |leader, cx| {
14311 leader.buffer.update(cx, |multibuffer, cx| {
14312 let excerpt_ids = multibuffer.excerpt_ids();
14313 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
14314 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
14315 });
14316 });
14317
14318 // Apply the update of removing the excerpts.
14319 follower_1
14320 .update_in(cx, |follower, window, cx| {
14321 follower.apply_update_proto(
14322 &project,
14323 update_message.borrow().clone().unwrap(),
14324 window,
14325 cx,
14326 )
14327 })
14328 .await
14329 .unwrap();
14330 follower_2
14331 .update_in(cx, |follower, window, cx| {
14332 follower.apply_update_proto(
14333 &project,
14334 update_message.borrow().clone().unwrap(),
14335 window,
14336 cx,
14337 )
14338 })
14339 .await
14340 .unwrap();
14341 update_message.borrow_mut().take();
14342 assert_eq!(
14343 follower_1.update(cx, |editor, cx| editor.text(cx)),
14344 leader.update(cx, |editor, cx| editor.text(cx))
14345 );
14346}
14347
14348#[gpui::test]
14349async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14350 init_test(cx, |_| {});
14351
14352 let mut cx = EditorTestContext::new(cx).await;
14353 let lsp_store =
14354 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
14355
14356 cx.set_state(indoc! {"
14357 ˇfn func(abc def: i32) -> u32 {
14358 }
14359 "});
14360
14361 cx.update(|_, cx| {
14362 lsp_store.update(cx, |lsp_store, cx| {
14363 lsp_store
14364 .update_diagnostics(
14365 LanguageServerId(0),
14366 lsp::PublishDiagnosticsParams {
14367 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
14368 version: None,
14369 diagnostics: vec![
14370 lsp::Diagnostic {
14371 range: lsp::Range::new(
14372 lsp::Position::new(0, 11),
14373 lsp::Position::new(0, 12),
14374 ),
14375 severity: Some(lsp::DiagnosticSeverity::ERROR),
14376 ..Default::default()
14377 },
14378 lsp::Diagnostic {
14379 range: lsp::Range::new(
14380 lsp::Position::new(0, 12),
14381 lsp::Position::new(0, 15),
14382 ),
14383 severity: Some(lsp::DiagnosticSeverity::ERROR),
14384 ..Default::default()
14385 },
14386 lsp::Diagnostic {
14387 range: lsp::Range::new(
14388 lsp::Position::new(0, 25),
14389 lsp::Position::new(0, 28),
14390 ),
14391 severity: Some(lsp::DiagnosticSeverity::ERROR),
14392 ..Default::default()
14393 },
14394 ],
14395 },
14396 None,
14397 DiagnosticSourceKind::Pushed,
14398 &[],
14399 cx,
14400 )
14401 .unwrap()
14402 });
14403 });
14404
14405 executor.run_until_parked();
14406
14407 cx.update_editor(|editor, window, cx| {
14408 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14409 });
14410
14411 cx.assert_editor_state(indoc! {"
14412 fn func(abc def: i32) -> ˇu32 {
14413 }
14414 "});
14415
14416 cx.update_editor(|editor, window, cx| {
14417 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14418 });
14419
14420 cx.assert_editor_state(indoc! {"
14421 fn func(abc ˇdef: i32) -> u32 {
14422 }
14423 "});
14424
14425 cx.update_editor(|editor, window, cx| {
14426 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14427 });
14428
14429 cx.assert_editor_state(indoc! {"
14430 fn func(abcˇ def: i32) -> u32 {
14431 }
14432 "});
14433
14434 cx.update_editor(|editor, window, cx| {
14435 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14436 });
14437
14438 cx.assert_editor_state(indoc! {"
14439 fn func(abc def: i32) -> ˇu32 {
14440 }
14441 "});
14442}
14443
14444#[gpui::test]
14445async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14446 init_test(cx, |_| {});
14447
14448 let mut cx = EditorTestContext::new(cx).await;
14449
14450 let diff_base = r#"
14451 use some::mod;
14452
14453 const A: u32 = 42;
14454
14455 fn main() {
14456 println!("hello");
14457
14458 println!("world");
14459 }
14460 "#
14461 .unindent();
14462
14463 // Edits are modified, removed, modified, added
14464 cx.set_state(
14465 &r#"
14466 use some::modified;
14467
14468 ˇ
14469 fn main() {
14470 println!("hello there");
14471
14472 println!("around the");
14473 println!("world");
14474 }
14475 "#
14476 .unindent(),
14477 );
14478
14479 cx.set_head_text(&diff_base);
14480 executor.run_until_parked();
14481
14482 cx.update_editor(|editor, window, cx| {
14483 //Wrap around the bottom of the buffer
14484 for _ in 0..3 {
14485 editor.go_to_next_hunk(&GoToHunk, window, cx);
14486 }
14487 });
14488
14489 cx.assert_editor_state(
14490 &r#"
14491 ˇuse some::modified;
14492
14493
14494 fn main() {
14495 println!("hello there");
14496
14497 println!("around the");
14498 println!("world");
14499 }
14500 "#
14501 .unindent(),
14502 );
14503
14504 cx.update_editor(|editor, window, cx| {
14505 //Wrap around the top of the buffer
14506 for _ in 0..2 {
14507 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14508 }
14509 });
14510
14511 cx.assert_editor_state(
14512 &r#"
14513 use some::modified;
14514
14515
14516 fn main() {
14517 ˇ println!("hello there");
14518
14519 println!("around the");
14520 println!("world");
14521 }
14522 "#
14523 .unindent(),
14524 );
14525
14526 cx.update_editor(|editor, window, cx| {
14527 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14528 });
14529
14530 cx.assert_editor_state(
14531 &r#"
14532 use some::modified;
14533
14534 ˇ
14535 fn main() {
14536 println!("hello there");
14537
14538 println!("around the");
14539 println!("world");
14540 }
14541 "#
14542 .unindent(),
14543 );
14544
14545 cx.update_editor(|editor, window, cx| {
14546 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14547 });
14548
14549 cx.assert_editor_state(
14550 &r#"
14551 ˇuse some::modified;
14552
14553
14554 fn main() {
14555 println!("hello there");
14556
14557 println!("around the");
14558 println!("world");
14559 }
14560 "#
14561 .unindent(),
14562 );
14563
14564 cx.update_editor(|editor, window, cx| {
14565 for _ in 0..2 {
14566 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14567 }
14568 });
14569
14570 cx.assert_editor_state(
14571 &r#"
14572 use some::modified;
14573
14574
14575 fn main() {
14576 ˇ println!("hello there");
14577
14578 println!("around the");
14579 println!("world");
14580 }
14581 "#
14582 .unindent(),
14583 );
14584
14585 cx.update_editor(|editor, window, cx| {
14586 editor.fold(&Fold, window, cx);
14587 });
14588
14589 cx.update_editor(|editor, window, cx| {
14590 editor.go_to_next_hunk(&GoToHunk, window, cx);
14591 });
14592
14593 cx.assert_editor_state(
14594 &r#"
14595 ˇuse some::modified;
14596
14597
14598 fn main() {
14599 println!("hello there");
14600
14601 println!("around the");
14602 println!("world");
14603 }
14604 "#
14605 .unindent(),
14606 );
14607}
14608
14609#[test]
14610fn test_split_words() {
14611 fn split(text: &str) -> Vec<&str> {
14612 split_words(text).collect()
14613 }
14614
14615 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
14616 assert_eq!(split("hello_world"), &["hello_", "world"]);
14617 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
14618 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
14619 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
14620 assert_eq!(split("helloworld"), &["helloworld"]);
14621
14622 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
14623}
14624
14625#[gpui::test]
14626async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
14627 init_test(cx, |_| {});
14628
14629 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
14630 let mut assert = |before, after| {
14631 let _state_context = cx.set_state(before);
14632 cx.run_until_parked();
14633 cx.update_editor(|editor, window, cx| {
14634 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
14635 });
14636 cx.run_until_parked();
14637 cx.assert_editor_state(after);
14638 };
14639
14640 // Outside bracket jumps to outside of matching bracket
14641 assert("console.logˇ(var);", "console.log(var)ˇ;");
14642 assert("console.log(var)ˇ;", "console.logˇ(var);");
14643
14644 // Inside bracket jumps to inside of matching bracket
14645 assert("console.log(ˇvar);", "console.log(varˇ);");
14646 assert("console.log(varˇ);", "console.log(ˇvar);");
14647
14648 // When outside a bracket and inside, favor jumping to the inside bracket
14649 assert(
14650 "console.log('foo', [1, 2, 3]ˇ);",
14651 "console.log(ˇ'foo', [1, 2, 3]);",
14652 );
14653 assert(
14654 "console.log(ˇ'foo', [1, 2, 3]);",
14655 "console.log('foo', [1, 2, 3]ˇ);",
14656 );
14657
14658 // Bias forward if two options are equally likely
14659 assert(
14660 "let result = curried_fun()ˇ();",
14661 "let result = curried_fun()()ˇ;",
14662 );
14663
14664 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
14665 assert(
14666 indoc! {"
14667 function test() {
14668 console.log('test')ˇ
14669 }"},
14670 indoc! {"
14671 function test() {
14672 console.logˇ('test')
14673 }"},
14674 );
14675}
14676
14677#[gpui::test]
14678async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
14679 init_test(cx, |_| {});
14680
14681 let fs = FakeFs::new(cx.executor());
14682 fs.insert_tree(
14683 path!("/a"),
14684 json!({
14685 "main.rs": "fn main() { let a = 5; }",
14686 "other.rs": "// Test file",
14687 }),
14688 )
14689 .await;
14690 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14691
14692 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14693 language_registry.add(Arc::new(Language::new(
14694 LanguageConfig {
14695 name: "Rust".into(),
14696 matcher: LanguageMatcher {
14697 path_suffixes: vec!["rs".to_string()],
14698 ..Default::default()
14699 },
14700 brackets: BracketPairConfig {
14701 pairs: vec![BracketPair {
14702 start: "{".to_string(),
14703 end: "}".to_string(),
14704 close: true,
14705 surround: true,
14706 newline: true,
14707 }],
14708 disabled_scopes_by_bracket_ix: Vec::new(),
14709 },
14710 ..Default::default()
14711 },
14712 Some(tree_sitter_rust::LANGUAGE.into()),
14713 )));
14714 let mut fake_servers = language_registry.register_fake_lsp(
14715 "Rust",
14716 FakeLspAdapter {
14717 capabilities: lsp::ServerCapabilities {
14718 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
14719 first_trigger_character: "{".to_string(),
14720 more_trigger_character: None,
14721 }),
14722 ..Default::default()
14723 },
14724 ..Default::default()
14725 },
14726 );
14727
14728 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14729
14730 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14731
14732 let worktree_id = workspace
14733 .update(cx, |workspace, _, cx| {
14734 workspace.project().update(cx, |project, cx| {
14735 project.worktrees(cx).next().unwrap().read(cx).id()
14736 })
14737 })
14738 .unwrap();
14739
14740 let buffer = project
14741 .update(cx, |project, cx| {
14742 project.open_local_buffer(path!("/a/main.rs"), cx)
14743 })
14744 .await
14745 .unwrap();
14746 let editor_handle = workspace
14747 .update(cx, |workspace, window, cx| {
14748 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
14749 })
14750 .unwrap()
14751 .await
14752 .unwrap()
14753 .downcast::<Editor>()
14754 .unwrap();
14755
14756 cx.executor().start_waiting();
14757 let fake_server = fake_servers.next().await.unwrap();
14758
14759 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
14760 |params, _| async move {
14761 assert_eq!(
14762 params.text_document_position.text_document.uri,
14763 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
14764 );
14765 assert_eq!(
14766 params.text_document_position.position,
14767 lsp::Position::new(0, 21),
14768 );
14769
14770 Ok(Some(vec![lsp::TextEdit {
14771 new_text: "]".to_string(),
14772 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14773 }]))
14774 },
14775 );
14776
14777 editor_handle.update_in(cx, |editor, window, cx| {
14778 window.focus(&editor.focus_handle(cx));
14779 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14780 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
14781 });
14782 editor.handle_input("{", window, cx);
14783 });
14784
14785 cx.executor().run_until_parked();
14786
14787 buffer.update(cx, |buffer, _| {
14788 assert_eq!(
14789 buffer.text(),
14790 "fn main() { let a = {5}; }",
14791 "No extra braces from on type formatting should appear in the buffer"
14792 )
14793 });
14794}
14795
14796#[gpui::test(iterations = 20, seeds(31))]
14797async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
14798 init_test(cx, |_| {});
14799
14800 let mut cx = EditorLspTestContext::new_rust(
14801 lsp::ServerCapabilities {
14802 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
14803 first_trigger_character: ".".to_string(),
14804 more_trigger_character: None,
14805 }),
14806 ..Default::default()
14807 },
14808 cx,
14809 )
14810 .await;
14811
14812 cx.update_buffer(|buffer, _| {
14813 // This causes autoindent to be async.
14814 buffer.set_sync_parse_timeout(Duration::ZERO)
14815 });
14816
14817 cx.set_state("fn c() {\n d()ˇ\n}\n");
14818 cx.simulate_keystroke("\n");
14819 cx.run_until_parked();
14820
14821 let buffer_cloned =
14822 cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap().clone());
14823 let mut request =
14824 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
14825 let buffer_cloned = buffer_cloned.clone();
14826 async move {
14827 buffer_cloned.update(&mut cx, |buffer, _| {
14828 assert_eq!(
14829 buffer.text(),
14830 "fn c() {\n d()\n .\n}\n",
14831 "OnTypeFormatting should triggered after autoindent applied"
14832 )
14833 })?;
14834
14835 Ok(Some(vec![]))
14836 }
14837 });
14838
14839 cx.simulate_keystroke(".");
14840 cx.run_until_parked();
14841
14842 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
14843 assert!(request.next().await.is_some());
14844 request.close();
14845 assert!(request.next().await.is_none());
14846}
14847
14848#[gpui::test]
14849async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
14850 init_test(cx, |_| {});
14851
14852 let fs = FakeFs::new(cx.executor());
14853 fs.insert_tree(
14854 path!("/a"),
14855 json!({
14856 "main.rs": "fn main() { let a = 5; }",
14857 "other.rs": "// Test file",
14858 }),
14859 )
14860 .await;
14861
14862 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14863
14864 let server_restarts = Arc::new(AtomicUsize::new(0));
14865 let closure_restarts = Arc::clone(&server_restarts);
14866 let language_server_name = "test language server";
14867 let language_name: LanguageName = "Rust".into();
14868
14869 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14870 language_registry.add(Arc::new(Language::new(
14871 LanguageConfig {
14872 name: language_name.clone(),
14873 matcher: LanguageMatcher {
14874 path_suffixes: vec!["rs".to_string()],
14875 ..Default::default()
14876 },
14877 ..Default::default()
14878 },
14879 Some(tree_sitter_rust::LANGUAGE.into()),
14880 )));
14881 let mut fake_servers = language_registry.register_fake_lsp(
14882 "Rust",
14883 FakeLspAdapter {
14884 name: language_server_name,
14885 initialization_options: Some(json!({
14886 "testOptionValue": true
14887 })),
14888 initializer: Some(Box::new(move |fake_server| {
14889 let task_restarts = Arc::clone(&closure_restarts);
14890 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
14891 task_restarts.fetch_add(1, atomic::Ordering::Release);
14892 futures::future::ready(Ok(()))
14893 });
14894 })),
14895 ..Default::default()
14896 },
14897 );
14898
14899 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14900 let _buffer = project
14901 .update(cx, |project, cx| {
14902 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
14903 })
14904 .await
14905 .unwrap();
14906 let _fake_server = fake_servers.next().await.unwrap();
14907 update_test_language_settings(cx, |language_settings| {
14908 language_settings.languages.insert(
14909 language_name.clone(),
14910 LanguageSettingsContent {
14911 tab_size: NonZeroU32::new(8),
14912 ..Default::default()
14913 },
14914 );
14915 });
14916 cx.executor().run_until_parked();
14917 assert_eq!(
14918 server_restarts.load(atomic::Ordering::Acquire),
14919 0,
14920 "Should not restart LSP server on an unrelated change"
14921 );
14922
14923 update_test_project_settings(cx, |project_settings| {
14924 project_settings.lsp.insert(
14925 "Some other server name".into(),
14926 LspSettings {
14927 binary: None,
14928 settings: None,
14929 initialization_options: Some(json!({
14930 "some other init value": false
14931 })),
14932 enable_lsp_tasks: false,
14933 },
14934 );
14935 });
14936 cx.executor().run_until_parked();
14937 assert_eq!(
14938 server_restarts.load(atomic::Ordering::Acquire),
14939 0,
14940 "Should not restart LSP server on an unrelated LSP settings change"
14941 );
14942
14943 update_test_project_settings(cx, |project_settings| {
14944 project_settings.lsp.insert(
14945 language_server_name.into(),
14946 LspSettings {
14947 binary: None,
14948 settings: None,
14949 initialization_options: Some(json!({
14950 "anotherInitValue": false
14951 })),
14952 enable_lsp_tasks: false,
14953 },
14954 );
14955 });
14956 cx.executor().run_until_parked();
14957 assert_eq!(
14958 server_restarts.load(atomic::Ordering::Acquire),
14959 1,
14960 "Should restart LSP server on a related LSP settings change"
14961 );
14962
14963 update_test_project_settings(cx, |project_settings| {
14964 project_settings.lsp.insert(
14965 language_server_name.into(),
14966 LspSettings {
14967 binary: None,
14968 settings: None,
14969 initialization_options: Some(json!({
14970 "anotherInitValue": false
14971 })),
14972 enable_lsp_tasks: false,
14973 },
14974 );
14975 });
14976 cx.executor().run_until_parked();
14977 assert_eq!(
14978 server_restarts.load(atomic::Ordering::Acquire),
14979 1,
14980 "Should not restart LSP server on a related LSP settings change that is the same"
14981 );
14982
14983 update_test_project_settings(cx, |project_settings| {
14984 project_settings.lsp.insert(
14985 language_server_name.into(),
14986 LspSettings {
14987 binary: None,
14988 settings: None,
14989 initialization_options: None,
14990 enable_lsp_tasks: false,
14991 },
14992 );
14993 });
14994 cx.executor().run_until_parked();
14995 assert_eq!(
14996 server_restarts.load(atomic::Ordering::Acquire),
14997 2,
14998 "Should restart LSP server on another related LSP settings change"
14999 );
15000}
15001
15002#[gpui::test]
15003async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
15004 init_test(cx, |_| {});
15005
15006 let mut cx = EditorLspTestContext::new_rust(
15007 lsp::ServerCapabilities {
15008 completion_provider: Some(lsp::CompletionOptions {
15009 trigger_characters: Some(vec![".".to_string()]),
15010 resolve_provider: Some(true),
15011 ..Default::default()
15012 }),
15013 ..Default::default()
15014 },
15015 cx,
15016 )
15017 .await;
15018
15019 cx.set_state("fn main() { let a = 2ˇ; }");
15020 cx.simulate_keystroke(".");
15021 let completion_item = lsp::CompletionItem {
15022 label: "some".into(),
15023 kind: Some(lsp::CompletionItemKind::SNIPPET),
15024 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15025 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15026 kind: lsp::MarkupKind::Markdown,
15027 value: "```rust\nSome(2)\n```".to_string(),
15028 })),
15029 deprecated: Some(false),
15030 sort_text: Some("fffffff2".to_string()),
15031 filter_text: Some("some".to_string()),
15032 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15033 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15034 range: lsp::Range {
15035 start: lsp::Position {
15036 line: 0,
15037 character: 22,
15038 },
15039 end: lsp::Position {
15040 line: 0,
15041 character: 22,
15042 },
15043 },
15044 new_text: "Some(2)".to_string(),
15045 })),
15046 additional_text_edits: Some(vec![lsp::TextEdit {
15047 range: lsp::Range {
15048 start: lsp::Position {
15049 line: 0,
15050 character: 20,
15051 },
15052 end: lsp::Position {
15053 line: 0,
15054 character: 22,
15055 },
15056 },
15057 new_text: "".to_string(),
15058 }]),
15059 ..Default::default()
15060 };
15061
15062 let closure_completion_item = completion_item.clone();
15063 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15064 let task_completion_item = closure_completion_item.clone();
15065 async move {
15066 Ok(Some(lsp::CompletionResponse::Array(vec![
15067 task_completion_item,
15068 ])))
15069 }
15070 });
15071
15072 request.next().await;
15073
15074 cx.condition(|editor, _| editor.context_menu_visible())
15075 .await;
15076 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15077 editor
15078 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15079 .unwrap()
15080 });
15081 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
15082
15083 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
15084 let task_completion_item = completion_item.clone();
15085 async move { Ok(task_completion_item) }
15086 })
15087 .next()
15088 .await
15089 .unwrap();
15090 apply_additional_edits.await.unwrap();
15091 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
15092}
15093
15094#[gpui::test]
15095async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
15096 init_test(cx, |_| {});
15097
15098 let mut cx = EditorLspTestContext::new_rust(
15099 lsp::ServerCapabilities {
15100 completion_provider: Some(lsp::CompletionOptions {
15101 trigger_characters: Some(vec![".".to_string()]),
15102 resolve_provider: Some(true),
15103 ..Default::default()
15104 }),
15105 ..Default::default()
15106 },
15107 cx,
15108 )
15109 .await;
15110
15111 cx.set_state("fn main() { let a = 2ˇ; }");
15112 cx.simulate_keystroke(".");
15113
15114 let item1 = lsp::CompletionItem {
15115 label: "method id()".to_string(),
15116 filter_text: Some("id".to_string()),
15117 detail: None,
15118 documentation: None,
15119 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15120 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15121 new_text: ".id".to_string(),
15122 })),
15123 ..lsp::CompletionItem::default()
15124 };
15125
15126 let item2 = lsp::CompletionItem {
15127 label: "other".to_string(),
15128 filter_text: Some("other".to_string()),
15129 detail: None,
15130 documentation: None,
15131 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15132 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15133 new_text: ".other".to_string(),
15134 })),
15135 ..lsp::CompletionItem::default()
15136 };
15137
15138 let item1 = item1.clone();
15139 cx.set_request_handler::<lsp::request::Completion, _, _>({
15140 let item1 = item1.clone();
15141 move |_, _, _| {
15142 let item1 = item1.clone();
15143 let item2 = item2.clone();
15144 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
15145 }
15146 })
15147 .next()
15148 .await;
15149
15150 cx.condition(|editor, _| editor.context_menu_visible())
15151 .await;
15152 cx.update_editor(|editor, _, _| {
15153 let context_menu = editor.context_menu.borrow_mut();
15154 let context_menu = context_menu
15155 .as_ref()
15156 .expect("Should have the context menu deployed");
15157 match context_menu {
15158 CodeContextMenu::Completions(completions_menu) => {
15159 let completions = completions_menu.completions.borrow_mut();
15160 assert_eq!(
15161 completions
15162 .iter()
15163 .map(|completion| &completion.label.text)
15164 .collect::<Vec<_>>(),
15165 vec!["method id()", "other"]
15166 )
15167 }
15168 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15169 }
15170 });
15171
15172 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
15173 let item1 = item1.clone();
15174 move |_, item_to_resolve, _| {
15175 let item1 = item1.clone();
15176 async move {
15177 if item1 == item_to_resolve {
15178 Ok(lsp::CompletionItem {
15179 label: "method id()".to_string(),
15180 filter_text: Some("id".to_string()),
15181 detail: Some("Now resolved!".to_string()),
15182 documentation: Some(lsp::Documentation::String("Docs".to_string())),
15183 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15184 range: lsp::Range::new(
15185 lsp::Position::new(0, 22),
15186 lsp::Position::new(0, 22),
15187 ),
15188 new_text: ".id".to_string(),
15189 })),
15190 ..lsp::CompletionItem::default()
15191 })
15192 } else {
15193 Ok(item_to_resolve)
15194 }
15195 }
15196 }
15197 })
15198 .next()
15199 .await
15200 .unwrap();
15201 cx.run_until_parked();
15202
15203 cx.update_editor(|editor, window, cx| {
15204 editor.context_menu_next(&Default::default(), window, cx);
15205 });
15206
15207 cx.update_editor(|editor, _, _| {
15208 let context_menu = editor.context_menu.borrow_mut();
15209 let context_menu = context_menu
15210 .as_ref()
15211 .expect("Should have the context menu deployed");
15212 match context_menu {
15213 CodeContextMenu::Completions(completions_menu) => {
15214 let completions = completions_menu.completions.borrow_mut();
15215 assert_eq!(
15216 completions
15217 .iter()
15218 .map(|completion| &completion.label.text)
15219 .collect::<Vec<_>>(),
15220 vec!["method id() Now resolved!", "other"],
15221 "Should update first completion label, but not second as the filter text did not match."
15222 );
15223 }
15224 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15225 }
15226 });
15227}
15228
15229#[gpui::test]
15230async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
15231 init_test(cx, |_| {});
15232 let mut cx = EditorLspTestContext::new_rust(
15233 lsp::ServerCapabilities {
15234 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
15235 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
15236 completion_provider: Some(lsp::CompletionOptions {
15237 resolve_provider: Some(true),
15238 ..Default::default()
15239 }),
15240 ..Default::default()
15241 },
15242 cx,
15243 )
15244 .await;
15245 cx.set_state(indoc! {"
15246 struct TestStruct {
15247 field: i32
15248 }
15249
15250 fn mainˇ() {
15251 let unused_var = 42;
15252 let test_struct = TestStruct { field: 42 };
15253 }
15254 "});
15255 let symbol_range = cx.lsp_range(indoc! {"
15256 struct TestStruct {
15257 field: i32
15258 }
15259
15260 «fn main»() {
15261 let unused_var = 42;
15262 let test_struct = TestStruct { field: 42 };
15263 }
15264 "});
15265 let mut hover_requests =
15266 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
15267 Ok(Some(lsp::Hover {
15268 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
15269 kind: lsp::MarkupKind::Markdown,
15270 value: "Function documentation".to_string(),
15271 }),
15272 range: Some(symbol_range),
15273 }))
15274 });
15275
15276 // Case 1: Test that code action menu hide hover popover
15277 cx.dispatch_action(Hover);
15278 hover_requests.next().await;
15279 cx.condition(|editor, _| editor.hover_state.visible()).await;
15280 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
15281 move |_, _, _| async move {
15282 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
15283 lsp::CodeAction {
15284 title: "Remove unused variable".to_string(),
15285 kind: Some(CodeActionKind::QUICKFIX),
15286 edit: Some(lsp::WorkspaceEdit {
15287 changes: Some(
15288 [(
15289 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
15290 vec![lsp::TextEdit {
15291 range: lsp::Range::new(
15292 lsp::Position::new(5, 4),
15293 lsp::Position::new(5, 27),
15294 ),
15295 new_text: "".to_string(),
15296 }],
15297 )]
15298 .into_iter()
15299 .collect(),
15300 ),
15301 ..Default::default()
15302 }),
15303 ..Default::default()
15304 },
15305 )]))
15306 },
15307 );
15308 cx.update_editor(|editor, window, cx| {
15309 editor.toggle_code_actions(
15310 &ToggleCodeActions {
15311 deployed_from: None,
15312 quick_launch: false,
15313 },
15314 window,
15315 cx,
15316 );
15317 });
15318 code_action_requests.next().await;
15319 cx.run_until_parked();
15320 cx.condition(|editor, _| editor.context_menu_visible())
15321 .await;
15322 cx.update_editor(|editor, _, _| {
15323 assert!(
15324 !editor.hover_state.visible(),
15325 "Hover popover should be hidden when code action menu is shown"
15326 );
15327 // Hide code actions
15328 editor.context_menu.take();
15329 });
15330
15331 // Case 2: Test that code completions hide hover popover
15332 cx.dispatch_action(Hover);
15333 hover_requests.next().await;
15334 cx.condition(|editor, _| editor.hover_state.visible()).await;
15335 let counter = Arc::new(AtomicUsize::new(0));
15336 let mut completion_requests =
15337 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15338 let counter = counter.clone();
15339 async move {
15340 counter.fetch_add(1, atomic::Ordering::Release);
15341 Ok(Some(lsp::CompletionResponse::Array(vec![
15342 lsp::CompletionItem {
15343 label: "main".into(),
15344 kind: Some(lsp::CompletionItemKind::FUNCTION),
15345 detail: Some("() -> ()".to_string()),
15346 ..Default::default()
15347 },
15348 lsp::CompletionItem {
15349 label: "TestStruct".into(),
15350 kind: Some(lsp::CompletionItemKind::STRUCT),
15351 detail: Some("struct TestStruct".to_string()),
15352 ..Default::default()
15353 },
15354 ])))
15355 }
15356 });
15357 cx.update_editor(|editor, window, cx| {
15358 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15359 });
15360 completion_requests.next().await;
15361 cx.condition(|editor, _| editor.context_menu_visible())
15362 .await;
15363 cx.update_editor(|editor, _, _| {
15364 assert!(
15365 !editor.hover_state.visible(),
15366 "Hover popover should be hidden when completion menu is shown"
15367 );
15368 });
15369}
15370
15371#[gpui::test]
15372async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
15373 init_test(cx, |_| {});
15374
15375 let mut cx = EditorLspTestContext::new_rust(
15376 lsp::ServerCapabilities {
15377 completion_provider: Some(lsp::CompletionOptions {
15378 trigger_characters: Some(vec![".".to_string()]),
15379 resolve_provider: Some(true),
15380 ..Default::default()
15381 }),
15382 ..Default::default()
15383 },
15384 cx,
15385 )
15386 .await;
15387
15388 cx.set_state("fn main() { let a = 2ˇ; }");
15389 cx.simulate_keystroke(".");
15390
15391 let unresolved_item_1 = lsp::CompletionItem {
15392 label: "id".to_string(),
15393 filter_text: Some("id".to_string()),
15394 detail: None,
15395 documentation: None,
15396 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15397 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15398 new_text: ".id".to_string(),
15399 })),
15400 ..lsp::CompletionItem::default()
15401 };
15402 let resolved_item_1 = lsp::CompletionItem {
15403 additional_text_edits: Some(vec![lsp::TextEdit {
15404 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15405 new_text: "!!".to_string(),
15406 }]),
15407 ..unresolved_item_1.clone()
15408 };
15409 let unresolved_item_2 = lsp::CompletionItem {
15410 label: "other".to_string(),
15411 filter_text: Some("other".to_string()),
15412 detail: None,
15413 documentation: None,
15414 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15415 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15416 new_text: ".other".to_string(),
15417 })),
15418 ..lsp::CompletionItem::default()
15419 };
15420 let resolved_item_2 = lsp::CompletionItem {
15421 additional_text_edits: Some(vec![lsp::TextEdit {
15422 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15423 new_text: "??".to_string(),
15424 }]),
15425 ..unresolved_item_2.clone()
15426 };
15427
15428 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
15429 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
15430 cx.lsp
15431 .server
15432 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
15433 let unresolved_item_1 = unresolved_item_1.clone();
15434 let resolved_item_1 = resolved_item_1.clone();
15435 let unresolved_item_2 = unresolved_item_2.clone();
15436 let resolved_item_2 = resolved_item_2.clone();
15437 let resolve_requests_1 = resolve_requests_1.clone();
15438 let resolve_requests_2 = resolve_requests_2.clone();
15439 move |unresolved_request, _| {
15440 let unresolved_item_1 = unresolved_item_1.clone();
15441 let resolved_item_1 = resolved_item_1.clone();
15442 let unresolved_item_2 = unresolved_item_2.clone();
15443 let resolved_item_2 = resolved_item_2.clone();
15444 let resolve_requests_1 = resolve_requests_1.clone();
15445 let resolve_requests_2 = resolve_requests_2.clone();
15446 async move {
15447 if unresolved_request == unresolved_item_1 {
15448 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
15449 Ok(resolved_item_1.clone())
15450 } else if unresolved_request == unresolved_item_2 {
15451 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
15452 Ok(resolved_item_2.clone())
15453 } else {
15454 panic!("Unexpected completion item {unresolved_request:?}")
15455 }
15456 }
15457 }
15458 })
15459 .detach();
15460
15461 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15462 let unresolved_item_1 = unresolved_item_1.clone();
15463 let unresolved_item_2 = unresolved_item_2.clone();
15464 async move {
15465 Ok(Some(lsp::CompletionResponse::Array(vec![
15466 unresolved_item_1,
15467 unresolved_item_2,
15468 ])))
15469 }
15470 })
15471 .next()
15472 .await;
15473
15474 cx.condition(|editor, _| editor.context_menu_visible())
15475 .await;
15476 cx.update_editor(|editor, _, _| {
15477 let context_menu = editor.context_menu.borrow_mut();
15478 let context_menu = context_menu
15479 .as_ref()
15480 .expect("Should have the context menu deployed");
15481 match context_menu {
15482 CodeContextMenu::Completions(completions_menu) => {
15483 let completions = completions_menu.completions.borrow_mut();
15484 assert_eq!(
15485 completions
15486 .iter()
15487 .map(|completion| &completion.label.text)
15488 .collect::<Vec<_>>(),
15489 vec!["id", "other"]
15490 )
15491 }
15492 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15493 }
15494 });
15495 cx.run_until_parked();
15496
15497 cx.update_editor(|editor, window, cx| {
15498 editor.context_menu_next(&ContextMenuNext, window, cx);
15499 });
15500 cx.run_until_parked();
15501 cx.update_editor(|editor, window, cx| {
15502 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
15503 });
15504 cx.run_until_parked();
15505 cx.update_editor(|editor, window, cx| {
15506 editor.context_menu_next(&ContextMenuNext, window, cx);
15507 });
15508 cx.run_until_parked();
15509 cx.update_editor(|editor, window, cx| {
15510 editor
15511 .compose_completion(&ComposeCompletion::default(), window, cx)
15512 .expect("No task returned")
15513 })
15514 .await
15515 .expect("Completion failed");
15516 cx.run_until_parked();
15517
15518 cx.update_editor(|editor, _, cx| {
15519 assert_eq!(
15520 resolve_requests_1.load(atomic::Ordering::Acquire),
15521 1,
15522 "Should always resolve once despite multiple selections"
15523 );
15524 assert_eq!(
15525 resolve_requests_2.load(atomic::Ordering::Acquire),
15526 1,
15527 "Should always resolve once after multiple selections and applying the completion"
15528 );
15529 assert_eq!(
15530 editor.text(cx),
15531 "fn main() { let a = ??.other; }",
15532 "Should use resolved data when applying the completion"
15533 );
15534 });
15535}
15536
15537#[gpui::test]
15538async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
15539 init_test(cx, |_| {});
15540
15541 let item_0 = lsp::CompletionItem {
15542 label: "abs".into(),
15543 insert_text: Some("abs".into()),
15544 data: Some(json!({ "very": "special"})),
15545 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
15546 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
15547 lsp::InsertReplaceEdit {
15548 new_text: "abs".to_string(),
15549 insert: lsp::Range::default(),
15550 replace: lsp::Range::default(),
15551 },
15552 )),
15553 ..lsp::CompletionItem::default()
15554 };
15555 let items = iter::once(item_0.clone())
15556 .chain((11..51).map(|i| lsp::CompletionItem {
15557 label: format!("item_{}", i),
15558 insert_text: Some(format!("item_{}", i)),
15559 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15560 ..lsp::CompletionItem::default()
15561 }))
15562 .collect::<Vec<_>>();
15563
15564 let default_commit_characters = vec!["?".to_string()];
15565 let default_data = json!({ "default": "data"});
15566 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
15567 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
15568 let default_edit_range = lsp::Range {
15569 start: lsp::Position {
15570 line: 0,
15571 character: 5,
15572 },
15573 end: lsp::Position {
15574 line: 0,
15575 character: 5,
15576 },
15577 };
15578
15579 let mut cx = EditorLspTestContext::new_rust(
15580 lsp::ServerCapabilities {
15581 completion_provider: Some(lsp::CompletionOptions {
15582 trigger_characters: Some(vec![".".to_string()]),
15583 resolve_provider: Some(true),
15584 ..Default::default()
15585 }),
15586 ..Default::default()
15587 },
15588 cx,
15589 )
15590 .await;
15591
15592 cx.set_state("fn main() { let a = 2ˇ; }");
15593 cx.simulate_keystroke(".");
15594
15595 let completion_data = default_data.clone();
15596 let completion_characters = default_commit_characters.clone();
15597 let completion_items = items.clone();
15598 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15599 let default_data = completion_data.clone();
15600 let default_commit_characters = completion_characters.clone();
15601 let items = completion_items.clone();
15602 async move {
15603 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15604 items,
15605 item_defaults: Some(lsp::CompletionListItemDefaults {
15606 data: Some(default_data.clone()),
15607 commit_characters: Some(default_commit_characters.clone()),
15608 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
15609 default_edit_range,
15610 )),
15611 insert_text_format: Some(default_insert_text_format),
15612 insert_text_mode: Some(default_insert_text_mode),
15613 }),
15614 ..lsp::CompletionList::default()
15615 })))
15616 }
15617 })
15618 .next()
15619 .await;
15620
15621 let resolved_items = Arc::new(Mutex::new(Vec::new()));
15622 cx.lsp
15623 .server
15624 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
15625 let closure_resolved_items = resolved_items.clone();
15626 move |item_to_resolve, _| {
15627 let closure_resolved_items = closure_resolved_items.clone();
15628 async move {
15629 closure_resolved_items.lock().push(item_to_resolve.clone());
15630 Ok(item_to_resolve)
15631 }
15632 }
15633 })
15634 .detach();
15635
15636 cx.condition(|editor, _| editor.context_menu_visible())
15637 .await;
15638 cx.run_until_parked();
15639 cx.update_editor(|editor, _, _| {
15640 let menu = editor.context_menu.borrow_mut();
15641 match menu.as_ref().expect("should have the completions menu") {
15642 CodeContextMenu::Completions(completions_menu) => {
15643 assert_eq!(
15644 completions_menu
15645 .entries
15646 .borrow()
15647 .iter()
15648 .map(|mat| mat.string.clone())
15649 .collect::<Vec<String>>(),
15650 items
15651 .iter()
15652 .map(|completion| completion.label.clone())
15653 .collect::<Vec<String>>()
15654 );
15655 }
15656 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
15657 }
15658 });
15659 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
15660 // with 4 from the end.
15661 assert_eq!(
15662 *resolved_items.lock(),
15663 [&items[0..16], &items[items.len() - 4..items.len()]]
15664 .concat()
15665 .iter()
15666 .cloned()
15667 .map(|mut item| {
15668 if item.data.is_none() {
15669 item.data = Some(default_data.clone());
15670 }
15671 item
15672 })
15673 .collect::<Vec<lsp::CompletionItem>>(),
15674 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
15675 );
15676 resolved_items.lock().clear();
15677
15678 cx.update_editor(|editor, window, cx| {
15679 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
15680 });
15681 cx.run_until_parked();
15682 // Completions that have already been resolved are skipped.
15683 assert_eq!(
15684 *resolved_items.lock(),
15685 items[items.len() - 17..items.len() - 4]
15686 .iter()
15687 .cloned()
15688 .map(|mut item| {
15689 if item.data.is_none() {
15690 item.data = Some(default_data.clone());
15691 }
15692 item
15693 })
15694 .collect::<Vec<lsp::CompletionItem>>()
15695 );
15696 resolved_items.lock().clear();
15697}
15698
15699#[gpui::test]
15700async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
15701 init_test(cx, |_| {});
15702
15703 let mut cx = EditorLspTestContext::new(
15704 Language::new(
15705 LanguageConfig {
15706 matcher: LanguageMatcher {
15707 path_suffixes: vec!["jsx".into()],
15708 ..Default::default()
15709 },
15710 overrides: [(
15711 "element".into(),
15712 LanguageConfigOverride {
15713 completion_query_characters: Override::Set(['-'].into_iter().collect()),
15714 ..Default::default()
15715 },
15716 )]
15717 .into_iter()
15718 .collect(),
15719 ..Default::default()
15720 },
15721 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15722 )
15723 .with_override_query("(jsx_self_closing_element) @element")
15724 .unwrap(),
15725 lsp::ServerCapabilities {
15726 completion_provider: Some(lsp::CompletionOptions {
15727 trigger_characters: Some(vec![":".to_string()]),
15728 ..Default::default()
15729 }),
15730 ..Default::default()
15731 },
15732 cx,
15733 )
15734 .await;
15735
15736 cx.lsp
15737 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15738 Ok(Some(lsp::CompletionResponse::Array(vec![
15739 lsp::CompletionItem {
15740 label: "bg-blue".into(),
15741 ..Default::default()
15742 },
15743 lsp::CompletionItem {
15744 label: "bg-red".into(),
15745 ..Default::default()
15746 },
15747 lsp::CompletionItem {
15748 label: "bg-yellow".into(),
15749 ..Default::default()
15750 },
15751 ])))
15752 });
15753
15754 cx.set_state(r#"<p class="bgˇ" />"#);
15755
15756 // Trigger completion when typing a dash, because the dash is an extra
15757 // word character in the 'element' scope, which contains the cursor.
15758 cx.simulate_keystroke("-");
15759 cx.executor().run_until_parked();
15760 cx.update_editor(|editor, _, _| {
15761 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15762 {
15763 assert_eq!(
15764 completion_menu_entries(&menu),
15765 &["bg-blue", "bg-red", "bg-yellow"]
15766 );
15767 } else {
15768 panic!("expected completion menu to be open");
15769 }
15770 });
15771
15772 cx.simulate_keystroke("l");
15773 cx.executor().run_until_parked();
15774 cx.update_editor(|editor, _, _| {
15775 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15776 {
15777 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
15778 } else {
15779 panic!("expected completion menu to be open");
15780 }
15781 });
15782
15783 // When filtering completions, consider the character after the '-' to
15784 // be the start of a subword.
15785 cx.set_state(r#"<p class="yelˇ" />"#);
15786 cx.simulate_keystroke("l");
15787 cx.executor().run_until_parked();
15788 cx.update_editor(|editor, _, _| {
15789 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15790 {
15791 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
15792 } else {
15793 panic!("expected completion menu to be open");
15794 }
15795 });
15796}
15797
15798fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
15799 let entries = menu.entries.borrow();
15800 entries.iter().map(|mat| mat.string.clone()).collect()
15801}
15802
15803#[gpui::test]
15804async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
15805 init_test(cx, |settings| {
15806 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
15807 FormatterList(vec![Formatter::Prettier].into()),
15808 ))
15809 });
15810
15811 let fs = FakeFs::new(cx.executor());
15812 fs.insert_file(path!("/file.ts"), Default::default()).await;
15813
15814 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
15815 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15816
15817 language_registry.add(Arc::new(Language::new(
15818 LanguageConfig {
15819 name: "TypeScript".into(),
15820 matcher: LanguageMatcher {
15821 path_suffixes: vec!["ts".to_string()],
15822 ..Default::default()
15823 },
15824 ..Default::default()
15825 },
15826 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
15827 )));
15828 update_test_language_settings(cx, |settings| {
15829 settings.defaults.prettier = Some(PrettierSettings {
15830 allowed: true,
15831 ..PrettierSettings::default()
15832 });
15833 });
15834
15835 let test_plugin = "test_plugin";
15836 let _ = language_registry.register_fake_lsp(
15837 "TypeScript",
15838 FakeLspAdapter {
15839 prettier_plugins: vec![test_plugin],
15840 ..Default::default()
15841 },
15842 );
15843
15844 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
15845 let buffer = project
15846 .update(cx, |project, cx| {
15847 project.open_local_buffer(path!("/file.ts"), cx)
15848 })
15849 .await
15850 .unwrap();
15851
15852 let buffer_text = "one\ntwo\nthree\n";
15853 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
15854 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
15855 editor.update_in(cx, |editor, window, cx| {
15856 editor.set_text(buffer_text, window, cx)
15857 });
15858
15859 editor
15860 .update_in(cx, |editor, window, cx| {
15861 editor.perform_format(
15862 project.clone(),
15863 FormatTrigger::Manual,
15864 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
15865 window,
15866 cx,
15867 )
15868 })
15869 .unwrap()
15870 .await;
15871 assert_eq!(
15872 editor.update(cx, |editor, cx| editor.text(cx)),
15873 buffer_text.to_string() + prettier_format_suffix,
15874 "Test prettier formatting was not applied to the original buffer text",
15875 );
15876
15877 update_test_language_settings(cx, |settings| {
15878 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
15879 });
15880 let format = editor.update_in(cx, |editor, window, cx| {
15881 editor.perform_format(
15882 project.clone(),
15883 FormatTrigger::Manual,
15884 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
15885 window,
15886 cx,
15887 )
15888 });
15889 format.await.unwrap();
15890 assert_eq!(
15891 editor.update(cx, |editor, cx| editor.text(cx)),
15892 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
15893 "Autoformatting (via test prettier) was not applied to the original buffer text",
15894 );
15895}
15896
15897#[gpui::test]
15898async fn test_addition_reverts(cx: &mut TestAppContext) {
15899 init_test(cx, |_| {});
15900 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15901 let base_text = indoc! {r#"
15902 struct Row;
15903 struct Row1;
15904 struct Row2;
15905
15906 struct Row4;
15907 struct Row5;
15908 struct Row6;
15909
15910 struct Row8;
15911 struct Row9;
15912 struct Row10;"#};
15913
15914 // When addition hunks are not adjacent to carets, no hunk revert is performed
15915 assert_hunk_revert(
15916 indoc! {r#"struct Row;
15917 struct Row1;
15918 struct Row1.1;
15919 struct Row1.2;
15920 struct Row2;ˇ
15921
15922 struct Row4;
15923 struct Row5;
15924 struct Row6;
15925
15926 struct Row8;
15927 ˇstruct Row9;
15928 struct Row9.1;
15929 struct Row9.2;
15930 struct Row9.3;
15931 struct Row10;"#},
15932 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
15933 indoc! {r#"struct Row;
15934 struct Row1;
15935 struct Row1.1;
15936 struct Row1.2;
15937 struct Row2;ˇ
15938
15939 struct Row4;
15940 struct Row5;
15941 struct Row6;
15942
15943 struct Row8;
15944 ˇstruct Row9;
15945 struct Row9.1;
15946 struct Row9.2;
15947 struct Row9.3;
15948 struct Row10;"#},
15949 base_text,
15950 &mut cx,
15951 );
15952 // Same for selections
15953 assert_hunk_revert(
15954 indoc! {r#"struct Row;
15955 struct Row1;
15956 struct Row2;
15957 struct Row2.1;
15958 struct Row2.2;
15959 «ˇ
15960 struct Row4;
15961 struct» Row5;
15962 «struct Row6;
15963 ˇ»
15964 struct Row9.1;
15965 struct Row9.2;
15966 struct Row9.3;
15967 struct Row8;
15968 struct Row9;
15969 struct Row10;"#},
15970 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
15971 indoc! {r#"struct Row;
15972 struct Row1;
15973 struct Row2;
15974 struct Row2.1;
15975 struct Row2.2;
15976 «ˇ
15977 struct Row4;
15978 struct» Row5;
15979 «struct Row6;
15980 ˇ»
15981 struct Row9.1;
15982 struct Row9.2;
15983 struct Row9.3;
15984 struct Row8;
15985 struct Row9;
15986 struct Row10;"#},
15987 base_text,
15988 &mut cx,
15989 );
15990
15991 // When carets and selections intersect the addition hunks, those are reverted.
15992 // Adjacent carets got merged.
15993 assert_hunk_revert(
15994 indoc! {r#"struct Row;
15995 ˇ// something on the top
15996 struct Row1;
15997 struct Row2;
15998 struct Roˇw3.1;
15999 struct Row2.2;
16000 struct Row2.3;ˇ
16001
16002 struct Row4;
16003 struct ˇRow5.1;
16004 struct Row5.2;
16005 struct «Rowˇ»5.3;
16006 struct Row5;
16007 struct Row6;
16008 ˇ
16009 struct Row9.1;
16010 struct «Rowˇ»9.2;
16011 struct «ˇRow»9.3;
16012 struct Row8;
16013 struct Row9;
16014 «ˇ// something on bottom»
16015 struct Row10;"#},
16016 vec![
16017 DiffHunkStatusKind::Added,
16018 DiffHunkStatusKind::Added,
16019 DiffHunkStatusKind::Added,
16020 DiffHunkStatusKind::Added,
16021 DiffHunkStatusKind::Added,
16022 ],
16023 indoc! {r#"struct Row;
16024 ˇstruct Row1;
16025 struct Row2;
16026 ˇ
16027 struct Row4;
16028 ˇstruct Row5;
16029 struct Row6;
16030 ˇ
16031 ˇstruct Row8;
16032 struct Row9;
16033 ˇstruct Row10;"#},
16034 base_text,
16035 &mut cx,
16036 );
16037}
16038
16039#[gpui::test]
16040async fn test_modification_reverts(cx: &mut TestAppContext) {
16041 init_test(cx, |_| {});
16042 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16043 let base_text = indoc! {r#"
16044 struct Row;
16045 struct Row1;
16046 struct Row2;
16047
16048 struct Row4;
16049 struct Row5;
16050 struct Row6;
16051
16052 struct Row8;
16053 struct Row9;
16054 struct Row10;"#};
16055
16056 // Modification hunks behave the same as the addition ones.
16057 assert_hunk_revert(
16058 indoc! {r#"struct Row;
16059 struct Row1;
16060 struct Row33;
16061 ˇ
16062 struct Row4;
16063 struct Row5;
16064 struct Row6;
16065 ˇ
16066 struct Row99;
16067 struct Row9;
16068 struct Row10;"#},
16069 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16070 indoc! {r#"struct Row;
16071 struct Row1;
16072 struct Row33;
16073 ˇ
16074 struct Row4;
16075 struct Row5;
16076 struct Row6;
16077 ˇ
16078 struct Row99;
16079 struct Row9;
16080 struct Row10;"#},
16081 base_text,
16082 &mut cx,
16083 );
16084 assert_hunk_revert(
16085 indoc! {r#"struct Row;
16086 struct Row1;
16087 struct Row33;
16088 «ˇ
16089 struct Row4;
16090 struct» Row5;
16091 «struct Row6;
16092 ˇ»
16093 struct Row99;
16094 struct Row9;
16095 struct Row10;"#},
16096 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16097 indoc! {r#"struct Row;
16098 struct Row1;
16099 struct Row33;
16100 «ˇ
16101 struct Row4;
16102 struct» Row5;
16103 «struct Row6;
16104 ˇ»
16105 struct Row99;
16106 struct Row9;
16107 struct Row10;"#},
16108 base_text,
16109 &mut cx,
16110 );
16111
16112 assert_hunk_revert(
16113 indoc! {r#"ˇstruct Row1.1;
16114 struct Row1;
16115 «ˇstr»uct Row22;
16116
16117 struct ˇRow44;
16118 struct Row5;
16119 struct «Rˇ»ow66;ˇ
16120
16121 «struˇ»ct Row88;
16122 struct Row9;
16123 struct Row1011;ˇ"#},
16124 vec![
16125 DiffHunkStatusKind::Modified,
16126 DiffHunkStatusKind::Modified,
16127 DiffHunkStatusKind::Modified,
16128 DiffHunkStatusKind::Modified,
16129 DiffHunkStatusKind::Modified,
16130 DiffHunkStatusKind::Modified,
16131 ],
16132 indoc! {r#"struct Row;
16133 ˇstruct Row1;
16134 struct Row2;
16135 ˇ
16136 struct Row4;
16137 ˇstruct Row5;
16138 struct Row6;
16139 ˇ
16140 struct Row8;
16141 ˇstruct Row9;
16142 struct Row10;ˇ"#},
16143 base_text,
16144 &mut cx,
16145 );
16146}
16147
16148#[gpui::test]
16149async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
16150 init_test(cx, |_| {});
16151 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16152 let base_text = indoc! {r#"
16153 one
16154
16155 two
16156 three
16157 "#};
16158
16159 cx.set_head_text(base_text);
16160 cx.set_state("\nˇ\n");
16161 cx.executor().run_until_parked();
16162 cx.update_editor(|editor, _window, cx| {
16163 editor.expand_selected_diff_hunks(cx);
16164 });
16165 cx.executor().run_until_parked();
16166 cx.update_editor(|editor, window, cx| {
16167 editor.backspace(&Default::default(), window, cx);
16168 });
16169 cx.run_until_parked();
16170 cx.assert_state_with_diff(
16171 indoc! {r#"
16172
16173 - two
16174 - threeˇ
16175 +
16176 "#}
16177 .to_string(),
16178 );
16179}
16180
16181#[gpui::test]
16182async fn test_deletion_reverts(cx: &mut TestAppContext) {
16183 init_test(cx, |_| {});
16184 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16185 let base_text = indoc! {r#"struct Row;
16186struct Row1;
16187struct Row2;
16188
16189struct Row4;
16190struct Row5;
16191struct Row6;
16192
16193struct Row8;
16194struct Row9;
16195struct Row10;"#};
16196
16197 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
16198 assert_hunk_revert(
16199 indoc! {r#"struct Row;
16200 struct Row2;
16201
16202 ˇstruct Row4;
16203 struct Row5;
16204 struct Row6;
16205 ˇ
16206 struct Row8;
16207 struct Row10;"#},
16208 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16209 indoc! {r#"struct Row;
16210 struct Row2;
16211
16212 ˇstruct Row4;
16213 struct Row5;
16214 struct Row6;
16215 ˇ
16216 struct Row8;
16217 struct Row10;"#},
16218 base_text,
16219 &mut cx,
16220 );
16221 assert_hunk_revert(
16222 indoc! {r#"struct Row;
16223 struct Row2;
16224
16225 «ˇstruct Row4;
16226 struct» Row5;
16227 «struct Row6;
16228 ˇ»
16229 struct Row8;
16230 struct Row10;"#},
16231 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16232 indoc! {r#"struct Row;
16233 struct Row2;
16234
16235 «ˇstruct Row4;
16236 struct» Row5;
16237 «struct Row6;
16238 ˇ»
16239 struct Row8;
16240 struct Row10;"#},
16241 base_text,
16242 &mut cx,
16243 );
16244
16245 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
16246 assert_hunk_revert(
16247 indoc! {r#"struct Row;
16248 ˇstruct Row2;
16249
16250 struct Row4;
16251 struct Row5;
16252 struct Row6;
16253
16254 struct Row8;ˇ
16255 struct Row10;"#},
16256 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16257 indoc! {r#"struct Row;
16258 struct Row1;
16259 ˇstruct Row2;
16260
16261 struct Row4;
16262 struct Row5;
16263 struct Row6;
16264
16265 struct Row8;ˇ
16266 struct Row9;
16267 struct Row10;"#},
16268 base_text,
16269 &mut cx,
16270 );
16271 assert_hunk_revert(
16272 indoc! {r#"struct Row;
16273 struct Row2«ˇ;
16274 struct Row4;
16275 struct» Row5;
16276 «struct Row6;
16277
16278 struct Row8;ˇ»
16279 struct Row10;"#},
16280 vec![
16281 DiffHunkStatusKind::Deleted,
16282 DiffHunkStatusKind::Deleted,
16283 DiffHunkStatusKind::Deleted,
16284 ],
16285 indoc! {r#"struct Row;
16286 struct Row1;
16287 struct Row2«ˇ;
16288
16289 struct Row4;
16290 struct» Row5;
16291 «struct Row6;
16292
16293 struct Row8;ˇ»
16294 struct Row9;
16295 struct Row10;"#},
16296 base_text,
16297 &mut cx,
16298 );
16299}
16300
16301#[gpui::test]
16302async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
16303 init_test(cx, |_| {});
16304
16305 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
16306 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
16307 let base_text_3 =
16308 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
16309
16310 let text_1 = edit_first_char_of_every_line(base_text_1);
16311 let text_2 = edit_first_char_of_every_line(base_text_2);
16312 let text_3 = edit_first_char_of_every_line(base_text_3);
16313
16314 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
16315 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
16316 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
16317
16318 let multibuffer = cx.new(|cx| {
16319 let mut multibuffer = MultiBuffer::new(ReadWrite);
16320 multibuffer.push_excerpts(
16321 buffer_1.clone(),
16322 [
16323 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16324 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16325 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16326 ],
16327 cx,
16328 );
16329 multibuffer.push_excerpts(
16330 buffer_2.clone(),
16331 [
16332 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16333 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16334 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16335 ],
16336 cx,
16337 );
16338 multibuffer.push_excerpts(
16339 buffer_3.clone(),
16340 [
16341 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16342 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16343 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16344 ],
16345 cx,
16346 );
16347 multibuffer
16348 });
16349
16350 let fs = FakeFs::new(cx.executor());
16351 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
16352 let (editor, cx) = cx
16353 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
16354 editor.update_in(cx, |editor, _window, cx| {
16355 for (buffer, diff_base) in [
16356 (buffer_1.clone(), base_text_1),
16357 (buffer_2.clone(), base_text_2),
16358 (buffer_3.clone(), base_text_3),
16359 ] {
16360 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
16361 editor
16362 .buffer
16363 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
16364 }
16365 });
16366 cx.executor().run_until_parked();
16367
16368 editor.update_in(cx, |editor, window, cx| {
16369 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}");
16370 editor.select_all(&SelectAll, window, cx);
16371 editor.git_restore(&Default::default(), window, cx);
16372 });
16373 cx.executor().run_until_parked();
16374
16375 // When all ranges are selected, all buffer hunks are reverted.
16376 editor.update(cx, |editor, cx| {
16377 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");
16378 });
16379 buffer_1.update(cx, |buffer, _| {
16380 assert_eq!(buffer.text(), base_text_1);
16381 });
16382 buffer_2.update(cx, |buffer, _| {
16383 assert_eq!(buffer.text(), base_text_2);
16384 });
16385 buffer_3.update(cx, |buffer, _| {
16386 assert_eq!(buffer.text(), base_text_3);
16387 });
16388
16389 editor.update_in(cx, |editor, window, cx| {
16390 editor.undo(&Default::default(), window, cx);
16391 });
16392
16393 editor.update_in(cx, |editor, window, cx| {
16394 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16395 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
16396 });
16397 editor.git_restore(&Default::default(), window, cx);
16398 });
16399
16400 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
16401 // but not affect buffer_2 and its related excerpts.
16402 editor.update(cx, |editor, cx| {
16403 assert_eq!(
16404 editor.text(cx),
16405 "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}"
16406 );
16407 });
16408 buffer_1.update(cx, |buffer, _| {
16409 assert_eq!(buffer.text(), base_text_1);
16410 });
16411 buffer_2.update(cx, |buffer, _| {
16412 assert_eq!(
16413 buffer.text(),
16414 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
16415 );
16416 });
16417 buffer_3.update(cx, |buffer, _| {
16418 assert_eq!(
16419 buffer.text(),
16420 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
16421 );
16422 });
16423
16424 fn edit_first_char_of_every_line(text: &str) -> String {
16425 text.split('\n')
16426 .map(|line| format!("X{}", &line[1..]))
16427 .collect::<Vec<_>>()
16428 .join("\n")
16429 }
16430}
16431
16432#[gpui::test]
16433async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
16434 init_test(cx, |_| {});
16435
16436 let cols = 4;
16437 let rows = 10;
16438 let sample_text_1 = sample_text(rows, cols, 'a');
16439 assert_eq!(
16440 sample_text_1,
16441 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
16442 );
16443 let sample_text_2 = sample_text(rows, cols, 'l');
16444 assert_eq!(
16445 sample_text_2,
16446 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
16447 );
16448 let sample_text_3 = sample_text(rows, cols, 'v');
16449 assert_eq!(
16450 sample_text_3,
16451 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
16452 );
16453
16454 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
16455 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
16456 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
16457
16458 let multi_buffer = cx.new(|cx| {
16459 let mut multibuffer = MultiBuffer::new(ReadWrite);
16460 multibuffer.push_excerpts(
16461 buffer_1.clone(),
16462 [
16463 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16464 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16465 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16466 ],
16467 cx,
16468 );
16469 multibuffer.push_excerpts(
16470 buffer_2.clone(),
16471 [
16472 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16473 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16474 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16475 ],
16476 cx,
16477 );
16478 multibuffer.push_excerpts(
16479 buffer_3.clone(),
16480 [
16481 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16482 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16483 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16484 ],
16485 cx,
16486 );
16487 multibuffer
16488 });
16489
16490 let fs = FakeFs::new(cx.executor());
16491 fs.insert_tree(
16492 "/a",
16493 json!({
16494 "main.rs": sample_text_1,
16495 "other.rs": sample_text_2,
16496 "lib.rs": sample_text_3,
16497 }),
16498 )
16499 .await;
16500 let project = Project::test(fs, ["/a".as_ref()], cx).await;
16501 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16502 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16503 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16504 Editor::new(
16505 EditorMode::full(),
16506 multi_buffer,
16507 Some(project.clone()),
16508 window,
16509 cx,
16510 )
16511 });
16512 let multibuffer_item_id = workspace
16513 .update(cx, |workspace, window, cx| {
16514 assert!(
16515 workspace.active_item(cx).is_none(),
16516 "active item should be None before the first item is added"
16517 );
16518 workspace.add_item_to_active_pane(
16519 Box::new(multi_buffer_editor.clone()),
16520 None,
16521 true,
16522 window,
16523 cx,
16524 );
16525 let active_item = workspace
16526 .active_item(cx)
16527 .expect("should have an active item after adding the multi buffer");
16528 assert!(
16529 !active_item.is_singleton(cx),
16530 "A multi buffer was expected to active after adding"
16531 );
16532 active_item.item_id()
16533 })
16534 .unwrap();
16535 cx.executor().run_until_parked();
16536
16537 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16538 editor.change_selections(
16539 SelectionEffects::scroll(Autoscroll::Next),
16540 window,
16541 cx,
16542 |s| s.select_ranges(Some(1..2)),
16543 );
16544 editor.open_excerpts(&OpenExcerpts, window, cx);
16545 });
16546 cx.executor().run_until_parked();
16547 let first_item_id = workspace
16548 .update(cx, |workspace, window, cx| {
16549 let active_item = workspace
16550 .active_item(cx)
16551 .expect("should have an active item after navigating into the 1st buffer");
16552 let first_item_id = active_item.item_id();
16553 assert_ne!(
16554 first_item_id, multibuffer_item_id,
16555 "Should navigate into the 1st buffer and activate it"
16556 );
16557 assert!(
16558 active_item.is_singleton(cx),
16559 "New active item should be a singleton buffer"
16560 );
16561 assert_eq!(
16562 active_item
16563 .act_as::<Editor>(cx)
16564 .expect("should have navigated into an editor for the 1st buffer")
16565 .read(cx)
16566 .text(cx),
16567 sample_text_1
16568 );
16569
16570 workspace
16571 .go_back(workspace.active_pane().downgrade(), window, cx)
16572 .detach_and_log_err(cx);
16573
16574 first_item_id
16575 })
16576 .unwrap();
16577 cx.executor().run_until_parked();
16578 workspace
16579 .update(cx, |workspace, _, cx| {
16580 let active_item = workspace
16581 .active_item(cx)
16582 .expect("should have an active item after navigating back");
16583 assert_eq!(
16584 active_item.item_id(),
16585 multibuffer_item_id,
16586 "Should navigate back to the multi buffer"
16587 );
16588 assert!(!active_item.is_singleton(cx));
16589 })
16590 .unwrap();
16591
16592 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16593 editor.change_selections(
16594 SelectionEffects::scroll(Autoscroll::Next),
16595 window,
16596 cx,
16597 |s| s.select_ranges(Some(39..40)),
16598 );
16599 editor.open_excerpts(&OpenExcerpts, window, cx);
16600 });
16601 cx.executor().run_until_parked();
16602 let second_item_id = workspace
16603 .update(cx, |workspace, window, cx| {
16604 let active_item = workspace
16605 .active_item(cx)
16606 .expect("should have an active item after navigating into the 2nd buffer");
16607 let second_item_id = active_item.item_id();
16608 assert_ne!(
16609 second_item_id, multibuffer_item_id,
16610 "Should navigate away from the multibuffer"
16611 );
16612 assert_ne!(
16613 second_item_id, first_item_id,
16614 "Should navigate into the 2nd buffer and activate it"
16615 );
16616 assert!(
16617 active_item.is_singleton(cx),
16618 "New active item should be a singleton buffer"
16619 );
16620 assert_eq!(
16621 active_item
16622 .act_as::<Editor>(cx)
16623 .expect("should have navigated into an editor")
16624 .read(cx)
16625 .text(cx),
16626 sample_text_2
16627 );
16628
16629 workspace
16630 .go_back(workspace.active_pane().downgrade(), window, cx)
16631 .detach_and_log_err(cx);
16632
16633 second_item_id
16634 })
16635 .unwrap();
16636 cx.executor().run_until_parked();
16637 workspace
16638 .update(cx, |workspace, _, cx| {
16639 let active_item = workspace
16640 .active_item(cx)
16641 .expect("should have an active item after navigating back from the 2nd buffer");
16642 assert_eq!(
16643 active_item.item_id(),
16644 multibuffer_item_id,
16645 "Should navigate back from the 2nd buffer to the multi buffer"
16646 );
16647 assert!(!active_item.is_singleton(cx));
16648 })
16649 .unwrap();
16650
16651 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16652 editor.change_selections(
16653 SelectionEffects::scroll(Autoscroll::Next),
16654 window,
16655 cx,
16656 |s| s.select_ranges(Some(70..70)),
16657 );
16658 editor.open_excerpts(&OpenExcerpts, window, cx);
16659 });
16660 cx.executor().run_until_parked();
16661 workspace
16662 .update(cx, |workspace, window, cx| {
16663 let active_item = workspace
16664 .active_item(cx)
16665 .expect("should have an active item after navigating into the 3rd buffer");
16666 let third_item_id = active_item.item_id();
16667 assert_ne!(
16668 third_item_id, multibuffer_item_id,
16669 "Should navigate into the 3rd buffer and activate it"
16670 );
16671 assert_ne!(third_item_id, first_item_id);
16672 assert_ne!(third_item_id, second_item_id);
16673 assert!(
16674 active_item.is_singleton(cx),
16675 "New active item should be a singleton buffer"
16676 );
16677 assert_eq!(
16678 active_item
16679 .act_as::<Editor>(cx)
16680 .expect("should have navigated into an editor")
16681 .read(cx)
16682 .text(cx),
16683 sample_text_3
16684 );
16685
16686 workspace
16687 .go_back(workspace.active_pane().downgrade(), window, cx)
16688 .detach_and_log_err(cx);
16689 })
16690 .unwrap();
16691 cx.executor().run_until_parked();
16692 workspace
16693 .update(cx, |workspace, _, cx| {
16694 let active_item = workspace
16695 .active_item(cx)
16696 .expect("should have an active item after navigating back from the 3rd buffer");
16697 assert_eq!(
16698 active_item.item_id(),
16699 multibuffer_item_id,
16700 "Should navigate back from the 3rd buffer to the multi buffer"
16701 );
16702 assert!(!active_item.is_singleton(cx));
16703 })
16704 .unwrap();
16705}
16706
16707#[gpui::test]
16708async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16709 init_test(cx, |_| {});
16710
16711 let mut cx = EditorTestContext::new(cx).await;
16712
16713 let diff_base = r#"
16714 use some::mod;
16715
16716 const A: u32 = 42;
16717
16718 fn main() {
16719 println!("hello");
16720
16721 println!("world");
16722 }
16723 "#
16724 .unindent();
16725
16726 cx.set_state(
16727 &r#"
16728 use some::modified;
16729
16730 ˇ
16731 fn main() {
16732 println!("hello there");
16733
16734 println!("around the");
16735 println!("world");
16736 }
16737 "#
16738 .unindent(),
16739 );
16740
16741 cx.set_head_text(&diff_base);
16742 executor.run_until_parked();
16743
16744 cx.update_editor(|editor, window, cx| {
16745 editor.go_to_next_hunk(&GoToHunk, window, cx);
16746 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16747 });
16748 executor.run_until_parked();
16749 cx.assert_state_with_diff(
16750 r#"
16751 use some::modified;
16752
16753
16754 fn main() {
16755 - println!("hello");
16756 + ˇ println!("hello there");
16757
16758 println!("around the");
16759 println!("world");
16760 }
16761 "#
16762 .unindent(),
16763 );
16764
16765 cx.update_editor(|editor, window, cx| {
16766 for _ in 0..2 {
16767 editor.go_to_next_hunk(&GoToHunk, window, cx);
16768 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16769 }
16770 });
16771 executor.run_until_parked();
16772 cx.assert_state_with_diff(
16773 r#"
16774 - use some::mod;
16775 + ˇuse some::modified;
16776
16777
16778 fn main() {
16779 - println!("hello");
16780 + println!("hello there");
16781
16782 + println!("around the");
16783 println!("world");
16784 }
16785 "#
16786 .unindent(),
16787 );
16788
16789 cx.update_editor(|editor, window, cx| {
16790 editor.go_to_next_hunk(&GoToHunk, window, cx);
16791 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16792 });
16793 executor.run_until_parked();
16794 cx.assert_state_with_diff(
16795 r#"
16796 - use some::mod;
16797 + use some::modified;
16798
16799 - const A: u32 = 42;
16800 ˇ
16801 fn main() {
16802 - println!("hello");
16803 + println!("hello there");
16804
16805 + println!("around the");
16806 println!("world");
16807 }
16808 "#
16809 .unindent(),
16810 );
16811
16812 cx.update_editor(|editor, window, cx| {
16813 editor.cancel(&Cancel, window, cx);
16814 });
16815
16816 cx.assert_state_with_diff(
16817 r#"
16818 use some::modified;
16819
16820 ˇ
16821 fn main() {
16822 println!("hello there");
16823
16824 println!("around the");
16825 println!("world");
16826 }
16827 "#
16828 .unindent(),
16829 );
16830}
16831
16832#[gpui::test]
16833async fn test_diff_base_change_with_expanded_diff_hunks(
16834 executor: BackgroundExecutor,
16835 cx: &mut TestAppContext,
16836) {
16837 init_test(cx, |_| {});
16838
16839 let mut cx = EditorTestContext::new(cx).await;
16840
16841 let diff_base = r#"
16842 use some::mod1;
16843 use some::mod2;
16844
16845 const A: u32 = 42;
16846 const B: u32 = 42;
16847 const C: u32 = 42;
16848
16849 fn main() {
16850 println!("hello");
16851
16852 println!("world");
16853 }
16854 "#
16855 .unindent();
16856
16857 cx.set_state(
16858 &r#"
16859 use some::mod2;
16860
16861 const A: u32 = 42;
16862 const C: u32 = 42;
16863
16864 fn main(ˇ) {
16865 //println!("hello");
16866
16867 println!("world");
16868 //
16869 //
16870 }
16871 "#
16872 .unindent(),
16873 );
16874
16875 cx.set_head_text(&diff_base);
16876 executor.run_until_parked();
16877
16878 cx.update_editor(|editor, window, cx| {
16879 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16880 });
16881 executor.run_until_parked();
16882 cx.assert_state_with_diff(
16883 r#"
16884 - use some::mod1;
16885 use some::mod2;
16886
16887 const A: u32 = 42;
16888 - const B: u32 = 42;
16889 const C: u32 = 42;
16890
16891 fn main(ˇ) {
16892 - println!("hello");
16893 + //println!("hello");
16894
16895 println!("world");
16896 + //
16897 + //
16898 }
16899 "#
16900 .unindent(),
16901 );
16902
16903 cx.set_head_text("new diff base!");
16904 executor.run_until_parked();
16905 cx.assert_state_with_diff(
16906 r#"
16907 - new diff base!
16908 + use some::mod2;
16909 +
16910 + const A: u32 = 42;
16911 + const C: u32 = 42;
16912 +
16913 + fn main(ˇ) {
16914 + //println!("hello");
16915 +
16916 + println!("world");
16917 + //
16918 + //
16919 + }
16920 "#
16921 .unindent(),
16922 );
16923}
16924
16925#[gpui::test]
16926async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
16927 init_test(cx, |_| {});
16928
16929 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
16930 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
16931 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
16932 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
16933 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
16934 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
16935
16936 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
16937 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
16938 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
16939
16940 let multi_buffer = cx.new(|cx| {
16941 let mut multibuffer = MultiBuffer::new(ReadWrite);
16942 multibuffer.push_excerpts(
16943 buffer_1.clone(),
16944 [
16945 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16946 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16947 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16948 ],
16949 cx,
16950 );
16951 multibuffer.push_excerpts(
16952 buffer_2.clone(),
16953 [
16954 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16955 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16956 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16957 ],
16958 cx,
16959 );
16960 multibuffer.push_excerpts(
16961 buffer_3.clone(),
16962 [
16963 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16964 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16965 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16966 ],
16967 cx,
16968 );
16969 multibuffer
16970 });
16971
16972 let editor =
16973 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
16974 editor
16975 .update(cx, |editor, _window, cx| {
16976 for (buffer, diff_base) in [
16977 (buffer_1.clone(), file_1_old),
16978 (buffer_2.clone(), file_2_old),
16979 (buffer_3.clone(), file_3_old),
16980 ] {
16981 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
16982 editor
16983 .buffer
16984 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
16985 }
16986 })
16987 .unwrap();
16988
16989 let mut cx = EditorTestContext::for_editor(editor, cx).await;
16990 cx.run_until_parked();
16991
16992 cx.assert_editor_state(
16993 &"
16994 ˇaaa
16995 ccc
16996 ddd
16997
16998 ggg
16999 hhh
17000
17001
17002 lll
17003 mmm
17004 NNN
17005
17006 qqq
17007 rrr
17008
17009 uuu
17010 111
17011 222
17012 333
17013
17014 666
17015 777
17016
17017 000
17018 !!!"
17019 .unindent(),
17020 );
17021
17022 cx.update_editor(|editor, window, cx| {
17023 editor.select_all(&SelectAll, window, cx);
17024 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17025 });
17026 cx.executor().run_until_parked();
17027
17028 cx.assert_state_with_diff(
17029 "
17030 «aaa
17031 - bbb
17032 ccc
17033 ddd
17034
17035 ggg
17036 hhh
17037
17038
17039 lll
17040 mmm
17041 - nnn
17042 + NNN
17043
17044 qqq
17045 rrr
17046
17047 uuu
17048 111
17049 222
17050 333
17051
17052 + 666
17053 777
17054
17055 000
17056 !!!ˇ»"
17057 .unindent(),
17058 );
17059}
17060
17061#[gpui::test]
17062async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
17063 init_test(cx, |_| {});
17064
17065 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
17066 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
17067
17068 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
17069 let multi_buffer = cx.new(|cx| {
17070 let mut multibuffer = MultiBuffer::new(ReadWrite);
17071 multibuffer.push_excerpts(
17072 buffer.clone(),
17073 [
17074 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
17075 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
17076 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
17077 ],
17078 cx,
17079 );
17080 multibuffer
17081 });
17082
17083 let editor =
17084 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17085 editor
17086 .update(cx, |editor, _window, cx| {
17087 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
17088 editor
17089 .buffer
17090 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
17091 })
17092 .unwrap();
17093
17094 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17095 cx.run_until_parked();
17096
17097 cx.update_editor(|editor, window, cx| {
17098 editor.expand_all_diff_hunks(&Default::default(), window, cx)
17099 });
17100 cx.executor().run_until_parked();
17101
17102 // When the start of a hunk coincides with the start of its excerpt,
17103 // the hunk is expanded. When the start of a a hunk is earlier than
17104 // the start of its excerpt, the hunk is not expanded.
17105 cx.assert_state_with_diff(
17106 "
17107 ˇaaa
17108 - bbb
17109 + BBB
17110
17111 - ddd
17112 - eee
17113 + DDD
17114 + EEE
17115 fff
17116
17117 iii
17118 "
17119 .unindent(),
17120 );
17121}
17122
17123#[gpui::test]
17124async fn test_edits_around_expanded_insertion_hunks(
17125 executor: BackgroundExecutor,
17126 cx: &mut TestAppContext,
17127) {
17128 init_test(cx, |_| {});
17129
17130 let mut cx = EditorTestContext::new(cx).await;
17131
17132 let diff_base = r#"
17133 use some::mod1;
17134 use some::mod2;
17135
17136 const A: u32 = 42;
17137
17138 fn main() {
17139 println!("hello");
17140
17141 println!("world");
17142 }
17143 "#
17144 .unindent();
17145 executor.run_until_parked();
17146 cx.set_state(
17147 &r#"
17148 use some::mod1;
17149 use some::mod2;
17150
17151 const A: u32 = 42;
17152 const B: u32 = 42;
17153 const C: u32 = 42;
17154 ˇ
17155
17156 fn main() {
17157 println!("hello");
17158
17159 println!("world");
17160 }
17161 "#
17162 .unindent(),
17163 );
17164
17165 cx.set_head_text(&diff_base);
17166 executor.run_until_parked();
17167
17168 cx.update_editor(|editor, window, cx| {
17169 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17170 });
17171 executor.run_until_parked();
17172
17173 cx.assert_state_with_diff(
17174 r#"
17175 use some::mod1;
17176 use some::mod2;
17177
17178 const A: u32 = 42;
17179 + const B: u32 = 42;
17180 + const C: u32 = 42;
17181 + ˇ
17182
17183 fn main() {
17184 println!("hello");
17185
17186 println!("world");
17187 }
17188 "#
17189 .unindent(),
17190 );
17191
17192 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
17193 executor.run_until_parked();
17194
17195 cx.assert_state_with_diff(
17196 r#"
17197 use some::mod1;
17198 use some::mod2;
17199
17200 const A: u32 = 42;
17201 + const B: u32 = 42;
17202 + const C: u32 = 42;
17203 + const D: u32 = 42;
17204 + ˇ
17205
17206 fn main() {
17207 println!("hello");
17208
17209 println!("world");
17210 }
17211 "#
17212 .unindent(),
17213 );
17214
17215 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
17216 executor.run_until_parked();
17217
17218 cx.assert_state_with_diff(
17219 r#"
17220 use some::mod1;
17221 use some::mod2;
17222
17223 const A: u32 = 42;
17224 + const B: u32 = 42;
17225 + const C: u32 = 42;
17226 + const D: u32 = 42;
17227 + const E: u32 = 42;
17228 + ˇ
17229
17230 fn main() {
17231 println!("hello");
17232
17233 println!("world");
17234 }
17235 "#
17236 .unindent(),
17237 );
17238
17239 cx.update_editor(|editor, window, cx| {
17240 editor.delete_line(&DeleteLine, window, cx);
17241 });
17242 executor.run_until_parked();
17243
17244 cx.assert_state_with_diff(
17245 r#"
17246 use some::mod1;
17247 use some::mod2;
17248
17249 const A: u32 = 42;
17250 + const B: u32 = 42;
17251 + const C: u32 = 42;
17252 + const D: u32 = 42;
17253 + const E: u32 = 42;
17254 ˇ
17255 fn main() {
17256 println!("hello");
17257
17258 println!("world");
17259 }
17260 "#
17261 .unindent(),
17262 );
17263
17264 cx.update_editor(|editor, window, cx| {
17265 editor.move_up(&MoveUp, window, cx);
17266 editor.delete_line(&DeleteLine, window, cx);
17267 editor.move_up(&MoveUp, window, cx);
17268 editor.delete_line(&DeleteLine, window, cx);
17269 editor.move_up(&MoveUp, window, cx);
17270 editor.delete_line(&DeleteLine, window, cx);
17271 });
17272 executor.run_until_parked();
17273 cx.assert_state_with_diff(
17274 r#"
17275 use some::mod1;
17276 use some::mod2;
17277
17278 const A: u32 = 42;
17279 + const B: u32 = 42;
17280 ˇ
17281 fn main() {
17282 println!("hello");
17283
17284 println!("world");
17285 }
17286 "#
17287 .unindent(),
17288 );
17289
17290 cx.update_editor(|editor, window, cx| {
17291 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
17292 editor.delete_line(&DeleteLine, window, cx);
17293 });
17294 executor.run_until_parked();
17295 cx.assert_state_with_diff(
17296 r#"
17297 ˇ
17298 fn main() {
17299 println!("hello");
17300
17301 println!("world");
17302 }
17303 "#
17304 .unindent(),
17305 );
17306}
17307
17308#[gpui::test]
17309async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
17310 init_test(cx, |_| {});
17311
17312 let mut cx = EditorTestContext::new(cx).await;
17313 cx.set_head_text(indoc! { "
17314 one
17315 two
17316 three
17317 four
17318 five
17319 "
17320 });
17321 cx.set_state(indoc! { "
17322 one
17323 ˇthree
17324 five
17325 "});
17326 cx.run_until_parked();
17327 cx.update_editor(|editor, window, cx| {
17328 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17329 });
17330 cx.assert_state_with_diff(
17331 indoc! { "
17332 one
17333 - two
17334 ˇthree
17335 - four
17336 five
17337 "}
17338 .to_string(),
17339 );
17340 cx.update_editor(|editor, window, cx| {
17341 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17342 });
17343
17344 cx.assert_state_with_diff(
17345 indoc! { "
17346 one
17347 ˇthree
17348 five
17349 "}
17350 .to_string(),
17351 );
17352
17353 cx.set_state(indoc! { "
17354 one
17355 ˇTWO
17356 three
17357 four
17358 five
17359 "});
17360 cx.run_until_parked();
17361 cx.update_editor(|editor, window, cx| {
17362 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17363 });
17364
17365 cx.assert_state_with_diff(
17366 indoc! { "
17367 one
17368 - two
17369 + ˇTWO
17370 three
17371 four
17372 five
17373 "}
17374 .to_string(),
17375 );
17376 cx.update_editor(|editor, window, cx| {
17377 editor.move_up(&Default::default(), window, cx);
17378 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17379 });
17380 cx.assert_state_with_diff(
17381 indoc! { "
17382 one
17383 ˇTWO
17384 three
17385 four
17386 five
17387 "}
17388 .to_string(),
17389 );
17390}
17391
17392#[gpui::test]
17393async fn test_edits_around_expanded_deletion_hunks(
17394 executor: BackgroundExecutor,
17395 cx: &mut TestAppContext,
17396) {
17397 init_test(cx, |_| {});
17398
17399 let mut cx = EditorTestContext::new(cx).await;
17400
17401 let diff_base = r#"
17402 use some::mod1;
17403 use some::mod2;
17404
17405 const A: u32 = 42;
17406 const B: u32 = 42;
17407 const C: u32 = 42;
17408
17409
17410 fn main() {
17411 println!("hello");
17412
17413 println!("world");
17414 }
17415 "#
17416 .unindent();
17417 executor.run_until_parked();
17418 cx.set_state(
17419 &r#"
17420 use some::mod1;
17421 use some::mod2;
17422
17423 ˇconst B: u32 = 42;
17424 const C: u32 = 42;
17425
17426
17427 fn main() {
17428 println!("hello");
17429
17430 println!("world");
17431 }
17432 "#
17433 .unindent(),
17434 );
17435
17436 cx.set_head_text(&diff_base);
17437 executor.run_until_parked();
17438
17439 cx.update_editor(|editor, window, cx| {
17440 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17441 });
17442 executor.run_until_parked();
17443
17444 cx.assert_state_with_diff(
17445 r#"
17446 use some::mod1;
17447 use some::mod2;
17448
17449 - const A: u32 = 42;
17450 ˇconst B: u32 = 42;
17451 const C: u32 = 42;
17452
17453
17454 fn main() {
17455 println!("hello");
17456
17457 println!("world");
17458 }
17459 "#
17460 .unindent(),
17461 );
17462
17463 cx.update_editor(|editor, window, cx| {
17464 editor.delete_line(&DeleteLine, window, cx);
17465 });
17466 executor.run_until_parked();
17467 cx.assert_state_with_diff(
17468 r#"
17469 use some::mod1;
17470 use some::mod2;
17471
17472 - const A: u32 = 42;
17473 - const B: u32 = 42;
17474 ˇconst C: u32 = 42;
17475
17476
17477 fn main() {
17478 println!("hello");
17479
17480 println!("world");
17481 }
17482 "#
17483 .unindent(),
17484 );
17485
17486 cx.update_editor(|editor, window, cx| {
17487 editor.delete_line(&DeleteLine, window, cx);
17488 });
17489 executor.run_until_parked();
17490 cx.assert_state_with_diff(
17491 r#"
17492 use some::mod1;
17493 use some::mod2;
17494
17495 - const A: u32 = 42;
17496 - const B: u32 = 42;
17497 - const C: u32 = 42;
17498 ˇ
17499
17500 fn main() {
17501 println!("hello");
17502
17503 println!("world");
17504 }
17505 "#
17506 .unindent(),
17507 );
17508
17509 cx.update_editor(|editor, window, cx| {
17510 editor.handle_input("replacement", window, cx);
17511 });
17512 executor.run_until_parked();
17513 cx.assert_state_with_diff(
17514 r#"
17515 use some::mod1;
17516 use some::mod2;
17517
17518 - const A: u32 = 42;
17519 - const B: u32 = 42;
17520 - const C: u32 = 42;
17521 -
17522 + replacementˇ
17523
17524 fn main() {
17525 println!("hello");
17526
17527 println!("world");
17528 }
17529 "#
17530 .unindent(),
17531 );
17532}
17533
17534#[gpui::test]
17535async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17536 init_test(cx, |_| {});
17537
17538 let mut cx = EditorTestContext::new(cx).await;
17539
17540 let base_text = r#"
17541 one
17542 two
17543 three
17544 four
17545 five
17546 "#
17547 .unindent();
17548 executor.run_until_parked();
17549 cx.set_state(
17550 &r#"
17551 one
17552 two
17553 fˇour
17554 five
17555 "#
17556 .unindent(),
17557 );
17558
17559 cx.set_head_text(&base_text);
17560 executor.run_until_parked();
17561
17562 cx.update_editor(|editor, window, cx| {
17563 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17564 });
17565 executor.run_until_parked();
17566
17567 cx.assert_state_with_diff(
17568 r#"
17569 one
17570 two
17571 - three
17572 fˇour
17573 five
17574 "#
17575 .unindent(),
17576 );
17577
17578 cx.update_editor(|editor, window, cx| {
17579 editor.backspace(&Backspace, window, cx);
17580 editor.backspace(&Backspace, window, cx);
17581 });
17582 executor.run_until_parked();
17583 cx.assert_state_with_diff(
17584 r#"
17585 one
17586 two
17587 - threeˇ
17588 - four
17589 + our
17590 five
17591 "#
17592 .unindent(),
17593 );
17594}
17595
17596#[gpui::test]
17597async fn test_edit_after_expanded_modification_hunk(
17598 executor: BackgroundExecutor,
17599 cx: &mut TestAppContext,
17600) {
17601 init_test(cx, |_| {});
17602
17603 let mut cx = EditorTestContext::new(cx).await;
17604
17605 let diff_base = r#"
17606 use some::mod1;
17607 use some::mod2;
17608
17609 const A: u32 = 42;
17610 const B: u32 = 42;
17611 const C: u32 = 42;
17612 const D: u32 = 42;
17613
17614
17615 fn main() {
17616 println!("hello");
17617
17618 println!("world");
17619 }"#
17620 .unindent();
17621
17622 cx.set_state(
17623 &r#"
17624 use some::mod1;
17625 use some::mod2;
17626
17627 const A: u32 = 42;
17628 const B: u32 = 42;
17629 const C: u32 = 43ˇ
17630 const D: u32 = 42;
17631
17632
17633 fn main() {
17634 println!("hello");
17635
17636 println!("world");
17637 }"#
17638 .unindent(),
17639 );
17640
17641 cx.set_head_text(&diff_base);
17642 executor.run_until_parked();
17643 cx.update_editor(|editor, window, cx| {
17644 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17645 });
17646 executor.run_until_parked();
17647
17648 cx.assert_state_with_diff(
17649 r#"
17650 use some::mod1;
17651 use some::mod2;
17652
17653 const A: u32 = 42;
17654 const B: u32 = 42;
17655 - const C: u32 = 42;
17656 + const C: u32 = 43ˇ
17657 const D: u32 = 42;
17658
17659
17660 fn main() {
17661 println!("hello");
17662
17663 println!("world");
17664 }"#
17665 .unindent(),
17666 );
17667
17668 cx.update_editor(|editor, window, cx| {
17669 editor.handle_input("\nnew_line\n", window, cx);
17670 });
17671 executor.run_until_parked();
17672
17673 cx.assert_state_with_diff(
17674 r#"
17675 use some::mod1;
17676 use some::mod2;
17677
17678 const A: u32 = 42;
17679 const B: u32 = 42;
17680 - const C: u32 = 42;
17681 + const C: u32 = 43
17682 + new_line
17683 + ˇ
17684 const D: u32 = 42;
17685
17686
17687 fn main() {
17688 println!("hello");
17689
17690 println!("world");
17691 }"#
17692 .unindent(),
17693 );
17694}
17695
17696#[gpui::test]
17697async fn test_stage_and_unstage_added_file_hunk(
17698 executor: BackgroundExecutor,
17699 cx: &mut TestAppContext,
17700) {
17701 init_test(cx, |_| {});
17702
17703 let mut cx = EditorTestContext::new(cx).await;
17704 cx.update_editor(|editor, _, cx| {
17705 editor.set_expand_all_diff_hunks(cx);
17706 });
17707
17708 let working_copy = r#"
17709 ˇfn main() {
17710 println!("hello, world!");
17711 }
17712 "#
17713 .unindent();
17714
17715 cx.set_state(&working_copy);
17716 executor.run_until_parked();
17717
17718 cx.assert_state_with_diff(
17719 r#"
17720 + ˇfn main() {
17721 + println!("hello, world!");
17722 + }
17723 "#
17724 .unindent(),
17725 );
17726 cx.assert_index_text(None);
17727
17728 cx.update_editor(|editor, window, cx| {
17729 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17730 });
17731 executor.run_until_parked();
17732 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
17733 cx.assert_state_with_diff(
17734 r#"
17735 + ˇfn main() {
17736 + println!("hello, world!");
17737 + }
17738 "#
17739 .unindent(),
17740 );
17741
17742 cx.update_editor(|editor, window, cx| {
17743 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17744 });
17745 executor.run_until_parked();
17746 cx.assert_index_text(None);
17747}
17748
17749async fn setup_indent_guides_editor(
17750 text: &str,
17751 cx: &mut TestAppContext,
17752) -> (BufferId, EditorTestContext) {
17753 init_test(cx, |_| {});
17754
17755 let mut cx = EditorTestContext::new(cx).await;
17756
17757 let buffer_id = cx.update_editor(|editor, window, cx| {
17758 editor.set_text(text, window, cx);
17759 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
17760
17761 buffer_ids[0]
17762 });
17763
17764 (buffer_id, cx)
17765}
17766
17767fn assert_indent_guides(
17768 range: Range<u32>,
17769 expected: Vec<IndentGuide>,
17770 active_indices: Option<Vec<usize>>,
17771 cx: &mut EditorTestContext,
17772) {
17773 let indent_guides = cx.update_editor(|editor, window, cx| {
17774 let snapshot = editor.snapshot(window, cx).display_snapshot;
17775 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
17776 editor,
17777 MultiBufferRow(range.start)..MultiBufferRow(range.end),
17778 true,
17779 &snapshot,
17780 cx,
17781 );
17782
17783 indent_guides.sort_by(|a, b| {
17784 a.depth.cmp(&b.depth).then(
17785 a.start_row
17786 .cmp(&b.start_row)
17787 .then(a.end_row.cmp(&b.end_row)),
17788 )
17789 });
17790 indent_guides
17791 });
17792
17793 if let Some(expected) = active_indices {
17794 let active_indices = cx.update_editor(|editor, window, cx| {
17795 let snapshot = editor.snapshot(window, cx).display_snapshot;
17796 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
17797 });
17798
17799 assert_eq!(
17800 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
17801 expected,
17802 "Active indent guide indices do not match"
17803 );
17804 }
17805
17806 assert_eq!(indent_guides, expected, "Indent guides do not match");
17807}
17808
17809fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
17810 IndentGuide {
17811 buffer_id,
17812 start_row: MultiBufferRow(start_row),
17813 end_row: MultiBufferRow(end_row),
17814 depth,
17815 tab_size: 4,
17816 settings: IndentGuideSettings {
17817 enabled: true,
17818 line_width: 1,
17819 active_line_width: 1,
17820 ..Default::default()
17821 },
17822 }
17823}
17824
17825#[gpui::test]
17826async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
17827 let (buffer_id, mut cx) = setup_indent_guides_editor(
17828 &"
17829 fn main() {
17830 let a = 1;
17831 }"
17832 .unindent(),
17833 cx,
17834 )
17835 .await;
17836
17837 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
17838}
17839
17840#[gpui::test]
17841async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
17842 let (buffer_id, mut cx) = setup_indent_guides_editor(
17843 &"
17844 fn main() {
17845 let a = 1;
17846 let b = 2;
17847 }"
17848 .unindent(),
17849 cx,
17850 )
17851 .await;
17852
17853 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
17854}
17855
17856#[gpui::test]
17857async fn test_indent_guide_nested(cx: &mut TestAppContext) {
17858 let (buffer_id, mut cx) = setup_indent_guides_editor(
17859 &"
17860 fn main() {
17861 let a = 1;
17862 if a == 3 {
17863 let b = 2;
17864 } else {
17865 let c = 3;
17866 }
17867 }"
17868 .unindent(),
17869 cx,
17870 )
17871 .await;
17872
17873 assert_indent_guides(
17874 0..8,
17875 vec![
17876 indent_guide(buffer_id, 1, 6, 0),
17877 indent_guide(buffer_id, 3, 3, 1),
17878 indent_guide(buffer_id, 5, 5, 1),
17879 ],
17880 None,
17881 &mut cx,
17882 );
17883}
17884
17885#[gpui::test]
17886async fn test_indent_guide_tab(cx: &mut TestAppContext) {
17887 let (buffer_id, mut cx) = setup_indent_guides_editor(
17888 &"
17889 fn main() {
17890 let a = 1;
17891 let b = 2;
17892 let c = 3;
17893 }"
17894 .unindent(),
17895 cx,
17896 )
17897 .await;
17898
17899 assert_indent_guides(
17900 0..5,
17901 vec![
17902 indent_guide(buffer_id, 1, 3, 0),
17903 indent_guide(buffer_id, 2, 2, 1),
17904 ],
17905 None,
17906 &mut cx,
17907 );
17908}
17909
17910#[gpui::test]
17911async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
17912 let (buffer_id, mut cx) = setup_indent_guides_editor(
17913 &"
17914 fn main() {
17915 let a = 1;
17916
17917 let c = 3;
17918 }"
17919 .unindent(),
17920 cx,
17921 )
17922 .await;
17923
17924 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
17925}
17926
17927#[gpui::test]
17928async fn test_indent_guide_complex(cx: &mut TestAppContext) {
17929 let (buffer_id, mut cx) = setup_indent_guides_editor(
17930 &"
17931 fn main() {
17932 let a = 1;
17933
17934 let c = 3;
17935
17936 if a == 3 {
17937 let b = 2;
17938 } else {
17939 let c = 3;
17940 }
17941 }"
17942 .unindent(),
17943 cx,
17944 )
17945 .await;
17946
17947 assert_indent_guides(
17948 0..11,
17949 vec![
17950 indent_guide(buffer_id, 1, 9, 0),
17951 indent_guide(buffer_id, 6, 6, 1),
17952 indent_guide(buffer_id, 8, 8, 1),
17953 ],
17954 None,
17955 &mut cx,
17956 );
17957}
17958
17959#[gpui::test]
17960async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
17961 let (buffer_id, mut cx) = setup_indent_guides_editor(
17962 &"
17963 fn main() {
17964 let a = 1;
17965
17966 let c = 3;
17967
17968 if a == 3 {
17969 let b = 2;
17970 } else {
17971 let c = 3;
17972 }
17973 }"
17974 .unindent(),
17975 cx,
17976 )
17977 .await;
17978
17979 assert_indent_guides(
17980 1..11,
17981 vec![
17982 indent_guide(buffer_id, 1, 9, 0),
17983 indent_guide(buffer_id, 6, 6, 1),
17984 indent_guide(buffer_id, 8, 8, 1),
17985 ],
17986 None,
17987 &mut cx,
17988 );
17989}
17990
17991#[gpui::test]
17992async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
17993 let (buffer_id, mut cx) = setup_indent_guides_editor(
17994 &"
17995 fn main() {
17996 let a = 1;
17997
17998 let c = 3;
17999
18000 if a == 3 {
18001 let b = 2;
18002 } else {
18003 let c = 3;
18004 }
18005 }"
18006 .unindent(),
18007 cx,
18008 )
18009 .await;
18010
18011 assert_indent_guides(
18012 1..10,
18013 vec![
18014 indent_guide(buffer_id, 1, 9, 0),
18015 indent_guide(buffer_id, 6, 6, 1),
18016 indent_guide(buffer_id, 8, 8, 1),
18017 ],
18018 None,
18019 &mut cx,
18020 );
18021}
18022
18023#[gpui::test]
18024async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
18025 let (buffer_id, mut cx) = setup_indent_guides_editor(
18026 &"
18027 fn main() {
18028 if a {
18029 b(
18030 c,
18031 d,
18032 )
18033 } else {
18034 e(
18035 f
18036 )
18037 }
18038 }"
18039 .unindent(),
18040 cx,
18041 )
18042 .await;
18043
18044 assert_indent_guides(
18045 0..11,
18046 vec![
18047 indent_guide(buffer_id, 1, 10, 0),
18048 indent_guide(buffer_id, 2, 5, 1),
18049 indent_guide(buffer_id, 7, 9, 1),
18050 indent_guide(buffer_id, 3, 4, 2),
18051 indent_guide(buffer_id, 8, 8, 2),
18052 ],
18053 None,
18054 &mut cx,
18055 );
18056
18057 cx.update_editor(|editor, window, cx| {
18058 editor.fold_at(MultiBufferRow(2), window, cx);
18059 assert_eq!(
18060 editor.display_text(cx),
18061 "
18062 fn main() {
18063 if a {
18064 b(⋯
18065 )
18066 } else {
18067 e(
18068 f
18069 )
18070 }
18071 }"
18072 .unindent()
18073 );
18074 });
18075
18076 assert_indent_guides(
18077 0..11,
18078 vec![
18079 indent_guide(buffer_id, 1, 10, 0),
18080 indent_guide(buffer_id, 2, 5, 1),
18081 indent_guide(buffer_id, 7, 9, 1),
18082 indent_guide(buffer_id, 8, 8, 2),
18083 ],
18084 None,
18085 &mut cx,
18086 );
18087}
18088
18089#[gpui::test]
18090async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
18091 let (buffer_id, mut cx) = setup_indent_guides_editor(
18092 &"
18093 block1
18094 block2
18095 block3
18096 block4
18097 block2
18098 block1
18099 block1"
18100 .unindent(),
18101 cx,
18102 )
18103 .await;
18104
18105 assert_indent_guides(
18106 1..10,
18107 vec![
18108 indent_guide(buffer_id, 1, 4, 0),
18109 indent_guide(buffer_id, 2, 3, 1),
18110 indent_guide(buffer_id, 3, 3, 2),
18111 ],
18112 None,
18113 &mut cx,
18114 );
18115}
18116
18117#[gpui::test]
18118async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
18119 let (buffer_id, mut cx) = setup_indent_guides_editor(
18120 &"
18121 block1
18122 block2
18123 block3
18124
18125 block1
18126 block1"
18127 .unindent(),
18128 cx,
18129 )
18130 .await;
18131
18132 assert_indent_guides(
18133 0..6,
18134 vec![
18135 indent_guide(buffer_id, 1, 2, 0),
18136 indent_guide(buffer_id, 2, 2, 1),
18137 ],
18138 None,
18139 &mut cx,
18140 );
18141}
18142
18143#[gpui::test]
18144async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
18145 let (buffer_id, mut cx) = setup_indent_guides_editor(
18146 &"
18147 function component() {
18148 \treturn (
18149 \t\t\t
18150 \t\t<div>
18151 \t\t\t<abc></abc>
18152 \t\t</div>
18153 \t)
18154 }"
18155 .unindent(),
18156 cx,
18157 )
18158 .await;
18159
18160 assert_indent_guides(
18161 0..8,
18162 vec![
18163 indent_guide(buffer_id, 1, 6, 0),
18164 indent_guide(buffer_id, 2, 5, 1),
18165 indent_guide(buffer_id, 4, 4, 2),
18166 ],
18167 None,
18168 &mut cx,
18169 );
18170}
18171
18172#[gpui::test]
18173async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
18174 let (buffer_id, mut cx) = setup_indent_guides_editor(
18175 &"
18176 function component() {
18177 \treturn (
18178 \t
18179 \t\t<div>
18180 \t\t\t<abc></abc>
18181 \t\t</div>
18182 \t)
18183 }"
18184 .unindent(),
18185 cx,
18186 )
18187 .await;
18188
18189 assert_indent_guides(
18190 0..8,
18191 vec![
18192 indent_guide(buffer_id, 1, 6, 0),
18193 indent_guide(buffer_id, 2, 5, 1),
18194 indent_guide(buffer_id, 4, 4, 2),
18195 ],
18196 None,
18197 &mut cx,
18198 );
18199}
18200
18201#[gpui::test]
18202async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
18203 let (buffer_id, mut cx) = setup_indent_guides_editor(
18204 &"
18205 block1
18206
18207
18208
18209 block2
18210 "
18211 .unindent(),
18212 cx,
18213 )
18214 .await;
18215
18216 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18217}
18218
18219#[gpui::test]
18220async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
18221 let (buffer_id, mut cx) = setup_indent_guides_editor(
18222 &"
18223 def a:
18224 \tb = 3
18225 \tif True:
18226 \t\tc = 4
18227 \t\td = 5
18228 \tprint(b)
18229 "
18230 .unindent(),
18231 cx,
18232 )
18233 .await;
18234
18235 assert_indent_guides(
18236 0..6,
18237 vec![
18238 indent_guide(buffer_id, 1, 5, 0),
18239 indent_guide(buffer_id, 3, 4, 1),
18240 ],
18241 None,
18242 &mut cx,
18243 );
18244}
18245
18246#[gpui::test]
18247async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
18248 let (buffer_id, mut cx) = setup_indent_guides_editor(
18249 &"
18250 fn main() {
18251 let a = 1;
18252 }"
18253 .unindent(),
18254 cx,
18255 )
18256 .await;
18257
18258 cx.update_editor(|editor, window, cx| {
18259 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18260 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18261 });
18262 });
18263
18264 assert_indent_guides(
18265 0..3,
18266 vec![indent_guide(buffer_id, 1, 1, 0)],
18267 Some(vec![0]),
18268 &mut cx,
18269 );
18270}
18271
18272#[gpui::test]
18273async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
18274 let (buffer_id, mut cx) = setup_indent_guides_editor(
18275 &"
18276 fn main() {
18277 if 1 == 2 {
18278 let a = 1;
18279 }
18280 }"
18281 .unindent(),
18282 cx,
18283 )
18284 .await;
18285
18286 cx.update_editor(|editor, window, cx| {
18287 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18288 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18289 });
18290 });
18291
18292 assert_indent_guides(
18293 0..4,
18294 vec![
18295 indent_guide(buffer_id, 1, 3, 0),
18296 indent_guide(buffer_id, 2, 2, 1),
18297 ],
18298 Some(vec![1]),
18299 &mut cx,
18300 );
18301
18302 cx.update_editor(|editor, window, cx| {
18303 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18304 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18305 });
18306 });
18307
18308 assert_indent_guides(
18309 0..4,
18310 vec![
18311 indent_guide(buffer_id, 1, 3, 0),
18312 indent_guide(buffer_id, 2, 2, 1),
18313 ],
18314 Some(vec![1]),
18315 &mut cx,
18316 );
18317
18318 cx.update_editor(|editor, window, cx| {
18319 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18320 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
18321 });
18322 });
18323
18324 assert_indent_guides(
18325 0..4,
18326 vec![
18327 indent_guide(buffer_id, 1, 3, 0),
18328 indent_guide(buffer_id, 2, 2, 1),
18329 ],
18330 Some(vec![0]),
18331 &mut cx,
18332 );
18333}
18334
18335#[gpui::test]
18336async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
18337 let (buffer_id, mut cx) = setup_indent_guides_editor(
18338 &"
18339 fn main() {
18340 let a = 1;
18341
18342 let b = 2;
18343 }"
18344 .unindent(),
18345 cx,
18346 )
18347 .await;
18348
18349 cx.update_editor(|editor, window, cx| {
18350 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18351 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18352 });
18353 });
18354
18355 assert_indent_guides(
18356 0..5,
18357 vec![indent_guide(buffer_id, 1, 3, 0)],
18358 Some(vec![0]),
18359 &mut cx,
18360 );
18361}
18362
18363#[gpui::test]
18364async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
18365 let (buffer_id, mut cx) = setup_indent_guides_editor(
18366 &"
18367 def m:
18368 a = 1
18369 pass"
18370 .unindent(),
18371 cx,
18372 )
18373 .await;
18374
18375 cx.update_editor(|editor, window, cx| {
18376 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18377 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18378 });
18379 });
18380
18381 assert_indent_guides(
18382 0..3,
18383 vec![indent_guide(buffer_id, 1, 2, 0)],
18384 Some(vec![0]),
18385 &mut cx,
18386 );
18387}
18388
18389#[gpui::test]
18390async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
18391 init_test(cx, |_| {});
18392 let mut cx = EditorTestContext::new(cx).await;
18393 let text = indoc! {
18394 "
18395 impl A {
18396 fn b() {
18397 0;
18398 3;
18399 5;
18400 6;
18401 7;
18402 }
18403 }
18404 "
18405 };
18406 let base_text = indoc! {
18407 "
18408 impl A {
18409 fn b() {
18410 0;
18411 1;
18412 2;
18413 3;
18414 4;
18415 }
18416 fn c() {
18417 5;
18418 6;
18419 7;
18420 }
18421 }
18422 "
18423 };
18424
18425 cx.update_editor(|editor, window, cx| {
18426 editor.set_text(text, window, cx);
18427
18428 editor.buffer().update(cx, |multibuffer, cx| {
18429 let buffer = multibuffer.as_singleton().unwrap();
18430 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
18431
18432 multibuffer.set_all_diff_hunks_expanded(cx);
18433 multibuffer.add_diff(diff, cx);
18434
18435 buffer.read(cx).remote_id()
18436 })
18437 });
18438 cx.run_until_parked();
18439
18440 cx.assert_state_with_diff(
18441 indoc! { "
18442 impl A {
18443 fn b() {
18444 0;
18445 - 1;
18446 - 2;
18447 3;
18448 - 4;
18449 - }
18450 - fn c() {
18451 5;
18452 6;
18453 7;
18454 }
18455 }
18456 ˇ"
18457 }
18458 .to_string(),
18459 );
18460
18461 let mut actual_guides = cx.update_editor(|editor, window, cx| {
18462 editor
18463 .snapshot(window, cx)
18464 .buffer_snapshot
18465 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
18466 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
18467 .collect::<Vec<_>>()
18468 });
18469 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
18470 assert_eq!(
18471 actual_guides,
18472 vec![
18473 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
18474 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
18475 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
18476 ]
18477 );
18478}
18479
18480#[gpui::test]
18481async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18482 init_test(cx, |_| {});
18483 let mut cx = EditorTestContext::new(cx).await;
18484
18485 let diff_base = r#"
18486 a
18487 b
18488 c
18489 "#
18490 .unindent();
18491
18492 cx.set_state(
18493 &r#"
18494 ˇA
18495 b
18496 C
18497 "#
18498 .unindent(),
18499 );
18500 cx.set_head_text(&diff_base);
18501 cx.update_editor(|editor, window, cx| {
18502 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18503 });
18504 executor.run_until_parked();
18505
18506 let both_hunks_expanded = r#"
18507 - a
18508 + ˇA
18509 b
18510 - c
18511 + C
18512 "#
18513 .unindent();
18514
18515 cx.assert_state_with_diff(both_hunks_expanded.clone());
18516
18517 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18518 let snapshot = editor.snapshot(window, cx);
18519 let hunks = editor
18520 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18521 .collect::<Vec<_>>();
18522 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18523 let buffer_id = hunks[0].buffer_id;
18524 hunks
18525 .into_iter()
18526 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18527 .collect::<Vec<_>>()
18528 });
18529 assert_eq!(hunk_ranges.len(), 2);
18530
18531 cx.update_editor(|editor, _, cx| {
18532 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18533 });
18534 executor.run_until_parked();
18535
18536 let second_hunk_expanded = r#"
18537 ˇA
18538 b
18539 - c
18540 + C
18541 "#
18542 .unindent();
18543
18544 cx.assert_state_with_diff(second_hunk_expanded);
18545
18546 cx.update_editor(|editor, _, cx| {
18547 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18548 });
18549 executor.run_until_parked();
18550
18551 cx.assert_state_with_diff(both_hunks_expanded.clone());
18552
18553 cx.update_editor(|editor, _, cx| {
18554 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18555 });
18556 executor.run_until_parked();
18557
18558 let first_hunk_expanded = r#"
18559 - a
18560 + ˇA
18561 b
18562 C
18563 "#
18564 .unindent();
18565
18566 cx.assert_state_with_diff(first_hunk_expanded);
18567
18568 cx.update_editor(|editor, _, cx| {
18569 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18570 });
18571 executor.run_until_parked();
18572
18573 cx.assert_state_with_diff(both_hunks_expanded);
18574
18575 cx.set_state(
18576 &r#"
18577 ˇA
18578 b
18579 "#
18580 .unindent(),
18581 );
18582 cx.run_until_parked();
18583
18584 // TODO this cursor position seems bad
18585 cx.assert_state_with_diff(
18586 r#"
18587 - ˇa
18588 + A
18589 b
18590 "#
18591 .unindent(),
18592 );
18593
18594 cx.update_editor(|editor, window, cx| {
18595 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18596 });
18597
18598 cx.assert_state_with_diff(
18599 r#"
18600 - ˇa
18601 + A
18602 b
18603 - c
18604 "#
18605 .unindent(),
18606 );
18607
18608 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18609 let snapshot = editor.snapshot(window, cx);
18610 let hunks = editor
18611 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18612 .collect::<Vec<_>>();
18613 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18614 let buffer_id = hunks[0].buffer_id;
18615 hunks
18616 .into_iter()
18617 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18618 .collect::<Vec<_>>()
18619 });
18620 assert_eq!(hunk_ranges.len(), 2);
18621
18622 cx.update_editor(|editor, _, cx| {
18623 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18624 });
18625 executor.run_until_parked();
18626
18627 cx.assert_state_with_diff(
18628 r#"
18629 - ˇa
18630 + A
18631 b
18632 "#
18633 .unindent(),
18634 );
18635}
18636
18637#[gpui::test]
18638async fn test_toggle_deletion_hunk_at_start_of_file(
18639 executor: BackgroundExecutor,
18640 cx: &mut TestAppContext,
18641) {
18642 init_test(cx, |_| {});
18643 let mut cx = EditorTestContext::new(cx).await;
18644
18645 let diff_base = r#"
18646 a
18647 b
18648 c
18649 "#
18650 .unindent();
18651
18652 cx.set_state(
18653 &r#"
18654 ˇb
18655 c
18656 "#
18657 .unindent(),
18658 );
18659 cx.set_head_text(&diff_base);
18660 cx.update_editor(|editor, window, cx| {
18661 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18662 });
18663 executor.run_until_parked();
18664
18665 let hunk_expanded = r#"
18666 - a
18667 ˇb
18668 c
18669 "#
18670 .unindent();
18671
18672 cx.assert_state_with_diff(hunk_expanded.clone());
18673
18674 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18675 let snapshot = editor.snapshot(window, cx);
18676 let hunks = editor
18677 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18678 .collect::<Vec<_>>();
18679 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18680 let buffer_id = hunks[0].buffer_id;
18681 hunks
18682 .into_iter()
18683 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18684 .collect::<Vec<_>>()
18685 });
18686 assert_eq!(hunk_ranges.len(), 1);
18687
18688 cx.update_editor(|editor, _, cx| {
18689 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18690 });
18691 executor.run_until_parked();
18692
18693 let hunk_collapsed = r#"
18694 ˇb
18695 c
18696 "#
18697 .unindent();
18698
18699 cx.assert_state_with_diff(hunk_collapsed);
18700
18701 cx.update_editor(|editor, _, cx| {
18702 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18703 });
18704 executor.run_until_parked();
18705
18706 cx.assert_state_with_diff(hunk_expanded.clone());
18707}
18708
18709#[gpui::test]
18710async fn test_display_diff_hunks(cx: &mut TestAppContext) {
18711 init_test(cx, |_| {});
18712
18713 let fs = FakeFs::new(cx.executor());
18714 fs.insert_tree(
18715 path!("/test"),
18716 json!({
18717 ".git": {},
18718 "file-1": "ONE\n",
18719 "file-2": "TWO\n",
18720 "file-3": "THREE\n",
18721 }),
18722 )
18723 .await;
18724
18725 fs.set_head_for_repo(
18726 path!("/test/.git").as_ref(),
18727 &[
18728 ("file-1".into(), "one\n".into()),
18729 ("file-2".into(), "two\n".into()),
18730 ("file-3".into(), "three\n".into()),
18731 ],
18732 "deadbeef",
18733 );
18734
18735 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
18736 let mut buffers = vec![];
18737 for i in 1..=3 {
18738 let buffer = project
18739 .update(cx, |project, cx| {
18740 let path = format!(path!("/test/file-{}"), i);
18741 project.open_local_buffer(path, cx)
18742 })
18743 .await
18744 .unwrap();
18745 buffers.push(buffer);
18746 }
18747
18748 let multibuffer = cx.new(|cx| {
18749 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
18750 multibuffer.set_all_diff_hunks_expanded(cx);
18751 for buffer in &buffers {
18752 let snapshot = buffer.read(cx).snapshot();
18753 multibuffer.set_excerpts_for_path(
18754 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
18755 buffer.clone(),
18756 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
18757 DEFAULT_MULTIBUFFER_CONTEXT,
18758 cx,
18759 );
18760 }
18761 multibuffer
18762 });
18763
18764 let editor = cx.add_window(|window, cx| {
18765 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
18766 });
18767 cx.run_until_parked();
18768
18769 let snapshot = editor
18770 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18771 .unwrap();
18772 let hunks = snapshot
18773 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
18774 .map(|hunk| match hunk {
18775 DisplayDiffHunk::Unfolded {
18776 display_row_range, ..
18777 } => display_row_range,
18778 DisplayDiffHunk::Folded { .. } => unreachable!(),
18779 })
18780 .collect::<Vec<_>>();
18781 assert_eq!(
18782 hunks,
18783 [
18784 DisplayRow(2)..DisplayRow(4),
18785 DisplayRow(7)..DisplayRow(9),
18786 DisplayRow(12)..DisplayRow(14),
18787 ]
18788 );
18789}
18790
18791#[gpui::test]
18792async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
18793 init_test(cx, |_| {});
18794
18795 let mut cx = EditorTestContext::new(cx).await;
18796 cx.set_head_text(indoc! { "
18797 one
18798 two
18799 three
18800 four
18801 five
18802 "
18803 });
18804 cx.set_index_text(indoc! { "
18805 one
18806 two
18807 three
18808 four
18809 five
18810 "
18811 });
18812 cx.set_state(indoc! {"
18813 one
18814 TWO
18815 ˇTHREE
18816 FOUR
18817 five
18818 "});
18819 cx.run_until_parked();
18820 cx.update_editor(|editor, window, cx| {
18821 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18822 });
18823 cx.run_until_parked();
18824 cx.assert_index_text(Some(indoc! {"
18825 one
18826 TWO
18827 THREE
18828 FOUR
18829 five
18830 "}));
18831 cx.set_state(indoc! { "
18832 one
18833 TWO
18834 ˇTHREE-HUNDRED
18835 FOUR
18836 five
18837 "});
18838 cx.run_until_parked();
18839 cx.update_editor(|editor, window, cx| {
18840 let snapshot = editor.snapshot(window, cx);
18841 let hunks = editor
18842 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18843 .collect::<Vec<_>>();
18844 assert_eq!(hunks.len(), 1);
18845 assert_eq!(
18846 hunks[0].status(),
18847 DiffHunkStatus {
18848 kind: DiffHunkStatusKind::Modified,
18849 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
18850 }
18851 );
18852
18853 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18854 });
18855 cx.run_until_parked();
18856 cx.assert_index_text(Some(indoc! {"
18857 one
18858 TWO
18859 THREE-HUNDRED
18860 FOUR
18861 five
18862 "}));
18863}
18864
18865#[gpui::test]
18866fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
18867 init_test(cx, |_| {});
18868
18869 let editor = cx.add_window(|window, cx| {
18870 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
18871 build_editor(buffer, window, cx)
18872 });
18873
18874 let render_args = Arc::new(Mutex::new(None));
18875 let snapshot = editor
18876 .update(cx, |editor, window, cx| {
18877 let snapshot = editor.buffer().read(cx).snapshot(cx);
18878 let range =
18879 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
18880
18881 struct RenderArgs {
18882 row: MultiBufferRow,
18883 folded: bool,
18884 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
18885 }
18886
18887 let crease = Crease::inline(
18888 range,
18889 FoldPlaceholder::test(),
18890 {
18891 let toggle_callback = render_args.clone();
18892 move |row, folded, callback, _window, _cx| {
18893 *toggle_callback.lock() = Some(RenderArgs {
18894 row,
18895 folded,
18896 callback,
18897 });
18898 div()
18899 }
18900 },
18901 |_row, _folded, _window, _cx| div(),
18902 );
18903
18904 editor.insert_creases(Some(crease), cx);
18905 let snapshot = editor.snapshot(window, cx);
18906 let _div = snapshot.render_crease_toggle(
18907 MultiBufferRow(1),
18908 false,
18909 cx.entity().clone(),
18910 window,
18911 cx,
18912 );
18913 snapshot
18914 })
18915 .unwrap();
18916
18917 let render_args = render_args.lock().take().unwrap();
18918 assert_eq!(render_args.row, MultiBufferRow(1));
18919 assert!(!render_args.folded);
18920 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
18921
18922 cx.update_window(*editor, |_, window, cx| {
18923 (render_args.callback)(true, window, cx)
18924 })
18925 .unwrap();
18926 let snapshot = editor
18927 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18928 .unwrap();
18929 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
18930
18931 cx.update_window(*editor, |_, window, cx| {
18932 (render_args.callback)(false, window, cx)
18933 })
18934 .unwrap();
18935 let snapshot = editor
18936 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18937 .unwrap();
18938 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
18939}
18940
18941#[gpui::test]
18942async fn test_input_text(cx: &mut TestAppContext) {
18943 init_test(cx, |_| {});
18944 let mut cx = EditorTestContext::new(cx).await;
18945
18946 cx.set_state(
18947 &r#"ˇone
18948 two
18949
18950 three
18951 fourˇ
18952 five
18953
18954 siˇx"#
18955 .unindent(),
18956 );
18957
18958 cx.dispatch_action(HandleInput(String::new()));
18959 cx.assert_editor_state(
18960 &r#"ˇone
18961 two
18962
18963 three
18964 fourˇ
18965 five
18966
18967 siˇx"#
18968 .unindent(),
18969 );
18970
18971 cx.dispatch_action(HandleInput("AAAA".to_string()));
18972 cx.assert_editor_state(
18973 &r#"AAAAˇone
18974 two
18975
18976 three
18977 fourAAAAˇ
18978 five
18979
18980 siAAAAˇx"#
18981 .unindent(),
18982 );
18983}
18984
18985#[gpui::test]
18986async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
18987 init_test(cx, |_| {});
18988
18989 let mut cx = EditorTestContext::new(cx).await;
18990 cx.set_state(
18991 r#"let foo = 1;
18992let foo = 2;
18993let foo = 3;
18994let fooˇ = 4;
18995let foo = 5;
18996let foo = 6;
18997let foo = 7;
18998let foo = 8;
18999let foo = 9;
19000let foo = 10;
19001let foo = 11;
19002let foo = 12;
19003let foo = 13;
19004let foo = 14;
19005let foo = 15;"#,
19006 );
19007
19008 cx.update_editor(|e, window, cx| {
19009 assert_eq!(
19010 e.next_scroll_position,
19011 NextScrollCursorCenterTopBottom::Center,
19012 "Default next scroll direction is center",
19013 );
19014
19015 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19016 assert_eq!(
19017 e.next_scroll_position,
19018 NextScrollCursorCenterTopBottom::Top,
19019 "After center, next scroll direction should be top",
19020 );
19021
19022 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19023 assert_eq!(
19024 e.next_scroll_position,
19025 NextScrollCursorCenterTopBottom::Bottom,
19026 "After top, next scroll direction should be bottom",
19027 );
19028
19029 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19030 assert_eq!(
19031 e.next_scroll_position,
19032 NextScrollCursorCenterTopBottom::Center,
19033 "After bottom, scrolling should start over",
19034 );
19035
19036 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19037 assert_eq!(
19038 e.next_scroll_position,
19039 NextScrollCursorCenterTopBottom::Top,
19040 "Scrolling continues if retriggered fast enough"
19041 );
19042 });
19043
19044 cx.executor()
19045 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
19046 cx.executor().run_until_parked();
19047 cx.update_editor(|e, _, _| {
19048 assert_eq!(
19049 e.next_scroll_position,
19050 NextScrollCursorCenterTopBottom::Center,
19051 "If scrolling is not triggered fast enough, it should reset"
19052 );
19053 });
19054}
19055
19056#[gpui::test]
19057async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
19058 init_test(cx, |_| {});
19059 let mut cx = EditorLspTestContext::new_rust(
19060 lsp::ServerCapabilities {
19061 definition_provider: Some(lsp::OneOf::Left(true)),
19062 references_provider: Some(lsp::OneOf::Left(true)),
19063 ..lsp::ServerCapabilities::default()
19064 },
19065 cx,
19066 )
19067 .await;
19068
19069 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
19070 let go_to_definition = cx
19071 .lsp
19072 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19073 move |params, _| async move {
19074 if empty_go_to_definition {
19075 Ok(None)
19076 } else {
19077 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
19078 uri: params.text_document_position_params.text_document.uri,
19079 range: lsp::Range::new(
19080 lsp::Position::new(4, 3),
19081 lsp::Position::new(4, 6),
19082 ),
19083 })))
19084 }
19085 },
19086 );
19087 let references = cx
19088 .lsp
19089 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
19090 Ok(Some(vec![lsp::Location {
19091 uri: params.text_document_position.text_document.uri,
19092 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
19093 }]))
19094 });
19095 (go_to_definition, references)
19096 };
19097
19098 cx.set_state(
19099 &r#"fn one() {
19100 let mut a = ˇtwo();
19101 }
19102
19103 fn two() {}"#
19104 .unindent(),
19105 );
19106 set_up_lsp_handlers(false, &mut cx);
19107 let navigated = cx
19108 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19109 .await
19110 .expect("Failed to navigate to definition");
19111 assert_eq!(
19112 navigated,
19113 Navigated::Yes,
19114 "Should have navigated to definition from the GetDefinition response"
19115 );
19116 cx.assert_editor_state(
19117 &r#"fn one() {
19118 let mut a = two();
19119 }
19120
19121 fn «twoˇ»() {}"#
19122 .unindent(),
19123 );
19124
19125 let editors = cx.update_workspace(|workspace, _, cx| {
19126 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19127 });
19128 cx.update_editor(|_, _, test_editor_cx| {
19129 assert_eq!(
19130 editors.len(),
19131 1,
19132 "Initially, only one, test, editor should be open in the workspace"
19133 );
19134 assert_eq!(
19135 test_editor_cx.entity(),
19136 editors.last().expect("Asserted len is 1").clone()
19137 );
19138 });
19139
19140 set_up_lsp_handlers(true, &mut cx);
19141 let navigated = cx
19142 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19143 .await
19144 .expect("Failed to navigate to lookup references");
19145 assert_eq!(
19146 navigated,
19147 Navigated::Yes,
19148 "Should have navigated to references as a fallback after empty GoToDefinition response"
19149 );
19150 // We should not change the selections in the existing file,
19151 // if opening another milti buffer with the references
19152 cx.assert_editor_state(
19153 &r#"fn one() {
19154 let mut a = two();
19155 }
19156
19157 fn «twoˇ»() {}"#
19158 .unindent(),
19159 );
19160 let editors = cx.update_workspace(|workspace, _, cx| {
19161 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19162 });
19163 cx.update_editor(|_, _, test_editor_cx| {
19164 assert_eq!(
19165 editors.len(),
19166 2,
19167 "After falling back to references search, we open a new editor with the results"
19168 );
19169 let references_fallback_text = editors
19170 .into_iter()
19171 .find(|new_editor| *new_editor != test_editor_cx.entity())
19172 .expect("Should have one non-test editor now")
19173 .read(test_editor_cx)
19174 .text(test_editor_cx);
19175 assert_eq!(
19176 references_fallback_text, "fn one() {\n let mut a = two();\n}",
19177 "Should use the range from the references response and not the GoToDefinition one"
19178 );
19179 });
19180}
19181
19182#[gpui::test]
19183async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
19184 init_test(cx, |_| {});
19185 cx.update(|cx| {
19186 let mut editor_settings = EditorSettings::get_global(cx).clone();
19187 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
19188 EditorSettings::override_global(editor_settings, cx);
19189 });
19190 let mut cx = EditorLspTestContext::new_rust(
19191 lsp::ServerCapabilities {
19192 definition_provider: Some(lsp::OneOf::Left(true)),
19193 references_provider: Some(lsp::OneOf::Left(true)),
19194 ..lsp::ServerCapabilities::default()
19195 },
19196 cx,
19197 )
19198 .await;
19199 let original_state = r#"fn one() {
19200 let mut a = ˇtwo();
19201 }
19202
19203 fn two() {}"#
19204 .unindent();
19205 cx.set_state(&original_state);
19206
19207 let mut go_to_definition = cx
19208 .lsp
19209 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19210 move |_, _| async move { Ok(None) },
19211 );
19212 let _references = cx
19213 .lsp
19214 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
19215 panic!("Should not call for references with no go to definition fallback")
19216 });
19217
19218 let navigated = cx
19219 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19220 .await
19221 .expect("Failed to navigate to lookup references");
19222 go_to_definition
19223 .next()
19224 .await
19225 .expect("Should have called the go_to_definition handler");
19226
19227 assert_eq!(
19228 navigated,
19229 Navigated::No,
19230 "Should have navigated to references as a fallback after empty GoToDefinition response"
19231 );
19232 cx.assert_editor_state(&original_state);
19233 let editors = cx.update_workspace(|workspace, _, cx| {
19234 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19235 });
19236 cx.update_editor(|_, _, _| {
19237 assert_eq!(
19238 editors.len(),
19239 1,
19240 "After unsuccessful fallback, no other editor should have been opened"
19241 );
19242 });
19243}
19244
19245#[gpui::test]
19246async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
19247 init_test(cx, |_| {});
19248
19249 let language = Arc::new(Language::new(
19250 LanguageConfig::default(),
19251 Some(tree_sitter_rust::LANGUAGE.into()),
19252 ));
19253
19254 let text = r#"
19255 #[cfg(test)]
19256 mod tests() {
19257 #[test]
19258 fn runnable_1() {
19259 let a = 1;
19260 }
19261
19262 #[test]
19263 fn runnable_2() {
19264 let a = 1;
19265 let b = 2;
19266 }
19267 }
19268 "#
19269 .unindent();
19270
19271 let fs = FakeFs::new(cx.executor());
19272 fs.insert_file("/file.rs", Default::default()).await;
19273
19274 let project = Project::test(fs, ["/a".as_ref()], cx).await;
19275 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19276 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19277 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
19278 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
19279
19280 let editor = cx.new_window_entity(|window, cx| {
19281 Editor::new(
19282 EditorMode::full(),
19283 multi_buffer,
19284 Some(project.clone()),
19285 window,
19286 cx,
19287 )
19288 });
19289
19290 editor.update_in(cx, |editor, window, cx| {
19291 let snapshot = editor.buffer().read(cx).snapshot(cx);
19292 editor.tasks.insert(
19293 (buffer.read(cx).remote_id(), 3),
19294 RunnableTasks {
19295 templates: vec![],
19296 offset: snapshot.anchor_before(43),
19297 column: 0,
19298 extra_variables: HashMap::default(),
19299 context_range: BufferOffset(43)..BufferOffset(85),
19300 },
19301 );
19302 editor.tasks.insert(
19303 (buffer.read(cx).remote_id(), 8),
19304 RunnableTasks {
19305 templates: vec![],
19306 offset: snapshot.anchor_before(86),
19307 column: 0,
19308 extra_variables: HashMap::default(),
19309 context_range: BufferOffset(86)..BufferOffset(191),
19310 },
19311 );
19312
19313 // Test finding task when cursor is inside function body
19314 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19315 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
19316 });
19317 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19318 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
19319
19320 // Test finding task when cursor is on function name
19321 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19322 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
19323 });
19324 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19325 assert_eq!(row, 8, "Should find task when cursor is on function name");
19326 });
19327}
19328
19329#[gpui::test]
19330async fn test_folding_buffers(cx: &mut TestAppContext) {
19331 init_test(cx, |_| {});
19332
19333 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
19334 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
19335 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
19336
19337 let fs = FakeFs::new(cx.executor());
19338 fs.insert_tree(
19339 path!("/a"),
19340 json!({
19341 "first.rs": sample_text_1,
19342 "second.rs": sample_text_2,
19343 "third.rs": sample_text_3,
19344 }),
19345 )
19346 .await;
19347 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19348 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19349 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19350 let worktree = project.update(cx, |project, cx| {
19351 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19352 assert_eq!(worktrees.len(), 1);
19353 worktrees.pop().unwrap()
19354 });
19355 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19356
19357 let buffer_1 = project
19358 .update(cx, |project, cx| {
19359 project.open_buffer((worktree_id, "first.rs"), cx)
19360 })
19361 .await
19362 .unwrap();
19363 let buffer_2 = project
19364 .update(cx, |project, cx| {
19365 project.open_buffer((worktree_id, "second.rs"), cx)
19366 })
19367 .await
19368 .unwrap();
19369 let buffer_3 = project
19370 .update(cx, |project, cx| {
19371 project.open_buffer((worktree_id, "third.rs"), cx)
19372 })
19373 .await
19374 .unwrap();
19375
19376 let multi_buffer = cx.new(|cx| {
19377 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19378 multi_buffer.push_excerpts(
19379 buffer_1.clone(),
19380 [
19381 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19382 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19383 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19384 ],
19385 cx,
19386 );
19387 multi_buffer.push_excerpts(
19388 buffer_2.clone(),
19389 [
19390 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19391 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19392 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19393 ],
19394 cx,
19395 );
19396 multi_buffer.push_excerpts(
19397 buffer_3.clone(),
19398 [
19399 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19400 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19401 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19402 ],
19403 cx,
19404 );
19405 multi_buffer
19406 });
19407 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19408 Editor::new(
19409 EditorMode::full(),
19410 multi_buffer.clone(),
19411 Some(project.clone()),
19412 window,
19413 cx,
19414 )
19415 });
19416
19417 assert_eq!(
19418 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19419 "\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",
19420 );
19421
19422 multi_buffer_editor.update(cx, |editor, cx| {
19423 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
19424 });
19425 assert_eq!(
19426 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19427 "\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",
19428 "After folding the first buffer, its text should not be displayed"
19429 );
19430
19431 multi_buffer_editor.update(cx, |editor, cx| {
19432 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
19433 });
19434 assert_eq!(
19435 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19436 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
19437 "After folding the second buffer, its text should not be displayed"
19438 );
19439
19440 multi_buffer_editor.update(cx, |editor, cx| {
19441 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
19442 });
19443 assert_eq!(
19444 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19445 "\n\n\n\n\n",
19446 "After folding the third buffer, its text should not be displayed"
19447 );
19448
19449 // Emulate selection inside the fold logic, that should work
19450 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19451 editor
19452 .snapshot(window, cx)
19453 .next_line_boundary(Point::new(0, 4));
19454 });
19455
19456 multi_buffer_editor.update(cx, |editor, cx| {
19457 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
19458 });
19459 assert_eq!(
19460 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19461 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19462 "After unfolding the second buffer, its text should be displayed"
19463 );
19464
19465 // Typing inside of buffer 1 causes that buffer to be unfolded.
19466 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19467 assert_eq!(
19468 multi_buffer
19469 .read(cx)
19470 .snapshot(cx)
19471 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
19472 .collect::<String>(),
19473 "bbbb"
19474 );
19475 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
19476 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
19477 });
19478 editor.handle_input("B", window, cx);
19479 });
19480
19481 assert_eq!(
19482 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19483 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19484 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
19485 );
19486
19487 multi_buffer_editor.update(cx, |editor, cx| {
19488 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
19489 });
19490 assert_eq!(
19491 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19492 "\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",
19493 "After unfolding the all buffers, all original text should be displayed"
19494 );
19495}
19496
19497#[gpui::test]
19498async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
19499 init_test(cx, |_| {});
19500
19501 let sample_text_1 = "1111\n2222\n3333".to_string();
19502 let sample_text_2 = "4444\n5555\n6666".to_string();
19503 let sample_text_3 = "7777\n8888\n9999".to_string();
19504
19505 let fs = FakeFs::new(cx.executor());
19506 fs.insert_tree(
19507 path!("/a"),
19508 json!({
19509 "first.rs": sample_text_1,
19510 "second.rs": sample_text_2,
19511 "third.rs": sample_text_3,
19512 }),
19513 )
19514 .await;
19515 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19516 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19517 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19518 let worktree = project.update(cx, |project, cx| {
19519 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19520 assert_eq!(worktrees.len(), 1);
19521 worktrees.pop().unwrap()
19522 });
19523 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19524
19525 let buffer_1 = project
19526 .update(cx, |project, cx| {
19527 project.open_buffer((worktree_id, "first.rs"), cx)
19528 })
19529 .await
19530 .unwrap();
19531 let buffer_2 = project
19532 .update(cx, |project, cx| {
19533 project.open_buffer((worktree_id, "second.rs"), cx)
19534 })
19535 .await
19536 .unwrap();
19537 let buffer_3 = project
19538 .update(cx, |project, cx| {
19539 project.open_buffer((worktree_id, "third.rs"), cx)
19540 })
19541 .await
19542 .unwrap();
19543
19544 let multi_buffer = cx.new(|cx| {
19545 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19546 multi_buffer.push_excerpts(
19547 buffer_1.clone(),
19548 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19549 cx,
19550 );
19551 multi_buffer.push_excerpts(
19552 buffer_2.clone(),
19553 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19554 cx,
19555 );
19556 multi_buffer.push_excerpts(
19557 buffer_3.clone(),
19558 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19559 cx,
19560 );
19561 multi_buffer
19562 });
19563
19564 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19565 Editor::new(
19566 EditorMode::full(),
19567 multi_buffer,
19568 Some(project.clone()),
19569 window,
19570 cx,
19571 )
19572 });
19573
19574 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
19575 assert_eq!(
19576 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19577 full_text,
19578 );
19579
19580 multi_buffer_editor.update(cx, |editor, cx| {
19581 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
19582 });
19583 assert_eq!(
19584 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19585 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
19586 "After folding the first buffer, its text should not be displayed"
19587 );
19588
19589 multi_buffer_editor.update(cx, |editor, cx| {
19590 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
19591 });
19592
19593 assert_eq!(
19594 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19595 "\n\n\n\n\n\n7777\n8888\n9999",
19596 "After folding the second buffer, its text should not be displayed"
19597 );
19598
19599 multi_buffer_editor.update(cx, |editor, cx| {
19600 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
19601 });
19602 assert_eq!(
19603 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19604 "\n\n\n\n\n",
19605 "After folding the third buffer, its text should not be displayed"
19606 );
19607
19608 multi_buffer_editor.update(cx, |editor, cx| {
19609 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
19610 });
19611 assert_eq!(
19612 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19613 "\n\n\n\n4444\n5555\n6666\n\n",
19614 "After unfolding the second buffer, its text should be displayed"
19615 );
19616
19617 multi_buffer_editor.update(cx, |editor, cx| {
19618 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
19619 });
19620 assert_eq!(
19621 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19622 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
19623 "After unfolding the first buffer, its text should be displayed"
19624 );
19625
19626 multi_buffer_editor.update(cx, |editor, cx| {
19627 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
19628 });
19629 assert_eq!(
19630 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19631 full_text,
19632 "After unfolding all buffers, all original text should be displayed"
19633 );
19634}
19635
19636#[gpui::test]
19637async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
19638 init_test(cx, |_| {});
19639
19640 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
19641
19642 let fs = FakeFs::new(cx.executor());
19643 fs.insert_tree(
19644 path!("/a"),
19645 json!({
19646 "main.rs": sample_text,
19647 }),
19648 )
19649 .await;
19650 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19651 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19652 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19653 let worktree = project.update(cx, |project, cx| {
19654 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19655 assert_eq!(worktrees.len(), 1);
19656 worktrees.pop().unwrap()
19657 });
19658 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19659
19660 let buffer_1 = project
19661 .update(cx, |project, cx| {
19662 project.open_buffer((worktree_id, "main.rs"), cx)
19663 })
19664 .await
19665 .unwrap();
19666
19667 let multi_buffer = cx.new(|cx| {
19668 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19669 multi_buffer.push_excerpts(
19670 buffer_1.clone(),
19671 [ExcerptRange::new(
19672 Point::new(0, 0)
19673 ..Point::new(
19674 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
19675 0,
19676 ),
19677 )],
19678 cx,
19679 );
19680 multi_buffer
19681 });
19682 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19683 Editor::new(
19684 EditorMode::full(),
19685 multi_buffer,
19686 Some(project.clone()),
19687 window,
19688 cx,
19689 )
19690 });
19691
19692 let selection_range = Point::new(1, 0)..Point::new(2, 0);
19693 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19694 enum TestHighlight {}
19695 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
19696 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
19697 editor.highlight_text::<TestHighlight>(
19698 vec![highlight_range.clone()],
19699 HighlightStyle::color(Hsla::green()),
19700 cx,
19701 );
19702 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19703 s.select_ranges(Some(highlight_range))
19704 });
19705 });
19706
19707 let full_text = format!("\n\n{sample_text}");
19708 assert_eq!(
19709 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19710 full_text,
19711 );
19712}
19713
19714#[gpui::test]
19715async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
19716 init_test(cx, |_| {});
19717 cx.update(|cx| {
19718 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
19719 "keymaps/default-linux.json",
19720 cx,
19721 )
19722 .unwrap();
19723 cx.bind_keys(default_key_bindings);
19724 });
19725
19726 let (editor, cx) = cx.add_window_view(|window, cx| {
19727 let multi_buffer = MultiBuffer::build_multi(
19728 [
19729 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
19730 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
19731 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
19732 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
19733 ],
19734 cx,
19735 );
19736 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
19737
19738 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
19739 // fold all but the second buffer, so that we test navigating between two
19740 // adjacent folded buffers, as well as folded buffers at the start and
19741 // end the multibuffer
19742 editor.fold_buffer(buffer_ids[0], cx);
19743 editor.fold_buffer(buffer_ids[2], cx);
19744 editor.fold_buffer(buffer_ids[3], cx);
19745
19746 editor
19747 });
19748 cx.simulate_resize(size(px(1000.), px(1000.)));
19749
19750 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
19751 cx.assert_excerpts_with_selections(indoc! {"
19752 [EXCERPT]
19753 ˇ[FOLDED]
19754 [EXCERPT]
19755 a1
19756 b1
19757 [EXCERPT]
19758 [FOLDED]
19759 [EXCERPT]
19760 [FOLDED]
19761 "
19762 });
19763 cx.simulate_keystroke("down");
19764 cx.assert_excerpts_with_selections(indoc! {"
19765 [EXCERPT]
19766 [FOLDED]
19767 [EXCERPT]
19768 ˇa1
19769 b1
19770 [EXCERPT]
19771 [FOLDED]
19772 [EXCERPT]
19773 [FOLDED]
19774 "
19775 });
19776 cx.simulate_keystroke("down");
19777 cx.assert_excerpts_with_selections(indoc! {"
19778 [EXCERPT]
19779 [FOLDED]
19780 [EXCERPT]
19781 a1
19782 ˇb1
19783 [EXCERPT]
19784 [FOLDED]
19785 [EXCERPT]
19786 [FOLDED]
19787 "
19788 });
19789 cx.simulate_keystroke("down");
19790 cx.assert_excerpts_with_selections(indoc! {"
19791 [EXCERPT]
19792 [FOLDED]
19793 [EXCERPT]
19794 a1
19795 b1
19796 ˇ[EXCERPT]
19797 [FOLDED]
19798 [EXCERPT]
19799 [FOLDED]
19800 "
19801 });
19802 cx.simulate_keystroke("down");
19803 cx.assert_excerpts_with_selections(indoc! {"
19804 [EXCERPT]
19805 [FOLDED]
19806 [EXCERPT]
19807 a1
19808 b1
19809 [EXCERPT]
19810 ˇ[FOLDED]
19811 [EXCERPT]
19812 [FOLDED]
19813 "
19814 });
19815 for _ in 0..5 {
19816 cx.simulate_keystroke("down");
19817 cx.assert_excerpts_with_selections(indoc! {"
19818 [EXCERPT]
19819 [FOLDED]
19820 [EXCERPT]
19821 a1
19822 b1
19823 [EXCERPT]
19824 [FOLDED]
19825 [EXCERPT]
19826 ˇ[FOLDED]
19827 "
19828 });
19829 }
19830
19831 cx.simulate_keystroke("up");
19832 cx.assert_excerpts_with_selections(indoc! {"
19833 [EXCERPT]
19834 [FOLDED]
19835 [EXCERPT]
19836 a1
19837 b1
19838 [EXCERPT]
19839 ˇ[FOLDED]
19840 [EXCERPT]
19841 [FOLDED]
19842 "
19843 });
19844 cx.simulate_keystroke("up");
19845 cx.assert_excerpts_with_selections(indoc! {"
19846 [EXCERPT]
19847 [FOLDED]
19848 [EXCERPT]
19849 a1
19850 b1
19851 ˇ[EXCERPT]
19852 [FOLDED]
19853 [EXCERPT]
19854 [FOLDED]
19855 "
19856 });
19857 cx.simulate_keystroke("up");
19858 cx.assert_excerpts_with_selections(indoc! {"
19859 [EXCERPT]
19860 [FOLDED]
19861 [EXCERPT]
19862 a1
19863 ˇb1
19864 [EXCERPT]
19865 [FOLDED]
19866 [EXCERPT]
19867 [FOLDED]
19868 "
19869 });
19870 cx.simulate_keystroke("up");
19871 cx.assert_excerpts_with_selections(indoc! {"
19872 [EXCERPT]
19873 [FOLDED]
19874 [EXCERPT]
19875 ˇa1
19876 b1
19877 [EXCERPT]
19878 [FOLDED]
19879 [EXCERPT]
19880 [FOLDED]
19881 "
19882 });
19883 for _ in 0..5 {
19884 cx.simulate_keystroke("up");
19885 cx.assert_excerpts_with_selections(indoc! {"
19886 [EXCERPT]
19887 ˇ[FOLDED]
19888 [EXCERPT]
19889 a1
19890 b1
19891 [EXCERPT]
19892 [FOLDED]
19893 [EXCERPT]
19894 [FOLDED]
19895 "
19896 });
19897 }
19898}
19899
19900#[gpui::test]
19901async fn test_inline_completion_text(cx: &mut TestAppContext) {
19902 init_test(cx, |_| {});
19903
19904 // Simple insertion
19905 assert_highlighted_edits(
19906 "Hello, world!",
19907 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
19908 true,
19909 cx,
19910 |highlighted_edits, cx| {
19911 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
19912 assert_eq!(highlighted_edits.highlights.len(), 1);
19913 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
19914 assert_eq!(
19915 highlighted_edits.highlights[0].1.background_color,
19916 Some(cx.theme().status().created_background)
19917 );
19918 },
19919 )
19920 .await;
19921
19922 // Replacement
19923 assert_highlighted_edits(
19924 "This is a test.",
19925 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
19926 false,
19927 cx,
19928 |highlighted_edits, cx| {
19929 assert_eq!(highlighted_edits.text, "That is a test.");
19930 assert_eq!(highlighted_edits.highlights.len(), 1);
19931 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
19932 assert_eq!(
19933 highlighted_edits.highlights[0].1.background_color,
19934 Some(cx.theme().status().created_background)
19935 );
19936 },
19937 )
19938 .await;
19939
19940 // Multiple edits
19941 assert_highlighted_edits(
19942 "Hello, world!",
19943 vec![
19944 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
19945 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
19946 ],
19947 false,
19948 cx,
19949 |highlighted_edits, cx| {
19950 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
19951 assert_eq!(highlighted_edits.highlights.len(), 2);
19952 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
19953 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
19954 assert_eq!(
19955 highlighted_edits.highlights[0].1.background_color,
19956 Some(cx.theme().status().created_background)
19957 );
19958 assert_eq!(
19959 highlighted_edits.highlights[1].1.background_color,
19960 Some(cx.theme().status().created_background)
19961 );
19962 },
19963 )
19964 .await;
19965
19966 // Multiple lines with edits
19967 assert_highlighted_edits(
19968 "First line\nSecond line\nThird line\nFourth line",
19969 vec![
19970 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
19971 (
19972 Point::new(2, 0)..Point::new(2, 10),
19973 "New third line".to_string(),
19974 ),
19975 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
19976 ],
19977 false,
19978 cx,
19979 |highlighted_edits, cx| {
19980 assert_eq!(
19981 highlighted_edits.text,
19982 "Second modified\nNew third line\nFourth updated line"
19983 );
19984 assert_eq!(highlighted_edits.highlights.len(), 3);
19985 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
19986 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
19987 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
19988 for highlight in &highlighted_edits.highlights {
19989 assert_eq!(
19990 highlight.1.background_color,
19991 Some(cx.theme().status().created_background)
19992 );
19993 }
19994 },
19995 )
19996 .await;
19997}
19998
19999#[gpui::test]
20000async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
20001 init_test(cx, |_| {});
20002
20003 // Deletion
20004 assert_highlighted_edits(
20005 "Hello, world!",
20006 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
20007 true,
20008 cx,
20009 |highlighted_edits, cx| {
20010 assert_eq!(highlighted_edits.text, "Hello, world!");
20011 assert_eq!(highlighted_edits.highlights.len(), 1);
20012 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
20013 assert_eq!(
20014 highlighted_edits.highlights[0].1.background_color,
20015 Some(cx.theme().status().deleted_background)
20016 );
20017 },
20018 )
20019 .await;
20020
20021 // Insertion
20022 assert_highlighted_edits(
20023 "Hello, world!",
20024 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
20025 true,
20026 cx,
20027 |highlighted_edits, cx| {
20028 assert_eq!(highlighted_edits.highlights.len(), 1);
20029 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
20030 assert_eq!(
20031 highlighted_edits.highlights[0].1.background_color,
20032 Some(cx.theme().status().created_background)
20033 );
20034 },
20035 )
20036 .await;
20037}
20038
20039async fn assert_highlighted_edits(
20040 text: &str,
20041 edits: Vec<(Range<Point>, String)>,
20042 include_deletions: bool,
20043 cx: &mut TestAppContext,
20044 assertion_fn: impl Fn(HighlightedText, &App),
20045) {
20046 let window = cx.add_window(|window, cx| {
20047 let buffer = MultiBuffer::build_simple(text, cx);
20048 Editor::new(EditorMode::full(), buffer, None, window, cx)
20049 });
20050 let cx = &mut VisualTestContext::from_window(*window, cx);
20051
20052 let (buffer, snapshot) = window
20053 .update(cx, |editor, _window, cx| {
20054 (
20055 editor.buffer().clone(),
20056 editor.buffer().read(cx).snapshot(cx),
20057 )
20058 })
20059 .unwrap();
20060
20061 let edits = edits
20062 .into_iter()
20063 .map(|(range, edit)| {
20064 (
20065 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
20066 edit,
20067 )
20068 })
20069 .collect::<Vec<_>>();
20070
20071 let text_anchor_edits = edits
20072 .clone()
20073 .into_iter()
20074 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
20075 .collect::<Vec<_>>();
20076
20077 let edit_preview = window
20078 .update(cx, |_, _window, cx| {
20079 buffer
20080 .read(cx)
20081 .as_singleton()
20082 .unwrap()
20083 .read(cx)
20084 .preview_edits(text_anchor_edits.into(), cx)
20085 })
20086 .unwrap()
20087 .await;
20088
20089 cx.update(|_window, cx| {
20090 let highlighted_edits = inline_completion_edit_text(
20091 &snapshot.as_singleton().unwrap().2,
20092 &edits,
20093 &edit_preview,
20094 include_deletions,
20095 cx,
20096 );
20097 assertion_fn(highlighted_edits, cx)
20098 });
20099}
20100
20101#[track_caller]
20102fn assert_breakpoint(
20103 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
20104 path: &Arc<Path>,
20105 expected: Vec<(u32, Breakpoint)>,
20106) {
20107 if expected.len() == 0usize {
20108 assert!(!breakpoints.contains_key(path), "{}", path.display());
20109 } else {
20110 let mut breakpoint = breakpoints
20111 .get(path)
20112 .unwrap()
20113 .into_iter()
20114 .map(|breakpoint| {
20115 (
20116 breakpoint.row,
20117 Breakpoint {
20118 message: breakpoint.message.clone(),
20119 state: breakpoint.state,
20120 condition: breakpoint.condition.clone(),
20121 hit_condition: breakpoint.hit_condition.clone(),
20122 },
20123 )
20124 })
20125 .collect::<Vec<_>>();
20126
20127 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
20128
20129 assert_eq!(expected, breakpoint);
20130 }
20131}
20132
20133fn add_log_breakpoint_at_cursor(
20134 editor: &mut Editor,
20135 log_message: &str,
20136 window: &mut Window,
20137 cx: &mut Context<Editor>,
20138) {
20139 let (anchor, bp) = editor
20140 .breakpoints_at_cursors(window, cx)
20141 .first()
20142 .and_then(|(anchor, bp)| {
20143 if let Some(bp) = bp {
20144 Some((*anchor, bp.clone()))
20145 } else {
20146 None
20147 }
20148 })
20149 .unwrap_or_else(|| {
20150 let cursor_position: Point = editor.selections.newest(cx).head();
20151
20152 let breakpoint_position = editor
20153 .snapshot(window, cx)
20154 .display_snapshot
20155 .buffer_snapshot
20156 .anchor_before(Point::new(cursor_position.row, 0));
20157
20158 (breakpoint_position, Breakpoint::new_log(&log_message))
20159 });
20160
20161 editor.edit_breakpoint_at_anchor(
20162 anchor,
20163 bp,
20164 BreakpointEditAction::EditLogMessage(log_message.into()),
20165 cx,
20166 );
20167}
20168
20169#[gpui::test]
20170async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
20171 init_test(cx, |_| {});
20172
20173 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20174 let fs = FakeFs::new(cx.executor());
20175 fs.insert_tree(
20176 path!("/a"),
20177 json!({
20178 "main.rs": sample_text,
20179 }),
20180 )
20181 .await;
20182 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20183 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20184 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20185
20186 let fs = FakeFs::new(cx.executor());
20187 fs.insert_tree(
20188 path!("/a"),
20189 json!({
20190 "main.rs": sample_text,
20191 }),
20192 )
20193 .await;
20194 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20195 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20196 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20197 let worktree_id = workspace
20198 .update(cx, |workspace, _window, cx| {
20199 workspace.project().update(cx, |project, cx| {
20200 project.worktrees(cx).next().unwrap().read(cx).id()
20201 })
20202 })
20203 .unwrap();
20204
20205 let buffer = project
20206 .update(cx, |project, cx| {
20207 project.open_buffer((worktree_id, "main.rs"), cx)
20208 })
20209 .await
20210 .unwrap();
20211
20212 let (editor, cx) = cx.add_window_view(|window, cx| {
20213 Editor::new(
20214 EditorMode::full(),
20215 MultiBuffer::build_from_buffer(buffer, cx),
20216 Some(project.clone()),
20217 window,
20218 cx,
20219 )
20220 });
20221
20222 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20223 let abs_path = project.read_with(cx, |project, cx| {
20224 project
20225 .absolute_path(&project_path, cx)
20226 .map(|path_buf| Arc::from(path_buf.to_owned()))
20227 .unwrap()
20228 });
20229
20230 // assert we can add breakpoint on the first line
20231 editor.update_in(cx, |editor, window, cx| {
20232 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20233 editor.move_to_end(&MoveToEnd, window, cx);
20234 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20235 });
20236
20237 let breakpoints = editor.update(cx, |editor, cx| {
20238 editor
20239 .breakpoint_store()
20240 .as_ref()
20241 .unwrap()
20242 .read(cx)
20243 .all_source_breakpoints(cx)
20244 .clone()
20245 });
20246
20247 assert_eq!(1, breakpoints.len());
20248 assert_breakpoint(
20249 &breakpoints,
20250 &abs_path,
20251 vec![
20252 (0, Breakpoint::new_standard()),
20253 (3, Breakpoint::new_standard()),
20254 ],
20255 );
20256
20257 editor.update_in(cx, |editor, window, cx| {
20258 editor.move_to_beginning(&MoveToBeginning, window, cx);
20259 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20260 });
20261
20262 let breakpoints = editor.update(cx, |editor, cx| {
20263 editor
20264 .breakpoint_store()
20265 .as_ref()
20266 .unwrap()
20267 .read(cx)
20268 .all_source_breakpoints(cx)
20269 .clone()
20270 });
20271
20272 assert_eq!(1, breakpoints.len());
20273 assert_breakpoint(
20274 &breakpoints,
20275 &abs_path,
20276 vec![(3, Breakpoint::new_standard())],
20277 );
20278
20279 editor.update_in(cx, |editor, window, cx| {
20280 editor.move_to_end(&MoveToEnd, window, cx);
20281 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20282 });
20283
20284 let breakpoints = editor.update(cx, |editor, cx| {
20285 editor
20286 .breakpoint_store()
20287 .as_ref()
20288 .unwrap()
20289 .read(cx)
20290 .all_source_breakpoints(cx)
20291 .clone()
20292 });
20293
20294 assert_eq!(0, breakpoints.len());
20295 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20296}
20297
20298#[gpui::test]
20299async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
20300 init_test(cx, |_| {});
20301
20302 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20303
20304 let fs = FakeFs::new(cx.executor());
20305 fs.insert_tree(
20306 path!("/a"),
20307 json!({
20308 "main.rs": sample_text,
20309 }),
20310 )
20311 .await;
20312 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20313 let (workspace, cx) =
20314 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20315
20316 let worktree_id = workspace.update(cx, |workspace, cx| {
20317 workspace.project().update(cx, |project, cx| {
20318 project.worktrees(cx).next().unwrap().read(cx).id()
20319 })
20320 });
20321
20322 let buffer = project
20323 .update(cx, |project, cx| {
20324 project.open_buffer((worktree_id, "main.rs"), cx)
20325 })
20326 .await
20327 .unwrap();
20328
20329 let (editor, cx) = cx.add_window_view(|window, cx| {
20330 Editor::new(
20331 EditorMode::full(),
20332 MultiBuffer::build_from_buffer(buffer, cx),
20333 Some(project.clone()),
20334 window,
20335 cx,
20336 )
20337 });
20338
20339 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20340 let abs_path = project.read_with(cx, |project, cx| {
20341 project
20342 .absolute_path(&project_path, cx)
20343 .map(|path_buf| Arc::from(path_buf.to_owned()))
20344 .unwrap()
20345 });
20346
20347 editor.update_in(cx, |editor, window, cx| {
20348 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20349 });
20350
20351 let breakpoints = editor.update(cx, |editor, cx| {
20352 editor
20353 .breakpoint_store()
20354 .as_ref()
20355 .unwrap()
20356 .read(cx)
20357 .all_source_breakpoints(cx)
20358 .clone()
20359 });
20360
20361 assert_breakpoint(
20362 &breakpoints,
20363 &abs_path,
20364 vec![(0, Breakpoint::new_log("hello world"))],
20365 );
20366
20367 // Removing a log message from a log breakpoint should remove it
20368 editor.update_in(cx, |editor, window, cx| {
20369 add_log_breakpoint_at_cursor(editor, "", window, cx);
20370 });
20371
20372 let breakpoints = editor.update(cx, |editor, cx| {
20373 editor
20374 .breakpoint_store()
20375 .as_ref()
20376 .unwrap()
20377 .read(cx)
20378 .all_source_breakpoints(cx)
20379 .clone()
20380 });
20381
20382 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20383
20384 editor.update_in(cx, |editor, window, cx| {
20385 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20386 editor.move_to_end(&MoveToEnd, window, cx);
20387 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20388 // Not adding a log message to a standard breakpoint shouldn't remove it
20389 add_log_breakpoint_at_cursor(editor, "", window, cx);
20390 });
20391
20392 let breakpoints = editor.update(cx, |editor, cx| {
20393 editor
20394 .breakpoint_store()
20395 .as_ref()
20396 .unwrap()
20397 .read(cx)
20398 .all_source_breakpoints(cx)
20399 .clone()
20400 });
20401
20402 assert_breakpoint(
20403 &breakpoints,
20404 &abs_path,
20405 vec![
20406 (0, Breakpoint::new_standard()),
20407 (3, Breakpoint::new_standard()),
20408 ],
20409 );
20410
20411 editor.update_in(cx, |editor, window, cx| {
20412 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20413 });
20414
20415 let breakpoints = editor.update(cx, |editor, cx| {
20416 editor
20417 .breakpoint_store()
20418 .as_ref()
20419 .unwrap()
20420 .read(cx)
20421 .all_source_breakpoints(cx)
20422 .clone()
20423 });
20424
20425 assert_breakpoint(
20426 &breakpoints,
20427 &abs_path,
20428 vec![
20429 (0, Breakpoint::new_standard()),
20430 (3, Breakpoint::new_log("hello world")),
20431 ],
20432 );
20433
20434 editor.update_in(cx, |editor, window, cx| {
20435 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
20436 });
20437
20438 let breakpoints = editor.update(cx, |editor, cx| {
20439 editor
20440 .breakpoint_store()
20441 .as_ref()
20442 .unwrap()
20443 .read(cx)
20444 .all_source_breakpoints(cx)
20445 .clone()
20446 });
20447
20448 assert_breakpoint(
20449 &breakpoints,
20450 &abs_path,
20451 vec![
20452 (0, Breakpoint::new_standard()),
20453 (3, Breakpoint::new_log("hello Earth!!")),
20454 ],
20455 );
20456}
20457
20458/// This also tests that Editor::breakpoint_at_cursor_head is working properly
20459/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
20460/// or when breakpoints were placed out of order. This tests for a regression too
20461#[gpui::test]
20462async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
20463 init_test(cx, |_| {});
20464
20465 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20466 let fs = FakeFs::new(cx.executor());
20467 fs.insert_tree(
20468 path!("/a"),
20469 json!({
20470 "main.rs": sample_text,
20471 }),
20472 )
20473 .await;
20474 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20475 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20476 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20477
20478 let fs = FakeFs::new(cx.executor());
20479 fs.insert_tree(
20480 path!("/a"),
20481 json!({
20482 "main.rs": sample_text,
20483 }),
20484 )
20485 .await;
20486 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20487 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20488 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20489 let worktree_id = workspace
20490 .update(cx, |workspace, _window, cx| {
20491 workspace.project().update(cx, |project, cx| {
20492 project.worktrees(cx).next().unwrap().read(cx).id()
20493 })
20494 })
20495 .unwrap();
20496
20497 let buffer = project
20498 .update(cx, |project, cx| {
20499 project.open_buffer((worktree_id, "main.rs"), cx)
20500 })
20501 .await
20502 .unwrap();
20503
20504 let (editor, cx) = cx.add_window_view(|window, cx| {
20505 Editor::new(
20506 EditorMode::full(),
20507 MultiBuffer::build_from_buffer(buffer, cx),
20508 Some(project.clone()),
20509 window,
20510 cx,
20511 )
20512 });
20513
20514 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20515 let abs_path = project.read_with(cx, |project, cx| {
20516 project
20517 .absolute_path(&project_path, cx)
20518 .map(|path_buf| Arc::from(path_buf.to_owned()))
20519 .unwrap()
20520 });
20521
20522 // assert we can add breakpoint on the first line
20523 editor.update_in(cx, |editor, window, cx| {
20524 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20525 editor.move_to_end(&MoveToEnd, window, cx);
20526 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20527 editor.move_up(&MoveUp, window, cx);
20528 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20529 });
20530
20531 let breakpoints = editor.update(cx, |editor, cx| {
20532 editor
20533 .breakpoint_store()
20534 .as_ref()
20535 .unwrap()
20536 .read(cx)
20537 .all_source_breakpoints(cx)
20538 .clone()
20539 });
20540
20541 assert_eq!(1, breakpoints.len());
20542 assert_breakpoint(
20543 &breakpoints,
20544 &abs_path,
20545 vec![
20546 (0, Breakpoint::new_standard()),
20547 (2, Breakpoint::new_standard()),
20548 (3, Breakpoint::new_standard()),
20549 ],
20550 );
20551
20552 editor.update_in(cx, |editor, window, cx| {
20553 editor.move_to_beginning(&MoveToBeginning, window, cx);
20554 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20555 editor.move_to_end(&MoveToEnd, window, cx);
20556 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20557 // Disabling a breakpoint that doesn't exist should do nothing
20558 editor.move_up(&MoveUp, window, cx);
20559 editor.move_up(&MoveUp, window, cx);
20560 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20561 });
20562
20563 let breakpoints = editor.update(cx, |editor, cx| {
20564 editor
20565 .breakpoint_store()
20566 .as_ref()
20567 .unwrap()
20568 .read(cx)
20569 .all_source_breakpoints(cx)
20570 .clone()
20571 });
20572
20573 let disable_breakpoint = {
20574 let mut bp = Breakpoint::new_standard();
20575 bp.state = BreakpointState::Disabled;
20576 bp
20577 };
20578
20579 assert_eq!(1, breakpoints.len());
20580 assert_breakpoint(
20581 &breakpoints,
20582 &abs_path,
20583 vec![
20584 (0, disable_breakpoint.clone()),
20585 (2, Breakpoint::new_standard()),
20586 (3, disable_breakpoint.clone()),
20587 ],
20588 );
20589
20590 editor.update_in(cx, |editor, window, cx| {
20591 editor.move_to_beginning(&MoveToBeginning, window, cx);
20592 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
20593 editor.move_to_end(&MoveToEnd, window, cx);
20594 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
20595 editor.move_up(&MoveUp, window, cx);
20596 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20597 });
20598
20599 let breakpoints = editor.update(cx, |editor, cx| {
20600 editor
20601 .breakpoint_store()
20602 .as_ref()
20603 .unwrap()
20604 .read(cx)
20605 .all_source_breakpoints(cx)
20606 .clone()
20607 });
20608
20609 assert_eq!(1, breakpoints.len());
20610 assert_breakpoint(
20611 &breakpoints,
20612 &abs_path,
20613 vec![
20614 (0, Breakpoint::new_standard()),
20615 (2, disable_breakpoint),
20616 (3, Breakpoint::new_standard()),
20617 ],
20618 );
20619}
20620
20621#[gpui::test]
20622async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
20623 init_test(cx, |_| {});
20624 let capabilities = lsp::ServerCapabilities {
20625 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
20626 prepare_provider: Some(true),
20627 work_done_progress_options: Default::default(),
20628 })),
20629 ..Default::default()
20630 };
20631 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
20632
20633 cx.set_state(indoc! {"
20634 struct Fˇoo {}
20635 "});
20636
20637 cx.update_editor(|editor, _, cx| {
20638 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
20639 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
20640 editor.highlight_background::<DocumentHighlightRead>(
20641 &[highlight_range],
20642 |theme| theme.colors().editor_document_highlight_read_background,
20643 cx,
20644 );
20645 });
20646
20647 let mut prepare_rename_handler = cx
20648 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
20649 move |_, _, _| async move {
20650 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
20651 start: lsp::Position {
20652 line: 0,
20653 character: 7,
20654 },
20655 end: lsp::Position {
20656 line: 0,
20657 character: 10,
20658 },
20659 })))
20660 },
20661 );
20662 let prepare_rename_task = cx
20663 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
20664 .expect("Prepare rename was not started");
20665 prepare_rename_handler.next().await.unwrap();
20666 prepare_rename_task.await.expect("Prepare rename failed");
20667
20668 let mut rename_handler =
20669 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
20670 let edit = lsp::TextEdit {
20671 range: lsp::Range {
20672 start: lsp::Position {
20673 line: 0,
20674 character: 7,
20675 },
20676 end: lsp::Position {
20677 line: 0,
20678 character: 10,
20679 },
20680 },
20681 new_text: "FooRenamed".to_string(),
20682 };
20683 Ok(Some(lsp::WorkspaceEdit::new(
20684 // Specify the same edit twice
20685 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
20686 )))
20687 });
20688 let rename_task = cx
20689 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
20690 .expect("Confirm rename was not started");
20691 rename_handler.next().await.unwrap();
20692 rename_task.await.expect("Confirm rename failed");
20693 cx.run_until_parked();
20694
20695 // Despite two edits, only one is actually applied as those are identical
20696 cx.assert_editor_state(indoc! {"
20697 struct FooRenamedˇ {}
20698 "});
20699}
20700
20701#[gpui::test]
20702async fn test_rename_without_prepare(cx: &mut TestAppContext) {
20703 init_test(cx, |_| {});
20704 // These capabilities indicate that the server does not support prepare rename.
20705 let capabilities = lsp::ServerCapabilities {
20706 rename_provider: Some(lsp::OneOf::Left(true)),
20707 ..Default::default()
20708 };
20709 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
20710
20711 cx.set_state(indoc! {"
20712 struct Fˇoo {}
20713 "});
20714
20715 cx.update_editor(|editor, _window, cx| {
20716 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
20717 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
20718 editor.highlight_background::<DocumentHighlightRead>(
20719 &[highlight_range],
20720 |theme| theme.colors().editor_document_highlight_read_background,
20721 cx,
20722 );
20723 });
20724
20725 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
20726 .expect("Prepare rename was not started")
20727 .await
20728 .expect("Prepare rename failed");
20729
20730 let mut rename_handler =
20731 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
20732 let edit = lsp::TextEdit {
20733 range: lsp::Range {
20734 start: lsp::Position {
20735 line: 0,
20736 character: 7,
20737 },
20738 end: lsp::Position {
20739 line: 0,
20740 character: 10,
20741 },
20742 },
20743 new_text: "FooRenamed".to_string(),
20744 };
20745 Ok(Some(lsp::WorkspaceEdit::new(
20746 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
20747 )))
20748 });
20749 let rename_task = cx
20750 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
20751 .expect("Confirm rename was not started");
20752 rename_handler.next().await.unwrap();
20753 rename_task.await.expect("Confirm rename failed");
20754 cx.run_until_parked();
20755
20756 // Correct range is renamed, as `surrounding_word` is used to find it.
20757 cx.assert_editor_state(indoc! {"
20758 struct FooRenamedˇ {}
20759 "});
20760}
20761
20762#[gpui::test]
20763async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
20764 init_test(cx, |_| {});
20765 let mut cx = EditorTestContext::new(cx).await;
20766
20767 let language = Arc::new(
20768 Language::new(
20769 LanguageConfig::default(),
20770 Some(tree_sitter_html::LANGUAGE.into()),
20771 )
20772 .with_brackets_query(
20773 r#"
20774 ("<" @open "/>" @close)
20775 ("</" @open ">" @close)
20776 ("<" @open ">" @close)
20777 ("\"" @open "\"" @close)
20778 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
20779 "#,
20780 )
20781 .unwrap(),
20782 );
20783 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20784
20785 cx.set_state(indoc! {"
20786 <span>ˇ</span>
20787 "});
20788 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
20789 cx.assert_editor_state(indoc! {"
20790 <span>
20791 ˇ
20792 </span>
20793 "});
20794
20795 cx.set_state(indoc! {"
20796 <span><span></span>ˇ</span>
20797 "});
20798 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
20799 cx.assert_editor_state(indoc! {"
20800 <span><span></span>
20801 ˇ</span>
20802 "});
20803
20804 cx.set_state(indoc! {"
20805 <span>ˇ
20806 </span>
20807 "});
20808 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
20809 cx.assert_editor_state(indoc! {"
20810 <span>
20811 ˇ
20812 </span>
20813 "});
20814}
20815
20816#[gpui::test(iterations = 10)]
20817async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
20818 init_test(cx, |_| {});
20819
20820 let fs = FakeFs::new(cx.executor());
20821 fs.insert_tree(
20822 path!("/dir"),
20823 json!({
20824 "a.ts": "a",
20825 }),
20826 )
20827 .await;
20828
20829 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
20830 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20831 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20832
20833 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20834 language_registry.add(Arc::new(Language::new(
20835 LanguageConfig {
20836 name: "TypeScript".into(),
20837 matcher: LanguageMatcher {
20838 path_suffixes: vec!["ts".to_string()],
20839 ..Default::default()
20840 },
20841 ..Default::default()
20842 },
20843 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
20844 )));
20845 let mut fake_language_servers = language_registry.register_fake_lsp(
20846 "TypeScript",
20847 FakeLspAdapter {
20848 capabilities: lsp::ServerCapabilities {
20849 code_lens_provider: Some(lsp::CodeLensOptions {
20850 resolve_provider: Some(true),
20851 }),
20852 execute_command_provider: Some(lsp::ExecuteCommandOptions {
20853 commands: vec!["_the/command".to_string()],
20854 ..lsp::ExecuteCommandOptions::default()
20855 }),
20856 ..lsp::ServerCapabilities::default()
20857 },
20858 ..FakeLspAdapter::default()
20859 },
20860 );
20861
20862 let (buffer, _handle) = project
20863 .update(cx, |p, cx| {
20864 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
20865 })
20866 .await
20867 .unwrap();
20868 cx.executor().run_until_parked();
20869
20870 let fake_server = fake_language_servers.next().await.unwrap();
20871
20872 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
20873 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
20874 drop(buffer_snapshot);
20875 let actions = cx
20876 .update_window(*workspace, |_, window, cx| {
20877 project.code_actions(&buffer, anchor..anchor, window, cx)
20878 })
20879 .unwrap();
20880
20881 fake_server
20882 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
20883 Ok(Some(vec![
20884 lsp::CodeLens {
20885 range: lsp::Range::default(),
20886 command: Some(lsp::Command {
20887 title: "Code lens command".to_owned(),
20888 command: "_the/command".to_owned(),
20889 arguments: None,
20890 }),
20891 data: None,
20892 },
20893 lsp::CodeLens {
20894 range: lsp::Range::default(),
20895 command: Some(lsp::Command {
20896 title: "Command not in capabilities".to_owned(),
20897 command: "not in capabilities".to_owned(),
20898 arguments: None,
20899 }),
20900 data: None,
20901 },
20902 lsp::CodeLens {
20903 range: lsp::Range {
20904 start: lsp::Position {
20905 line: 1,
20906 character: 1,
20907 },
20908 end: lsp::Position {
20909 line: 1,
20910 character: 1,
20911 },
20912 },
20913 command: Some(lsp::Command {
20914 title: "Command not in range".to_owned(),
20915 command: "_the/command".to_owned(),
20916 arguments: None,
20917 }),
20918 data: None,
20919 },
20920 ]))
20921 })
20922 .next()
20923 .await;
20924
20925 let actions = actions.await.unwrap();
20926 assert_eq!(
20927 actions.len(),
20928 1,
20929 "Should have only one valid action for the 0..0 range"
20930 );
20931 let action = actions[0].clone();
20932 let apply = project.update(cx, |project, cx| {
20933 project.apply_code_action(buffer.clone(), action, true, cx)
20934 });
20935
20936 // Resolving the code action does not populate its edits. In absence of
20937 // edits, we must execute the given command.
20938 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
20939 |mut lens, _| async move {
20940 let lens_command = lens.command.as_mut().expect("should have a command");
20941 assert_eq!(lens_command.title, "Code lens command");
20942 lens_command.arguments = Some(vec![json!("the-argument")]);
20943 Ok(lens)
20944 },
20945 );
20946
20947 // While executing the command, the language server sends the editor
20948 // a `workspaceEdit` request.
20949 fake_server
20950 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
20951 let fake = fake_server.clone();
20952 move |params, _| {
20953 assert_eq!(params.command, "_the/command");
20954 let fake = fake.clone();
20955 async move {
20956 fake.server
20957 .request::<lsp::request::ApplyWorkspaceEdit>(
20958 lsp::ApplyWorkspaceEditParams {
20959 label: None,
20960 edit: lsp::WorkspaceEdit {
20961 changes: Some(
20962 [(
20963 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
20964 vec![lsp::TextEdit {
20965 range: lsp::Range::new(
20966 lsp::Position::new(0, 0),
20967 lsp::Position::new(0, 0),
20968 ),
20969 new_text: "X".into(),
20970 }],
20971 )]
20972 .into_iter()
20973 .collect(),
20974 ),
20975 ..Default::default()
20976 },
20977 },
20978 )
20979 .await
20980 .into_response()
20981 .unwrap();
20982 Ok(Some(json!(null)))
20983 }
20984 }
20985 })
20986 .next()
20987 .await;
20988
20989 // Applying the code lens command returns a project transaction containing the edits
20990 // sent by the language server in its `workspaceEdit` request.
20991 let transaction = apply.await.unwrap();
20992 assert!(transaction.0.contains_key(&buffer));
20993 buffer.update(cx, |buffer, cx| {
20994 assert_eq!(buffer.text(), "Xa");
20995 buffer.undo(cx);
20996 assert_eq!(buffer.text(), "a");
20997 });
20998}
20999
21000#[gpui::test]
21001async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
21002 init_test(cx, |_| {});
21003
21004 let fs = FakeFs::new(cx.executor());
21005 let main_text = r#"fn main() {
21006println!("1");
21007println!("2");
21008println!("3");
21009println!("4");
21010println!("5");
21011}"#;
21012 let lib_text = "mod foo {}";
21013 fs.insert_tree(
21014 path!("/a"),
21015 json!({
21016 "lib.rs": lib_text,
21017 "main.rs": main_text,
21018 }),
21019 )
21020 .await;
21021
21022 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21023 let (workspace, cx) =
21024 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21025 let worktree_id = workspace.update(cx, |workspace, cx| {
21026 workspace.project().update(cx, |project, cx| {
21027 project.worktrees(cx).next().unwrap().read(cx).id()
21028 })
21029 });
21030
21031 let expected_ranges = vec![
21032 Point::new(0, 0)..Point::new(0, 0),
21033 Point::new(1, 0)..Point::new(1, 1),
21034 Point::new(2, 0)..Point::new(2, 2),
21035 Point::new(3, 0)..Point::new(3, 3),
21036 ];
21037
21038 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21039 let editor_1 = workspace
21040 .update_in(cx, |workspace, window, cx| {
21041 workspace.open_path(
21042 (worktree_id, "main.rs"),
21043 Some(pane_1.downgrade()),
21044 true,
21045 window,
21046 cx,
21047 )
21048 })
21049 .unwrap()
21050 .await
21051 .downcast::<Editor>()
21052 .unwrap();
21053 pane_1.update(cx, |pane, cx| {
21054 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21055 open_editor.update(cx, |editor, cx| {
21056 assert_eq!(
21057 editor.display_text(cx),
21058 main_text,
21059 "Original main.rs text on initial open",
21060 );
21061 assert_eq!(
21062 editor
21063 .selections
21064 .all::<Point>(cx)
21065 .into_iter()
21066 .map(|s| s.range())
21067 .collect::<Vec<_>>(),
21068 vec![Point::zero()..Point::zero()],
21069 "Default selections on initial open",
21070 );
21071 })
21072 });
21073 editor_1.update_in(cx, |editor, window, cx| {
21074 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21075 s.select_ranges(expected_ranges.clone());
21076 });
21077 });
21078
21079 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
21080 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
21081 });
21082 let editor_2 = workspace
21083 .update_in(cx, |workspace, window, cx| {
21084 workspace.open_path(
21085 (worktree_id, "main.rs"),
21086 Some(pane_2.downgrade()),
21087 true,
21088 window,
21089 cx,
21090 )
21091 })
21092 .unwrap()
21093 .await
21094 .downcast::<Editor>()
21095 .unwrap();
21096 pane_2.update(cx, |pane, cx| {
21097 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21098 open_editor.update(cx, |editor, cx| {
21099 assert_eq!(
21100 editor.display_text(cx),
21101 main_text,
21102 "Original main.rs text on initial open in another panel",
21103 );
21104 assert_eq!(
21105 editor
21106 .selections
21107 .all::<Point>(cx)
21108 .into_iter()
21109 .map(|s| s.range())
21110 .collect::<Vec<_>>(),
21111 vec![Point::zero()..Point::zero()],
21112 "Default selections on initial open in another panel",
21113 );
21114 })
21115 });
21116
21117 editor_2.update_in(cx, |editor, window, cx| {
21118 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
21119 });
21120
21121 let _other_editor_1 = workspace
21122 .update_in(cx, |workspace, window, cx| {
21123 workspace.open_path(
21124 (worktree_id, "lib.rs"),
21125 Some(pane_1.downgrade()),
21126 true,
21127 window,
21128 cx,
21129 )
21130 })
21131 .unwrap()
21132 .await
21133 .downcast::<Editor>()
21134 .unwrap();
21135 pane_1
21136 .update_in(cx, |pane, window, cx| {
21137 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
21138 })
21139 .await
21140 .unwrap();
21141 drop(editor_1);
21142 pane_1.update(cx, |pane, cx| {
21143 pane.active_item()
21144 .unwrap()
21145 .downcast::<Editor>()
21146 .unwrap()
21147 .update(cx, |editor, cx| {
21148 assert_eq!(
21149 editor.display_text(cx),
21150 lib_text,
21151 "Other file should be open and active",
21152 );
21153 });
21154 assert_eq!(pane.items().count(), 1, "No other editors should be open");
21155 });
21156
21157 let _other_editor_2 = workspace
21158 .update_in(cx, |workspace, window, cx| {
21159 workspace.open_path(
21160 (worktree_id, "lib.rs"),
21161 Some(pane_2.downgrade()),
21162 true,
21163 window,
21164 cx,
21165 )
21166 })
21167 .unwrap()
21168 .await
21169 .downcast::<Editor>()
21170 .unwrap();
21171 pane_2
21172 .update_in(cx, |pane, window, cx| {
21173 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
21174 })
21175 .await
21176 .unwrap();
21177 drop(editor_2);
21178 pane_2.update(cx, |pane, cx| {
21179 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21180 open_editor.update(cx, |editor, cx| {
21181 assert_eq!(
21182 editor.display_text(cx),
21183 lib_text,
21184 "Other file should be open and active in another panel too",
21185 );
21186 });
21187 assert_eq!(
21188 pane.items().count(),
21189 1,
21190 "No other editors should be open in another pane",
21191 );
21192 });
21193
21194 let _editor_1_reopened = workspace
21195 .update_in(cx, |workspace, window, cx| {
21196 workspace.open_path(
21197 (worktree_id, "main.rs"),
21198 Some(pane_1.downgrade()),
21199 true,
21200 window,
21201 cx,
21202 )
21203 })
21204 .unwrap()
21205 .await
21206 .downcast::<Editor>()
21207 .unwrap();
21208 let _editor_2_reopened = workspace
21209 .update_in(cx, |workspace, window, cx| {
21210 workspace.open_path(
21211 (worktree_id, "main.rs"),
21212 Some(pane_2.downgrade()),
21213 true,
21214 window,
21215 cx,
21216 )
21217 })
21218 .unwrap()
21219 .await
21220 .downcast::<Editor>()
21221 .unwrap();
21222 pane_1.update(cx, |pane, cx| {
21223 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21224 open_editor.update(cx, |editor, cx| {
21225 assert_eq!(
21226 editor.display_text(cx),
21227 main_text,
21228 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
21229 );
21230 assert_eq!(
21231 editor
21232 .selections
21233 .all::<Point>(cx)
21234 .into_iter()
21235 .map(|s| s.range())
21236 .collect::<Vec<_>>(),
21237 expected_ranges,
21238 "Previous editor in the 1st panel had selections and should get them restored on reopen",
21239 );
21240 })
21241 });
21242 pane_2.update(cx, |pane, cx| {
21243 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21244 open_editor.update(cx, |editor, cx| {
21245 assert_eq!(
21246 editor.display_text(cx),
21247 r#"fn main() {
21248⋯rintln!("1");
21249⋯intln!("2");
21250⋯ntln!("3");
21251println!("4");
21252println!("5");
21253}"#,
21254 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
21255 );
21256 assert_eq!(
21257 editor
21258 .selections
21259 .all::<Point>(cx)
21260 .into_iter()
21261 .map(|s| s.range())
21262 .collect::<Vec<_>>(),
21263 vec![Point::zero()..Point::zero()],
21264 "Previous editor in the 2nd pane had no selections changed hence should restore none",
21265 );
21266 })
21267 });
21268}
21269
21270#[gpui::test]
21271async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
21272 init_test(cx, |_| {});
21273
21274 let fs = FakeFs::new(cx.executor());
21275 let main_text = r#"fn main() {
21276println!("1");
21277println!("2");
21278println!("3");
21279println!("4");
21280println!("5");
21281}"#;
21282 let lib_text = "mod foo {}";
21283 fs.insert_tree(
21284 path!("/a"),
21285 json!({
21286 "lib.rs": lib_text,
21287 "main.rs": main_text,
21288 }),
21289 )
21290 .await;
21291
21292 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21293 let (workspace, cx) =
21294 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21295 let worktree_id = workspace.update(cx, |workspace, cx| {
21296 workspace.project().update(cx, |project, cx| {
21297 project.worktrees(cx).next().unwrap().read(cx).id()
21298 })
21299 });
21300
21301 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21302 let editor = workspace
21303 .update_in(cx, |workspace, window, cx| {
21304 workspace.open_path(
21305 (worktree_id, "main.rs"),
21306 Some(pane.downgrade()),
21307 true,
21308 window,
21309 cx,
21310 )
21311 })
21312 .unwrap()
21313 .await
21314 .downcast::<Editor>()
21315 .unwrap();
21316 pane.update(cx, |pane, cx| {
21317 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21318 open_editor.update(cx, |editor, cx| {
21319 assert_eq!(
21320 editor.display_text(cx),
21321 main_text,
21322 "Original main.rs text on initial open",
21323 );
21324 })
21325 });
21326 editor.update_in(cx, |editor, window, cx| {
21327 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
21328 });
21329
21330 cx.update_global(|store: &mut SettingsStore, cx| {
21331 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21332 s.restore_on_file_reopen = Some(false);
21333 });
21334 });
21335 editor.update_in(cx, |editor, window, cx| {
21336 editor.fold_ranges(
21337 vec![
21338 Point::new(1, 0)..Point::new(1, 1),
21339 Point::new(2, 0)..Point::new(2, 2),
21340 Point::new(3, 0)..Point::new(3, 3),
21341 ],
21342 false,
21343 window,
21344 cx,
21345 );
21346 });
21347 pane.update_in(cx, |pane, window, cx| {
21348 pane.close_all_items(&CloseAllItems::default(), window, cx)
21349 })
21350 .await
21351 .unwrap();
21352 pane.update(cx, |pane, _| {
21353 assert!(pane.active_item().is_none());
21354 });
21355 cx.update_global(|store: &mut SettingsStore, cx| {
21356 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21357 s.restore_on_file_reopen = Some(true);
21358 });
21359 });
21360
21361 let _editor_reopened = workspace
21362 .update_in(cx, |workspace, window, cx| {
21363 workspace.open_path(
21364 (worktree_id, "main.rs"),
21365 Some(pane.downgrade()),
21366 true,
21367 window,
21368 cx,
21369 )
21370 })
21371 .unwrap()
21372 .await
21373 .downcast::<Editor>()
21374 .unwrap();
21375 pane.update(cx, |pane, cx| {
21376 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21377 open_editor.update(cx, |editor, cx| {
21378 assert_eq!(
21379 editor.display_text(cx),
21380 main_text,
21381 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
21382 );
21383 })
21384 });
21385}
21386
21387#[gpui::test]
21388async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
21389 struct EmptyModalView {
21390 focus_handle: gpui::FocusHandle,
21391 }
21392 impl EventEmitter<DismissEvent> for EmptyModalView {}
21393 impl Render for EmptyModalView {
21394 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
21395 div()
21396 }
21397 }
21398 impl Focusable for EmptyModalView {
21399 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
21400 self.focus_handle.clone()
21401 }
21402 }
21403 impl workspace::ModalView for EmptyModalView {}
21404 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
21405 EmptyModalView {
21406 focus_handle: cx.focus_handle(),
21407 }
21408 }
21409
21410 init_test(cx, |_| {});
21411
21412 let fs = FakeFs::new(cx.executor());
21413 let project = Project::test(fs, [], cx).await;
21414 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21415 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
21416 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21417 let editor = cx.new_window_entity(|window, cx| {
21418 Editor::new(
21419 EditorMode::full(),
21420 buffer,
21421 Some(project.clone()),
21422 window,
21423 cx,
21424 )
21425 });
21426 workspace
21427 .update(cx, |workspace, window, cx| {
21428 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
21429 })
21430 .unwrap();
21431 editor.update_in(cx, |editor, window, cx| {
21432 editor.open_context_menu(&OpenContextMenu, window, cx);
21433 assert!(editor.mouse_context_menu.is_some());
21434 });
21435 workspace
21436 .update(cx, |workspace, window, cx| {
21437 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
21438 })
21439 .unwrap();
21440 cx.read(|cx| {
21441 assert!(editor.read(cx).mouse_context_menu.is_none());
21442 });
21443}
21444
21445#[gpui::test]
21446async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
21447 init_test(cx, |_| {});
21448
21449 let fs = FakeFs::new(cx.executor());
21450 fs.insert_file(path!("/file.html"), Default::default())
21451 .await;
21452
21453 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
21454
21455 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21456 let html_language = Arc::new(Language::new(
21457 LanguageConfig {
21458 name: "HTML".into(),
21459 matcher: LanguageMatcher {
21460 path_suffixes: vec!["html".to_string()],
21461 ..LanguageMatcher::default()
21462 },
21463 brackets: BracketPairConfig {
21464 pairs: vec![BracketPair {
21465 start: "<".into(),
21466 end: ">".into(),
21467 close: true,
21468 ..Default::default()
21469 }],
21470 ..Default::default()
21471 },
21472 ..Default::default()
21473 },
21474 Some(tree_sitter_html::LANGUAGE.into()),
21475 ));
21476 language_registry.add(html_language);
21477 let mut fake_servers = language_registry.register_fake_lsp(
21478 "HTML",
21479 FakeLspAdapter {
21480 capabilities: lsp::ServerCapabilities {
21481 completion_provider: Some(lsp::CompletionOptions {
21482 resolve_provider: Some(true),
21483 ..Default::default()
21484 }),
21485 ..Default::default()
21486 },
21487 ..Default::default()
21488 },
21489 );
21490
21491 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21492 let cx = &mut VisualTestContext::from_window(*workspace, cx);
21493
21494 let worktree_id = workspace
21495 .update(cx, |workspace, _window, cx| {
21496 workspace.project().update(cx, |project, cx| {
21497 project.worktrees(cx).next().unwrap().read(cx).id()
21498 })
21499 })
21500 .unwrap();
21501 project
21502 .update(cx, |project, cx| {
21503 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
21504 })
21505 .await
21506 .unwrap();
21507 let editor = workspace
21508 .update(cx, |workspace, window, cx| {
21509 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
21510 })
21511 .unwrap()
21512 .await
21513 .unwrap()
21514 .downcast::<Editor>()
21515 .unwrap();
21516
21517 let fake_server = fake_servers.next().await.unwrap();
21518 editor.update_in(cx, |editor, window, cx| {
21519 editor.set_text("<ad></ad>", window, cx);
21520 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
21521 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
21522 });
21523 let Some((buffer, _)) = editor
21524 .buffer
21525 .read(cx)
21526 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
21527 else {
21528 panic!("Failed to get buffer for selection position");
21529 };
21530 let buffer = buffer.read(cx);
21531 let buffer_id = buffer.remote_id();
21532 let opening_range =
21533 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
21534 let closing_range =
21535 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
21536 let mut linked_ranges = HashMap::default();
21537 linked_ranges.insert(
21538 buffer_id,
21539 vec![(opening_range.clone(), vec![closing_range.clone()])],
21540 );
21541 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
21542 });
21543 let mut completion_handle =
21544 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
21545 Ok(Some(lsp::CompletionResponse::Array(vec![
21546 lsp::CompletionItem {
21547 label: "head".to_string(),
21548 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
21549 lsp::InsertReplaceEdit {
21550 new_text: "head".to_string(),
21551 insert: lsp::Range::new(
21552 lsp::Position::new(0, 1),
21553 lsp::Position::new(0, 3),
21554 ),
21555 replace: lsp::Range::new(
21556 lsp::Position::new(0, 1),
21557 lsp::Position::new(0, 3),
21558 ),
21559 },
21560 )),
21561 ..Default::default()
21562 },
21563 ])))
21564 });
21565 editor.update_in(cx, |editor, window, cx| {
21566 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
21567 });
21568 cx.run_until_parked();
21569 completion_handle.next().await.unwrap();
21570 editor.update(cx, |editor, _| {
21571 assert!(
21572 editor.context_menu_visible(),
21573 "Completion menu should be visible"
21574 );
21575 });
21576 editor.update_in(cx, |editor, window, cx| {
21577 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
21578 });
21579 cx.executor().run_until_parked();
21580 editor.update(cx, |editor, cx| {
21581 assert_eq!(editor.text(cx), "<head></head>");
21582 });
21583}
21584
21585#[gpui::test]
21586async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
21587 init_test(cx, |_| {});
21588
21589 let fs = FakeFs::new(cx.executor());
21590 fs.insert_tree(
21591 path!("/root"),
21592 json!({
21593 "a": {
21594 "main.rs": "fn main() {}",
21595 },
21596 "foo": {
21597 "bar": {
21598 "external_file.rs": "pub mod external {}",
21599 }
21600 }
21601 }),
21602 )
21603 .await;
21604
21605 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
21606 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21607 language_registry.add(rust_lang());
21608 let _fake_servers = language_registry.register_fake_lsp(
21609 "Rust",
21610 FakeLspAdapter {
21611 ..FakeLspAdapter::default()
21612 },
21613 );
21614 let (workspace, cx) =
21615 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21616 let worktree_id = workspace.update(cx, |workspace, cx| {
21617 workspace.project().update(cx, |project, cx| {
21618 project.worktrees(cx).next().unwrap().read(cx).id()
21619 })
21620 });
21621
21622 let assert_language_servers_count =
21623 |expected: usize, context: &str, cx: &mut VisualTestContext| {
21624 project.update(cx, |project, cx| {
21625 let current = project
21626 .lsp_store()
21627 .read(cx)
21628 .as_local()
21629 .unwrap()
21630 .language_servers
21631 .len();
21632 assert_eq!(expected, current, "{context}");
21633 });
21634 };
21635
21636 assert_language_servers_count(
21637 0,
21638 "No servers should be running before any file is open",
21639 cx,
21640 );
21641 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21642 let main_editor = workspace
21643 .update_in(cx, |workspace, window, cx| {
21644 workspace.open_path(
21645 (worktree_id, "main.rs"),
21646 Some(pane.downgrade()),
21647 true,
21648 window,
21649 cx,
21650 )
21651 })
21652 .unwrap()
21653 .await
21654 .downcast::<Editor>()
21655 .unwrap();
21656 pane.update(cx, |pane, cx| {
21657 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21658 open_editor.update(cx, |editor, cx| {
21659 assert_eq!(
21660 editor.display_text(cx),
21661 "fn main() {}",
21662 "Original main.rs text on initial open",
21663 );
21664 });
21665 assert_eq!(open_editor, main_editor);
21666 });
21667 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
21668
21669 let external_editor = workspace
21670 .update_in(cx, |workspace, window, cx| {
21671 workspace.open_abs_path(
21672 PathBuf::from("/root/foo/bar/external_file.rs"),
21673 OpenOptions::default(),
21674 window,
21675 cx,
21676 )
21677 })
21678 .await
21679 .expect("opening external file")
21680 .downcast::<Editor>()
21681 .expect("downcasted external file's open element to editor");
21682 pane.update(cx, |pane, cx| {
21683 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21684 open_editor.update(cx, |editor, cx| {
21685 assert_eq!(
21686 editor.display_text(cx),
21687 "pub mod external {}",
21688 "External file is open now",
21689 );
21690 });
21691 assert_eq!(open_editor, external_editor);
21692 });
21693 assert_language_servers_count(
21694 1,
21695 "Second, external, *.rs file should join the existing server",
21696 cx,
21697 );
21698
21699 pane.update_in(cx, |pane, window, cx| {
21700 pane.close_active_item(&CloseActiveItem::default(), window, cx)
21701 })
21702 .await
21703 .unwrap();
21704 pane.update_in(cx, |pane, window, cx| {
21705 pane.navigate_backward(window, cx);
21706 });
21707 cx.run_until_parked();
21708 pane.update(cx, |pane, cx| {
21709 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21710 open_editor.update(cx, |editor, cx| {
21711 assert_eq!(
21712 editor.display_text(cx),
21713 "pub mod external {}",
21714 "External file is open now",
21715 );
21716 });
21717 });
21718 assert_language_servers_count(
21719 1,
21720 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
21721 cx,
21722 );
21723
21724 cx.update(|_, cx| {
21725 workspace::reload(&workspace::Reload::default(), cx);
21726 });
21727 assert_language_servers_count(
21728 1,
21729 "After reloading the worktree with local and external files opened, only one project should be started",
21730 cx,
21731 );
21732}
21733
21734#[gpui::test]
21735async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
21736 init_test(cx, |_| {});
21737
21738 let mut cx = EditorTestContext::new(cx).await;
21739 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
21740 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21741
21742 // test cursor move to start of each line on tab
21743 // for `if`, `elif`, `else`, `while`, `with` and `for`
21744 cx.set_state(indoc! {"
21745 def main():
21746 ˇ for item in items:
21747 ˇ while item.active:
21748 ˇ if item.value > 10:
21749 ˇ continue
21750 ˇ elif item.value < 0:
21751 ˇ break
21752 ˇ else:
21753 ˇ with item.context() as ctx:
21754 ˇ yield count
21755 ˇ else:
21756 ˇ log('while else')
21757 ˇ else:
21758 ˇ log('for else')
21759 "});
21760 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21761 cx.assert_editor_state(indoc! {"
21762 def main():
21763 ˇfor item in items:
21764 ˇwhile item.active:
21765 ˇif item.value > 10:
21766 ˇcontinue
21767 ˇelif item.value < 0:
21768 ˇbreak
21769 ˇelse:
21770 ˇwith item.context() as ctx:
21771 ˇyield count
21772 ˇelse:
21773 ˇlog('while else')
21774 ˇelse:
21775 ˇlog('for else')
21776 "});
21777 // test relative indent is preserved when tab
21778 // for `if`, `elif`, `else`, `while`, `with` and `for`
21779 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21780 cx.assert_editor_state(indoc! {"
21781 def main():
21782 ˇfor item in items:
21783 ˇwhile item.active:
21784 ˇif item.value > 10:
21785 ˇcontinue
21786 ˇelif item.value < 0:
21787 ˇbreak
21788 ˇelse:
21789 ˇwith item.context() as ctx:
21790 ˇyield count
21791 ˇelse:
21792 ˇlog('while else')
21793 ˇelse:
21794 ˇlog('for else')
21795 "});
21796
21797 // test cursor move to start of each line on tab
21798 // for `try`, `except`, `else`, `finally`, `match` and `def`
21799 cx.set_state(indoc! {"
21800 def main():
21801 ˇ try:
21802 ˇ fetch()
21803 ˇ except ValueError:
21804 ˇ handle_error()
21805 ˇ else:
21806 ˇ match value:
21807 ˇ case _:
21808 ˇ finally:
21809 ˇ def status():
21810 ˇ return 0
21811 "});
21812 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21813 cx.assert_editor_state(indoc! {"
21814 def main():
21815 ˇtry:
21816 ˇfetch()
21817 ˇexcept ValueError:
21818 ˇhandle_error()
21819 ˇelse:
21820 ˇmatch value:
21821 ˇcase _:
21822 ˇfinally:
21823 ˇdef status():
21824 ˇreturn 0
21825 "});
21826 // test relative indent is preserved when tab
21827 // for `try`, `except`, `else`, `finally`, `match` and `def`
21828 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21829 cx.assert_editor_state(indoc! {"
21830 def main():
21831 ˇtry:
21832 ˇfetch()
21833 ˇexcept ValueError:
21834 ˇhandle_error()
21835 ˇelse:
21836 ˇmatch value:
21837 ˇcase _:
21838 ˇfinally:
21839 ˇdef status():
21840 ˇreturn 0
21841 "});
21842}
21843
21844#[gpui::test]
21845async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
21846 init_test(cx, |_| {});
21847
21848 let mut cx = EditorTestContext::new(cx).await;
21849 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
21850 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21851
21852 // test `else` auto outdents when typed inside `if` block
21853 cx.set_state(indoc! {"
21854 def main():
21855 if i == 2:
21856 return
21857 ˇ
21858 "});
21859 cx.update_editor(|editor, window, cx| {
21860 editor.handle_input("else:", window, cx);
21861 });
21862 cx.assert_editor_state(indoc! {"
21863 def main():
21864 if i == 2:
21865 return
21866 else:ˇ
21867 "});
21868
21869 // test `except` auto outdents when typed inside `try` block
21870 cx.set_state(indoc! {"
21871 def main():
21872 try:
21873 i = 2
21874 ˇ
21875 "});
21876 cx.update_editor(|editor, window, cx| {
21877 editor.handle_input("except:", window, cx);
21878 });
21879 cx.assert_editor_state(indoc! {"
21880 def main():
21881 try:
21882 i = 2
21883 except:ˇ
21884 "});
21885
21886 // test `else` auto outdents when typed inside `except` block
21887 cx.set_state(indoc! {"
21888 def main():
21889 try:
21890 i = 2
21891 except:
21892 j = 2
21893 ˇ
21894 "});
21895 cx.update_editor(|editor, window, cx| {
21896 editor.handle_input("else:", window, cx);
21897 });
21898 cx.assert_editor_state(indoc! {"
21899 def main():
21900 try:
21901 i = 2
21902 except:
21903 j = 2
21904 else:ˇ
21905 "});
21906
21907 // test `finally` auto outdents when typed inside `else` block
21908 cx.set_state(indoc! {"
21909 def main():
21910 try:
21911 i = 2
21912 except:
21913 j = 2
21914 else:
21915 k = 2
21916 ˇ
21917 "});
21918 cx.update_editor(|editor, window, cx| {
21919 editor.handle_input("finally:", window, cx);
21920 });
21921 cx.assert_editor_state(indoc! {"
21922 def main():
21923 try:
21924 i = 2
21925 except:
21926 j = 2
21927 else:
21928 k = 2
21929 finally:ˇ
21930 "});
21931
21932 // test `else` does not outdents when typed inside `except` block right after for block
21933 cx.set_state(indoc! {"
21934 def main():
21935 try:
21936 i = 2
21937 except:
21938 for i in range(n):
21939 pass
21940 ˇ
21941 "});
21942 cx.update_editor(|editor, window, cx| {
21943 editor.handle_input("else:", window, cx);
21944 });
21945 cx.assert_editor_state(indoc! {"
21946 def main():
21947 try:
21948 i = 2
21949 except:
21950 for i in range(n):
21951 pass
21952 else:ˇ
21953 "});
21954
21955 // test `finally` auto outdents when typed inside `else` block right after for block
21956 cx.set_state(indoc! {"
21957 def main():
21958 try:
21959 i = 2
21960 except:
21961 j = 2
21962 else:
21963 for i in range(n):
21964 pass
21965 ˇ
21966 "});
21967 cx.update_editor(|editor, window, cx| {
21968 editor.handle_input("finally:", window, cx);
21969 });
21970 cx.assert_editor_state(indoc! {"
21971 def main():
21972 try:
21973 i = 2
21974 except:
21975 j = 2
21976 else:
21977 for i in range(n):
21978 pass
21979 finally:ˇ
21980 "});
21981
21982 // test `except` outdents to inner "try" block
21983 cx.set_state(indoc! {"
21984 def main():
21985 try:
21986 i = 2
21987 if i == 2:
21988 try:
21989 i = 3
21990 ˇ
21991 "});
21992 cx.update_editor(|editor, window, cx| {
21993 editor.handle_input("except:", window, cx);
21994 });
21995 cx.assert_editor_state(indoc! {"
21996 def main():
21997 try:
21998 i = 2
21999 if i == 2:
22000 try:
22001 i = 3
22002 except:ˇ
22003 "});
22004
22005 // test `except` outdents to outer "try" block
22006 cx.set_state(indoc! {"
22007 def main():
22008 try:
22009 i = 2
22010 if i == 2:
22011 try:
22012 i = 3
22013 ˇ
22014 "});
22015 cx.update_editor(|editor, window, cx| {
22016 editor.handle_input("except:", window, cx);
22017 });
22018 cx.assert_editor_state(indoc! {"
22019 def main():
22020 try:
22021 i = 2
22022 if i == 2:
22023 try:
22024 i = 3
22025 except:ˇ
22026 "});
22027
22028 // test `else` stays at correct indent when typed after `for` block
22029 cx.set_state(indoc! {"
22030 def main():
22031 for i in range(10):
22032 if i == 3:
22033 break
22034 ˇ
22035 "});
22036 cx.update_editor(|editor, window, cx| {
22037 editor.handle_input("else:", window, cx);
22038 });
22039 cx.assert_editor_state(indoc! {"
22040 def main():
22041 for i in range(10):
22042 if i == 3:
22043 break
22044 else:ˇ
22045 "});
22046
22047 // test does not outdent on typing after line with square brackets
22048 cx.set_state(indoc! {"
22049 def f() -> list[str]:
22050 ˇ
22051 "});
22052 cx.update_editor(|editor, window, cx| {
22053 editor.handle_input("a", window, cx);
22054 });
22055 cx.assert_editor_state(indoc! {"
22056 def f() -> list[str]:
22057 aˇ
22058 "});
22059}
22060
22061#[gpui::test]
22062async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
22063 init_test(cx, |_| {});
22064 update_test_language_settings(cx, |settings| {
22065 settings.defaults.extend_comment_on_newline = Some(false);
22066 });
22067 let mut cx = EditorTestContext::new(cx).await;
22068 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22069 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22070
22071 // test correct indent after newline on comment
22072 cx.set_state(indoc! {"
22073 # COMMENT:ˇ
22074 "});
22075 cx.update_editor(|editor, window, cx| {
22076 editor.newline(&Newline, window, cx);
22077 });
22078 cx.assert_editor_state(indoc! {"
22079 # COMMENT:
22080 ˇ
22081 "});
22082
22083 // test correct indent after newline in brackets
22084 cx.set_state(indoc! {"
22085 {ˇ}
22086 "});
22087 cx.update_editor(|editor, window, cx| {
22088 editor.newline(&Newline, window, cx);
22089 });
22090 cx.run_until_parked();
22091 cx.assert_editor_state(indoc! {"
22092 {
22093 ˇ
22094 }
22095 "});
22096
22097 cx.set_state(indoc! {"
22098 (ˇ)
22099 "});
22100 cx.update_editor(|editor, window, cx| {
22101 editor.newline(&Newline, window, cx);
22102 });
22103 cx.run_until_parked();
22104 cx.assert_editor_state(indoc! {"
22105 (
22106 ˇ
22107 )
22108 "});
22109
22110 // do not indent after empty lists or dictionaries
22111 cx.set_state(indoc! {"
22112 a = []ˇ
22113 "});
22114 cx.update_editor(|editor, window, cx| {
22115 editor.newline(&Newline, window, cx);
22116 });
22117 cx.run_until_parked();
22118 cx.assert_editor_state(indoc! {"
22119 a = []
22120 ˇ
22121 "});
22122}
22123
22124fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
22125 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
22126 point..point
22127}
22128
22129#[track_caller]
22130fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
22131 let (text, ranges) = marked_text_ranges(marked_text, true);
22132 assert_eq!(editor.text(cx), text);
22133 assert_eq!(
22134 editor.selections.ranges(cx),
22135 ranges,
22136 "Assert selections are {}",
22137 marked_text
22138 );
22139}
22140
22141pub fn handle_signature_help_request(
22142 cx: &mut EditorLspTestContext,
22143 mocked_response: lsp::SignatureHelp,
22144) -> impl Future<Output = ()> + use<> {
22145 let mut request =
22146 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
22147 let mocked_response = mocked_response.clone();
22148 async move { Ok(Some(mocked_response)) }
22149 });
22150
22151 async move {
22152 request.next().await;
22153 }
22154}
22155
22156#[track_caller]
22157pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
22158 cx.update_editor(|editor, _, _| {
22159 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
22160 let entries = menu.entries.borrow();
22161 let entries = entries
22162 .iter()
22163 .map(|entry| entry.string.as_str())
22164 .collect::<Vec<_>>();
22165 assert_eq!(entries, expected);
22166 } else {
22167 panic!("Expected completions menu");
22168 }
22169 });
22170}
22171
22172/// Handle completion request passing a marked string specifying where the completion
22173/// should be triggered from using '|' character, what range should be replaced, and what completions
22174/// should be returned using '<' and '>' to delimit the range.
22175///
22176/// Also see `handle_completion_request_with_insert_and_replace`.
22177#[track_caller]
22178pub fn handle_completion_request(
22179 marked_string: &str,
22180 completions: Vec<&'static str>,
22181 is_incomplete: bool,
22182 counter: Arc<AtomicUsize>,
22183 cx: &mut EditorLspTestContext,
22184) -> impl Future<Output = ()> {
22185 let complete_from_marker: TextRangeMarker = '|'.into();
22186 let replace_range_marker: TextRangeMarker = ('<', '>').into();
22187 let (_, mut marked_ranges) = marked_text_ranges_by(
22188 marked_string,
22189 vec![complete_from_marker.clone(), replace_range_marker.clone()],
22190 );
22191
22192 let complete_from_position =
22193 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
22194 let replace_range =
22195 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
22196
22197 let mut request =
22198 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
22199 let completions = completions.clone();
22200 counter.fetch_add(1, atomic::Ordering::Release);
22201 async move {
22202 assert_eq!(params.text_document_position.text_document.uri, url.clone());
22203 assert_eq!(
22204 params.text_document_position.position,
22205 complete_from_position
22206 );
22207 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
22208 is_incomplete: is_incomplete,
22209 item_defaults: None,
22210 items: completions
22211 .iter()
22212 .map(|completion_text| lsp::CompletionItem {
22213 label: completion_text.to_string(),
22214 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
22215 range: replace_range,
22216 new_text: completion_text.to_string(),
22217 })),
22218 ..Default::default()
22219 })
22220 .collect(),
22221 })))
22222 }
22223 });
22224
22225 async move {
22226 request.next().await;
22227 }
22228}
22229
22230/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
22231/// given instead, which also contains an `insert` range.
22232///
22233/// This function uses markers to define ranges:
22234/// - `|` marks the cursor position
22235/// - `<>` marks the replace range
22236/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
22237pub fn handle_completion_request_with_insert_and_replace(
22238 cx: &mut EditorLspTestContext,
22239 marked_string: &str,
22240 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
22241 counter: Arc<AtomicUsize>,
22242) -> impl Future<Output = ()> {
22243 let complete_from_marker: TextRangeMarker = '|'.into();
22244 let replace_range_marker: TextRangeMarker = ('<', '>').into();
22245 let insert_range_marker: TextRangeMarker = ('{', '}').into();
22246
22247 let (_, mut marked_ranges) = marked_text_ranges_by(
22248 marked_string,
22249 vec![
22250 complete_from_marker.clone(),
22251 replace_range_marker.clone(),
22252 insert_range_marker.clone(),
22253 ],
22254 );
22255
22256 let complete_from_position =
22257 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
22258 let replace_range =
22259 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
22260
22261 let insert_range = match marked_ranges.remove(&insert_range_marker) {
22262 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
22263 _ => lsp::Range {
22264 start: replace_range.start,
22265 end: complete_from_position,
22266 },
22267 };
22268
22269 let mut request =
22270 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
22271 let completions = completions.clone();
22272 counter.fetch_add(1, atomic::Ordering::Release);
22273 async move {
22274 assert_eq!(params.text_document_position.text_document.uri, url.clone());
22275 assert_eq!(
22276 params.text_document_position.position, complete_from_position,
22277 "marker `|` position doesn't match",
22278 );
22279 Ok(Some(lsp::CompletionResponse::Array(
22280 completions
22281 .iter()
22282 .map(|(label, new_text)| lsp::CompletionItem {
22283 label: label.to_string(),
22284 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
22285 lsp::InsertReplaceEdit {
22286 insert: insert_range,
22287 replace: replace_range,
22288 new_text: new_text.to_string(),
22289 },
22290 )),
22291 ..Default::default()
22292 })
22293 .collect(),
22294 )))
22295 }
22296 });
22297
22298 async move {
22299 request.next().await;
22300 }
22301}
22302
22303fn handle_resolve_completion_request(
22304 cx: &mut EditorLspTestContext,
22305 edits: Option<Vec<(&'static str, &'static str)>>,
22306) -> impl Future<Output = ()> {
22307 let edits = edits.map(|edits| {
22308 edits
22309 .iter()
22310 .map(|(marked_string, new_text)| {
22311 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
22312 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
22313 lsp::TextEdit::new(replace_range, new_text.to_string())
22314 })
22315 .collect::<Vec<_>>()
22316 });
22317
22318 let mut request =
22319 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
22320 let edits = edits.clone();
22321 async move {
22322 Ok(lsp::CompletionItem {
22323 additional_text_edits: edits,
22324 ..Default::default()
22325 })
22326 }
22327 });
22328
22329 async move {
22330 request.next().await;
22331 }
22332}
22333
22334pub(crate) fn update_test_language_settings(
22335 cx: &mut TestAppContext,
22336 f: impl Fn(&mut AllLanguageSettingsContent),
22337) {
22338 cx.update(|cx| {
22339 SettingsStore::update_global(cx, |store, cx| {
22340 store.update_user_settings::<AllLanguageSettings>(cx, f);
22341 });
22342 });
22343}
22344
22345pub(crate) fn update_test_project_settings(
22346 cx: &mut TestAppContext,
22347 f: impl Fn(&mut ProjectSettings),
22348) {
22349 cx.update(|cx| {
22350 SettingsStore::update_global(cx, |store, cx| {
22351 store.update_user_settings::<ProjectSettings>(cx, f);
22352 });
22353 });
22354}
22355
22356pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
22357 cx.update(|cx| {
22358 assets::Assets.load_test_fonts(cx);
22359 let store = SettingsStore::test(cx);
22360 cx.set_global(store);
22361 theme::init(theme::LoadThemes::JustBase, cx);
22362 release_channel::init(SemanticVersion::default(), cx);
22363 client::init_settings(cx);
22364 language::init(cx);
22365 Project::init_settings(cx);
22366 workspace::init_settings(cx);
22367 crate::init(cx);
22368 });
22369
22370 update_test_language_settings(cx, f);
22371}
22372
22373#[track_caller]
22374fn assert_hunk_revert(
22375 not_reverted_text_with_selections: &str,
22376 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
22377 expected_reverted_text_with_selections: &str,
22378 base_text: &str,
22379 cx: &mut EditorLspTestContext,
22380) {
22381 cx.set_state(not_reverted_text_with_selections);
22382 cx.set_head_text(base_text);
22383 cx.executor().run_until_parked();
22384
22385 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
22386 let snapshot = editor.snapshot(window, cx);
22387 let reverted_hunk_statuses = snapshot
22388 .buffer_snapshot
22389 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
22390 .map(|hunk| hunk.status().kind)
22391 .collect::<Vec<_>>();
22392
22393 editor.git_restore(&Default::default(), window, cx);
22394 reverted_hunk_statuses
22395 });
22396 cx.executor().run_until_parked();
22397 cx.assert_editor_state(expected_reverted_text_with_selections);
22398 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
22399}
22400
22401#[gpui::test(iterations = 10)]
22402async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
22403 init_test(cx, |_| {});
22404
22405 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
22406 let counter = diagnostic_requests.clone();
22407
22408 let fs = FakeFs::new(cx.executor());
22409 fs.insert_tree(
22410 path!("/a"),
22411 json!({
22412 "first.rs": "fn main() { let a = 5; }",
22413 "second.rs": "// Test file",
22414 }),
22415 )
22416 .await;
22417
22418 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22419 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22420 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22421
22422 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22423 language_registry.add(rust_lang());
22424 let mut fake_servers = language_registry.register_fake_lsp(
22425 "Rust",
22426 FakeLspAdapter {
22427 capabilities: lsp::ServerCapabilities {
22428 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
22429 lsp::DiagnosticOptions {
22430 identifier: None,
22431 inter_file_dependencies: true,
22432 workspace_diagnostics: true,
22433 work_done_progress_options: Default::default(),
22434 },
22435 )),
22436 ..Default::default()
22437 },
22438 ..Default::default()
22439 },
22440 );
22441
22442 let editor = workspace
22443 .update(cx, |workspace, window, cx| {
22444 workspace.open_abs_path(
22445 PathBuf::from(path!("/a/first.rs")),
22446 OpenOptions::default(),
22447 window,
22448 cx,
22449 )
22450 })
22451 .unwrap()
22452 .await
22453 .unwrap()
22454 .downcast::<Editor>()
22455 .unwrap();
22456 let fake_server = fake_servers.next().await.unwrap();
22457 let server_id = fake_server.server.server_id();
22458 let mut first_request = fake_server
22459 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
22460 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
22461 let result_id = Some(new_result_id.to_string());
22462 assert_eq!(
22463 params.text_document.uri,
22464 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
22465 );
22466 async move {
22467 Ok(lsp::DocumentDiagnosticReportResult::Report(
22468 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
22469 related_documents: None,
22470 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
22471 items: Vec::new(),
22472 result_id,
22473 },
22474 }),
22475 ))
22476 }
22477 });
22478
22479 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
22480 project.update(cx, |project, cx| {
22481 let buffer_id = editor
22482 .read(cx)
22483 .buffer()
22484 .read(cx)
22485 .as_singleton()
22486 .expect("created a singleton buffer")
22487 .read(cx)
22488 .remote_id();
22489 let buffer_result_id = project
22490 .lsp_store()
22491 .read(cx)
22492 .result_id(server_id, buffer_id, cx);
22493 assert_eq!(expected, buffer_result_id);
22494 });
22495 };
22496
22497 ensure_result_id(None, cx);
22498 cx.executor().advance_clock(Duration::from_millis(60));
22499 cx.executor().run_until_parked();
22500 assert_eq!(
22501 diagnostic_requests.load(atomic::Ordering::Acquire),
22502 1,
22503 "Opening file should trigger diagnostic request"
22504 );
22505 first_request
22506 .next()
22507 .await
22508 .expect("should have sent the first diagnostics pull request");
22509 ensure_result_id(Some("1".to_string()), cx);
22510
22511 // Editing should trigger diagnostics
22512 editor.update_in(cx, |editor, window, cx| {
22513 editor.handle_input("2", window, cx)
22514 });
22515 cx.executor().advance_clock(Duration::from_millis(60));
22516 cx.executor().run_until_parked();
22517 assert_eq!(
22518 diagnostic_requests.load(atomic::Ordering::Acquire),
22519 2,
22520 "Editing should trigger diagnostic request"
22521 );
22522 ensure_result_id(Some("2".to_string()), cx);
22523
22524 // Moving cursor should not trigger diagnostic request
22525 editor.update_in(cx, |editor, window, cx| {
22526 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22527 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
22528 });
22529 });
22530 cx.executor().advance_clock(Duration::from_millis(60));
22531 cx.executor().run_until_parked();
22532 assert_eq!(
22533 diagnostic_requests.load(atomic::Ordering::Acquire),
22534 2,
22535 "Cursor movement should not trigger diagnostic request"
22536 );
22537 ensure_result_id(Some("2".to_string()), cx);
22538 // Multiple rapid edits should be debounced
22539 for _ in 0..5 {
22540 editor.update_in(cx, |editor, window, cx| {
22541 editor.handle_input("x", window, cx)
22542 });
22543 }
22544 cx.executor().advance_clock(Duration::from_millis(60));
22545 cx.executor().run_until_parked();
22546
22547 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
22548 assert!(
22549 final_requests <= 4,
22550 "Multiple rapid edits should be debounced (got {final_requests} requests)",
22551 );
22552 ensure_result_id(Some(final_requests.to_string()), cx);
22553}
22554
22555#[gpui::test]
22556async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
22557 // Regression test for issue #11671
22558 // Previously, adding a cursor after moving multiple cursors would reset
22559 // the cursor count instead of adding to the existing cursors.
22560 init_test(cx, |_| {});
22561 let mut cx = EditorTestContext::new(cx).await;
22562
22563 // Create a simple buffer with cursor at start
22564 cx.set_state(indoc! {"
22565 ˇaaaa
22566 bbbb
22567 cccc
22568 dddd
22569 eeee
22570 ffff
22571 gggg
22572 hhhh"});
22573
22574 // Add 2 cursors below (so we have 3 total)
22575 cx.update_editor(|editor, window, cx| {
22576 editor.add_selection_below(&Default::default(), window, cx);
22577 editor.add_selection_below(&Default::default(), window, cx);
22578 });
22579
22580 // Verify we have 3 cursors
22581 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
22582 assert_eq!(
22583 initial_count, 3,
22584 "Should have 3 cursors after adding 2 below"
22585 );
22586
22587 // Move down one line
22588 cx.update_editor(|editor, window, cx| {
22589 editor.move_down(&MoveDown, window, cx);
22590 });
22591
22592 // Add another cursor below
22593 cx.update_editor(|editor, window, cx| {
22594 editor.add_selection_below(&Default::default(), window, cx);
22595 });
22596
22597 // Should now have 4 cursors (3 original + 1 new)
22598 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
22599 assert_eq!(
22600 final_count, 4,
22601 "Should have 4 cursors after moving and adding another"
22602 );
22603}
22604
22605#[gpui::test(iterations = 10)]
22606async fn test_document_colors(cx: &mut TestAppContext) {
22607 let expected_color = Rgba {
22608 r: 0.33,
22609 g: 0.33,
22610 b: 0.33,
22611 a: 0.33,
22612 };
22613
22614 init_test(cx, |_| {});
22615
22616 let fs = FakeFs::new(cx.executor());
22617 fs.insert_tree(
22618 path!("/a"),
22619 json!({
22620 "first.rs": "fn main() { let a = 5; }",
22621 }),
22622 )
22623 .await;
22624
22625 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22626 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22627 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22628
22629 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22630 language_registry.add(rust_lang());
22631 let mut fake_servers = language_registry.register_fake_lsp(
22632 "Rust",
22633 FakeLspAdapter {
22634 capabilities: lsp::ServerCapabilities {
22635 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
22636 ..lsp::ServerCapabilities::default()
22637 },
22638 name: "rust-analyzer",
22639 ..FakeLspAdapter::default()
22640 },
22641 );
22642 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
22643 "Rust",
22644 FakeLspAdapter {
22645 capabilities: lsp::ServerCapabilities {
22646 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
22647 ..lsp::ServerCapabilities::default()
22648 },
22649 name: "not-rust-analyzer",
22650 ..FakeLspAdapter::default()
22651 },
22652 );
22653
22654 let editor = workspace
22655 .update(cx, |workspace, window, cx| {
22656 workspace.open_abs_path(
22657 PathBuf::from(path!("/a/first.rs")),
22658 OpenOptions::default(),
22659 window,
22660 cx,
22661 )
22662 })
22663 .unwrap()
22664 .await
22665 .unwrap()
22666 .downcast::<Editor>()
22667 .unwrap();
22668 let fake_language_server = fake_servers.next().await.unwrap();
22669 let fake_language_server_without_capabilities =
22670 fake_servers_without_capabilities.next().await.unwrap();
22671 let requests_made = Arc::new(AtomicUsize::new(0));
22672 let closure_requests_made = Arc::clone(&requests_made);
22673 let mut color_request_handle = fake_language_server
22674 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
22675 let requests_made = Arc::clone(&closure_requests_made);
22676 async move {
22677 assert_eq!(
22678 params.text_document.uri,
22679 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
22680 );
22681 requests_made.fetch_add(1, atomic::Ordering::Release);
22682 Ok(vec![
22683 lsp::ColorInformation {
22684 range: lsp::Range {
22685 start: lsp::Position {
22686 line: 0,
22687 character: 0,
22688 },
22689 end: lsp::Position {
22690 line: 0,
22691 character: 1,
22692 },
22693 },
22694 color: lsp::Color {
22695 red: 0.33,
22696 green: 0.33,
22697 blue: 0.33,
22698 alpha: 0.33,
22699 },
22700 },
22701 lsp::ColorInformation {
22702 range: lsp::Range {
22703 start: lsp::Position {
22704 line: 0,
22705 character: 0,
22706 },
22707 end: lsp::Position {
22708 line: 0,
22709 character: 1,
22710 },
22711 },
22712 color: lsp::Color {
22713 red: 0.33,
22714 green: 0.33,
22715 blue: 0.33,
22716 alpha: 0.33,
22717 },
22718 },
22719 ])
22720 }
22721 });
22722
22723 let _handle = fake_language_server_without_capabilities
22724 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
22725 panic!("Should not be called");
22726 });
22727 cx.executor().advance_clock(Duration::from_millis(100));
22728 color_request_handle.next().await.unwrap();
22729 cx.run_until_parked();
22730 assert_eq!(
22731 1,
22732 requests_made.load(atomic::Ordering::Acquire),
22733 "Should query for colors once per editor open"
22734 );
22735 editor.update_in(cx, |editor, _, cx| {
22736 assert_eq!(
22737 vec![expected_color],
22738 extract_color_inlays(editor, cx),
22739 "Should have an initial inlay"
22740 );
22741 });
22742
22743 // opening another file in a split should not influence the LSP query counter
22744 workspace
22745 .update(cx, |workspace, window, cx| {
22746 assert_eq!(
22747 workspace.panes().len(),
22748 1,
22749 "Should have one pane with one editor"
22750 );
22751 workspace.move_item_to_pane_in_direction(
22752 &MoveItemToPaneInDirection {
22753 direction: SplitDirection::Right,
22754 focus: false,
22755 clone: true,
22756 },
22757 window,
22758 cx,
22759 );
22760 })
22761 .unwrap();
22762 cx.run_until_parked();
22763 workspace
22764 .update(cx, |workspace, _, cx| {
22765 let panes = workspace.panes();
22766 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
22767 for pane in panes {
22768 let editor = pane
22769 .read(cx)
22770 .active_item()
22771 .and_then(|item| item.downcast::<Editor>())
22772 .expect("Should have opened an editor in each split");
22773 let editor_file = editor
22774 .read(cx)
22775 .buffer()
22776 .read(cx)
22777 .as_singleton()
22778 .expect("test deals with singleton buffers")
22779 .read(cx)
22780 .file()
22781 .expect("test buffese should have a file")
22782 .path();
22783 assert_eq!(
22784 editor_file.as_ref(),
22785 Path::new("first.rs"),
22786 "Both editors should be opened for the same file"
22787 )
22788 }
22789 })
22790 .unwrap();
22791
22792 cx.executor().advance_clock(Duration::from_millis(500));
22793 let save = editor.update_in(cx, |editor, window, cx| {
22794 editor.move_to_end(&MoveToEnd, window, cx);
22795 editor.handle_input("dirty", window, cx);
22796 editor.save(
22797 SaveOptions {
22798 format: true,
22799 autosave: true,
22800 },
22801 project.clone(),
22802 window,
22803 cx,
22804 )
22805 });
22806 save.await.unwrap();
22807
22808 color_request_handle.next().await.unwrap();
22809 cx.run_until_parked();
22810 assert_eq!(
22811 3,
22812 requests_made.load(atomic::Ordering::Acquire),
22813 "Should query for colors once per save and once per formatting after save"
22814 );
22815
22816 drop(editor);
22817 let close = workspace
22818 .update(cx, |workspace, window, cx| {
22819 workspace.active_pane().update(cx, |pane, cx| {
22820 pane.close_active_item(&CloseActiveItem::default(), window, cx)
22821 })
22822 })
22823 .unwrap();
22824 close.await.unwrap();
22825 let close = workspace
22826 .update(cx, |workspace, window, cx| {
22827 workspace.active_pane().update(cx, |pane, cx| {
22828 pane.close_active_item(&CloseActiveItem::default(), window, cx)
22829 })
22830 })
22831 .unwrap();
22832 close.await.unwrap();
22833 assert_eq!(
22834 3,
22835 requests_made.load(atomic::Ordering::Acquire),
22836 "After saving and closing all editors, no extra requests should be made"
22837 );
22838 workspace
22839 .update(cx, |workspace, _, cx| {
22840 assert!(
22841 workspace.active_item(cx).is_none(),
22842 "Should close all editors"
22843 )
22844 })
22845 .unwrap();
22846
22847 workspace
22848 .update(cx, |workspace, window, cx| {
22849 workspace.active_pane().update(cx, |pane, cx| {
22850 pane.navigate_backward(window, cx);
22851 })
22852 })
22853 .unwrap();
22854 cx.executor().advance_clock(Duration::from_millis(100));
22855 cx.run_until_parked();
22856 let editor = workspace
22857 .update(cx, |workspace, _, cx| {
22858 workspace
22859 .active_item(cx)
22860 .expect("Should have reopened the editor again after navigating back")
22861 .downcast::<Editor>()
22862 .expect("Should be an editor")
22863 })
22864 .unwrap();
22865 color_request_handle.next().await.unwrap();
22866 assert_eq!(
22867 3,
22868 requests_made.load(atomic::Ordering::Acquire),
22869 "Cache should be reused on buffer close and reopen"
22870 );
22871 editor.update(cx, |editor, cx| {
22872 assert_eq!(
22873 vec![expected_color],
22874 extract_color_inlays(editor, cx),
22875 "Should have an initial inlay"
22876 );
22877 });
22878}
22879
22880#[gpui::test]
22881async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
22882 init_test(cx, |_| {});
22883 let (editor, cx) = cx.add_window_view(Editor::single_line);
22884 editor.update_in(cx, |editor, window, cx| {
22885 editor.set_text("oops\n\nwow\n", window, cx)
22886 });
22887 cx.run_until_parked();
22888 editor.update(cx, |editor, cx| {
22889 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
22890 });
22891 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
22892 cx.run_until_parked();
22893 editor.update(cx, |editor, cx| {
22894 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
22895 });
22896}
22897
22898#[track_caller]
22899fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
22900 editor
22901 .all_inlays(cx)
22902 .into_iter()
22903 .filter_map(|inlay| inlay.get_color())
22904 .map(Rgba::from)
22905 .collect()
22906}