1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 inline_completion_tests::FakeInlineCompletionProvider,
6 linked_editing_ranges::LinkedEditingRanges,
7 scroll::scroll_amount::ScrollAmount,
8 test::{
9 assert_text_with_selections, build_editor,
10 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
11 editor_test_context::EditorTestContext,
12 select_ranges,
13 },
14};
15use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
16use futures::StreamExt;
17use gpui::{
18 BackgroundExecutor, DismissEvent, SemanticVersion, TestAppContext, UpdateGlobal,
19 VisualTestContext, WindowBounds, WindowOptions, div,
20};
21use indoc::indoc;
22use language::{
23 BracketPairConfig,
24 Capability::ReadWrite,
25 FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName,
26 Override, Point,
27 language_settings::{
28 AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
29 LanguageSettingsContent, LspInsertMode, PrettierSettings,
30 },
31 tree_sitter_python,
32};
33use language_settings::{Formatter, FormatterList, IndentGuideSettings};
34use lsp::CompletionParams;
35use multi_buffer::{IndentGuide, PathKey};
36use parking_lot::Mutex;
37use pretty_assertions::{assert_eq, assert_ne};
38use project::{
39 FakeFs,
40 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
41 project_settings::{LspSettings, ProjectSettings},
42};
43use serde_json::{self, json};
44use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
45use std::{
46 iter,
47 sync::atomic::{self, AtomicUsize},
48};
49use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
50use text::ToPoint as _;
51use unindent::Unindent;
52use util::{
53 assert_set_eq, path,
54 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
55 uri,
56};
57use workspace::{
58 CloseActiveItem, CloseAllItems, CloseInactiveItems, NavigationEntry, OpenOptions, ViewId,
59 item::{FollowEvent, FollowableItem, Item, ItemHandle},
60};
61
62#[gpui::test]
63fn test_edit_events(cx: &mut TestAppContext) {
64 init_test(cx, |_| {});
65
66 let buffer = cx.new(|cx| {
67 let mut buffer = language::Buffer::local("123456", cx);
68 buffer.set_group_interval(Duration::from_secs(1));
69 buffer
70 });
71
72 let events = Rc::new(RefCell::new(Vec::new()));
73 let editor1 = cx.add_window({
74 let events = events.clone();
75 |window, cx| {
76 let entity = cx.entity().clone();
77 cx.subscribe_in(
78 &entity,
79 window,
80 move |_, _, event: &EditorEvent, _, _| match event {
81 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
82 EditorEvent::BufferEdited => {
83 events.borrow_mut().push(("editor1", "buffer edited"))
84 }
85 _ => {}
86 },
87 )
88 .detach();
89 Editor::for_buffer(buffer.clone(), None, window, cx)
90 }
91 });
92
93 let editor2 = cx.add_window({
94 let events = events.clone();
95 |window, cx| {
96 cx.subscribe_in(
97 &cx.entity().clone(),
98 window,
99 move |_, _, event: &EditorEvent, _, _| match event {
100 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
101 EditorEvent::BufferEdited => {
102 events.borrow_mut().push(("editor2", "buffer edited"))
103 }
104 _ => {}
105 },
106 )
107 .detach();
108 Editor::for_buffer(buffer.clone(), None, window, cx)
109 }
110 });
111
112 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
113
114 // Mutating editor 1 will emit an `Edited` event only for that editor.
115 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
116 assert_eq!(
117 mem::take(&mut *events.borrow_mut()),
118 [
119 ("editor1", "edited"),
120 ("editor1", "buffer edited"),
121 ("editor2", "buffer edited"),
122 ]
123 );
124
125 // Mutating editor 2 will emit an `Edited` event only for that editor.
126 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
127 assert_eq!(
128 mem::take(&mut *events.borrow_mut()),
129 [
130 ("editor2", "edited"),
131 ("editor1", "buffer edited"),
132 ("editor2", "buffer edited"),
133 ]
134 );
135
136 // Undoing on editor 1 will emit an `Edited` event only for that editor.
137 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
138 assert_eq!(
139 mem::take(&mut *events.borrow_mut()),
140 [
141 ("editor1", "edited"),
142 ("editor1", "buffer edited"),
143 ("editor2", "buffer edited"),
144 ]
145 );
146
147 // Redoing on editor 1 will emit an `Edited` event only for that editor.
148 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
149 assert_eq!(
150 mem::take(&mut *events.borrow_mut()),
151 [
152 ("editor1", "edited"),
153 ("editor1", "buffer edited"),
154 ("editor2", "buffer edited"),
155 ]
156 );
157
158 // Undoing on editor 2 will emit an `Edited` event only for that editor.
159 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
160 assert_eq!(
161 mem::take(&mut *events.borrow_mut()),
162 [
163 ("editor2", "edited"),
164 ("editor1", "buffer edited"),
165 ("editor2", "buffer edited"),
166 ]
167 );
168
169 // Redoing on editor 2 will emit an `Edited` event only for that editor.
170 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
171 assert_eq!(
172 mem::take(&mut *events.borrow_mut()),
173 [
174 ("editor2", "edited"),
175 ("editor1", "buffer edited"),
176 ("editor2", "buffer edited"),
177 ]
178 );
179
180 // No event is emitted when the mutation is a no-op.
181 _ = editor2.update(cx, |editor, window, cx| {
182 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
183
184 editor.backspace(&Backspace, window, cx);
185 });
186 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
187}
188
189#[gpui::test]
190fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
191 init_test(cx, |_| {});
192
193 let mut now = Instant::now();
194 let group_interval = Duration::from_millis(1);
195 let buffer = cx.new(|cx| {
196 let mut buf = language::Buffer::local("123456", cx);
197 buf.set_group_interval(group_interval);
198 buf
199 });
200 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
201 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
202
203 _ = editor.update(cx, |editor, window, cx| {
204 editor.start_transaction_at(now, window, cx);
205 editor.change_selections(None, window, cx, |s| s.select_ranges([2..4]));
206
207 editor.insert("cd", window, cx);
208 editor.end_transaction_at(now, cx);
209 assert_eq!(editor.text(cx), "12cd56");
210 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
211
212 editor.start_transaction_at(now, window, cx);
213 editor.change_selections(None, window, cx, |s| s.select_ranges([4..5]));
214 editor.insert("e", window, cx);
215 editor.end_transaction_at(now, cx);
216 assert_eq!(editor.text(cx), "12cde6");
217 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
218
219 now += group_interval + Duration::from_millis(1);
220 editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]));
221
222 // Simulate an edit in another editor
223 buffer.update(cx, |buffer, cx| {
224 buffer.start_transaction_at(now, cx);
225 buffer.edit([(0..1, "a")], None, cx);
226 buffer.edit([(1..1, "b")], None, cx);
227 buffer.end_transaction_at(now, cx);
228 });
229
230 assert_eq!(editor.text(cx), "ab2cde6");
231 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
232
233 // Last transaction happened past the group interval in a different editor.
234 // Undo it individually and don't restore selections.
235 editor.undo(&Undo, window, cx);
236 assert_eq!(editor.text(cx), "12cde6");
237 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
238
239 // First two transactions happened within the group interval in this editor.
240 // Undo them together and restore selections.
241 editor.undo(&Undo, window, cx);
242 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
243 assert_eq!(editor.text(cx), "123456");
244 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
245
246 // Redo the first two transactions together.
247 editor.redo(&Redo, window, cx);
248 assert_eq!(editor.text(cx), "12cde6");
249 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
250
251 // Redo the last transaction on its own.
252 editor.redo(&Redo, window, cx);
253 assert_eq!(editor.text(cx), "ab2cde6");
254 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
255
256 // Test empty transactions.
257 editor.start_transaction_at(now, window, cx);
258 editor.end_transaction_at(now, cx);
259 editor.undo(&Undo, window, cx);
260 assert_eq!(editor.text(cx), "12cde6");
261 });
262}
263
264#[gpui::test]
265fn test_ime_composition(cx: &mut TestAppContext) {
266 init_test(cx, |_| {});
267
268 let buffer = cx.new(|cx| {
269 let mut buffer = language::Buffer::local("abcde", cx);
270 // Ensure automatic grouping doesn't occur.
271 buffer.set_group_interval(Duration::ZERO);
272 buffer
273 });
274
275 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
276 cx.add_window(|window, cx| {
277 let mut editor = build_editor(buffer.clone(), window, cx);
278
279 // Start a new IME composition.
280 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
281 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
282 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
283 assert_eq!(editor.text(cx), "äbcde");
284 assert_eq!(
285 editor.marked_text_ranges(cx),
286 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
287 );
288
289 // Finalize IME composition.
290 editor.replace_text_in_range(None, "ā", window, cx);
291 assert_eq!(editor.text(cx), "ābcde");
292 assert_eq!(editor.marked_text_ranges(cx), None);
293
294 // IME composition edits are grouped and are undone/redone at once.
295 editor.undo(&Default::default(), window, cx);
296 assert_eq!(editor.text(cx), "abcde");
297 assert_eq!(editor.marked_text_ranges(cx), None);
298 editor.redo(&Default::default(), window, cx);
299 assert_eq!(editor.text(cx), "ābcde");
300 assert_eq!(editor.marked_text_ranges(cx), None);
301
302 // Start a new IME composition.
303 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
304 assert_eq!(
305 editor.marked_text_ranges(cx),
306 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
307 );
308
309 // Undoing during an IME composition cancels it.
310 editor.undo(&Default::default(), window, cx);
311 assert_eq!(editor.text(cx), "ābcde");
312 assert_eq!(editor.marked_text_ranges(cx), None);
313
314 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
315 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
316 assert_eq!(editor.text(cx), "ābcdè");
317 assert_eq!(
318 editor.marked_text_ranges(cx),
319 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
320 );
321
322 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
323 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
324 assert_eq!(editor.text(cx), "ābcdę");
325 assert_eq!(editor.marked_text_ranges(cx), None);
326
327 // Start a new IME composition with multiple cursors.
328 editor.change_selections(None, window, cx, |s| {
329 s.select_ranges([
330 OffsetUtf16(1)..OffsetUtf16(1),
331 OffsetUtf16(3)..OffsetUtf16(3),
332 OffsetUtf16(5)..OffsetUtf16(5),
333 ])
334 });
335 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
336 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
337 assert_eq!(
338 editor.marked_text_ranges(cx),
339 Some(vec![
340 OffsetUtf16(0)..OffsetUtf16(3),
341 OffsetUtf16(4)..OffsetUtf16(7),
342 OffsetUtf16(8)..OffsetUtf16(11)
343 ])
344 );
345
346 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
347 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
348 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
349 assert_eq!(
350 editor.marked_text_ranges(cx),
351 Some(vec![
352 OffsetUtf16(1)..OffsetUtf16(2),
353 OffsetUtf16(5)..OffsetUtf16(6),
354 OffsetUtf16(9)..OffsetUtf16(10)
355 ])
356 );
357
358 // Finalize IME composition with multiple cursors.
359 editor.replace_text_in_range(Some(9..10), "2", window, cx);
360 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
361 assert_eq!(editor.marked_text_ranges(cx), None);
362
363 editor
364 });
365}
366
367#[gpui::test]
368fn test_selection_with_mouse(cx: &mut TestAppContext) {
369 init_test(cx, |_| {});
370
371 let editor = cx.add_window(|window, cx| {
372 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
373 build_editor(buffer, window, cx)
374 });
375
376 _ = editor.update(cx, |editor, window, cx| {
377 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
378 });
379 assert_eq!(
380 editor
381 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
382 .unwrap(),
383 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
384 );
385
386 _ = editor.update(cx, |editor, window, cx| {
387 editor.update_selection(
388 DisplayPoint::new(DisplayRow(3), 3),
389 0,
390 gpui::Point::<f32>::default(),
391 window,
392 cx,
393 );
394 });
395
396 assert_eq!(
397 editor
398 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
399 .unwrap(),
400 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
401 );
402
403 _ = editor.update(cx, |editor, window, cx| {
404 editor.update_selection(
405 DisplayPoint::new(DisplayRow(1), 1),
406 0,
407 gpui::Point::<f32>::default(),
408 window,
409 cx,
410 );
411 });
412
413 assert_eq!(
414 editor
415 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
416 .unwrap(),
417 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
418 );
419
420 _ = editor.update(cx, |editor, window, cx| {
421 editor.end_selection(window, cx);
422 editor.update_selection(
423 DisplayPoint::new(DisplayRow(3), 3),
424 0,
425 gpui::Point::<f32>::default(),
426 window,
427 cx,
428 );
429 });
430
431 assert_eq!(
432 editor
433 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
434 .unwrap(),
435 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
436 );
437
438 _ = editor.update(cx, |editor, window, cx| {
439 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
440 editor.update_selection(
441 DisplayPoint::new(DisplayRow(0), 0),
442 0,
443 gpui::Point::<f32>::default(),
444 window,
445 cx,
446 );
447 });
448
449 assert_eq!(
450 editor
451 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
452 .unwrap(),
453 [
454 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
455 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
456 ]
457 );
458
459 _ = editor.update(cx, |editor, window, cx| {
460 editor.end_selection(window, cx);
461 });
462
463 assert_eq!(
464 editor
465 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
466 .unwrap(),
467 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
468 );
469}
470
471#[gpui::test]
472fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
473 init_test(cx, |_| {});
474
475 let editor = cx.add_window(|window, cx| {
476 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
477 build_editor(buffer, window, cx)
478 });
479
480 _ = editor.update(cx, |editor, window, cx| {
481 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
482 });
483
484 _ = editor.update(cx, |editor, window, cx| {
485 editor.end_selection(window, cx);
486 });
487
488 _ = editor.update(cx, |editor, window, cx| {
489 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
490 });
491
492 _ = editor.update(cx, |editor, window, cx| {
493 editor.end_selection(window, cx);
494 });
495
496 assert_eq!(
497 editor
498 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
499 .unwrap(),
500 [
501 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
502 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
503 ]
504 );
505
506 _ = editor.update(cx, |editor, window, cx| {
507 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
508 });
509
510 _ = editor.update(cx, |editor, window, cx| {
511 editor.end_selection(window, cx);
512 });
513
514 assert_eq!(
515 editor
516 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
517 .unwrap(),
518 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
519 );
520}
521
522#[gpui::test]
523fn test_canceling_pending_selection(cx: &mut TestAppContext) {
524 init_test(cx, |_| {});
525
526 let editor = cx.add_window(|window, cx| {
527 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
528 build_editor(buffer, window, cx)
529 });
530
531 _ = editor.update(cx, |editor, window, cx| {
532 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
533 assert_eq!(
534 editor.selections.display_ranges(cx),
535 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
536 );
537 });
538
539 _ = editor.update(cx, |editor, window, cx| {
540 editor.update_selection(
541 DisplayPoint::new(DisplayRow(3), 3),
542 0,
543 gpui::Point::<f32>::default(),
544 window,
545 cx,
546 );
547 assert_eq!(
548 editor.selections.display_ranges(cx),
549 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
550 );
551 });
552
553 _ = editor.update(cx, |editor, window, cx| {
554 editor.cancel(&Cancel, window, cx);
555 editor.update_selection(
556 DisplayPoint::new(DisplayRow(1), 1),
557 0,
558 gpui::Point::<f32>::default(),
559 window,
560 cx,
561 );
562 assert_eq!(
563 editor.selections.display_ranges(cx),
564 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
565 );
566 });
567}
568
569#[gpui::test]
570fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
571 init_test(cx, |_| {});
572
573 let editor = cx.add_window(|window, cx| {
574 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
575 build_editor(buffer, window, cx)
576 });
577
578 _ = editor.update(cx, |editor, window, cx| {
579 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
580 assert_eq!(
581 editor.selections.display_ranges(cx),
582 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
583 );
584
585 editor.move_down(&Default::default(), window, cx);
586 assert_eq!(
587 editor.selections.display_ranges(cx),
588 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
589 );
590
591 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
592 assert_eq!(
593 editor.selections.display_ranges(cx),
594 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
595 );
596
597 editor.move_up(&Default::default(), window, cx);
598 assert_eq!(
599 editor.selections.display_ranges(cx),
600 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
601 );
602 });
603}
604
605#[gpui::test]
606fn test_clone(cx: &mut TestAppContext) {
607 init_test(cx, |_| {});
608
609 let (text, selection_ranges) = marked_text_ranges(
610 indoc! {"
611 one
612 two
613 threeˇ
614 four
615 fiveˇ
616 "},
617 true,
618 );
619
620 let editor = cx.add_window(|window, cx| {
621 let buffer = MultiBuffer::build_simple(&text, cx);
622 build_editor(buffer, window, cx)
623 });
624
625 _ = editor.update(cx, |editor, window, cx| {
626 editor.change_selections(None, window, cx, |s| {
627 s.select_ranges(selection_ranges.clone())
628 });
629 editor.fold_creases(
630 vec![
631 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
632 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
633 ],
634 true,
635 window,
636 cx,
637 );
638 });
639
640 let cloned_editor = editor
641 .update(cx, |editor, _, cx| {
642 cx.open_window(Default::default(), |window, cx| {
643 cx.new(|cx| editor.clone(window, cx))
644 })
645 })
646 .unwrap()
647 .unwrap();
648
649 let snapshot = editor
650 .update(cx, |e, window, cx| e.snapshot(window, cx))
651 .unwrap();
652 let cloned_snapshot = cloned_editor
653 .update(cx, |e, window, cx| e.snapshot(window, cx))
654 .unwrap();
655
656 assert_eq!(
657 cloned_editor
658 .update(cx, |e, _, cx| e.display_text(cx))
659 .unwrap(),
660 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
661 );
662 assert_eq!(
663 cloned_snapshot
664 .folds_in_range(0..text.len())
665 .collect::<Vec<_>>(),
666 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
667 );
668 assert_set_eq!(
669 cloned_editor
670 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
671 .unwrap(),
672 editor
673 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
674 .unwrap()
675 );
676 assert_set_eq!(
677 cloned_editor
678 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
679 .unwrap(),
680 editor
681 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
682 .unwrap()
683 );
684}
685
686#[gpui::test]
687async fn test_navigation_history(cx: &mut TestAppContext) {
688 init_test(cx, |_| {});
689
690 use workspace::item::Item;
691
692 let fs = FakeFs::new(cx.executor());
693 let project = Project::test(fs, [], cx).await;
694 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
695 let pane = workspace
696 .update(cx, |workspace, _, _| workspace.active_pane().clone())
697 .unwrap();
698
699 _ = workspace.update(cx, |_v, window, cx| {
700 cx.new(|cx| {
701 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
702 let mut editor = build_editor(buffer.clone(), window, cx);
703 let handle = cx.entity();
704 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
705
706 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
707 editor.nav_history.as_mut().unwrap().pop_backward(cx)
708 }
709
710 // Move the cursor a small distance.
711 // Nothing is added to the navigation history.
712 editor.change_selections(None, window, cx, |s| {
713 s.select_display_ranges([
714 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
715 ])
716 });
717 editor.change_selections(None, window, cx, |s| {
718 s.select_display_ranges([
719 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
720 ])
721 });
722 assert!(pop_history(&mut editor, cx).is_none());
723
724 // Move the cursor a large distance.
725 // The history can jump back to the previous position.
726 editor.change_selections(None, window, cx, |s| {
727 s.select_display_ranges([
728 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
729 ])
730 });
731 let nav_entry = pop_history(&mut editor, cx).unwrap();
732 editor.navigate(nav_entry.data.unwrap(), window, cx);
733 assert_eq!(nav_entry.item.id(), cx.entity_id());
734 assert_eq!(
735 editor.selections.display_ranges(cx),
736 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
737 );
738 assert!(pop_history(&mut editor, cx).is_none());
739
740 // Move the cursor a small distance via the mouse.
741 // Nothing is added to the navigation history.
742 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
743 editor.end_selection(window, cx);
744 assert_eq!(
745 editor.selections.display_ranges(cx),
746 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
747 );
748 assert!(pop_history(&mut editor, cx).is_none());
749
750 // Move the cursor a large distance via the mouse.
751 // The history can jump back to the previous position.
752 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
753 editor.end_selection(window, cx);
754 assert_eq!(
755 editor.selections.display_ranges(cx),
756 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
757 );
758 let nav_entry = pop_history(&mut editor, cx).unwrap();
759 editor.navigate(nav_entry.data.unwrap(), window, cx);
760 assert_eq!(nav_entry.item.id(), cx.entity_id());
761 assert_eq!(
762 editor.selections.display_ranges(cx),
763 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
764 );
765 assert!(pop_history(&mut editor, cx).is_none());
766
767 // Set scroll position to check later
768 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
769 let original_scroll_position = editor.scroll_manager.anchor();
770
771 // Jump to the end of the document and adjust scroll
772 editor.move_to_end(&MoveToEnd, window, cx);
773 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
774 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
775
776 let nav_entry = pop_history(&mut editor, cx).unwrap();
777 editor.navigate(nav_entry.data.unwrap(), window, cx);
778 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
779
780 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
781 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
782 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
783 let invalid_point = Point::new(9999, 0);
784 editor.navigate(
785 Box::new(NavigationData {
786 cursor_anchor: invalid_anchor,
787 cursor_position: invalid_point,
788 scroll_anchor: ScrollAnchor {
789 anchor: invalid_anchor,
790 offset: Default::default(),
791 },
792 scroll_top_row: invalid_point.row,
793 }),
794 window,
795 cx,
796 );
797 assert_eq!(
798 editor.selections.display_ranges(cx),
799 &[editor.max_point(cx)..editor.max_point(cx)]
800 );
801 assert_eq!(
802 editor.scroll_position(cx),
803 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
804 );
805
806 editor
807 })
808 });
809}
810
811#[gpui::test]
812fn test_cancel(cx: &mut TestAppContext) {
813 init_test(cx, |_| {});
814
815 let editor = cx.add_window(|window, cx| {
816 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
817 build_editor(buffer, window, cx)
818 });
819
820 _ = editor.update(cx, |editor, window, cx| {
821 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
822 editor.update_selection(
823 DisplayPoint::new(DisplayRow(1), 1),
824 0,
825 gpui::Point::<f32>::default(),
826 window,
827 cx,
828 );
829 editor.end_selection(window, cx);
830
831 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
832 editor.update_selection(
833 DisplayPoint::new(DisplayRow(0), 3),
834 0,
835 gpui::Point::<f32>::default(),
836 window,
837 cx,
838 );
839 editor.end_selection(window, cx);
840 assert_eq!(
841 editor.selections.display_ranges(cx),
842 [
843 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
844 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
845 ]
846 );
847 });
848
849 _ = editor.update(cx, |editor, window, cx| {
850 editor.cancel(&Cancel, window, cx);
851 assert_eq!(
852 editor.selections.display_ranges(cx),
853 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
854 );
855 });
856
857 _ = editor.update(cx, |editor, window, cx| {
858 editor.cancel(&Cancel, window, cx);
859 assert_eq!(
860 editor.selections.display_ranges(cx),
861 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
862 );
863 });
864}
865
866#[gpui::test]
867fn test_fold_action(cx: &mut TestAppContext) {
868 init_test(cx, |_| {});
869
870 let editor = cx.add_window(|window, cx| {
871 let buffer = MultiBuffer::build_simple(
872 &"
873 impl Foo {
874 // Hello!
875
876 fn a() {
877 1
878 }
879
880 fn b() {
881 2
882 }
883
884 fn c() {
885 3
886 }
887 }
888 "
889 .unindent(),
890 cx,
891 );
892 build_editor(buffer.clone(), window, cx)
893 });
894
895 _ = editor.update(cx, |editor, window, cx| {
896 editor.change_selections(None, window, cx, |s| {
897 s.select_display_ranges([
898 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
899 ]);
900 });
901 editor.fold(&Fold, window, cx);
902 assert_eq!(
903 editor.display_text(cx),
904 "
905 impl Foo {
906 // Hello!
907
908 fn a() {
909 1
910 }
911
912 fn b() {⋯
913 }
914
915 fn c() {⋯
916 }
917 }
918 "
919 .unindent(),
920 );
921
922 editor.fold(&Fold, window, cx);
923 assert_eq!(
924 editor.display_text(cx),
925 "
926 impl Foo {⋯
927 }
928 "
929 .unindent(),
930 );
931
932 editor.unfold_lines(&UnfoldLines, window, cx);
933 assert_eq!(
934 editor.display_text(cx),
935 "
936 impl Foo {
937 // Hello!
938
939 fn a() {
940 1
941 }
942
943 fn b() {⋯
944 }
945
946 fn c() {⋯
947 }
948 }
949 "
950 .unindent(),
951 );
952
953 editor.unfold_lines(&UnfoldLines, window, cx);
954 assert_eq!(
955 editor.display_text(cx),
956 editor.buffer.read(cx).read(cx).text()
957 );
958 });
959}
960
961#[gpui::test]
962fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
963 init_test(cx, |_| {});
964
965 let editor = cx.add_window(|window, cx| {
966 let buffer = MultiBuffer::build_simple(
967 &"
968 class Foo:
969 # Hello!
970
971 def a():
972 print(1)
973
974 def b():
975 print(2)
976
977 def c():
978 print(3)
979 "
980 .unindent(),
981 cx,
982 );
983 build_editor(buffer.clone(), window, cx)
984 });
985
986 _ = editor.update(cx, |editor, window, cx| {
987 editor.change_selections(None, window, cx, |s| {
988 s.select_display_ranges([
989 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
990 ]);
991 });
992 editor.fold(&Fold, window, cx);
993 assert_eq!(
994 editor.display_text(cx),
995 "
996 class Foo:
997 # Hello!
998
999 def a():
1000 print(1)
1001
1002 def b():⋯
1003
1004 def c():⋯
1005 "
1006 .unindent(),
1007 );
1008
1009 editor.fold(&Fold, window, cx);
1010 assert_eq!(
1011 editor.display_text(cx),
1012 "
1013 class Foo:⋯
1014 "
1015 .unindent(),
1016 );
1017
1018 editor.unfold_lines(&UnfoldLines, window, cx);
1019 assert_eq!(
1020 editor.display_text(cx),
1021 "
1022 class Foo:
1023 # Hello!
1024
1025 def a():
1026 print(1)
1027
1028 def b():⋯
1029
1030 def c():⋯
1031 "
1032 .unindent(),
1033 );
1034
1035 editor.unfold_lines(&UnfoldLines, window, cx);
1036 assert_eq!(
1037 editor.display_text(cx),
1038 editor.buffer.read(cx).read(cx).text()
1039 );
1040 });
1041}
1042
1043#[gpui::test]
1044fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1045 init_test(cx, |_| {});
1046
1047 let editor = cx.add_window(|window, cx| {
1048 let buffer = MultiBuffer::build_simple(
1049 &"
1050 class Foo:
1051 # Hello!
1052
1053 def a():
1054 print(1)
1055
1056 def b():
1057 print(2)
1058
1059
1060 def c():
1061 print(3)
1062
1063
1064 "
1065 .unindent(),
1066 cx,
1067 );
1068 build_editor(buffer.clone(), window, cx)
1069 });
1070
1071 _ = editor.update(cx, |editor, window, cx| {
1072 editor.change_selections(None, window, cx, |s| {
1073 s.select_display_ranges([
1074 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1075 ]);
1076 });
1077 editor.fold(&Fold, window, cx);
1078 assert_eq!(
1079 editor.display_text(cx),
1080 "
1081 class Foo:
1082 # Hello!
1083
1084 def a():
1085 print(1)
1086
1087 def b():⋯
1088
1089
1090 def c():⋯
1091
1092
1093 "
1094 .unindent(),
1095 );
1096
1097 editor.fold(&Fold, window, cx);
1098 assert_eq!(
1099 editor.display_text(cx),
1100 "
1101 class Foo:⋯
1102
1103
1104 "
1105 .unindent(),
1106 );
1107
1108 editor.unfold_lines(&UnfoldLines, window, cx);
1109 assert_eq!(
1110 editor.display_text(cx),
1111 "
1112 class Foo:
1113 # Hello!
1114
1115 def a():
1116 print(1)
1117
1118 def b():⋯
1119
1120
1121 def c():⋯
1122
1123
1124 "
1125 .unindent(),
1126 );
1127
1128 editor.unfold_lines(&UnfoldLines, window, cx);
1129 assert_eq!(
1130 editor.display_text(cx),
1131 editor.buffer.read(cx).read(cx).text()
1132 );
1133 });
1134}
1135
1136#[gpui::test]
1137fn test_fold_at_level(cx: &mut TestAppContext) {
1138 init_test(cx, |_| {});
1139
1140 let editor = cx.add_window(|window, cx| {
1141 let buffer = MultiBuffer::build_simple(
1142 &"
1143 class Foo:
1144 # Hello!
1145
1146 def a():
1147 print(1)
1148
1149 def b():
1150 print(2)
1151
1152
1153 class Bar:
1154 # World!
1155
1156 def a():
1157 print(1)
1158
1159 def b():
1160 print(2)
1161
1162
1163 "
1164 .unindent(),
1165 cx,
1166 );
1167 build_editor(buffer.clone(), window, cx)
1168 });
1169
1170 _ = editor.update(cx, |editor, window, cx| {
1171 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1172 assert_eq!(
1173 editor.display_text(cx),
1174 "
1175 class Foo:
1176 # Hello!
1177
1178 def a():⋯
1179
1180 def b():⋯
1181
1182
1183 class Bar:
1184 # World!
1185
1186 def a():⋯
1187
1188 def b():⋯
1189
1190
1191 "
1192 .unindent(),
1193 );
1194
1195 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1196 assert_eq!(
1197 editor.display_text(cx),
1198 "
1199 class Foo:⋯
1200
1201
1202 class Bar:⋯
1203
1204
1205 "
1206 .unindent(),
1207 );
1208
1209 editor.unfold_all(&UnfoldAll, window, cx);
1210 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1211 assert_eq!(
1212 editor.display_text(cx),
1213 "
1214 class Foo:
1215 # Hello!
1216
1217 def a():
1218 print(1)
1219
1220 def b():
1221 print(2)
1222
1223
1224 class Bar:
1225 # World!
1226
1227 def a():
1228 print(1)
1229
1230 def b():
1231 print(2)
1232
1233
1234 "
1235 .unindent(),
1236 );
1237
1238 assert_eq!(
1239 editor.display_text(cx),
1240 editor.buffer.read(cx).read(cx).text()
1241 );
1242 });
1243}
1244
1245#[gpui::test]
1246fn test_move_cursor(cx: &mut TestAppContext) {
1247 init_test(cx, |_| {});
1248
1249 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1250 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1251
1252 buffer.update(cx, |buffer, cx| {
1253 buffer.edit(
1254 vec![
1255 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1256 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1257 ],
1258 None,
1259 cx,
1260 );
1261 });
1262 _ = editor.update(cx, |editor, window, cx| {
1263 assert_eq!(
1264 editor.selections.display_ranges(cx),
1265 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1266 );
1267
1268 editor.move_down(&MoveDown, window, cx);
1269 assert_eq!(
1270 editor.selections.display_ranges(cx),
1271 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1272 );
1273
1274 editor.move_right(&MoveRight, window, cx);
1275 assert_eq!(
1276 editor.selections.display_ranges(cx),
1277 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1278 );
1279
1280 editor.move_left(&MoveLeft, window, cx);
1281 assert_eq!(
1282 editor.selections.display_ranges(cx),
1283 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1284 );
1285
1286 editor.move_up(&MoveUp, window, cx);
1287 assert_eq!(
1288 editor.selections.display_ranges(cx),
1289 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1290 );
1291
1292 editor.move_to_end(&MoveToEnd, window, cx);
1293 assert_eq!(
1294 editor.selections.display_ranges(cx),
1295 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1296 );
1297
1298 editor.move_to_beginning(&MoveToBeginning, window, cx);
1299 assert_eq!(
1300 editor.selections.display_ranges(cx),
1301 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1302 );
1303
1304 editor.change_selections(None, window, cx, |s| {
1305 s.select_display_ranges([
1306 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1307 ]);
1308 });
1309 editor.select_to_beginning(&SelectToBeginning, window, cx);
1310 assert_eq!(
1311 editor.selections.display_ranges(cx),
1312 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1313 );
1314
1315 editor.select_to_end(&SelectToEnd, window, cx);
1316 assert_eq!(
1317 editor.selections.display_ranges(cx),
1318 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1319 );
1320 });
1321}
1322
1323#[gpui::test]
1324fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1325 init_test(cx, |_| {});
1326
1327 let editor = cx.add_window(|window, cx| {
1328 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1329 build_editor(buffer.clone(), window, cx)
1330 });
1331
1332 assert_eq!('🟥'.len_utf8(), 4);
1333 assert_eq!('α'.len_utf8(), 2);
1334
1335 _ = editor.update(cx, |editor, window, cx| {
1336 editor.fold_creases(
1337 vec![
1338 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1339 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1340 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1341 ],
1342 true,
1343 window,
1344 cx,
1345 );
1346 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1347
1348 editor.move_right(&MoveRight, window, cx);
1349 assert_eq!(
1350 editor.selections.display_ranges(cx),
1351 &[empty_range(0, "🟥".len())]
1352 );
1353 editor.move_right(&MoveRight, window, cx);
1354 assert_eq!(
1355 editor.selections.display_ranges(cx),
1356 &[empty_range(0, "🟥🟧".len())]
1357 );
1358 editor.move_right(&MoveRight, window, cx);
1359 assert_eq!(
1360 editor.selections.display_ranges(cx),
1361 &[empty_range(0, "🟥🟧⋯".len())]
1362 );
1363
1364 editor.move_down(&MoveDown, window, cx);
1365 assert_eq!(
1366 editor.selections.display_ranges(cx),
1367 &[empty_range(1, "ab⋯e".len())]
1368 );
1369 editor.move_left(&MoveLeft, window, cx);
1370 assert_eq!(
1371 editor.selections.display_ranges(cx),
1372 &[empty_range(1, "ab⋯".len())]
1373 );
1374 editor.move_left(&MoveLeft, window, cx);
1375 assert_eq!(
1376 editor.selections.display_ranges(cx),
1377 &[empty_range(1, "ab".len())]
1378 );
1379 editor.move_left(&MoveLeft, window, cx);
1380 assert_eq!(
1381 editor.selections.display_ranges(cx),
1382 &[empty_range(1, "a".len())]
1383 );
1384
1385 editor.move_down(&MoveDown, window, cx);
1386 assert_eq!(
1387 editor.selections.display_ranges(cx),
1388 &[empty_range(2, "α".len())]
1389 );
1390 editor.move_right(&MoveRight, window, cx);
1391 assert_eq!(
1392 editor.selections.display_ranges(cx),
1393 &[empty_range(2, "αβ".len())]
1394 );
1395 editor.move_right(&MoveRight, window, cx);
1396 assert_eq!(
1397 editor.selections.display_ranges(cx),
1398 &[empty_range(2, "αβ⋯".len())]
1399 );
1400 editor.move_right(&MoveRight, window, cx);
1401 assert_eq!(
1402 editor.selections.display_ranges(cx),
1403 &[empty_range(2, "αβ⋯ε".len())]
1404 );
1405
1406 editor.move_up(&MoveUp, window, cx);
1407 assert_eq!(
1408 editor.selections.display_ranges(cx),
1409 &[empty_range(1, "ab⋯e".len())]
1410 );
1411 editor.move_down(&MoveDown, window, cx);
1412 assert_eq!(
1413 editor.selections.display_ranges(cx),
1414 &[empty_range(2, "αβ⋯ε".len())]
1415 );
1416 editor.move_up(&MoveUp, window, cx);
1417 assert_eq!(
1418 editor.selections.display_ranges(cx),
1419 &[empty_range(1, "ab⋯e".len())]
1420 );
1421
1422 editor.move_up(&MoveUp, window, cx);
1423 assert_eq!(
1424 editor.selections.display_ranges(cx),
1425 &[empty_range(0, "🟥🟧".len())]
1426 );
1427 editor.move_left(&MoveLeft, window, cx);
1428 assert_eq!(
1429 editor.selections.display_ranges(cx),
1430 &[empty_range(0, "🟥".len())]
1431 );
1432 editor.move_left(&MoveLeft, window, cx);
1433 assert_eq!(
1434 editor.selections.display_ranges(cx),
1435 &[empty_range(0, "".len())]
1436 );
1437 });
1438}
1439
1440#[gpui::test]
1441fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1442 init_test(cx, |_| {});
1443
1444 let editor = cx.add_window(|window, cx| {
1445 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1446 build_editor(buffer.clone(), window, cx)
1447 });
1448 _ = editor.update(cx, |editor, window, cx| {
1449 editor.change_selections(None, window, cx, |s| {
1450 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1451 });
1452
1453 // moving above start of document should move selection to start of document,
1454 // but the next move down should still be at the original goal_x
1455 editor.move_up(&MoveUp, window, cx);
1456 assert_eq!(
1457 editor.selections.display_ranges(cx),
1458 &[empty_range(0, "".len())]
1459 );
1460
1461 editor.move_down(&MoveDown, window, cx);
1462 assert_eq!(
1463 editor.selections.display_ranges(cx),
1464 &[empty_range(1, "abcd".len())]
1465 );
1466
1467 editor.move_down(&MoveDown, window, cx);
1468 assert_eq!(
1469 editor.selections.display_ranges(cx),
1470 &[empty_range(2, "αβγ".len())]
1471 );
1472
1473 editor.move_down(&MoveDown, window, cx);
1474 assert_eq!(
1475 editor.selections.display_ranges(cx),
1476 &[empty_range(3, "abcd".len())]
1477 );
1478
1479 editor.move_down(&MoveDown, window, cx);
1480 assert_eq!(
1481 editor.selections.display_ranges(cx),
1482 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1483 );
1484
1485 // moving past end of document should not change goal_x
1486 editor.move_down(&MoveDown, window, cx);
1487 assert_eq!(
1488 editor.selections.display_ranges(cx),
1489 &[empty_range(5, "".len())]
1490 );
1491
1492 editor.move_down(&MoveDown, window, cx);
1493 assert_eq!(
1494 editor.selections.display_ranges(cx),
1495 &[empty_range(5, "".len())]
1496 );
1497
1498 editor.move_up(&MoveUp, window, cx);
1499 assert_eq!(
1500 editor.selections.display_ranges(cx),
1501 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1502 );
1503
1504 editor.move_up(&MoveUp, window, cx);
1505 assert_eq!(
1506 editor.selections.display_ranges(cx),
1507 &[empty_range(3, "abcd".len())]
1508 );
1509
1510 editor.move_up(&MoveUp, window, cx);
1511 assert_eq!(
1512 editor.selections.display_ranges(cx),
1513 &[empty_range(2, "αβγ".len())]
1514 );
1515 });
1516}
1517
1518#[gpui::test]
1519fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1520 init_test(cx, |_| {});
1521 let move_to_beg = MoveToBeginningOfLine {
1522 stop_at_soft_wraps: true,
1523 stop_at_indent: true,
1524 };
1525
1526 let delete_to_beg = DeleteToBeginningOfLine {
1527 stop_at_indent: false,
1528 };
1529
1530 let move_to_end = MoveToEndOfLine {
1531 stop_at_soft_wraps: true,
1532 };
1533
1534 let editor = cx.add_window(|window, cx| {
1535 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1536 build_editor(buffer, window, cx)
1537 });
1538 _ = editor.update(cx, |editor, window, cx| {
1539 editor.change_selections(None, window, cx, |s| {
1540 s.select_display_ranges([
1541 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1542 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1543 ]);
1544 });
1545 });
1546
1547 _ = editor.update(cx, |editor, window, cx| {
1548 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1549 assert_eq!(
1550 editor.selections.display_ranges(cx),
1551 &[
1552 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1553 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1554 ]
1555 );
1556 });
1557
1558 _ = editor.update(cx, |editor, window, cx| {
1559 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1560 assert_eq!(
1561 editor.selections.display_ranges(cx),
1562 &[
1563 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1564 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1565 ]
1566 );
1567 });
1568
1569 _ = editor.update(cx, |editor, window, cx| {
1570 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1571 assert_eq!(
1572 editor.selections.display_ranges(cx),
1573 &[
1574 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1575 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1576 ]
1577 );
1578 });
1579
1580 _ = editor.update(cx, |editor, window, cx| {
1581 editor.move_to_end_of_line(&move_to_end, window, cx);
1582 assert_eq!(
1583 editor.selections.display_ranges(cx),
1584 &[
1585 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1586 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1587 ]
1588 );
1589 });
1590
1591 // Moving to the end of line again is a no-op.
1592 _ = editor.update(cx, |editor, window, cx| {
1593 editor.move_to_end_of_line(&move_to_end, window, cx);
1594 assert_eq!(
1595 editor.selections.display_ranges(cx),
1596 &[
1597 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1598 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1599 ]
1600 );
1601 });
1602
1603 _ = editor.update(cx, |editor, window, cx| {
1604 editor.move_left(&MoveLeft, window, cx);
1605 editor.select_to_beginning_of_line(
1606 &SelectToBeginningOfLine {
1607 stop_at_soft_wraps: true,
1608 stop_at_indent: true,
1609 },
1610 window,
1611 cx,
1612 );
1613 assert_eq!(
1614 editor.selections.display_ranges(cx),
1615 &[
1616 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1617 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1618 ]
1619 );
1620 });
1621
1622 _ = editor.update(cx, |editor, window, cx| {
1623 editor.select_to_beginning_of_line(
1624 &SelectToBeginningOfLine {
1625 stop_at_soft_wraps: true,
1626 stop_at_indent: true,
1627 },
1628 window,
1629 cx,
1630 );
1631 assert_eq!(
1632 editor.selections.display_ranges(cx),
1633 &[
1634 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1635 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1636 ]
1637 );
1638 });
1639
1640 _ = editor.update(cx, |editor, window, cx| {
1641 editor.select_to_beginning_of_line(
1642 &SelectToBeginningOfLine {
1643 stop_at_soft_wraps: true,
1644 stop_at_indent: true,
1645 },
1646 window,
1647 cx,
1648 );
1649 assert_eq!(
1650 editor.selections.display_ranges(cx),
1651 &[
1652 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1653 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1654 ]
1655 );
1656 });
1657
1658 _ = editor.update(cx, |editor, window, cx| {
1659 editor.select_to_end_of_line(
1660 &SelectToEndOfLine {
1661 stop_at_soft_wraps: true,
1662 },
1663 window,
1664 cx,
1665 );
1666 assert_eq!(
1667 editor.selections.display_ranges(cx),
1668 &[
1669 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1670 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1671 ]
1672 );
1673 });
1674
1675 _ = editor.update(cx, |editor, window, cx| {
1676 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1677 assert_eq!(editor.display_text(cx), "ab\n de");
1678 assert_eq!(
1679 editor.selections.display_ranges(cx),
1680 &[
1681 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1682 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1683 ]
1684 );
1685 });
1686
1687 _ = editor.update(cx, |editor, window, cx| {
1688 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1689 assert_eq!(editor.display_text(cx), "\n");
1690 assert_eq!(
1691 editor.selections.display_ranges(cx),
1692 &[
1693 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1694 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1695 ]
1696 );
1697 });
1698}
1699
1700#[gpui::test]
1701fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1702 init_test(cx, |_| {});
1703 let move_to_beg = MoveToBeginningOfLine {
1704 stop_at_soft_wraps: false,
1705 stop_at_indent: false,
1706 };
1707
1708 let move_to_end = MoveToEndOfLine {
1709 stop_at_soft_wraps: false,
1710 };
1711
1712 let editor = cx.add_window(|window, cx| {
1713 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1714 build_editor(buffer, window, cx)
1715 });
1716
1717 _ = editor.update(cx, |editor, window, cx| {
1718 editor.set_wrap_width(Some(140.0.into()), cx);
1719
1720 // We expect the following lines after wrapping
1721 // ```
1722 // thequickbrownfox
1723 // jumpedoverthelazydo
1724 // gs
1725 // ```
1726 // The final `gs` was soft-wrapped onto a new line.
1727 assert_eq!(
1728 "thequickbrownfox\njumpedoverthelaz\nydogs",
1729 editor.display_text(cx),
1730 );
1731
1732 // First, let's assert behavior on the first line, that was not soft-wrapped.
1733 // Start the cursor at the `k` on the first line
1734 editor.change_selections(None, window, cx, |s| {
1735 s.select_display_ranges([
1736 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1737 ]);
1738 });
1739
1740 // Moving to the beginning of the line should put us at the beginning of the line.
1741 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1742 assert_eq!(
1743 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1744 editor.selections.display_ranges(cx)
1745 );
1746
1747 // Moving to the end of the line should put us at the end of the line.
1748 editor.move_to_end_of_line(&move_to_end, window, cx);
1749 assert_eq!(
1750 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1751 editor.selections.display_ranges(cx)
1752 );
1753
1754 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1755 // Start the cursor at the last line (`y` that was wrapped to a new line)
1756 editor.change_selections(None, window, cx, |s| {
1757 s.select_display_ranges([
1758 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1759 ]);
1760 });
1761
1762 // Moving to the beginning of the line should put us at the start of the second line of
1763 // display text, i.e., the `j`.
1764 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1765 assert_eq!(
1766 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1767 editor.selections.display_ranges(cx)
1768 );
1769
1770 // Moving to the beginning of the line again should be a no-op.
1771 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1772 assert_eq!(
1773 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1774 editor.selections.display_ranges(cx)
1775 );
1776
1777 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1778 // next display line.
1779 editor.move_to_end_of_line(&move_to_end, window, cx);
1780 assert_eq!(
1781 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1782 editor.selections.display_ranges(cx)
1783 );
1784
1785 // Moving to the end of the line again should be a no-op.
1786 editor.move_to_end_of_line(&move_to_end, window, cx);
1787 assert_eq!(
1788 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1789 editor.selections.display_ranges(cx)
1790 );
1791 });
1792}
1793
1794#[gpui::test]
1795fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1796 init_test(cx, |_| {});
1797
1798 let move_to_beg = MoveToBeginningOfLine {
1799 stop_at_soft_wraps: true,
1800 stop_at_indent: true,
1801 };
1802
1803 let select_to_beg = SelectToBeginningOfLine {
1804 stop_at_soft_wraps: true,
1805 stop_at_indent: true,
1806 };
1807
1808 let delete_to_beg = DeleteToBeginningOfLine {
1809 stop_at_indent: true,
1810 };
1811
1812 let move_to_end = MoveToEndOfLine {
1813 stop_at_soft_wraps: false,
1814 };
1815
1816 let editor = cx.add_window(|window, cx| {
1817 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1818 build_editor(buffer, window, cx)
1819 });
1820
1821 _ = editor.update(cx, |editor, window, cx| {
1822 editor.change_selections(None, window, cx, |s| {
1823 s.select_display_ranges([
1824 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1825 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1826 ]);
1827 });
1828
1829 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1830 // and the second cursor at the first non-whitespace character in the line.
1831 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1832 assert_eq!(
1833 editor.selections.display_ranges(cx),
1834 &[
1835 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1836 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1837 ]
1838 );
1839
1840 // Moving to the beginning of the line again should be a no-op for the first cursor,
1841 // and should move the second cursor to the beginning of the line.
1842 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1843 assert_eq!(
1844 editor.selections.display_ranges(cx),
1845 &[
1846 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1847 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1848 ]
1849 );
1850
1851 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1852 // and should move the second cursor back to the first non-whitespace character in the line.
1853 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1854 assert_eq!(
1855 editor.selections.display_ranges(cx),
1856 &[
1857 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1858 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1859 ]
1860 );
1861
1862 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1863 // and to the first non-whitespace character in the line for the second cursor.
1864 editor.move_to_end_of_line(&move_to_end, window, cx);
1865 editor.move_left(&MoveLeft, window, cx);
1866 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1867 assert_eq!(
1868 editor.selections.display_ranges(cx),
1869 &[
1870 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1871 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1872 ]
1873 );
1874
1875 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1876 // and should select to the beginning of the line for the second cursor.
1877 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1878 assert_eq!(
1879 editor.selections.display_ranges(cx),
1880 &[
1881 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1882 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1883 ]
1884 );
1885
1886 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1887 // and should delete to the first non-whitespace character in the line for the second cursor.
1888 editor.move_to_end_of_line(&move_to_end, window, cx);
1889 editor.move_left(&MoveLeft, window, cx);
1890 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1891 assert_eq!(editor.text(cx), "c\n f");
1892 });
1893}
1894
1895#[gpui::test]
1896fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1897 init_test(cx, |_| {});
1898
1899 let editor = cx.add_window(|window, cx| {
1900 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1901 build_editor(buffer, window, cx)
1902 });
1903 _ = editor.update(cx, |editor, window, cx| {
1904 editor.change_selections(None, window, cx, |s| {
1905 s.select_display_ranges([
1906 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1907 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1908 ])
1909 });
1910
1911 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1912 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1913
1914 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1915 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1916
1917 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1918 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1919
1920 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1921 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1922
1923 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1924 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
1925
1926 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1927 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1928
1929 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1930 assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1931
1932 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1933 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1934
1935 editor.move_right(&MoveRight, window, cx);
1936 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1937 assert_selection_ranges(
1938 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1939 editor,
1940 cx,
1941 );
1942
1943 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1944 assert_selection_ranges(
1945 "use std«ˇ::s»tr::{foo, bar}\n\n«ˇ {b»az.qux()}",
1946 editor,
1947 cx,
1948 );
1949
1950 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1951 assert_selection_ranges(
1952 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1953 editor,
1954 cx,
1955 );
1956 });
1957}
1958
1959#[gpui::test]
1960fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1961 init_test(cx, |_| {});
1962
1963 let editor = cx.add_window(|window, cx| {
1964 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1965 build_editor(buffer, window, cx)
1966 });
1967
1968 _ = editor.update(cx, |editor, window, cx| {
1969 editor.set_wrap_width(Some(140.0.into()), cx);
1970 assert_eq!(
1971 editor.display_text(cx),
1972 "use one::{\n two::three::\n four::five\n};"
1973 );
1974
1975 editor.change_selections(None, window, cx, |s| {
1976 s.select_display_ranges([
1977 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1978 ]);
1979 });
1980
1981 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1982 assert_eq!(
1983 editor.selections.display_ranges(cx),
1984 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1985 );
1986
1987 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1988 assert_eq!(
1989 editor.selections.display_ranges(cx),
1990 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1991 );
1992
1993 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1994 assert_eq!(
1995 editor.selections.display_ranges(cx),
1996 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1997 );
1998
1999 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2000 assert_eq!(
2001 editor.selections.display_ranges(cx),
2002 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2003 );
2004
2005 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2006 assert_eq!(
2007 editor.selections.display_ranges(cx),
2008 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2009 );
2010
2011 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2012 assert_eq!(
2013 editor.selections.display_ranges(cx),
2014 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2015 );
2016 });
2017}
2018
2019#[gpui::test]
2020async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2021 init_test(cx, |_| {});
2022 let mut cx = EditorTestContext::new(cx).await;
2023
2024 let line_height = cx.editor(|editor, window, _| {
2025 editor
2026 .style()
2027 .unwrap()
2028 .text
2029 .line_height_in_pixels(window.rem_size())
2030 });
2031 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2032
2033 cx.set_state(
2034 &r#"ˇone
2035 two
2036
2037 three
2038 fourˇ
2039 five
2040
2041 six"#
2042 .unindent(),
2043 );
2044
2045 cx.update_editor(|editor, window, cx| {
2046 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2047 });
2048 cx.assert_editor_state(
2049 &r#"one
2050 two
2051 ˇ
2052 three
2053 four
2054 five
2055 ˇ
2056 six"#
2057 .unindent(),
2058 );
2059
2060 cx.update_editor(|editor, window, cx| {
2061 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2062 });
2063 cx.assert_editor_state(
2064 &r#"one
2065 two
2066
2067 three
2068 four
2069 five
2070 ˇ
2071 sixˇ"#
2072 .unindent(),
2073 );
2074
2075 cx.update_editor(|editor, window, cx| {
2076 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2077 });
2078 cx.assert_editor_state(
2079 &r#"one
2080 two
2081
2082 three
2083 four
2084 five
2085
2086 sixˇ"#
2087 .unindent(),
2088 );
2089
2090 cx.update_editor(|editor, window, cx| {
2091 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2092 });
2093 cx.assert_editor_state(
2094 &r#"one
2095 two
2096
2097 three
2098 four
2099 five
2100 ˇ
2101 six"#
2102 .unindent(),
2103 );
2104
2105 cx.update_editor(|editor, window, cx| {
2106 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2107 });
2108 cx.assert_editor_state(
2109 &r#"one
2110 two
2111 ˇ
2112 three
2113 four
2114 five
2115
2116 six"#
2117 .unindent(),
2118 );
2119
2120 cx.update_editor(|editor, window, cx| {
2121 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2122 });
2123 cx.assert_editor_state(
2124 &r#"ˇone
2125 two
2126
2127 three
2128 four
2129 five
2130
2131 six"#
2132 .unindent(),
2133 );
2134}
2135
2136#[gpui::test]
2137async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2138 init_test(cx, |_| {});
2139 let mut cx = EditorTestContext::new(cx).await;
2140 let line_height = cx.editor(|editor, window, _| {
2141 editor
2142 .style()
2143 .unwrap()
2144 .text
2145 .line_height_in_pixels(window.rem_size())
2146 });
2147 let window = cx.window;
2148 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2149
2150 cx.set_state(
2151 r#"ˇone
2152 two
2153 three
2154 four
2155 five
2156 six
2157 seven
2158 eight
2159 nine
2160 ten
2161 "#,
2162 );
2163
2164 cx.update_editor(|editor, window, cx| {
2165 assert_eq!(
2166 editor.snapshot(window, cx).scroll_position(),
2167 gpui::Point::new(0., 0.)
2168 );
2169 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2170 assert_eq!(
2171 editor.snapshot(window, cx).scroll_position(),
2172 gpui::Point::new(0., 3.)
2173 );
2174 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2175 assert_eq!(
2176 editor.snapshot(window, cx).scroll_position(),
2177 gpui::Point::new(0., 6.)
2178 );
2179 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2180 assert_eq!(
2181 editor.snapshot(window, cx).scroll_position(),
2182 gpui::Point::new(0., 3.)
2183 );
2184
2185 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2186 assert_eq!(
2187 editor.snapshot(window, cx).scroll_position(),
2188 gpui::Point::new(0., 1.)
2189 );
2190 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2191 assert_eq!(
2192 editor.snapshot(window, cx).scroll_position(),
2193 gpui::Point::new(0., 3.)
2194 );
2195 });
2196}
2197
2198#[gpui::test]
2199async fn test_autoscroll(cx: &mut TestAppContext) {
2200 init_test(cx, |_| {});
2201 let mut cx = EditorTestContext::new(cx).await;
2202
2203 let line_height = cx.update_editor(|editor, window, cx| {
2204 editor.set_vertical_scroll_margin(2, cx);
2205 editor
2206 .style()
2207 .unwrap()
2208 .text
2209 .line_height_in_pixels(window.rem_size())
2210 });
2211 let window = cx.window;
2212 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2213
2214 cx.set_state(
2215 r#"ˇone
2216 two
2217 three
2218 four
2219 five
2220 six
2221 seven
2222 eight
2223 nine
2224 ten
2225 "#,
2226 );
2227 cx.update_editor(|editor, window, cx| {
2228 assert_eq!(
2229 editor.snapshot(window, cx).scroll_position(),
2230 gpui::Point::new(0., 0.0)
2231 );
2232 });
2233
2234 // Add a cursor below the visible area. Since both cursors cannot fit
2235 // on screen, the editor autoscrolls to reveal the newest cursor, and
2236 // allows the vertical scroll margin below that cursor.
2237 cx.update_editor(|editor, window, cx| {
2238 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2239 selections.select_ranges([
2240 Point::new(0, 0)..Point::new(0, 0),
2241 Point::new(6, 0)..Point::new(6, 0),
2242 ]);
2243 })
2244 });
2245 cx.update_editor(|editor, window, cx| {
2246 assert_eq!(
2247 editor.snapshot(window, cx).scroll_position(),
2248 gpui::Point::new(0., 3.0)
2249 );
2250 });
2251
2252 // Move down. The editor cursor scrolls down to track the newest cursor.
2253 cx.update_editor(|editor, window, cx| {
2254 editor.move_down(&Default::default(), window, cx);
2255 });
2256 cx.update_editor(|editor, window, cx| {
2257 assert_eq!(
2258 editor.snapshot(window, cx).scroll_position(),
2259 gpui::Point::new(0., 4.0)
2260 );
2261 });
2262
2263 // Add a cursor above the visible area. Since both cursors fit on screen,
2264 // the editor scrolls to show both.
2265 cx.update_editor(|editor, window, cx| {
2266 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2267 selections.select_ranges([
2268 Point::new(1, 0)..Point::new(1, 0),
2269 Point::new(6, 0)..Point::new(6, 0),
2270 ]);
2271 })
2272 });
2273 cx.update_editor(|editor, window, cx| {
2274 assert_eq!(
2275 editor.snapshot(window, cx).scroll_position(),
2276 gpui::Point::new(0., 1.0)
2277 );
2278 });
2279}
2280
2281#[gpui::test]
2282async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2283 init_test(cx, |_| {});
2284 let mut cx = EditorTestContext::new(cx).await;
2285
2286 let line_height = cx.editor(|editor, window, _cx| {
2287 editor
2288 .style()
2289 .unwrap()
2290 .text
2291 .line_height_in_pixels(window.rem_size())
2292 });
2293 let window = cx.window;
2294 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2295 cx.set_state(
2296 &r#"
2297 ˇone
2298 two
2299 threeˇ
2300 four
2301 five
2302 six
2303 seven
2304 eight
2305 nine
2306 ten
2307 "#
2308 .unindent(),
2309 );
2310
2311 cx.update_editor(|editor, window, cx| {
2312 editor.move_page_down(&MovePageDown::default(), window, cx)
2313 });
2314 cx.assert_editor_state(
2315 &r#"
2316 one
2317 two
2318 three
2319 ˇfour
2320 five
2321 sixˇ
2322 seven
2323 eight
2324 nine
2325 ten
2326 "#
2327 .unindent(),
2328 );
2329
2330 cx.update_editor(|editor, window, cx| {
2331 editor.move_page_down(&MovePageDown::default(), window, cx)
2332 });
2333 cx.assert_editor_state(
2334 &r#"
2335 one
2336 two
2337 three
2338 four
2339 five
2340 six
2341 ˇseven
2342 eight
2343 nineˇ
2344 ten
2345 "#
2346 .unindent(),
2347 );
2348
2349 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2350 cx.assert_editor_state(
2351 &r#"
2352 one
2353 two
2354 three
2355 ˇfour
2356 five
2357 sixˇ
2358 seven
2359 eight
2360 nine
2361 ten
2362 "#
2363 .unindent(),
2364 );
2365
2366 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2367 cx.assert_editor_state(
2368 &r#"
2369 ˇone
2370 two
2371 threeˇ
2372 four
2373 five
2374 six
2375 seven
2376 eight
2377 nine
2378 ten
2379 "#
2380 .unindent(),
2381 );
2382
2383 // Test select collapsing
2384 cx.update_editor(|editor, window, cx| {
2385 editor.move_page_down(&MovePageDown::default(), window, cx);
2386 editor.move_page_down(&MovePageDown::default(), window, cx);
2387 editor.move_page_down(&MovePageDown::default(), window, cx);
2388 });
2389 cx.assert_editor_state(
2390 &r#"
2391 one
2392 two
2393 three
2394 four
2395 five
2396 six
2397 seven
2398 eight
2399 nine
2400 ˇten
2401 ˇ"#
2402 .unindent(),
2403 );
2404}
2405
2406#[gpui::test]
2407async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2408 init_test(cx, |_| {});
2409 let mut cx = EditorTestContext::new(cx).await;
2410 cx.set_state("one «two threeˇ» four");
2411 cx.update_editor(|editor, window, cx| {
2412 editor.delete_to_beginning_of_line(
2413 &DeleteToBeginningOfLine {
2414 stop_at_indent: false,
2415 },
2416 window,
2417 cx,
2418 );
2419 assert_eq!(editor.text(cx), " four");
2420 });
2421}
2422
2423#[gpui::test]
2424fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2425 init_test(cx, |_| {});
2426
2427 let editor = cx.add_window(|window, cx| {
2428 let buffer = MultiBuffer::build_simple("one two three four", cx);
2429 build_editor(buffer.clone(), window, cx)
2430 });
2431
2432 _ = editor.update(cx, |editor, window, cx| {
2433 editor.change_selections(None, window, cx, |s| {
2434 s.select_display_ranges([
2435 // an empty selection - the preceding word fragment is deleted
2436 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2437 // characters selected - they are deleted
2438 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2439 ])
2440 });
2441 editor.delete_to_previous_word_start(
2442 &DeleteToPreviousWordStart {
2443 ignore_newlines: false,
2444 },
2445 window,
2446 cx,
2447 );
2448 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2449 });
2450
2451 _ = editor.update(cx, |editor, window, cx| {
2452 editor.change_selections(None, window, cx, |s| {
2453 s.select_display_ranges([
2454 // an empty selection - the following word fragment is deleted
2455 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2456 // characters selected - they are deleted
2457 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2458 ])
2459 });
2460 editor.delete_to_next_word_end(
2461 &DeleteToNextWordEnd {
2462 ignore_newlines: false,
2463 },
2464 window,
2465 cx,
2466 );
2467 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2468 });
2469}
2470
2471#[gpui::test]
2472fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2473 init_test(cx, |_| {});
2474
2475 let editor = cx.add_window(|window, cx| {
2476 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2477 build_editor(buffer.clone(), window, cx)
2478 });
2479 let del_to_prev_word_start = DeleteToPreviousWordStart {
2480 ignore_newlines: false,
2481 };
2482 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2483 ignore_newlines: true,
2484 };
2485
2486 _ = editor.update(cx, |editor, window, cx| {
2487 editor.change_selections(None, window, cx, |s| {
2488 s.select_display_ranges([
2489 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2490 ])
2491 });
2492 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2493 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2494 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2495 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2496 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2497 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2498 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2499 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2500 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2501 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2502 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2503 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2504 });
2505}
2506
2507#[gpui::test]
2508fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2509 init_test(cx, |_| {});
2510
2511 let editor = cx.add_window(|window, cx| {
2512 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2513 build_editor(buffer.clone(), window, cx)
2514 });
2515 let del_to_next_word_end = DeleteToNextWordEnd {
2516 ignore_newlines: false,
2517 };
2518 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2519 ignore_newlines: true,
2520 };
2521
2522 _ = editor.update(cx, |editor, window, cx| {
2523 editor.change_selections(None, window, cx, |s| {
2524 s.select_display_ranges([
2525 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2526 ])
2527 });
2528 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2529 assert_eq!(
2530 editor.buffer.read(cx).read(cx).text(),
2531 "one\n two\nthree\n four"
2532 );
2533 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2534 assert_eq!(
2535 editor.buffer.read(cx).read(cx).text(),
2536 "\n two\nthree\n four"
2537 );
2538 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2539 assert_eq!(
2540 editor.buffer.read(cx).read(cx).text(),
2541 "two\nthree\n four"
2542 );
2543 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2544 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2545 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2546 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2547 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2548 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2549 });
2550}
2551
2552#[gpui::test]
2553fn test_newline(cx: &mut TestAppContext) {
2554 init_test(cx, |_| {});
2555
2556 let editor = cx.add_window(|window, cx| {
2557 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2558 build_editor(buffer.clone(), window, cx)
2559 });
2560
2561 _ = editor.update(cx, |editor, window, cx| {
2562 editor.change_selections(None, window, cx, |s| {
2563 s.select_display_ranges([
2564 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2565 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2566 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2567 ])
2568 });
2569
2570 editor.newline(&Newline, window, cx);
2571 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2572 });
2573}
2574
2575#[gpui::test]
2576fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2577 init_test(cx, |_| {});
2578
2579 let editor = cx.add_window(|window, cx| {
2580 let buffer = MultiBuffer::build_simple(
2581 "
2582 a
2583 b(
2584 X
2585 )
2586 c(
2587 X
2588 )
2589 "
2590 .unindent()
2591 .as_str(),
2592 cx,
2593 );
2594 let mut editor = build_editor(buffer.clone(), window, cx);
2595 editor.change_selections(None, window, cx, |s| {
2596 s.select_ranges([
2597 Point::new(2, 4)..Point::new(2, 5),
2598 Point::new(5, 4)..Point::new(5, 5),
2599 ])
2600 });
2601 editor
2602 });
2603
2604 _ = editor.update(cx, |editor, window, cx| {
2605 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2606 editor.buffer.update(cx, |buffer, cx| {
2607 buffer.edit(
2608 [
2609 (Point::new(1, 2)..Point::new(3, 0), ""),
2610 (Point::new(4, 2)..Point::new(6, 0), ""),
2611 ],
2612 None,
2613 cx,
2614 );
2615 assert_eq!(
2616 buffer.read(cx).text(),
2617 "
2618 a
2619 b()
2620 c()
2621 "
2622 .unindent()
2623 );
2624 });
2625 assert_eq!(
2626 editor.selections.ranges(cx),
2627 &[
2628 Point::new(1, 2)..Point::new(1, 2),
2629 Point::new(2, 2)..Point::new(2, 2),
2630 ],
2631 );
2632
2633 editor.newline(&Newline, window, cx);
2634 assert_eq!(
2635 editor.text(cx),
2636 "
2637 a
2638 b(
2639 )
2640 c(
2641 )
2642 "
2643 .unindent()
2644 );
2645
2646 // The selections are moved after the inserted newlines
2647 assert_eq!(
2648 editor.selections.ranges(cx),
2649 &[
2650 Point::new(2, 0)..Point::new(2, 0),
2651 Point::new(4, 0)..Point::new(4, 0),
2652 ],
2653 );
2654 });
2655}
2656
2657#[gpui::test]
2658async fn test_newline_above(cx: &mut TestAppContext) {
2659 init_test(cx, |settings| {
2660 settings.defaults.tab_size = NonZeroU32::new(4)
2661 });
2662
2663 let language = Arc::new(
2664 Language::new(
2665 LanguageConfig::default(),
2666 Some(tree_sitter_rust::LANGUAGE.into()),
2667 )
2668 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2669 .unwrap(),
2670 );
2671
2672 let mut cx = EditorTestContext::new(cx).await;
2673 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2674 cx.set_state(indoc! {"
2675 const a: ˇA = (
2676 (ˇ
2677 «const_functionˇ»(ˇ),
2678 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2679 )ˇ
2680 ˇ);ˇ
2681 "});
2682
2683 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2684 cx.assert_editor_state(indoc! {"
2685 ˇ
2686 const a: A = (
2687 ˇ
2688 (
2689 ˇ
2690 ˇ
2691 const_function(),
2692 ˇ
2693 ˇ
2694 ˇ
2695 ˇ
2696 something_else,
2697 ˇ
2698 )
2699 ˇ
2700 ˇ
2701 );
2702 "});
2703}
2704
2705#[gpui::test]
2706async fn test_newline_below(cx: &mut TestAppContext) {
2707 init_test(cx, |settings| {
2708 settings.defaults.tab_size = NonZeroU32::new(4)
2709 });
2710
2711 let language = Arc::new(
2712 Language::new(
2713 LanguageConfig::default(),
2714 Some(tree_sitter_rust::LANGUAGE.into()),
2715 )
2716 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2717 .unwrap(),
2718 );
2719
2720 let mut cx = EditorTestContext::new(cx).await;
2721 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2722 cx.set_state(indoc! {"
2723 const a: ˇA = (
2724 (ˇ
2725 «const_functionˇ»(ˇ),
2726 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2727 )ˇ
2728 ˇ);ˇ
2729 "});
2730
2731 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2732 cx.assert_editor_state(indoc! {"
2733 const a: A = (
2734 ˇ
2735 (
2736 ˇ
2737 const_function(),
2738 ˇ
2739 ˇ
2740 something_else,
2741 ˇ
2742 ˇ
2743 ˇ
2744 ˇ
2745 )
2746 ˇ
2747 );
2748 ˇ
2749 ˇ
2750 "});
2751}
2752
2753#[gpui::test]
2754async fn test_newline_comments(cx: &mut TestAppContext) {
2755 init_test(cx, |settings| {
2756 settings.defaults.tab_size = NonZeroU32::new(4)
2757 });
2758
2759 let language = Arc::new(Language::new(
2760 LanguageConfig {
2761 line_comments: vec!["// ".into()],
2762 ..LanguageConfig::default()
2763 },
2764 None,
2765 ));
2766 {
2767 let mut cx = EditorTestContext::new(cx).await;
2768 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2769 cx.set_state(indoc! {"
2770 // Fooˇ
2771 "});
2772
2773 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2774 cx.assert_editor_state(indoc! {"
2775 // Foo
2776 // ˇ
2777 "});
2778 // Ensure that we add comment prefix when existing line contains space
2779 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2780 cx.assert_editor_state(
2781 indoc! {"
2782 // Foo
2783 //s
2784 // ˇ
2785 "}
2786 .replace("s", " ") // s is used as space placeholder to prevent format on save
2787 .as_str(),
2788 );
2789 // Ensure that we add comment prefix when existing line does not contain space
2790 cx.set_state(indoc! {"
2791 // Foo
2792 //ˇ
2793 "});
2794 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2795 cx.assert_editor_state(indoc! {"
2796 // Foo
2797 //
2798 // ˇ
2799 "});
2800 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2801 cx.set_state(indoc! {"
2802 ˇ// Foo
2803 "});
2804 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2805 cx.assert_editor_state(indoc! {"
2806
2807 ˇ// Foo
2808 "});
2809 }
2810 // Ensure that comment continuations can be disabled.
2811 update_test_language_settings(cx, |settings| {
2812 settings.defaults.extend_comment_on_newline = Some(false);
2813 });
2814 let mut cx = EditorTestContext::new(cx).await;
2815 cx.set_state(indoc! {"
2816 // Fooˇ
2817 "});
2818 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2819 cx.assert_editor_state(indoc! {"
2820 // Foo
2821 ˇ
2822 "});
2823}
2824
2825#[gpui::test]
2826async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
2827 init_test(cx, |settings| {
2828 settings.defaults.tab_size = NonZeroU32::new(4)
2829 });
2830
2831 let language = Arc::new(Language::new(
2832 LanguageConfig {
2833 line_comments: vec!["// ".into(), "/// ".into()],
2834 ..LanguageConfig::default()
2835 },
2836 None,
2837 ));
2838 {
2839 let mut cx = EditorTestContext::new(cx).await;
2840 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2841 cx.set_state(indoc! {"
2842 //ˇ
2843 "});
2844 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2845 cx.assert_editor_state(indoc! {"
2846 //
2847 // ˇ
2848 "});
2849
2850 cx.set_state(indoc! {"
2851 ///ˇ
2852 "});
2853 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2854 cx.assert_editor_state(indoc! {"
2855 ///
2856 /// ˇ
2857 "});
2858 }
2859}
2860
2861#[gpui::test]
2862async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
2863 init_test(cx, |settings| {
2864 settings.defaults.tab_size = NonZeroU32::new(4)
2865 });
2866
2867 let language = Arc::new(
2868 Language::new(
2869 LanguageConfig {
2870 documentation: Some(language::DocumentationConfig {
2871 start: "/**".into(),
2872 end: "*/".into(),
2873 prefix: "* ".into(),
2874 tab_size: NonZeroU32::new(1).unwrap(),
2875 }),
2876
2877 ..LanguageConfig::default()
2878 },
2879 Some(tree_sitter_rust::LANGUAGE.into()),
2880 )
2881 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
2882 .unwrap(),
2883 );
2884
2885 {
2886 let mut cx = EditorTestContext::new(cx).await;
2887 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2888 cx.set_state(indoc! {"
2889 /**ˇ
2890 "});
2891
2892 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2893 cx.assert_editor_state(indoc! {"
2894 /**
2895 * ˇ
2896 "});
2897 // Ensure that if cursor is before the comment start,
2898 // we do not actually insert a comment prefix.
2899 cx.set_state(indoc! {"
2900 ˇ/**
2901 "});
2902 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2903 cx.assert_editor_state(indoc! {"
2904
2905 ˇ/**
2906 "});
2907 // Ensure that if cursor is between it doesn't add comment prefix.
2908 cx.set_state(indoc! {"
2909 /*ˇ*
2910 "});
2911 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2912 cx.assert_editor_state(indoc! {"
2913 /*
2914 ˇ*
2915 "});
2916 // Ensure that if suffix exists on same line after cursor it adds new line.
2917 cx.set_state(indoc! {"
2918 /**ˇ*/
2919 "});
2920 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2921 cx.assert_editor_state(indoc! {"
2922 /**
2923 * ˇ
2924 */
2925 "});
2926 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2927 cx.set_state(indoc! {"
2928 /**ˇ */
2929 "});
2930 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2931 cx.assert_editor_state(indoc! {"
2932 /**
2933 * ˇ
2934 */
2935 "});
2936 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2937 cx.set_state(indoc! {"
2938 /** ˇ*/
2939 "});
2940 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2941 cx.assert_editor_state(
2942 indoc! {"
2943 /**s
2944 * ˇ
2945 */
2946 "}
2947 .replace("s", " ") // s is used as space placeholder to prevent format on save
2948 .as_str(),
2949 );
2950 // Ensure that delimiter space is preserved when newline on already
2951 // spaced delimiter.
2952 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2953 cx.assert_editor_state(
2954 indoc! {"
2955 /**s
2956 *s
2957 * ˇ
2958 */
2959 "}
2960 .replace("s", " ") // s is used as space placeholder to prevent format on save
2961 .as_str(),
2962 );
2963 // Ensure that delimiter space is preserved when space is not
2964 // on existing delimiter.
2965 cx.set_state(indoc! {"
2966 /**
2967 *ˇ
2968 */
2969 "});
2970 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2971 cx.assert_editor_state(indoc! {"
2972 /**
2973 *
2974 * ˇ
2975 */
2976 "});
2977 // Ensure that if suffix exists on same line after cursor it
2978 // doesn't add extra new line if prefix is not on same line.
2979 cx.set_state(indoc! {"
2980 /**
2981 ˇ*/
2982 "});
2983 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2984 cx.assert_editor_state(indoc! {"
2985 /**
2986
2987 ˇ*/
2988 "});
2989 // Ensure that it detects suffix after existing prefix.
2990 cx.set_state(indoc! {"
2991 /**ˇ/
2992 "});
2993 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2994 cx.assert_editor_state(indoc! {"
2995 /**
2996 ˇ/
2997 "});
2998 // Ensure that if suffix exists on same line before
2999 // cursor it does not add comment prefix.
3000 cx.set_state(indoc! {"
3001 /** */ˇ
3002 "});
3003 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3004 cx.assert_editor_state(indoc! {"
3005 /** */
3006 ˇ
3007 "});
3008 // Ensure that if suffix exists on same line before
3009 // cursor it does not add comment prefix.
3010 cx.set_state(indoc! {"
3011 /**
3012 *
3013 */ˇ
3014 "});
3015 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3016 cx.assert_editor_state(indoc! {"
3017 /**
3018 *
3019 */
3020 ˇ
3021 "});
3022
3023 // Ensure that inline comment followed by code
3024 // doesn't add comment prefix on newline
3025 cx.set_state(indoc! {"
3026 /** */ textˇ
3027 "});
3028 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3029 cx.assert_editor_state(indoc! {"
3030 /** */ text
3031 ˇ
3032 "});
3033
3034 // Ensure that text after comment end tag
3035 // doesn't add comment prefix on newline
3036 cx.set_state(indoc! {"
3037 /**
3038 *
3039 */ˇtext
3040 "});
3041 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3042 cx.assert_editor_state(indoc! {"
3043 /**
3044 *
3045 */
3046 ˇtext
3047 "});
3048
3049 // Ensure if not comment block it doesn't
3050 // add comment prefix on newline
3051 cx.set_state(indoc! {"
3052 * textˇ
3053 "});
3054 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3055 cx.assert_editor_state(indoc! {"
3056 * text
3057 ˇ
3058 "});
3059 }
3060 // Ensure that comment continuations can be disabled.
3061 update_test_language_settings(cx, |settings| {
3062 settings.defaults.extend_comment_on_newline = Some(false);
3063 });
3064 let mut cx = EditorTestContext::new(cx).await;
3065 cx.set_state(indoc! {"
3066 /**ˇ
3067 "});
3068 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3069 cx.assert_editor_state(indoc! {"
3070 /**
3071 ˇ
3072 "});
3073}
3074
3075#[gpui::test]
3076fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3077 init_test(cx, |_| {});
3078
3079 let editor = cx.add_window(|window, cx| {
3080 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3081 let mut editor = build_editor(buffer.clone(), window, cx);
3082 editor.change_selections(None, window, cx, |s| {
3083 s.select_ranges([3..4, 11..12, 19..20])
3084 });
3085 editor
3086 });
3087
3088 _ = editor.update(cx, |editor, window, cx| {
3089 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3090 editor.buffer.update(cx, |buffer, cx| {
3091 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3092 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3093 });
3094 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3095
3096 editor.insert("Z", window, cx);
3097 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3098
3099 // The selections are moved after the inserted characters
3100 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3101 });
3102}
3103
3104#[gpui::test]
3105async fn test_tab(cx: &mut TestAppContext) {
3106 init_test(cx, |settings| {
3107 settings.defaults.tab_size = NonZeroU32::new(3)
3108 });
3109
3110 let mut cx = EditorTestContext::new(cx).await;
3111 cx.set_state(indoc! {"
3112 ˇabˇc
3113 ˇ🏀ˇ🏀ˇefg
3114 dˇ
3115 "});
3116 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3117 cx.assert_editor_state(indoc! {"
3118 ˇab ˇc
3119 ˇ🏀 ˇ🏀 ˇefg
3120 d ˇ
3121 "});
3122
3123 cx.set_state(indoc! {"
3124 a
3125 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3126 "});
3127 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3128 cx.assert_editor_state(indoc! {"
3129 a
3130 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3131 "});
3132}
3133
3134#[gpui::test]
3135async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3136 init_test(cx, |_| {});
3137
3138 let mut cx = EditorTestContext::new(cx).await;
3139 let language = Arc::new(
3140 Language::new(
3141 LanguageConfig::default(),
3142 Some(tree_sitter_rust::LANGUAGE.into()),
3143 )
3144 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3145 .unwrap(),
3146 );
3147 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3148
3149 // test when all cursors are not at suggested indent
3150 // then simply move to their suggested indent location
3151 cx.set_state(indoc! {"
3152 const a: B = (
3153 c(
3154 ˇ
3155 ˇ )
3156 );
3157 "});
3158 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3159 cx.assert_editor_state(indoc! {"
3160 const a: B = (
3161 c(
3162 ˇ
3163 ˇ)
3164 );
3165 "});
3166
3167 // test cursor already at suggested indent not moving when
3168 // other cursors are yet to reach their suggested indents
3169 cx.set_state(indoc! {"
3170 ˇ
3171 const a: B = (
3172 c(
3173 d(
3174 ˇ
3175 )
3176 ˇ
3177 ˇ )
3178 );
3179 "});
3180 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3181 cx.assert_editor_state(indoc! {"
3182 ˇ
3183 const a: B = (
3184 c(
3185 d(
3186 ˇ
3187 )
3188 ˇ
3189 ˇ)
3190 );
3191 "});
3192 // test when all cursors are at suggested indent then tab is inserted
3193 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3194 cx.assert_editor_state(indoc! {"
3195 ˇ
3196 const a: B = (
3197 c(
3198 d(
3199 ˇ
3200 )
3201 ˇ
3202 ˇ)
3203 );
3204 "});
3205
3206 // test when current indent is less than suggested indent,
3207 // we adjust line to match suggested indent and move cursor to it
3208 //
3209 // when no other cursor is at word boundary, all of them should move
3210 cx.set_state(indoc! {"
3211 const a: B = (
3212 c(
3213 d(
3214 ˇ
3215 ˇ )
3216 ˇ )
3217 );
3218 "});
3219 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3220 cx.assert_editor_state(indoc! {"
3221 const a: B = (
3222 c(
3223 d(
3224 ˇ
3225 ˇ)
3226 ˇ)
3227 );
3228 "});
3229
3230 // test when current indent is less than suggested indent,
3231 // we adjust line to match suggested indent and move cursor to it
3232 //
3233 // when some other cursor is at word boundary, it should not move
3234 cx.set_state(indoc! {"
3235 const a: B = (
3236 c(
3237 d(
3238 ˇ
3239 ˇ )
3240 ˇ)
3241 );
3242 "});
3243 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3244 cx.assert_editor_state(indoc! {"
3245 const a: B = (
3246 c(
3247 d(
3248 ˇ
3249 ˇ)
3250 ˇ)
3251 );
3252 "});
3253
3254 // test when current indent is more than suggested indent,
3255 // we just move cursor to current indent instead of suggested indent
3256 //
3257 // when no other cursor is at word boundary, all of them should move
3258 cx.set_state(indoc! {"
3259 const a: B = (
3260 c(
3261 d(
3262 ˇ
3263 ˇ )
3264 ˇ )
3265 );
3266 "});
3267 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3268 cx.assert_editor_state(indoc! {"
3269 const a: B = (
3270 c(
3271 d(
3272 ˇ
3273 ˇ)
3274 ˇ)
3275 );
3276 "});
3277 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3278 cx.assert_editor_state(indoc! {"
3279 const a: B = (
3280 c(
3281 d(
3282 ˇ
3283 ˇ)
3284 ˇ)
3285 );
3286 "});
3287
3288 // test when current indent is more than suggested indent,
3289 // we just move cursor to current indent instead of suggested indent
3290 //
3291 // when some other cursor is at word boundary, it doesn't move
3292 cx.set_state(indoc! {"
3293 const a: B = (
3294 c(
3295 d(
3296 ˇ
3297 ˇ )
3298 ˇ)
3299 );
3300 "});
3301 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3302 cx.assert_editor_state(indoc! {"
3303 const a: B = (
3304 c(
3305 d(
3306 ˇ
3307 ˇ)
3308 ˇ)
3309 );
3310 "});
3311
3312 // handle auto-indent when there are multiple cursors on the same line
3313 cx.set_state(indoc! {"
3314 const a: B = (
3315 c(
3316 ˇ ˇ
3317 ˇ )
3318 );
3319 "});
3320 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3321 cx.assert_editor_state(indoc! {"
3322 const a: B = (
3323 c(
3324 ˇ
3325 ˇ)
3326 );
3327 "});
3328}
3329
3330#[gpui::test]
3331async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3332 init_test(cx, |settings| {
3333 settings.defaults.tab_size = NonZeroU32::new(3)
3334 });
3335
3336 let mut cx = EditorTestContext::new(cx).await;
3337 cx.set_state(indoc! {"
3338 ˇ
3339 \t ˇ
3340 \t ˇ
3341 \t ˇ
3342 \t \t\t \t \t\t \t\t \t \t ˇ
3343 "});
3344
3345 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3346 cx.assert_editor_state(indoc! {"
3347 ˇ
3348 \t ˇ
3349 \t ˇ
3350 \t ˇ
3351 \t \t\t \t \t\t \t\t \t \t ˇ
3352 "});
3353}
3354
3355#[gpui::test]
3356async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3357 init_test(cx, |settings| {
3358 settings.defaults.tab_size = NonZeroU32::new(4)
3359 });
3360
3361 let language = Arc::new(
3362 Language::new(
3363 LanguageConfig::default(),
3364 Some(tree_sitter_rust::LANGUAGE.into()),
3365 )
3366 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3367 .unwrap(),
3368 );
3369
3370 let mut cx = EditorTestContext::new(cx).await;
3371 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3372 cx.set_state(indoc! {"
3373 fn a() {
3374 if b {
3375 \t ˇc
3376 }
3377 }
3378 "});
3379
3380 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3381 cx.assert_editor_state(indoc! {"
3382 fn a() {
3383 if b {
3384 ˇc
3385 }
3386 }
3387 "});
3388}
3389
3390#[gpui::test]
3391async fn test_indent_outdent(cx: &mut TestAppContext) {
3392 init_test(cx, |settings| {
3393 settings.defaults.tab_size = NonZeroU32::new(4);
3394 });
3395
3396 let mut cx = EditorTestContext::new(cx).await;
3397
3398 cx.set_state(indoc! {"
3399 «oneˇ» «twoˇ»
3400 three
3401 four
3402 "});
3403 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3404 cx.assert_editor_state(indoc! {"
3405 «oneˇ» «twoˇ»
3406 three
3407 four
3408 "});
3409
3410 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3411 cx.assert_editor_state(indoc! {"
3412 «oneˇ» «twoˇ»
3413 three
3414 four
3415 "});
3416
3417 // select across line ending
3418 cx.set_state(indoc! {"
3419 one two
3420 t«hree
3421 ˇ» four
3422 "});
3423 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3424 cx.assert_editor_state(indoc! {"
3425 one two
3426 t«hree
3427 ˇ» four
3428 "});
3429
3430 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3431 cx.assert_editor_state(indoc! {"
3432 one two
3433 t«hree
3434 ˇ» four
3435 "});
3436
3437 // Ensure that indenting/outdenting works when the cursor is at column 0.
3438 cx.set_state(indoc! {"
3439 one two
3440 ˇthree
3441 four
3442 "});
3443 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3444 cx.assert_editor_state(indoc! {"
3445 one two
3446 ˇthree
3447 four
3448 "});
3449
3450 cx.set_state(indoc! {"
3451 one two
3452 ˇ three
3453 four
3454 "});
3455 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3456 cx.assert_editor_state(indoc! {"
3457 one two
3458 ˇthree
3459 four
3460 "});
3461}
3462
3463#[gpui::test]
3464async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3465 init_test(cx, |settings| {
3466 settings.defaults.hard_tabs = Some(true);
3467 });
3468
3469 let mut cx = EditorTestContext::new(cx).await;
3470
3471 // select two ranges on one line
3472 cx.set_state(indoc! {"
3473 «oneˇ» «twoˇ»
3474 three
3475 four
3476 "});
3477 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3478 cx.assert_editor_state(indoc! {"
3479 \t«oneˇ» «twoˇ»
3480 three
3481 four
3482 "});
3483 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3484 cx.assert_editor_state(indoc! {"
3485 \t\t«oneˇ» «twoˇ»
3486 three
3487 four
3488 "});
3489 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3490 cx.assert_editor_state(indoc! {"
3491 \t«oneˇ» «twoˇ»
3492 three
3493 four
3494 "});
3495 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3496 cx.assert_editor_state(indoc! {"
3497 «oneˇ» «twoˇ»
3498 three
3499 four
3500 "});
3501
3502 // select across a line ending
3503 cx.set_state(indoc! {"
3504 one two
3505 t«hree
3506 ˇ»four
3507 "});
3508 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3509 cx.assert_editor_state(indoc! {"
3510 one two
3511 \tt«hree
3512 ˇ»four
3513 "});
3514 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3515 cx.assert_editor_state(indoc! {"
3516 one two
3517 \t\tt«hree
3518 ˇ»four
3519 "});
3520 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3521 cx.assert_editor_state(indoc! {"
3522 one two
3523 \tt«hree
3524 ˇ»four
3525 "});
3526 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3527 cx.assert_editor_state(indoc! {"
3528 one two
3529 t«hree
3530 ˇ»four
3531 "});
3532
3533 // Ensure that indenting/outdenting works when the cursor is at column 0.
3534 cx.set_state(indoc! {"
3535 one two
3536 ˇthree
3537 four
3538 "});
3539 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3540 cx.assert_editor_state(indoc! {"
3541 one two
3542 ˇthree
3543 four
3544 "});
3545 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3546 cx.assert_editor_state(indoc! {"
3547 one two
3548 \tˇthree
3549 four
3550 "});
3551 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3552 cx.assert_editor_state(indoc! {"
3553 one two
3554 ˇthree
3555 four
3556 "});
3557}
3558
3559#[gpui::test]
3560fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3561 init_test(cx, |settings| {
3562 settings.languages.extend([
3563 (
3564 "TOML".into(),
3565 LanguageSettingsContent {
3566 tab_size: NonZeroU32::new(2),
3567 ..Default::default()
3568 },
3569 ),
3570 (
3571 "Rust".into(),
3572 LanguageSettingsContent {
3573 tab_size: NonZeroU32::new(4),
3574 ..Default::default()
3575 },
3576 ),
3577 ]);
3578 });
3579
3580 let toml_language = Arc::new(Language::new(
3581 LanguageConfig {
3582 name: "TOML".into(),
3583 ..Default::default()
3584 },
3585 None,
3586 ));
3587 let rust_language = Arc::new(Language::new(
3588 LanguageConfig {
3589 name: "Rust".into(),
3590 ..Default::default()
3591 },
3592 None,
3593 ));
3594
3595 let toml_buffer =
3596 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3597 let rust_buffer =
3598 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3599 let multibuffer = cx.new(|cx| {
3600 let mut multibuffer = MultiBuffer::new(ReadWrite);
3601 multibuffer.push_excerpts(
3602 toml_buffer.clone(),
3603 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3604 cx,
3605 );
3606 multibuffer.push_excerpts(
3607 rust_buffer.clone(),
3608 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3609 cx,
3610 );
3611 multibuffer
3612 });
3613
3614 cx.add_window(|window, cx| {
3615 let mut editor = build_editor(multibuffer, window, cx);
3616
3617 assert_eq!(
3618 editor.text(cx),
3619 indoc! {"
3620 a = 1
3621 b = 2
3622
3623 const c: usize = 3;
3624 "}
3625 );
3626
3627 select_ranges(
3628 &mut editor,
3629 indoc! {"
3630 «aˇ» = 1
3631 b = 2
3632
3633 «const c:ˇ» usize = 3;
3634 "},
3635 window,
3636 cx,
3637 );
3638
3639 editor.tab(&Tab, window, cx);
3640 assert_text_with_selections(
3641 &mut editor,
3642 indoc! {"
3643 «aˇ» = 1
3644 b = 2
3645
3646 «const c:ˇ» usize = 3;
3647 "},
3648 cx,
3649 );
3650 editor.backtab(&Backtab, window, cx);
3651 assert_text_with_selections(
3652 &mut editor,
3653 indoc! {"
3654 «aˇ» = 1
3655 b = 2
3656
3657 «const c:ˇ» usize = 3;
3658 "},
3659 cx,
3660 );
3661
3662 editor
3663 });
3664}
3665
3666#[gpui::test]
3667async fn test_backspace(cx: &mut TestAppContext) {
3668 init_test(cx, |_| {});
3669
3670 let mut cx = EditorTestContext::new(cx).await;
3671
3672 // Basic backspace
3673 cx.set_state(indoc! {"
3674 onˇe two three
3675 fou«rˇ» five six
3676 seven «ˇeight nine
3677 »ten
3678 "});
3679 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3680 cx.assert_editor_state(indoc! {"
3681 oˇe two three
3682 fouˇ five six
3683 seven ˇten
3684 "});
3685
3686 // Test backspace inside and around indents
3687 cx.set_state(indoc! {"
3688 zero
3689 ˇone
3690 ˇtwo
3691 ˇ ˇ ˇ three
3692 ˇ ˇ four
3693 "});
3694 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3695 cx.assert_editor_state(indoc! {"
3696 zero
3697 ˇone
3698 ˇtwo
3699 ˇ threeˇ four
3700 "});
3701}
3702
3703#[gpui::test]
3704async fn test_delete(cx: &mut TestAppContext) {
3705 init_test(cx, |_| {});
3706
3707 let mut cx = EditorTestContext::new(cx).await;
3708 cx.set_state(indoc! {"
3709 onˇe two three
3710 fou«rˇ» five six
3711 seven «ˇeight nine
3712 »ten
3713 "});
3714 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3715 cx.assert_editor_state(indoc! {"
3716 onˇ two three
3717 fouˇ five six
3718 seven ˇten
3719 "});
3720}
3721
3722#[gpui::test]
3723fn test_delete_line(cx: &mut TestAppContext) {
3724 init_test(cx, |_| {});
3725
3726 let editor = cx.add_window(|window, cx| {
3727 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3728 build_editor(buffer, window, cx)
3729 });
3730 _ = editor.update(cx, |editor, window, cx| {
3731 editor.change_selections(None, window, cx, |s| {
3732 s.select_display_ranges([
3733 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3734 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3735 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3736 ])
3737 });
3738 editor.delete_line(&DeleteLine, window, cx);
3739 assert_eq!(editor.display_text(cx), "ghi");
3740 assert_eq!(
3741 editor.selections.display_ranges(cx),
3742 vec![
3743 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3744 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3745 ]
3746 );
3747 });
3748
3749 let editor = cx.add_window(|window, cx| {
3750 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3751 build_editor(buffer, window, cx)
3752 });
3753 _ = editor.update(cx, |editor, window, cx| {
3754 editor.change_selections(None, window, cx, |s| {
3755 s.select_display_ranges([
3756 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3757 ])
3758 });
3759 editor.delete_line(&DeleteLine, window, cx);
3760 assert_eq!(editor.display_text(cx), "ghi\n");
3761 assert_eq!(
3762 editor.selections.display_ranges(cx),
3763 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3764 );
3765 });
3766}
3767
3768#[gpui::test]
3769fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3770 init_test(cx, |_| {});
3771
3772 cx.add_window(|window, cx| {
3773 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3774 let mut editor = build_editor(buffer.clone(), window, cx);
3775 let buffer = buffer.read(cx).as_singleton().unwrap();
3776
3777 assert_eq!(
3778 editor.selections.ranges::<Point>(cx),
3779 &[Point::new(0, 0)..Point::new(0, 0)]
3780 );
3781
3782 // When on single line, replace newline at end by space
3783 editor.join_lines(&JoinLines, window, cx);
3784 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3785 assert_eq!(
3786 editor.selections.ranges::<Point>(cx),
3787 &[Point::new(0, 3)..Point::new(0, 3)]
3788 );
3789
3790 // When multiple lines are selected, remove newlines that are spanned by the selection
3791 editor.change_selections(None, window, cx, |s| {
3792 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3793 });
3794 editor.join_lines(&JoinLines, window, cx);
3795 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3796 assert_eq!(
3797 editor.selections.ranges::<Point>(cx),
3798 &[Point::new(0, 11)..Point::new(0, 11)]
3799 );
3800
3801 // Undo should be transactional
3802 editor.undo(&Undo, window, cx);
3803 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3804 assert_eq!(
3805 editor.selections.ranges::<Point>(cx),
3806 &[Point::new(0, 5)..Point::new(2, 2)]
3807 );
3808
3809 // When joining an empty line don't insert a space
3810 editor.change_selections(None, window, cx, |s| {
3811 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3812 });
3813 editor.join_lines(&JoinLines, window, cx);
3814 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3815 assert_eq!(
3816 editor.selections.ranges::<Point>(cx),
3817 [Point::new(2, 3)..Point::new(2, 3)]
3818 );
3819
3820 // We can remove trailing newlines
3821 editor.join_lines(&JoinLines, window, cx);
3822 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3823 assert_eq!(
3824 editor.selections.ranges::<Point>(cx),
3825 [Point::new(2, 3)..Point::new(2, 3)]
3826 );
3827
3828 // We don't blow up on the last line
3829 editor.join_lines(&JoinLines, window, cx);
3830 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3831 assert_eq!(
3832 editor.selections.ranges::<Point>(cx),
3833 [Point::new(2, 3)..Point::new(2, 3)]
3834 );
3835
3836 // reset to test indentation
3837 editor.buffer.update(cx, |buffer, cx| {
3838 buffer.edit(
3839 [
3840 (Point::new(1, 0)..Point::new(1, 2), " "),
3841 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3842 ],
3843 None,
3844 cx,
3845 )
3846 });
3847
3848 // We remove any leading spaces
3849 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3850 editor.change_selections(None, window, cx, |s| {
3851 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3852 });
3853 editor.join_lines(&JoinLines, window, cx);
3854 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3855
3856 // We don't insert a space for a line containing only spaces
3857 editor.join_lines(&JoinLines, window, cx);
3858 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3859
3860 // We ignore any leading tabs
3861 editor.join_lines(&JoinLines, window, cx);
3862 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3863
3864 editor
3865 });
3866}
3867
3868#[gpui::test]
3869fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3870 init_test(cx, |_| {});
3871
3872 cx.add_window(|window, cx| {
3873 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3874 let mut editor = build_editor(buffer.clone(), window, cx);
3875 let buffer = buffer.read(cx).as_singleton().unwrap();
3876
3877 editor.change_selections(None, window, cx, |s| {
3878 s.select_ranges([
3879 Point::new(0, 2)..Point::new(1, 1),
3880 Point::new(1, 2)..Point::new(1, 2),
3881 Point::new(3, 1)..Point::new(3, 2),
3882 ])
3883 });
3884
3885 editor.join_lines(&JoinLines, window, cx);
3886 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3887
3888 assert_eq!(
3889 editor.selections.ranges::<Point>(cx),
3890 [
3891 Point::new(0, 7)..Point::new(0, 7),
3892 Point::new(1, 3)..Point::new(1, 3)
3893 ]
3894 );
3895 editor
3896 });
3897}
3898
3899#[gpui::test]
3900async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3901 init_test(cx, |_| {});
3902
3903 let mut cx = EditorTestContext::new(cx).await;
3904
3905 let diff_base = r#"
3906 Line 0
3907 Line 1
3908 Line 2
3909 Line 3
3910 "#
3911 .unindent();
3912
3913 cx.set_state(
3914 &r#"
3915 ˇLine 0
3916 Line 1
3917 Line 2
3918 Line 3
3919 "#
3920 .unindent(),
3921 );
3922
3923 cx.set_head_text(&diff_base);
3924 executor.run_until_parked();
3925
3926 // Join lines
3927 cx.update_editor(|editor, window, cx| {
3928 editor.join_lines(&JoinLines, window, cx);
3929 });
3930 executor.run_until_parked();
3931
3932 cx.assert_editor_state(
3933 &r#"
3934 Line 0ˇ Line 1
3935 Line 2
3936 Line 3
3937 "#
3938 .unindent(),
3939 );
3940 // Join again
3941 cx.update_editor(|editor, window, cx| {
3942 editor.join_lines(&JoinLines, window, cx);
3943 });
3944 executor.run_until_parked();
3945
3946 cx.assert_editor_state(
3947 &r#"
3948 Line 0 Line 1ˇ Line 2
3949 Line 3
3950 "#
3951 .unindent(),
3952 );
3953}
3954
3955#[gpui::test]
3956async fn test_custom_newlines_cause_no_false_positive_diffs(
3957 executor: BackgroundExecutor,
3958 cx: &mut TestAppContext,
3959) {
3960 init_test(cx, |_| {});
3961 let mut cx = EditorTestContext::new(cx).await;
3962 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3963 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3964 executor.run_until_parked();
3965
3966 cx.update_editor(|editor, window, cx| {
3967 let snapshot = editor.snapshot(window, cx);
3968 assert_eq!(
3969 snapshot
3970 .buffer_snapshot
3971 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3972 .collect::<Vec<_>>(),
3973 Vec::new(),
3974 "Should not have any diffs for files with custom newlines"
3975 );
3976 });
3977}
3978
3979#[gpui::test]
3980async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3981 init_test(cx, |_| {});
3982
3983 let mut cx = EditorTestContext::new(cx).await;
3984
3985 // Test sort_lines_case_insensitive()
3986 cx.set_state(indoc! {"
3987 «z
3988 y
3989 x
3990 Z
3991 Y
3992 Xˇ»
3993 "});
3994 cx.update_editor(|e, window, cx| {
3995 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3996 });
3997 cx.assert_editor_state(indoc! {"
3998 «x
3999 X
4000 y
4001 Y
4002 z
4003 Zˇ»
4004 "});
4005
4006 // Test reverse_lines()
4007 cx.set_state(indoc! {"
4008 «5
4009 4
4010 3
4011 2
4012 1ˇ»
4013 "});
4014 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4015 cx.assert_editor_state(indoc! {"
4016 «1
4017 2
4018 3
4019 4
4020 5ˇ»
4021 "});
4022
4023 // Skip testing shuffle_line()
4024
4025 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
4026 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
4027
4028 // Don't manipulate when cursor is on single line, but expand the selection
4029 cx.set_state(indoc! {"
4030 ddˇdd
4031 ccc
4032 bb
4033 a
4034 "});
4035 cx.update_editor(|e, window, cx| {
4036 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4037 });
4038 cx.assert_editor_state(indoc! {"
4039 «ddddˇ»
4040 ccc
4041 bb
4042 a
4043 "});
4044
4045 // Basic manipulate case
4046 // Start selection moves to column 0
4047 // End of selection shrinks to fit shorter line
4048 cx.set_state(indoc! {"
4049 dd«d
4050 ccc
4051 bb
4052 aaaaaˇ»
4053 "});
4054 cx.update_editor(|e, window, cx| {
4055 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4056 });
4057 cx.assert_editor_state(indoc! {"
4058 «aaaaa
4059 bb
4060 ccc
4061 dddˇ»
4062 "});
4063
4064 // Manipulate case with newlines
4065 cx.set_state(indoc! {"
4066 dd«d
4067 ccc
4068
4069 bb
4070 aaaaa
4071
4072 ˇ»
4073 "});
4074 cx.update_editor(|e, window, cx| {
4075 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4076 });
4077 cx.assert_editor_state(indoc! {"
4078 «
4079
4080 aaaaa
4081 bb
4082 ccc
4083 dddˇ»
4084
4085 "});
4086
4087 // Adding new line
4088 cx.set_state(indoc! {"
4089 aa«a
4090 bbˇ»b
4091 "});
4092 cx.update_editor(|e, window, cx| {
4093 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
4094 });
4095 cx.assert_editor_state(indoc! {"
4096 «aaa
4097 bbb
4098 added_lineˇ»
4099 "});
4100
4101 // Removing line
4102 cx.set_state(indoc! {"
4103 aa«a
4104 bbbˇ»
4105 "});
4106 cx.update_editor(|e, window, cx| {
4107 e.manipulate_lines(window, cx, |lines| {
4108 lines.pop();
4109 })
4110 });
4111 cx.assert_editor_state(indoc! {"
4112 «aaaˇ»
4113 "});
4114
4115 // Removing all lines
4116 cx.set_state(indoc! {"
4117 aa«a
4118 bbbˇ»
4119 "});
4120 cx.update_editor(|e, window, cx| {
4121 e.manipulate_lines(window, cx, |lines| {
4122 lines.drain(..);
4123 })
4124 });
4125 cx.assert_editor_state(indoc! {"
4126 ˇ
4127 "});
4128}
4129
4130#[gpui::test]
4131async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4132 init_test(cx, |_| {});
4133
4134 let mut cx = EditorTestContext::new(cx).await;
4135
4136 // Consider continuous selection as single selection
4137 cx.set_state(indoc! {"
4138 Aaa«aa
4139 cˇ»c«c
4140 bb
4141 aaaˇ»aa
4142 "});
4143 cx.update_editor(|e, window, cx| {
4144 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4145 });
4146 cx.assert_editor_state(indoc! {"
4147 «Aaaaa
4148 ccc
4149 bb
4150 aaaaaˇ»
4151 "});
4152
4153 cx.set_state(indoc! {"
4154 Aaa«aa
4155 cˇ»c«c
4156 bb
4157 aaaˇ»aa
4158 "});
4159 cx.update_editor(|e, window, cx| {
4160 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4161 });
4162 cx.assert_editor_state(indoc! {"
4163 «Aaaaa
4164 ccc
4165 bbˇ»
4166 "});
4167
4168 // Consider non continuous selection as distinct dedup operations
4169 cx.set_state(indoc! {"
4170 «aaaaa
4171 bb
4172 aaaaa
4173 aaaaaˇ»
4174
4175 aaa«aaˇ»
4176 "});
4177 cx.update_editor(|e, window, cx| {
4178 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4179 });
4180 cx.assert_editor_state(indoc! {"
4181 «aaaaa
4182 bbˇ»
4183
4184 «aaaaaˇ»
4185 "});
4186}
4187
4188#[gpui::test]
4189async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4190 init_test(cx, |_| {});
4191
4192 let mut cx = EditorTestContext::new(cx).await;
4193
4194 cx.set_state(indoc! {"
4195 «Aaa
4196 aAa
4197 Aaaˇ»
4198 "});
4199 cx.update_editor(|e, window, cx| {
4200 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4201 });
4202 cx.assert_editor_state(indoc! {"
4203 «Aaa
4204 aAaˇ»
4205 "});
4206
4207 cx.set_state(indoc! {"
4208 «Aaa
4209 aAa
4210 aaAˇ»
4211 "});
4212 cx.update_editor(|e, window, cx| {
4213 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4214 });
4215 cx.assert_editor_state(indoc! {"
4216 «Aaaˇ»
4217 "});
4218}
4219
4220#[gpui::test]
4221async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
4222 init_test(cx, |_| {});
4223
4224 let mut cx = EditorTestContext::new(cx).await;
4225
4226 // Manipulate with multiple selections on a single line
4227 cx.set_state(indoc! {"
4228 dd«dd
4229 cˇ»c«c
4230 bb
4231 aaaˇ»aa
4232 "});
4233 cx.update_editor(|e, window, cx| {
4234 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4235 });
4236 cx.assert_editor_state(indoc! {"
4237 «aaaaa
4238 bb
4239 ccc
4240 ddddˇ»
4241 "});
4242
4243 // Manipulate with multiple disjoin selections
4244 cx.set_state(indoc! {"
4245 5«
4246 4
4247 3
4248 2
4249 1ˇ»
4250
4251 dd«dd
4252 ccc
4253 bb
4254 aaaˇ»aa
4255 "});
4256 cx.update_editor(|e, window, cx| {
4257 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4258 });
4259 cx.assert_editor_state(indoc! {"
4260 «1
4261 2
4262 3
4263 4
4264 5ˇ»
4265
4266 «aaaaa
4267 bb
4268 ccc
4269 ddddˇ»
4270 "});
4271
4272 // Adding lines on each selection
4273 cx.set_state(indoc! {"
4274 2«
4275 1ˇ»
4276
4277 bb«bb
4278 aaaˇ»aa
4279 "});
4280 cx.update_editor(|e, window, cx| {
4281 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
4282 });
4283 cx.assert_editor_state(indoc! {"
4284 «2
4285 1
4286 added lineˇ»
4287
4288 «bbbb
4289 aaaaa
4290 added lineˇ»
4291 "});
4292
4293 // Removing lines on each selection
4294 cx.set_state(indoc! {"
4295 2«
4296 1ˇ»
4297
4298 bb«bb
4299 aaaˇ»aa
4300 "});
4301 cx.update_editor(|e, window, cx| {
4302 e.manipulate_lines(window, cx, |lines| {
4303 lines.pop();
4304 })
4305 });
4306 cx.assert_editor_state(indoc! {"
4307 «2ˇ»
4308
4309 «bbbbˇ»
4310 "});
4311}
4312
4313#[gpui::test]
4314async fn test_toggle_case(cx: &mut TestAppContext) {
4315 init_test(cx, |_| {});
4316
4317 let mut cx = EditorTestContext::new(cx).await;
4318
4319 // If all lower case -> upper case
4320 cx.set_state(indoc! {"
4321 «hello worldˇ»
4322 "});
4323 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4324 cx.assert_editor_state(indoc! {"
4325 «HELLO WORLDˇ»
4326 "});
4327
4328 // If all upper case -> lower case
4329 cx.set_state(indoc! {"
4330 «HELLO WORLDˇ»
4331 "});
4332 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4333 cx.assert_editor_state(indoc! {"
4334 «hello worldˇ»
4335 "});
4336
4337 // If any upper case characters are identified -> lower case
4338 // This matches JetBrains IDEs
4339 cx.set_state(indoc! {"
4340 «hEllo worldˇ»
4341 "});
4342 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4343 cx.assert_editor_state(indoc! {"
4344 «hello worldˇ»
4345 "});
4346}
4347
4348#[gpui::test]
4349async fn test_manipulate_text(cx: &mut TestAppContext) {
4350 init_test(cx, |_| {});
4351
4352 let mut cx = EditorTestContext::new(cx).await;
4353
4354 // Test convert_to_upper_case()
4355 cx.set_state(indoc! {"
4356 «hello worldˇ»
4357 "});
4358 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4359 cx.assert_editor_state(indoc! {"
4360 «HELLO WORLDˇ»
4361 "});
4362
4363 // Test convert_to_lower_case()
4364 cx.set_state(indoc! {"
4365 «HELLO WORLDˇ»
4366 "});
4367 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4368 cx.assert_editor_state(indoc! {"
4369 «hello worldˇ»
4370 "});
4371
4372 // Test multiple line, single selection case
4373 cx.set_state(indoc! {"
4374 «The quick brown
4375 fox jumps over
4376 the lazy dogˇ»
4377 "});
4378 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4379 cx.assert_editor_state(indoc! {"
4380 «The Quick Brown
4381 Fox Jumps Over
4382 The Lazy Dogˇ»
4383 "});
4384
4385 // Test multiple line, single selection case
4386 cx.set_state(indoc! {"
4387 «The quick brown
4388 fox jumps over
4389 the lazy dogˇ»
4390 "});
4391 cx.update_editor(|e, window, cx| {
4392 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4393 });
4394 cx.assert_editor_state(indoc! {"
4395 «TheQuickBrown
4396 FoxJumpsOver
4397 TheLazyDogˇ»
4398 "});
4399
4400 // From here on out, test more complex cases of manipulate_text()
4401
4402 // Test no selection case - should affect words cursors are in
4403 // Cursor at beginning, middle, and end of word
4404 cx.set_state(indoc! {"
4405 ˇhello big beauˇtiful worldˇ
4406 "});
4407 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4408 cx.assert_editor_state(indoc! {"
4409 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4410 "});
4411
4412 // Test multiple selections on a single line and across multiple lines
4413 cx.set_state(indoc! {"
4414 «Theˇ» quick «brown
4415 foxˇ» jumps «overˇ»
4416 the «lazyˇ» dog
4417 "});
4418 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4419 cx.assert_editor_state(indoc! {"
4420 «THEˇ» quick «BROWN
4421 FOXˇ» jumps «OVERˇ»
4422 the «LAZYˇ» dog
4423 "});
4424
4425 // Test case where text length grows
4426 cx.set_state(indoc! {"
4427 «tschüߡ»
4428 "});
4429 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4430 cx.assert_editor_state(indoc! {"
4431 «TSCHÜSSˇ»
4432 "});
4433
4434 // Test to make sure we don't crash when text shrinks
4435 cx.set_state(indoc! {"
4436 aaa_bbbˇ
4437 "});
4438 cx.update_editor(|e, window, cx| {
4439 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4440 });
4441 cx.assert_editor_state(indoc! {"
4442 «aaaBbbˇ»
4443 "});
4444
4445 // Test to make sure we all aware of the fact that each word can grow and shrink
4446 // Final selections should be aware of this fact
4447 cx.set_state(indoc! {"
4448 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4449 "});
4450 cx.update_editor(|e, window, cx| {
4451 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4452 });
4453 cx.assert_editor_state(indoc! {"
4454 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4455 "});
4456
4457 cx.set_state(indoc! {"
4458 «hElLo, WoRld!ˇ»
4459 "});
4460 cx.update_editor(|e, window, cx| {
4461 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4462 });
4463 cx.assert_editor_state(indoc! {"
4464 «HeLlO, wOrLD!ˇ»
4465 "});
4466}
4467
4468#[gpui::test]
4469fn test_duplicate_line(cx: &mut TestAppContext) {
4470 init_test(cx, |_| {});
4471
4472 let editor = cx.add_window(|window, cx| {
4473 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4474 build_editor(buffer, window, cx)
4475 });
4476 _ = editor.update(cx, |editor, window, cx| {
4477 editor.change_selections(None, window, cx, |s| {
4478 s.select_display_ranges([
4479 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4480 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4481 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4482 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4483 ])
4484 });
4485 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4486 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4487 assert_eq!(
4488 editor.selections.display_ranges(cx),
4489 vec![
4490 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4491 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4492 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4493 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4494 ]
4495 );
4496 });
4497
4498 let editor = cx.add_window(|window, cx| {
4499 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4500 build_editor(buffer, window, cx)
4501 });
4502 _ = editor.update(cx, |editor, window, cx| {
4503 editor.change_selections(None, window, cx, |s| {
4504 s.select_display_ranges([
4505 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4506 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4507 ])
4508 });
4509 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4510 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4511 assert_eq!(
4512 editor.selections.display_ranges(cx),
4513 vec![
4514 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4515 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4516 ]
4517 );
4518 });
4519
4520 // With `move_upwards` the selections stay in place, except for
4521 // the lines inserted above them
4522 let editor = cx.add_window(|window, cx| {
4523 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4524 build_editor(buffer, window, cx)
4525 });
4526 _ = editor.update(cx, |editor, window, cx| {
4527 editor.change_selections(None, window, cx, |s| {
4528 s.select_display_ranges([
4529 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4530 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4531 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4532 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4533 ])
4534 });
4535 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4536 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4537 assert_eq!(
4538 editor.selections.display_ranges(cx),
4539 vec![
4540 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4541 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4542 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4543 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4544 ]
4545 );
4546 });
4547
4548 let editor = cx.add_window(|window, cx| {
4549 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4550 build_editor(buffer, window, cx)
4551 });
4552 _ = editor.update(cx, |editor, window, cx| {
4553 editor.change_selections(None, window, cx, |s| {
4554 s.select_display_ranges([
4555 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4556 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4557 ])
4558 });
4559 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4560 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4561 assert_eq!(
4562 editor.selections.display_ranges(cx),
4563 vec![
4564 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4565 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4566 ]
4567 );
4568 });
4569
4570 let editor = cx.add_window(|window, cx| {
4571 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4572 build_editor(buffer, window, cx)
4573 });
4574 _ = editor.update(cx, |editor, window, cx| {
4575 editor.change_selections(None, window, cx, |s| {
4576 s.select_display_ranges([
4577 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4578 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4579 ])
4580 });
4581 editor.duplicate_selection(&DuplicateSelection, window, cx);
4582 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4583 assert_eq!(
4584 editor.selections.display_ranges(cx),
4585 vec![
4586 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4587 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4588 ]
4589 );
4590 });
4591}
4592
4593#[gpui::test]
4594fn test_move_line_up_down(cx: &mut TestAppContext) {
4595 init_test(cx, |_| {});
4596
4597 let editor = cx.add_window(|window, cx| {
4598 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4599 build_editor(buffer, window, cx)
4600 });
4601 _ = editor.update(cx, |editor, window, cx| {
4602 editor.fold_creases(
4603 vec![
4604 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4605 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4606 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4607 ],
4608 true,
4609 window,
4610 cx,
4611 );
4612 editor.change_selections(None, window, cx, |s| {
4613 s.select_display_ranges([
4614 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4615 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4616 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4617 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4618 ])
4619 });
4620 assert_eq!(
4621 editor.display_text(cx),
4622 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4623 );
4624
4625 editor.move_line_up(&MoveLineUp, window, cx);
4626 assert_eq!(
4627 editor.display_text(cx),
4628 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4629 );
4630 assert_eq!(
4631 editor.selections.display_ranges(cx),
4632 vec![
4633 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4634 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4635 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4636 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4637 ]
4638 );
4639 });
4640
4641 _ = editor.update(cx, |editor, window, cx| {
4642 editor.move_line_down(&MoveLineDown, window, cx);
4643 assert_eq!(
4644 editor.display_text(cx),
4645 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4646 );
4647 assert_eq!(
4648 editor.selections.display_ranges(cx),
4649 vec![
4650 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4651 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4652 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4653 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4654 ]
4655 );
4656 });
4657
4658 _ = editor.update(cx, |editor, window, cx| {
4659 editor.move_line_down(&MoveLineDown, window, cx);
4660 assert_eq!(
4661 editor.display_text(cx),
4662 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4663 );
4664 assert_eq!(
4665 editor.selections.display_ranges(cx),
4666 vec![
4667 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4668 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4669 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4670 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4671 ]
4672 );
4673 });
4674
4675 _ = editor.update(cx, |editor, window, cx| {
4676 editor.move_line_up(&MoveLineUp, window, cx);
4677 assert_eq!(
4678 editor.display_text(cx),
4679 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4680 );
4681 assert_eq!(
4682 editor.selections.display_ranges(cx),
4683 vec![
4684 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4685 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4686 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4687 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4688 ]
4689 );
4690 });
4691}
4692
4693#[gpui::test]
4694fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4695 init_test(cx, |_| {});
4696
4697 let editor = cx.add_window(|window, cx| {
4698 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4699 build_editor(buffer, window, cx)
4700 });
4701 _ = editor.update(cx, |editor, window, cx| {
4702 let snapshot = editor.buffer.read(cx).snapshot(cx);
4703 editor.insert_blocks(
4704 [BlockProperties {
4705 style: BlockStyle::Fixed,
4706 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4707 height: Some(1),
4708 render: Arc::new(|_| div().into_any()),
4709 priority: 0,
4710 render_in_minimap: true,
4711 }],
4712 Some(Autoscroll::fit()),
4713 cx,
4714 );
4715 editor.change_selections(None, window, cx, |s| {
4716 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4717 });
4718 editor.move_line_down(&MoveLineDown, window, cx);
4719 });
4720}
4721
4722#[gpui::test]
4723async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4724 init_test(cx, |_| {});
4725
4726 let mut cx = EditorTestContext::new(cx).await;
4727 cx.set_state(
4728 &"
4729 ˇzero
4730 one
4731 two
4732 three
4733 four
4734 five
4735 "
4736 .unindent(),
4737 );
4738
4739 // Create a four-line block that replaces three lines of text.
4740 cx.update_editor(|editor, window, cx| {
4741 let snapshot = editor.snapshot(window, cx);
4742 let snapshot = &snapshot.buffer_snapshot;
4743 let placement = BlockPlacement::Replace(
4744 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4745 );
4746 editor.insert_blocks(
4747 [BlockProperties {
4748 placement,
4749 height: Some(4),
4750 style: BlockStyle::Sticky,
4751 render: Arc::new(|_| gpui::div().into_any_element()),
4752 priority: 0,
4753 render_in_minimap: true,
4754 }],
4755 None,
4756 cx,
4757 );
4758 });
4759
4760 // Move down so that the cursor touches the block.
4761 cx.update_editor(|editor, window, cx| {
4762 editor.move_down(&Default::default(), window, cx);
4763 });
4764 cx.assert_editor_state(
4765 &"
4766 zero
4767 «one
4768 two
4769 threeˇ»
4770 four
4771 five
4772 "
4773 .unindent(),
4774 );
4775
4776 // Move down past the block.
4777 cx.update_editor(|editor, window, cx| {
4778 editor.move_down(&Default::default(), window, cx);
4779 });
4780 cx.assert_editor_state(
4781 &"
4782 zero
4783 one
4784 two
4785 three
4786 ˇfour
4787 five
4788 "
4789 .unindent(),
4790 );
4791}
4792
4793#[gpui::test]
4794fn test_transpose(cx: &mut TestAppContext) {
4795 init_test(cx, |_| {});
4796
4797 _ = cx.add_window(|window, cx| {
4798 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4799 editor.set_style(EditorStyle::default(), window, cx);
4800 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4801 editor.transpose(&Default::default(), window, cx);
4802 assert_eq!(editor.text(cx), "bac");
4803 assert_eq!(editor.selections.ranges(cx), [2..2]);
4804
4805 editor.transpose(&Default::default(), window, cx);
4806 assert_eq!(editor.text(cx), "bca");
4807 assert_eq!(editor.selections.ranges(cx), [3..3]);
4808
4809 editor.transpose(&Default::default(), window, cx);
4810 assert_eq!(editor.text(cx), "bac");
4811 assert_eq!(editor.selections.ranges(cx), [3..3]);
4812
4813 editor
4814 });
4815
4816 _ = cx.add_window(|window, cx| {
4817 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4818 editor.set_style(EditorStyle::default(), window, cx);
4819 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4820 editor.transpose(&Default::default(), window, cx);
4821 assert_eq!(editor.text(cx), "acb\nde");
4822 assert_eq!(editor.selections.ranges(cx), [3..3]);
4823
4824 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4825 editor.transpose(&Default::default(), window, cx);
4826 assert_eq!(editor.text(cx), "acbd\ne");
4827 assert_eq!(editor.selections.ranges(cx), [5..5]);
4828
4829 editor.transpose(&Default::default(), window, cx);
4830 assert_eq!(editor.text(cx), "acbde\n");
4831 assert_eq!(editor.selections.ranges(cx), [6..6]);
4832
4833 editor.transpose(&Default::default(), window, cx);
4834 assert_eq!(editor.text(cx), "acbd\ne");
4835 assert_eq!(editor.selections.ranges(cx), [6..6]);
4836
4837 editor
4838 });
4839
4840 _ = cx.add_window(|window, cx| {
4841 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4842 editor.set_style(EditorStyle::default(), window, cx);
4843 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4844 editor.transpose(&Default::default(), window, cx);
4845 assert_eq!(editor.text(cx), "bacd\ne");
4846 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4847
4848 editor.transpose(&Default::default(), window, cx);
4849 assert_eq!(editor.text(cx), "bcade\n");
4850 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4851
4852 editor.transpose(&Default::default(), window, cx);
4853 assert_eq!(editor.text(cx), "bcda\ne");
4854 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4855
4856 editor.transpose(&Default::default(), window, cx);
4857 assert_eq!(editor.text(cx), "bcade\n");
4858 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4859
4860 editor.transpose(&Default::default(), window, cx);
4861 assert_eq!(editor.text(cx), "bcaed\n");
4862 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4863
4864 editor
4865 });
4866
4867 _ = cx.add_window(|window, cx| {
4868 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4869 editor.set_style(EditorStyle::default(), window, cx);
4870 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4871 editor.transpose(&Default::default(), window, cx);
4872 assert_eq!(editor.text(cx), "🏀🍐✋");
4873 assert_eq!(editor.selections.ranges(cx), [8..8]);
4874
4875 editor.transpose(&Default::default(), window, cx);
4876 assert_eq!(editor.text(cx), "🏀✋🍐");
4877 assert_eq!(editor.selections.ranges(cx), [11..11]);
4878
4879 editor.transpose(&Default::default(), window, cx);
4880 assert_eq!(editor.text(cx), "🏀🍐✋");
4881 assert_eq!(editor.selections.ranges(cx), [11..11]);
4882
4883 editor
4884 });
4885}
4886
4887#[gpui::test]
4888async fn test_rewrap(cx: &mut TestAppContext) {
4889 init_test(cx, |settings| {
4890 settings.languages.extend([
4891 (
4892 "Markdown".into(),
4893 LanguageSettingsContent {
4894 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4895 ..Default::default()
4896 },
4897 ),
4898 (
4899 "Plain Text".into(),
4900 LanguageSettingsContent {
4901 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4902 ..Default::default()
4903 },
4904 ),
4905 ])
4906 });
4907
4908 let mut cx = EditorTestContext::new(cx).await;
4909
4910 let language_with_c_comments = Arc::new(Language::new(
4911 LanguageConfig {
4912 line_comments: vec!["// ".into()],
4913 ..LanguageConfig::default()
4914 },
4915 None,
4916 ));
4917 let language_with_pound_comments = Arc::new(Language::new(
4918 LanguageConfig {
4919 line_comments: vec!["# ".into()],
4920 ..LanguageConfig::default()
4921 },
4922 None,
4923 ));
4924 let markdown_language = Arc::new(Language::new(
4925 LanguageConfig {
4926 name: "Markdown".into(),
4927 ..LanguageConfig::default()
4928 },
4929 None,
4930 ));
4931 let language_with_doc_comments = Arc::new(Language::new(
4932 LanguageConfig {
4933 line_comments: vec!["// ".into(), "/// ".into()],
4934 ..LanguageConfig::default()
4935 },
4936 Some(tree_sitter_rust::LANGUAGE.into()),
4937 ));
4938
4939 let plaintext_language = Arc::new(Language::new(
4940 LanguageConfig {
4941 name: "Plain Text".into(),
4942 ..LanguageConfig::default()
4943 },
4944 None,
4945 ));
4946
4947 assert_rewrap(
4948 indoc! {"
4949 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4950 "},
4951 indoc! {"
4952 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4953 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4954 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4955 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4956 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4957 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4958 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4959 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4960 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4961 // porttitor id. Aliquam id accumsan eros.
4962 "},
4963 language_with_c_comments.clone(),
4964 &mut cx,
4965 );
4966
4967 // Test that rewrapping works inside of a selection
4968 assert_rewrap(
4969 indoc! {"
4970 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.ˇ»
4971 "},
4972 indoc! {"
4973 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4974 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4975 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4976 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4977 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4978 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4979 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4980 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4981 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4982 // porttitor id. Aliquam id accumsan eros.ˇ»
4983 "},
4984 language_with_c_comments.clone(),
4985 &mut cx,
4986 );
4987
4988 // Test that cursors that expand to the same region are collapsed.
4989 assert_rewrap(
4990 indoc! {"
4991 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4992 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4993 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4994 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4995 "},
4996 indoc! {"
4997 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4998 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4999 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
5000 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
5001 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
5002 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
5003 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
5004 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
5005 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
5006 // porttitor id. Aliquam id accumsan eros.
5007 "},
5008 language_with_c_comments.clone(),
5009 &mut cx,
5010 );
5011
5012 // Test that non-contiguous selections are treated separately.
5013 assert_rewrap(
5014 indoc! {"
5015 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
5016 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
5017 //
5018 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
5019 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
5020 "},
5021 indoc! {"
5022 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
5023 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
5024 // auctor, eu lacinia sapien scelerisque.
5025 //
5026 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
5027 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
5028 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
5029 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
5030 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
5031 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
5032 // vulputate turpis porttitor id. Aliquam id accumsan eros.
5033 "},
5034 language_with_c_comments.clone(),
5035 &mut cx,
5036 );
5037
5038 // Test that different comment prefixes are supported.
5039 assert_rewrap(
5040 indoc! {"
5041 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
5042 "},
5043 indoc! {"
5044 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
5045 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5046 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5047 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5048 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
5049 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
5050 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
5051 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
5052 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
5053 # accumsan eros.
5054 "},
5055 language_with_pound_comments.clone(),
5056 &mut cx,
5057 );
5058
5059 // Test that rewrapping is ignored outside of comments in most languages.
5060 assert_rewrap(
5061 indoc! {"
5062 /// Adds two numbers.
5063 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
5064 fn add(a: u32, b: u32) -> u32 {
5065 a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + bˇ
5066 }
5067 "},
5068 indoc! {"
5069 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
5070 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
5071 fn add(a: u32, b: u32) -> u32 {
5072 a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + bˇ
5073 }
5074 "},
5075 language_with_doc_comments.clone(),
5076 &mut cx,
5077 );
5078
5079 // Test that rewrapping works in Markdown and Plain Text languages.
5080 assert_rewrap(
5081 indoc! {"
5082 # Hello
5083
5084 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi.
5085 "},
5086 indoc! {"
5087 # Hello
5088
5089 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
5090 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5091 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5092 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5093 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
5094 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
5095 Integer sit amet scelerisque nisi.
5096 "},
5097 markdown_language,
5098 &mut cx,
5099 );
5100
5101 assert_rewrap(
5102 indoc! {"
5103 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi.
5104 "},
5105 indoc! {"
5106 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
5107 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5108 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5109 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5110 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
5111 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
5112 Integer sit amet scelerisque nisi.
5113 "},
5114 plaintext_language.clone(),
5115 &mut cx,
5116 );
5117
5118 // Test rewrapping unaligned comments in a selection.
5119 assert_rewrap(
5120 indoc! {"
5121 fn foo() {
5122 if true {
5123 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
5124 // Praesent semper egestas tellus id dignissim.ˇ»
5125 do_something();
5126 } else {
5127 //
5128 }
5129 }
5130 "},
5131 indoc! {"
5132 fn foo() {
5133 if true {
5134 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
5135 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
5136 // egestas tellus id dignissim.ˇ»
5137 do_something();
5138 } else {
5139 //
5140 }
5141 }
5142 "},
5143 language_with_doc_comments.clone(),
5144 &mut cx,
5145 );
5146
5147 assert_rewrap(
5148 indoc! {"
5149 fn foo() {
5150 if true {
5151 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
5152 // Praesent semper egestas tellus id dignissim.»
5153 do_something();
5154 } else {
5155 //
5156 }
5157
5158 }
5159 "},
5160 indoc! {"
5161 fn foo() {
5162 if true {
5163 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
5164 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
5165 // egestas tellus id dignissim.»
5166 do_something();
5167 } else {
5168 //
5169 }
5170
5171 }
5172 "},
5173 language_with_doc_comments.clone(),
5174 &mut cx,
5175 );
5176
5177 assert_rewrap(
5178 indoc! {"
5179 «ˇone one one one one one one one one one one one one one one one one one one one one one one one one
5180
5181 two»
5182
5183 three
5184
5185 «ˇ\t
5186
5187 four four four four four four four four four four four four four four four four four four four four»
5188
5189 «ˇfive five five five five five five five five five five five five five five five five five five five
5190 \t»
5191 six six six six six six six six six six six six six six six six six six six six six six six six six
5192 "},
5193 indoc! {"
5194 «ˇone one one one one one one one one one one one one one one one one one one one
5195 one one one one one
5196
5197 two»
5198
5199 three
5200
5201 «ˇ\t
5202
5203 four four four four four four four four four four four four four four four four
5204 four four four four»
5205
5206 «ˇfive five five five five five five five five five five five five five five five
5207 five five five five
5208 \t»
5209 six six six six six six six six six six six six six six six six six six six six six six six six six
5210 "},
5211 plaintext_language.clone(),
5212 &mut cx,
5213 );
5214
5215 assert_rewrap(
5216 indoc! {"
5217 //ˇ long long long long long long long long long long long long long long long long long long long long long long long long long long long long
5218 //ˇ
5219 //ˇ long long long long long long long long long long long long long long long long long long long long long long long long long long long long
5220 //ˇ short short short
5221 int main(void) {
5222 return 17;
5223 }
5224 "},
5225 indoc! {"
5226 //ˇ long long long long long long long long long long long long long long long
5227 // long long long long long long long long long long long long long
5228 //ˇ
5229 //ˇ long long long long long long long long long long long long long long long
5230 //ˇ long long long long long long long long long long long long long short short
5231 // short
5232 int main(void) {
5233 return 17;
5234 }
5235 "},
5236 language_with_c_comments,
5237 &mut cx,
5238 );
5239
5240 #[track_caller]
5241 fn assert_rewrap(
5242 unwrapped_text: &str,
5243 wrapped_text: &str,
5244 language: Arc<Language>,
5245 cx: &mut EditorTestContext,
5246 ) {
5247 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5248 cx.set_state(unwrapped_text);
5249 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5250 cx.assert_editor_state(wrapped_text);
5251 }
5252}
5253
5254#[gpui::test]
5255async fn test_hard_wrap(cx: &mut TestAppContext) {
5256 init_test(cx, |_| {});
5257 let mut cx = EditorTestContext::new(cx).await;
5258
5259 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5260 cx.update_editor(|editor, _, cx| {
5261 editor.set_hard_wrap(Some(14), cx);
5262 });
5263
5264 cx.set_state(indoc!(
5265 "
5266 one two three ˇ
5267 "
5268 ));
5269 cx.simulate_input("four");
5270 cx.run_until_parked();
5271
5272 cx.assert_editor_state(indoc!(
5273 "
5274 one two three
5275 fourˇ
5276 "
5277 ));
5278
5279 cx.update_editor(|editor, window, cx| {
5280 editor.newline(&Default::default(), window, cx);
5281 });
5282 cx.run_until_parked();
5283 cx.assert_editor_state(indoc!(
5284 "
5285 one two three
5286 four
5287 ˇ
5288 "
5289 ));
5290
5291 cx.simulate_input("five");
5292 cx.run_until_parked();
5293 cx.assert_editor_state(indoc!(
5294 "
5295 one two three
5296 four
5297 fiveˇ
5298 "
5299 ));
5300
5301 cx.update_editor(|editor, window, cx| {
5302 editor.newline(&Default::default(), window, cx);
5303 });
5304 cx.run_until_parked();
5305 cx.simulate_input("# ");
5306 cx.run_until_parked();
5307 cx.assert_editor_state(indoc!(
5308 "
5309 one two three
5310 four
5311 five
5312 # ˇ
5313 "
5314 ));
5315
5316 cx.update_editor(|editor, window, cx| {
5317 editor.newline(&Default::default(), window, cx);
5318 });
5319 cx.run_until_parked();
5320 cx.assert_editor_state(indoc!(
5321 "
5322 one two three
5323 four
5324 five
5325 #\x20
5326 #ˇ
5327 "
5328 ));
5329
5330 cx.simulate_input(" 6");
5331 cx.run_until_parked();
5332 cx.assert_editor_state(indoc!(
5333 "
5334 one two three
5335 four
5336 five
5337 #
5338 # 6ˇ
5339 "
5340 ));
5341}
5342
5343#[gpui::test]
5344async fn test_clipboard(cx: &mut TestAppContext) {
5345 init_test(cx, |_| {});
5346
5347 let mut cx = EditorTestContext::new(cx).await;
5348
5349 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5350 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5351 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5352
5353 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5354 cx.set_state("two ˇfour ˇsix ˇ");
5355 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5356 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5357
5358 // Paste again but with only two cursors. Since the number of cursors doesn't
5359 // match the number of slices in the clipboard, the entire clipboard text
5360 // is pasted at each cursor.
5361 cx.set_state("ˇtwo one✅ four three six five ˇ");
5362 cx.update_editor(|e, window, cx| {
5363 e.handle_input("( ", window, cx);
5364 e.paste(&Paste, window, cx);
5365 e.handle_input(") ", window, cx);
5366 });
5367 cx.assert_editor_state(
5368 &([
5369 "( one✅ ",
5370 "three ",
5371 "five ) ˇtwo one✅ four three six five ( one✅ ",
5372 "three ",
5373 "five ) ˇ",
5374 ]
5375 .join("\n")),
5376 );
5377
5378 // Cut with three selections, one of which is full-line.
5379 cx.set_state(indoc! {"
5380 1«2ˇ»3
5381 4ˇ567
5382 «8ˇ»9"});
5383 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5384 cx.assert_editor_state(indoc! {"
5385 1ˇ3
5386 ˇ9"});
5387
5388 // Paste with three selections, noticing how the copied selection that was full-line
5389 // gets inserted before the second cursor.
5390 cx.set_state(indoc! {"
5391 1ˇ3
5392 9ˇ
5393 «oˇ»ne"});
5394 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5395 cx.assert_editor_state(indoc! {"
5396 12ˇ3
5397 4567
5398 9ˇ
5399 8ˇne"});
5400
5401 // Copy with a single cursor only, which writes the whole line into the clipboard.
5402 cx.set_state(indoc! {"
5403 The quick brown
5404 fox juˇmps over
5405 the lazy dog"});
5406 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5407 assert_eq!(
5408 cx.read_from_clipboard()
5409 .and_then(|item| item.text().as_deref().map(str::to_string)),
5410 Some("fox jumps over\n".to_string())
5411 );
5412
5413 // Paste with three selections, noticing how the copied full-line selection is inserted
5414 // before the empty selections but replaces the selection that is non-empty.
5415 cx.set_state(indoc! {"
5416 Tˇhe quick brown
5417 «foˇ»x jumps over
5418 tˇhe lazy dog"});
5419 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5420 cx.assert_editor_state(indoc! {"
5421 fox jumps over
5422 Tˇhe quick brown
5423 fox jumps over
5424 ˇx jumps over
5425 fox jumps over
5426 tˇhe lazy dog"});
5427}
5428
5429#[gpui::test]
5430async fn test_copy_trim(cx: &mut TestAppContext) {
5431 init_test(cx, |_| {});
5432
5433 let mut cx = EditorTestContext::new(cx).await;
5434 cx.set_state(
5435 r#" «for selection in selections.iter() {
5436 let mut start = selection.start;
5437 let mut end = selection.end;
5438 let is_entire_line = selection.is_empty();
5439 if is_entire_line {
5440 start = Point::new(start.row, 0);ˇ»
5441 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5442 }
5443 "#,
5444 );
5445 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5446 assert_eq!(
5447 cx.read_from_clipboard()
5448 .and_then(|item| item.text().as_deref().map(str::to_string)),
5449 Some(
5450 "for selection in selections.iter() {
5451 let mut start = selection.start;
5452 let mut end = selection.end;
5453 let is_entire_line = selection.is_empty();
5454 if is_entire_line {
5455 start = Point::new(start.row, 0);"
5456 .to_string()
5457 ),
5458 "Regular copying preserves all indentation selected",
5459 );
5460 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5461 assert_eq!(
5462 cx.read_from_clipboard()
5463 .and_then(|item| item.text().as_deref().map(str::to_string)),
5464 Some(
5465 "for selection in selections.iter() {
5466let mut start = selection.start;
5467let mut end = selection.end;
5468let is_entire_line = selection.is_empty();
5469if is_entire_line {
5470 start = Point::new(start.row, 0);"
5471 .to_string()
5472 ),
5473 "Copying with stripping should strip all leading whitespaces"
5474 );
5475
5476 cx.set_state(
5477 r#" « for selection in selections.iter() {
5478 let mut start = selection.start;
5479 let mut end = selection.end;
5480 let is_entire_line = selection.is_empty();
5481 if is_entire_line {
5482 start = Point::new(start.row, 0);ˇ»
5483 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5484 }
5485 "#,
5486 );
5487 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5488 assert_eq!(
5489 cx.read_from_clipboard()
5490 .and_then(|item| item.text().as_deref().map(str::to_string)),
5491 Some(
5492 " for selection in selections.iter() {
5493 let mut start = selection.start;
5494 let mut end = selection.end;
5495 let is_entire_line = selection.is_empty();
5496 if is_entire_line {
5497 start = Point::new(start.row, 0);"
5498 .to_string()
5499 ),
5500 "Regular copying preserves all indentation selected",
5501 );
5502 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5503 assert_eq!(
5504 cx.read_from_clipboard()
5505 .and_then(|item| item.text().as_deref().map(str::to_string)),
5506 Some(
5507 "for selection in selections.iter() {
5508let mut start = selection.start;
5509let mut end = selection.end;
5510let is_entire_line = selection.is_empty();
5511if is_entire_line {
5512 start = Point::new(start.row, 0);"
5513 .to_string()
5514 ),
5515 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5516 );
5517
5518 cx.set_state(
5519 r#" «ˇ for selection in selections.iter() {
5520 let mut start = selection.start;
5521 let mut end = selection.end;
5522 let is_entire_line = selection.is_empty();
5523 if is_entire_line {
5524 start = Point::new(start.row, 0);»
5525 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5526 }
5527 "#,
5528 );
5529 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5530 assert_eq!(
5531 cx.read_from_clipboard()
5532 .and_then(|item| item.text().as_deref().map(str::to_string)),
5533 Some(
5534 " for selection in selections.iter() {
5535 let mut start = selection.start;
5536 let mut end = selection.end;
5537 let is_entire_line = selection.is_empty();
5538 if is_entire_line {
5539 start = Point::new(start.row, 0);"
5540 .to_string()
5541 ),
5542 "Regular copying for reverse selection works the same",
5543 );
5544 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5545 assert_eq!(
5546 cx.read_from_clipboard()
5547 .and_then(|item| item.text().as_deref().map(str::to_string)),
5548 Some(
5549 "for selection in selections.iter() {
5550let mut start = selection.start;
5551let mut end = selection.end;
5552let is_entire_line = selection.is_empty();
5553if is_entire_line {
5554 start = Point::new(start.row, 0);"
5555 .to_string()
5556 ),
5557 "Copying with stripping for reverse selection works the same"
5558 );
5559
5560 cx.set_state(
5561 r#" for selection «in selections.iter() {
5562 let mut start = selection.start;
5563 let mut end = selection.end;
5564 let is_entire_line = selection.is_empty();
5565 if is_entire_line {
5566 start = Point::new(start.row, 0);ˇ»
5567 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5568 }
5569 "#,
5570 );
5571 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5572 assert_eq!(
5573 cx.read_from_clipboard()
5574 .and_then(|item| item.text().as_deref().map(str::to_string)),
5575 Some(
5576 "in selections.iter() {
5577 let mut start = selection.start;
5578 let mut end = selection.end;
5579 let is_entire_line = selection.is_empty();
5580 if is_entire_line {
5581 start = Point::new(start.row, 0);"
5582 .to_string()
5583 ),
5584 "When selecting past the indent, the copying works as usual",
5585 );
5586 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5587 assert_eq!(
5588 cx.read_from_clipboard()
5589 .and_then(|item| item.text().as_deref().map(str::to_string)),
5590 Some(
5591 "in selections.iter() {
5592 let mut start = selection.start;
5593 let mut end = selection.end;
5594 let is_entire_line = selection.is_empty();
5595 if is_entire_line {
5596 start = Point::new(start.row, 0);"
5597 .to_string()
5598 ),
5599 "When selecting past the indent, nothing is trimmed"
5600 );
5601
5602 cx.set_state(
5603 r#" «for selection in selections.iter() {
5604 let mut start = selection.start;
5605
5606 let mut end = selection.end;
5607 let is_entire_line = selection.is_empty();
5608 if is_entire_line {
5609 start = Point::new(start.row, 0);
5610ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
5611 }
5612 "#,
5613 );
5614 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5615 assert_eq!(
5616 cx.read_from_clipboard()
5617 .and_then(|item| item.text().as_deref().map(str::to_string)),
5618 Some(
5619 "for selection in selections.iter() {
5620let mut start = selection.start;
5621
5622let mut end = selection.end;
5623let is_entire_line = selection.is_empty();
5624if is_entire_line {
5625 start = Point::new(start.row, 0);
5626"
5627 .to_string()
5628 ),
5629 "Copying with stripping should ignore empty lines"
5630 );
5631}
5632
5633#[gpui::test]
5634async fn test_paste_multiline(cx: &mut TestAppContext) {
5635 init_test(cx, |_| {});
5636
5637 let mut cx = EditorTestContext::new(cx).await;
5638 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5639
5640 // Cut an indented block, without the leading whitespace.
5641 cx.set_state(indoc! {"
5642 const a: B = (
5643 c(),
5644 «d(
5645 e,
5646 f
5647 )ˇ»
5648 );
5649 "});
5650 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5651 cx.assert_editor_state(indoc! {"
5652 const a: B = (
5653 c(),
5654 ˇ
5655 );
5656 "});
5657
5658 // Paste it at the same position.
5659 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5660 cx.assert_editor_state(indoc! {"
5661 const a: B = (
5662 c(),
5663 d(
5664 e,
5665 f
5666 )ˇ
5667 );
5668 "});
5669
5670 // Paste it at a line with a lower indent level.
5671 cx.set_state(indoc! {"
5672 ˇ
5673 const a: B = (
5674 c(),
5675 );
5676 "});
5677 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5678 cx.assert_editor_state(indoc! {"
5679 d(
5680 e,
5681 f
5682 )ˇ
5683 const a: B = (
5684 c(),
5685 );
5686 "});
5687
5688 // Cut an indented block, with the leading whitespace.
5689 cx.set_state(indoc! {"
5690 const a: B = (
5691 c(),
5692 « d(
5693 e,
5694 f
5695 )
5696 ˇ»);
5697 "});
5698 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5699 cx.assert_editor_state(indoc! {"
5700 const a: B = (
5701 c(),
5702 ˇ);
5703 "});
5704
5705 // Paste it at the same position.
5706 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5707 cx.assert_editor_state(indoc! {"
5708 const a: B = (
5709 c(),
5710 d(
5711 e,
5712 f
5713 )
5714 ˇ);
5715 "});
5716
5717 // Paste it at a line with a higher indent level.
5718 cx.set_state(indoc! {"
5719 const a: B = (
5720 c(),
5721 d(
5722 e,
5723 fˇ
5724 )
5725 );
5726 "});
5727 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5728 cx.assert_editor_state(indoc! {"
5729 const a: B = (
5730 c(),
5731 d(
5732 e,
5733 f d(
5734 e,
5735 f
5736 )
5737 ˇ
5738 )
5739 );
5740 "});
5741
5742 // Copy an indented block, starting mid-line
5743 cx.set_state(indoc! {"
5744 const a: B = (
5745 c(),
5746 somethin«g(
5747 e,
5748 f
5749 )ˇ»
5750 );
5751 "});
5752 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5753
5754 // Paste it on a line with a lower indent level
5755 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
5756 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5757 cx.assert_editor_state(indoc! {"
5758 const a: B = (
5759 c(),
5760 something(
5761 e,
5762 f
5763 )
5764 );
5765 g(
5766 e,
5767 f
5768 )ˇ"});
5769}
5770
5771#[gpui::test]
5772async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
5773 init_test(cx, |_| {});
5774
5775 cx.write_to_clipboard(ClipboardItem::new_string(
5776 " d(\n e\n );\n".into(),
5777 ));
5778
5779 let mut cx = EditorTestContext::new(cx).await;
5780 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5781
5782 cx.set_state(indoc! {"
5783 fn a() {
5784 b();
5785 if c() {
5786 ˇ
5787 }
5788 }
5789 "});
5790
5791 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5792 cx.assert_editor_state(indoc! {"
5793 fn a() {
5794 b();
5795 if c() {
5796 d(
5797 e
5798 );
5799 ˇ
5800 }
5801 }
5802 "});
5803
5804 cx.set_state(indoc! {"
5805 fn a() {
5806 b();
5807 ˇ
5808 }
5809 "});
5810
5811 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5812 cx.assert_editor_state(indoc! {"
5813 fn a() {
5814 b();
5815 d(
5816 e
5817 );
5818 ˇ
5819 }
5820 "});
5821}
5822
5823#[gpui::test]
5824fn test_select_all(cx: &mut TestAppContext) {
5825 init_test(cx, |_| {});
5826
5827 let editor = cx.add_window(|window, cx| {
5828 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
5829 build_editor(buffer, window, cx)
5830 });
5831 _ = editor.update(cx, |editor, window, cx| {
5832 editor.select_all(&SelectAll, window, cx);
5833 assert_eq!(
5834 editor.selections.display_ranges(cx),
5835 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
5836 );
5837 });
5838}
5839
5840#[gpui::test]
5841fn test_select_line(cx: &mut TestAppContext) {
5842 init_test(cx, |_| {});
5843
5844 let editor = cx.add_window(|window, cx| {
5845 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
5846 build_editor(buffer, window, cx)
5847 });
5848 _ = editor.update(cx, |editor, window, cx| {
5849 editor.change_selections(None, window, cx, |s| {
5850 s.select_display_ranges([
5851 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5852 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5853 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5854 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
5855 ])
5856 });
5857 editor.select_line(&SelectLine, window, cx);
5858 assert_eq!(
5859 editor.selections.display_ranges(cx),
5860 vec![
5861 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
5862 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
5863 ]
5864 );
5865 });
5866
5867 _ = editor.update(cx, |editor, window, cx| {
5868 editor.select_line(&SelectLine, window, cx);
5869 assert_eq!(
5870 editor.selections.display_ranges(cx),
5871 vec![
5872 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
5873 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
5874 ]
5875 );
5876 });
5877
5878 _ = editor.update(cx, |editor, window, cx| {
5879 editor.select_line(&SelectLine, window, cx);
5880 assert_eq!(
5881 editor.selections.display_ranges(cx),
5882 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
5883 );
5884 });
5885}
5886
5887#[gpui::test]
5888async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
5889 init_test(cx, |_| {});
5890 let mut cx = EditorTestContext::new(cx).await;
5891
5892 #[track_caller]
5893 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
5894 cx.set_state(initial_state);
5895 cx.update_editor(|e, window, cx| {
5896 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
5897 });
5898 cx.assert_editor_state(expected_state);
5899 }
5900
5901 // Selection starts and ends at the middle of lines, left-to-right
5902 test(
5903 &mut cx,
5904 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
5905 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5906 );
5907 // Same thing, right-to-left
5908 test(
5909 &mut cx,
5910 "aa\nb«b\ncc\ndd\neˇ»e\nff",
5911 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5912 );
5913
5914 // Whole buffer, left-to-right, last line *doesn't* end with newline
5915 test(
5916 &mut cx,
5917 "«ˇaa\nbb\ncc\ndd\nee\nff»",
5918 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5919 );
5920 // Same thing, right-to-left
5921 test(
5922 &mut cx,
5923 "«aa\nbb\ncc\ndd\nee\nffˇ»",
5924 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5925 );
5926
5927 // Whole buffer, left-to-right, last line ends with newline
5928 test(
5929 &mut cx,
5930 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
5931 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5932 );
5933 // Same thing, right-to-left
5934 test(
5935 &mut cx,
5936 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
5937 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5938 );
5939
5940 // Starts at the end of a line, ends at the start of another
5941 test(
5942 &mut cx,
5943 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
5944 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
5945 );
5946}
5947
5948#[gpui::test]
5949async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
5950 init_test(cx, |_| {});
5951
5952 let editor = cx.add_window(|window, cx| {
5953 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
5954 build_editor(buffer, window, cx)
5955 });
5956
5957 // setup
5958 _ = editor.update(cx, |editor, window, cx| {
5959 editor.fold_creases(
5960 vec![
5961 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5962 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5963 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5964 ],
5965 true,
5966 window,
5967 cx,
5968 );
5969 assert_eq!(
5970 editor.display_text(cx),
5971 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5972 );
5973 });
5974
5975 _ = editor.update(cx, |editor, window, cx| {
5976 editor.change_selections(None, window, cx, |s| {
5977 s.select_display_ranges([
5978 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5979 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5980 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5981 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
5982 ])
5983 });
5984 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5985 assert_eq!(
5986 editor.display_text(cx),
5987 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5988 );
5989 });
5990 EditorTestContext::for_editor(editor, cx)
5991 .await
5992 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
5993
5994 _ = editor.update(cx, |editor, window, cx| {
5995 editor.change_selections(None, window, cx, |s| {
5996 s.select_display_ranges([
5997 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
5998 ])
5999 });
6000 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6001 assert_eq!(
6002 editor.display_text(cx),
6003 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
6004 );
6005 assert_eq!(
6006 editor.selections.display_ranges(cx),
6007 [
6008 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
6009 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
6010 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
6011 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
6012 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
6013 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
6014 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
6015 ]
6016 );
6017 });
6018 EditorTestContext::for_editor(editor, cx)
6019 .await
6020 .assert_editor_state(
6021 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
6022 );
6023}
6024
6025#[gpui::test]
6026async fn test_add_selection_above_below(cx: &mut TestAppContext) {
6027 init_test(cx, |_| {});
6028
6029 let mut cx = EditorTestContext::new(cx).await;
6030
6031 cx.set_state(indoc!(
6032 r#"abc
6033 defˇghi
6034
6035 jk
6036 nlmo
6037 "#
6038 ));
6039
6040 cx.update_editor(|editor, window, cx| {
6041 editor.add_selection_above(&Default::default(), window, cx);
6042 });
6043
6044 cx.assert_editor_state(indoc!(
6045 r#"abcˇ
6046 defˇghi
6047
6048 jk
6049 nlmo
6050 "#
6051 ));
6052
6053 cx.update_editor(|editor, window, cx| {
6054 editor.add_selection_above(&Default::default(), window, cx);
6055 });
6056
6057 cx.assert_editor_state(indoc!(
6058 r#"abcˇ
6059 defˇghi
6060
6061 jk
6062 nlmo
6063 "#
6064 ));
6065
6066 cx.update_editor(|editor, window, cx| {
6067 editor.add_selection_below(&Default::default(), window, cx);
6068 });
6069
6070 cx.assert_editor_state(indoc!(
6071 r#"abc
6072 defˇghi
6073
6074 jk
6075 nlmo
6076 "#
6077 ));
6078
6079 cx.update_editor(|editor, window, cx| {
6080 editor.undo_selection(&Default::default(), window, cx);
6081 });
6082
6083 cx.assert_editor_state(indoc!(
6084 r#"abcˇ
6085 defˇghi
6086
6087 jk
6088 nlmo
6089 "#
6090 ));
6091
6092 cx.update_editor(|editor, window, cx| {
6093 editor.redo_selection(&Default::default(), window, cx);
6094 });
6095
6096 cx.assert_editor_state(indoc!(
6097 r#"abc
6098 defˇghi
6099
6100 jk
6101 nlmo
6102 "#
6103 ));
6104
6105 cx.update_editor(|editor, window, cx| {
6106 editor.add_selection_below(&Default::default(), window, cx);
6107 });
6108
6109 cx.assert_editor_state(indoc!(
6110 r#"abc
6111 defˇghi
6112 ˇ
6113 jk
6114 nlmo
6115 "#
6116 ));
6117
6118 cx.update_editor(|editor, window, cx| {
6119 editor.add_selection_below(&Default::default(), window, cx);
6120 });
6121
6122 cx.assert_editor_state(indoc!(
6123 r#"abc
6124 defˇghi
6125 ˇ
6126 jkˇ
6127 nlmo
6128 "#
6129 ));
6130
6131 cx.update_editor(|editor, window, cx| {
6132 editor.add_selection_below(&Default::default(), window, cx);
6133 });
6134
6135 cx.assert_editor_state(indoc!(
6136 r#"abc
6137 defˇghi
6138 ˇ
6139 jkˇ
6140 nlmˇo
6141 "#
6142 ));
6143
6144 cx.update_editor(|editor, window, cx| {
6145 editor.add_selection_below(&Default::default(), window, cx);
6146 });
6147
6148 cx.assert_editor_state(indoc!(
6149 r#"abc
6150 defˇghi
6151 ˇ
6152 jkˇ
6153 nlmˇo
6154 ˇ"#
6155 ));
6156
6157 // change selections
6158 cx.set_state(indoc!(
6159 r#"abc
6160 def«ˇg»hi
6161
6162 jk
6163 nlmo
6164 "#
6165 ));
6166
6167 cx.update_editor(|editor, window, cx| {
6168 editor.add_selection_below(&Default::default(), window, cx);
6169 });
6170
6171 cx.assert_editor_state(indoc!(
6172 r#"abc
6173 def«ˇg»hi
6174
6175 jk
6176 nlm«ˇo»
6177 "#
6178 ));
6179
6180 cx.update_editor(|editor, window, cx| {
6181 editor.add_selection_below(&Default::default(), window, cx);
6182 });
6183
6184 cx.assert_editor_state(indoc!(
6185 r#"abc
6186 def«ˇg»hi
6187
6188 jk
6189 nlm«ˇo»
6190 "#
6191 ));
6192
6193 cx.update_editor(|editor, window, cx| {
6194 editor.add_selection_above(&Default::default(), window, cx);
6195 });
6196
6197 cx.assert_editor_state(indoc!(
6198 r#"abc
6199 def«ˇg»hi
6200
6201 jk
6202 nlmo
6203 "#
6204 ));
6205
6206 cx.update_editor(|editor, window, cx| {
6207 editor.add_selection_above(&Default::default(), window, cx);
6208 });
6209
6210 cx.assert_editor_state(indoc!(
6211 r#"abc
6212 def«ˇg»hi
6213
6214 jk
6215 nlmo
6216 "#
6217 ));
6218
6219 // Change selections again
6220 cx.set_state(indoc!(
6221 r#"a«bc
6222 defgˇ»hi
6223
6224 jk
6225 nlmo
6226 "#
6227 ));
6228
6229 cx.update_editor(|editor, window, cx| {
6230 editor.add_selection_below(&Default::default(), window, cx);
6231 });
6232
6233 cx.assert_editor_state(indoc!(
6234 r#"a«bcˇ»
6235 d«efgˇ»hi
6236
6237 j«kˇ»
6238 nlmo
6239 "#
6240 ));
6241
6242 cx.update_editor(|editor, window, cx| {
6243 editor.add_selection_below(&Default::default(), window, cx);
6244 });
6245 cx.assert_editor_state(indoc!(
6246 r#"a«bcˇ»
6247 d«efgˇ»hi
6248
6249 j«kˇ»
6250 n«lmoˇ»
6251 "#
6252 ));
6253 cx.update_editor(|editor, window, cx| {
6254 editor.add_selection_above(&Default::default(), window, cx);
6255 });
6256
6257 cx.assert_editor_state(indoc!(
6258 r#"a«bcˇ»
6259 d«efgˇ»hi
6260
6261 j«kˇ»
6262 nlmo
6263 "#
6264 ));
6265
6266 // Change selections again
6267 cx.set_state(indoc!(
6268 r#"abc
6269 d«ˇefghi
6270
6271 jk
6272 nlm»o
6273 "#
6274 ));
6275
6276 cx.update_editor(|editor, window, cx| {
6277 editor.add_selection_above(&Default::default(), window, cx);
6278 });
6279
6280 cx.assert_editor_state(indoc!(
6281 r#"a«ˇbc»
6282 d«ˇef»ghi
6283
6284 j«ˇk»
6285 n«ˇlm»o
6286 "#
6287 ));
6288
6289 cx.update_editor(|editor, window, cx| {
6290 editor.add_selection_below(&Default::default(), window, cx);
6291 });
6292
6293 cx.assert_editor_state(indoc!(
6294 r#"abc
6295 d«ˇef»ghi
6296
6297 j«ˇk»
6298 n«ˇlm»o
6299 "#
6300 ));
6301}
6302
6303#[gpui::test]
6304async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
6305 init_test(cx, |_| {});
6306 let mut cx = EditorTestContext::new(cx).await;
6307
6308 cx.set_state(indoc!(
6309 r#"line onˇe
6310 liˇne two
6311 line three
6312 line four"#
6313 ));
6314
6315 cx.update_editor(|editor, window, cx| {
6316 editor.add_selection_below(&Default::default(), window, cx);
6317 });
6318
6319 // test multiple cursors expand in the same direction
6320 cx.assert_editor_state(indoc!(
6321 r#"line onˇe
6322 liˇne twˇo
6323 liˇne three
6324 line four"#
6325 ));
6326
6327 cx.update_editor(|editor, window, cx| {
6328 editor.add_selection_below(&Default::default(), window, cx);
6329 });
6330
6331 cx.update_editor(|editor, window, cx| {
6332 editor.add_selection_below(&Default::default(), window, cx);
6333 });
6334
6335 // test multiple cursors expand below overflow
6336 cx.assert_editor_state(indoc!(
6337 r#"line onˇe
6338 liˇne twˇo
6339 liˇne thˇree
6340 liˇne foˇur"#
6341 ));
6342
6343 cx.update_editor(|editor, window, cx| {
6344 editor.add_selection_above(&Default::default(), window, cx);
6345 });
6346
6347 // test multiple cursors retrieves back correctly
6348 cx.assert_editor_state(indoc!(
6349 r#"line onˇe
6350 liˇne twˇo
6351 liˇne thˇree
6352 line four"#
6353 ));
6354
6355 cx.update_editor(|editor, window, cx| {
6356 editor.add_selection_above(&Default::default(), window, cx);
6357 });
6358
6359 cx.update_editor(|editor, window, cx| {
6360 editor.add_selection_above(&Default::default(), window, cx);
6361 });
6362
6363 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
6364 cx.assert_editor_state(indoc!(
6365 r#"liˇne onˇe
6366 liˇne two
6367 line three
6368 line four"#
6369 ));
6370
6371 cx.update_editor(|editor, window, cx| {
6372 editor.undo_selection(&Default::default(), window, cx);
6373 });
6374
6375 // test undo
6376 cx.assert_editor_state(indoc!(
6377 r#"line onˇe
6378 liˇne twˇo
6379 line three
6380 line four"#
6381 ));
6382
6383 cx.update_editor(|editor, window, cx| {
6384 editor.redo_selection(&Default::default(), window, cx);
6385 });
6386
6387 // test redo
6388 cx.assert_editor_state(indoc!(
6389 r#"liˇne onˇe
6390 liˇne two
6391 line three
6392 line four"#
6393 ));
6394
6395 cx.set_state(indoc!(
6396 r#"abcd
6397 ef«ghˇ»
6398 ijkl
6399 «mˇ»nop"#
6400 ));
6401
6402 cx.update_editor(|editor, window, cx| {
6403 editor.add_selection_above(&Default::default(), window, cx);
6404 });
6405
6406 // test multiple selections expand in the same direction
6407 cx.assert_editor_state(indoc!(
6408 r#"ab«cdˇ»
6409 ef«ghˇ»
6410 «iˇ»jkl
6411 «mˇ»nop"#
6412 ));
6413
6414 cx.update_editor(|editor, window, cx| {
6415 editor.add_selection_above(&Default::default(), window, cx);
6416 });
6417
6418 // test multiple selection upward overflow
6419 cx.assert_editor_state(indoc!(
6420 r#"ab«cdˇ»
6421 «eˇ»f«ghˇ»
6422 «iˇ»jkl
6423 «mˇ»nop"#
6424 ));
6425
6426 cx.update_editor(|editor, window, cx| {
6427 editor.add_selection_below(&Default::default(), window, cx);
6428 });
6429
6430 // test multiple selection retrieves back correctly
6431 cx.assert_editor_state(indoc!(
6432 r#"abcd
6433 ef«ghˇ»
6434 «iˇ»jkl
6435 «mˇ»nop"#
6436 ));
6437
6438 cx.update_editor(|editor, window, cx| {
6439 editor.add_selection_below(&Default::default(), window, cx);
6440 });
6441
6442 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
6443 cx.assert_editor_state(indoc!(
6444 r#"abcd
6445 ef«ghˇ»
6446 ij«klˇ»
6447 «mˇ»nop"#
6448 ));
6449
6450 cx.update_editor(|editor, window, cx| {
6451 editor.undo_selection(&Default::default(), window, cx);
6452 });
6453
6454 // test undo
6455 cx.assert_editor_state(indoc!(
6456 r#"abcd
6457 ef«ghˇ»
6458 «iˇ»jkl
6459 «mˇ»nop"#
6460 ));
6461
6462 cx.update_editor(|editor, window, cx| {
6463 editor.redo_selection(&Default::default(), window, cx);
6464 });
6465
6466 // test redo
6467 cx.assert_editor_state(indoc!(
6468 r#"abcd
6469 ef«ghˇ»
6470 ij«klˇ»
6471 «mˇ»nop"#
6472 ));
6473}
6474
6475#[gpui::test]
6476async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
6477 init_test(cx, |_| {});
6478 let mut cx = EditorTestContext::new(cx).await;
6479
6480 cx.set_state(indoc!(
6481 r#"line onˇe
6482 liˇne two
6483 line three
6484 line four"#
6485 ));
6486
6487 cx.update_editor(|editor, window, cx| {
6488 editor.add_selection_below(&Default::default(), window, cx);
6489 editor.add_selection_below(&Default::default(), window, cx);
6490 editor.add_selection_below(&Default::default(), window, cx);
6491 });
6492
6493 // initial state with two multi cursor groups
6494 cx.assert_editor_state(indoc!(
6495 r#"line onˇe
6496 liˇne twˇo
6497 liˇne thˇree
6498 liˇne foˇur"#
6499 ));
6500
6501 // add single cursor in middle - simulate opt click
6502 cx.update_editor(|editor, window, cx| {
6503 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
6504 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6505 editor.end_selection(window, cx);
6506 });
6507
6508 cx.assert_editor_state(indoc!(
6509 r#"line onˇe
6510 liˇne twˇo
6511 liˇneˇ thˇree
6512 liˇne foˇur"#
6513 ));
6514
6515 cx.update_editor(|editor, window, cx| {
6516 editor.add_selection_above(&Default::default(), window, cx);
6517 });
6518
6519 // test new added selection expands above and existing selection shrinks
6520 cx.assert_editor_state(indoc!(
6521 r#"line onˇe
6522 liˇneˇ twˇo
6523 liˇneˇ thˇree
6524 line four"#
6525 ));
6526
6527 cx.update_editor(|editor, window, cx| {
6528 editor.add_selection_above(&Default::default(), window, cx);
6529 });
6530
6531 // test new added selection expands above and existing selection shrinks
6532 cx.assert_editor_state(indoc!(
6533 r#"lineˇ onˇe
6534 liˇneˇ twˇo
6535 lineˇ three
6536 line four"#
6537 ));
6538
6539 // intial state with two selection groups
6540 cx.set_state(indoc!(
6541 r#"abcd
6542 ef«ghˇ»
6543 ijkl
6544 «mˇ»nop"#
6545 ));
6546
6547 cx.update_editor(|editor, window, cx| {
6548 editor.add_selection_above(&Default::default(), window, cx);
6549 editor.add_selection_above(&Default::default(), window, cx);
6550 });
6551
6552 cx.assert_editor_state(indoc!(
6553 r#"ab«cdˇ»
6554 «eˇ»f«ghˇ»
6555 «iˇ»jkl
6556 «mˇ»nop"#
6557 ));
6558
6559 // add single selection in middle - simulate opt drag
6560 cx.update_editor(|editor, window, cx| {
6561 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
6562 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6563 editor.update_selection(
6564 DisplayPoint::new(DisplayRow(2), 4),
6565 0,
6566 gpui::Point::<f32>::default(),
6567 window,
6568 cx,
6569 );
6570 editor.end_selection(window, cx);
6571 });
6572
6573 cx.assert_editor_state(indoc!(
6574 r#"ab«cdˇ»
6575 «eˇ»f«ghˇ»
6576 «iˇ»jk«lˇ»
6577 «mˇ»nop"#
6578 ));
6579
6580 cx.update_editor(|editor, window, cx| {
6581 editor.add_selection_below(&Default::default(), window, cx);
6582 });
6583
6584 // test new added selection expands below, others shrinks from above
6585 cx.assert_editor_state(indoc!(
6586 r#"abcd
6587 ef«ghˇ»
6588 «iˇ»jk«lˇ»
6589 «mˇ»no«pˇ»"#
6590 ));
6591}
6592
6593#[gpui::test]
6594async fn test_select_next(cx: &mut TestAppContext) {
6595 init_test(cx, |_| {});
6596
6597 let mut cx = EditorTestContext::new(cx).await;
6598 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6599
6600 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6601 .unwrap();
6602 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6603
6604 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6605 .unwrap();
6606 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6607
6608 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6609 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6610
6611 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6612 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6613
6614 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6615 .unwrap();
6616 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6617
6618 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6619 .unwrap();
6620 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6621
6622 // Test selection direction should be preserved
6623 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6624
6625 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6626 .unwrap();
6627 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
6628}
6629
6630#[gpui::test]
6631async fn test_select_all_matches(cx: &mut TestAppContext) {
6632 init_test(cx, |_| {});
6633
6634 let mut cx = EditorTestContext::new(cx).await;
6635
6636 // Test caret-only selections
6637 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6638 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6639 .unwrap();
6640 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6641
6642 // Test left-to-right selections
6643 cx.set_state("abc\n«abcˇ»\nabc");
6644 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6645 .unwrap();
6646 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
6647
6648 // Test right-to-left selections
6649 cx.set_state("abc\n«ˇabc»\nabc");
6650 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6651 .unwrap();
6652 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
6653
6654 // Test selecting whitespace with caret selection
6655 cx.set_state("abc\nˇ abc\nabc");
6656 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6657 .unwrap();
6658 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6659
6660 // Test selecting whitespace with left-to-right selection
6661 cx.set_state("abc\n«ˇ »abc\nabc");
6662 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6663 .unwrap();
6664 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
6665
6666 // Test no matches with right-to-left selection
6667 cx.set_state("abc\n« ˇ»abc\nabc");
6668 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6669 .unwrap();
6670 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6671}
6672
6673#[gpui::test]
6674async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
6675 init_test(cx, |_| {});
6676
6677 let mut cx = EditorTestContext::new(cx).await;
6678
6679 let large_body_1 = "\nd".repeat(200);
6680 let large_body_2 = "\ne".repeat(200);
6681
6682 cx.set_state(&format!(
6683 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
6684 ));
6685 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
6686 let scroll_position = editor.scroll_position(cx);
6687 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
6688 scroll_position
6689 });
6690
6691 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6692 .unwrap();
6693 cx.assert_editor_state(&format!(
6694 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
6695 ));
6696 let scroll_position_after_selection =
6697 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
6698 assert_eq!(
6699 initial_scroll_position, scroll_position_after_selection,
6700 "Scroll position should not change after selecting all matches"
6701 );
6702}
6703
6704#[gpui::test]
6705async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
6706 init_test(cx, |_| {});
6707
6708 let mut cx = EditorLspTestContext::new_rust(
6709 lsp::ServerCapabilities {
6710 document_formatting_provider: Some(lsp::OneOf::Left(true)),
6711 ..Default::default()
6712 },
6713 cx,
6714 )
6715 .await;
6716
6717 cx.set_state(indoc! {"
6718 line 1
6719 line 2
6720 linˇe 3
6721 line 4
6722 line 5
6723 "});
6724
6725 // Make an edit
6726 cx.update_editor(|editor, window, cx| {
6727 editor.handle_input("X", window, cx);
6728 });
6729
6730 // Move cursor to a different position
6731 cx.update_editor(|editor, window, cx| {
6732 editor.change_selections(None, window, cx, |s| {
6733 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
6734 });
6735 });
6736
6737 cx.assert_editor_state(indoc! {"
6738 line 1
6739 line 2
6740 linXe 3
6741 line 4
6742 liˇne 5
6743 "});
6744
6745 cx.lsp
6746 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
6747 Ok(Some(vec![lsp::TextEdit::new(
6748 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
6749 "PREFIX ".to_string(),
6750 )]))
6751 });
6752
6753 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
6754 .unwrap()
6755 .await
6756 .unwrap();
6757
6758 cx.assert_editor_state(indoc! {"
6759 PREFIX line 1
6760 line 2
6761 linXe 3
6762 line 4
6763 liˇne 5
6764 "});
6765
6766 // Undo formatting
6767 cx.update_editor(|editor, window, cx| {
6768 editor.undo(&Default::default(), window, cx);
6769 });
6770
6771 // Verify cursor moved back to position after edit
6772 cx.assert_editor_state(indoc! {"
6773 line 1
6774 line 2
6775 linXˇe 3
6776 line 4
6777 line 5
6778 "});
6779}
6780
6781#[gpui::test]
6782async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext) {
6783 init_test(cx, |_| {});
6784
6785 let mut cx = EditorTestContext::new(cx).await;
6786
6787 let provider = cx.new(|_| FakeInlineCompletionProvider::default());
6788 cx.update_editor(|editor, window, cx| {
6789 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
6790 });
6791
6792 cx.set_state(indoc! {"
6793 line 1
6794 line 2
6795 linˇe 3
6796 line 4
6797 line 5
6798 line 6
6799 line 7
6800 line 8
6801 line 9
6802 line 10
6803 "});
6804
6805 let snapshot = cx.buffer_snapshot();
6806 let edit_position = snapshot.anchor_after(Point::new(2, 4));
6807
6808 cx.update(|_, cx| {
6809 provider.update(cx, |provider, _| {
6810 provider.set_inline_completion(Some(inline_completion::InlineCompletion {
6811 id: None,
6812 edits: vec![(edit_position..edit_position, "X".into())],
6813 edit_preview: None,
6814 }))
6815 })
6816 });
6817
6818 cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx));
6819 cx.update_editor(|editor, window, cx| {
6820 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
6821 });
6822
6823 cx.assert_editor_state(indoc! {"
6824 line 1
6825 line 2
6826 lineXˇ 3
6827 line 4
6828 line 5
6829 line 6
6830 line 7
6831 line 8
6832 line 9
6833 line 10
6834 "});
6835
6836 cx.update_editor(|editor, window, cx| {
6837 editor.change_selections(None, window, cx, |s| {
6838 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
6839 });
6840 });
6841
6842 cx.assert_editor_state(indoc! {"
6843 line 1
6844 line 2
6845 lineX 3
6846 line 4
6847 line 5
6848 line 6
6849 line 7
6850 line 8
6851 line 9
6852 liˇne 10
6853 "});
6854
6855 cx.update_editor(|editor, window, cx| {
6856 editor.undo(&Default::default(), window, cx);
6857 });
6858
6859 cx.assert_editor_state(indoc! {"
6860 line 1
6861 line 2
6862 lineˇ 3
6863 line 4
6864 line 5
6865 line 6
6866 line 7
6867 line 8
6868 line 9
6869 line 10
6870 "});
6871}
6872
6873#[gpui::test]
6874async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
6875 init_test(cx, |_| {});
6876
6877 let mut cx = EditorTestContext::new(cx).await;
6878 cx.set_state(
6879 r#"let foo = 2;
6880lˇet foo = 2;
6881let fooˇ = 2;
6882let foo = 2;
6883let foo = ˇ2;"#,
6884 );
6885
6886 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6887 .unwrap();
6888 cx.assert_editor_state(
6889 r#"let foo = 2;
6890«letˇ» foo = 2;
6891let «fooˇ» = 2;
6892let foo = 2;
6893let foo = «2ˇ»;"#,
6894 );
6895
6896 // noop for multiple selections with different contents
6897 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6898 .unwrap();
6899 cx.assert_editor_state(
6900 r#"let foo = 2;
6901«letˇ» foo = 2;
6902let «fooˇ» = 2;
6903let foo = 2;
6904let foo = «2ˇ»;"#,
6905 );
6906
6907 // Test last selection direction should be preserved
6908 cx.set_state(
6909 r#"let foo = 2;
6910let foo = 2;
6911let «fooˇ» = 2;
6912let «ˇfoo» = 2;
6913let foo = 2;"#,
6914 );
6915
6916 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6917 .unwrap();
6918 cx.assert_editor_state(
6919 r#"let foo = 2;
6920let foo = 2;
6921let «fooˇ» = 2;
6922let «ˇfoo» = 2;
6923let «ˇfoo» = 2;"#,
6924 );
6925}
6926
6927#[gpui::test]
6928async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
6929 init_test(cx, |_| {});
6930
6931 let mut cx =
6932 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
6933
6934 cx.assert_editor_state(indoc! {"
6935 ˇbbb
6936 ccc
6937
6938 bbb
6939 ccc
6940 "});
6941 cx.dispatch_action(SelectPrevious::default());
6942 cx.assert_editor_state(indoc! {"
6943 «bbbˇ»
6944 ccc
6945
6946 bbb
6947 ccc
6948 "});
6949 cx.dispatch_action(SelectPrevious::default());
6950 cx.assert_editor_state(indoc! {"
6951 «bbbˇ»
6952 ccc
6953
6954 «bbbˇ»
6955 ccc
6956 "});
6957}
6958
6959#[gpui::test]
6960async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
6961 init_test(cx, |_| {});
6962
6963 let mut cx = EditorTestContext::new(cx).await;
6964 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6965
6966 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6967 .unwrap();
6968 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6969
6970 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6971 .unwrap();
6972 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6973
6974 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6975 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6976
6977 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6978 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6979
6980 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6981 .unwrap();
6982 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
6983
6984 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6985 .unwrap();
6986 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6987}
6988
6989#[gpui::test]
6990async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
6991 init_test(cx, |_| {});
6992
6993 let mut cx = EditorTestContext::new(cx).await;
6994 cx.set_state("aˇ");
6995
6996 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6997 .unwrap();
6998 cx.assert_editor_state("«aˇ»");
6999 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7000 .unwrap();
7001 cx.assert_editor_state("«aˇ»");
7002}
7003
7004#[gpui::test]
7005async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
7006 init_test(cx, |_| {});
7007
7008 let mut cx = EditorTestContext::new(cx).await;
7009 cx.set_state(
7010 r#"let foo = 2;
7011lˇet foo = 2;
7012let fooˇ = 2;
7013let foo = 2;
7014let foo = ˇ2;"#,
7015 );
7016
7017 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7018 .unwrap();
7019 cx.assert_editor_state(
7020 r#"let foo = 2;
7021«letˇ» foo = 2;
7022let «fooˇ» = 2;
7023let foo = 2;
7024let foo = «2ˇ»;"#,
7025 );
7026
7027 // noop for multiple selections with different contents
7028 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7029 .unwrap();
7030 cx.assert_editor_state(
7031 r#"let foo = 2;
7032«letˇ» foo = 2;
7033let «fooˇ» = 2;
7034let foo = 2;
7035let foo = «2ˇ»;"#,
7036 );
7037}
7038
7039#[gpui::test]
7040async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
7041 init_test(cx, |_| {});
7042
7043 let mut cx = EditorTestContext::new(cx).await;
7044 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7045
7046 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7047 .unwrap();
7048 // selection direction is preserved
7049 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7050
7051 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7052 .unwrap();
7053 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7054
7055 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7056 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7057
7058 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7059 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7060
7061 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7062 .unwrap();
7063 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
7064
7065 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7066 .unwrap();
7067 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
7068}
7069
7070#[gpui::test]
7071async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
7072 init_test(cx, |_| {});
7073
7074 let language = Arc::new(Language::new(
7075 LanguageConfig::default(),
7076 Some(tree_sitter_rust::LANGUAGE.into()),
7077 ));
7078
7079 let text = r#"
7080 use mod1::mod2::{mod3, mod4};
7081
7082 fn fn_1(param1: bool, param2: &str) {
7083 let var1 = "text";
7084 }
7085 "#
7086 .unindent();
7087
7088 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7089 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7090 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7091
7092 editor
7093 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7094 .await;
7095
7096 editor.update_in(cx, |editor, window, cx| {
7097 editor.change_selections(None, window, cx, |s| {
7098 s.select_display_ranges([
7099 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
7100 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
7101 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
7102 ]);
7103 });
7104 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7105 });
7106 editor.update(cx, |editor, cx| {
7107 assert_text_with_selections(
7108 editor,
7109 indoc! {r#"
7110 use mod1::mod2::{mod3, «mod4ˇ»};
7111
7112 fn fn_1«ˇ(param1: bool, param2: &str)» {
7113 let var1 = "«ˇtext»";
7114 }
7115 "#},
7116 cx,
7117 );
7118 });
7119
7120 editor.update_in(cx, |editor, window, cx| {
7121 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7122 });
7123 editor.update(cx, |editor, cx| {
7124 assert_text_with_selections(
7125 editor,
7126 indoc! {r#"
7127 use mod1::mod2::«{mod3, mod4}ˇ»;
7128
7129 «ˇfn fn_1(param1: bool, param2: &str) {
7130 let var1 = "text";
7131 }»
7132 "#},
7133 cx,
7134 );
7135 });
7136
7137 editor.update_in(cx, |editor, window, cx| {
7138 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7139 });
7140 assert_eq!(
7141 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7142 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7143 );
7144
7145 // Trying to expand the selected syntax node one more time has no effect.
7146 editor.update_in(cx, |editor, window, cx| {
7147 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7148 });
7149 assert_eq!(
7150 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7151 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7152 );
7153
7154 editor.update_in(cx, |editor, window, cx| {
7155 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7156 });
7157 editor.update(cx, |editor, cx| {
7158 assert_text_with_selections(
7159 editor,
7160 indoc! {r#"
7161 use mod1::mod2::«{mod3, mod4}ˇ»;
7162
7163 «ˇfn fn_1(param1: bool, param2: &str) {
7164 let var1 = "text";
7165 }»
7166 "#},
7167 cx,
7168 );
7169 });
7170
7171 editor.update_in(cx, |editor, window, cx| {
7172 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7173 });
7174 editor.update(cx, |editor, cx| {
7175 assert_text_with_selections(
7176 editor,
7177 indoc! {r#"
7178 use mod1::mod2::{mod3, «mod4ˇ»};
7179
7180 fn fn_1«ˇ(param1: bool, param2: &str)» {
7181 let var1 = "«ˇtext»";
7182 }
7183 "#},
7184 cx,
7185 );
7186 });
7187
7188 editor.update_in(cx, |editor, window, cx| {
7189 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7190 });
7191 editor.update(cx, |editor, cx| {
7192 assert_text_with_selections(
7193 editor,
7194 indoc! {r#"
7195 use mod1::mod2::{mod3, mo«ˇ»d4};
7196
7197 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7198 let var1 = "te«ˇ»xt";
7199 }
7200 "#},
7201 cx,
7202 );
7203 });
7204
7205 // Trying to shrink the selected syntax node one more time has no effect.
7206 editor.update_in(cx, |editor, window, cx| {
7207 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7208 });
7209 editor.update_in(cx, |editor, _, cx| {
7210 assert_text_with_selections(
7211 editor,
7212 indoc! {r#"
7213 use mod1::mod2::{mod3, mo«ˇ»d4};
7214
7215 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7216 let var1 = "te«ˇ»xt";
7217 }
7218 "#},
7219 cx,
7220 );
7221 });
7222
7223 // Ensure that we keep expanding the selection if the larger selection starts or ends within
7224 // a fold.
7225 editor.update_in(cx, |editor, window, cx| {
7226 editor.fold_creases(
7227 vec![
7228 Crease::simple(
7229 Point::new(0, 21)..Point::new(0, 24),
7230 FoldPlaceholder::test(),
7231 ),
7232 Crease::simple(
7233 Point::new(3, 20)..Point::new(3, 22),
7234 FoldPlaceholder::test(),
7235 ),
7236 ],
7237 true,
7238 window,
7239 cx,
7240 );
7241 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7242 });
7243 editor.update(cx, |editor, cx| {
7244 assert_text_with_selections(
7245 editor,
7246 indoc! {r#"
7247 use mod1::mod2::«{mod3, mod4}ˇ»;
7248
7249 fn fn_1«ˇ(param1: bool, param2: &str)» {
7250 let var1 = "«ˇtext»";
7251 }
7252 "#},
7253 cx,
7254 );
7255 });
7256}
7257
7258#[gpui::test]
7259async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
7260 init_test(cx, |_| {});
7261
7262 let language = Arc::new(Language::new(
7263 LanguageConfig::default(),
7264 Some(tree_sitter_rust::LANGUAGE.into()),
7265 ));
7266
7267 let text = "let a = 2;";
7268
7269 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7270 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7271 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7272
7273 editor
7274 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7275 .await;
7276
7277 // Test case 1: Cursor at end of word
7278 editor.update_in(cx, |editor, window, cx| {
7279 editor.change_selections(None, window, cx, |s| {
7280 s.select_display_ranges([
7281 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
7282 ]);
7283 });
7284 });
7285 editor.update(cx, |editor, cx| {
7286 assert_text_with_selections(editor, "let aˇ = 2;", cx);
7287 });
7288 editor.update_in(cx, |editor, window, cx| {
7289 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7290 });
7291 editor.update(cx, |editor, cx| {
7292 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
7293 });
7294 editor.update_in(cx, |editor, window, cx| {
7295 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7296 });
7297 editor.update(cx, |editor, cx| {
7298 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7299 });
7300
7301 // Test case 2: Cursor at end of statement
7302 editor.update_in(cx, |editor, window, cx| {
7303 editor.change_selections(None, window, cx, |s| {
7304 s.select_display_ranges([
7305 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
7306 ]);
7307 });
7308 });
7309 editor.update(cx, |editor, cx| {
7310 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
7311 });
7312 editor.update_in(cx, |editor, window, cx| {
7313 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7314 });
7315 editor.update(cx, |editor, cx| {
7316 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7317 });
7318}
7319
7320#[gpui::test]
7321async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
7322 init_test(cx, |_| {});
7323
7324 let language = Arc::new(Language::new(
7325 LanguageConfig::default(),
7326 Some(tree_sitter_rust::LANGUAGE.into()),
7327 ));
7328
7329 let text = r#"
7330 use mod1::mod2::{mod3, mod4};
7331
7332 fn fn_1(param1: bool, param2: &str) {
7333 let var1 = "hello world";
7334 }
7335 "#
7336 .unindent();
7337
7338 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7339 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7340 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7341
7342 editor
7343 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7344 .await;
7345
7346 // Test 1: Cursor on a letter of a string word
7347 editor.update_in(cx, |editor, window, cx| {
7348 editor.change_selections(None, window, cx, |s| {
7349 s.select_display_ranges([
7350 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
7351 ]);
7352 });
7353 });
7354 editor.update_in(cx, |editor, window, cx| {
7355 assert_text_with_selections(
7356 editor,
7357 indoc! {r#"
7358 use mod1::mod2::{mod3, mod4};
7359
7360 fn fn_1(param1: bool, param2: &str) {
7361 let var1 = "hˇello world";
7362 }
7363 "#},
7364 cx,
7365 );
7366 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7367 assert_text_with_selections(
7368 editor,
7369 indoc! {r#"
7370 use mod1::mod2::{mod3, mod4};
7371
7372 fn fn_1(param1: bool, param2: &str) {
7373 let var1 = "«ˇhello» world";
7374 }
7375 "#},
7376 cx,
7377 );
7378 });
7379
7380 // Test 2: Partial selection within a word
7381 editor.update_in(cx, |editor, window, cx| {
7382 editor.change_selections(None, window, cx, |s| {
7383 s.select_display_ranges([
7384 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
7385 ]);
7386 });
7387 });
7388 editor.update_in(cx, |editor, window, cx| {
7389 assert_text_with_selections(
7390 editor,
7391 indoc! {r#"
7392 use mod1::mod2::{mod3, mod4};
7393
7394 fn fn_1(param1: bool, param2: &str) {
7395 let var1 = "h«elˇ»lo world";
7396 }
7397 "#},
7398 cx,
7399 );
7400 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7401 assert_text_with_selections(
7402 editor,
7403 indoc! {r#"
7404 use mod1::mod2::{mod3, mod4};
7405
7406 fn fn_1(param1: bool, param2: &str) {
7407 let var1 = "«ˇhello» world";
7408 }
7409 "#},
7410 cx,
7411 );
7412 });
7413
7414 // Test 3: Complete word already selected
7415 editor.update_in(cx, |editor, window, cx| {
7416 editor.change_selections(None, window, cx, |s| {
7417 s.select_display_ranges([
7418 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
7419 ]);
7420 });
7421 });
7422 editor.update_in(cx, |editor, window, cx| {
7423 assert_text_with_selections(
7424 editor,
7425 indoc! {r#"
7426 use mod1::mod2::{mod3, mod4};
7427
7428 fn fn_1(param1: bool, param2: &str) {
7429 let var1 = "«helloˇ» world";
7430 }
7431 "#},
7432 cx,
7433 );
7434 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7435 assert_text_with_selections(
7436 editor,
7437 indoc! {r#"
7438 use mod1::mod2::{mod3, mod4};
7439
7440 fn fn_1(param1: bool, param2: &str) {
7441 let var1 = "«hello worldˇ»";
7442 }
7443 "#},
7444 cx,
7445 );
7446 });
7447
7448 // Test 4: Selection spanning across words
7449 editor.update_in(cx, |editor, window, cx| {
7450 editor.change_selections(None, window, cx, |s| {
7451 s.select_display_ranges([
7452 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
7453 ]);
7454 });
7455 });
7456 editor.update_in(cx, |editor, window, cx| {
7457 assert_text_with_selections(
7458 editor,
7459 indoc! {r#"
7460 use mod1::mod2::{mod3, mod4};
7461
7462 fn fn_1(param1: bool, param2: &str) {
7463 let var1 = "hel«lo woˇ»rld";
7464 }
7465 "#},
7466 cx,
7467 );
7468 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7469 assert_text_with_selections(
7470 editor,
7471 indoc! {r#"
7472 use mod1::mod2::{mod3, mod4};
7473
7474 fn fn_1(param1: bool, param2: &str) {
7475 let var1 = "«ˇhello world»";
7476 }
7477 "#},
7478 cx,
7479 );
7480 });
7481
7482 // Test 5: Expansion beyond string
7483 editor.update_in(cx, |editor, window, cx| {
7484 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7485 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7486 assert_text_with_selections(
7487 editor,
7488 indoc! {r#"
7489 use mod1::mod2::{mod3, mod4};
7490
7491 fn fn_1(param1: bool, param2: &str) {
7492 «ˇlet var1 = "hello world";»
7493 }
7494 "#},
7495 cx,
7496 );
7497 });
7498}
7499
7500#[gpui::test]
7501async fn test_fold_function_bodies(cx: &mut TestAppContext) {
7502 init_test(cx, |_| {});
7503
7504 let base_text = r#"
7505 impl A {
7506 // this is an uncommitted comment
7507
7508 fn b() {
7509 c();
7510 }
7511
7512 // this is another uncommitted comment
7513
7514 fn d() {
7515 // e
7516 // f
7517 }
7518 }
7519
7520 fn g() {
7521 // h
7522 }
7523 "#
7524 .unindent();
7525
7526 let text = r#"
7527 ˇimpl A {
7528
7529 fn b() {
7530 c();
7531 }
7532
7533 fn d() {
7534 // e
7535 // f
7536 }
7537 }
7538
7539 fn g() {
7540 // h
7541 }
7542 "#
7543 .unindent();
7544
7545 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7546 cx.set_state(&text);
7547 cx.set_head_text(&base_text);
7548 cx.update_editor(|editor, window, cx| {
7549 editor.expand_all_diff_hunks(&Default::default(), window, cx);
7550 });
7551
7552 cx.assert_state_with_diff(
7553 "
7554 ˇimpl A {
7555 - // this is an uncommitted comment
7556
7557 fn b() {
7558 c();
7559 }
7560
7561 - // this is another uncommitted comment
7562 -
7563 fn d() {
7564 // e
7565 // f
7566 }
7567 }
7568
7569 fn g() {
7570 // h
7571 }
7572 "
7573 .unindent(),
7574 );
7575
7576 let expected_display_text = "
7577 impl A {
7578 // this is an uncommitted comment
7579
7580 fn b() {
7581 ⋯
7582 }
7583
7584 // this is another uncommitted comment
7585
7586 fn d() {
7587 ⋯
7588 }
7589 }
7590
7591 fn g() {
7592 ⋯
7593 }
7594 "
7595 .unindent();
7596
7597 cx.update_editor(|editor, window, cx| {
7598 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
7599 assert_eq!(editor.display_text(cx), expected_display_text);
7600 });
7601}
7602
7603#[gpui::test]
7604async fn test_autoindent(cx: &mut TestAppContext) {
7605 init_test(cx, |_| {});
7606
7607 let language = Arc::new(
7608 Language::new(
7609 LanguageConfig {
7610 brackets: BracketPairConfig {
7611 pairs: vec![
7612 BracketPair {
7613 start: "{".to_string(),
7614 end: "}".to_string(),
7615 close: false,
7616 surround: false,
7617 newline: true,
7618 },
7619 BracketPair {
7620 start: "(".to_string(),
7621 end: ")".to_string(),
7622 close: false,
7623 surround: false,
7624 newline: true,
7625 },
7626 ],
7627 ..Default::default()
7628 },
7629 ..Default::default()
7630 },
7631 Some(tree_sitter_rust::LANGUAGE.into()),
7632 )
7633 .with_indents_query(
7634 r#"
7635 (_ "(" ")" @end) @indent
7636 (_ "{" "}" @end) @indent
7637 "#,
7638 )
7639 .unwrap(),
7640 );
7641
7642 let text = "fn a() {}";
7643
7644 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7645 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7646 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7647 editor
7648 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7649 .await;
7650
7651 editor.update_in(cx, |editor, window, cx| {
7652 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
7653 editor.newline(&Newline, window, cx);
7654 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
7655 assert_eq!(
7656 editor.selections.ranges(cx),
7657 &[
7658 Point::new(1, 4)..Point::new(1, 4),
7659 Point::new(3, 4)..Point::new(3, 4),
7660 Point::new(5, 0)..Point::new(5, 0)
7661 ]
7662 );
7663 });
7664}
7665
7666#[gpui::test]
7667async fn test_autoindent_selections(cx: &mut TestAppContext) {
7668 init_test(cx, |_| {});
7669
7670 {
7671 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7672 cx.set_state(indoc! {"
7673 impl A {
7674
7675 fn b() {}
7676
7677 «fn c() {
7678
7679 }ˇ»
7680 }
7681 "});
7682
7683 cx.update_editor(|editor, window, cx| {
7684 editor.autoindent(&Default::default(), window, cx);
7685 });
7686
7687 cx.assert_editor_state(indoc! {"
7688 impl A {
7689
7690 fn b() {}
7691
7692 «fn c() {
7693
7694 }ˇ»
7695 }
7696 "});
7697 }
7698
7699 {
7700 let mut cx = EditorTestContext::new_multibuffer(
7701 cx,
7702 [indoc! { "
7703 impl A {
7704 «
7705 // a
7706 fn b(){}
7707 »
7708 «
7709 }
7710 fn c(){}
7711 »
7712 "}],
7713 );
7714
7715 let buffer = cx.update_editor(|editor, _, cx| {
7716 let buffer = editor.buffer().update(cx, |buffer, _| {
7717 buffer.all_buffers().iter().next().unwrap().clone()
7718 });
7719 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7720 buffer
7721 });
7722
7723 cx.run_until_parked();
7724 cx.update_editor(|editor, window, cx| {
7725 editor.select_all(&Default::default(), window, cx);
7726 editor.autoindent(&Default::default(), window, cx)
7727 });
7728 cx.run_until_parked();
7729
7730 cx.update(|_, cx| {
7731 assert_eq!(
7732 buffer.read(cx).text(),
7733 indoc! { "
7734 impl A {
7735
7736 // a
7737 fn b(){}
7738
7739
7740 }
7741 fn c(){}
7742
7743 " }
7744 )
7745 });
7746 }
7747}
7748
7749#[gpui::test]
7750async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
7751 init_test(cx, |_| {});
7752
7753 let mut cx = EditorTestContext::new(cx).await;
7754
7755 let language = Arc::new(Language::new(
7756 LanguageConfig {
7757 brackets: BracketPairConfig {
7758 pairs: vec![
7759 BracketPair {
7760 start: "{".to_string(),
7761 end: "}".to_string(),
7762 close: true,
7763 surround: true,
7764 newline: true,
7765 },
7766 BracketPair {
7767 start: "(".to_string(),
7768 end: ")".to_string(),
7769 close: true,
7770 surround: true,
7771 newline: true,
7772 },
7773 BracketPair {
7774 start: "/*".to_string(),
7775 end: " */".to_string(),
7776 close: true,
7777 surround: true,
7778 newline: true,
7779 },
7780 BracketPair {
7781 start: "[".to_string(),
7782 end: "]".to_string(),
7783 close: false,
7784 surround: false,
7785 newline: true,
7786 },
7787 BracketPair {
7788 start: "\"".to_string(),
7789 end: "\"".to_string(),
7790 close: true,
7791 surround: true,
7792 newline: false,
7793 },
7794 BracketPair {
7795 start: "<".to_string(),
7796 end: ">".to_string(),
7797 close: false,
7798 surround: true,
7799 newline: true,
7800 },
7801 ],
7802 ..Default::default()
7803 },
7804 autoclose_before: "})]".to_string(),
7805 ..Default::default()
7806 },
7807 Some(tree_sitter_rust::LANGUAGE.into()),
7808 ));
7809
7810 cx.language_registry().add(language.clone());
7811 cx.update_buffer(|buffer, cx| {
7812 buffer.set_language(Some(language), cx);
7813 });
7814
7815 cx.set_state(
7816 &r#"
7817 🏀ˇ
7818 εˇ
7819 ❤️ˇ
7820 "#
7821 .unindent(),
7822 );
7823
7824 // autoclose multiple nested brackets at multiple cursors
7825 cx.update_editor(|editor, window, cx| {
7826 editor.handle_input("{", window, cx);
7827 editor.handle_input("{", window, cx);
7828 editor.handle_input("{", window, cx);
7829 });
7830 cx.assert_editor_state(
7831 &"
7832 🏀{{{ˇ}}}
7833 ε{{{ˇ}}}
7834 ❤️{{{ˇ}}}
7835 "
7836 .unindent(),
7837 );
7838
7839 // insert a different closing bracket
7840 cx.update_editor(|editor, window, cx| {
7841 editor.handle_input(")", window, cx);
7842 });
7843 cx.assert_editor_state(
7844 &"
7845 🏀{{{)ˇ}}}
7846 ε{{{)ˇ}}}
7847 ❤️{{{)ˇ}}}
7848 "
7849 .unindent(),
7850 );
7851
7852 // skip over the auto-closed brackets when typing a closing bracket
7853 cx.update_editor(|editor, window, cx| {
7854 editor.move_right(&MoveRight, window, cx);
7855 editor.handle_input("}", window, cx);
7856 editor.handle_input("}", window, cx);
7857 editor.handle_input("}", window, cx);
7858 });
7859 cx.assert_editor_state(
7860 &"
7861 🏀{{{)}}}}ˇ
7862 ε{{{)}}}}ˇ
7863 ❤️{{{)}}}}ˇ
7864 "
7865 .unindent(),
7866 );
7867
7868 // autoclose multi-character pairs
7869 cx.set_state(
7870 &"
7871 ˇ
7872 ˇ
7873 "
7874 .unindent(),
7875 );
7876 cx.update_editor(|editor, window, cx| {
7877 editor.handle_input("/", window, cx);
7878 editor.handle_input("*", window, cx);
7879 });
7880 cx.assert_editor_state(
7881 &"
7882 /*ˇ */
7883 /*ˇ */
7884 "
7885 .unindent(),
7886 );
7887
7888 // one cursor autocloses a multi-character pair, one cursor
7889 // does not autoclose.
7890 cx.set_state(
7891 &"
7892 /ˇ
7893 ˇ
7894 "
7895 .unindent(),
7896 );
7897 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
7898 cx.assert_editor_state(
7899 &"
7900 /*ˇ */
7901 *ˇ
7902 "
7903 .unindent(),
7904 );
7905
7906 // Don't autoclose if the next character isn't whitespace and isn't
7907 // listed in the language's "autoclose_before" section.
7908 cx.set_state("ˇa b");
7909 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7910 cx.assert_editor_state("{ˇa b");
7911
7912 // Don't autoclose if `close` is false for the bracket pair
7913 cx.set_state("ˇ");
7914 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
7915 cx.assert_editor_state("[ˇ");
7916
7917 // Surround with brackets if text is selected
7918 cx.set_state("«aˇ» b");
7919 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7920 cx.assert_editor_state("{«aˇ»} b");
7921
7922 // Autoclose when not immediately after a word character
7923 cx.set_state("a ˇ");
7924 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7925 cx.assert_editor_state("a \"ˇ\"");
7926
7927 // Autoclose pair where the start and end characters are the same
7928 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7929 cx.assert_editor_state("a \"\"ˇ");
7930
7931 // Don't autoclose when immediately after a word character
7932 cx.set_state("aˇ");
7933 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7934 cx.assert_editor_state("a\"ˇ");
7935
7936 // Do autoclose when after a non-word character
7937 cx.set_state("{ˇ");
7938 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7939 cx.assert_editor_state("{\"ˇ\"");
7940
7941 // Non identical pairs autoclose regardless of preceding character
7942 cx.set_state("aˇ");
7943 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7944 cx.assert_editor_state("a{ˇ}");
7945
7946 // Don't autoclose pair if autoclose is disabled
7947 cx.set_state("ˇ");
7948 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7949 cx.assert_editor_state("<ˇ");
7950
7951 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
7952 cx.set_state("«aˇ» b");
7953 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7954 cx.assert_editor_state("<«aˇ»> b");
7955}
7956
7957#[gpui::test]
7958async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
7959 init_test(cx, |settings| {
7960 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7961 });
7962
7963 let mut cx = EditorTestContext::new(cx).await;
7964
7965 let language = Arc::new(Language::new(
7966 LanguageConfig {
7967 brackets: BracketPairConfig {
7968 pairs: vec![
7969 BracketPair {
7970 start: "{".to_string(),
7971 end: "}".to_string(),
7972 close: true,
7973 surround: true,
7974 newline: true,
7975 },
7976 BracketPair {
7977 start: "(".to_string(),
7978 end: ")".to_string(),
7979 close: true,
7980 surround: true,
7981 newline: true,
7982 },
7983 BracketPair {
7984 start: "[".to_string(),
7985 end: "]".to_string(),
7986 close: false,
7987 surround: false,
7988 newline: true,
7989 },
7990 ],
7991 ..Default::default()
7992 },
7993 autoclose_before: "})]".to_string(),
7994 ..Default::default()
7995 },
7996 Some(tree_sitter_rust::LANGUAGE.into()),
7997 ));
7998
7999 cx.language_registry().add(language.clone());
8000 cx.update_buffer(|buffer, cx| {
8001 buffer.set_language(Some(language), cx);
8002 });
8003
8004 cx.set_state(
8005 &"
8006 ˇ
8007 ˇ
8008 ˇ
8009 "
8010 .unindent(),
8011 );
8012
8013 // ensure only matching closing brackets are skipped over
8014 cx.update_editor(|editor, window, cx| {
8015 editor.handle_input("}", window, cx);
8016 editor.move_left(&MoveLeft, window, cx);
8017 editor.handle_input(")", window, cx);
8018 editor.move_left(&MoveLeft, window, cx);
8019 });
8020 cx.assert_editor_state(
8021 &"
8022 ˇ)}
8023 ˇ)}
8024 ˇ)}
8025 "
8026 .unindent(),
8027 );
8028
8029 // skip-over closing brackets at multiple cursors
8030 cx.update_editor(|editor, window, cx| {
8031 editor.handle_input(")", window, cx);
8032 editor.handle_input("}", window, cx);
8033 });
8034 cx.assert_editor_state(
8035 &"
8036 )}ˇ
8037 )}ˇ
8038 )}ˇ
8039 "
8040 .unindent(),
8041 );
8042
8043 // ignore non-close brackets
8044 cx.update_editor(|editor, window, cx| {
8045 editor.handle_input("]", window, cx);
8046 editor.move_left(&MoveLeft, window, cx);
8047 editor.handle_input("]", window, cx);
8048 });
8049 cx.assert_editor_state(
8050 &"
8051 )}]ˇ]
8052 )}]ˇ]
8053 )}]ˇ]
8054 "
8055 .unindent(),
8056 );
8057}
8058
8059#[gpui::test]
8060async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
8061 init_test(cx, |_| {});
8062
8063 let mut cx = EditorTestContext::new(cx).await;
8064
8065 let html_language = Arc::new(
8066 Language::new(
8067 LanguageConfig {
8068 name: "HTML".into(),
8069 brackets: BracketPairConfig {
8070 pairs: vec![
8071 BracketPair {
8072 start: "<".into(),
8073 end: ">".into(),
8074 close: true,
8075 ..Default::default()
8076 },
8077 BracketPair {
8078 start: "{".into(),
8079 end: "}".into(),
8080 close: true,
8081 ..Default::default()
8082 },
8083 BracketPair {
8084 start: "(".into(),
8085 end: ")".into(),
8086 close: true,
8087 ..Default::default()
8088 },
8089 ],
8090 ..Default::default()
8091 },
8092 autoclose_before: "})]>".into(),
8093 ..Default::default()
8094 },
8095 Some(tree_sitter_html::LANGUAGE.into()),
8096 )
8097 .with_injection_query(
8098 r#"
8099 (script_element
8100 (raw_text) @injection.content
8101 (#set! injection.language "javascript"))
8102 "#,
8103 )
8104 .unwrap(),
8105 );
8106
8107 let javascript_language = Arc::new(Language::new(
8108 LanguageConfig {
8109 name: "JavaScript".into(),
8110 brackets: BracketPairConfig {
8111 pairs: vec![
8112 BracketPair {
8113 start: "/*".into(),
8114 end: " */".into(),
8115 close: true,
8116 ..Default::default()
8117 },
8118 BracketPair {
8119 start: "{".into(),
8120 end: "}".into(),
8121 close: true,
8122 ..Default::default()
8123 },
8124 BracketPair {
8125 start: "(".into(),
8126 end: ")".into(),
8127 close: true,
8128 ..Default::default()
8129 },
8130 ],
8131 ..Default::default()
8132 },
8133 autoclose_before: "})]>".into(),
8134 ..Default::default()
8135 },
8136 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8137 ));
8138
8139 cx.language_registry().add(html_language.clone());
8140 cx.language_registry().add(javascript_language.clone());
8141
8142 cx.update_buffer(|buffer, cx| {
8143 buffer.set_language(Some(html_language), cx);
8144 });
8145
8146 cx.set_state(
8147 &r#"
8148 <body>ˇ
8149 <script>
8150 var x = 1;ˇ
8151 </script>
8152 </body>ˇ
8153 "#
8154 .unindent(),
8155 );
8156
8157 // Precondition: different languages are active at different locations.
8158 cx.update_editor(|editor, window, cx| {
8159 let snapshot = editor.snapshot(window, cx);
8160 let cursors = editor.selections.ranges::<usize>(cx);
8161 let languages = cursors
8162 .iter()
8163 .map(|c| snapshot.language_at(c.start).unwrap().name())
8164 .collect::<Vec<_>>();
8165 assert_eq!(
8166 languages,
8167 &["HTML".into(), "JavaScript".into(), "HTML".into()]
8168 );
8169 });
8170
8171 // Angle brackets autoclose in HTML, but not JavaScript.
8172 cx.update_editor(|editor, window, cx| {
8173 editor.handle_input("<", window, cx);
8174 editor.handle_input("a", window, cx);
8175 });
8176 cx.assert_editor_state(
8177 &r#"
8178 <body><aˇ>
8179 <script>
8180 var x = 1;<aˇ
8181 </script>
8182 </body><aˇ>
8183 "#
8184 .unindent(),
8185 );
8186
8187 // Curly braces and parens autoclose in both HTML and JavaScript.
8188 cx.update_editor(|editor, window, cx| {
8189 editor.handle_input(" b=", window, cx);
8190 editor.handle_input("{", window, cx);
8191 editor.handle_input("c", window, cx);
8192 editor.handle_input("(", window, cx);
8193 });
8194 cx.assert_editor_state(
8195 &r#"
8196 <body><a b={c(ˇ)}>
8197 <script>
8198 var x = 1;<a b={c(ˇ)}
8199 </script>
8200 </body><a b={c(ˇ)}>
8201 "#
8202 .unindent(),
8203 );
8204
8205 // Brackets that were already autoclosed are skipped.
8206 cx.update_editor(|editor, window, cx| {
8207 editor.handle_input(")", window, cx);
8208 editor.handle_input("d", window, cx);
8209 editor.handle_input("}", window, cx);
8210 });
8211 cx.assert_editor_state(
8212 &r#"
8213 <body><a b={c()d}ˇ>
8214 <script>
8215 var x = 1;<a b={c()d}ˇ
8216 </script>
8217 </body><a b={c()d}ˇ>
8218 "#
8219 .unindent(),
8220 );
8221 cx.update_editor(|editor, window, cx| {
8222 editor.handle_input(">", window, cx);
8223 });
8224 cx.assert_editor_state(
8225 &r#"
8226 <body><a b={c()d}>ˇ
8227 <script>
8228 var x = 1;<a b={c()d}>ˇ
8229 </script>
8230 </body><a b={c()d}>ˇ
8231 "#
8232 .unindent(),
8233 );
8234
8235 // Reset
8236 cx.set_state(
8237 &r#"
8238 <body>ˇ
8239 <script>
8240 var x = 1;ˇ
8241 </script>
8242 </body>ˇ
8243 "#
8244 .unindent(),
8245 );
8246
8247 cx.update_editor(|editor, window, cx| {
8248 editor.handle_input("<", window, cx);
8249 });
8250 cx.assert_editor_state(
8251 &r#"
8252 <body><ˇ>
8253 <script>
8254 var x = 1;<ˇ
8255 </script>
8256 </body><ˇ>
8257 "#
8258 .unindent(),
8259 );
8260
8261 // When backspacing, the closing angle brackets are removed.
8262 cx.update_editor(|editor, window, cx| {
8263 editor.backspace(&Backspace, window, cx);
8264 });
8265 cx.assert_editor_state(
8266 &r#"
8267 <body>ˇ
8268 <script>
8269 var x = 1;ˇ
8270 </script>
8271 </body>ˇ
8272 "#
8273 .unindent(),
8274 );
8275
8276 // Block comments autoclose in JavaScript, but not HTML.
8277 cx.update_editor(|editor, window, cx| {
8278 editor.handle_input("/", window, cx);
8279 editor.handle_input("*", window, cx);
8280 });
8281 cx.assert_editor_state(
8282 &r#"
8283 <body>/*ˇ
8284 <script>
8285 var x = 1;/*ˇ */
8286 </script>
8287 </body>/*ˇ
8288 "#
8289 .unindent(),
8290 );
8291}
8292
8293#[gpui::test]
8294async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
8295 init_test(cx, |_| {});
8296
8297 let mut cx = EditorTestContext::new(cx).await;
8298
8299 let rust_language = Arc::new(
8300 Language::new(
8301 LanguageConfig {
8302 name: "Rust".into(),
8303 brackets: serde_json::from_value(json!([
8304 { "start": "{", "end": "}", "close": true, "newline": true },
8305 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
8306 ]))
8307 .unwrap(),
8308 autoclose_before: "})]>".into(),
8309 ..Default::default()
8310 },
8311 Some(tree_sitter_rust::LANGUAGE.into()),
8312 )
8313 .with_override_query("(string_literal) @string")
8314 .unwrap(),
8315 );
8316
8317 cx.language_registry().add(rust_language.clone());
8318 cx.update_buffer(|buffer, cx| {
8319 buffer.set_language(Some(rust_language), cx);
8320 });
8321
8322 cx.set_state(
8323 &r#"
8324 let x = ˇ
8325 "#
8326 .unindent(),
8327 );
8328
8329 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
8330 cx.update_editor(|editor, window, cx| {
8331 editor.handle_input("\"", window, cx);
8332 });
8333 cx.assert_editor_state(
8334 &r#"
8335 let x = "ˇ"
8336 "#
8337 .unindent(),
8338 );
8339
8340 // Inserting another quotation mark. The cursor moves across the existing
8341 // automatically-inserted quotation mark.
8342 cx.update_editor(|editor, window, cx| {
8343 editor.handle_input("\"", window, cx);
8344 });
8345 cx.assert_editor_state(
8346 &r#"
8347 let x = ""ˇ
8348 "#
8349 .unindent(),
8350 );
8351
8352 // Reset
8353 cx.set_state(
8354 &r#"
8355 let x = ˇ
8356 "#
8357 .unindent(),
8358 );
8359
8360 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
8361 cx.update_editor(|editor, window, cx| {
8362 editor.handle_input("\"", window, cx);
8363 editor.handle_input(" ", window, cx);
8364 editor.move_left(&Default::default(), window, cx);
8365 editor.handle_input("\\", window, cx);
8366 editor.handle_input("\"", window, cx);
8367 });
8368 cx.assert_editor_state(
8369 &r#"
8370 let x = "\"ˇ "
8371 "#
8372 .unindent(),
8373 );
8374
8375 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
8376 // mark. Nothing is inserted.
8377 cx.update_editor(|editor, window, cx| {
8378 editor.move_right(&Default::default(), window, cx);
8379 editor.handle_input("\"", window, cx);
8380 });
8381 cx.assert_editor_state(
8382 &r#"
8383 let x = "\" "ˇ
8384 "#
8385 .unindent(),
8386 );
8387}
8388
8389#[gpui::test]
8390async fn test_surround_with_pair(cx: &mut TestAppContext) {
8391 init_test(cx, |_| {});
8392
8393 let language = Arc::new(Language::new(
8394 LanguageConfig {
8395 brackets: BracketPairConfig {
8396 pairs: vec![
8397 BracketPair {
8398 start: "{".to_string(),
8399 end: "}".to_string(),
8400 close: true,
8401 surround: true,
8402 newline: true,
8403 },
8404 BracketPair {
8405 start: "/* ".to_string(),
8406 end: "*/".to_string(),
8407 close: true,
8408 surround: true,
8409 ..Default::default()
8410 },
8411 ],
8412 ..Default::default()
8413 },
8414 ..Default::default()
8415 },
8416 Some(tree_sitter_rust::LANGUAGE.into()),
8417 ));
8418
8419 let text = r#"
8420 a
8421 b
8422 c
8423 "#
8424 .unindent();
8425
8426 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8427 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8428 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8429 editor
8430 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8431 .await;
8432
8433 editor.update_in(cx, |editor, window, cx| {
8434 editor.change_selections(None, window, cx, |s| {
8435 s.select_display_ranges([
8436 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8437 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8438 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
8439 ])
8440 });
8441
8442 editor.handle_input("{", window, cx);
8443 editor.handle_input("{", window, cx);
8444 editor.handle_input("{", window, cx);
8445 assert_eq!(
8446 editor.text(cx),
8447 "
8448 {{{a}}}
8449 {{{b}}}
8450 {{{c}}}
8451 "
8452 .unindent()
8453 );
8454 assert_eq!(
8455 editor.selections.display_ranges(cx),
8456 [
8457 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
8458 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
8459 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
8460 ]
8461 );
8462
8463 editor.undo(&Undo, window, cx);
8464 editor.undo(&Undo, window, cx);
8465 editor.undo(&Undo, window, cx);
8466 assert_eq!(
8467 editor.text(cx),
8468 "
8469 a
8470 b
8471 c
8472 "
8473 .unindent()
8474 );
8475 assert_eq!(
8476 editor.selections.display_ranges(cx),
8477 [
8478 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8479 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8480 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8481 ]
8482 );
8483
8484 // Ensure inserting the first character of a multi-byte bracket pair
8485 // doesn't surround the selections with the bracket.
8486 editor.handle_input("/", window, cx);
8487 assert_eq!(
8488 editor.text(cx),
8489 "
8490 /
8491 /
8492 /
8493 "
8494 .unindent()
8495 );
8496 assert_eq!(
8497 editor.selections.display_ranges(cx),
8498 [
8499 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8500 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8501 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8502 ]
8503 );
8504
8505 editor.undo(&Undo, window, cx);
8506 assert_eq!(
8507 editor.text(cx),
8508 "
8509 a
8510 b
8511 c
8512 "
8513 .unindent()
8514 );
8515 assert_eq!(
8516 editor.selections.display_ranges(cx),
8517 [
8518 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8519 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8520 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8521 ]
8522 );
8523
8524 // Ensure inserting the last character of a multi-byte bracket pair
8525 // doesn't surround the selections with the bracket.
8526 editor.handle_input("*", window, cx);
8527 assert_eq!(
8528 editor.text(cx),
8529 "
8530 *
8531 *
8532 *
8533 "
8534 .unindent()
8535 );
8536 assert_eq!(
8537 editor.selections.display_ranges(cx),
8538 [
8539 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8540 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8541 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8542 ]
8543 );
8544 });
8545}
8546
8547#[gpui::test]
8548async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
8549 init_test(cx, |_| {});
8550
8551 let language = Arc::new(Language::new(
8552 LanguageConfig {
8553 brackets: BracketPairConfig {
8554 pairs: vec![BracketPair {
8555 start: "{".to_string(),
8556 end: "}".to_string(),
8557 close: true,
8558 surround: true,
8559 newline: true,
8560 }],
8561 ..Default::default()
8562 },
8563 autoclose_before: "}".to_string(),
8564 ..Default::default()
8565 },
8566 Some(tree_sitter_rust::LANGUAGE.into()),
8567 ));
8568
8569 let text = r#"
8570 a
8571 b
8572 c
8573 "#
8574 .unindent();
8575
8576 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8577 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8578 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8579 editor
8580 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8581 .await;
8582
8583 editor.update_in(cx, |editor, window, cx| {
8584 editor.change_selections(None, window, cx, |s| {
8585 s.select_ranges([
8586 Point::new(0, 1)..Point::new(0, 1),
8587 Point::new(1, 1)..Point::new(1, 1),
8588 Point::new(2, 1)..Point::new(2, 1),
8589 ])
8590 });
8591
8592 editor.handle_input("{", window, cx);
8593 editor.handle_input("{", window, cx);
8594 editor.handle_input("_", window, cx);
8595 assert_eq!(
8596 editor.text(cx),
8597 "
8598 a{{_}}
8599 b{{_}}
8600 c{{_}}
8601 "
8602 .unindent()
8603 );
8604 assert_eq!(
8605 editor.selections.ranges::<Point>(cx),
8606 [
8607 Point::new(0, 4)..Point::new(0, 4),
8608 Point::new(1, 4)..Point::new(1, 4),
8609 Point::new(2, 4)..Point::new(2, 4)
8610 ]
8611 );
8612
8613 editor.backspace(&Default::default(), window, cx);
8614 editor.backspace(&Default::default(), window, cx);
8615 assert_eq!(
8616 editor.text(cx),
8617 "
8618 a{}
8619 b{}
8620 c{}
8621 "
8622 .unindent()
8623 );
8624 assert_eq!(
8625 editor.selections.ranges::<Point>(cx),
8626 [
8627 Point::new(0, 2)..Point::new(0, 2),
8628 Point::new(1, 2)..Point::new(1, 2),
8629 Point::new(2, 2)..Point::new(2, 2)
8630 ]
8631 );
8632
8633 editor.delete_to_previous_word_start(&Default::default(), window, cx);
8634 assert_eq!(
8635 editor.text(cx),
8636 "
8637 a
8638 b
8639 c
8640 "
8641 .unindent()
8642 );
8643 assert_eq!(
8644 editor.selections.ranges::<Point>(cx),
8645 [
8646 Point::new(0, 1)..Point::new(0, 1),
8647 Point::new(1, 1)..Point::new(1, 1),
8648 Point::new(2, 1)..Point::new(2, 1)
8649 ]
8650 );
8651 });
8652}
8653
8654#[gpui::test]
8655async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
8656 init_test(cx, |settings| {
8657 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8658 });
8659
8660 let mut cx = EditorTestContext::new(cx).await;
8661
8662 let language = Arc::new(Language::new(
8663 LanguageConfig {
8664 brackets: BracketPairConfig {
8665 pairs: vec![
8666 BracketPair {
8667 start: "{".to_string(),
8668 end: "}".to_string(),
8669 close: true,
8670 surround: true,
8671 newline: true,
8672 },
8673 BracketPair {
8674 start: "(".to_string(),
8675 end: ")".to_string(),
8676 close: true,
8677 surround: true,
8678 newline: true,
8679 },
8680 BracketPair {
8681 start: "[".to_string(),
8682 end: "]".to_string(),
8683 close: false,
8684 surround: true,
8685 newline: true,
8686 },
8687 ],
8688 ..Default::default()
8689 },
8690 autoclose_before: "})]".to_string(),
8691 ..Default::default()
8692 },
8693 Some(tree_sitter_rust::LANGUAGE.into()),
8694 ));
8695
8696 cx.language_registry().add(language.clone());
8697 cx.update_buffer(|buffer, cx| {
8698 buffer.set_language(Some(language), cx);
8699 });
8700
8701 cx.set_state(
8702 &"
8703 {(ˇ)}
8704 [[ˇ]]
8705 {(ˇ)}
8706 "
8707 .unindent(),
8708 );
8709
8710 cx.update_editor(|editor, window, cx| {
8711 editor.backspace(&Default::default(), window, cx);
8712 editor.backspace(&Default::default(), window, cx);
8713 });
8714
8715 cx.assert_editor_state(
8716 &"
8717 ˇ
8718 ˇ]]
8719 ˇ
8720 "
8721 .unindent(),
8722 );
8723
8724 cx.update_editor(|editor, window, cx| {
8725 editor.handle_input("{", window, cx);
8726 editor.handle_input("{", window, cx);
8727 editor.move_right(&MoveRight, window, cx);
8728 editor.move_right(&MoveRight, window, cx);
8729 editor.move_left(&MoveLeft, window, cx);
8730 editor.move_left(&MoveLeft, window, cx);
8731 editor.backspace(&Default::default(), window, cx);
8732 });
8733
8734 cx.assert_editor_state(
8735 &"
8736 {ˇ}
8737 {ˇ}]]
8738 {ˇ}
8739 "
8740 .unindent(),
8741 );
8742
8743 cx.update_editor(|editor, window, cx| {
8744 editor.backspace(&Default::default(), window, cx);
8745 });
8746
8747 cx.assert_editor_state(
8748 &"
8749 ˇ
8750 ˇ]]
8751 ˇ
8752 "
8753 .unindent(),
8754 );
8755}
8756
8757#[gpui::test]
8758async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
8759 init_test(cx, |_| {});
8760
8761 let language = Arc::new(Language::new(
8762 LanguageConfig::default(),
8763 Some(tree_sitter_rust::LANGUAGE.into()),
8764 ));
8765
8766 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
8767 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8768 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8769 editor
8770 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8771 .await;
8772
8773 editor.update_in(cx, |editor, window, cx| {
8774 editor.set_auto_replace_emoji_shortcode(true);
8775
8776 editor.handle_input("Hello ", window, cx);
8777 editor.handle_input(":wave", window, cx);
8778 assert_eq!(editor.text(cx), "Hello :wave".unindent());
8779
8780 editor.handle_input(":", window, cx);
8781 assert_eq!(editor.text(cx), "Hello 👋".unindent());
8782
8783 editor.handle_input(" :smile", window, cx);
8784 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
8785
8786 editor.handle_input(":", window, cx);
8787 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
8788
8789 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
8790 editor.handle_input(":wave", window, cx);
8791 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
8792
8793 editor.handle_input(":", window, cx);
8794 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
8795
8796 editor.handle_input(":1", window, cx);
8797 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
8798
8799 editor.handle_input(":", window, cx);
8800 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
8801
8802 // Ensure shortcode does not get replaced when it is part of a word
8803 editor.handle_input(" Test:wave", window, cx);
8804 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
8805
8806 editor.handle_input(":", window, cx);
8807 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
8808
8809 editor.set_auto_replace_emoji_shortcode(false);
8810
8811 // Ensure shortcode does not get replaced when auto replace is off
8812 editor.handle_input(" :wave", window, cx);
8813 assert_eq!(
8814 editor.text(cx),
8815 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
8816 );
8817
8818 editor.handle_input(":", window, cx);
8819 assert_eq!(
8820 editor.text(cx),
8821 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
8822 );
8823 });
8824}
8825
8826#[gpui::test]
8827async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
8828 init_test(cx, |_| {});
8829
8830 let (text, insertion_ranges) = marked_text_ranges(
8831 indoc! {"
8832 ˇ
8833 "},
8834 false,
8835 );
8836
8837 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
8838 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8839
8840 _ = editor.update_in(cx, |editor, window, cx| {
8841 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
8842
8843 editor
8844 .insert_snippet(&insertion_ranges, snippet, window, cx)
8845 .unwrap();
8846
8847 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
8848 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
8849 assert_eq!(editor.text(cx), expected_text);
8850 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
8851 }
8852
8853 assert(
8854 editor,
8855 cx,
8856 indoc! {"
8857 type «» =•
8858 "},
8859 );
8860
8861 assert!(editor.context_menu_visible(), "There should be a matches");
8862 });
8863}
8864
8865#[gpui::test]
8866async fn test_snippets(cx: &mut TestAppContext) {
8867 init_test(cx, |_| {});
8868
8869 let mut cx = EditorTestContext::new(cx).await;
8870
8871 cx.set_state(indoc! {"
8872 a.ˇ b
8873 a.ˇ b
8874 a.ˇ b
8875 "});
8876
8877 cx.update_editor(|editor, window, cx| {
8878 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
8879 let insertion_ranges = editor
8880 .selections
8881 .all(cx)
8882 .iter()
8883 .map(|s| s.range().clone())
8884 .collect::<Vec<_>>();
8885 editor
8886 .insert_snippet(&insertion_ranges, snippet, window, cx)
8887 .unwrap();
8888 });
8889
8890 cx.assert_editor_state(indoc! {"
8891 a.f(«oneˇ», two, «threeˇ») b
8892 a.f(«oneˇ», two, «threeˇ») b
8893 a.f(«oneˇ», two, «threeˇ») b
8894 "});
8895
8896 // Can't move earlier than the first tab stop
8897 cx.update_editor(|editor, window, cx| {
8898 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
8899 });
8900 cx.assert_editor_state(indoc! {"
8901 a.f(«oneˇ», two, «threeˇ») b
8902 a.f(«oneˇ», two, «threeˇ») b
8903 a.f(«oneˇ», two, «threeˇ») b
8904 "});
8905
8906 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
8907 cx.assert_editor_state(indoc! {"
8908 a.f(one, «twoˇ», three) b
8909 a.f(one, «twoˇ», three) b
8910 a.f(one, «twoˇ», three) b
8911 "});
8912
8913 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
8914 cx.assert_editor_state(indoc! {"
8915 a.f(«oneˇ», two, «threeˇ») b
8916 a.f(«oneˇ», two, «threeˇ») b
8917 a.f(«oneˇ», two, «threeˇ») b
8918 "});
8919
8920 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
8921 cx.assert_editor_state(indoc! {"
8922 a.f(one, «twoˇ», three) b
8923 a.f(one, «twoˇ», three) b
8924 a.f(one, «twoˇ», three) b
8925 "});
8926 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
8927 cx.assert_editor_state(indoc! {"
8928 a.f(one, two, three)ˇ b
8929 a.f(one, two, three)ˇ b
8930 a.f(one, two, three)ˇ b
8931 "});
8932
8933 // As soon as the last tab stop is reached, snippet state is gone
8934 cx.update_editor(|editor, window, cx| {
8935 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
8936 });
8937 cx.assert_editor_state(indoc! {"
8938 a.f(one, two, three)ˇ b
8939 a.f(one, two, three)ˇ b
8940 a.f(one, two, three)ˇ b
8941 "});
8942}
8943
8944#[gpui::test]
8945async fn test_snippet_indentation(cx: &mut TestAppContext) {
8946 init_test(cx, |_| {});
8947
8948 let mut cx = EditorTestContext::new(cx).await;
8949
8950 cx.update_editor(|editor, window, cx| {
8951 let snippet = Snippet::parse(indoc! {"
8952 /*
8953 * Multiline comment with leading indentation
8954 *
8955 * $1
8956 */
8957 $0"})
8958 .unwrap();
8959 let insertion_ranges = editor
8960 .selections
8961 .all(cx)
8962 .iter()
8963 .map(|s| s.range().clone())
8964 .collect::<Vec<_>>();
8965 editor
8966 .insert_snippet(&insertion_ranges, snippet, window, cx)
8967 .unwrap();
8968 });
8969
8970 cx.assert_editor_state(indoc! {"
8971 /*
8972 * Multiline comment with leading indentation
8973 *
8974 * ˇ
8975 */
8976 "});
8977
8978 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
8979 cx.assert_editor_state(indoc! {"
8980 /*
8981 * Multiline comment with leading indentation
8982 *
8983 *•
8984 */
8985 ˇ"});
8986}
8987
8988#[gpui::test]
8989async fn test_document_format_during_save(cx: &mut TestAppContext) {
8990 init_test(cx, |_| {});
8991
8992 let fs = FakeFs::new(cx.executor());
8993 fs.insert_file(path!("/file.rs"), Default::default()).await;
8994
8995 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
8996
8997 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8998 language_registry.add(rust_lang());
8999 let mut fake_servers = language_registry.register_fake_lsp(
9000 "Rust",
9001 FakeLspAdapter {
9002 capabilities: lsp::ServerCapabilities {
9003 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9004 ..Default::default()
9005 },
9006 ..Default::default()
9007 },
9008 );
9009
9010 let buffer = project
9011 .update(cx, |project, cx| {
9012 project.open_local_buffer(path!("/file.rs"), cx)
9013 })
9014 .await
9015 .unwrap();
9016
9017 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9018 let (editor, cx) = cx.add_window_view(|window, cx| {
9019 build_editor_with_project(project.clone(), buffer, window, cx)
9020 });
9021 editor.update_in(cx, |editor, window, cx| {
9022 editor.set_text("one\ntwo\nthree\n", window, cx)
9023 });
9024 assert!(cx.read(|cx| editor.is_dirty(cx)));
9025
9026 cx.executor().start_waiting();
9027 let fake_server = fake_servers.next().await.unwrap();
9028
9029 {
9030 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9031 move |params, _| async move {
9032 assert_eq!(
9033 params.text_document.uri,
9034 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9035 );
9036 assert_eq!(params.options.tab_size, 4);
9037 Ok(Some(vec![lsp::TextEdit::new(
9038 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9039 ", ".to_string(),
9040 )]))
9041 },
9042 );
9043 let save = editor
9044 .update_in(cx, |editor, window, cx| {
9045 editor.save(true, project.clone(), window, cx)
9046 })
9047 .unwrap();
9048 cx.executor().start_waiting();
9049 save.await;
9050
9051 assert_eq!(
9052 editor.update(cx, |editor, cx| editor.text(cx)),
9053 "one, two\nthree\n"
9054 );
9055 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9056 }
9057
9058 {
9059 editor.update_in(cx, |editor, window, cx| {
9060 editor.set_text("one\ntwo\nthree\n", window, cx)
9061 });
9062 assert!(cx.read(|cx| editor.is_dirty(cx)));
9063
9064 // Ensure we can still save even if formatting hangs.
9065 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9066 move |params, _| async move {
9067 assert_eq!(
9068 params.text_document.uri,
9069 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9070 );
9071 futures::future::pending::<()>().await;
9072 unreachable!()
9073 },
9074 );
9075 let save = editor
9076 .update_in(cx, |editor, window, cx| {
9077 editor.save(true, project.clone(), window, cx)
9078 })
9079 .unwrap();
9080 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9081 cx.executor().start_waiting();
9082 save.await;
9083 assert_eq!(
9084 editor.update(cx, |editor, cx| editor.text(cx)),
9085 "one\ntwo\nthree\n"
9086 );
9087 }
9088
9089 // For non-dirty buffer, no formatting request should be sent
9090 {
9091 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9092
9093 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
9094 panic!("Should not be invoked on non-dirty buffer");
9095 });
9096 let save = editor
9097 .update_in(cx, |editor, window, cx| {
9098 editor.save(true, project.clone(), window, cx)
9099 })
9100 .unwrap();
9101 cx.executor().start_waiting();
9102 save.await;
9103 }
9104
9105 // Set rust language override and assert overridden tabsize is sent to language server
9106 update_test_language_settings(cx, |settings| {
9107 settings.languages.insert(
9108 "Rust".into(),
9109 LanguageSettingsContent {
9110 tab_size: NonZeroU32::new(8),
9111 ..Default::default()
9112 },
9113 );
9114 });
9115
9116 {
9117 editor.update_in(cx, |editor, window, cx| {
9118 editor.set_text("somehting_new\n", window, cx)
9119 });
9120 assert!(cx.read(|cx| editor.is_dirty(cx)));
9121 let _formatting_request_signal = fake_server
9122 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9123 assert_eq!(
9124 params.text_document.uri,
9125 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9126 );
9127 assert_eq!(params.options.tab_size, 8);
9128 Ok(Some(vec![]))
9129 });
9130 let save = editor
9131 .update_in(cx, |editor, window, cx| {
9132 editor.save(true, project.clone(), window, cx)
9133 })
9134 .unwrap();
9135 cx.executor().start_waiting();
9136 save.await;
9137 }
9138}
9139
9140#[gpui::test]
9141async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
9142 init_test(cx, |_| {});
9143
9144 let cols = 4;
9145 let rows = 10;
9146 let sample_text_1 = sample_text(rows, cols, 'a');
9147 assert_eq!(
9148 sample_text_1,
9149 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
9150 );
9151 let sample_text_2 = sample_text(rows, cols, 'l');
9152 assert_eq!(
9153 sample_text_2,
9154 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
9155 );
9156 let sample_text_3 = sample_text(rows, cols, 'v');
9157 assert_eq!(
9158 sample_text_3,
9159 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
9160 );
9161
9162 let fs = FakeFs::new(cx.executor());
9163 fs.insert_tree(
9164 path!("/a"),
9165 json!({
9166 "main.rs": sample_text_1,
9167 "other.rs": sample_text_2,
9168 "lib.rs": sample_text_3,
9169 }),
9170 )
9171 .await;
9172
9173 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
9174 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9175 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9176
9177 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9178 language_registry.add(rust_lang());
9179 let mut fake_servers = language_registry.register_fake_lsp(
9180 "Rust",
9181 FakeLspAdapter {
9182 capabilities: lsp::ServerCapabilities {
9183 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9184 ..Default::default()
9185 },
9186 ..Default::default()
9187 },
9188 );
9189
9190 let worktree = project.update(cx, |project, cx| {
9191 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
9192 assert_eq!(worktrees.len(), 1);
9193 worktrees.pop().unwrap()
9194 });
9195 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9196
9197 let buffer_1 = project
9198 .update(cx, |project, cx| {
9199 project.open_buffer((worktree_id, "main.rs"), cx)
9200 })
9201 .await
9202 .unwrap();
9203 let buffer_2 = project
9204 .update(cx, |project, cx| {
9205 project.open_buffer((worktree_id, "other.rs"), cx)
9206 })
9207 .await
9208 .unwrap();
9209 let buffer_3 = project
9210 .update(cx, |project, cx| {
9211 project.open_buffer((worktree_id, "lib.rs"), cx)
9212 })
9213 .await
9214 .unwrap();
9215
9216 let multi_buffer = cx.new(|cx| {
9217 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9218 multi_buffer.push_excerpts(
9219 buffer_1.clone(),
9220 [
9221 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9222 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9223 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9224 ],
9225 cx,
9226 );
9227 multi_buffer.push_excerpts(
9228 buffer_2.clone(),
9229 [
9230 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9231 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9232 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9233 ],
9234 cx,
9235 );
9236 multi_buffer.push_excerpts(
9237 buffer_3.clone(),
9238 [
9239 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9240 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9241 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9242 ],
9243 cx,
9244 );
9245 multi_buffer
9246 });
9247 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
9248 Editor::new(
9249 EditorMode::full(),
9250 multi_buffer,
9251 Some(project.clone()),
9252 window,
9253 cx,
9254 )
9255 });
9256
9257 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9258 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
9259 s.select_ranges(Some(1..2))
9260 });
9261 editor.insert("|one|two|three|", window, cx);
9262 });
9263 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9264 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9265 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
9266 s.select_ranges(Some(60..70))
9267 });
9268 editor.insert("|four|five|six|", window, cx);
9269 });
9270 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9271
9272 // First two buffers should be edited, but not the third one.
9273 assert_eq!(
9274 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9275 "a|one|two|three|aa\nbbbb\ncccc\n\nffff\ngggg\n\njjjj\nllll\nmmmm\nnnnn|four|five|six|\nr\n\nuuuu\nvvvv\nwwww\nxxxx\n\n{{{{\n||||\n\n\u{7f}\u{7f}\u{7f}\u{7f}",
9276 );
9277 buffer_1.update(cx, |buffer, _| {
9278 assert!(buffer.is_dirty());
9279 assert_eq!(
9280 buffer.text(),
9281 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
9282 )
9283 });
9284 buffer_2.update(cx, |buffer, _| {
9285 assert!(buffer.is_dirty());
9286 assert_eq!(
9287 buffer.text(),
9288 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
9289 )
9290 });
9291 buffer_3.update(cx, |buffer, _| {
9292 assert!(!buffer.is_dirty());
9293 assert_eq!(buffer.text(), sample_text_3,)
9294 });
9295 cx.executor().run_until_parked();
9296
9297 cx.executor().start_waiting();
9298 let save = multi_buffer_editor
9299 .update_in(cx, |editor, window, cx| {
9300 editor.save(true, project.clone(), window, cx)
9301 })
9302 .unwrap();
9303
9304 let fake_server = fake_servers.next().await.unwrap();
9305 fake_server
9306 .server
9307 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
9308 Ok(Some(vec![lsp::TextEdit::new(
9309 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9310 format!("[{} formatted]", params.text_document.uri),
9311 )]))
9312 })
9313 .detach();
9314 save.await;
9315
9316 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
9317 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
9318 assert_eq!(
9319 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9320 uri!(
9321 "a|o[file:///a/main.rs formatted]bbbb\ncccc\n\nffff\ngggg\n\njjjj\n\nlll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|\nr\n\nuuuu\n\nvvvv\nwwww\nxxxx\n\n{{{{\n||||\n\n\u{7f}\u{7f}\u{7f}\u{7f}"
9322 ),
9323 );
9324 buffer_1.update(cx, |buffer, _| {
9325 assert!(!buffer.is_dirty());
9326 assert_eq!(
9327 buffer.text(),
9328 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
9329 )
9330 });
9331 buffer_2.update(cx, |buffer, _| {
9332 assert!(!buffer.is_dirty());
9333 assert_eq!(
9334 buffer.text(),
9335 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
9336 )
9337 });
9338 buffer_3.update(cx, |buffer, _| {
9339 assert!(!buffer.is_dirty());
9340 assert_eq!(buffer.text(), sample_text_3,)
9341 });
9342}
9343
9344#[gpui::test]
9345async fn test_range_format_during_save(cx: &mut TestAppContext) {
9346 init_test(cx, |_| {});
9347
9348 let fs = FakeFs::new(cx.executor());
9349 fs.insert_file(path!("/file.rs"), Default::default()).await;
9350
9351 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9352
9353 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9354 language_registry.add(rust_lang());
9355 let mut fake_servers = language_registry.register_fake_lsp(
9356 "Rust",
9357 FakeLspAdapter {
9358 capabilities: lsp::ServerCapabilities {
9359 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
9360 ..Default::default()
9361 },
9362 ..Default::default()
9363 },
9364 );
9365
9366 let buffer = project
9367 .update(cx, |project, cx| {
9368 project.open_local_buffer(path!("/file.rs"), cx)
9369 })
9370 .await
9371 .unwrap();
9372
9373 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9374 let (editor, cx) = cx.add_window_view(|window, cx| {
9375 build_editor_with_project(project.clone(), buffer, window, cx)
9376 });
9377 editor.update_in(cx, |editor, window, cx| {
9378 editor.set_text("one\ntwo\nthree\n", window, cx)
9379 });
9380 assert!(cx.read(|cx| editor.is_dirty(cx)));
9381
9382 cx.executor().start_waiting();
9383 let fake_server = fake_servers.next().await.unwrap();
9384
9385 let save = editor
9386 .update_in(cx, |editor, window, cx| {
9387 editor.save(true, project.clone(), window, cx)
9388 })
9389 .unwrap();
9390 fake_server
9391 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
9392 assert_eq!(
9393 params.text_document.uri,
9394 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9395 );
9396 assert_eq!(params.options.tab_size, 4);
9397 Ok(Some(vec![lsp::TextEdit::new(
9398 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9399 ", ".to_string(),
9400 )]))
9401 })
9402 .next()
9403 .await;
9404 cx.executor().start_waiting();
9405 save.await;
9406 assert_eq!(
9407 editor.update(cx, |editor, cx| editor.text(cx)),
9408 "one, two\nthree\n"
9409 );
9410 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9411
9412 editor.update_in(cx, |editor, window, cx| {
9413 editor.set_text("one\ntwo\nthree\n", window, cx)
9414 });
9415 assert!(cx.read(|cx| editor.is_dirty(cx)));
9416
9417 // Ensure we can still save even if formatting hangs.
9418 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
9419 move |params, _| async move {
9420 assert_eq!(
9421 params.text_document.uri,
9422 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9423 );
9424 futures::future::pending::<()>().await;
9425 unreachable!()
9426 },
9427 );
9428 let save = editor
9429 .update_in(cx, |editor, window, cx| {
9430 editor.save(true, project.clone(), window, cx)
9431 })
9432 .unwrap();
9433 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9434 cx.executor().start_waiting();
9435 save.await;
9436 assert_eq!(
9437 editor.update(cx, |editor, cx| editor.text(cx)),
9438 "one\ntwo\nthree\n"
9439 );
9440 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9441
9442 // For non-dirty buffer, no formatting request should be sent
9443 let save = editor
9444 .update_in(cx, |editor, window, cx| {
9445 editor.save(true, project.clone(), window, cx)
9446 })
9447 .unwrap();
9448 let _pending_format_request = fake_server
9449 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
9450 panic!("Should not be invoked on non-dirty buffer");
9451 })
9452 .next();
9453 cx.executor().start_waiting();
9454 save.await;
9455
9456 // Set Rust language override and assert overridden tabsize is sent to language server
9457 update_test_language_settings(cx, |settings| {
9458 settings.languages.insert(
9459 "Rust".into(),
9460 LanguageSettingsContent {
9461 tab_size: NonZeroU32::new(8),
9462 ..Default::default()
9463 },
9464 );
9465 });
9466
9467 editor.update_in(cx, |editor, window, cx| {
9468 editor.set_text("somehting_new\n", window, cx)
9469 });
9470 assert!(cx.read(|cx| editor.is_dirty(cx)));
9471 let save = editor
9472 .update_in(cx, |editor, window, cx| {
9473 editor.save(true, project.clone(), window, cx)
9474 })
9475 .unwrap();
9476 fake_server
9477 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
9478 assert_eq!(
9479 params.text_document.uri,
9480 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9481 );
9482 assert_eq!(params.options.tab_size, 8);
9483 Ok(Some(Vec::new()))
9484 })
9485 .next()
9486 .await;
9487 save.await;
9488}
9489
9490#[gpui::test]
9491async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
9492 init_test(cx, |settings| {
9493 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
9494 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
9495 ))
9496 });
9497
9498 let fs = FakeFs::new(cx.executor());
9499 fs.insert_file(path!("/file.rs"), Default::default()).await;
9500
9501 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9502
9503 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9504 language_registry.add(Arc::new(Language::new(
9505 LanguageConfig {
9506 name: "Rust".into(),
9507 matcher: LanguageMatcher {
9508 path_suffixes: vec!["rs".to_string()],
9509 ..Default::default()
9510 },
9511 ..LanguageConfig::default()
9512 },
9513 Some(tree_sitter_rust::LANGUAGE.into()),
9514 )));
9515 update_test_language_settings(cx, |settings| {
9516 // Enable Prettier formatting for the same buffer, and ensure
9517 // LSP is called instead of Prettier.
9518 settings.defaults.prettier = Some(PrettierSettings {
9519 allowed: true,
9520 ..PrettierSettings::default()
9521 });
9522 });
9523 let mut fake_servers = language_registry.register_fake_lsp(
9524 "Rust",
9525 FakeLspAdapter {
9526 capabilities: lsp::ServerCapabilities {
9527 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9528 ..Default::default()
9529 },
9530 ..Default::default()
9531 },
9532 );
9533
9534 let buffer = project
9535 .update(cx, |project, cx| {
9536 project.open_local_buffer(path!("/file.rs"), cx)
9537 })
9538 .await
9539 .unwrap();
9540
9541 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9542 let (editor, cx) = cx.add_window_view(|window, cx| {
9543 build_editor_with_project(project.clone(), buffer, window, cx)
9544 });
9545 editor.update_in(cx, |editor, window, cx| {
9546 editor.set_text("one\ntwo\nthree\n", window, cx)
9547 });
9548
9549 cx.executor().start_waiting();
9550 let fake_server = fake_servers.next().await.unwrap();
9551
9552 let format = editor
9553 .update_in(cx, |editor, window, cx| {
9554 editor.perform_format(
9555 project.clone(),
9556 FormatTrigger::Manual,
9557 FormatTarget::Buffers,
9558 window,
9559 cx,
9560 )
9561 })
9562 .unwrap();
9563 fake_server
9564 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9565 assert_eq!(
9566 params.text_document.uri,
9567 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9568 );
9569 assert_eq!(params.options.tab_size, 4);
9570 Ok(Some(vec![lsp::TextEdit::new(
9571 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9572 ", ".to_string(),
9573 )]))
9574 })
9575 .next()
9576 .await;
9577 cx.executor().start_waiting();
9578 format.await;
9579 assert_eq!(
9580 editor.update(cx, |editor, cx| editor.text(cx)),
9581 "one, two\nthree\n"
9582 );
9583
9584 editor.update_in(cx, |editor, window, cx| {
9585 editor.set_text("one\ntwo\nthree\n", window, cx)
9586 });
9587 // Ensure we don't lock if formatting hangs.
9588 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9589 move |params, _| async move {
9590 assert_eq!(
9591 params.text_document.uri,
9592 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9593 );
9594 futures::future::pending::<()>().await;
9595 unreachable!()
9596 },
9597 );
9598 let format = editor
9599 .update_in(cx, |editor, window, cx| {
9600 editor.perform_format(
9601 project,
9602 FormatTrigger::Manual,
9603 FormatTarget::Buffers,
9604 window,
9605 cx,
9606 )
9607 })
9608 .unwrap();
9609 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9610 cx.executor().start_waiting();
9611 format.await;
9612 assert_eq!(
9613 editor.update(cx, |editor, cx| editor.text(cx)),
9614 "one\ntwo\nthree\n"
9615 );
9616}
9617
9618#[gpui::test]
9619async fn test_multiple_formatters(cx: &mut TestAppContext) {
9620 init_test(cx, |settings| {
9621 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
9622 settings.defaults.formatter =
9623 Some(language_settings::SelectedFormatter::List(FormatterList(
9624 vec![
9625 Formatter::LanguageServer { name: None },
9626 Formatter::CodeActions(
9627 [
9628 ("code-action-1".into(), true),
9629 ("code-action-2".into(), true),
9630 ]
9631 .into_iter()
9632 .collect(),
9633 ),
9634 ]
9635 .into(),
9636 )))
9637 });
9638
9639 let fs = FakeFs::new(cx.executor());
9640 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
9641 .await;
9642
9643 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9644 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9645 language_registry.add(rust_lang());
9646
9647 let mut fake_servers = language_registry.register_fake_lsp(
9648 "Rust",
9649 FakeLspAdapter {
9650 capabilities: lsp::ServerCapabilities {
9651 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9652 execute_command_provider: Some(lsp::ExecuteCommandOptions {
9653 commands: vec!["the-command-for-code-action-1".into()],
9654 ..Default::default()
9655 }),
9656 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9657 ..Default::default()
9658 },
9659 ..Default::default()
9660 },
9661 );
9662
9663 let buffer = project
9664 .update(cx, |project, cx| {
9665 project.open_local_buffer(path!("/file.rs"), cx)
9666 })
9667 .await
9668 .unwrap();
9669
9670 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9671 let (editor, cx) = cx.add_window_view(|window, cx| {
9672 build_editor_with_project(project.clone(), buffer, window, cx)
9673 });
9674
9675 cx.executor().start_waiting();
9676
9677 let fake_server = fake_servers.next().await.unwrap();
9678 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9679 move |_params, _| async move {
9680 Ok(Some(vec![lsp::TextEdit::new(
9681 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9682 "applied-formatting\n".to_string(),
9683 )]))
9684 },
9685 );
9686 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9687 move |params, _| async move {
9688 assert_eq!(
9689 params.context.only,
9690 Some(vec!["code-action-1".into(), "code-action-2".into()])
9691 );
9692 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
9693 Ok(Some(vec![
9694 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9695 kind: Some("code-action-1".into()),
9696 edit: Some(lsp::WorkspaceEdit::new(
9697 [(
9698 uri.clone(),
9699 vec![lsp::TextEdit::new(
9700 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9701 "applied-code-action-1-edit\n".to_string(),
9702 )],
9703 )]
9704 .into_iter()
9705 .collect(),
9706 )),
9707 command: Some(lsp::Command {
9708 command: "the-command-for-code-action-1".into(),
9709 ..Default::default()
9710 }),
9711 ..Default::default()
9712 }),
9713 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9714 kind: Some("code-action-2".into()),
9715 edit: Some(lsp::WorkspaceEdit::new(
9716 [(
9717 uri.clone(),
9718 vec![lsp::TextEdit::new(
9719 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9720 "applied-code-action-2-edit\n".to_string(),
9721 )],
9722 )]
9723 .into_iter()
9724 .collect(),
9725 )),
9726 ..Default::default()
9727 }),
9728 ]))
9729 },
9730 );
9731
9732 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
9733 move |params, _| async move { Ok(params) }
9734 });
9735
9736 let command_lock = Arc::new(futures::lock::Mutex::new(()));
9737 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
9738 let fake = fake_server.clone();
9739 let lock = command_lock.clone();
9740 move |params, _| {
9741 assert_eq!(params.command, "the-command-for-code-action-1");
9742 let fake = fake.clone();
9743 let lock = lock.clone();
9744 async move {
9745 lock.lock().await;
9746 fake.server
9747 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
9748 label: None,
9749 edit: lsp::WorkspaceEdit {
9750 changes: Some(
9751 [(
9752 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
9753 vec![lsp::TextEdit {
9754 range: lsp::Range::new(
9755 lsp::Position::new(0, 0),
9756 lsp::Position::new(0, 0),
9757 ),
9758 new_text: "applied-code-action-1-command\n".into(),
9759 }],
9760 )]
9761 .into_iter()
9762 .collect(),
9763 ),
9764 ..Default::default()
9765 },
9766 })
9767 .await
9768 .into_response()
9769 .unwrap();
9770 Ok(Some(json!(null)))
9771 }
9772 }
9773 });
9774
9775 cx.executor().start_waiting();
9776 editor
9777 .update_in(cx, |editor, window, cx| {
9778 editor.perform_format(
9779 project.clone(),
9780 FormatTrigger::Manual,
9781 FormatTarget::Buffers,
9782 window,
9783 cx,
9784 )
9785 })
9786 .unwrap()
9787 .await;
9788 editor.update(cx, |editor, cx| {
9789 assert_eq!(
9790 editor.text(cx),
9791 r#"
9792 applied-code-action-2-edit
9793 applied-code-action-1-command
9794 applied-code-action-1-edit
9795 applied-formatting
9796 one
9797 two
9798 three
9799 "#
9800 .unindent()
9801 );
9802 });
9803
9804 editor.update_in(cx, |editor, window, cx| {
9805 editor.undo(&Default::default(), window, cx);
9806 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9807 });
9808
9809 // Perform a manual edit while waiting for an LSP command
9810 // that's being run as part of a formatting code action.
9811 let lock_guard = command_lock.lock().await;
9812 let format = editor
9813 .update_in(cx, |editor, window, cx| {
9814 editor.perform_format(
9815 project.clone(),
9816 FormatTrigger::Manual,
9817 FormatTarget::Buffers,
9818 window,
9819 cx,
9820 )
9821 })
9822 .unwrap();
9823 cx.run_until_parked();
9824 editor.update(cx, |editor, cx| {
9825 assert_eq!(
9826 editor.text(cx),
9827 r#"
9828 applied-code-action-1-edit
9829 applied-formatting
9830 one
9831 two
9832 three
9833 "#
9834 .unindent()
9835 );
9836
9837 editor.buffer.update(cx, |buffer, cx| {
9838 let ix = buffer.len(cx);
9839 buffer.edit([(ix..ix, "edited\n")], None, cx);
9840 });
9841 });
9842
9843 // Allow the LSP command to proceed. Because the buffer was edited,
9844 // the second code action will not be run.
9845 drop(lock_guard);
9846 format.await;
9847 editor.update_in(cx, |editor, window, cx| {
9848 assert_eq!(
9849 editor.text(cx),
9850 r#"
9851 applied-code-action-1-command
9852 applied-code-action-1-edit
9853 applied-formatting
9854 one
9855 two
9856 three
9857 edited
9858 "#
9859 .unindent()
9860 );
9861
9862 // The manual edit is undone first, because it is the last thing the user did
9863 // (even though the command completed afterwards).
9864 editor.undo(&Default::default(), window, cx);
9865 assert_eq!(
9866 editor.text(cx),
9867 r#"
9868 applied-code-action-1-command
9869 applied-code-action-1-edit
9870 applied-formatting
9871 one
9872 two
9873 three
9874 "#
9875 .unindent()
9876 );
9877
9878 // All the formatting (including the command, which completed after the manual edit)
9879 // is undone together.
9880 editor.undo(&Default::default(), window, cx);
9881 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9882 });
9883}
9884
9885#[gpui::test]
9886async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
9887 init_test(cx, |settings| {
9888 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
9889 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
9890 ))
9891 });
9892
9893 let fs = FakeFs::new(cx.executor());
9894 fs.insert_file(path!("/file.ts"), Default::default()).await;
9895
9896 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9897
9898 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9899 language_registry.add(Arc::new(Language::new(
9900 LanguageConfig {
9901 name: "TypeScript".into(),
9902 matcher: LanguageMatcher {
9903 path_suffixes: vec!["ts".to_string()],
9904 ..Default::default()
9905 },
9906 ..LanguageConfig::default()
9907 },
9908 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
9909 )));
9910 update_test_language_settings(cx, |settings| {
9911 settings.defaults.prettier = Some(PrettierSettings {
9912 allowed: true,
9913 ..PrettierSettings::default()
9914 });
9915 });
9916 let mut fake_servers = language_registry.register_fake_lsp(
9917 "TypeScript",
9918 FakeLspAdapter {
9919 capabilities: lsp::ServerCapabilities {
9920 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9921 ..Default::default()
9922 },
9923 ..Default::default()
9924 },
9925 );
9926
9927 let buffer = project
9928 .update(cx, |project, cx| {
9929 project.open_local_buffer(path!("/file.ts"), cx)
9930 })
9931 .await
9932 .unwrap();
9933
9934 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9935 let (editor, cx) = cx.add_window_view(|window, cx| {
9936 build_editor_with_project(project.clone(), buffer, window, cx)
9937 });
9938 editor.update_in(cx, |editor, window, cx| {
9939 editor.set_text(
9940 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9941 window,
9942 cx,
9943 )
9944 });
9945
9946 cx.executor().start_waiting();
9947 let fake_server = fake_servers.next().await.unwrap();
9948
9949 let format = editor
9950 .update_in(cx, |editor, window, cx| {
9951 editor.perform_code_action_kind(
9952 project.clone(),
9953 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9954 window,
9955 cx,
9956 )
9957 })
9958 .unwrap();
9959 fake_server
9960 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
9961 assert_eq!(
9962 params.text_document.uri,
9963 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9964 );
9965 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
9966 lsp::CodeAction {
9967 title: "Organize Imports".to_string(),
9968 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
9969 edit: Some(lsp::WorkspaceEdit {
9970 changes: Some(
9971 [(
9972 params.text_document.uri.clone(),
9973 vec![lsp::TextEdit::new(
9974 lsp::Range::new(
9975 lsp::Position::new(1, 0),
9976 lsp::Position::new(2, 0),
9977 ),
9978 "".to_string(),
9979 )],
9980 )]
9981 .into_iter()
9982 .collect(),
9983 ),
9984 ..Default::default()
9985 }),
9986 ..Default::default()
9987 },
9988 )]))
9989 })
9990 .next()
9991 .await;
9992 cx.executor().start_waiting();
9993 format.await;
9994 assert_eq!(
9995 editor.update(cx, |editor, cx| editor.text(cx)),
9996 "import { a } from 'module';\n\nconst x = a;\n"
9997 );
9998
9999 editor.update_in(cx, |editor, window, cx| {
10000 editor.set_text(
10001 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10002 window,
10003 cx,
10004 )
10005 });
10006 // Ensure we don't lock if code action hangs.
10007 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10008 move |params, _| async move {
10009 assert_eq!(
10010 params.text_document.uri,
10011 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10012 );
10013 futures::future::pending::<()>().await;
10014 unreachable!()
10015 },
10016 );
10017 let format = editor
10018 .update_in(cx, |editor, window, cx| {
10019 editor.perform_code_action_kind(
10020 project,
10021 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10022 window,
10023 cx,
10024 )
10025 })
10026 .unwrap();
10027 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
10028 cx.executor().start_waiting();
10029 format.await;
10030 assert_eq!(
10031 editor.update(cx, |editor, cx| editor.text(cx)),
10032 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
10033 );
10034}
10035
10036#[gpui::test]
10037async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
10038 init_test(cx, |_| {});
10039
10040 let mut cx = EditorLspTestContext::new_rust(
10041 lsp::ServerCapabilities {
10042 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10043 ..Default::default()
10044 },
10045 cx,
10046 )
10047 .await;
10048
10049 cx.set_state(indoc! {"
10050 one.twoˇ
10051 "});
10052
10053 // The format request takes a long time. When it completes, it inserts
10054 // a newline and an indent before the `.`
10055 cx.lsp
10056 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
10057 let executor = cx.background_executor().clone();
10058 async move {
10059 executor.timer(Duration::from_millis(100)).await;
10060 Ok(Some(vec![lsp::TextEdit {
10061 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
10062 new_text: "\n ".into(),
10063 }]))
10064 }
10065 });
10066
10067 // Submit a format request.
10068 let format_1 = cx
10069 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10070 .unwrap();
10071 cx.executor().run_until_parked();
10072
10073 // Submit a second format request.
10074 let format_2 = cx
10075 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10076 .unwrap();
10077 cx.executor().run_until_parked();
10078
10079 // Wait for both format requests to complete
10080 cx.executor().advance_clock(Duration::from_millis(200));
10081 cx.executor().start_waiting();
10082 format_1.await.unwrap();
10083 cx.executor().start_waiting();
10084 format_2.await.unwrap();
10085
10086 // The formatting edits only happens once.
10087 cx.assert_editor_state(indoc! {"
10088 one
10089 .twoˇ
10090 "});
10091}
10092
10093#[gpui::test]
10094async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
10095 init_test(cx, |settings| {
10096 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
10097 });
10098
10099 let mut cx = EditorLspTestContext::new_rust(
10100 lsp::ServerCapabilities {
10101 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10102 ..Default::default()
10103 },
10104 cx,
10105 )
10106 .await;
10107
10108 // Set up a buffer white some trailing whitespace and no trailing newline.
10109 cx.set_state(
10110 &[
10111 "one ", //
10112 "twoˇ", //
10113 "three ", //
10114 "four", //
10115 ]
10116 .join("\n"),
10117 );
10118
10119 // Submit a format request.
10120 let format = cx
10121 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10122 .unwrap();
10123
10124 // Record which buffer changes have been sent to the language server
10125 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
10126 cx.lsp
10127 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
10128 let buffer_changes = buffer_changes.clone();
10129 move |params, _| {
10130 buffer_changes.lock().extend(
10131 params
10132 .content_changes
10133 .into_iter()
10134 .map(|e| (e.range.unwrap(), e.text)),
10135 );
10136 }
10137 });
10138
10139 // Handle formatting requests to the language server.
10140 cx.lsp
10141 .set_request_handler::<lsp::request::Formatting, _, _>({
10142 let buffer_changes = buffer_changes.clone();
10143 move |_, _| {
10144 // When formatting is requested, trailing whitespace has already been stripped,
10145 // and the trailing newline has already been added.
10146 assert_eq!(
10147 &buffer_changes.lock()[1..],
10148 &[
10149 (
10150 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
10151 "".into()
10152 ),
10153 (
10154 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
10155 "".into()
10156 ),
10157 (
10158 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
10159 "\n".into()
10160 ),
10161 ]
10162 );
10163
10164 // Insert blank lines between each line of the buffer.
10165 async move {
10166 Ok(Some(vec![
10167 lsp::TextEdit {
10168 range: lsp::Range::new(
10169 lsp::Position::new(1, 0),
10170 lsp::Position::new(1, 0),
10171 ),
10172 new_text: "\n".into(),
10173 },
10174 lsp::TextEdit {
10175 range: lsp::Range::new(
10176 lsp::Position::new(2, 0),
10177 lsp::Position::new(2, 0),
10178 ),
10179 new_text: "\n".into(),
10180 },
10181 ]))
10182 }
10183 }
10184 });
10185
10186 // After formatting the buffer, the trailing whitespace is stripped,
10187 // a newline is appended, and the edits provided by the language server
10188 // have been applied.
10189 format.await.unwrap();
10190 cx.assert_editor_state(
10191 &[
10192 "one", //
10193 "", //
10194 "twoˇ", //
10195 "", //
10196 "three", //
10197 "four", //
10198 "", //
10199 ]
10200 .join("\n"),
10201 );
10202
10203 // Undoing the formatting undoes the trailing whitespace removal, the
10204 // trailing newline, and the LSP edits.
10205 cx.update_buffer(|buffer, cx| buffer.undo(cx));
10206 cx.assert_editor_state(
10207 &[
10208 "one ", //
10209 "twoˇ", //
10210 "three ", //
10211 "four", //
10212 ]
10213 .join("\n"),
10214 );
10215}
10216
10217#[gpui::test]
10218async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
10219 cx: &mut TestAppContext,
10220) {
10221 init_test(cx, |_| {});
10222
10223 cx.update(|cx| {
10224 cx.update_global::<SettingsStore, _>(|settings, cx| {
10225 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10226 settings.auto_signature_help = Some(true);
10227 });
10228 });
10229 });
10230
10231 let mut cx = EditorLspTestContext::new_rust(
10232 lsp::ServerCapabilities {
10233 signature_help_provider: Some(lsp::SignatureHelpOptions {
10234 ..Default::default()
10235 }),
10236 ..Default::default()
10237 },
10238 cx,
10239 )
10240 .await;
10241
10242 let language = Language::new(
10243 LanguageConfig {
10244 name: "Rust".into(),
10245 brackets: BracketPairConfig {
10246 pairs: vec![
10247 BracketPair {
10248 start: "{".to_string(),
10249 end: "}".to_string(),
10250 close: true,
10251 surround: true,
10252 newline: true,
10253 },
10254 BracketPair {
10255 start: "(".to_string(),
10256 end: ")".to_string(),
10257 close: true,
10258 surround: true,
10259 newline: true,
10260 },
10261 BracketPair {
10262 start: "/*".to_string(),
10263 end: " */".to_string(),
10264 close: true,
10265 surround: true,
10266 newline: true,
10267 },
10268 BracketPair {
10269 start: "[".to_string(),
10270 end: "]".to_string(),
10271 close: false,
10272 surround: false,
10273 newline: true,
10274 },
10275 BracketPair {
10276 start: "\"".to_string(),
10277 end: "\"".to_string(),
10278 close: true,
10279 surround: true,
10280 newline: false,
10281 },
10282 BracketPair {
10283 start: "<".to_string(),
10284 end: ">".to_string(),
10285 close: false,
10286 surround: true,
10287 newline: true,
10288 },
10289 ],
10290 ..Default::default()
10291 },
10292 autoclose_before: "})]".to_string(),
10293 ..Default::default()
10294 },
10295 Some(tree_sitter_rust::LANGUAGE.into()),
10296 );
10297 let language = Arc::new(language);
10298
10299 cx.language_registry().add(language.clone());
10300 cx.update_buffer(|buffer, cx| {
10301 buffer.set_language(Some(language), cx);
10302 });
10303
10304 cx.set_state(
10305 &r#"
10306 fn main() {
10307 sampleˇ
10308 }
10309 "#
10310 .unindent(),
10311 );
10312
10313 cx.update_editor(|editor, window, cx| {
10314 editor.handle_input("(", window, cx);
10315 });
10316 cx.assert_editor_state(
10317 &"
10318 fn main() {
10319 sample(ˇ)
10320 }
10321 "
10322 .unindent(),
10323 );
10324
10325 let mocked_response = lsp::SignatureHelp {
10326 signatures: vec![lsp::SignatureInformation {
10327 label: "fn sample(param1: u8, param2: u8)".to_string(),
10328 documentation: None,
10329 parameters: Some(vec![
10330 lsp::ParameterInformation {
10331 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10332 documentation: None,
10333 },
10334 lsp::ParameterInformation {
10335 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10336 documentation: None,
10337 },
10338 ]),
10339 active_parameter: None,
10340 }],
10341 active_signature: Some(0),
10342 active_parameter: Some(0),
10343 };
10344 handle_signature_help_request(&mut cx, mocked_response).await;
10345
10346 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10347 .await;
10348
10349 cx.editor(|editor, _, _| {
10350 let signature_help_state = editor.signature_help_state.popover().cloned();
10351 assert_eq!(
10352 signature_help_state.unwrap().label,
10353 "param1: u8, param2: u8"
10354 );
10355 });
10356}
10357
10358#[gpui::test]
10359async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
10360 init_test(cx, |_| {});
10361
10362 cx.update(|cx| {
10363 cx.update_global::<SettingsStore, _>(|settings, cx| {
10364 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10365 settings.auto_signature_help = Some(false);
10366 settings.show_signature_help_after_edits = Some(false);
10367 });
10368 });
10369 });
10370
10371 let mut cx = EditorLspTestContext::new_rust(
10372 lsp::ServerCapabilities {
10373 signature_help_provider: Some(lsp::SignatureHelpOptions {
10374 ..Default::default()
10375 }),
10376 ..Default::default()
10377 },
10378 cx,
10379 )
10380 .await;
10381
10382 let language = Language::new(
10383 LanguageConfig {
10384 name: "Rust".into(),
10385 brackets: BracketPairConfig {
10386 pairs: vec![
10387 BracketPair {
10388 start: "{".to_string(),
10389 end: "}".to_string(),
10390 close: true,
10391 surround: true,
10392 newline: true,
10393 },
10394 BracketPair {
10395 start: "(".to_string(),
10396 end: ")".to_string(),
10397 close: true,
10398 surround: true,
10399 newline: true,
10400 },
10401 BracketPair {
10402 start: "/*".to_string(),
10403 end: " */".to_string(),
10404 close: true,
10405 surround: true,
10406 newline: true,
10407 },
10408 BracketPair {
10409 start: "[".to_string(),
10410 end: "]".to_string(),
10411 close: false,
10412 surround: false,
10413 newline: true,
10414 },
10415 BracketPair {
10416 start: "\"".to_string(),
10417 end: "\"".to_string(),
10418 close: true,
10419 surround: true,
10420 newline: false,
10421 },
10422 BracketPair {
10423 start: "<".to_string(),
10424 end: ">".to_string(),
10425 close: false,
10426 surround: true,
10427 newline: true,
10428 },
10429 ],
10430 ..Default::default()
10431 },
10432 autoclose_before: "})]".to_string(),
10433 ..Default::default()
10434 },
10435 Some(tree_sitter_rust::LANGUAGE.into()),
10436 );
10437 let language = Arc::new(language);
10438
10439 cx.language_registry().add(language.clone());
10440 cx.update_buffer(|buffer, cx| {
10441 buffer.set_language(Some(language), cx);
10442 });
10443
10444 // Ensure that signature_help is not called when no signature help is enabled.
10445 cx.set_state(
10446 &r#"
10447 fn main() {
10448 sampleˇ
10449 }
10450 "#
10451 .unindent(),
10452 );
10453 cx.update_editor(|editor, window, cx| {
10454 editor.handle_input("(", window, cx);
10455 });
10456 cx.assert_editor_state(
10457 &"
10458 fn main() {
10459 sample(ˇ)
10460 }
10461 "
10462 .unindent(),
10463 );
10464 cx.editor(|editor, _, _| {
10465 assert!(editor.signature_help_state.task().is_none());
10466 });
10467
10468 let mocked_response = lsp::SignatureHelp {
10469 signatures: vec![lsp::SignatureInformation {
10470 label: "fn sample(param1: u8, param2: u8)".to_string(),
10471 documentation: None,
10472 parameters: Some(vec![
10473 lsp::ParameterInformation {
10474 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10475 documentation: None,
10476 },
10477 lsp::ParameterInformation {
10478 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10479 documentation: None,
10480 },
10481 ]),
10482 active_parameter: None,
10483 }],
10484 active_signature: Some(0),
10485 active_parameter: Some(0),
10486 };
10487
10488 // Ensure that signature_help is called when enabled afte edits
10489 cx.update(|_, cx| {
10490 cx.update_global::<SettingsStore, _>(|settings, cx| {
10491 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10492 settings.auto_signature_help = Some(false);
10493 settings.show_signature_help_after_edits = Some(true);
10494 });
10495 });
10496 });
10497 cx.set_state(
10498 &r#"
10499 fn main() {
10500 sampleˇ
10501 }
10502 "#
10503 .unindent(),
10504 );
10505 cx.update_editor(|editor, window, cx| {
10506 editor.handle_input("(", window, cx);
10507 });
10508 cx.assert_editor_state(
10509 &"
10510 fn main() {
10511 sample(ˇ)
10512 }
10513 "
10514 .unindent(),
10515 );
10516 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10517 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10518 .await;
10519 cx.update_editor(|editor, _, _| {
10520 let signature_help_state = editor.signature_help_state.popover().cloned();
10521 assert!(signature_help_state.is_some());
10522 assert_eq!(
10523 signature_help_state.unwrap().label,
10524 "param1: u8, param2: u8"
10525 );
10526 editor.signature_help_state = SignatureHelpState::default();
10527 });
10528
10529 // Ensure that signature_help is called when auto signature help override is enabled
10530 cx.update(|_, cx| {
10531 cx.update_global::<SettingsStore, _>(|settings, cx| {
10532 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10533 settings.auto_signature_help = Some(true);
10534 settings.show_signature_help_after_edits = Some(false);
10535 });
10536 });
10537 });
10538 cx.set_state(
10539 &r#"
10540 fn main() {
10541 sampleˇ
10542 }
10543 "#
10544 .unindent(),
10545 );
10546 cx.update_editor(|editor, window, cx| {
10547 editor.handle_input("(", window, cx);
10548 });
10549 cx.assert_editor_state(
10550 &"
10551 fn main() {
10552 sample(ˇ)
10553 }
10554 "
10555 .unindent(),
10556 );
10557 handle_signature_help_request(&mut cx, mocked_response).await;
10558 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10559 .await;
10560 cx.editor(|editor, _, _| {
10561 let signature_help_state = editor.signature_help_state.popover().cloned();
10562 assert!(signature_help_state.is_some());
10563 assert_eq!(
10564 signature_help_state.unwrap().label,
10565 "param1: u8, param2: u8"
10566 );
10567 });
10568}
10569
10570#[gpui::test]
10571async fn test_signature_help(cx: &mut TestAppContext) {
10572 init_test(cx, |_| {});
10573 cx.update(|cx| {
10574 cx.update_global::<SettingsStore, _>(|settings, cx| {
10575 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10576 settings.auto_signature_help = Some(true);
10577 });
10578 });
10579 });
10580
10581 let mut cx = EditorLspTestContext::new_rust(
10582 lsp::ServerCapabilities {
10583 signature_help_provider: Some(lsp::SignatureHelpOptions {
10584 ..Default::default()
10585 }),
10586 ..Default::default()
10587 },
10588 cx,
10589 )
10590 .await;
10591
10592 // A test that directly calls `show_signature_help`
10593 cx.update_editor(|editor, window, cx| {
10594 editor.show_signature_help(&ShowSignatureHelp, window, cx);
10595 });
10596
10597 let mocked_response = lsp::SignatureHelp {
10598 signatures: vec![lsp::SignatureInformation {
10599 label: "fn sample(param1: u8, param2: u8)".to_string(),
10600 documentation: None,
10601 parameters: Some(vec![
10602 lsp::ParameterInformation {
10603 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10604 documentation: None,
10605 },
10606 lsp::ParameterInformation {
10607 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10608 documentation: None,
10609 },
10610 ]),
10611 active_parameter: None,
10612 }],
10613 active_signature: Some(0),
10614 active_parameter: Some(0),
10615 };
10616 handle_signature_help_request(&mut cx, mocked_response).await;
10617
10618 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10619 .await;
10620
10621 cx.editor(|editor, _, _| {
10622 let signature_help_state = editor.signature_help_state.popover().cloned();
10623 assert!(signature_help_state.is_some());
10624 assert_eq!(
10625 signature_help_state.unwrap().label,
10626 "param1: u8, param2: u8"
10627 );
10628 });
10629
10630 // When exiting outside from inside the brackets, `signature_help` is closed.
10631 cx.set_state(indoc! {"
10632 fn main() {
10633 sample(ˇ);
10634 }
10635
10636 fn sample(param1: u8, param2: u8) {}
10637 "});
10638
10639 cx.update_editor(|editor, window, cx| {
10640 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
10641 });
10642
10643 let mocked_response = lsp::SignatureHelp {
10644 signatures: Vec::new(),
10645 active_signature: None,
10646 active_parameter: None,
10647 };
10648 handle_signature_help_request(&mut cx, mocked_response).await;
10649
10650 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10651 .await;
10652
10653 cx.editor(|editor, _, _| {
10654 assert!(!editor.signature_help_state.is_shown());
10655 });
10656
10657 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
10658 cx.set_state(indoc! {"
10659 fn main() {
10660 sample(ˇ);
10661 }
10662
10663 fn sample(param1: u8, param2: u8) {}
10664 "});
10665
10666 let mocked_response = lsp::SignatureHelp {
10667 signatures: vec![lsp::SignatureInformation {
10668 label: "fn sample(param1: u8, param2: u8)".to_string(),
10669 documentation: None,
10670 parameters: Some(vec![
10671 lsp::ParameterInformation {
10672 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10673 documentation: None,
10674 },
10675 lsp::ParameterInformation {
10676 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10677 documentation: None,
10678 },
10679 ]),
10680 active_parameter: None,
10681 }],
10682 active_signature: Some(0),
10683 active_parameter: Some(0),
10684 };
10685 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10686 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10687 .await;
10688 cx.editor(|editor, _, _| {
10689 assert!(editor.signature_help_state.is_shown());
10690 });
10691
10692 // Restore the popover with more parameter input
10693 cx.set_state(indoc! {"
10694 fn main() {
10695 sample(param1, param2ˇ);
10696 }
10697
10698 fn sample(param1: u8, param2: u8) {}
10699 "});
10700
10701 let mocked_response = lsp::SignatureHelp {
10702 signatures: vec![lsp::SignatureInformation {
10703 label: "fn sample(param1: u8, param2: u8)".to_string(),
10704 documentation: None,
10705 parameters: Some(vec![
10706 lsp::ParameterInformation {
10707 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10708 documentation: None,
10709 },
10710 lsp::ParameterInformation {
10711 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10712 documentation: None,
10713 },
10714 ]),
10715 active_parameter: None,
10716 }],
10717 active_signature: Some(0),
10718 active_parameter: Some(1),
10719 };
10720 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10721 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10722 .await;
10723
10724 // When selecting a range, the popover is gone.
10725 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
10726 cx.update_editor(|editor, window, cx| {
10727 editor.change_selections(None, window, cx, |s| {
10728 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10729 })
10730 });
10731 cx.assert_editor_state(indoc! {"
10732 fn main() {
10733 sample(param1, «ˇparam2»);
10734 }
10735
10736 fn sample(param1: u8, param2: u8) {}
10737 "});
10738 cx.editor(|editor, _, _| {
10739 assert!(!editor.signature_help_state.is_shown());
10740 });
10741
10742 // When unselecting again, the popover is back if within the brackets.
10743 cx.update_editor(|editor, window, cx| {
10744 editor.change_selections(None, window, cx, |s| {
10745 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10746 })
10747 });
10748 cx.assert_editor_state(indoc! {"
10749 fn main() {
10750 sample(param1, ˇparam2);
10751 }
10752
10753 fn sample(param1: u8, param2: u8) {}
10754 "});
10755 handle_signature_help_request(&mut cx, mocked_response).await;
10756 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10757 .await;
10758 cx.editor(|editor, _, _| {
10759 assert!(editor.signature_help_state.is_shown());
10760 });
10761
10762 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
10763 cx.update_editor(|editor, window, cx| {
10764 editor.change_selections(None, window, cx, |s| {
10765 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
10766 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10767 })
10768 });
10769 cx.assert_editor_state(indoc! {"
10770 fn main() {
10771 sample(param1, ˇparam2);
10772 }
10773
10774 fn sample(param1: u8, param2: u8) {}
10775 "});
10776
10777 let mocked_response = lsp::SignatureHelp {
10778 signatures: vec![lsp::SignatureInformation {
10779 label: "fn sample(param1: u8, param2: u8)".to_string(),
10780 documentation: None,
10781 parameters: Some(vec![
10782 lsp::ParameterInformation {
10783 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10784 documentation: None,
10785 },
10786 lsp::ParameterInformation {
10787 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10788 documentation: None,
10789 },
10790 ]),
10791 active_parameter: None,
10792 }],
10793 active_signature: Some(0),
10794 active_parameter: Some(1),
10795 };
10796 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10797 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10798 .await;
10799 cx.update_editor(|editor, _, cx| {
10800 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
10801 });
10802 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10803 .await;
10804 cx.update_editor(|editor, window, cx| {
10805 editor.change_selections(None, window, cx, |s| {
10806 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10807 })
10808 });
10809 cx.assert_editor_state(indoc! {"
10810 fn main() {
10811 sample(param1, «ˇparam2»);
10812 }
10813
10814 fn sample(param1: u8, param2: u8) {}
10815 "});
10816 cx.update_editor(|editor, window, cx| {
10817 editor.change_selections(None, window, cx, |s| {
10818 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10819 })
10820 });
10821 cx.assert_editor_state(indoc! {"
10822 fn main() {
10823 sample(param1, ˇparam2);
10824 }
10825
10826 fn sample(param1: u8, param2: u8) {}
10827 "});
10828 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
10829 .await;
10830}
10831
10832#[gpui::test]
10833async fn test_completion_mode(cx: &mut TestAppContext) {
10834 init_test(cx, |_| {});
10835 let mut cx = EditorLspTestContext::new_rust(
10836 lsp::ServerCapabilities {
10837 completion_provider: Some(lsp::CompletionOptions {
10838 resolve_provider: Some(true),
10839 ..Default::default()
10840 }),
10841 ..Default::default()
10842 },
10843 cx,
10844 )
10845 .await;
10846
10847 struct Run {
10848 run_description: &'static str,
10849 initial_state: String,
10850 buffer_marked_text: String,
10851 completion_label: &'static str,
10852 completion_text: &'static str,
10853 expected_with_insert_mode: String,
10854 expected_with_replace_mode: String,
10855 expected_with_replace_subsequence_mode: String,
10856 expected_with_replace_suffix_mode: String,
10857 }
10858
10859 let runs = [
10860 Run {
10861 run_description: "Start of word matches completion text",
10862 initial_state: "before ediˇ after".into(),
10863 buffer_marked_text: "before <edi|> after".into(),
10864 completion_label: "editor",
10865 completion_text: "editor",
10866 expected_with_insert_mode: "before editorˇ after".into(),
10867 expected_with_replace_mode: "before editorˇ after".into(),
10868 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10869 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10870 },
10871 Run {
10872 run_description: "Accept same text at the middle of the word",
10873 initial_state: "before ediˇtor after".into(),
10874 buffer_marked_text: "before <edi|tor> after".into(),
10875 completion_label: "editor",
10876 completion_text: "editor",
10877 expected_with_insert_mode: "before editorˇtor after".into(),
10878 expected_with_replace_mode: "before editorˇ after".into(),
10879 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10880 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10881 },
10882 Run {
10883 run_description: "End of word matches completion text -- cursor at end",
10884 initial_state: "before torˇ after".into(),
10885 buffer_marked_text: "before <tor|> after".into(),
10886 completion_label: "editor",
10887 completion_text: "editor",
10888 expected_with_insert_mode: "before editorˇ after".into(),
10889 expected_with_replace_mode: "before editorˇ after".into(),
10890 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10891 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10892 },
10893 Run {
10894 run_description: "End of word matches completion text -- cursor at start",
10895 initial_state: "before ˇtor after".into(),
10896 buffer_marked_text: "before <|tor> after".into(),
10897 completion_label: "editor",
10898 completion_text: "editor",
10899 expected_with_insert_mode: "before editorˇtor after".into(),
10900 expected_with_replace_mode: "before editorˇ after".into(),
10901 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10902 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10903 },
10904 Run {
10905 run_description: "Prepend text containing whitespace",
10906 initial_state: "pˇfield: bool".into(),
10907 buffer_marked_text: "<p|field>: bool".into(),
10908 completion_label: "pub ",
10909 completion_text: "pub ",
10910 expected_with_insert_mode: "pub ˇfield: bool".into(),
10911 expected_with_replace_mode: "pub ˇ: bool".into(),
10912 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
10913 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
10914 },
10915 Run {
10916 run_description: "Add element to start of list",
10917 initial_state: "[element_ˇelement_2]".into(),
10918 buffer_marked_text: "[<element_|element_2>]".into(),
10919 completion_label: "element_1",
10920 completion_text: "element_1",
10921 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
10922 expected_with_replace_mode: "[element_1ˇ]".into(),
10923 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
10924 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
10925 },
10926 Run {
10927 run_description: "Add element to start of list -- first and second elements are equal",
10928 initial_state: "[elˇelement]".into(),
10929 buffer_marked_text: "[<el|element>]".into(),
10930 completion_label: "element",
10931 completion_text: "element",
10932 expected_with_insert_mode: "[elementˇelement]".into(),
10933 expected_with_replace_mode: "[elementˇ]".into(),
10934 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
10935 expected_with_replace_suffix_mode: "[elementˇ]".into(),
10936 },
10937 Run {
10938 run_description: "Ends with matching suffix",
10939 initial_state: "SubˇError".into(),
10940 buffer_marked_text: "<Sub|Error>".into(),
10941 completion_label: "SubscriptionError",
10942 completion_text: "SubscriptionError",
10943 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
10944 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10945 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10946 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
10947 },
10948 Run {
10949 run_description: "Suffix is a subsequence -- contiguous",
10950 initial_state: "SubˇErr".into(),
10951 buffer_marked_text: "<Sub|Err>".into(),
10952 completion_label: "SubscriptionError",
10953 completion_text: "SubscriptionError",
10954 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
10955 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10956 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10957 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
10958 },
10959 Run {
10960 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
10961 initial_state: "Suˇscrirr".into(),
10962 buffer_marked_text: "<Su|scrirr>".into(),
10963 completion_label: "SubscriptionError",
10964 completion_text: "SubscriptionError",
10965 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
10966 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10967 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10968 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
10969 },
10970 Run {
10971 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
10972 initial_state: "foo(indˇix)".into(),
10973 buffer_marked_text: "foo(<ind|ix>)".into(),
10974 completion_label: "node_index",
10975 completion_text: "node_index",
10976 expected_with_insert_mode: "foo(node_indexˇix)".into(),
10977 expected_with_replace_mode: "foo(node_indexˇ)".into(),
10978 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
10979 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
10980 },
10981 Run {
10982 run_description: "Replace range ends before cursor - should extend to cursor",
10983 initial_state: "before editˇo after".into(),
10984 buffer_marked_text: "before <{ed}>it|o after".into(),
10985 completion_label: "editor",
10986 completion_text: "editor",
10987 expected_with_insert_mode: "before editorˇo after".into(),
10988 expected_with_replace_mode: "before editorˇo after".into(),
10989 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
10990 expected_with_replace_suffix_mode: "before editorˇo after".into(),
10991 },
10992 Run {
10993 run_description: "Uses label for suffix matching",
10994 initial_state: "before ediˇtor after".into(),
10995 buffer_marked_text: "before <edi|tor> after".into(),
10996 completion_label: "editor",
10997 completion_text: "editor()",
10998 expected_with_insert_mode: "before editor()ˇtor after".into(),
10999 expected_with_replace_mode: "before editor()ˇ after".into(),
11000 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
11001 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
11002 },
11003 Run {
11004 run_description: "Case insensitive subsequence and suffix matching",
11005 initial_state: "before EDiˇtoR after".into(),
11006 buffer_marked_text: "before <EDi|toR> after".into(),
11007 completion_label: "editor",
11008 completion_text: "editor",
11009 expected_with_insert_mode: "before editorˇtoR after".into(),
11010 expected_with_replace_mode: "before editorˇ after".into(),
11011 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11012 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11013 },
11014 ];
11015
11016 for run in runs {
11017 let run_variations = [
11018 (LspInsertMode::Insert, run.expected_with_insert_mode),
11019 (LspInsertMode::Replace, run.expected_with_replace_mode),
11020 (
11021 LspInsertMode::ReplaceSubsequence,
11022 run.expected_with_replace_subsequence_mode,
11023 ),
11024 (
11025 LspInsertMode::ReplaceSuffix,
11026 run.expected_with_replace_suffix_mode,
11027 ),
11028 ];
11029
11030 for (lsp_insert_mode, expected_text) in run_variations {
11031 eprintln!(
11032 "run = {:?}, mode = {lsp_insert_mode:.?}",
11033 run.run_description,
11034 );
11035
11036 update_test_language_settings(&mut cx, |settings| {
11037 settings.defaults.completions = Some(CompletionSettings {
11038 lsp_insert_mode,
11039 words: WordsCompletionMode::Disabled,
11040 lsp: true,
11041 lsp_fetch_timeout_ms: 0,
11042 });
11043 });
11044
11045 cx.set_state(&run.initial_state);
11046 cx.update_editor(|editor, window, cx| {
11047 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11048 });
11049
11050 let counter = Arc::new(AtomicUsize::new(0));
11051 handle_completion_request_with_insert_and_replace(
11052 &mut cx,
11053 &run.buffer_marked_text,
11054 vec![(run.completion_label, run.completion_text)],
11055 counter.clone(),
11056 )
11057 .await;
11058 cx.condition(|editor, _| editor.context_menu_visible())
11059 .await;
11060 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11061
11062 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11063 editor
11064 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11065 .unwrap()
11066 });
11067 cx.assert_editor_state(&expected_text);
11068 handle_resolve_completion_request(&mut cx, None).await;
11069 apply_additional_edits.await.unwrap();
11070 }
11071 }
11072}
11073
11074#[gpui::test]
11075async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
11076 init_test(cx, |_| {});
11077 let mut cx = EditorLspTestContext::new_rust(
11078 lsp::ServerCapabilities {
11079 completion_provider: Some(lsp::CompletionOptions {
11080 resolve_provider: Some(true),
11081 ..Default::default()
11082 }),
11083 ..Default::default()
11084 },
11085 cx,
11086 )
11087 .await;
11088
11089 let initial_state = "SubˇError";
11090 let buffer_marked_text = "<Sub|Error>";
11091 let completion_text = "SubscriptionError";
11092 let expected_with_insert_mode = "SubscriptionErrorˇError";
11093 let expected_with_replace_mode = "SubscriptionErrorˇ";
11094
11095 update_test_language_settings(&mut cx, |settings| {
11096 settings.defaults.completions = Some(CompletionSettings {
11097 words: WordsCompletionMode::Disabled,
11098 // set the opposite here to ensure that the action is overriding the default behavior
11099 lsp_insert_mode: LspInsertMode::Insert,
11100 lsp: true,
11101 lsp_fetch_timeout_ms: 0,
11102 });
11103 });
11104
11105 cx.set_state(initial_state);
11106 cx.update_editor(|editor, window, cx| {
11107 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11108 });
11109
11110 let counter = Arc::new(AtomicUsize::new(0));
11111 handle_completion_request_with_insert_and_replace(
11112 &mut cx,
11113 &buffer_marked_text,
11114 vec![(completion_text, completion_text)],
11115 counter.clone(),
11116 )
11117 .await;
11118 cx.condition(|editor, _| editor.context_menu_visible())
11119 .await;
11120 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11121
11122 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11123 editor
11124 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11125 .unwrap()
11126 });
11127 cx.assert_editor_state(&expected_with_replace_mode);
11128 handle_resolve_completion_request(&mut cx, None).await;
11129 apply_additional_edits.await.unwrap();
11130
11131 update_test_language_settings(&mut cx, |settings| {
11132 settings.defaults.completions = Some(CompletionSettings {
11133 words: WordsCompletionMode::Disabled,
11134 // set the opposite here to ensure that the action is overriding the default behavior
11135 lsp_insert_mode: LspInsertMode::Replace,
11136 lsp: true,
11137 lsp_fetch_timeout_ms: 0,
11138 });
11139 });
11140
11141 cx.set_state(initial_state);
11142 cx.update_editor(|editor, window, cx| {
11143 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11144 });
11145 handle_completion_request_with_insert_and_replace(
11146 &mut cx,
11147 &buffer_marked_text,
11148 vec![(completion_text, completion_text)],
11149 counter.clone(),
11150 )
11151 .await;
11152 cx.condition(|editor, _| editor.context_menu_visible())
11153 .await;
11154 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11155
11156 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11157 editor
11158 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
11159 .unwrap()
11160 });
11161 cx.assert_editor_state(&expected_with_insert_mode);
11162 handle_resolve_completion_request(&mut cx, None).await;
11163 apply_additional_edits.await.unwrap();
11164}
11165
11166#[gpui::test]
11167async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
11168 init_test(cx, |_| {});
11169 let mut cx = EditorLspTestContext::new_rust(
11170 lsp::ServerCapabilities {
11171 completion_provider: Some(lsp::CompletionOptions {
11172 resolve_provider: Some(true),
11173 ..Default::default()
11174 }),
11175 ..Default::default()
11176 },
11177 cx,
11178 )
11179 .await;
11180
11181 // scenario: surrounding text matches completion text
11182 let completion_text = "to_offset";
11183 let initial_state = indoc! {"
11184 1. buf.to_offˇsuffix
11185 2. buf.to_offˇsuf
11186 3. buf.to_offˇfix
11187 4. buf.to_offˇ
11188 5. into_offˇensive
11189 6. ˇsuffix
11190 7. let ˇ //
11191 8. aaˇzz
11192 9. buf.to_off«zzzzzˇ»suffix
11193 10. buf.«ˇzzzzz»suffix
11194 11. to_off«ˇzzzzz»
11195
11196 buf.to_offˇsuffix // newest cursor
11197 "};
11198 let completion_marked_buffer = indoc! {"
11199 1. buf.to_offsuffix
11200 2. buf.to_offsuf
11201 3. buf.to_offfix
11202 4. buf.to_off
11203 5. into_offensive
11204 6. suffix
11205 7. let //
11206 8. aazz
11207 9. buf.to_offzzzzzsuffix
11208 10. buf.zzzzzsuffix
11209 11. to_offzzzzz
11210
11211 buf.<to_off|suffix> // newest cursor
11212 "};
11213 let expected = indoc! {"
11214 1. buf.to_offsetˇ
11215 2. buf.to_offsetˇsuf
11216 3. buf.to_offsetˇfix
11217 4. buf.to_offsetˇ
11218 5. into_offsetˇensive
11219 6. to_offsetˇsuffix
11220 7. let to_offsetˇ //
11221 8. aato_offsetˇzz
11222 9. buf.to_offsetˇ
11223 10. buf.to_offsetˇsuffix
11224 11. to_offsetˇ
11225
11226 buf.to_offsetˇ // newest cursor
11227 "};
11228 cx.set_state(initial_state);
11229 cx.update_editor(|editor, window, cx| {
11230 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11231 });
11232 handle_completion_request_with_insert_and_replace(
11233 &mut cx,
11234 completion_marked_buffer,
11235 vec![(completion_text, completion_text)],
11236 Arc::new(AtomicUsize::new(0)),
11237 )
11238 .await;
11239 cx.condition(|editor, _| editor.context_menu_visible())
11240 .await;
11241 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11242 editor
11243 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11244 .unwrap()
11245 });
11246 cx.assert_editor_state(expected);
11247 handle_resolve_completion_request(&mut cx, None).await;
11248 apply_additional_edits.await.unwrap();
11249
11250 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
11251 let completion_text = "foo_and_bar";
11252 let initial_state = indoc! {"
11253 1. ooanbˇ
11254 2. zooanbˇ
11255 3. ooanbˇz
11256 4. zooanbˇz
11257 5. ooanˇ
11258 6. oanbˇ
11259
11260 ooanbˇ
11261 "};
11262 let completion_marked_buffer = indoc! {"
11263 1. ooanb
11264 2. zooanb
11265 3. ooanbz
11266 4. zooanbz
11267 5. ooan
11268 6. oanb
11269
11270 <ooanb|>
11271 "};
11272 let expected = indoc! {"
11273 1. foo_and_barˇ
11274 2. zfoo_and_barˇ
11275 3. foo_and_barˇz
11276 4. zfoo_and_barˇz
11277 5. ooanfoo_and_barˇ
11278 6. oanbfoo_and_barˇ
11279
11280 foo_and_barˇ
11281 "};
11282 cx.set_state(initial_state);
11283 cx.update_editor(|editor, window, cx| {
11284 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11285 });
11286 handle_completion_request_with_insert_and_replace(
11287 &mut cx,
11288 completion_marked_buffer,
11289 vec![(completion_text, completion_text)],
11290 Arc::new(AtomicUsize::new(0)),
11291 )
11292 .await;
11293 cx.condition(|editor, _| editor.context_menu_visible())
11294 .await;
11295 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11296 editor
11297 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11298 .unwrap()
11299 });
11300 cx.assert_editor_state(expected);
11301 handle_resolve_completion_request(&mut cx, None).await;
11302 apply_additional_edits.await.unwrap();
11303
11304 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
11305 // (expects the same as if it was inserted at the end)
11306 let completion_text = "foo_and_bar";
11307 let initial_state = indoc! {"
11308 1. ooˇanb
11309 2. zooˇanb
11310 3. ooˇanbz
11311 4. zooˇanbz
11312
11313 ooˇanb
11314 "};
11315 let completion_marked_buffer = indoc! {"
11316 1. ooanb
11317 2. zooanb
11318 3. ooanbz
11319 4. zooanbz
11320
11321 <oo|anb>
11322 "};
11323 let expected = indoc! {"
11324 1. foo_and_barˇ
11325 2. zfoo_and_barˇ
11326 3. foo_and_barˇz
11327 4. zfoo_and_barˇz
11328
11329 foo_and_barˇ
11330 "};
11331 cx.set_state(initial_state);
11332 cx.update_editor(|editor, window, cx| {
11333 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11334 });
11335 handle_completion_request_with_insert_and_replace(
11336 &mut cx,
11337 completion_marked_buffer,
11338 vec![(completion_text, completion_text)],
11339 Arc::new(AtomicUsize::new(0)),
11340 )
11341 .await;
11342 cx.condition(|editor, _| editor.context_menu_visible())
11343 .await;
11344 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11345 editor
11346 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11347 .unwrap()
11348 });
11349 cx.assert_editor_state(expected);
11350 handle_resolve_completion_request(&mut cx, None).await;
11351 apply_additional_edits.await.unwrap();
11352}
11353
11354// This used to crash
11355#[gpui::test]
11356async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
11357 init_test(cx, |_| {});
11358
11359 let buffer_text = indoc! {"
11360 fn main() {
11361 10.satu;
11362
11363 //
11364 // separate cursors so they open in different excerpts (manually reproducible)
11365 //
11366
11367 10.satu20;
11368 }
11369 "};
11370 let multibuffer_text_with_selections = indoc! {"
11371 fn main() {
11372 10.satuˇ;
11373
11374 //
11375
11376 //
11377
11378 10.satuˇ20;
11379 }
11380 "};
11381 let expected_multibuffer = indoc! {"
11382 fn main() {
11383 10.saturating_sub()ˇ;
11384
11385 //
11386
11387 //
11388
11389 10.saturating_sub()ˇ;
11390 }
11391 "};
11392
11393 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
11394 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
11395
11396 let fs = FakeFs::new(cx.executor());
11397 fs.insert_tree(
11398 path!("/a"),
11399 json!({
11400 "main.rs": buffer_text,
11401 }),
11402 )
11403 .await;
11404
11405 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11406 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11407 language_registry.add(rust_lang());
11408 let mut fake_servers = language_registry.register_fake_lsp(
11409 "Rust",
11410 FakeLspAdapter {
11411 capabilities: lsp::ServerCapabilities {
11412 completion_provider: Some(lsp::CompletionOptions {
11413 resolve_provider: None,
11414 ..lsp::CompletionOptions::default()
11415 }),
11416 ..lsp::ServerCapabilities::default()
11417 },
11418 ..FakeLspAdapter::default()
11419 },
11420 );
11421 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11422 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11423 let buffer = project
11424 .update(cx, |project, cx| {
11425 project.open_local_buffer(path!("/a/main.rs"), cx)
11426 })
11427 .await
11428 .unwrap();
11429
11430 let multi_buffer = cx.new(|cx| {
11431 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
11432 multi_buffer.push_excerpts(
11433 buffer.clone(),
11434 [ExcerptRange::new(0..first_excerpt_end)],
11435 cx,
11436 );
11437 multi_buffer.push_excerpts(
11438 buffer.clone(),
11439 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
11440 cx,
11441 );
11442 multi_buffer
11443 });
11444
11445 let editor = workspace
11446 .update(cx, |_, window, cx| {
11447 cx.new(|cx| {
11448 Editor::new(
11449 EditorMode::Full {
11450 scale_ui_elements_with_buffer_font_size: false,
11451 show_active_line_background: false,
11452 sized_by_content: false,
11453 },
11454 multi_buffer.clone(),
11455 Some(project.clone()),
11456 window,
11457 cx,
11458 )
11459 })
11460 })
11461 .unwrap();
11462
11463 let pane = workspace
11464 .update(cx, |workspace, _, _| workspace.active_pane().clone())
11465 .unwrap();
11466 pane.update_in(cx, |pane, window, cx| {
11467 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
11468 });
11469
11470 let fake_server = fake_servers.next().await.unwrap();
11471
11472 editor.update_in(cx, |editor, window, cx| {
11473 editor.change_selections(None, window, cx, |s| {
11474 s.select_ranges([
11475 Point::new(1, 11)..Point::new(1, 11),
11476 Point::new(7, 11)..Point::new(7, 11),
11477 ])
11478 });
11479
11480 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
11481 });
11482
11483 editor.update_in(cx, |editor, window, cx| {
11484 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11485 });
11486
11487 fake_server
11488 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11489 let completion_item = lsp::CompletionItem {
11490 label: "saturating_sub()".into(),
11491 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
11492 lsp::InsertReplaceEdit {
11493 new_text: "saturating_sub()".to_owned(),
11494 insert: lsp::Range::new(
11495 lsp::Position::new(7, 7),
11496 lsp::Position::new(7, 11),
11497 ),
11498 replace: lsp::Range::new(
11499 lsp::Position::new(7, 7),
11500 lsp::Position::new(7, 13),
11501 ),
11502 },
11503 )),
11504 ..lsp::CompletionItem::default()
11505 };
11506
11507 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
11508 })
11509 .next()
11510 .await
11511 .unwrap();
11512
11513 cx.condition(&editor, |editor, _| editor.context_menu_visible())
11514 .await;
11515
11516 editor
11517 .update_in(cx, |editor, window, cx| {
11518 editor
11519 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11520 .unwrap()
11521 })
11522 .await
11523 .unwrap();
11524
11525 editor.update(cx, |editor, cx| {
11526 assert_text_with_selections(editor, expected_multibuffer, cx);
11527 })
11528}
11529
11530#[gpui::test]
11531async fn test_completion(cx: &mut TestAppContext) {
11532 init_test(cx, |_| {});
11533
11534 let mut cx = EditorLspTestContext::new_rust(
11535 lsp::ServerCapabilities {
11536 completion_provider: Some(lsp::CompletionOptions {
11537 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11538 resolve_provider: Some(true),
11539 ..Default::default()
11540 }),
11541 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11542 ..Default::default()
11543 },
11544 cx,
11545 )
11546 .await;
11547 let counter = Arc::new(AtomicUsize::new(0));
11548
11549 cx.set_state(indoc! {"
11550 oneˇ
11551 two
11552 three
11553 "});
11554 cx.simulate_keystroke(".");
11555 handle_completion_request(
11556 indoc! {"
11557 one.|<>
11558 two
11559 three
11560 "},
11561 vec!["first_completion", "second_completion"],
11562 true,
11563 counter.clone(),
11564 &mut cx,
11565 )
11566 .await;
11567 cx.condition(|editor, _| editor.context_menu_visible())
11568 .await;
11569 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11570
11571 let _handler = handle_signature_help_request(
11572 &mut cx,
11573 lsp::SignatureHelp {
11574 signatures: vec![lsp::SignatureInformation {
11575 label: "test signature".to_string(),
11576 documentation: None,
11577 parameters: Some(vec![lsp::ParameterInformation {
11578 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
11579 documentation: None,
11580 }]),
11581 active_parameter: None,
11582 }],
11583 active_signature: None,
11584 active_parameter: None,
11585 },
11586 );
11587 cx.update_editor(|editor, window, cx| {
11588 assert!(
11589 !editor.signature_help_state.is_shown(),
11590 "No signature help was called for"
11591 );
11592 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11593 });
11594 cx.run_until_parked();
11595 cx.update_editor(|editor, _, _| {
11596 assert!(
11597 !editor.signature_help_state.is_shown(),
11598 "No signature help should be shown when completions menu is open"
11599 );
11600 });
11601
11602 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11603 editor.context_menu_next(&Default::default(), window, cx);
11604 editor
11605 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11606 .unwrap()
11607 });
11608 cx.assert_editor_state(indoc! {"
11609 one.second_completionˇ
11610 two
11611 three
11612 "});
11613
11614 handle_resolve_completion_request(
11615 &mut cx,
11616 Some(vec![
11617 (
11618 //This overlaps with the primary completion edit which is
11619 //misbehavior from the LSP spec, test that we filter it out
11620 indoc! {"
11621 one.second_ˇcompletion
11622 two
11623 threeˇ
11624 "},
11625 "overlapping additional edit",
11626 ),
11627 (
11628 indoc! {"
11629 one.second_completion
11630 two
11631 threeˇ
11632 "},
11633 "\nadditional edit",
11634 ),
11635 ]),
11636 )
11637 .await;
11638 apply_additional_edits.await.unwrap();
11639 cx.assert_editor_state(indoc! {"
11640 one.second_completionˇ
11641 two
11642 three
11643 additional edit
11644 "});
11645
11646 cx.set_state(indoc! {"
11647 one.second_completion
11648 twoˇ
11649 threeˇ
11650 additional edit
11651 "});
11652 cx.simulate_keystroke(" ");
11653 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11654 cx.simulate_keystroke("s");
11655 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11656
11657 cx.assert_editor_state(indoc! {"
11658 one.second_completion
11659 two sˇ
11660 three sˇ
11661 additional edit
11662 "});
11663 handle_completion_request(
11664 indoc! {"
11665 one.second_completion
11666 two s
11667 three <s|>
11668 additional edit
11669 "},
11670 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11671 true,
11672 counter.clone(),
11673 &mut cx,
11674 )
11675 .await;
11676 cx.condition(|editor, _| editor.context_menu_visible())
11677 .await;
11678 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11679
11680 cx.simulate_keystroke("i");
11681
11682 handle_completion_request(
11683 indoc! {"
11684 one.second_completion
11685 two si
11686 three <si|>
11687 additional edit
11688 "},
11689 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11690 true,
11691 counter.clone(),
11692 &mut cx,
11693 )
11694 .await;
11695 cx.condition(|editor, _| editor.context_menu_visible())
11696 .await;
11697 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
11698
11699 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11700 editor
11701 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11702 .unwrap()
11703 });
11704 cx.assert_editor_state(indoc! {"
11705 one.second_completion
11706 two sixth_completionˇ
11707 three sixth_completionˇ
11708 additional edit
11709 "});
11710
11711 apply_additional_edits.await.unwrap();
11712
11713 update_test_language_settings(&mut cx, |settings| {
11714 settings.defaults.show_completions_on_input = Some(false);
11715 });
11716 cx.set_state("editorˇ");
11717 cx.simulate_keystroke(".");
11718 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11719 cx.simulate_keystrokes("c l o");
11720 cx.assert_editor_state("editor.cloˇ");
11721 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11722 cx.update_editor(|editor, window, cx| {
11723 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11724 });
11725 handle_completion_request(
11726 "editor.<clo|>",
11727 vec!["close", "clobber"],
11728 true,
11729 counter.clone(),
11730 &mut cx,
11731 )
11732 .await;
11733 cx.condition(|editor, _| editor.context_menu_visible())
11734 .await;
11735 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
11736
11737 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11738 editor
11739 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11740 .unwrap()
11741 });
11742 cx.assert_editor_state("editor.closeˇ");
11743 handle_resolve_completion_request(&mut cx, None).await;
11744 apply_additional_edits.await.unwrap();
11745}
11746
11747#[gpui::test]
11748async fn test_completion_reuse(cx: &mut TestAppContext) {
11749 init_test(cx, |_| {});
11750
11751 let mut cx = EditorLspTestContext::new_rust(
11752 lsp::ServerCapabilities {
11753 completion_provider: Some(lsp::CompletionOptions {
11754 trigger_characters: Some(vec![".".to_string()]),
11755 ..Default::default()
11756 }),
11757 ..Default::default()
11758 },
11759 cx,
11760 )
11761 .await;
11762
11763 let counter = Arc::new(AtomicUsize::new(0));
11764 cx.set_state("objˇ");
11765 cx.simulate_keystroke(".");
11766
11767 // Initial completion request returns complete results
11768 let is_incomplete = false;
11769 handle_completion_request(
11770 "obj.|<>",
11771 vec!["a", "ab", "abc"],
11772 is_incomplete,
11773 counter.clone(),
11774 &mut cx,
11775 )
11776 .await;
11777 cx.run_until_parked();
11778 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11779 cx.assert_editor_state("obj.ˇ");
11780 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
11781
11782 // Type "a" - filters existing completions
11783 cx.simulate_keystroke("a");
11784 cx.run_until_parked();
11785 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11786 cx.assert_editor_state("obj.aˇ");
11787 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
11788
11789 // Type "b" - filters existing completions
11790 cx.simulate_keystroke("b");
11791 cx.run_until_parked();
11792 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11793 cx.assert_editor_state("obj.abˇ");
11794 check_displayed_completions(vec!["ab", "abc"], &mut cx);
11795
11796 // Type "c" - filters existing completions
11797 cx.simulate_keystroke("c");
11798 cx.run_until_parked();
11799 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11800 cx.assert_editor_state("obj.abcˇ");
11801 check_displayed_completions(vec!["abc"], &mut cx);
11802
11803 // Backspace to delete "c" - filters existing completions
11804 cx.update_editor(|editor, window, cx| {
11805 editor.backspace(&Backspace, window, cx);
11806 });
11807 cx.run_until_parked();
11808 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11809 cx.assert_editor_state("obj.abˇ");
11810 check_displayed_completions(vec!["ab", "abc"], &mut cx);
11811
11812 // Moving cursor to the left dismisses menu.
11813 cx.update_editor(|editor, window, cx| {
11814 editor.move_left(&MoveLeft, window, cx);
11815 });
11816 cx.run_until_parked();
11817 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11818 cx.assert_editor_state("obj.aˇb");
11819 cx.update_editor(|editor, _, _| {
11820 assert_eq!(editor.context_menu_visible(), false);
11821 });
11822
11823 // Type "b" - new request
11824 cx.simulate_keystroke("b");
11825 let is_incomplete = false;
11826 handle_completion_request(
11827 "obj.<ab|>a",
11828 vec!["ab", "abc"],
11829 is_incomplete,
11830 counter.clone(),
11831 &mut cx,
11832 )
11833 .await;
11834 cx.run_until_parked();
11835 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11836 cx.assert_editor_state("obj.abˇb");
11837 check_displayed_completions(vec!["ab", "abc"], &mut cx);
11838
11839 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
11840 cx.update_editor(|editor, window, cx| {
11841 editor.backspace(&Backspace, window, cx);
11842 });
11843 let is_incomplete = false;
11844 handle_completion_request(
11845 "obj.<a|>b",
11846 vec!["a", "ab", "abc"],
11847 is_incomplete,
11848 counter.clone(),
11849 &mut cx,
11850 )
11851 .await;
11852 cx.run_until_parked();
11853 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
11854 cx.assert_editor_state("obj.aˇb");
11855 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
11856
11857 // Backspace to delete "a" - dismisses menu.
11858 cx.update_editor(|editor, window, cx| {
11859 editor.backspace(&Backspace, window, cx);
11860 });
11861 cx.run_until_parked();
11862 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
11863 cx.assert_editor_state("obj.ˇb");
11864 cx.update_editor(|editor, _, _| {
11865 assert_eq!(editor.context_menu_visible(), false);
11866 });
11867}
11868
11869#[gpui::test]
11870async fn test_word_completion(cx: &mut TestAppContext) {
11871 let lsp_fetch_timeout_ms = 10;
11872 init_test(cx, |language_settings| {
11873 language_settings.defaults.completions = Some(CompletionSettings {
11874 words: WordsCompletionMode::Fallback,
11875 lsp: true,
11876 lsp_fetch_timeout_ms: 10,
11877 lsp_insert_mode: LspInsertMode::Insert,
11878 });
11879 });
11880
11881 let mut cx = EditorLspTestContext::new_rust(
11882 lsp::ServerCapabilities {
11883 completion_provider: Some(lsp::CompletionOptions {
11884 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11885 ..lsp::CompletionOptions::default()
11886 }),
11887 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11888 ..lsp::ServerCapabilities::default()
11889 },
11890 cx,
11891 )
11892 .await;
11893
11894 let throttle_completions = Arc::new(AtomicBool::new(false));
11895
11896 let lsp_throttle_completions = throttle_completions.clone();
11897 let _completion_requests_handler =
11898 cx.lsp
11899 .server
11900 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
11901 let lsp_throttle_completions = lsp_throttle_completions.clone();
11902 let cx = cx.clone();
11903 async move {
11904 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
11905 cx.background_executor()
11906 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
11907 .await;
11908 }
11909 Ok(Some(lsp::CompletionResponse::Array(vec![
11910 lsp::CompletionItem {
11911 label: "first".into(),
11912 ..lsp::CompletionItem::default()
11913 },
11914 lsp::CompletionItem {
11915 label: "last".into(),
11916 ..lsp::CompletionItem::default()
11917 },
11918 ])))
11919 }
11920 });
11921
11922 cx.set_state(indoc! {"
11923 oneˇ
11924 two
11925 three
11926 "});
11927 cx.simulate_keystroke(".");
11928 cx.executor().run_until_parked();
11929 cx.condition(|editor, _| editor.context_menu_visible())
11930 .await;
11931 cx.update_editor(|editor, window, cx| {
11932 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11933 {
11934 assert_eq!(
11935 completion_menu_entries(&menu),
11936 &["first", "last"],
11937 "When LSP server is fast to reply, no fallback word completions are used"
11938 );
11939 } else {
11940 panic!("expected completion menu to be open");
11941 }
11942 editor.cancel(&Cancel, window, cx);
11943 });
11944 cx.executor().run_until_parked();
11945 cx.condition(|editor, _| !editor.context_menu_visible())
11946 .await;
11947
11948 throttle_completions.store(true, atomic::Ordering::Release);
11949 cx.simulate_keystroke(".");
11950 cx.executor()
11951 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
11952 cx.executor().run_until_parked();
11953 cx.condition(|editor, _| editor.context_menu_visible())
11954 .await;
11955 cx.update_editor(|editor, _, _| {
11956 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11957 {
11958 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
11959 "When LSP server is slow, document words can be shown instead, if configured accordingly");
11960 } else {
11961 panic!("expected completion menu to be open");
11962 }
11963 });
11964}
11965
11966#[gpui::test]
11967async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
11968 init_test(cx, |language_settings| {
11969 language_settings.defaults.completions = Some(CompletionSettings {
11970 words: WordsCompletionMode::Enabled,
11971 lsp: true,
11972 lsp_fetch_timeout_ms: 0,
11973 lsp_insert_mode: LspInsertMode::Insert,
11974 });
11975 });
11976
11977 let mut cx = EditorLspTestContext::new_rust(
11978 lsp::ServerCapabilities {
11979 completion_provider: Some(lsp::CompletionOptions {
11980 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11981 ..lsp::CompletionOptions::default()
11982 }),
11983 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11984 ..lsp::ServerCapabilities::default()
11985 },
11986 cx,
11987 )
11988 .await;
11989
11990 let _completion_requests_handler =
11991 cx.lsp
11992 .server
11993 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11994 Ok(Some(lsp::CompletionResponse::Array(vec![
11995 lsp::CompletionItem {
11996 label: "first".into(),
11997 ..lsp::CompletionItem::default()
11998 },
11999 lsp::CompletionItem {
12000 label: "last".into(),
12001 ..lsp::CompletionItem::default()
12002 },
12003 ])))
12004 });
12005
12006 cx.set_state(indoc! {"ˇ
12007 first
12008 last
12009 second
12010 "});
12011 cx.simulate_keystroke(".");
12012 cx.executor().run_until_parked();
12013 cx.condition(|editor, _| editor.context_menu_visible())
12014 .await;
12015 cx.update_editor(|editor, _, _| {
12016 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12017 {
12018 assert_eq!(
12019 completion_menu_entries(&menu),
12020 &["first", "last", "second"],
12021 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
12022 );
12023 } else {
12024 panic!("expected completion menu to be open");
12025 }
12026 });
12027}
12028
12029#[gpui::test]
12030async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
12031 init_test(cx, |language_settings| {
12032 language_settings.defaults.completions = Some(CompletionSettings {
12033 words: WordsCompletionMode::Disabled,
12034 lsp: true,
12035 lsp_fetch_timeout_ms: 0,
12036 lsp_insert_mode: LspInsertMode::Insert,
12037 });
12038 });
12039
12040 let mut cx = EditorLspTestContext::new_rust(
12041 lsp::ServerCapabilities {
12042 completion_provider: Some(lsp::CompletionOptions {
12043 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12044 ..lsp::CompletionOptions::default()
12045 }),
12046 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12047 ..lsp::ServerCapabilities::default()
12048 },
12049 cx,
12050 )
12051 .await;
12052
12053 let _completion_requests_handler =
12054 cx.lsp
12055 .server
12056 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12057 panic!("LSP completions should not be queried when dealing with word completions")
12058 });
12059
12060 cx.set_state(indoc! {"ˇ
12061 first
12062 last
12063 second
12064 "});
12065 cx.update_editor(|editor, window, cx| {
12066 editor.show_word_completions(&ShowWordCompletions, window, cx);
12067 });
12068 cx.executor().run_until_parked();
12069 cx.condition(|editor, _| editor.context_menu_visible())
12070 .await;
12071 cx.update_editor(|editor, _, _| {
12072 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12073 {
12074 assert_eq!(
12075 completion_menu_entries(&menu),
12076 &["first", "last", "second"],
12077 "`ShowWordCompletions` action should show word completions"
12078 );
12079 } else {
12080 panic!("expected completion menu to be open");
12081 }
12082 });
12083
12084 cx.simulate_keystroke("l");
12085 cx.executor().run_until_parked();
12086 cx.condition(|editor, _| editor.context_menu_visible())
12087 .await;
12088 cx.update_editor(|editor, _, _| {
12089 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12090 {
12091 assert_eq!(
12092 completion_menu_entries(&menu),
12093 &["last"],
12094 "After showing word completions, further editing should filter them and not query the LSP"
12095 );
12096 } else {
12097 panic!("expected completion menu to be open");
12098 }
12099 });
12100}
12101
12102#[gpui::test]
12103async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
12104 init_test(cx, |language_settings| {
12105 language_settings.defaults.completions = Some(CompletionSettings {
12106 words: WordsCompletionMode::Fallback,
12107 lsp: false,
12108 lsp_fetch_timeout_ms: 0,
12109 lsp_insert_mode: LspInsertMode::Insert,
12110 });
12111 });
12112
12113 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12114
12115 cx.set_state(indoc! {"ˇ
12116 0_usize
12117 let
12118 33
12119 4.5f32
12120 "});
12121 cx.update_editor(|editor, window, cx| {
12122 editor.show_completions(&ShowCompletions::default(), window, cx);
12123 });
12124 cx.executor().run_until_parked();
12125 cx.condition(|editor, _| editor.context_menu_visible())
12126 .await;
12127 cx.update_editor(|editor, window, cx| {
12128 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12129 {
12130 assert_eq!(
12131 completion_menu_entries(&menu),
12132 &["let"],
12133 "With no digits in the completion query, no digits should be in the word completions"
12134 );
12135 } else {
12136 panic!("expected completion menu to be open");
12137 }
12138 editor.cancel(&Cancel, window, cx);
12139 });
12140
12141 cx.set_state(indoc! {"3ˇ
12142 0_usize
12143 let
12144 3
12145 33.35f32
12146 "});
12147 cx.update_editor(|editor, window, cx| {
12148 editor.show_completions(&ShowCompletions::default(), window, cx);
12149 });
12150 cx.executor().run_until_parked();
12151 cx.condition(|editor, _| editor.context_menu_visible())
12152 .await;
12153 cx.update_editor(|editor, _, _| {
12154 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12155 {
12156 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
12157 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
12158 } else {
12159 panic!("expected completion menu to be open");
12160 }
12161 });
12162}
12163
12164fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
12165 let position = || lsp::Position {
12166 line: params.text_document_position.position.line,
12167 character: params.text_document_position.position.character,
12168 };
12169 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12170 range: lsp::Range {
12171 start: position(),
12172 end: position(),
12173 },
12174 new_text: text.to_string(),
12175 }))
12176}
12177
12178#[gpui::test]
12179async fn test_multiline_completion(cx: &mut TestAppContext) {
12180 init_test(cx, |_| {});
12181
12182 let fs = FakeFs::new(cx.executor());
12183 fs.insert_tree(
12184 path!("/a"),
12185 json!({
12186 "main.ts": "a",
12187 }),
12188 )
12189 .await;
12190
12191 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12192 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12193 let typescript_language = Arc::new(Language::new(
12194 LanguageConfig {
12195 name: "TypeScript".into(),
12196 matcher: LanguageMatcher {
12197 path_suffixes: vec!["ts".to_string()],
12198 ..LanguageMatcher::default()
12199 },
12200 line_comments: vec!["// ".into()],
12201 ..LanguageConfig::default()
12202 },
12203 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12204 ));
12205 language_registry.add(typescript_language.clone());
12206 let mut fake_servers = language_registry.register_fake_lsp(
12207 "TypeScript",
12208 FakeLspAdapter {
12209 capabilities: lsp::ServerCapabilities {
12210 completion_provider: Some(lsp::CompletionOptions {
12211 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12212 ..lsp::CompletionOptions::default()
12213 }),
12214 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12215 ..lsp::ServerCapabilities::default()
12216 },
12217 // Emulate vtsls label generation
12218 label_for_completion: Some(Box::new(|item, _| {
12219 let text = if let Some(description) = item
12220 .label_details
12221 .as_ref()
12222 .and_then(|label_details| label_details.description.as_ref())
12223 {
12224 format!("{} {}", item.label, description)
12225 } else if let Some(detail) = &item.detail {
12226 format!("{} {}", item.label, detail)
12227 } else {
12228 item.label.clone()
12229 };
12230 let len = text.len();
12231 Some(language::CodeLabel {
12232 text,
12233 runs: Vec::new(),
12234 filter_range: 0..len,
12235 })
12236 })),
12237 ..FakeLspAdapter::default()
12238 },
12239 );
12240 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12241 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12242 let worktree_id = workspace
12243 .update(cx, |workspace, _window, cx| {
12244 workspace.project().update(cx, |project, cx| {
12245 project.worktrees(cx).next().unwrap().read(cx).id()
12246 })
12247 })
12248 .unwrap();
12249 let _buffer = project
12250 .update(cx, |project, cx| {
12251 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
12252 })
12253 .await
12254 .unwrap();
12255 let editor = workspace
12256 .update(cx, |workspace, window, cx| {
12257 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
12258 })
12259 .unwrap()
12260 .await
12261 .unwrap()
12262 .downcast::<Editor>()
12263 .unwrap();
12264 let fake_server = fake_servers.next().await.unwrap();
12265
12266 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
12267 let multiline_label_2 = "a\nb\nc\n";
12268 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
12269 let multiline_description = "d\ne\nf\n";
12270 let multiline_detail_2 = "g\nh\ni\n";
12271
12272 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
12273 move |params, _| async move {
12274 Ok(Some(lsp::CompletionResponse::Array(vec![
12275 lsp::CompletionItem {
12276 label: multiline_label.to_string(),
12277 text_edit: gen_text_edit(¶ms, "new_text_1"),
12278 ..lsp::CompletionItem::default()
12279 },
12280 lsp::CompletionItem {
12281 label: "single line label 1".to_string(),
12282 detail: Some(multiline_detail.to_string()),
12283 text_edit: gen_text_edit(¶ms, "new_text_2"),
12284 ..lsp::CompletionItem::default()
12285 },
12286 lsp::CompletionItem {
12287 label: "single line label 2".to_string(),
12288 label_details: Some(lsp::CompletionItemLabelDetails {
12289 description: Some(multiline_description.to_string()),
12290 detail: None,
12291 }),
12292 text_edit: gen_text_edit(¶ms, "new_text_2"),
12293 ..lsp::CompletionItem::default()
12294 },
12295 lsp::CompletionItem {
12296 label: multiline_label_2.to_string(),
12297 detail: Some(multiline_detail_2.to_string()),
12298 text_edit: gen_text_edit(¶ms, "new_text_3"),
12299 ..lsp::CompletionItem::default()
12300 },
12301 lsp::CompletionItem {
12302 label: "Label with many spaces and \t but without newlines".to_string(),
12303 detail: Some(
12304 "Details with many spaces and \t but without newlines".to_string(),
12305 ),
12306 text_edit: gen_text_edit(¶ms, "new_text_4"),
12307 ..lsp::CompletionItem::default()
12308 },
12309 ])))
12310 },
12311 );
12312
12313 editor.update_in(cx, |editor, window, cx| {
12314 cx.focus_self(window);
12315 editor.move_to_end(&MoveToEnd, window, cx);
12316 editor.handle_input(".", window, cx);
12317 });
12318 cx.run_until_parked();
12319 completion_handle.next().await.unwrap();
12320
12321 editor.update(cx, |editor, _| {
12322 assert!(editor.context_menu_visible());
12323 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12324 {
12325 let completion_labels = menu
12326 .completions
12327 .borrow()
12328 .iter()
12329 .map(|c| c.label.text.clone())
12330 .collect::<Vec<_>>();
12331 assert_eq!(
12332 completion_labels,
12333 &[
12334 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
12335 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
12336 "single line label 2 d e f ",
12337 "a b c g h i ",
12338 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
12339 ],
12340 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
12341 );
12342
12343 for completion in menu
12344 .completions
12345 .borrow()
12346 .iter() {
12347 assert_eq!(
12348 completion.label.filter_range,
12349 0..completion.label.text.len(),
12350 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
12351 );
12352 }
12353 } else {
12354 panic!("expected completion menu to be open");
12355 }
12356 });
12357}
12358
12359#[gpui::test]
12360async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
12361 init_test(cx, |_| {});
12362 let mut cx = EditorLspTestContext::new_rust(
12363 lsp::ServerCapabilities {
12364 completion_provider: Some(lsp::CompletionOptions {
12365 trigger_characters: Some(vec![".".to_string()]),
12366 ..Default::default()
12367 }),
12368 ..Default::default()
12369 },
12370 cx,
12371 )
12372 .await;
12373 cx.lsp
12374 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12375 Ok(Some(lsp::CompletionResponse::Array(vec![
12376 lsp::CompletionItem {
12377 label: "first".into(),
12378 ..Default::default()
12379 },
12380 lsp::CompletionItem {
12381 label: "last".into(),
12382 ..Default::default()
12383 },
12384 ])))
12385 });
12386 cx.set_state("variableˇ");
12387 cx.simulate_keystroke(".");
12388 cx.executor().run_until_parked();
12389
12390 cx.update_editor(|editor, _, _| {
12391 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12392 {
12393 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
12394 } else {
12395 panic!("expected completion menu to be open");
12396 }
12397 });
12398
12399 cx.update_editor(|editor, window, cx| {
12400 editor.move_page_down(&MovePageDown::default(), window, cx);
12401 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12402 {
12403 assert!(
12404 menu.selected_item == 1,
12405 "expected PageDown to select the last item from the context menu"
12406 );
12407 } else {
12408 panic!("expected completion menu to stay open after PageDown");
12409 }
12410 });
12411
12412 cx.update_editor(|editor, window, cx| {
12413 editor.move_page_up(&MovePageUp::default(), window, cx);
12414 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12415 {
12416 assert!(
12417 menu.selected_item == 0,
12418 "expected PageUp to select the first item from the context menu"
12419 );
12420 } else {
12421 panic!("expected completion menu to stay open after PageUp");
12422 }
12423 });
12424}
12425
12426#[gpui::test]
12427async fn test_as_is_completions(cx: &mut TestAppContext) {
12428 init_test(cx, |_| {});
12429 let mut cx = EditorLspTestContext::new_rust(
12430 lsp::ServerCapabilities {
12431 completion_provider: Some(lsp::CompletionOptions {
12432 ..Default::default()
12433 }),
12434 ..Default::default()
12435 },
12436 cx,
12437 )
12438 .await;
12439 cx.lsp
12440 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12441 Ok(Some(lsp::CompletionResponse::Array(vec![
12442 lsp::CompletionItem {
12443 label: "unsafe".into(),
12444 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12445 range: lsp::Range {
12446 start: lsp::Position {
12447 line: 1,
12448 character: 2,
12449 },
12450 end: lsp::Position {
12451 line: 1,
12452 character: 3,
12453 },
12454 },
12455 new_text: "unsafe".to_string(),
12456 })),
12457 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
12458 ..Default::default()
12459 },
12460 ])))
12461 });
12462 cx.set_state("fn a() {}\n nˇ");
12463 cx.executor().run_until_parked();
12464 cx.update_editor(|editor, window, cx| {
12465 editor.show_completions(
12466 &ShowCompletions {
12467 trigger: Some("\n".into()),
12468 },
12469 window,
12470 cx,
12471 );
12472 });
12473 cx.executor().run_until_parked();
12474
12475 cx.update_editor(|editor, window, cx| {
12476 editor.confirm_completion(&Default::default(), window, cx)
12477 });
12478 cx.executor().run_until_parked();
12479 cx.assert_editor_state("fn a() {}\n unsafeˇ");
12480}
12481
12482#[gpui::test]
12483async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
12484 init_test(cx, |_| {});
12485
12486 let mut cx = EditorLspTestContext::new_rust(
12487 lsp::ServerCapabilities {
12488 completion_provider: Some(lsp::CompletionOptions {
12489 trigger_characters: Some(vec![".".to_string()]),
12490 resolve_provider: Some(true),
12491 ..Default::default()
12492 }),
12493 ..Default::default()
12494 },
12495 cx,
12496 )
12497 .await;
12498
12499 cx.set_state("fn main() { let a = 2ˇ; }");
12500 cx.simulate_keystroke(".");
12501 let completion_item = lsp::CompletionItem {
12502 label: "Some".into(),
12503 kind: Some(lsp::CompletionItemKind::SNIPPET),
12504 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
12505 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
12506 kind: lsp::MarkupKind::Markdown,
12507 value: "```rust\nSome(2)\n```".to_string(),
12508 })),
12509 deprecated: Some(false),
12510 sort_text: Some("Some".to_string()),
12511 filter_text: Some("Some".to_string()),
12512 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
12513 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12514 range: lsp::Range {
12515 start: lsp::Position {
12516 line: 0,
12517 character: 22,
12518 },
12519 end: lsp::Position {
12520 line: 0,
12521 character: 22,
12522 },
12523 },
12524 new_text: "Some(2)".to_string(),
12525 })),
12526 additional_text_edits: Some(vec![lsp::TextEdit {
12527 range: lsp::Range {
12528 start: lsp::Position {
12529 line: 0,
12530 character: 20,
12531 },
12532 end: lsp::Position {
12533 line: 0,
12534 character: 22,
12535 },
12536 },
12537 new_text: "".to_string(),
12538 }]),
12539 ..Default::default()
12540 };
12541
12542 let closure_completion_item = completion_item.clone();
12543 let counter = Arc::new(AtomicUsize::new(0));
12544 let counter_clone = counter.clone();
12545 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
12546 let task_completion_item = closure_completion_item.clone();
12547 counter_clone.fetch_add(1, atomic::Ordering::Release);
12548 async move {
12549 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
12550 is_incomplete: true,
12551 item_defaults: None,
12552 items: vec![task_completion_item],
12553 })))
12554 }
12555 });
12556
12557 cx.condition(|editor, _| editor.context_menu_visible())
12558 .await;
12559 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
12560 assert!(request.next().await.is_some());
12561 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12562
12563 cx.simulate_keystrokes("S o m");
12564 cx.condition(|editor, _| editor.context_menu_visible())
12565 .await;
12566 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
12567 assert!(request.next().await.is_some());
12568 assert!(request.next().await.is_some());
12569 assert!(request.next().await.is_some());
12570 request.close();
12571 assert!(request.next().await.is_none());
12572 assert_eq!(
12573 counter.load(atomic::Ordering::Acquire),
12574 4,
12575 "With the completions menu open, only one LSP request should happen per input"
12576 );
12577}
12578
12579#[gpui::test]
12580async fn test_toggle_comment(cx: &mut TestAppContext) {
12581 init_test(cx, |_| {});
12582 let mut cx = EditorTestContext::new(cx).await;
12583 let language = Arc::new(Language::new(
12584 LanguageConfig {
12585 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
12586 ..Default::default()
12587 },
12588 Some(tree_sitter_rust::LANGUAGE.into()),
12589 ));
12590 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
12591
12592 // If multiple selections intersect a line, the line is only toggled once.
12593 cx.set_state(indoc! {"
12594 fn a() {
12595 «//b();
12596 ˇ»// «c();
12597 //ˇ» d();
12598 }
12599 "});
12600
12601 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12602
12603 cx.assert_editor_state(indoc! {"
12604 fn a() {
12605 «b();
12606 c();
12607 ˇ» d();
12608 }
12609 "});
12610
12611 // The comment prefix is inserted at the same column for every line in a
12612 // selection.
12613 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12614
12615 cx.assert_editor_state(indoc! {"
12616 fn a() {
12617 // «b();
12618 // c();
12619 ˇ»// d();
12620 }
12621 "});
12622
12623 // If a selection ends at the beginning of a line, that line is not toggled.
12624 cx.set_selections_state(indoc! {"
12625 fn a() {
12626 // b();
12627 «// c();
12628 ˇ» // d();
12629 }
12630 "});
12631
12632 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12633
12634 cx.assert_editor_state(indoc! {"
12635 fn a() {
12636 // b();
12637 «c();
12638 ˇ» // d();
12639 }
12640 "});
12641
12642 // If a selection span a single line and is empty, the line is toggled.
12643 cx.set_state(indoc! {"
12644 fn a() {
12645 a();
12646 b();
12647 ˇ
12648 }
12649 "});
12650
12651 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12652
12653 cx.assert_editor_state(indoc! {"
12654 fn a() {
12655 a();
12656 b();
12657 //•ˇ
12658 }
12659 "});
12660
12661 // If a selection span multiple lines, empty lines are not toggled.
12662 cx.set_state(indoc! {"
12663 fn a() {
12664 «a();
12665
12666 c();ˇ»
12667 }
12668 "});
12669
12670 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12671
12672 cx.assert_editor_state(indoc! {"
12673 fn a() {
12674 // «a();
12675
12676 // c();ˇ»
12677 }
12678 "});
12679
12680 // If a selection includes multiple comment prefixes, all lines are uncommented.
12681 cx.set_state(indoc! {"
12682 fn a() {
12683 «// a();
12684 /// b();
12685 //! c();ˇ»
12686 }
12687 "});
12688
12689 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12690
12691 cx.assert_editor_state(indoc! {"
12692 fn a() {
12693 «a();
12694 b();
12695 c();ˇ»
12696 }
12697 "});
12698}
12699
12700#[gpui::test]
12701async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
12702 init_test(cx, |_| {});
12703 let mut cx = EditorTestContext::new(cx).await;
12704 let language = Arc::new(Language::new(
12705 LanguageConfig {
12706 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
12707 ..Default::default()
12708 },
12709 Some(tree_sitter_rust::LANGUAGE.into()),
12710 ));
12711 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
12712
12713 let toggle_comments = &ToggleComments {
12714 advance_downwards: false,
12715 ignore_indent: true,
12716 };
12717
12718 // If multiple selections intersect a line, the line is only toggled once.
12719 cx.set_state(indoc! {"
12720 fn a() {
12721 // «b();
12722 // c();
12723 // ˇ» d();
12724 }
12725 "});
12726
12727 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12728
12729 cx.assert_editor_state(indoc! {"
12730 fn a() {
12731 «b();
12732 c();
12733 ˇ» d();
12734 }
12735 "});
12736
12737 // The comment prefix is inserted at the beginning of each line
12738 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12739
12740 cx.assert_editor_state(indoc! {"
12741 fn a() {
12742 // «b();
12743 // c();
12744 // ˇ» d();
12745 }
12746 "});
12747
12748 // If a selection ends at the beginning of a line, that line is not toggled.
12749 cx.set_selections_state(indoc! {"
12750 fn a() {
12751 // b();
12752 // «c();
12753 ˇ»// d();
12754 }
12755 "});
12756
12757 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12758
12759 cx.assert_editor_state(indoc! {"
12760 fn a() {
12761 // b();
12762 «c();
12763 ˇ»// d();
12764 }
12765 "});
12766
12767 // If a selection span a single line and is empty, the line is toggled.
12768 cx.set_state(indoc! {"
12769 fn a() {
12770 a();
12771 b();
12772 ˇ
12773 }
12774 "});
12775
12776 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12777
12778 cx.assert_editor_state(indoc! {"
12779 fn a() {
12780 a();
12781 b();
12782 //ˇ
12783 }
12784 "});
12785
12786 // If a selection span multiple lines, empty lines are not toggled.
12787 cx.set_state(indoc! {"
12788 fn a() {
12789 «a();
12790
12791 c();ˇ»
12792 }
12793 "});
12794
12795 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12796
12797 cx.assert_editor_state(indoc! {"
12798 fn a() {
12799 // «a();
12800
12801 // c();ˇ»
12802 }
12803 "});
12804
12805 // If a selection includes multiple comment prefixes, all lines are uncommented.
12806 cx.set_state(indoc! {"
12807 fn a() {
12808 // «a();
12809 /// b();
12810 //! c();ˇ»
12811 }
12812 "});
12813
12814 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12815
12816 cx.assert_editor_state(indoc! {"
12817 fn a() {
12818 «a();
12819 b();
12820 c();ˇ»
12821 }
12822 "});
12823}
12824
12825#[gpui::test]
12826async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
12827 init_test(cx, |_| {});
12828
12829 let language = Arc::new(Language::new(
12830 LanguageConfig {
12831 line_comments: vec!["// ".into()],
12832 ..Default::default()
12833 },
12834 Some(tree_sitter_rust::LANGUAGE.into()),
12835 ));
12836
12837 let mut cx = EditorTestContext::new(cx).await;
12838
12839 cx.language_registry().add(language.clone());
12840 cx.update_buffer(|buffer, cx| {
12841 buffer.set_language(Some(language), cx);
12842 });
12843
12844 let toggle_comments = &ToggleComments {
12845 advance_downwards: true,
12846 ignore_indent: false,
12847 };
12848
12849 // Single cursor on one line -> advance
12850 // Cursor moves horizontally 3 characters as well on non-blank line
12851 cx.set_state(indoc!(
12852 "fn a() {
12853 ˇdog();
12854 cat();
12855 }"
12856 ));
12857 cx.update_editor(|editor, window, cx| {
12858 editor.toggle_comments(toggle_comments, window, cx);
12859 });
12860 cx.assert_editor_state(indoc!(
12861 "fn a() {
12862 // dog();
12863 catˇ();
12864 }"
12865 ));
12866
12867 // Single selection on one line -> don't advance
12868 cx.set_state(indoc!(
12869 "fn a() {
12870 «dog()ˇ»;
12871 cat();
12872 }"
12873 ));
12874 cx.update_editor(|editor, window, cx| {
12875 editor.toggle_comments(toggle_comments, window, cx);
12876 });
12877 cx.assert_editor_state(indoc!(
12878 "fn a() {
12879 // «dog()ˇ»;
12880 cat();
12881 }"
12882 ));
12883
12884 // Multiple cursors on one line -> advance
12885 cx.set_state(indoc!(
12886 "fn a() {
12887 ˇdˇog();
12888 cat();
12889 }"
12890 ));
12891 cx.update_editor(|editor, window, cx| {
12892 editor.toggle_comments(toggle_comments, window, cx);
12893 });
12894 cx.assert_editor_state(indoc!(
12895 "fn a() {
12896 // dog();
12897 catˇ(ˇ);
12898 }"
12899 ));
12900
12901 // Multiple cursors on one line, with selection -> don't advance
12902 cx.set_state(indoc!(
12903 "fn a() {
12904 ˇdˇog«()ˇ»;
12905 cat();
12906 }"
12907 ));
12908 cx.update_editor(|editor, window, cx| {
12909 editor.toggle_comments(toggle_comments, window, cx);
12910 });
12911 cx.assert_editor_state(indoc!(
12912 "fn a() {
12913 // ˇdˇog«()ˇ»;
12914 cat();
12915 }"
12916 ));
12917
12918 // Single cursor on one line -> advance
12919 // Cursor moves to column 0 on blank line
12920 cx.set_state(indoc!(
12921 "fn a() {
12922 ˇdog();
12923
12924 cat();
12925 }"
12926 ));
12927 cx.update_editor(|editor, window, cx| {
12928 editor.toggle_comments(toggle_comments, window, cx);
12929 });
12930 cx.assert_editor_state(indoc!(
12931 "fn a() {
12932 // dog();
12933 ˇ
12934 cat();
12935 }"
12936 ));
12937
12938 // Single cursor on one line -> advance
12939 // Cursor starts and ends at column 0
12940 cx.set_state(indoc!(
12941 "fn a() {
12942 ˇ dog();
12943 cat();
12944 }"
12945 ));
12946 cx.update_editor(|editor, window, cx| {
12947 editor.toggle_comments(toggle_comments, window, cx);
12948 });
12949 cx.assert_editor_state(indoc!(
12950 "fn a() {
12951 // dog();
12952 ˇ cat();
12953 }"
12954 ));
12955}
12956
12957#[gpui::test]
12958async fn test_toggle_block_comment(cx: &mut TestAppContext) {
12959 init_test(cx, |_| {});
12960
12961 let mut cx = EditorTestContext::new(cx).await;
12962
12963 let html_language = Arc::new(
12964 Language::new(
12965 LanguageConfig {
12966 name: "HTML".into(),
12967 block_comment: Some(("<!-- ".into(), " -->".into())),
12968 ..Default::default()
12969 },
12970 Some(tree_sitter_html::LANGUAGE.into()),
12971 )
12972 .with_injection_query(
12973 r#"
12974 (script_element
12975 (raw_text) @injection.content
12976 (#set! injection.language "javascript"))
12977 "#,
12978 )
12979 .unwrap(),
12980 );
12981
12982 let javascript_language = Arc::new(Language::new(
12983 LanguageConfig {
12984 name: "JavaScript".into(),
12985 line_comments: vec!["// ".into()],
12986 ..Default::default()
12987 },
12988 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
12989 ));
12990
12991 cx.language_registry().add(html_language.clone());
12992 cx.language_registry().add(javascript_language.clone());
12993 cx.update_buffer(|buffer, cx| {
12994 buffer.set_language(Some(html_language), cx);
12995 });
12996
12997 // Toggle comments for empty selections
12998 cx.set_state(
12999 &r#"
13000 <p>A</p>ˇ
13001 <p>B</p>ˇ
13002 <p>C</p>ˇ
13003 "#
13004 .unindent(),
13005 );
13006 cx.update_editor(|editor, window, cx| {
13007 editor.toggle_comments(&ToggleComments::default(), window, cx)
13008 });
13009 cx.assert_editor_state(
13010 &r#"
13011 <!-- <p>A</p>ˇ -->
13012 <!-- <p>B</p>ˇ -->
13013 <!-- <p>C</p>ˇ -->
13014 "#
13015 .unindent(),
13016 );
13017 cx.update_editor(|editor, window, cx| {
13018 editor.toggle_comments(&ToggleComments::default(), window, cx)
13019 });
13020 cx.assert_editor_state(
13021 &r#"
13022 <p>A</p>ˇ
13023 <p>B</p>ˇ
13024 <p>C</p>ˇ
13025 "#
13026 .unindent(),
13027 );
13028
13029 // Toggle comments for mixture of empty and non-empty selections, where
13030 // multiple selections occupy a given line.
13031 cx.set_state(
13032 &r#"
13033 <p>A«</p>
13034 <p>ˇ»B</p>ˇ
13035 <p>C«</p>
13036 <p>ˇ»D</p>ˇ
13037 "#
13038 .unindent(),
13039 );
13040
13041 cx.update_editor(|editor, window, cx| {
13042 editor.toggle_comments(&ToggleComments::default(), window, cx)
13043 });
13044 cx.assert_editor_state(
13045 &r#"
13046 <!-- <p>A«</p>
13047 <p>ˇ»B</p>ˇ -->
13048 <!-- <p>C«</p>
13049 <p>ˇ»D</p>ˇ -->
13050 "#
13051 .unindent(),
13052 );
13053 cx.update_editor(|editor, window, cx| {
13054 editor.toggle_comments(&ToggleComments::default(), window, cx)
13055 });
13056 cx.assert_editor_state(
13057 &r#"
13058 <p>A«</p>
13059 <p>ˇ»B</p>ˇ
13060 <p>C«</p>
13061 <p>ˇ»D</p>ˇ
13062 "#
13063 .unindent(),
13064 );
13065
13066 // Toggle comments when different languages are active for different
13067 // selections.
13068 cx.set_state(
13069 &r#"
13070 ˇ<script>
13071 ˇvar x = new Y();
13072 ˇ</script>
13073 "#
13074 .unindent(),
13075 );
13076 cx.executor().run_until_parked();
13077 cx.update_editor(|editor, window, cx| {
13078 editor.toggle_comments(&ToggleComments::default(), window, cx)
13079 });
13080 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
13081 // Uncommenting and commenting from this position brings in even more wrong artifacts.
13082 cx.assert_editor_state(
13083 &r#"
13084 <!-- ˇ<script> -->
13085 // ˇvar x = new Y();
13086 <!-- ˇ</script> -->
13087 "#
13088 .unindent(),
13089 );
13090}
13091
13092#[gpui::test]
13093fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
13094 init_test(cx, |_| {});
13095
13096 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13097 let multibuffer = cx.new(|cx| {
13098 let mut multibuffer = MultiBuffer::new(ReadWrite);
13099 multibuffer.push_excerpts(
13100 buffer.clone(),
13101 [
13102 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
13103 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
13104 ],
13105 cx,
13106 );
13107 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
13108 multibuffer
13109 });
13110
13111 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
13112 editor.update_in(cx, |editor, window, cx| {
13113 assert_eq!(editor.text(cx), "aaaa\nbbbb");
13114 editor.change_selections(None, window, cx, |s| {
13115 s.select_ranges([
13116 Point::new(0, 0)..Point::new(0, 0),
13117 Point::new(1, 0)..Point::new(1, 0),
13118 ])
13119 });
13120
13121 editor.handle_input("X", window, cx);
13122 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
13123 assert_eq!(
13124 editor.selections.ranges(cx),
13125 [
13126 Point::new(0, 1)..Point::new(0, 1),
13127 Point::new(1, 1)..Point::new(1, 1),
13128 ]
13129 );
13130
13131 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
13132 editor.change_selections(None, window, cx, |s| {
13133 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
13134 });
13135 editor.backspace(&Default::default(), window, cx);
13136 assert_eq!(editor.text(cx), "Xa\nbbb");
13137 assert_eq!(
13138 editor.selections.ranges(cx),
13139 [Point::new(1, 0)..Point::new(1, 0)]
13140 );
13141
13142 editor.change_selections(None, window, cx, |s| {
13143 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
13144 });
13145 editor.backspace(&Default::default(), window, cx);
13146 assert_eq!(editor.text(cx), "X\nbb");
13147 assert_eq!(
13148 editor.selections.ranges(cx),
13149 [Point::new(0, 1)..Point::new(0, 1)]
13150 );
13151 });
13152}
13153
13154#[gpui::test]
13155fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
13156 init_test(cx, |_| {});
13157
13158 let markers = vec![('[', ']').into(), ('(', ')').into()];
13159 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
13160 indoc! {"
13161 [aaaa
13162 (bbbb]
13163 cccc)",
13164 },
13165 markers.clone(),
13166 );
13167 let excerpt_ranges = markers.into_iter().map(|marker| {
13168 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
13169 ExcerptRange::new(context.clone())
13170 });
13171 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
13172 let multibuffer = cx.new(|cx| {
13173 let mut multibuffer = MultiBuffer::new(ReadWrite);
13174 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
13175 multibuffer
13176 });
13177
13178 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
13179 editor.update_in(cx, |editor, window, cx| {
13180 let (expected_text, selection_ranges) = marked_text_ranges(
13181 indoc! {"
13182 aaaa
13183 bˇbbb
13184 bˇbbˇb
13185 cccc"
13186 },
13187 true,
13188 );
13189 assert_eq!(editor.text(cx), expected_text);
13190 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
13191
13192 editor.handle_input("X", window, cx);
13193
13194 let (expected_text, expected_selections) = marked_text_ranges(
13195 indoc! {"
13196 aaaa
13197 bXˇbbXb
13198 bXˇbbXˇb
13199 cccc"
13200 },
13201 false,
13202 );
13203 assert_eq!(editor.text(cx), expected_text);
13204 assert_eq!(editor.selections.ranges(cx), expected_selections);
13205
13206 editor.newline(&Newline, window, cx);
13207 let (expected_text, expected_selections) = marked_text_ranges(
13208 indoc! {"
13209 aaaa
13210 bX
13211 ˇbbX
13212 b
13213 bX
13214 ˇbbX
13215 ˇb
13216 cccc"
13217 },
13218 false,
13219 );
13220 assert_eq!(editor.text(cx), expected_text);
13221 assert_eq!(editor.selections.ranges(cx), expected_selections);
13222 });
13223}
13224
13225#[gpui::test]
13226fn test_refresh_selections(cx: &mut TestAppContext) {
13227 init_test(cx, |_| {});
13228
13229 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13230 let mut excerpt1_id = None;
13231 let multibuffer = cx.new(|cx| {
13232 let mut multibuffer = MultiBuffer::new(ReadWrite);
13233 excerpt1_id = multibuffer
13234 .push_excerpts(
13235 buffer.clone(),
13236 [
13237 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
13238 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
13239 ],
13240 cx,
13241 )
13242 .into_iter()
13243 .next();
13244 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
13245 multibuffer
13246 });
13247
13248 let editor = cx.add_window(|window, cx| {
13249 let mut editor = build_editor(multibuffer.clone(), window, cx);
13250 let snapshot = editor.snapshot(window, cx);
13251 editor.change_selections(None, window, cx, |s| {
13252 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
13253 });
13254 editor.begin_selection(
13255 Point::new(2, 1).to_display_point(&snapshot),
13256 true,
13257 1,
13258 window,
13259 cx,
13260 );
13261 assert_eq!(
13262 editor.selections.ranges(cx),
13263 [
13264 Point::new(1, 3)..Point::new(1, 3),
13265 Point::new(2, 1)..Point::new(2, 1),
13266 ]
13267 );
13268 editor
13269 });
13270
13271 // Refreshing selections is a no-op when excerpts haven't changed.
13272 _ = editor.update(cx, |editor, window, cx| {
13273 editor.change_selections(None, window, cx, |s| s.refresh());
13274 assert_eq!(
13275 editor.selections.ranges(cx),
13276 [
13277 Point::new(1, 3)..Point::new(1, 3),
13278 Point::new(2, 1)..Point::new(2, 1),
13279 ]
13280 );
13281 });
13282
13283 multibuffer.update(cx, |multibuffer, cx| {
13284 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
13285 });
13286 _ = editor.update(cx, |editor, window, cx| {
13287 // Removing an excerpt causes the first selection to become degenerate.
13288 assert_eq!(
13289 editor.selections.ranges(cx),
13290 [
13291 Point::new(0, 0)..Point::new(0, 0),
13292 Point::new(0, 1)..Point::new(0, 1)
13293 ]
13294 );
13295
13296 // Refreshing selections will relocate the first selection to the original buffer
13297 // location.
13298 editor.change_selections(None, window, cx, |s| s.refresh());
13299 assert_eq!(
13300 editor.selections.ranges(cx),
13301 [
13302 Point::new(0, 1)..Point::new(0, 1),
13303 Point::new(0, 3)..Point::new(0, 3)
13304 ]
13305 );
13306 assert!(editor.selections.pending_anchor().is_some());
13307 });
13308}
13309
13310#[gpui::test]
13311fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
13312 init_test(cx, |_| {});
13313
13314 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13315 let mut excerpt1_id = None;
13316 let multibuffer = cx.new(|cx| {
13317 let mut multibuffer = MultiBuffer::new(ReadWrite);
13318 excerpt1_id = multibuffer
13319 .push_excerpts(
13320 buffer.clone(),
13321 [
13322 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
13323 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
13324 ],
13325 cx,
13326 )
13327 .into_iter()
13328 .next();
13329 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
13330 multibuffer
13331 });
13332
13333 let editor = cx.add_window(|window, cx| {
13334 let mut editor = build_editor(multibuffer.clone(), window, cx);
13335 let snapshot = editor.snapshot(window, cx);
13336 editor.begin_selection(
13337 Point::new(1, 3).to_display_point(&snapshot),
13338 false,
13339 1,
13340 window,
13341 cx,
13342 );
13343 assert_eq!(
13344 editor.selections.ranges(cx),
13345 [Point::new(1, 3)..Point::new(1, 3)]
13346 );
13347 editor
13348 });
13349
13350 multibuffer.update(cx, |multibuffer, cx| {
13351 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
13352 });
13353 _ = editor.update(cx, |editor, window, cx| {
13354 assert_eq!(
13355 editor.selections.ranges(cx),
13356 [Point::new(0, 0)..Point::new(0, 0)]
13357 );
13358
13359 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
13360 editor.change_selections(None, window, cx, |s| s.refresh());
13361 assert_eq!(
13362 editor.selections.ranges(cx),
13363 [Point::new(0, 3)..Point::new(0, 3)]
13364 );
13365 assert!(editor.selections.pending_anchor().is_some());
13366 });
13367}
13368
13369#[gpui::test]
13370async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
13371 init_test(cx, |_| {});
13372
13373 let language = Arc::new(
13374 Language::new(
13375 LanguageConfig {
13376 brackets: BracketPairConfig {
13377 pairs: vec![
13378 BracketPair {
13379 start: "{".to_string(),
13380 end: "}".to_string(),
13381 close: true,
13382 surround: true,
13383 newline: true,
13384 },
13385 BracketPair {
13386 start: "/* ".to_string(),
13387 end: " */".to_string(),
13388 close: true,
13389 surround: true,
13390 newline: true,
13391 },
13392 ],
13393 ..Default::default()
13394 },
13395 ..Default::default()
13396 },
13397 Some(tree_sitter_rust::LANGUAGE.into()),
13398 )
13399 .with_indents_query("")
13400 .unwrap(),
13401 );
13402
13403 let text = concat!(
13404 "{ }\n", //
13405 " x\n", //
13406 " /* */\n", //
13407 "x\n", //
13408 "{{} }\n", //
13409 );
13410
13411 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
13412 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13413 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
13414 editor
13415 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
13416 .await;
13417
13418 editor.update_in(cx, |editor, window, cx| {
13419 editor.change_selections(None, window, cx, |s| {
13420 s.select_display_ranges([
13421 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
13422 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
13423 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
13424 ])
13425 });
13426 editor.newline(&Newline, window, cx);
13427
13428 assert_eq!(
13429 editor.buffer().read(cx).read(cx).text(),
13430 concat!(
13431 "{ \n", // Suppress rustfmt
13432 "\n", //
13433 "}\n", //
13434 " x\n", //
13435 " /* \n", //
13436 " \n", //
13437 " */\n", //
13438 "x\n", //
13439 "{{} \n", //
13440 "}\n", //
13441 )
13442 );
13443 });
13444}
13445
13446#[gpui::test]
13447fn test_highlighted_ranges(cx: &mut TestAppContext) {
13448 init_test(cx, |_| {});
13449
13450 let editor = cx.add_window(|window, cx| {
13451 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
13452 build_editor(buffer.clone(), window, cx)
13453 });
13454
13455 _ = editor.update(cx, |editor, window, cx| {
13456 struct Type1;
13457 struct Type2;
13458
13459 let buffer = editor.buffer.read(cx).snapshot(cx);
13460
13461 let anchor_range =
13462 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
13463
13464 editor.highlight_background::<Type1>(
13465 &[
13466 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
13467 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
13468 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
13469 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
13470 ],
13471 |_| Hsla::red(),
13472 cx,
13473 );
13474 editor.highlight_background::<Type2>(
13475 &[
13476 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
13477 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
13478 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
13479 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
13480 ],
13481 |_| Hsla::green(),
13482 cx,
13483 );
13484
13485 let snapshot = editor.snapshot(window, cx);
13486 let mut highlighted_ranges = editor.background_highlights_in_range(
13487 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
13488 &snapshot,
13489 cx.theme().colors(),
13490 );
13491 // Enforce a consistent ordering based on color without relying on the ordering of the
13492 // highlight's `TypeId` which is non-executor.
13493 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
13494 assert_eq!(
13495 highlighted_ranges,
13496 &[
13497 (
13498 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
13499 Hsla::red(),
13500 ),
13501 (
13502 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
13503 Hsla::red(),
13504 ),
13505 (
13506 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
13507 Hsla::green(),
13508 ),
13509 (
13510 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
13511 Hsla::green(),
13512 ),
13513 ]
13514 );
13515 assert_eq!(
13516 editor.background_highlights_in_range(
13517 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
13518 &snapshot,
13519 cx.theme().colors(),
13520 ),
13521 &[(
13522 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
13523 Hsla::red(),
13524 )]
13525 );
13526 });
13527}
13528
13529#[gpui::test]
13530async fn test_following(cx: &mut TestAppContext) {
13531 init_test(cx, |_| {});
13532
13533 let fs = FakeFs::new(cx.executor());
13534 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
13535
13536 let buffer = project.update(cx, |project, cx| {
13537 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
13538 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
13539 });
13540 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
13541 let follower = cx.update(|cx| {
13542 cx.open_window(
13543 WindowOptions {
13544 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
13545 gpui::Point::new(px(0.), px(0.)),
13546 gpui::Point::new(px(10.), px(80.)),
13547 ))),
13548 ..Default::default()
13549 },
13550 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
13551 )
13552 .unwrap()
13553 });
13554
13555 let is_still_following = Rc::new(RefCell::new(true));
13556 let follower_edit_event_count = Rc::new(RefCell::new(0));
13557 let pending_update = Rc::new(RefCell::new(None));
13558 let leader_entity = leader.root(cx).unwrap();
13559 let follower_entity = follower.root(cx).unwrap();
13560 _ = follower.update(cx, {
13561 let update = pending_update.clone();
13562 let is_still_following = is_still_following.clone();
13563 let follower_edit_event_count = follower_edit_event_count.clone();
13564 |_, window, cx| {
13565 cx.subscribe_in(
13566 &leader_entity,
13567 window,
13568 move |_, leader, event, window, cx| {
13569 leader.read(cx).add_event_to_update_proto(
13570 event,
13571 &mut update.borrow_mut(),
13572 window,
13573 cx,
13574 );
13575 },
13576 )
13577 .detach();
13578
13579 cx.subscribe_in(
13580 &follower_entity,
13581 window,
13582 move |_, _, event: &EditorEvent, _window, _cx| {
13583 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
13584 *is_still_following.borrow_mut() = false;
13585 }
13586
13587 if let EditorEvent::BufferEdited = event {
13588 *follower_edit_event_count.borrow_mut() += 1;
13589 }
13590 },
13591 )
13592 .detach();
13593 }
13594 });
13595
13596 // Update the selections only
13597 _ = leader.update(cx, |leader, window, cx| {
13598 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
13599 });
13600 follower
13601 .update(cx, |follower, window, cx| {
13602 follower.apply_update_proto(
13603 &project,
13604 pending_update.borrow_mut().take().unwrap(),
13605 window,
13606 cx,
13607 )
13608 })
13609 .unwrap()
13610 .await
13611 .unwrap();
13612 _ = follower.update(cx, |follower, _, cx| {
13613 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
13614 });
13615 assert!(*is_still_following.borrow());
13616 assert_eq!(*follower_edit_event_count.borrow(), 0);
13617
13618 // Update the scroll position only
13619 _ = leader.update(cx, |leader, window, cx| {
13620 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
13621 });
13622 follower
13623 .update(cx, |follower, window, cx| {
13624 follower.apply_update_proto(
13625 &project,
13626 pending_update.borrow_mut().take().unwrap(),
13627 window,
13628 cx,
13629 )
13630 })
13631 .unwrap()
13632 .await
13633 .unwrap();
13634 assert_eq!(
13635 follower
13636 .update(cx, |follower, _, cx| follower.scroll_position(cx))
13637 .unwrap(),
13638 gpui::Point::new(1.5, 3.5)
13639 );
13640 assert!(*is_still_following.borrow());
13641 assert_eq!(*follower_edit_event_count.borrow(), 0);
13642
13643 // Update the selections and scroll position. The follower's scroll position is updated
13644 // via autoscroll, not via the leader's exact scroll position.
13645 _ = leader.update(cx, |leader, window, cx| {
13646 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
13647 leader.request_autoscroll(Autoscroll::newest(), cx);
13648 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
13649 });
13650 follower
13651 .update(cx, |follower, window, cx| {
13652 follower.apply_update_proto(
13653 &project,
13654 pending_update.borrow_mut().take().unwrap(),
13655 window,
13656 cx,
13657 )
13658 })
13659 .unwrap()
13660 .await
13661 .unwrap();
13662 _ = follower.update(cx, |follower, _, cx| {
13663 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
13664 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
13665 });
13666 assert!(*is_still_following.borrow());
13667
13668 // Creating a pending selection that precedes another selection
13669 _ = leader.update(cx, |leader, window, cx| {
13670 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
13671 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
13672 });
13673 follower
13674 .update(cx, |follower, window, cx| {
13675 follower.apply_update_proto(
13676 &project,
13677 pending_update.borrow_mut().take().unwrap(),
13678 window,
13679 cx,
13680 )
13681 })
13682 .unwrap()
13683 .await
13684 .unwrap();
13685 _ = follower.update(cx, |follower, _, cx| {
13686 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
13687 });
13688 assert!(*is_still_following.borrow());
13689
13690 // Extend the pending selection so that it surrounds another selection
13691 _ = leader.update(cx, |leader, window, cx| {
13692 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
13693 });
13694 follower
13695 .update(cx, |follower, window, cx| {
13696 follower.apply_update_proto(
13697 &project,
13698 pending_update.borrow_mut().take().unwrap(),
13699 window,
13700 cx,
13701 )
13702 })
13703 .unwrap()
13704 .await
13705 .unwrap();
13706 _ = follower.update(cx, |follower, _, cx| {
13707 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
13708 });
13709
13710 // Scrolling locally breaks the follow
13711 _ = follower.update(cx, |follower, window, cx| {
13712 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
13713 follower.set_scroll_anchor(
13714 ScrollAnchor {
13715 anchor: top_anchor,
13716 offset: gpui::Point::new(0.0, 0.5),
13717 },
13718 window,
13719 cx,
13720 );
13721 });
13722 assert!(!(*is_still_following.borrow()));
13723}
13724
13725#[gpui::test]
13726async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
13727 init_test(cx, |_| {});
13728
13729 let fs = FakeFs::new(cx.executor());
13730 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
13731 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13732 let pane = workspace
13733 .update(cx, |workspace, _, _| workspace.active_pane().clone())
13734 .unwrap();
13735
13736 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
13737
13738 let leader = pane.update_in(cx, |_, window, cx| {
13739 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
13740 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
13741 });
13742
13743 // Start following the editor when it has no excerpts.
13744 let mut state_message =
13745 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
13746 let workspace_entity = workspace.root(cx).unwrap();
13747 let follower_1 = cx
13748 .update_window(*workspace.deref(), |_, window, cx| {
13749 Editor::from_state_proto(
13750 workspace_entity,
13751 ViewId {
13752 creator: CollaboratorId::PeerId(PeerId::default()),
13753 id: 0,
13754 },
13755 &mut state_message,
13756 window,
13757 cx,
13758 )
13759 })
13760 .unwrap()
13761 .unwrap()
13762 .await
13763 .unwrap();
13764
13765 let update_message = Rc::new(RefCell::new(None));
13766 follower_1.update_in(cx, {
13767 let update = update_message.clone();
13768 |_, window, cx| {
13769 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
13770 leader.read(cx).add_event_to_update_proto(
13771 event,
13772 &mut update.borrow_mut(),
13773 window,
13774 cx,
13775 );
13776 })
13777 .detach();
13778 }
13779 });
13780
13781 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
13782 (
13783 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
13784 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
13785 )
13786 });
13787
13788 // Insert some excerpts.
13789 leader.update(cx, |leader, cx| {
13790 leader.buffer.update(cx, |multibuffer, cx| {
13791 multibuffer.set_excerpts_for_path(
13792 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
13793 buffer_1.clone(),
13794 vec![
13795 Point::row_range(0..3),
13796 Point::row_range(1..6),
13797 Point::row_range(12..15),
13798 ],
13799 0,
13800 cx,
13801 );
13802 multibuffer.set_excerpts_for_path(
13803 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
13804 buffer_2.clone(),
13805 vec![Point::row_range(0..6), Point::row_range(8..12)],
13806 0,
13807 cx,
13808 );
13809 });
13810 });
13811
13812 // Apply the update of adding the excerpts.
13813 follower_1
13814 .update_in(cx, |follower, window, cx| {
13815 follower.apply_update_proto(
13816 &project,
13817 update_message.borrow().clone().unwrap(),
13818 window,
13819 cx,
13820 )
13821 })
13822 .await
13823 .unwrap();
13824 assert_eq!(
13825 follower_1.update(cx, |editor, cx| editor.text(cx)),
13826 leader.update(cx, |editor, cx| editor.text(cx))
13827 );
13828 update_message.borrow_mut().take();
13829
13830 // Start following separately after it already has excerpts.
13831 let mut state_message =
13832 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
13833 let workspace_entity = workspace.root(cx).unwrap();
13834 let follower_2 = cx
13835 .update_window(*workspace.deref(), |_, window, cx| {
13836 Editor::from_state_proto(
13837 workspace_entity,
13838 ViewId {
13839 creator: CollaboratorId::PeerId(PeerId::default()),
13840 id: 0,
13841 },
13842 &mut state_message,
13843 window,
13844 cx,
13845 )
13846 })
13847 .unwrap()
13848 .unwrap()
13849 .await
13850 .unwrap();
13851 assert_eq!(
13852 follower_2.update(cx, |editor, cx| editor.text(cx)),
13853 leader.update(cx, |editor, cx| editor.text(cx))
13854 );
13855
13856 // Remove some excerpts.
13857 leader.update(cx, |leader, cx| {
13858 leader.buffer.update(cx, |multibuffer, cx| {
13859 let excerpt_ids = multibuffer.excerpt_ids();
13860 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
13861 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
13862 });
13863 });
13864
13865 // Apply the update of removing the excerpts.
13866 follower_1
13867 .update_in(cx, |follower, window, cx| {
13868 follower.apply_update_proto(
13869 &project,
13870 update_message.borrow().clone().unwrap(),
13871 window,
13872 cx,
13873 )
13874 })
13875 .await
13876 .unwrap();
13877 follower_2
13878 .update_in(cx, |follower, window, cx| {
13879 follower.apply_update_proto(
13880 &project,
13881 update_message.borrow().clone().unwrap(),
13882 window,
13883 cx,
13884 )
13885 })
13886 .await
13887 .unwrap();
13888 update_message.borrow_mut().take();
13889 assert_eq!(
13890 follower_1.update(cx, |editor, cx| editor.text(cx)),
13891 leader.update(cx, |editor, cx| editor.text(cx))
13892 );
13893}
13894
13895#[gpui::test]
13896async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13897 init_test(cx, |_| {});
13898
13899 let mut cx = EditorTestContext::new(cx).await;
13900 let lsp_store =
13901 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
13902
13903 cx.set_state(indoc! {"
13904 ˇfn func(abc def: i32) -> u32 {
13905 }
13906 "});
13907
13908 cx.update(|_, cx| {
13909 lsp_store.update(cx, |lsp_store, cx| {
13910 lsp_store
13911 .update_diagnostics(
13912 LanguageServerId(0),
13913 lsp::PublishDiagnosticsParams {
13914 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
13915 version: None,
13916 diagnostics: vec![
13917 lsp::Diagnostic {
13918 range: lsp::Range::new(
13919 lsp::Position::new(0, 11),
13920 lsp::Position::new(0, 12),
13921 ),
13922 severity: Some(lsp::DiagnosticSeverity::ERROR),
13923 ..Default::default()
13924 },
13925 lsp::Diagnostic {
13926 range: lsp::Range::new(
13927 lsp::Position::new(0, 12),
13928 lsp::Position::new(0, 15),
13929 ),
13930 severity: Some(lsp::DiagnosticSeverity::ERROR),
13931 ..Default::default()
13932 },
13933 lsp::Diagnostic {
13934 range: lsp::Range::new(
13935 lsp::Position::new(0, 25),
13936 lsp::Position::new(0, 28),
13937 ),
13938 severity: Some(lsp::DiagnosticSeverity::ERROR),
13939 ..Default::default()
13940 },
13941 ],
13942 },
13943 None,
13944 DiagnosticSourceKind::Pushed,
13945 &[],
13946 cx,
13947 )
13948 .unwrap()
13949 });
13950 });
13951
13952 executor.run_until_parked();
13953
13954 cx.update_editor(|editor, window, cx| {
13955 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13956 });
13957
13958 cx.assert_editor_state(indoc! {"
13959 fn func(abc def: i32) -> ˇu32 {
13960 }
13961 "});
13962
13963 cx.update_editor(|editor, window, cx| {
13964 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13965 });
13966
13967 cx.assert_editor_state(indoc! {"
13968 fn func(abc ˇdef: i32) -> u32 {
13969 }
13970 "});
13971
13972 cx.update_editor(|editor, window, cx| {
13973 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13974 });
13975
13976 cx.assert_editor_state(indoc! {"
13977 fn func(abcˇ def: i32) -> u32 {
13978 }
13979 "});
13980
13981 cx.update_editor(|editor, window, cx| {
13982 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13983 });
13984
13985 cx.assert_editor_state(indoc! {"
13986 fn func(abc def: i32) -> ˇu32 {
13987 }
13988 "});
13989}
13990
13991#[gpui::test]
13992async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13993 init_test(cx, |_| {});
13994
13995 let mut cx = EditorTestContext::new(cx).await;
13996
13997 let diff_base = r#"
13998 use some::mod;
13999
14000 const A: u32 = 42;
14001
14002 fn main() {
14003 println!("hello");
14004
14005 println!("world");
14006 }
14007 "#
14008 .unindent();
14009
14010 // Edits are modified, removed, modified, added
14011 cx.set_state(
14012 &r#"
14013 use some::modified;
14014
14015 ˇ
14016 fn main() {
14017 println!("hello there");
14018
14019 println!("around the");
14020 println!("world");
14021 }
14022 "#
14023 .unindent(),
14024 );
14025
14026 cx.set_head_text(&diff_base);
14027 executor.run_until_parked();
14028
14029 cx.update_editor(|editor, window, cx| {
14030 //Wrap around the bottom of the buffer
14031 for _ in 0..3 {
14032 editor.go_to_next_hunk(&GoToHunk, window, cx);
14033 }
14034 });
14035
14036 cx.assert_editor_state(
14037 &r#"
14038 ˇuse some::modified;
14039
14040
14041 fn main() {
14042 println!("hello there");
14043
14044 println!("around the");
14045 println!("world");
14046 }
14047 "#
14048 .unindent(),
14049 );
14050
14051 cx.update_editor(|editor, window, cx| {
14052 //Wrap around the top of the buffer
14053 for _ in 0..2 {
14054 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14055 }
14056 });
14057
14058 cx.assert_editor_state(
14059 &r#"
14060 use some::modified;
14061
14062
14063 fn main() {
14064 ˇ println!("hello there");
14065
14066 println!("around the");
14067 println!("world");
14068 }
14069 "#
14070 .unindent(),
14071 );
14072
14073 cx.update_editor(|editor, window, cx| {
14074 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14075 });
14076
14077 cx.assert_editor_state(
14078 &r#"
14079 use some::modified;
14080
14081 ˇ
14082 fn main() {
14083 println!("hello there");
14084
14085 println!("around the");
14086 println!("world");
14087 }
14088 "#
14089 .unindent(),
14090 );
14091
14092 cx.update_editor(|editor, window, cx| {
14093 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14094 });
14095
14096 cx.assert_editor_state(
14097 &r#"
14098 ˇuse some::modified;
14099
14100
14101 fn main() {
14102 println!("hello there");
14103
14104 println!("around the");
14105 println!("world");
14106 }
14107 "#
14108 .unindent(),
14109 );
14110
14111 cx.update_editor(|editor, window, cx| {
14112 for _ in 0..2 {
14113 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14114 }
14115 });
14116
14117 cx.assert_editor_state(
14118 &r#"
14119 use some::modified;
14120
14121
14122 fn main() {
14123 ˇ println!("hello there");
14124
14125 println!("around the");
14126 println!("world");
14127 }
14128 "#
14129 .unindent(),
14130 );
14131
14132 cx.update_editor(|editor, window, cx| {
14133 editor.fold(&Fold, window, cx);
14134 });
14135
14136 cx.update_editor(|editor, window, cx| {
14137 editor.go_to_next_hunk(&GoToHunk, window, cx);
14138 });
14139
14140 cx.assert_editor_state(
14141 &r#"
14142 ˇuse some::modified;
14143
14144
14145 fn main() {
14146 println!("hello there");
14147
14148 println!("around the");
14149 println!("world");
14150 }
14151 "#
14152 .unindent(),
14153 );
14154}
14155
14156#[test]
14157fn test_split_words() {
14158 fn split(text: &str) -> Vec<&str> {
14159 split_words(text).collect()
14160 }
14161
14162 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
14163 assert_eq!(split("hello_world"), &["hello_", "world"]);
14164 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
14165 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
14166 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
14167 assert_eq!(split("helloworld"), &["helloworld"]);
14168
14169 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
14170}
14171
14172#[gpui::test]
14173async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
14174 init_test(cx, |_| {});
14175
14176 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
14177 let mut assert = |before, after| {
14178 let _state_context = cx.set_state(before);
14179 cx.run_until_parked();
14180 cx.update_editor(|editor, window, cx| {
14181 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
14182 });
14183 cx.run_until_parked();
14184 cx.assert_editor_state(after);
14185 };
14186
14187 // Outside bracket jumps to outside of matching bracket
14188 assert("console.logˇ(var);", "console.log(var)ˇ;");
14189 assert("console.log(var)ˇ;", "console.logˇ(var);");
14190
14191 // Inside bracket jumps to inside of matching bracket
14192 assert("console.log(ˇvar);", "console.log(varˇ);");
14193 assert("console.log(varˇ);", "console.log(ˇvar);");
14194
14195 // When outside a bracket and inside, favor jumping to the inside bracket
14196 assert(
14197 "console.log('foo', [1, 2, 3]ˇ);",
14198 "console.log(ˇ'foo', [1, 2, 3]);",
14199 );
14200 assert(
14201 "console.log(ˇ'foo', [1, 2, 3]);",
14202 "console.log('foo', [1, 2, 3]ˇ);",
14203 );
14204
14205 // Bias forward if two options are equally likely
14206 assert(
14207 "let result = curried_fun()ˇ();",
14208 "let result = curried_fun()()ˇ;",
14209 );
14210
14211 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
14212 assert(
14213 indoc! {"
14214 function test() {
14215 console.log('test')ˇ
14216 }"},
14217 indoc! {"
14218 function test() {
14219 console.logˇ('test')
14220 }"},
14221 );
14222}
14223
14224#[gpui::test]
14225async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
14226 init_test(cx, |_| {});
14227
14228 let fs = FakeFs::new(cx.executor());
14229 fs.insert_tree(
14230 path!("/a"),
14231 json!({
14232 "main.rs": "fn main() { let a = 5; }",
14233 "other.rs": "// Test file",
14234 }),
14235 )
14236 .await;
14237 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14238
14239 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14240 language_registry.add(Arc::new(Language::new(
14241 LanguageConfig {
14242 name: "Rust".into(),
14243 matcher: LanguageMatcher {
14244 path_suffixes: vec!["rs".to_string()],
14245 ..Default::default()
14246 },
14247 brackets: BracketPairConfig {
14248 pairs: vec![BracketPair {
14249 start: "{".to_string(),
14250 end: "}".to_string(),
14251 close: true,
14252 surround: true,
14253 newline: true,
14254 }],
14255 disabled_scopes_by_bracket_ix: Vec::new(),
14256 },
14257 ..Default::default()
14258 },
14259 Some(tree_sitter_rust::LANGUAGE.into()),
14260 )));
14261 let mut fake_servers = language_registry.register_fake_lsp(
14262 "Rust",
14263 FakeLspAdapter {
14264 capabilities: lsp::ServerCapabilities {
14265 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
14266 first_trigger_character: "{".to_string(),
14267 more_trigger_character: None,
14268 }),
14269 ..Default::default()
14270 },
14271 ..Default::default()
14272 },
14273 );
14274
14275 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14276
14277 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14278
14279 let worktree_id = workspace
14280 .update(cx, |workspace, _, cx| {
14281 workspace.project().update(cx, |project, cx| {
14282 project.worktrees(cx).next().unwrap().read(cx).id()
14283 })
14284 })
14285 .unwrap();
14286
14287 let buffer = project
14288 .update(cx, |project, cx| {
14289 project.open_local_buffer(path!("/a/main.rs"), cx)
14290 })
14291 .await
14292 .unwrap();
14293 let editor_handle = workspace
14294 .update(cx, |workspace, window, cx| {
14295 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
14296 })
14297 .unwrap()
14298 .await
14299 .unwrap()
14300 .downcast::<Editor>()
14301 .unwrap();
14302
14303 cx.executor().start_waiting();
14304 let fake_server = fake_servers.next().await.unwrap();
14305
14306 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
14307 |params, _| async move {
14308 assert_eq!(
14309 params.text_document_position.text_document.uri,
14310 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
14311 );
14312 assert_eq!(
14313 params.text_document_position.position,
14314 lsp::Position::new(0, 21),
14315 );
14316
14317 Ok(Some(vec![lsp::TextEdit {
14318 new_text: "]".to_string(),
14319 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14320 }]))
14321 },
14322 );
14323
14324 editor_handle.update_in(cx, |editor, window, cx| {
14325 window.focus(&editor.focus_handle(cx));
14326 editor.change_selections(None, window, cx, |s| {
14327 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
14328 });
14329 editor.handle_input("{", window, cx);
14330 });
14331
14332 cx.executor().run_until_parked();
14333
14334 buffer.update(cx, |buffer, _| {
14335 assert_eq!(
14336 buffer.text(),
14337 "fn main() { let a = {5}; }",
14338 "No extra braces from on type formatting should appear in the buffer"
14339 )
14340 });
14341}
14342
14343#[gpui::test]
14344async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
14345 init_test(cx, |_| {});
14346
14347 let fs = FakeFs::new(cx.executor());
14348 fs.insert_tree(
14349 path!("/a"),
14350 json!({
14351 "main.rs": "fn main() { let a = 5; }",
14352 "other.rs": "// Test file",
14353 }),
14354 )
14355 .await;
14356
14357 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14358
14359 let server_restarts = Arc::new(AtomicUsize::new(0));
14360 let closure_restarts = Arc::clone(&server_restarts);
14361 let language_server_name = "test language server";
14362 let language_name: LanguageName = "Rust".into();
14363
14364 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14365 language_registry.add(Arc::new(Language::new(
14366 LanguageConfig {
14367 name: language_name.clone(),
14368 matcher: LanguageMatcher {
14369 path_suffixes: vec!["rs".to_string()],
14370 ..Default::default()
14371 },
14372 ..Default::default()
14373 },
14374 Some(tree_sitter_rust::LANGUAGE.into()),
14375 )));
14376 let mut fake_servers = language_registry.register_fake_lsp(
14377 "Rust",
14378 FakeLspAdapter {
14379 name: language_server_name,
14380 initialization_options: Some(json!({
14381 "testOptionValue": true
14382 })),
14383 initializer: Some(Box::new(move |fake_server| {
14384 let task_restarts = Arc::clone(&closure_restarts);
14385 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
14386 task_restarts.fetch_add(1, atomic::Ordering::Release);
14387 futures::future::ready(Ok(()))
14388 });
14389 })),
14390 ..Default::default()
14391 },
14392 );
14393
14394 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14395 let _buffer = project
14396 .update(cx, |project, cx| {
14397 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
14398 })
14399 .await
14400 .unwrap();
14401 let _fake_server = fake_servers.next().await.unwrap();
14402 update_test_language_settings(cx, |language_settings| {
14403 language_settings.languages.insert(
14404 language_name.clone(),
14405 LanguageSettingsContent {
14406 tab_size: NonZeroU32::new(8),
14407 ..Default::default()
14408 },
14409 );
14410 });
14411 cx.executor().run_until_parked();
14412 assert_eq!(
14413 server_restarts.load(atomic::Ordering::Acquire),
14414 0,
14415 "Should not restart LSP server on an unrelated change"
14416 );
14417
14418 update_test_project_settings(cx, |project_settings| {
14419 project_settings.lsp.insert(
14420 "Some other server name".into(),
14421 LspSettings {
14422 binary: None,
14423 settings: None,
14424 initialization_options: Some(json!({
14425 "some other init value": false
14426 })),
14427 enable_lsp_tasks: false,
14428 },
14429 );
14430 });
14431 cx.executor().run_until_parked();
14432 assert_eq!(
14433 server_restarts.load(atomic::Ordering::Acquire),
14434 0,
14435 "Should not restart LSP server on an unrelated LSP settings change"
14436 );
14437
14438 update_test_project_settings(cx, |project_settings| {
14439 project_settings.lsp.insert(
14440 language_server_name.into(),
14441 LspSettings {
14442 binary: None,
14443 settings: None,
14444 initialization_options: Some(json!({
14445 "anotherInitValue": false
14446 })),
14447 enable_lsp_tasks: false,
14448 },
14449 );
14450 });
14451 cx.executor().run_until_parked();
14452 assert_eq!(
14453 server_restarts.load(atomic::Ordering::Acquire),
14454 1,
14455 "Should restart LSP server on a related LSP settings change"
14456 );
14457
14458 update_test_project_settings(cx, |project_settings| {
14459 project_settings.lsp.insert(
14460 language_server_name.into(),
14461 LspSettings {
14462 binary: None,
14463 settings: None,
14464 initialization_options: Some(json!({
14465 "anotherInitValue": false
14466 })),
14467 enable_lsp_tasks: false,
14468 },
14469 );
14470 });
14471 cx.executor().run_until_parked();
14472 assert_eq!(
14473 server_restarts.load(atomic::Ordering::Acquire),
14474 1,
14475 "Should not restart LSP server on a related LSP settings change that is the same"
14476 );
14477
14478 update_test_project_settings(cx, |project_settings| {
14479 project_settings.lsp.insert(
14480 language_server_name.into(),
14481 LspSettings {
14482 binary: None,
14483 settings: None,
14484 initialization_options: None,
14485 enable_lsp_tasks: false,
14486 },
14487 );
14488 });
14489 cx.executor().run_until_parked();
14490 assert_eq!(
14491 server_restarts.load(atomic::Ordering::Acquire),
14492 2,
14493 "Should restart LSP server on another related LSP settings change"
14494 );
14495}
14496
14497#[gpui::test]
14498async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
14499 init_test(cx, |_| {});
14500
14501 let mut cx = EditorLspTestContext::new_rust(
14502 lsp::ServerCapabilities {
14503 completion_provider: Some(lsp::CompletionOptions {
14504 trigger_characters: Some(vec![".".to_string()]),
14505 resolve_provider: Some(true),
14506 ..Default::default()
14507 }),
14508 ..Default::default()
14509 },
14510 cx,
14511 )
14512 .await;
14513
14514 cx.set_state("fn main() { let a = 2ˇ; }");
14515 cx.simulate_keystroke(".");
14516 let completion_item = lsp::CompletionItem {
14517 label: "some".into(),
14518 kind: Some(lsp::CompletionItemKind::SNIPPET),
14519 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
14520 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
14521 kind: lsp::MarkupKind::Markdown,
14522 value: "```rust\nSome(2)\n```".to_string(),
14523 })),
14524 deprecated: Some(false),
14525 sort_text: Some("fffffff2".to_string()),
14526 filter_text: Some("some".to_string()),
14527 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
14528 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14529 range: lsp::Range {
14530 start: lsp::Position {
14531 line: 0,
14532 character: 22,
14533 },
14534 end: lsp::Position {
14535 line: 0,
14536 character: 22,
14537 },
14538 },
14539 new_text: "Some(2)".to_string(),
14540 })),
14541 additional_text_edits: Some(vec![lsp::TextEdit {
14542 range: lsp::Range {
14543 start: lsp::Position {
14544 line: 0,
14545 character: 20,
14546 },
14547 end: lsp::Position {
14548 line: 0,
14549 character: 22,
14550 },
14551 },
14552 new_text: "".to_string(),
14553 }]),
14554 ..Default::default()
14555 };
14556
14557 let closure_completion_item = completion_item.clone();
14558 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14559 let task_completion_item = closure_completion_item.clone();
14560 async move {
14561 Ok(Some(lsp::CompletionResponse::Array(vec![
14562 task_completion_item,
14563 ])))
14564 }
14565 });
14566
14567 request.next().await;
14568
14569 cx.condition(|editor, _| editor.context_menu_visible())
14570 .await;
14571 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14572 editor
14573 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14574 .unwrap()
14575 });
14576 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
14577
14578 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
14579 let task_completion_item = completion_item.clone();
14580 async move { Ok(task_completion_item) }
14581 })
14582 .next()
14583 .await
14584 .unwrap();
14585 apply_additional_edits.await.unwrap();
14586 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
14587}
14588
14589#[gpui::test]
14590async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
14591 init_test(cx, |_| {});
14592
14593 let mut cx = EditorLspTestContext::new_rust(
14594 lsp::ServerCapabilities {
14595 completion_provider: Some(lsp::CompletionOptions {
14596 trigger_characters: Some(vec![".".to_string()]),
14597 resolve_provider: Some(true),
14598 ..Default::default()
14599 }),
14600 ..Default::default()
14601 },
14602 cx,
14603 )
14604 .await;
14605
14606 cx.set_state("fn main() { let a = 2ˇ; }");
14607 cx.simulate_keystroke(".");
14608
14609 let item1 = lsp::CompletionItem {
14610 label: "method id()".to_string(),
14611 filter_text: Some("id".to_string()),
14612 detail: None,
14613 documentation: None,
14614 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14615 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14616 new_text: ".id".to_string(),
14617 })),
14618 ..lsp::CompletionItem::default()
14619 };
14620
14621 let item2 = lsp::CompletionItem {
14622 label: "other".to_string(),
14623 filter_text: Some("other".to_string()),
14624 detail: None,
14625 documentation: None,
14626 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14627 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14628 new_text: ".other".to_string(),
14629 })),
14630 ..lsp::CompletionItem::default()
14631 };
14632
14633 let item1 = item1.clone();
14634 cx.set_request_handler::<lsp::request::Completion, _, _>({
14635 let item1 = item1.clone();
14636 move |_, _, _| {
14637 let item1 = item1.clone();
14638 let item2 = item2.clone();
14639 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
14640 }
14641 })
14642 .next()
14643 .await;
14644
14645 cx.condition(|editor, _| editor.context_menu_visible())
14646 .await;
14647 cx.update_editor(|editor, _, _| {
14648 let context_menu = editor.context_menu.borrow_mut();
14649 let context_menu = context_menu
14650 .as_ref()
14651 .expect("Should have the context menu deployed");
14652 match context_menu {
14653 CodeContextMenu::Completions(completions_menu) => {
14654 let completions = completions_menu.completions.borrow_mut();
14655 assert_eq!(
14656 completions
14657 .iter()
14658 .map(|completion| &completion.label.text)
14659 .collect::<Vec<_>>(),
14660 vec!["method id()", "other"]
14661 )
14662 }
14663 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14664 }
14665 });
14666
14667 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
14668 let item1 = item1.clone();
14669 move |_, item_to_resolve, _| {
14670 let item1 = item1.clone();
14671 async move {
14672 if item1 == item_to_resolve {
14673 Ok(lsp::CompletionItem {
14674 label: "method id()".to_string(),
14675 filter_text: Some("id".to_string()),
14676 detail: Some("Now resolved!".to_string()),
14677 documentation: Some(lsp::Documentation::String("Docs".to_string())),
14678 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14679 range: lsp::Range::new(
14680 lsp::Position::new(0, 22),
14681 lsp::Position::new(0, 22),
14682 ),
14683 new_text: ".id".to_string(),
14684 })),
14685 ..lsp::CompletionItem::default()
14686 })
14687 } else {
14688 Ok(item_to_resolve)
14689 }
14690 }
14691 }
14692 })
14693 .next()
14694 .await
14695 .unwrap();
14696 cx.run_until_parked();
14697
14698 cx.update_editor(|editor, window, cx| {
14699 editor.context_menu_next(&Default::default(), window, cx);
14700 });
14701
14702 cx.update_editor(|editor, _, _| {
14703 let context_menu = editor.context_menu.borrow_mut();
14704 let context_menu = context_menu
14705 .as_ref()
14706 .expect("Should have the context menu deployed");
14707 match context_menu {
14708 CodeContextMenu::Completions(completions_menu) => {
14709 let completions = completions_menu.completions.borrow_mut();
14710 assert_eq!(
14711 completions
14712 .iter()
14713 .map(|completion| &completion.label.text)
14714 .collect::<Vec<_>>(),
14715 vec!["method id() Now resolved!", "other"],
14716 "Should update first completion label, but not second as the filter text did not match."
14717 );
14718 }
14719 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14720 }
14721 });
14722}
14723
14724#[gpui::test]
14725async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
14726 init_test(cx, |_| {});
14727 let mut cx = EditorLspTestContext::new_rust(
14728 lsp::ServerCapabilities {
14729 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
14730 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
14731 completion_provider: Some(lsp::CompletionOptions {
14732 resolve_provider: Some(true),
14733 ..Default::default()
14734 }),
14735 ..Default::default()
14736 },
14737 cx,
14738 )
14739 .await;
14740 cx.set_state(indoc! {"
14741 struct TestStruct {
14742 field: i32
14743 }
14744
14745 fn mainˇ() {
14746 let unused_var = 42;
14747 let test_struct = TestStruct { field: 42 };
14748 }
14749 "});
14750 let symbol_range = cx.lsp_range(indoc! {"
14751 struct TestStruct {
14752 field: i32
14753 }
14754
14755 «fn main»() {
14756 let unused_var = 42;
14757 let test_struct = TestStruct { field: 42 };
14758 }
14759 "});
14760 let mut hover_requests =
14761 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
14762 Ok(Some(lsp::Hover {
14763 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
14764 kind: lsp::MarkupKind::Markdown,
14765 value: "Function documentation".to_string(),
14766 }),
14767 range: Some(symbol_range),
14768 }))
14769 });
14770
14771 // Case 1: Test that code action menu hide hover popover
14772 cx.dispatch_action(Hover);
14773 hover_requests.next().await;
14774 cx.condition(|editor, _| editor.hover_state.visible()).await;
14775 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
14776 move |_, _, _| async move {
14777 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
14778 lsp::CodeAction {
14779 title: "Remove unused variable".to_string(),
14780 kind: Some(CodeActionKind::QUICKFIX),
14781 edit: Some(lsp::WorkspaceEdit {
14782 changes: Some(
14783 [(
14784 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
14785 vec![lsp::TextEdit {
14786 range: lsp::Range::new(
14787 lsp::Position::new(5, 4),
14788 lsp::Position::new(5, 27),
14789 ),
14790 new_text: "".to_string(),
14791 }],
14792 )]
14793 .into_iter()
14794 .collect(),
14795 ),
14796 ..Default::default()
14797 }),
14798 ..Default::default()
14799 },
14800 )]))
14801 },
14802 );
14803 cx.update_editor(|editor, window, cx| {
14804 editor.toggle_code_actions(
14805 &ToggleCodeActions {
14806 deployed_from: None,
14807 quick_launch: false,
14808 },
14809 window,
14810 cx,
14811 );
14812 });
14813 code_action_requests.next().await;
14814 cx.run_until_parked();
14815 cx.condition(|editor, _| editor.context_menu_visible())
14816 .await;
14817 cx.update_editor(|editor, _, _| {
14818 assert!(
14819 !editor.hover_state.visible(),
14820 "Hover popover should be hidden when code action menu is shown"
14821 );
14822 // Hide code actions
14823 editor.context_menu.take();
14824 });
14825
14826 // Case 2: Test that code completions hide hover popover
14827 cx.dispatch_action(Hover);
14828 hover_requests.next().await;
14829 cx.condition(|editor, _| editor.hover_state.visible()).await;
14830 let counter = Arc::new(AtomicUsize::new(0));
14831 let mut completion_requests =
14832 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14833 let counter = counter.clone();
14834 async move {
14835 counter.fetch_add(1, atomic::Ordering::Release);
14836 Ok(Some(lsp::CompletionResponse::Array(vec![
14837 lsp::CompletionItem {
14838 label: "main".into(),
14839 kind: Some(lsp::CompletionItemKind::FUNCTION),
14840 detail: Some("() -> ()".to_string()),
14841 ..Default::default()
14842 },
14843 lsp::CompletionItem {
14844 label: "TestStruct".into(),
14845 kind: Some(lsp::CompletionItemKind::STRUCT),
14846 detail: Some("struct TestStruct".to_string()),
14847 ..Default::default()
14848 },
14849 ])))
14850 }
14851 });
14852 cx.update_editor(|editor, window, cx| {
14853 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14854 });
14855 completion_requests.next().await;
14856 cx.condition(|editor, _| editor.context_menu_visible())
14857 .await;
14858 cx.update_editor(|editor, _, _| {
14859 assert!(
14860 !editor.hover_state.visible(),
14861 "Hover popover should be hidden when completion menu is shown"
14862 );
14863 });
14864}
14865
14866#[gpui::test]
14867async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
14868 init_test(cx, |_| {});
14869
14870 let mut cx = EditorLspTestContext::new_rust(
14871 lsp::ServerCapabilities {
14872 completion_provider: Some(lsp::CompletionOptions {
14873 trigger_characters: Some(vec![".".to_string()]),
14874 resolve_provider: Some(true),
14875 ..Default::default()
14876 }),
14877 ..Default::default()
14878 },
14879 cx,
14880 )
14881 .await;
14882
14883 cx.set_state("fn main() { let a = 2ˇ; }");
14884 cx.simulate_keystroke(".");
14885
14886 let unresolved_item_1 = lsp::CompletionItem {
14887 label: "id".to_string(),
14888 filter_text: Some("id".to_string()),
14889 detail: None,
14890 documentation: None,
14891 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14892 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14893 new_text: ".id".to_string(),
14894 })),
14895 ..lsp::CompletionItem::default()
14896 };
14897 let resolved_item_1 = lsp::CompletionItem {
14898 additional_text_edits: Some(vec![lsp::TextEdit {
14899 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
14900 new_text: "!!".to_string(),
14901 }]),
14902 ..unresolved_item_1.clone()
14903 };
14904 let unresolved_item_2 = lsp::CompletionItem {
14905 label: "other".to_string(),
14906 filter_text: Some("other".to_string()),
14907 detail: None,
14908 documentation: None,
14909 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14910 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14911 new_text: ".other".to_string(),
14912 })),
14913 ..lsp::CompletionItem::default()
14914 };
14915 let resolved_item_2 = lsp::CompletionItem {
14916 additional_text_edits: Some(vec![lsp::TextEdit {
14917 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
14918 new_text: "??".to_string(),
14919 }]),
14920 ..unresolved_item_2.clone()
14921 };
14922
14923 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
14924 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
14925 cx.lsp
14926 .server
14927 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
14928 let unresolved_item_1 = unresolved_item_1.clone();
14929 let resolved_item_1 = resolved_item_1.clone();
14930 let unresolved_item_2 = unresolved_item_2.clone();
14931 let resolved_item_2 = resolved_item_2.clone();
14932 let resolve_requests_1 = resolve_requests_1.clone();
14933 let resolve_requests_2 = resolve_requests_2.clone();
14934 move |unresolved_request, _| {
14935 let unresolved_item_1 = unresolved_item_1.clone();
14936 let resolved_item_1 = resolved_item_1.clone();
14937 let unresolved_item_2 = unresolved_item_2.clone();
14938 let resolved_item_2 = resolved_item_2.clone();
14939 let resolve_requests_1 = resolve_requests_1.clone();
14940 let resolve_requests_2 = resolve_requests_2.clone();
14941 async move {
14942 if unresolved_request == unresolved_item_1 {
14943 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
14944 Ok(resolved_item_1.clone())
14945 } else if unresolved_request == unresolved_item_2 {
14946 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
14947 Ok(resolved_item_2.clone())
14948 } else {
14949 panic!("Unexpected completion item {unresolved_request:?}")
14950 }
14951 }
14952 }
14953 })
14954 .detach();
14955
14956 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14957 let unresolved_item_1 = unresolved_item_1.clone();
14958 let unresolved_item_2 = unresolved_item_2.clone();
14959 async move {
14960 Ok(Some(lsp::CompletionResponse::Array(vec![
14961 unresolved_item_1,
14962 unresolved_item_2,
14963 ])))
14964 }
14965 })
14966 .next()
14967 .await;
14968
14969 cx.condition(|editor, _| editor.context_menu_visible())
14970 .await;
14971 cx.update_editor(|editor, _, _| {
14972 let context_menu = editor.context_menu.borrow_mut();
14973 let context_menu = context_menu
14974 .as_ref()
14975 .expect("Should have the context menu deployed");
14976 match context_menu {
14977 CodeContextMenu::Completions(completions_menu) => {
14978 let completions = completions_menu.completions.borrow_mut();
14979 assert_eq!(
14980 completions
14981 .iter()
14982 .map(|completion| &completion.label.text)
14983 .collect::<Vec<_>>(),
14984 vec!["id", "other"]
14985 )
14986 }
14987 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14988 }
14989 });
14990 cx.run_until_parked();
14991
14992 cx.update_editor(|editor, window, cx| {
14993 editor.context_menu_next(&ContextMenuNext, window, cx);
14994 });
14995 cx.run_until_parked();
14996 cx.update_editor(|editor, window, cx| {
14997 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
14998 });
14999 cx.run_until_parked();
15000 cx.update_editor(|editor, window, cx| {
15001 editor.context_menu_next(&ContextMenuNext, window, cx);
15002 });
15003 cx.run_until_parked();
15004 cx.update_editor(|editor, window, cx| {
15005 editor
15006 .compose_completion(&ComposeCompletion::default(), window, cx)
15007 .expect("No task returned")
15008 })
15009 .await
15010 .expect("Completion failed");
15011 cx.run_until_parked();
15012
15013 cx.update_editor(|editor, _, cx| {
15014 assert_eq!(
15015 resolve_requests_1.load(atomic::Ordering::Acquire),
15016 1,
15017 "Should always resolve once despite multiple selections"
15018 );
15019 assert_eq!(
15020 resolve_requests_2.load(atomic::Ordering::Acquire),
15021 1,
15022 "Should always resolve once after multiple selections and applying the completion"
15023 );
15024 assert_eq!(
15025 editor.text(cx),
15026 "fn main() { let a = ??.other; }",
15027 "Should use resolved data when applying the completion"
15028 );
15029 });
15030}
15031
15032#[gpui::test]
15033async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
15034 init_test(cx, |_| {});
15035
15036 let item_0 = lsp::CompletionItem {
15037 label: "abs".into(),
15038 insert_text: Some("abs".into()),
15039 data: Some(json!({ "very": "special"})),
15040 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
15041 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
15042 lsp::InsertReplaceEdit {
15043 new_text: "abs".to_string(),
15044 insert: lsp::Range::default(),
15045 replace: lsp::Range::default(),
15046 },
15047 )),
15048 ..lsp::CompletionItem::default()
15049 };
15050 let items = iter::once(item_0.clone())
15051 .chain((11..51).map(|i| lsp::CompletionItem {
15052 label: format!("item_{}", i),
15053 insert_text: Some(format!("item_{}", i)),
15054 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15055 ..lsp::CompletionItem::default()
15056 }))
15057 .collect::<Vec<_>>();
15058
15059 let default_commit_characters = vec!["?".to_string()];
15060 let default_data = json!({ "default": "data"});
15061 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
15062 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
15063 let default_edit_range = lsp::Range {
15064 start: lsp::Position {
15065 line: 0,
15066 character: 5,
15067 },
15068 end: lsp::Position {
15069 line: 0,
15070 character: 5,
15071 },
15072 };
15073
15074 let mut cx = EditorLspTestContext::new_rust(
15075 lsp::ServerCapabilities {
15076 completion_provider: Some(lsp::CompletionOptions {
15077 trigger_characters: Some(vec![".".to_string()]),
15078 resolve_provider: Some(true),
15079 ..Default::default()
15080 }),
15081 ..Default::default()
15082 },
15083 cx,
15084 )
15085 .await;
15086
15087 cx.set_state("fn main() { let a = 2ˇ; }");
15088 cx.simulate_keystroke(".");
15089
15090 let completion_data = default_data.clone();
15091 let completion_characters = default_commit_characters.clone();
15092 let completion_items = items.clone();
15093 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15094 let default_data = completion_data.clone();
15095 let default_commit_characters = completion_characters.clone();
15096 let items = completion_items.clone();
15097 async move {
15098 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15099 items,
15100 item_defaults: Some(lsp::CompletionListItemDefaults {
15101 data: Some(default_data.clone()),
15102 commit_characters: Some(default_commit_characters.clone()),
15103 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
15104 default_edit_range,
15105 )),
15106 insert_text_format: Some(default_insert_text_format),
15107 insert_text_mode: Some(default_insert_text_mode),
15108 }),
15109 ..lsp::CompletionList::default()
15110 })))
15111 }
15112 })
15113 .next()
15114 .await;
15115
15116 let resolved_items = Arc::new(Mutex::new(Vec::new()));
15117 cx.lsp
15118 .server
15119 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
15120 let closure_resolved_items = resolved_items.clone();
15121 move |item_to_resolve, _| {
15122 let closure_resolved_items = closure_resolved_items.clone();
15123 async move {
15124 closure_resolved_items.lock().push(item_to_resolve.clone());
15125 Ok(item_to_resolve)
15126 }
15127 }
15128 })
15129 .detach();
15130
15131 cx.condition(|editor, _| editor.context_menu_visible())
15132 .await;
15133 cx.run_until_parked();
15134 cx.update_editor(|editor, _, _| {
15135 let menu = editor.context_menu.borrow_mut();
15136 match menu.as_ref().expect("should have the completions menu") {
15137 CodeContextMenu::Completions(completions_menu) => {
15138 assert_eq!(
15139 completions_menu
15140 .entries
15141 .borrow()
15142 .iter()
15143 .map(|mat| mat.string.clone())
15144 .collect::<Vec<String>>(),
15145 items
15146 .iter()
15147 .map(|completion| completion.label.clone())
15148 .collect::<Vec<String>>()
15149 );
15150 }
15151 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
15152 }
15153 });
15154 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
15155 // with 4 from the end.
15156 assert_eq!(
15157 *resolved_items.lock(),
15158 [&items[0..16], &items[items.len() - 4..items.len()]]
15159 .concat()
15160 .iter()
15161 .cloned()
15162 .map(|mut item| {
15163 if item.data.is_none() {
15164 item.data = Some(default_data.clone());
15165 }
15166 item
15167 })
15168 .collect::<Vec<lsp::CompletionItem>>(),
15169 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
15170 );
15171 resolved_items.lock().clear();
15172
15173 cx.update_editor(|editor, window, cx| {
15174 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
15175 });
15176 cx.run_until_parked();
15177 // Completions that have already been resolved are skipped.
15178 assert_eq!(
15179 *resolved_items.lock(),
15180 items[items.len() - 16..items.len() - 4]
15181 .iter()
15182 .cloned()
15183 .map(|mut item| {
15184 if item.data.is_none() {
15185 item.data = Some(default_data.clone());
15186 }
15187 item
15188 })
15189 .collect::<Vec<lsp::CompletionItem>>()
15190 );
15191 resolved_items.lock().clear();
15192}
15193
15194#[gpui::test]
15195async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
15196 init_test(cx, |_| {});
15197
15198 let mut cx = EditorLspTestContext::new(
15199 Language::new(
15200 LanguageConfig {
15201 matcher: LanguageMatcher {
15202 path_suffixes: vec!["jsx".into()],
15203 ..Default::default()
15204 },
15205 overrides: [(
15206 "element".into(),
15207 LanguageConfigOverride {
15208 completion_query_characters: Override::Set(['-'].into_iter().collect()),
15209 ..Default::default()
15210 },
15211 )]
15212 .into_iter()
15213 .collect(),
15214 ..Default::default()
15215 },
15216 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15217 )
15218 .with_override_query("(jsx_self_closing_element) @element")
15219 .unwrap(),
15220 lsp::ServerCapabilities {
15221 completion_provider: Some(lsp::CompletionOptions {
15222 trigger_characters: Some(vec![":".to_string()]),
15223 ..Default::default()
15224 }),
15225 ..Default::default()
15226 },
15227 cx,
15228 )
15229 .await;
15230
15231 cx.lsp
15232 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15233 Ok(Some(lsp::CompletionResponse::Array(vec![
15234 lsp::CompletionItem {
15235 label: "bg-blue".into(),
15236 ..Default::default()
15237 },
15238 lsp::CompletionItem {
15239 label: "bg-red".into(),
15240 ..Default::default()
15241 },
15242 lsp::CompletionItem {
15243 label: "bg-yellow".into(),
15244 ..Default::default()
15245 },
15246 ])))
15247 });
15248
15249 cx.set_state(r#"<p class="bgˇ" />"#);
15250
15251 // Trigger completion when typing a dash, because the dash is an extra
15252 // word character in the 'element' scope, which contains the cursor.
15253 cx.simulate_keystroke("-");
15254 cx.executor().run_until_parked();
15255 cx.update_editor(|editor, _, _| {
15256 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15257 {
15258 assert_eq!(
15259 completion_menu_entries(&menu),
15260 &["bg-red", "bg-blue", "bg-yellow"]
15261 );
15262 } else {
15263 panic!("expected completion menu to be open");
15264 }
15265 });
15266
15267 cx.simulate_keystroke("l");
15268 cx.executor().run_until_parked();
15269 cx.update_editor(|editor, _, _| {
15270 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15271 {
15272 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
15273 } else {
15274 panic!("expected completion menu to be open");
15275 }
15276 });
15277
15278 // When filtering completions, consider the character after the '-' to
15279 // be the start of a subword.
15280 cx.set_state(r#"<p class="yelˇ" />"#);
15281 cx.simulate_keystroke("l");
15282 cx.executor().run_until_parked();
15283 cx.update_editor(|editor, _, _| {
15284 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15285 {
15286 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
15287 } else {
15288 panic!("expected completion menu to be open");
15289 }
15290 });
15291}
15292
15293fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
15294 let entries = menu.entries.borrow();
15295 entries.iter().map(|mat| mat.string.clone()).collect()
15296}
15297
15298#[gpui::test]
15299async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
15300 init_test(cx, |settings| {
15301 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
15302 FormatterList(vec![Formatter::Prettier].into()),
15303 ))
15304 });
15305
15306 let fs = FakeFs::new(cx.executor());
15307 fs.insert_file(path!("/file.ts"), Default::default()).await;
15308
15309 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
15310 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15311
15312 language_registry.add(Arc::new(Language::new(
15313 LanguageConfig {
15314 name: "TypeScript".into(),
15315 matcher: LanguageMatcher {
15316 path_suffixes: vec!["ts".to_string()],
15317 ..Default::default()
15318 },
15319 ..Default::default()
15320 },
15321 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
15322 )));
15323 update_test_language_settings(cx, |settings| {
15324 settings.defaults.prettier = Some(PrettierSettings {
15325 allowed: true,
15326 ..PrettierSettings::default()
15327 });
15328 });
15329
15330 let test_plugin = "test_plugin";
15331 let _ = language_registry.register_fake_lsp(
15332 "TypeScript",
15333 FakeLspAdapter {
15334 prettier_plugins: vec![test_plugin],
15335 ..Default::default()
15336 },
15337 );
15338
15339 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
15340 let buffer = project
15341 .update(cx, |project, cx| {
15342 project.open_local_buffer(path!("/file.ts"), cx)
15343 })
15344 .await
15345 .unwrap();
15346
15347 let buffer_text = "one\ntwo\nthree\n";
15348 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
15349 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
15350 editor.update_in(cx, |editor, window, cx| {
15351 editor.set_text(buffer_text, window, cx)
15352 });
15353
15354 editor
15355 .update_in(cx, |editor, window, cx| {
15356 editor.perform_format(
15357 project.clone(),
15358 FormatTrigger::Manual,
15359 FormatTarget::Buffers,
15360 window,
15361 cx,
15362 )
15363 })
15364 .unwrap()
15365 .await;
15366 assert_eq!(
15367 editor.update(cx, |editor, cx| editor.text(cx)),
15368 buffer_text.to_string() + prettier_format_suffix,
15369 "Test prettier formatting was not applied to the original buffer text",
15370 );
15371
15372 update_test_language_settings(cx, |settings| {
15373 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
15374 });
15375 let format = editor.update_in(cx, |editor, window, cx| {
15376 editor.perform_format(
15377 project.clone(),
15378 FormatTrigger::Manual,
15379 FormatTarget::Buffers,
15380 window,
15381 cx,
15382 )
15383 });
15384 format.await.unwrap();
15385 assert_eq!(
15386 editor.update(cx, |editor, cx| editor.text(cx)),
15387 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
15388 "Autoformatting (via test prettier) was not applied to the original buffer text",
15389 );
15390}
15391
15392#[gpui::test]
15393async fn test_addition_reverts(cx: &mut TestAppContext) {
15394 init_test(cx, |_| {});
15395 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15396 let base_text = indoc! {r#"
15397 struct Row;
15398 struct Row1;
15399 struct Row2;
15400
15401 struct Row4;
15402 struct Row5;
15403 struct Row6;
15404
15405 struct Row8;
15406 struct Row9;
15407 struct Row10;"#};
15408
15409 // When addition hunks are not adjacent to carets, no hunk revert is performed
15410 assert_hunk_revert(
15411 indoc! {r#"struct Row;
15412 struct Row1;
15413 struct Row1.1;
15414 struct Row1.2;
15415 struct Row2;ˇ
15416
15417 struct Row4;
15418 struct Row5;
15419 struct Row6;
15420
15421 struct Row8;
15422 ˇstruct Row9;
15423 struct Row9.1;
15424 struct Row9.2;
15425 struct Row9.3;
15426 struct Row10;"#},
15427 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
15428 indoc! {r#"struct Row;
15429 struct Row1;
15430 struct Row1.1;
15431 struct Row1.2;
15432 struct Row2;ˇ
15433
15434 struct Row4;
15435 struct Row5;
15436 struct Row6;
15437
15438 struct Row8;
15439 ˇstruct Row9;
15440 struct Row9.1;
15441 struct Row9.2;
15442 struct Row9.3;
15443 struct Row10;"#},
15444 base_text,
15445 &mut cx,
15446 );
15447 // Same for selections
15448 assert_hunk_revert(
15449 indoc! {r#"struct Row;
15450 struct Row1;
15451 struct Row2;
15452 struct Row2.1;
15453 struct Row2.2;
15454 «ˇ
15455 struct Row4;
15456 struct» Row5;
15457 «struct Row6;
15458 ˇ»
15459 struct Row9.1;
15460 struct Row9.2;
15461 struct Row9.3;
15462 struct Row8;
15463 struct Row9;
15464 struct Row10;"#},
15465 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
15466 indoc! {r#"struct Row;
15467 struct Row1;
15468 struct Row2;
15469 struct Row2.1;
15470 struct Row2.2;
15471 «ˇ
15472 struct Row4;
15473 struct» Row5;
15474 «struct Row6;
15475 ˇ»
15476 struct Row9.1;
15477 struct Row9.2;
15478 struct Row9.3;
15479 struct Row8;
15480 struct Row9;
15481 struct Row10;"#},
15482 base_text,
15483 &mut cx,
15484 );
15485
15486 // When carets and selections intersect the addition hunks, those are reverted.
15487 // Adjacent carets got merged.
15488 assert_hunk_revert(
15489 indoc! {r#"struct Row;
15490 ˇ// something on the top
15491 struct Row1;
15492 struct Row2;
15493 struct Roˇw3.1;
15494 struct Row2.2;
15495 struct Row2.3;ˇ
15496
15497 struct Row4;
15498 struct ˇRow5.1;
15499 struct Row5.2;
15500 struct «Rowˇ»5.3;
15501 struct Row5;
15502 struct Row6;
15503 ˇ
15504 struct Row9.1;
15505 struct «Rowˇ»9.2;
15506 struct «ˇRow»9.3;
15507 struct Row8;
15508 struct Row9;
15509 «ˇ// something on bottom»
15510 struct Row10;"#},
15511 vec![
15512 DiffHunkStatusKind::Added,
15513 DiffHunkStatusKind::Added,
15514 DiffHunkStatusKind::Added,
15515 DiffHunkStatusKind::Added,
15516 DiffHunkStatusKind::Added,
15517 ],
15518 indoc! {r#"struct Row;
15519 ˇstruct Row1;
15520 struct Row2;
15521 ˇ
15522 struct Row4;
15523 ˇstruct Row5;
15524 struct Row6;
15525 ˇ
15526 ˇstruct Row8;
15527 struct Row9;
15528 ˇstruct Row10;"#},
15529 base_text,
15530 &mut cx,
15531 );
15532}
15533
15534#[gpui::test]
15535async fn test_modification_reverts(cx: &mut TestAppContext) {
15536 init_test(cx, |_| {});
15537 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15538 let base_text = indoc! {r#"
15539 struct Row;
15540 struct Row1;
15541 struct Row2;
15542
15543 struct Row4;
15544 struct Row5;
15545 struct Row6;
15546
15547 struct Row8;
15548 struct Row9;
15549 struct Row10;"#};
15550
15551 // Modification hunks behave the same as the addition ones.
15552 assert_hunk_revert(
15553 indoc! {r#"struct Row;
15554 struct Row1;
15555 struct Row33;
15556 ˇ
15557 struct Row4;
15558 struct Row5;
15559 struct Row6;
15560 ˇ
15561 struct Row99;
15562 struct Row9;
15563 struct Row10;"#},
15564 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
15565 indoc! {r#"struct Row;
15566 struct Row1;
15567 struct Row33;
15568 ˇ
15569 struct Row4;
15570 struct Row5;
15571 struct Row6;
15572 ˇ
15573 struct Row99;
15574 struct Row9;
15575 struct Row10;"#},
15576 base_text,
15577 &mut cx,
15578 );
15579 assert_hunk_revert(
15580 indoc! {r#"struct Row;
15581 struct Row1;
15582 struct Row33;
15583 «ˇ
15584 struct Row4;
15585 struct» Row5;
15586 «struct Row6;
15587 ˇ»
15588 struct Row99;
15589 struct Row9;
15590 struct Row10;"#},
15591 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
15592 indoc! {r#"struct Row;
15593 struct Row1;
15594 struct Row33;
15595 «ˇ
15596 struct Row4;
15597 struct» Row5;
15598 «struct Row6;
15599 ˇ»
15600 struct Row99;
15601 struct Row9;
15602 struct Row10;"#},
15603 base_text,
15604 &mut cx,
15605 );
15606
15607 assert_hunk_revert(
15608 indoc! {r#"ˇstruct Row1.1;
15609 struct Row1;
15610 «ˇstr»uct Row22;
15611
15612 struct ˇRow44;
15613 struct Row5;
15614 struct «Rˇ»ow66;ˇ
15615
15616 «struˇ»ct Row88;
15617 struct Row9;
15618 struct Row1011;ˇ"#},
15619 vec![
15620 DiffHunkStatusKind::Modified,
15621 DiffHunkStatusKind::Modified,
15622 DiffHunkStatusKind::Modified,
15623 DiffHunkStatusKind::Modified,
15624 DiffHunkStatusKind::Modified,
15625 DiffHunkStatusKind::Modified,
15626 ],
15627 indoc! {r#"struct Row;
15628 ˇstruct Row1;
15629 struct Row2;
15630 ˇ
15631 struct Row4;
15632 ˇstruct Row5;
15633 struct Row6;
15634 ˇ
15635 struct Row8;
15636 ˇstruct Row9;
15637 struct Row10;ˇ"#},
15638 base_text,
15639 &mut cx,
15640 );
15641}
15642
15643#[gpui::test]
15644async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
15645 init_test(cx, |_| {});
15646 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15647 let base_text = indoc! {r#"
15648 one
15649
15650 two
15651 three
15652 "#};
15653
15654 cx.set_head_text(base_text);
15655 cx.set_state("\nˇ\n");
15656 cx.executor().run_until_parked();
15657 cx.update_editor(|editor, _window, cx| {
15658 editor.expand_selected_diff_hunks(cx);
15659 });
15660 cx.executor().run_until_parked();
15661 cx.update_editor(|editor, window, cx| {
15662 editor.backspace(&Default::default(), window, cx);
15663 });
15664 cx.run_until_parked();
15665 cx.assert_state_with_diff(
15666 indoc! {r#"
15667
15668 - two
15669 - threeˇ
15670 +
15671 "#}
15672 .to_string(),
15673 );
15674}
15675
15676#[gpui::test]
15677async fn test_deletion_reverts(cx: &mut TestAppContext) {
15678 init_test(cx, |_| {});
15679 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15680 let base_text = indoc! {r#"struct Row;
15681struct Row1;
15682struct Row2;
15683
15684struct Row4;
15685struct Row5;
15686struct Row6;
15687
15688struct Row8;
15689struct Row9;
15690struct Row10;"#};
15691
15692 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
15693 assert_hunk_revert(
15694 indoc! {r#"struct Row;
15695 struct Row2;
15696
15697 ˇstruct Row4;
15698 struct Row5;
15699 struct Row6;
15700 ˇ
15701 struct Row8;
15702 struct Row10;"#},
15703 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15704 indoc! {r#"struct Row;
15705 struct Row2;
15706
15707 ˇstruct Row4;
15708 struct Row5;
15709 struct Row6;
15710 ˇ
15711 struct Row8;
15712 struct Row10;"#},
15713 base_text,
15714 &mut cx,
15715 );
15716 assert_hunk_revert(
15717 indoc! {r#"struct Row;
15718 struct Row2;
15719
15720 «ˇstruct Row4;
15721 struct» Row5;
15722 «struct Row6;
15723 ˇ»
15724 struct Row8;
15725 struct Row10;"#},
15726 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15727 indoc! {r#"struct Row;
15728 struct Row2;
15729
15730 «ˇstruct Row4;
15731 struct» Row5;
15732 «struct Row6;
15733 ˇ»
15734 struct Row8;
15735 struct Row10;"#},
15736 base_text,
15737 &mut cx,
15738 );
15739
15740 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
15741 assert_hunk_revert(
15742 indoc! {r#"struct Row;
15743 ˇstruct Row2;
15744
15745 struct Row4;
15746 struct Row5;
15747 struct Row6;
15748
15749 struct Row8;ˇ
15750 struct Row10;"#},
15751 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15752 indoc! {r#"struct Row;
15753 struct Row1;
15754 ˇstruct Row2;
15755
15756 struct Row4;
15757 struct Row5;
15758 struct Row6;
15759
15760 struct Row8;ˇ
15761 struct Row9;
15762 struct Row10;"#},
15763 base_text,
15764 &mut cx,
15765 );
15766 assert_hunk_revert(
15767 indoc! {r#"struct Row;
15768 struct Row2«ˇ;
15769 struct Row4;
15770 struct» Row5;
15771 «struct Row6;
15772
15773 struct Row8;ˇ»
15774 struct Row10;"#},
15775 vec![
15776 DiffHunkStatusKind::Deleted,
15777 DiffHunkStatusKind::Deleted,
15778 DiffHunkStatusKind::Deleted,
15779 ],
15780 indoc! {r#"struct Row;
15781 struct Row1;
15782 struct Row2«ˇ;
15783
15784 struct Row4;
15785 struct» Row5;
15786 «struct Row6;
15787
15788 struct Row8;ˇ»
15789 struct Row9;
15790 struct Row10;"#},
15791 base_text,
15792 &mut cx,
15793 );
15794}
15795
15796#[gpui::test]
15797async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
15798 init_test(cx, |_| {});
15799
15800 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
15801 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
15802 let base_text_3 =
15803 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
15804
15805 let text_1 = edit_first_char_of_every_line(base_text_1);
15806 let text_2 = edit_first_char_of_every_line(base_text_2);
15807 let text_3 = edit_first_char_of_every_line(base_text_3);
15808
15809 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
15810 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
15811 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
15812
15813 let multibuffer = cx.new(|cx| {
15814 let mut multibuffer = MultiBuffer::new(ReadWrite);
15815 multibuffer.push_excerpts(
15816 buffer_1.clone(),
15817 [
15818 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15819 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15820 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15821 ],
15822 cx,
15823 );
15824 multibuffer.push_excerpts(
15825 buffer_2.clone(),
15826 [
15827 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15828 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15829 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15830 ],
15831 cx,
15832 );
15833 multibuffer.push_excerpts(
15834 buffer_3.clone(),
15835 [
15836 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15837 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15838 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15839 ],
15840 cx,
15841 );
15842 multibuffer
15843 });
15844
15845 let fs = FakeFs::new(cx.executor());
15846 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
15847 let (editor, cx) = cx
15848 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
15849 editor.update_in(cx, |editor, _window, cx| {
15850 for (buffer, diff_base) in [
15851 (buffer_1.clone(), base_text_1),
15852 (buffer_2.clone(), base_text_2),
15853 (buffer_3.clone(), base_text_3),
15854 ] {
15855 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
15856 editor
15857 .buffer
15858 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
15859 }
15860 });
15861 cx.executor().run_until_parked();
15862
15863 editor.update_in(cx, |editor, window, cx| {
15864 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}");
15865 editor.select_all(&SelectAll, window, cx);
15866 editor.git_restore(&Default::default(), window, cx);
15867 });
15868 cx.executor().run_until_parked();
15869
15870 // When all ranges are selected, all buffer hunks are reverted.
15871 editor.update(cx, |editor, cx| {
15872 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");
15873 });
15874 buffer_1.update(cx, |buffer, _| {
15875 assert_eq!(buffer.text(), base_text_1);
15876 });
15877 buffer_2.update(cx, |buffer, _| {
15878 assert_eq!(buffer.text(), base_text_2);
15879 });
15880 buffer_3.update(cx, |buffer, _| {
15881 assert_eq!(buffer.text(), base_text_3);
15882 });
15883
15884 editor.update_in(cx, |editor, window, cx| {
15885 editor.undo(&Default::default(), window, cx);
15886 });
15887
15888 editor.update_in(cx, |editor, window, cx| {
15889 editor.change_selections(None, window, cx, |s| {
15890 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
15891 });
15892 editor.git_restore(&Default::default(), window, cx);
15893 });
15894
15895 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
15896 // but not affect buffer_2 and its related excerpts.
15897 editor.update(cx, |editor, cx| {
15898 assert_eq!(
15899 editor.text(cx),
15900 "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}"
15901 );
15902 });
15903 buffer_1.update(cx, |buffer, _| {
15904 assert_eq!(buffer.text(), base_text_1);
15905 });
15906 buffer_2.update(cx, |buffer, _| {
15907 assert_eq!(
15908 buffer.text(),
15909 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
15910 );
15911 });
15912 buffer_3.update(cx, |buffer, _| {
15913 assert_eq!(
15914 buffer.text(),
15915 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
15916 );
15917 });
15918
15919 fn edit_first_char_of_every_line(text: &str) -> String {
15920 text.split('\n')
15921 .map(|line| format!("X{}", &line[1..]))
15922 .collect::<Vec<_>>()
15923 .join("\n")
15924 }
15925}
15926
15927#[gpui::test]
15928async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
15929 init_test(cx, |_| {});
15930
15931 let cols = 4;
15932 let rows = 10;
15933 let sample_text_1 = sample_text(rows, cols, 'a');
15934 assert_eq!(
15935 sample_text_1,
15936 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
15937 );
15938 let sample_text_2 = sample_text(rows, cols, 'l');
15939 assert_eq!(
15940 sample_text_2,
15941 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
15942 );
15943 let sample_text_3 = sample_text(rows, cols, 'v');
15944 assert_eq!(
15945 sample_text_3,
15946 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
15947 );
15948
15949 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
15950 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
15951 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
15952
15953 let multi_buffer = cx.new(|cx| {
15954 let mut multibuffer = MultiBuffer::new(ReadWrite);
15955 multibuffer.push_excerpts(
15956 buffer_1.clone(),
15957 [
15958 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15959 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15960 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15961 ],
15962 cx,
15963 );
15964 multibuffer.push_excerpts(
15965 buffer_2.clone(),
15966 [
15967 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15968 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15969 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15970 ],
15971 cx,
15972 );
15973 multibuffer.push_excerpts(
15974 buffer_3.clone(),
15975 [
15976 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15977 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15978 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15979 ],
15980 cx,
15981 );
15982 multibuffer
15983 });
15984
15985 let fs = FakeFs::new(cx.executor());
15986 fs.insert_tree(
15987 "/a",
15988 json!({
15989 "main.rs": sample_text_1,
15990 "other.rs": sample_text_2,
15991 "lib.rs": sample_text_3,
15992 }),
15993 )
15994 .await;
15995 let project = Project::test(fs, ["/a".as_ref()], cx).await;
15996 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15997 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15998 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15999 Editor::new(
16000 EditorMode::full(),
16001 multi_buffer,
16002 Some(project.clone()),
16003 window,
16004 cx,
16005 )
16006 });
16007 let multibuffer_item_id = workspace
16008 .update(cx, |workspace, window, cx| {
16009 assert!(
16010 workspace.active_item(cx).is_none(),
16011 "active item should be None before the first item is added"
16012 );
16013 workspace.add_item_to_active_pane(
16014 Box::new(multi_buffer_editor.clone()),
16015 None,
16016 true,
16017 window,
16018 cx,
16019 );
16020 let active_item = workspace
16021 .active_item(cx)
16022 .expect("should have an active item after adding the multi buffer");
16023 assert!(
16024 !active_item.is_singleton(cx),
16025 "A multi buffer was expected to active after adding"
16026 );
16027 active_item.item_id()
16028 })
16029 .unwrap();
16030 cx.executor().run_until_parked();
16031
16032 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16033 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
16034 s.select_ranges(Some(1..2))
16035 });
16036 editor.open_excerpts(&OpenExcerpts, window, cx);
16037 });
16038 cx.executor().run_until_parked();
16039 let first_item_id = workspace
16040 .update(cx, |workspace, window, cx| {
16041 let active_item = workspace
16042 .active_item(cx)
16043 .expect("should have an active item after navigating into the 1st buffer");
16044 let first_item_id = active_item.item_id();
16045 assert_ne!(
16046 first_item_id, multibuffer_item_id,
16047 "Should navigate into the 1st buffer and activate it"
16048 );
16049 assert!(
16050 active_item.is_singleton(cx),
16051 "New active item should be a singleton buffer"
16052 );
16053 assert_eq!(
16054 active_item
16055 .act_as::<Editor>(cx)
16056 .expect("should have navigated into an editor for the 1st buffer")
16057 .read(cx)
16058 .text(cx),
16059 sample_text_1
16060 );
16061
16062 workspace
16063 .go_back(workspace.active_pane().downgrade(), window, cx)
16064 .detach_and_log_err(cx);
16065
16066 first_item_id
16067 })
16068 .unwrap();
16069 cx.executor().run_until_parked();
16070 workspace
16071 .update(cx, |workspace, _, cx| {
16072 let active_item = workspace
16073 .active_item(cx)
16074 .expect("should have an active item after navigating back");
16075 assert_eq!(
16076 active_item.item_id(),
16077 multibuffer_item_id,
16078 "Should navigate back to the multi buffer"
16079 );
16080 assert!(!active_item.is_singleton(cx));
16081 })
16082 .unwrap();
16083
16084 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16085 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
16086 s.select_ranges(Some(39..40))
16087 });
16088 editor.open_excerpts(&OpenExcerpts, window, cx);
16089 });
16090 cx.executor().run_until_parked();
16091 let second_item_id = workspace
16092 .update(cx, |workspace, window, cx| {
16093 let active_item = workspace
16094 .active_item(cx)
16095 .expect("should have an active item after navigating into the 2nd buffer");
16096 let second_item_id = active_item.item_id();
16097 assert_ne!(
16098 second_item_id, multibuffer_item_id,
16099 "Should navigate away from the multibuffer"
16100 );
16101 assert_ne!(
16102 second_item_id, first_item_id,
16103 "Should navigate into the 2nd buffer and activate it"
16104 );
16105 assert!(
16106 active_item.is_singleton(cx),
16107 "New active item should be a singleton buffer"
16108 );
16109 assert_eq!(
16110 active_item
16111 .act_as::<Editor>(cx)
16112 .expect("should have navigated into an editor")
16113 .read(cx)
16114 .text(cx),
16115 sample_text_2
16116 );
16117
16118 workspace
16119 .go_back(workspace.active_pane().downgrade(), window, cx)
16120 .detach_and_log_err(cx);
16121
16122 second_item_id
16123 })
16124 .unwrap();
16125 cx.executor().run_until_parked();
16126 workspace
16127 .update(cx, |workspace, _, cx| {
16128 let active_item = workspace
16129 .active_item(cx)
16130 .expect("should have an active item after navigating back from the 2nd buffer");
16131 assert_eq!(
16132 active_item.item_id(),
16133 multibuffer_item_id,
16134 "Should navigate back from the 2nd buffer to the multi buffer"
16135 );
16136 assert!(!active_item.is_singleton(cx));
16137 })
16138 .unwrap();
16139
16140 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16141 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
16142 s.select_ranges(Some(70..70))
16143 });
16144 editor.open_excerpts(&OpenExcerpts, window, cx);
16145 });
16146 cx.executor().run_until_parked();
16147 workspace
16148 .update(cx, |workspace, window, cx| {
16149 let active_item = workspace
16150 .active_item(cx)
16151 .expect("should have an active item after navigating into the 3rd buffer");
16152 let third_item_id = active_item.item_id();
16153 assert_ne!(
16154 third_item_id, multibuffer_item_id,
16155 "Should navigate into the 3rd buffer and activate it"
16156 );
16157 assert_ne!(third_item_id, first_item_id);
16158 assert_ne!(third_item_id, second_item_id);
16159 assert!(
16160 active_item.is_singleton(cx),
16161 "New active item should be a singleton buffer"
16162 );
16163 assert_eq!(
16164 active_item
16165 .act_as::<Editor>(cx)
16166 .expect("should have navigated into an editor")
16167 .read(cx)
16168 .text(cx),
16169 sample_text_3
16170 );
16171
16172 workspace
16173 .go_back(workspace.active_pane().downgrade(), window, cx)
16174 .detach_and_log_err(cx);
16175 })
16176 .unwrap();
16177 cx.executor().run_until_parked();
16178 workspace
16179 .update(cx, |workspace, _, cx| {
16180 let active_item = workspace
16181 .active_item(cx)
16182 .expect("should have an active item after navigating back from the 3rd buffer");
16183 assert_eq!(
16184 active_item.item_id(),
16185 multibuffer_item_id,
16186 "Should navigate back from the 3rd buffer to the multi buffer"
16187 );
16188 assert!(!active_item.is_singleton(cx));
16189 })
16190 .unwrap();
16191}
16192
16193#[gpui::test]
16194async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16195 init_test(cx, |_| {});
16196
16197 let mut cx = EditorTestContext::new(cx).await;
16198
16199 let diff_base = r#"
16200 use some::mod;
16201
16202 const A: u32 = 42;
16203
16204 fn main() {
16205 println!("hello");
16206
16207 println!("world");
16208 }
16209 "#
16210 .unindent();
16211
16212 cx.set_state(
16213 &r#"
16214 use some::modified;
16215
16216 ˇ
16217 fn main() {
16218 println!("hello there");
16219
16220 println!("around the");
16221 println!("world");
16222 }
16223 "#
16224 .unindent(),
16225 );
16226
16227 cx.set_head_text(&diff_base);
16228 executor.run_until_parked();
16229
16230 cx.update_editor(|editor, window, cx| {
16231 editor.go_to_next_hunk(&GoToHunk, window, cx);
16232 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16233 });
16234 executor.run_until_parked();
16235 cx.assert_state_with_diff(
16236 r#"
16237 use some::modified;
16238
16239
16240 fn main() {
16241 - println!("hello");
16242 + ˇ println!("hello there");
16243
16244 println!("around the");
16245 println!("world");
16246 }
16247 "#
16248 .unindent(),
16249 );
16250
16251 cx.update_editor(|editor, window, cx| {
16252 for _ in 0..2 {
16253 editor.go_to_next_hunk(&GoToHunk, window, cx);
16254 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16255 }
16256 });
16257 executor.run_until_parked();
16258 cx.assert_state_with_diff(
16259 r#"
16260 - use some::mod;
16261 + ˇuse some::modified;
16262
16263
16264 fn main() {
16265 - println!("hello");
16266 + println!("hello there");
16267
16268 + println!("around the");
16269 println!("world");
16270 }
16271 "#
16272 .unindent(),
16273 );
16274
16275 cx.update_editor(|editor, window, cx| {
16276 editor.go_to_next_hunk(&GoToHunk, window, cx);
16277 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16278 });
16279 executor.run_until_parked();
16280 cx.assert_state_with_diff(
16281 r#"
16282 - use some::mod;
16283 + use some::modified;
16284
16285 - const A: u32 = 42;
16286 ˇ
16287 fn main() {
16288 - println!("hello");
16289 + println!("hello there");
16290
16291 + println!("around the");
16292 println!("world");
16293 }
16294 "#
16295 .unindent(),
16296 );
16297
16298 cx.update_editor(|editor, window, cx| {
16299 editor.cancel(&Cancel, window, cx);
16300 });
16301
16302 cx.assert_state_with_diff(
16303 r#"
16304 use some::modified;
16305
16306 ˇ
16307 fn main() {
16308 println!("hello there");
16309
16310 println!("around the");
16311 println!("world");
16312 }
16313 "#
16314 .unindent(),
16315 );
16316}
16317
16318#[gpui::test]
16319async fn test_diff_base_change_with_expanded_diff_hunks(
16320 executor: BackgroundExecutor,
16321 cx: &mut TestAppContext,
16322) {
16323 init_test(cx, |_| {});
16324
16325 let mut cx = EditorTestContext::new(cx).await;
16326
16327 let diff_base = r#"
16328 use some::mod1;
16329 use some::mod2;
16330
16331 const A: u32 = 42;
16332 const B: u32 = 42;
16333 const C: u32 = 42;
16334
16335 fn main() {
16336 println!("hello");
16337
16338 println!("world");
16339 }
16340 "#
16341 .unindent();
16342
16343 cx.set_state(
16344 &r#"
16345 use some::mod2;
16346
16347 const A: u32 = 42;
16348 const C: u32 = 42;
16349
16350 fn main(ˇ) {
16351 //println!("hello");
16352
16353 println!("world");
16354 //
16355 //
16356 }
16357 "#
16358 .unindent(),
16359 );
16360
16361 cx.set_head_text(&diff_base);
16362 executor.run_until_parked();
16363
16364 cx.update_editor(|editor, window, cx| {
16365 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16366 });
16367 executor.run_until_parked();
16368 cx.assert_state_with_diff(
16369 r#"
16370 - use some::mod1;
16371 use some::mod2;
16372
16373 const A: u32 = 42;
16374 - const B: u32 = 42;
16375 const C: u32 = 42;
16376
16377 fn main(ˇ) {
16378 - println!("hello");
16379 + //println!("hello");
16380
16381 println!("world");
16382 + //
16383 + //
16384 }
16385 "#
16386 .unindent(),
16387 );
16388
16389 cx.set_head_text("new diff base!");
16390 executor.run_until_parked();
16391 cx.assert_state_with_diff(
16392 r#"
16393 - new diff base!
16394 + use some::mod2;
16395 +
16396 + const A: u32 = 42;
16397 + const C: u32 = 42;
16398 +
16399 + fn main(ˇ) {
16400 + //println!("hello");
16401 +
16402 + println!("world");
16403 + //
16404 + //
16405 + }
16406 "#
16407 .unindent(),
16408 );
16409}
16410
16411#[gpui::test]
16412async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
16413 init_test(cx, |_| {});
16414
16415 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
16416 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
16417 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
16418 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
16419 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
16420 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
16421
16422 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
16423 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
16424 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
16425
16426 let multi_buffer = cx.new(|cx| {
16427 let mut multibuffer = MultiBuffer::new(ReadWrite);
16428 multibuffer.push_excerpts(
16429 buffer_1.clone(),
16430 [
16431 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16432 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16433 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16434 ],
16435 cx,
16436 );
16437 multibuffer.push_excerpts(
16438 buffer_2.clone(),
16439 [
16440 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16441 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16442 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16443 ],
16444 cx,
16445 );
16446 multibuffer.push_excerpts(
16447 buffer_3.clone(),
16448 [
16449 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16450 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16451 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16452 ],
16453 cx,
16454 );
16455 multibuffer
16456 });
16457
16458 let editor =
16459 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
16460 editor
16461 .update(cx, |editor, _window, cx| {
16462 for (buffer, diff_base) in [
16463 (buffer_1.clone(), file_1_old),
16464 (buffer_2.clone(), file_2_old),
16465 (buffer_3.clone(), file_3_old),
16466 ] {
16467 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
16468 editor
16469 .buffer
16470 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
16471 }
16472 })
16473 .unwrap();
16474
16475 let mut cx = EditorTestContext::for_editor(editor, cx).await;
16476 cx.run_until_parked();
16477
16478 cx.assert_editor_state(
16479 &"
16480 ˇaaa
16481 ccc
16482 ddd
16483
16484 ggg
16485 hhh
16486
16487
16488 lll
16489 mmm
16490 NNN
16491
16492 qqq
16493 rrr
16494
16495 uuu
16496 111
16497 222
16498 333
16499
16500 666
16501 777
16502
16503 000
16504 !!!"
16505 .unindent(),
16506 );
16507
16508 cx.update_editor(|editor, window, cx| {
16509 editor.select_all(&SelectAll, window, cx);
16510 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16511 });
16512 cx.executor().run_until_parked();
16513
16514 cx.assert_state_with_diff(
16515 "
16516 «aaa
16517 - bbb
16518 ccc
16519 ddd
16520
16521 ggg
16522 hhh
16523
16524
16525 lll
16526 mmm
16527 - nnn
16528 + NNN
16529
16530 qqq
16531 rrr
16532
16533 uuu
16534 111
16535 222
16536 333
16537
16538 + 666
16539 777
16540
16541 000
16542 !!!ˇ»"
16543 .unindent(),
16544 );
16545}
16546
16547#[gpui::test]
16548async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
16549 init_test(cx, |_| {});
16550
16551 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
16552 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
16553
16554 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
16555 let multi_buffer = cx.new(|cx| {
16556 let mut multibuffer = MultiBuffer::new(ReadWrite);
16557 multibuffer.push_excerpts(
16558 buffer.clone(),
16559 [
16560 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
16561 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
16562 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
16563 ],
16564 cx,
16565 );
16566 multibuffer
16567 });
16568
16569 let editor =
16570 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
16571 editor
16572 .update(cx, |editor, _window, cx| {
16573 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
16574 editor
16575 .buffer
16576 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
16577 })
16578 .unwrap();
16579
16580 let mut cx = EditorTestContext::for_editor(editor, cx).await;
16581 cx.run_until_parked();
16582
16583 cx.update_editor(|editor, window, cx| {
16584 editor.expand_all_diff_hunks(&Default::default(), window, cx)
16585 });
16586 cx.executor().run_until_parked();
16587
16588 // When the start of a hunk coincides with the start of its excerpt,
16589 // the hunk is expanded. When the start of a a hunk is earlier than
16590 // the start of its excerpt, the hunk is not expanded.
16591 cx.assert_state_with_diff(
16592 "
16593 ˇaaa
16594 - bbb
16595 + BBB
16596
16597 - ddd
16598 - eee
16599 + DDD
16600 + EEE
16601 fff
16602
16603 iii
16604 "
16605 .unindent(),
16606 );
16607}
16608
16609#[gpui::test]
16610async fn test_edits_around_expanded_insertion_hunks(
16611 executor: BackgroundExecutor,
16612 cx: &mut TestAppContext,
16613) {
16614 init_test(cx, |_| {});
16615
16616 let mut cx = EditorTestContext::new(cx).await;
16617
16618 let diff_base = r#"
16619 use some::mod1;
16620 use some::mod2;
16621
16622 const A: u32 = 42;
16623
16624 fn main() {
16625 println!("hello");
16626
16627 println!("world");
16628 }
16629 "#
16630 .unindent();
16631 executor.run_until_parked();
16632 cx.set_state(
16633 &r#"
16634 use some::mod1;
16635 use some::mod2;
16636
16637 const A: u32 = 42;
16638 const B: u32 = 42;
16639 const C: u32 = 42;
16640 ˇ
16641
16642 fn main() {
16643 println!("hello");
16644
16645 println!("world");
16646 }
16647 "#
16648 .unindent(),
16649 );
16650
16651 cx.set_head_text(&diff_base);
16652 executor.run_until_parked();
16653
16654 cx.update_editor(|editor, window, cx| {
16655 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16656 });
16657 executor.run_until_parked();
16658
16659 cx.assert_state_with_diff(
16660 r#"
16661 use some::mod1;
16662 use some::mod2;
16663
16664 const A: u32 = 42;
16665 + const B: u32 = 42;
16666 + const C: u32 = 42;
16667 + ˇ
16668
16669 fn main() {
16670 println!("hello");
16671
16672 println!("world");
16673 }
16674 "#
16675 .unindent(),
16676 );
16677
16678 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
16679 executor.run_until_parked();
16680
16681 cx.assert_state_with_diff(
16682 r#"
16683 use some::mod1;
16684 use some::mod2;
16685
16686 const A: u32 = 42;
16687 + const B: u32 = 42;
16688 + const C: u32 = 42;
16689 + const D: u32 = 42;
16690 + ˇ
16691
16692 fn main() {
16693 println!("hello");
16694
16695 println!("world");
16696 }
16697 "#
16698 .unindent(),
16699 );
16700
16701 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
16702 executor.run_until_parked();
16703
16704 cx.assert_state_with_diff(
16705 r#"
16706 use some::mod1;
16707 use some::mod2;
16708
16709 const A: u32 = 42;
16710 + const B: u32 = 42;
16711 + const C: u32 = 42;
16712 + const D: u32 = 42;
16713 + const E: u32 = 42;
16714 + ˇ
16715
16716 fn main() {
16717 println!("hello");
16718
16719 println!("world");
16720 }
16721 "#
16722 .unindent(),
16723 );
16724
16725 cx.update_editor(|editor, window, cx| {
16726 editor.delete_line(&DeleteLine, window, cx);
16727 });
16728 executor.run_until_parked();
16729
16730 cx.assert_state_with_diff(
16731 r#"
16732 use some::mod1;
16733 use some::mod2;
16734
16735 const A: u32 = 42;
16736 + const B: u32 = 42;
16737 + const C: u32 = 42;
16738 + const D: u32 = 42;
16739 + const E: u32 = 42;
16740 ˇ
16741 fn main() {
16742 println!("hello");
16743
16744 println!("world");
16745 }
16746 "#
16747 .unindent(),
16748 );
16749
16750 cx.update_editor(|editor, window, cx| {
16751 editor.move_up(&MoveUp, window, cx);
16752 editor.delete_line(&DeleteLine, window, cx);
16753 editor.move_up(&MoveUp, window, cx);
16754 editor.delete_line(&DeleteLine, window, cx);
16755 editor.move_up(&MoveUp, window, cx);
16756 editor.delete_line(&DeleteLine, window, cx);
16757 });
16758 executor.run_until_parked();
16759 cx.assert_state_with_diff(
16760 r#"
16761 use some::mod1;
16762 use some::mod2;
16763
16764 const A: u32 = 42;
16765 + const B: u32 = 42;
16766 ˇ
16767 fn main() {
16768 println!("hello");
16769
16770 println!("world");
16771 }
16772 "#
16773 .unindent(),
16774 );
16775
16776 cx.update_editor(|editor, window, cx| {
16777 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
16778 editor.delete_line(&DeleteLine, window, cx);
16779 });
16780 executor.run_until_parked();
16781 cx.assert_state_with_diff(
16782 r#"
16783 ˇ
16784 fn main() {
16785 println!("hello");
16786
16787 println!("world");
16788 }
16789 "#
16790 .unindent(),
16791 );
16792}
16793
16794#[gpui::test]
16795async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
16796 init_test(cx, |_| {});
16797
16798 let mut cx = EditorTestContext::new(cx).await;
16799 cx.set_head_text(indoc! { "
16800 one
16801 two
16802 three
16803 four
16804 five
16805 "
16806 });
16807 cx.set_state(indoc! { "
16808 one
16809 ˇthree
16810 five
16811 "});
16812 cx.run_until_parked();
16813 cx.update_editor(|editor, window, cx| {
16814 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16815 });
16816 cx.assert_state_with_diff(
16817 indoc! { "
16818 one
16819 - two
16820 ˇthree
16821 - four
16822 five
16823 "}
16824 .to_string(),
16825 );
16826 cx.update_editor(|editor, window, cx| {
16827 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16828 });
16829
16830 cx.assert_state_with_diff(
16831 indoc! { "
16832 one
16833 ˇthree
16834 five
16835 "}
16836 .to_string(),
16837 );
16838
16839 cx.set_state(indoc! { "
16840 one
16841 ˇTWO
16842 three
16843 four
16844 five
16845 "});
16846 cx.run_until_parked();
16847 cx.update_editor(|editor, window, cx| {
16848 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16849 });
16850
16851 cx.assert_state_with_diff(
16852 indoc! { "
16853 one
16854 - two
16855 + ˇTWO
16856 three
16857 four
16858 five
16859 "}
16860 .to_string(),
16861 );
16862 cx.update_editor(|editor, window, cx| {
16863 editor.move_up(&Default::default(), window, cx);
16864 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16865 });
16866 cx.assert_state_with_diff(
16867 indoc! { "
16868 one
16869 ˇTWO
16870 three
16871 four
16872 five
16873 "}
16874 .to_string(),
16875 );
16876}
16877
16878#[gpui::test]
16879async fn test_edits_around_expanded_deletion_hunks(
16880 executor: BackgroundExecutor,
16881 cx: &mut TestAppContext,
16882) {
16883 init_test(cx, |_| {});
16884
16885 let mut cx = EditorTestContext::new(cx).await;
16886
16887 let diff_base = r#"
16888 use some::mod1;
16889 use some::mod2;
16890
16891 const A: u32 = 42;
16892 const B: u32 = 42;
16893 const C: u32 = 42;
16894
16895
16896 fn main() {
16897 println!("hello");
16898
16899 println!("world");
16900 }
16901 "#
16902 .unindent();
16903 executor.run_until_parked();
16904 cx.set_state(
16905 &r#"
16906 use some::mod1;
16907 use some::mod2;
16908
16909 ˇconst B: u32 = 42;
16910 const C: u32 = 42;
16911
16912
16913 fn main() {
16914 println!("hello");
16915
16916 println!("world");
16917 }
16918 "#
16919 .unindent(),
16920 );
16921
16922 cx.set_head_text(&diff_base);
16923 executor.run_until_parked();
16924
16925 cx.update_editor(|editor, window, cx| {
16926 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16927 });
16928 executor.run_until_parked();
16929
16930 cx.assert_state_with_diff(
16931 r#"
16932 use some::mod1;
16933 use some::mod2;
16934
16935 - const A: u32 = 42;
16936 ˇconst B: u32 = 42;
16937 const C: u32 = 42;
16938
16939
16940 fn main() {
16941 println!("hello");
16942
16943 println!("world");
16944 }
16945 "#
16946 .unindent(),
16947 );
16948
16949 cx.update_editor(|editor, window, cx| {
16950 editor.delete_line(&DeleteLine, window, cx);
16951 });
16952 executor.run_until_parked();
16953 cx.assert_state_with_diff(
16954 r#"
16955 use some::mod1;
16956 use some::mod2;
16957
16958 - const A: u32 = 42;
16959 - const B: u32 = 42;
16960 ˇconst C: u32 = 42;
16961
16962
16963 fn main() {
16964 println!("hello");
16965
16966 println!("world");
16967 }
16968 "#
16969 .unindent(),
16970 );
16971
16972 cx.update_editor(|editor, window, cx| {
16973 editor.delete_line(&DeleteLine, window, cx);
16974 });
16975 executor.run_until_parked();
16976 cx.assert_state_with_diff(
16977 r#"
16978 use some::mod1;
16979 use some::mod2;
16980
16981 - const A: u32 = 42;
16982 - const B: u32 = 42;
16983 - const C: u32 = 42;
16984 ˇ
16985
16986 fn main() {
16987 println!("hello");
16988
16989 println!("world");
16990 }
16991 "#
16992 .unindent(),
16993 );
16994
16995 cx.update_editor(|editor, window, cx| {
16996 editor.handle_input("replacement", window, cx);
16997 });
16998 executor.run_until_parked();
16999 cx.assert_state_with_diff(
17000 r#"
17001 use some::mod1;
17002 use some::mod2;
17003
17004 - const A: u32 = 42;
17005 - const B: u32 = 42;
17006 - const C: u32 = 42;
17007 -
17008 + replacementˇ
17009
17010 fn main() {
17011 println!("hello");
17012
17013 println!("world");
17014 }
17015 "#
17016 .unindent(),
17017 );
17018}
17019
17020#[gpui::test]
17021async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17022 init_test(cx, |_| {});
17023
17024 let mut cx = EditorTestContext::new(cx).await;
17025
17026 let base_text = r#"
17027 one
17028 two
17029 three
17030 four
17031 five
17032 "#
17033 .unindent();
17034 executor.run_until_parked();
17035 cx.set_state(
17036 &r#"
17037 one
17038 two
17039 fˇour
17040 five
17041 "#
17042 .unindent(),
17043 );
17044
17045 cx.set_head_text(&base_text);
17046 executor.run_until_parked();
17047
17048 cx.update_editor(|editor, window, cx| {
17049 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17050 });
17051 executor.run_until_parked();
17052
17053 cx.assert_state_with_diff(
17054 r#"
17055 one
17056 two
17057 - three
17058 fˇour
17059 five
17060 "#
17061 .unindent(),
17062 );
17063
17064 cx.update_editor(|editor, window, cx| {
17065 editor.backspace(&Backspace, window, cx);
17066 editor.backspace(&Backspace, window, cx);
17067 });
17068 executor.run_until_parked();
17069 cx.assert_state_with_diff(
17070 r#"
17071 one
17072 two
17073 - threeˇ
17074 - four
17075 + our
17076 five
17077 "#
17078 .unindent(),
17079 );
17080}
17081
17082#[gpui::test]
17083async fn test_edit_after_expanded_modification_hunk(
17084 executor: BackgroundExecutor,
17085 cx: &mut TestAppContext,
17086) {
17087 init_test(cx, |_| {});
17088
17089 let mut cx = EditorTestContext::new(cx).await;
17090
17091 let diff_base = r#"
17092 use some::mod1;
17093 use some::mod2;
17094
17095 const A: u32 = 42;
17096 const B: u32 = 42;
17097 const C: u32 = 42;
17098 const D: u32 = 42;
17099
17100
17101 fn main() {
17102 println!("hello");
17103
17104 println!("world");
17105 }"#
17106 .unindent();
17107
17108 cx.set_state(
17109 &r#"
17110 use some::mod1;
17111 use some::mod2;
17112
17113 const A: u32 = 42;
17114 const B: u32 = 42;
17115 const C: u32 = 43ˇ
17116 const D: u32 = 42;
17117
17118
17119 fn main() {
17120 println!("hello");
17121
17122 println!("world");
17123 }"#
17124 .unindent(),
17125 );
17126
17127 cx.set_head_text(&diff_base);
17128 executor.run_until_parked();
17129 cx.update_editor(|editor, window, cx| {
17130 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17131 });
17132 executor.run_until_parked();
17133
17134 cx.assert_state_with_diff(
17135 r#"
17136 use some::mod1;
17137 use some::mod2;
17138
17139 const A: u32 = 42;
17140 const B: u32 = 42;
17141 - const C: u32 = 42;
17142 + const C: u32 = 43ˇ
17143 const D: u32 = 42;
17144
17145
17146 fn main() {
17147 println!("hello");
17148
17149 println!("world");
17150 }"#
17151 .unindent(),
17152 );
17153
17154 cx.update_editor(|editor, window, cx| {
17155 editor.handle_input("\nnew_line\n", window, cx);
17156 });
17157 executor.run_until_parked();
17158
17159 cx.assert_state_with_diff(
17160 r#"
17161 use some::mod1;
17162 use some::mod2;
17163
17164 const A: u32 = 42;
17165 const B: u32 = 42;
17166 - const C: u32 = 42;
17167 + const C: u32 = 43
17168 + new_line
17169 + ˇ
17170 const D: u32 = 42;
17171
17172
17173 fn main() {
17174 println!("hello");
17175
17176 println!("world");
17177 }"#
17178 .unindent(),
17179 );
17180}
17181
17182#[gpui::test]
17183async fn test_stage_and_unstage_added_file_hunk(
17184 executor: BackgroundExecutor,
17185 cx: &mut TestAppContext,
17186) {
17187 init_test(cx, |_| {});
17188
17189 let mut cx = EditorTestContext::new(cx).await;
17190 cx.update_editor(|editor, _, cx| {
17191 editor.set_expand_all_diff_hunks(cx);
17192 });
17193
17194 let working_copy = r#"
17195 ˇfn main() {
17196 println!("hello, world!");
17197 }
17198 "#
17199 .unindent();
17200
17201 cx.set_state(&working_copy);
17202 executor.run_until_parked();
17203
17204 cx.assert_state_with_diff(
17205 r#"
17206 + ˇfn main() {
17207 + println!("hello, world!");
17208 + }
17209 "#
17210 .unindent(),
17211 );
17212 cx.assert_index_text(None);
17213
17214 cx.update_editor(|editor, window, cx| {
17215 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17216 });
17217 executor.run_until_parked();
17218 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
17219 cx.assert_state_with_diff(
17220 r#"
17221 + ˇfn main() {
17222 + println!("hello, world!");
17223 + }
17224 "#
17225 .unindent(),
17226 );
17227
17228 cx.update_editor(|editor, window, cx| {
17229 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17230 });
17231 executor.run_until_parked();
17232 cx.assert_index_text(None);
17233}
17234
17235async fn setup_indent_guides_editor(
17236 text: &str,
17237 cx: &mut TestAppContext,
17238) -> (BufferId, EditorTestContext) {
17239 init_test(cx, |_| {});
17240
17241 let mut cx = EditorTestContext::new(cx).await;
17242
17243 let buffer_id = cx.update_editor(|editor, window, cx| {
17244 editor.set_text(text, window, cx);
17245 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
17246
17247 buffer_ids[0]
17248 });
17249
17250 (buffer_id, cx)
17251}
17252
17253fn assert_indent_guides(
17254 range: Range<u32>,
17255 expected: Vec<IndentGuide>,
17256 active_indices: Option<Vec<usize>>,
17257 cx: &mut EditorTestContext,
17258) {
17259 let indent_guides = cx.update_editor(|editor, window, cx| {
17260 let snapshot = editor.snapshot(window, cx).display_snapshot;
17261 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
17262 editor,
17263 MultiBufferRow(range.start)..MultiBufferRow(range.end),
17264 true,
17265 &snapshot,
17266 cx,
17267 );
17268
17269 indent_guides.sort_by(|a, b| {
17270 a.depth.cmp(&b.depth).then(
17271 a.start_row
17272 .cmp(&b.start_row)
17273 .then(a.end_row.cmp(&b.end_row)),
17274 )
17275 });
17276 indent_guides
17277 });
17278
17279 if let Some(expected) = active_indices {
17280 let active_indices = cx.update_editor(|editor, window, cx| {
17281 let snapshot = editor.snapshot(window, cx).display_snapshot;
17282 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
17283 });
17284
17285 assert_eq!(
17286 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
17287 expected,
17288 "Active indent guide indices do not match"
17289 );
17290 }
17291
17292 assert_eq!(indent_guides, expected, "Indent guides do not match");
17293}
17294
17295fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
17296 IndentGuide {
17297 buffer_id,
17298 start_row: MultiBufferRow(start_row),
17299 end_row: MultiBufferRow(end_row),
17300 depth,
17301 tab_size: 4,
17302 settings: IndentGuideSettings {
17303 enabled: true,
17304 line_width: 1,
17305 active_line_width: 1,
17306 ..Default::default()
17307 },
17308 }
17309}
17310
17311#[gpui::test]
17312async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
17313 let (buffer_id, mut cx) = setup_indent_guides_editor(
17314 &"
17315 fn main() {
17316 let a = 1;
17317 }"
17318 .unindent(),
17319 cx,
17320 )
17321 .await;
17322
17323 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
17324}
17325
17326#[gpui::test]
17327async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
17328 let (buffer_id, mut cx) = setup_indent_guides_editor(
17329 &"
17330 fn main() {
17331 let a = 1;
17332 let b = 2;
17333 }"
17334 .unindent(),
17335 cx,
17336 )
17337 .await;
17338
17339 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
17340}
17341
17342#[gpui::test]
17343async fn test_indent_guide_nested(cx: &mut TestAppContext) {
17344 let (buffer_id, mut cx) = setup_indent_guides_editor(
17345 &"
17346 fn main() {
17347 let a = 1;
17348 if a == 3 {
17349 let b = 2;
17350 } else {
17351 let c = 3;
17352 }
17353 }"
17354 .unindent(),
17355 cx,
17356 )
17357 .await;
17358
17359 assert_indent_guides(
17360 0..8,
17361 vec![
17362 indent_guide(buffer_id, 1, 6, 0),
17363 indent_guide(buffer_id, 3, 3, 1),
17364 indent_guide(buffer_id, 5, 5, 1),
17365 ],
17366 None,
17367 &mut cx,
17368 );
17369}
17370
17371#[gpui::test]
17372async fn test_indent_guide_tab(cx: &mut TestAppContext) {
17373 let (buffer_id, mut cx) = setup_indent_guides_editor(
17374 &"
17375 fn main() {
17376 let a = 1;
17377 let b = 2;
17378 let c = 3;
17379 }"
17380 .unindent(),
17381 cx,
17382 )
17383 .await;
17384
17385 assert_indent_guides(
17386 0..5,
17387 vec![
17388 indent_guide(buffer_id, 1, 3, 0),
17389 indent_guide(buffer_id, 2, 2, 1),
17390 ],
17391 None,
17392 &mut cx,
17393 );
17394}
17395
17396#[gpui::test]
17397async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
17398 let (buffer_id, mut cx) = setup_indent_guides_editor(
17399 &"
17400 fn main() {
17401 let a = 1;
17402
17403 let c = 3;
17404 }"
17405 .unindent(),
17406 cx,
17407 )
17408 .await;
17409
17410 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
17411}
17412
17413#[gpui::test]
17414async fn test_indent_guide_complex(cx: &mut TestAppContext) {
17415 let (buffer_id, mut cx) = setup_indent_guides_editor(
17416 &"
17417 fn main() {
17418 let a = 1;
17419
17420 let c = 3;
17421
17422 if a == 3 {
17423 let b = 2;
17424 } else {
17425 let c = 3;
17426 }
17427 }"
17428 .unindent(),
17429 cx,
17430 )
17431 .await;
17432
17433 assert_indent_guides(
17434 0..11,
17435 vec![
17436 indent_guide(buffer_id, 1, 9, 0),
17437 indent_guide(buffer_id, 6, 6, 1),
17438 indent_guide(buffer_id, 8, 8, 1),
17439 ],
17440 None,
17441 &mut cx,
17442 );
17443}
17444
17445#[gpui::test]
17446async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
17447 let (buffer_id, mut cx) = setup_indent_guides_editor(
17448 &"
17449 fn main() {
17450 let a = 1;
17451
17452 let c = 3;
17453
17454 if a == 3 {
17455 let b = 2;
17456 } else {
17457 let c = 3;
17458 }
17459 }"
17460 .unindent(),
17461 cx,
17462 )
17463 .await;
17464
17465 assert_indent_guides(
17466 1..11,
17467 vec![
17468 indent_guide(buffer_id, 1, 9, 0),
17469 indent_guide(buffer_id, 6, 6, 1),
17470 indent_guide(buffer_id, 8, 8, 1),
17471 ],
17472 None,
17473 &mut cx,
17474 );
17475}
17476
17477#[gpui::test]
17478async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
17479 let (buffer_id, mut cx) = setup_indent_guides_editor(
17480 &"
17481 fn main() {
17482 let a = 1;
17483
17484 let c = 3;
17485
17486 if a == 3 {
17487 let b = 2;
17488 } else {
17489 let c = 3;
17490 }
17491 }"
17492 .unindent(),
17493 cx,
17494 )
17495 .await;
17496
17497 assert_indent_guides(
17498 1..10,
17499 vec![
17500 indent_guide(buffer_id, 1, 9, 0),
17501 indent_guide(buffer_id, 6, 6, 1),
17502 indent_guide(buffer_id, 8, 8, 1),
17503 ],
17504 None,
17505 &mut cx,
17506 );
17507}
17508
17509#[gpui::test]
17510async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
17511 let (buffer_id, mut cx) = setup_indent_guides_editor(
17512 &"
17513 fn main() {
17514 if a {
17515 b(
17516 c,
17517 d,
17518 )
17519 } else {
17520 e(
17521 f
17522 )
17523 }
17524 }"
17525 .unindent(),
17526 cx,
17527 )
17528 .await;
17529
17530 assert_indent_guides(
17531 0..11,
17532 vec![
17533 indent_guide(buffer_id, 1, 10, 0),
17534 indent_guide(buffer_id, 2, 5, 1),
17535 indent_guide(buffer_id, 7, 9, 1),
17536 indent_guide(buffer_id, 3, 4, 2),
17537 indent_guide(buffer_id, 8, 8, 2),
17538 ],
17539 None,
17540 &mut cx,
17541 );
17542
17543 cx.update_editor(|editor, window, cx| {
17544 editor.fold_at(MultiBufferRow(2), window, cx);
17545 assert_eq!(
17546 editor.display_text(cx),
17547 "
17548 fn main() {
17549 if a {
17550 b(⋯
17551 )
17552 } else {
17553 e(
17554 f
17555 )
17556 }
17557 }"
17558 .unindent()
17559 );
17560 });
17561
17562 assert_indent_guides(
17563 0..11,
17564 vec![
17565 indent_guide(buffer_id, 1, 10, 0),
17566 indent_guide(buffer_id, 2, 5, 1),
17567 indent_guide(buffer_id, 7, 9, 1),
17568 indent_guide(buffer_id, 8, 8, 2),
17569 ],
17570 None,
17571 &mut cx,
17572 );
17573}
17574
17575#[gpui::test]
17576async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
17577 let (buffer_id, mut cx) = setup_indent_guides_editor(
17578 &"
17579 block1
17580 block2
17581 block3
17582 block4
17583 block2
17584 block1
17585 block1"
17586 .unindent(),
17587 cx,
17588 )
17589 .await;
17590
17591 assert_indent_guides(
17592 1..10,
17593 vec![
17594 indent_guide(buffer_id, 1, 4, 0),
17595 indent_guide(buffer_id, 2, 3, 1),
17596 indent_guide(buffer_id, 3, 3, 2),
17597 ],
17598 None,
17599 &mut cx,
17600 );
17601}
17602
17603#[gpui::test]
17604async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
17605 let (buffer_id, mut cx) = setup_indent_guides_editor(
17606 &"
17607 block1
17608 block2
17609 block3
17610
17611 block1
17612 block1"
17613 .unindent(),
17614 cx,
17615 )
17616 .await;
17617
17618 assert_indent_guides(
17619 0..6,
17620 vec![
17621 indent_guide(buffer_id, 1, 2, 0),
17622 indent_guide(buffer_id, 2, 2, 1),
17623 ],
17624 None,
17625 &mut cx,
17626 );
17627}
17628
17629#[gpui::test]
17630async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
17631 let (buffer_id, mut cx) = setup_indent_guides_editor(
17632 &"
17633 function component() {
17634 \treturn (
17635 \t\t\t
17636 \t\t<div>
17637 \t\t\t<abc></abc>
17638 \t\t</div>
17639 \t)
17640 }"
17641 .unindent(),
17642 cx,
17643 )
17644 .await;
17645
17646 assert_indent_guides(
17647 0..8,
17648 vec![
17649 indent_guide(buffer_id, 1, 6, 0),
17650 indent_guide(buffer_id, 2, 5, 1),
17651 indent_guide(buffer_id, 4, 4, 2),
17652 ],
17653 None,
17654 &mut cx,
17655 );
17656}
17657
17658#[gpui::test]
17659async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
17660 let (buffer_id, mut cx) = setup_indent_guides_editor(
17661 &"
17662 function component() {
17663 \treturn (
17664 \t
17665 \t\t<div>
17666 \t\t\t<abc></abc>
17667 \t\t</div>
17668 \t)
17669 }"
17670 .unindent(),
17671 cx,
17672 )
17673 .await;
17674
17675 assert_indent_guides(
17676 0..8,
17677 vec![
17678 indent_guide(buffer_id, 1, 6, 0),
17679 indent_guide(buffer_id, 2, 5, 1),
17680 indent_guide(buffer_id, 4, 4, 2),
17681 ],
17682 None,
17683 &mut cx,
17684 );
17685}
17686
17687#[gpui::test]
17688async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
17689 let (buffer_id, mut cx) = setup_indent_guides_editor(
17690 &"
17691 block1
17692
17693
17694
17695 block2
17696 "
17697 .unindent(),
17698 cx,
17699 )
17700 .await;
17701
17702 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
17703}
17704
17705#[gpui::test]
17706async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
17707 let (buffer_id, mut cx) = setup_indent_guides_editor(
17708 &"
17709 def a:
17710 \tb = 3
17711 \tif True:
17712 \t\tc = 4
17713 \t\td = 5
17714 \tprint(b)
17715 "
17716 .unindent(),
17717 cx,
17718 )
17719 .await;
17720
17721 assert_indent_guides(
17722 0..6,
17723 vec![
17724 indent_guide(buffer_id, 1, 5, 0),
17725 indent_guide(buffer_id, 3, 4, 1),
17726 ],
17727 None,
17728 &mut cx,
17729 );
17730}
17731
17732#[gpui::test]
17733async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
17734 let (buffer_id, mut cx) = setup_indent_guides_editor(
17735 &"
17736 fn main() {
17737 let a = 1;
17738 }"
17739 .unindent(),
17740 cx,
17741 )
17742 .await;
17743
17744 cx.update_editor(|editor, window, cx| {
17745 editor.change_selections(None, window, cx, |s| {
17746 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17747 });
17748 });
17749
17750 assert_indent_guides(
17751 0..3,
17752 vec![indent_guide(buffer_id, 1, 1, 0)],
17753 Some(vec![0]),
17754 &mut cx,
17755 );
17756}
17757
17758#[gpui::test]
17759async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
17760 let (buffer_id, mut cx) = setup_indent_guides_editor(
17761 &"
17762 fn main() {
17763 if 1 == 2 {
17764 let a = 1;
17765 }
17766 }"
17767 .unindent(),
17768 cx,
17769 )
17770 .await;
17771
17772 cx.update_editor(|editor, window, cx| {
17773 editor.change_selections(None, window, cx, |s| {
17774 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17775 });
17776 });
17777
17778 assert_indent_guides(
17779 0..4,
17780 vec![
17781 indent_guide(buffer_id, 1, 3, 0),
17782 indent_guide(buffer_id, 2, 2, 1),
17783 ],
17784 Some(vec![1]),
17785 &mut cx,
17786 );
17787
17788 cx.update_editor(|editor, window, cx| {
17789 editor.change_selections(None, window, cx, |s| {
17790 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
17791 });
17792 });
17793
17794 assert_indent_guides(
17795 0..4,
17796 vec![
17797 indent_guide(buffer_id, 1, 3, 0),
17798 indent_guide(buffer_id, 2, 2, 1),
17799 ],
17800 Some(vec![1]),
17801 &mut cx,
17802 );
17803
17804 cx.update_editor(|editor, window, cx| {
17805 editor.change_selections(None, window, cx, |s| {
17806 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
17807 });
17808 });
17809
17810 assert_indent_guides(
17811 0..4,
17812 vec![
17813 indent_guide(buffer_id, 1, 3, 0),
17814 indent_guide(buffer_id, 2, 2, 1),
17815 ],
17816 Some(vec![0]),
17817 &mut cx,
17818 );
17819}
17820
17821#[gpui::test]
17822async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
17823 let (buffer_id, mut cx) = setup_indent_guides_editor(
17824 &"
17825 fn main() {
17826 let a = 1;
17827
17828 let b = 2;
17829 }"
17830 .unindent(),
17831 cx,
17832 )
17833 .await;
17834
17835 cx.update_editor(|editor, window, cx| {
17836 editor.change_selections(None, window, cx, |s| {
17837 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
17838 });
17839 });
17840
17841 assert_indent_guides(
17842 0..5,
17843 vec![indent_guide(buffer_id, 1, 3, 0)],
17844 Some(vec![0]),
17845 &mut cx,
17846 );
17847}
17848
17849#[gpui::test]
17850async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
17851 let (buffer_id, mut cx) = setup_indent_guides_editor(
17852 &"
17853 def m:
17854 a = 1
17855 pass"
17856 .unindent(),
17857 cx,
17858 )
17859 .await;
17860
17861 cx.update_editor(|editor, window, cx| {
17862 editor.change_selections(None, window, cx, |s| {
17863 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17864 });
17865 });
17866
17867 assert_indent_guides(
17868 0..3,
17869 vec![indent_guide(buffer_id, 1, 2, 0)],
17870 Some(vec![0]),
17871 &mut cx,
17872 );
17873}
17874
17875#[gpui::test]
17876async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
17877 init_test(cx, |_| {});
17878 let mut cx = EditorTestContext::new(cx).await;
17879 let text = indoc! {
17880 "
17881 impl A {
17882 fn b() {
17883 0;
17884 3;
17885 5;
17886 6;
17887 7;
17888 }
17889 }
17890 "
17891 };
17892 let base_text = indoc! {
17893 "
17894 impl A {
17895 fn b() {
17896 0;
17897 1;
17898 2;
17899 3;
17900 4;
17901 }
17902 fn c() {
17903 5;
17904 6;
17905 7;
17906 }
17907 }
17908 "
17909 };
17910
17911 cx.update_editor(|editor, window, cx| {
17912 editor.set_text(text, window, cx);
17913
17914 editor.buffer().update(cx, |multibuffer, cx| {
17915 let buffer = multibuffer.as_singleton().unwrap();
17916 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
17917
17918 multibuffer.set_all_diff_hunks_expanded(cx);
17919 multibuffer.add_diff(diff, cx);
17920
17921 buffer.read(cx).remote_id()
17922 })
17923 });
17924 cx.run_until_parked();
17925
17926 cx.assert_state_with_diff(
17927 indoc! { "
17928 impl A {
17929 fn b() {
17930 0;
17931 - 1;
17932 - 2;
17933 3;
17934 - 4;
17935 - }
17936 - fn c() {
17937 5;
17938 6;
17939 7;
17940 }
17941 }
17942 ˇ"
17943 }
17944 .to_string(),
17945 );
17946
17947 let mut actual_guides = cx.update_editor(|editor, window, cx| {
17948 editor
17949 .snapshot(window, cx)
17950 .buffer_snapshot
17951 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
17952 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
17953 .collect::<Vec<_>>()
17954 });
17955 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
17956 assert_eq!(
17957 actual_guides,
17958 vec![
17959 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
17960 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
17961 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
17962 ]
17963 );
17964}
17965
17966#[gpui::test]
17967async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17968 init_test(cx, |_| {});
17969 let mut cx = EditorTestContext::new(cx).await;
17970
17971 let diff_base = r#"
17972 a
17973 b
17974 c
17975 "#
17976 .unindent();
17977
17978 cx.set_state(
17979 &r#"
17980 ˇA
17981 b
17982 C
17983 "#
17984 .unindent(),
17985 );
17986 cx.set_head_text(&diff_base);
17987 cx.update_editor(|editor, window, cx| {
17988 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17989 });
17990 executor.run_until_parked();
17991
17992 let both_hunks_expanded = r#"
17993 - a
17994 + ˇA
17995 b
17996 - c
17997 + C
17998 "#
17999 .unindent();
18000
18001 cx.assert_state_with_diff(both_hunks_expanded.clone());
18002
18003 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18004 let snapshot = editor.snapshot(window, cx);
18005 let hunks = editor
18006 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18007 .collect::<Vec<_>>();
18008 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18009 let buffer_id = hunks[0].buffer_id;
18010 hunks
18011 .into_iter()
18012 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18013 .collect::<Vec<_>>()
18014 });
18015 assert_eq!(hunk_ranges.len(), 2);
18016
18017 cx.update_editor(|editor, _, cx| {
18018 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18019 });
18020 executor.run_until_parked();
18021
18022 let second_hunk_expanded = r#"
18023 ˇA
18024 b
18025 - c
18026 + C
18027 "#
18028 .unindent();
18029
18030 cx.assert_state_with_diff(second_hunk_expanded);
18031
18032 cx.update_editor(|editor, _, cx| {
18033 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18034 });
18035 executor.run_until_parked();
18036
18037 cx.assert_state_with_diff(both_hunks_expanded.clone());
18038
18039 cx.update_editor(|editor, _, cx| {
18040 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18041 });
18042 executor.run_until_parked();
18043
18044 let first_hunk_expanded = r#"
18045 - a
18046 + ˇA
18047 b
18048 C
18049 "#
18050 .unindent();
18051
18052 cx.assert_state_with_diff(first_hunk_expanded);
18053
18054 cx.update_editor(|editor, _, cx| {
18055 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18056 });
18057 executor.run_until_parked();
18058
18059 cx.assert_state_with_diff(both_hunks_expanded);
18060
18061 cx.set_state(
18062 &r#"
18063 ˇA
18064 b
18065 "#
18066 .unindent(),
18067 );
18068 cx.run_until_parked();
18069
18070 // TODO this cursor position seems bad
18071 cx.assert_state_with_diff(
18072 r#"
18073 - ˇa
18074 + A
18075 b
18076 "#
18077 .unindent(),
18078 );
18079
18080 cx.update_editor(|editor, window, cx| {
18081 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18082 });
18083
18084 cx.assert_state_with_diff(
18085 r#"
18086 - ˇa
18087 + A
18088 b
18089 - c
18090 "#
18091 .unindent(),
18092 );
18093
18094 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18095 let snapshot = editor.snapshot(window, cx);
18096 let hunks = editor
18097 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18098 .collect::<Vec<_>>();
18099 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18100 let buffer_id = hunks[0].buffer_id;
18101 hunks
18102 .into_iter()
18103 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18104 .collect::<Vec<_>>()
18105 });
18106 assert_eq!(hunk_ranges.len(), 2);
18107
18108 cx.update_editor(|editor, _, cx| {
18109 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18110 });
18111 executor.run_until_parked();
18112
18113 cx.assert_state_with_diff(
18114 r#"
18115 - ˇa
18116 + A
18117 b
18118 "#
18119 .unindent(),
18120 );
18121}
18122
18123#[gpui::test]
18124async fn test_toggle_deletion_hunk_at_start_of_file(
18125 executor: BackgroundExecutor,
18126 cx: &mut TestAppContext,
18127) {
18128 init_test(cx, |_| {});
18129 let mut cx = EditorTestContext::new(cx).await;
18130
18131 let diff_base = r#"
18132 a
18133 b
18134 c
18135 "#
18136 .unindent();
18137
18138 cx.set_state(
18139 &r#"
18140 ˇb
18141 c
18142 "#
18143 .unindent(),
18144 );
18145 cx.set_head_text(&diff_base);
18146 cx.update_editor(|editor, window, cx| {
18147 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18148 });
18149 executor.run_until_parked();
18150
18151 let hunk_expanded = r#"
18152 - a
18153 ˇb
18154 c
18155 "#
18156 .unindent();
18157
18158 cx.assert_state_with_diff(hunk_expanded.clone());
18159
18160 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18161 let snapshot = editor.snapshot(window, cx);
18162 let hunks = editor
18163 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18164 .collect::<Vec<_>>();
18165 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18166 let buffer_id = hunks[0].buffer_id;
18167 hunks
18168 .into_iter()
18169 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18170 .collect::<Vec<_>>()
18171 });
18172 assert_eq!(hunk_ranges.len(), 1);
18173
18174 cx.update_editor(|editor, _, cx| {
18175 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18176 });
18177 executor.run_until_parked();
18178
18179 let hunk_collapsed = r#"
18180 ˇb
18181 c
18182 "#
18183 .unindent();
18184
18185 cx.assert_state_with_diff(hunk_collapsed);
18186
18187 cx.update_editor(|editor, _, cx| {
18188 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18189 });
18190 executor.run_until_parked();
18191
18192 cx.assert_state_with_diff(hunk_expanded.clone());
18193}
18194
18195#[gpui::test]
18196async fn test_display_diff_hunks(cx: &mut TestAppContext) {
18197 init_test(cx, |_| {});
18198
18199 let fs = FakeFs::new(cx.executor());
18200 fs.insert_tree(
18201 path!("/test"),
18202 json!({
18203 ".git": {},
18204 "file-1": "ONE\n",
18205 "file-2": "TWO\n",
18206 "file-3": "THREE\n",
18207 }),
18208 )
18209 .await;
18210
18211 fs.set_head_for_repo(
18212 path!("/test/.git").as_ref(),
18213 &[
18214 ("file-1".into(), "one\n".into()),
18215 ("file-2".into(), "two\n".into()),
18216 ("file-3".into(), "three\n".into()),
18217 ],
18218 "deadbeef",
18219 );
18220
18221 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
18222 let mut buffers = vec![];
18223 for i in 1..=3 {
18224 let buffer = project
18225 .update(cx, |project, cx| {
18226 let path = format!(path!("/test/file-{}"), i);
18227 project.open_local_buffer(path, cx)
18228 })
18229 .await
18230 .unwrap();
18231 buffers.push(buffer);
18232 }
18233
18234 let multibuffer = cx.new(|cx| {
18235 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
18236 multibuffer.set_all_diff_hunks_expanded(cx);
18237 for buffer in &buffers {
18238 let snapshot = buffer.read(cx).snapshot();
18239 multibuffer.set_excerpts_for_path(
18240 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
18241 buffer.clone(),
18242 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
18243 DEFAULT_MULTIBUFFER_CONTEXT,
18244 cx,
18245 );
18246 }
18247 multibuffer
18248 });
18249
18250 let editor = cx.add_window(|window, cx| {
18251 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
18252 });
18253 cx.run_until_parked();
18254
18255 let snapshot = editor
18256 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18257 .unwrap();
18258 let hunks = snapshot
18259 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
18260 .map(|hunk| match hunk {
18261 DisplayDiffHunk::Unfolded {
18262 display_row_range, ..
18263 } => display_row_range,
18264 DisplayDiffHunk::Folded { .. } => unreachable!(),
18265 })
18266 .collect::<Vec<_>>();
18267 assert_eq!(
18268 hunks,
18269 [
18270 DisplayRow(2)..DisplayRow(4),
18271 DisplayRow(7)..DisplayRow(9),
18272 DisplayRow(12)..DisplayRow(14),
18273 ]
18274 );
18275}
18276
18277#[gpui::test]
18278async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
18279 init_test(cx, |_| {});
18280
18281 let mut cx = EditorTestContext::new(cx).await;
18282 cx.set_head_text(indoc! { "
18283 one
18284 two
18285 three
18286 four
18287 five
18288 "
18289 });
18290 cx.set_index_text(indoc! { "
18291 one
18292 two
18293 three
18294 four
18295 five
18296 "
18297 });
18298 cx.set_state(indoc! {"
18299 one
18300 TWO
18301 ˇTHREE
18302 FOUR
18303 five
18304 "});
18305 cx.run_until_parked();
18306 cx.update_editor(|editor, window, cx| {
18307 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18308 });
18309 cx.run_until_parked();
18310 cx.assert_index_text(Some(indoc! {"
18311 one
18312 TWO
18313 THREE
18314 FOUR
18315 five
18316 "}));
18317 cx.set_state(indoc! { "
18318 one
18319 TWO
18320 ˇTHREE-HUNDRED
18321 FOUR
18322 five
18323 "});
18324 cx.run_until_parked();
18325 cx.update_editor(|editor, window, cx| {
18326 let snapshot = editor.snapshot(window, cx);
18327 let hunks = editor
18328 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18329 .collect::<Vec<_>>();
18330 assert_eq!(hunks.len(), 1);
18331 assert_eq!(
18332 hunks[0].status(),
18333 DiffHunkStatus {
18334 kind: DiffHunkStatusKind::Modified,
18335 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
18336 }
18337 );
18338
18339 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18340 });
18341 cx.run_until_parked();
18342 cx.assert_index_text(Some(indoc! {"
18343 one
18344 TWO
18345 THREE-HUNDRED
18346 FOUR
18347 five
18348 "}));
18349}
18350
18351#[gpui::test]
18352fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
18353 init_test(cx, |_| {});
18354
18355 let editor = cx.add_window(|window, cx| {
18356 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
18357 build_editor(buffer, window, cx)
18358 });
18359
18360 let render_args = Arc::new(Mutex::new(None));
18361 let snapshot = editor
18362 .update(cx, |editor, window, cx| {
18363 let snapshot = editor.buffer().read(cx).snapshot(cx);
18364 let range =
18365 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
18366
18367 struct RenderArgs {
18368 row: MultiBufferRow,
18369 folded: bool,
18370 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
18371 }
18372
18373 let crease = Crease::inline(
18374 range,
18375 FoldPlaceholder::test(),
18376 {
18377 let toggle_callback = render_args.clone();
18378 move |row, folded, callback, _window, _cx| {
18379 *toggle_callback.lock() = Some(RenderArgs {
18380 row,
18381 folded,
18382 callback,
18383 });
18384 div()
18385 }
18386 },
18387 |_row, _folded, _window, _cx| div(),
18388 );
18389
18390 editor.insert_creases(Some(crease), cx);
18391 let snapshot = editor.snapshot(window, cx);
18392 let _div = snapshot.render_crease_toggle(
18393 MultiBufferRow(1),
18394 false,
18395 cx.entity().clone(),
18396 window,
18397 cx,
18398 );
18399 snapshot
18400 })
18401 .unwrap();
18402
18403 let render_args = render_args.lock().take().unwrap();
18404 assert_eq!(render_args.row, MultiBufferRow(1));
18405 assert!(!render_args.folded);
18406 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
18407
18408 cx.update_window(*editor, |_, window, cx| {
18409 (render_args.callback)(true, window, cx)
18410 })
18411 .unwrap();
18412 let snapshot = editor
18413 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18414 .unwrap();
18415 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
18416
18417 cx.update_window(*editor, |_, window, cx| {
18418 (render_args.callback)(false, window, cx)
18419 })
18420 .unwrap();
18421 let snapshot = editor
18422 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18423 .unwrap();
18424 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
18425}
18426
18427#[gpui::test]
18428async fn test_input_text(cx: &mut TestAppContext) {
18429 init_test(cx, |_| {});
18430 let mut cx = EditorTestContext::new(cx).await;
18431
18432 cx.set_state(
18433 &r#"ˇone
18434 two
18435
18436 three
18437 fourˇ
18438 five
18439
18440 siˇx"#
18441 .unindent(),
18442 );
18443
18444 cx.dispatch_action(HandleInput(String::new()));
18445 cx.assert_editor_state(
18446 &r#"ˇone
18447 two
18448
18449 three
18450 fourˇ
18451 five
18452
18453 siˇx"#
18454 .unindent(),
18455 );
18456
18457 cx.dispatch_action(HandleInput("AAAA".to_string()));
18458 cx.assert_editor_state(
18459 &r#"AAAAˇone
18460 two
18461
18462 three
18463 fourAAAAˇ
18464 five
18465
18466 siAAAAˇx"#
18467 .unindent(),
18468 );
18469}
18470
18471#[gpui::test]
18472async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
18473 init_test(cx, |_| {});
18474
18475 let mut cx = EditorTestContext::new(cx).await;
18476 cx.set_state(
18477 r#"let foo = 1;
18478let foo = 2;
18479let foo = 3;
18480let fooˇ = 4;
18481let foo = 5;
18482let foo = 6;
18483let foo = 7;
18484let foo = 8;
18485let foo = 9;
18486let foo = 10;
18487let foo = 11;
18488let foo = 12;
18489let foo = 13;
18490let foo = 14;
18491let foo = 15;"#,
18492 );
18493
18494 cx.update_editor(|e, window, cx| {
18495 assert_eq!(
18496 e.next_scroll_position,
18497 NextScrollCursorCenterTopBottom::Center,
18498 "Default next scroll direction is center",
18499 );
18500
18501 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18502 assert_eq!(
18503 e.next_scroll_position,
18504 NextScrollCursorCenterTopBottom::Top,
18505 "After center, next scroll direction should be top",
18506 );
18507
18508 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18509 assert_eq!(
18510 e.next_scroll_position,
18511 NextScrollCursorCenterTopBottom::Bottom,
18512 "After top, next scroll direction should be bottom",
18513 );
18514
18515 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18516 assert_eq!(
18517 e.next_scroll_position,
18518 NextScrollCursorCenterTopBottom::Center,
18519 "After bottom, scrolling should start over",
18520 );
18521
18522 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18523 assert_eq!(
18524 e.next_scroll_position,
18525 NextScrollCursorCenterTopBottom::Top,
18526 "Scrolling continues if retriggered fast enough"
18527 );
18528 });
18529
18530 cx.executor()
18531 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
18532 cx.executor().run_until_parked();
18533 cx.update_editor(|e, _, _| {
18534 assert_eq!(
18535 e.next_scroll_position,
18536 NextScrollCursorCenterTopBottom::Center,
18537 "If scrolling is not triggered fast enough, it should reset"
18538 );
18539 });
18540}
18541
18542#[gpui::test]
18543async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
18544 init_test(cx, |_| {});
18545 let mut cx = EditorLspTestContext::new_rust(
18546 lsp::ServerCapabilities {
18547 definition_provider: Some(lsp::OneOf::Left(true)),
18548 references_provider: Some(lsp::OneOf::Left(true)),
18549 ..lsp::ServerCapabilities::default()
18550 },
18551 cx,
18552 )
18553 .await;
18554
18555 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
18556 let go_to_definition = cx
18557 .lsp
18558 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
18559 move |params, _| async move {
18560 if empty_go_to_definition {
18561 Ok(None)
18562 } else {
18563 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
18564 uri: params.text_document_position_params.text_document.uri,
18565 range: lsp::Range::new(
18566 lsp::Position::new(4, 3),
18567 lsp::Position::new(4, 6),
18568 ),
18569 })))
18570 }
18571 },
18572 );
18573 let references = cx
18574 .lsp
18575 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
18576 Ok(Some(vec![lsp::Location {
18577 uri: params.text_document_position.text_document.uri,
18578 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
18579 }]))
18580 });
18581 (go_to_definition, references)
18582 };
18583
18584 cx.set_state(
18585 &r#"fn one() {
18586 let mut a = ˇtwo();
18587 }
18588
18589 fn two() {}"#
18590 .unindent(),
18591 );
18592 set_up_lsp_handlers(false, &mut cx);
18593 let navigated = cx
18594 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
18595 .await
18596 .expect("Failed to navigate to definition");
18597 assert_eq!(
18598 navigated,
18599 Navigated::Yes,
18600 "Should have navigated to definition from the GetDefinition response"
18601 );
18602 cx.assert_editor_state(
18603 &r#"fn one() {
18604 let mut a = two();
18605 }
18606
18607 fn «twoˇ»() {}"#
18608 .unindent(),
18609 );
18610
18611 let editors = cx.update_workspace(|workspace, _, cx| {
18612 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
18613 });
18614 cx.update_editor(|_, _, test_editor_cx| {
18615 assert_eq!(
18616 editors.len(),
18617 1,
18618 "Initially, only one, test, editor should be open in the workspace"
18619 );
18620 assert_eq!(
18621 test_editor_cx.entity(),
18622 editors.last().expect("Asserted len is 1").clone()
18623 );
18624 });
18625
18626 set_up_lsp_handlers(true, &mut cx);
18627 let navigated = cx
18628 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
18629 .await
18630 .expect("Failed to navigate to lookup references");
18631 assert_eq!(
18632 navigated,
18633 Navigated::Yes,
18634 "Should have navigated to references as a fallback after empty GoToDefinition response"
18635 );
18636 // We should not change the selections in the existing file,
18637 // if opening another milti buffer with the references
18638 cx.assert_editor_state(
18639 &r#"fn one() {
18640 let mut a = two();
18641 }
18642
18643 fn «twoˇ»() {}"#
18644 .unindent(),
18645 );
18646 let editors = cx.update_workspace(|workspace, _, cx| {
18647 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
18648 });
18649 cx.update_editor(|_, _, test_editor_cx| {
18650 assert_eq!(
18651 editors.len(),
18652 2,
18653 "After falling back to references search, we open a new editor with the results"
18654 );
18655 let references_fallback_text = editors
18656 .into_iter()
18657 .find(|new_editor| *new_editor != test_editor_cx.entity())
18658 .expect("Should have one non-test editor now")
18659 .read(test_editor_cx)
18660 .text(test_editor_cx);
18661 assert_eq!(
18662 references_fallback_text, "fn one() {\n let mut a = two();\n}",
18663 "Should use the range from the references response and not the GoToDefinition one"
18664 );
18665 });
18666}
18667
18668#[gpui::test]
18669async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
18670 init_test(cx, |_| {});
18671 cx.update(|cx| {
18672 let mut editor_settings = EditorSettings::get_global(cx).clone();
18673 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
18674 EditorSettings::override_global(editor_settings, cx);
18675 });
18676 let mut cx = EditorLspTestContext::new_rust(
18677 lsp::ServerCapabilities {
18678 definition_provider: Some(lsp::OneOf::Left(true)),
18679 references_provider: Some(lsp::OneOf::Left(true)),
18680 ..lsp::ServerCapabilities::default()
18681 },
18682 cx,
18683 )
18684 .await;
18685 let original_state = r#"fn one() {
18686 let mut a = ˇtwo();
18687 }
18688
18689 fn two() {}"#
18690 .unindent();
18691 cx.set_state(&original_state);
18692
18693 let mut go_to_definition = cx
18694 .lsp
18695 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
18696 move |_, _| async move { Ok(None) },
18697 );
18698 let _references = cx
18699 .lsp
18700 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
18701 panic!("Should not call for references with no go to definition fallback")
18702 });
18703
18704 let navigated = cx
18705 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
18706 .await
18707 .expect("Failed to navigate to lookup references");
18708 go_to_definition
18709 .next()
18710 .await
18711 .expect("Should have called the go_to_definition handler");
18712
18713 assert_eq!(
18714 navigated,
18715 Navigated::No,
18716 "Should have navigated to references as a fallback after empty GoToDefinition response"
18717 );
18718 cx.assert_editor_state(&original_state);
18719 let editors = cx.update_workspace(|workspace, _, cx| {
18720 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
18721 });
18722 cx.update_editor(|_, _, _| {
18723 assert_eq!(
18724 editors.len(),
18725 1,
18726 "After unsuccessful fallback, no other editor should have been opened"
18727 );
18728 });
18729}
18730
18731#[gpui::test]
18732async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
18733 init_test(cx, |_| {});
18734
18735 let language = Arc::new(Language::new(
18736 LanguageConfig::default(),
18737 Some(tree_sitter_rust::LANGUAGE.into()),
18738 ));
18739
18740 let text = r#"
18741 #[cfg(test)]
18742 mod tests() {
18743 #[test]
18744 fn runnable_1() {
18745 let a = 1;
18746 }
18747
18748 #[test]
18749 fn runnable_2() {
18750 let a = 1;
18751 let b = 2;
18752 }
18753 }
18754 "#
18755 .unindent();
18756
18757 let fs = FakeFs::new(cx.executor());
18758 fs.insert_file("/file.rs", Default::default()).await;
18759
18760 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18761 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18762 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18763 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
18764 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
18765
18766 let editor = cx.new_window_entity(|window, cx| {
18767 Editor::new(
18768 EditorMode::full(),
18769 multi_buffer,
18770 Some(project.clone()),
18771 window,
18772 cx,
18773 )
18774 });
18775
18776 editor.update_in(cx, |editor, window, cx| {
18777 let snapshot = editor.buffer().read(cx).snapshot(cx);
18778 editor.tasks.insert(
18779 (buffer.read(cx).remote_id(), 3),
18780 RunnableTasks {
18781 templates: vec![],
18782 offset: snapshot.anchor_before(43),
18783 column: 0,
18784 extra_variables: HashMap::default(),
18785 context_range: BufferOffset(43)..BufferOffset(85),
18786 },
18787 );
18788 editor.tasks.insert(
18789 (buffer.read(cx).remote_id(), 8),
18790 RunnableTasks {
18791 templates: vec![],
18792 offset: snapshot.anchor_before(86),
18793 column: 0,
18794 extra_variables: HashMap::default(),
18795 context_range: BufferOffset(86)..BufferOffset(191),
18796 },
18797 );
18798
18799 // Test finding task when cursor is inside function body
18800 editor.change_selections(None, window, cx, |s| {
18801 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
18802 });
18803 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
18804 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
18805
18806 // Test finding task when cursor is on function name
18807 editor.change_selections(None, window, cx, |s| {
18808 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
18809 });
18810 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
18811 assert_eq!(row, 8, "Should find task when cursor is on function name");
18812 });
18813}
18814
18815#[gpui::test]
18816async fn test_folding_buffers(cx: &mut TestAppContext) {
18817 init_test(cx, |_| {});
18818
18819 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
18820 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
18821 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
18822
18823 let fs = FakeFs::new(cx.executor());
18824 fs.insert_tree(
18825 path!("/a"),
18826 json!({
18827 "first.rs": sample_text_1,
18828 "second.rs": sample_text_2,
18829 "third.rs": sample_text_3,
18830 }),
18831 )
18832 .await;
18833 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18834 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18835 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18836 let worktree = project.update(cx, |project, cx| {
18837 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18838 assert_eq!(worktrees.len(), 1);
18839 worktrees.pop().unwrap()
18840 });
18841 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18842
18843 let buffer_1 = project
18844 .update(cx, |project, cx| {
18845 project.open_buffer((worktree_id, "first.rs"), cx)
18846 })
18847 .await
18848 .unwrap();
18849 let buffer_2 = project
18850 .update(cx, |project, cx| {
18851 project.open_buffer((worktree_id, "second.rs"), cx)
18852 })
18853 .await
18854 .unwrap();
18855 let buffer_3 = project
18856 .update(cx, |project, cx| {
18857 project.open_buffer((worktree_id, "third.rs"), cx)
18858 })
18859 .await
18860 .unwrap();
18861
18862 let multi_buffer = cx.new(|cx| {
18863 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18864 multi_buffer.push_excerpts(
18865 buffer_1.clone(),
18866 [
18867 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18868 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18869 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18870 ],
18871 cx,
18872 );
18873 multi_buffer.push_excerpts(
18874 buffer_2.clone(),
18875 [
18876 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18877 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18878 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18879 ],
18880 cx,
18881 );
18882 multi_buffer.push_excerpts(
18883 buffer_3.clone(),
18884 [
18885 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18886 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18887 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18888 ],
18889 cx,
18890 );
18891 multi_buffer
18892 });
18893 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18894 Editor::new(
18895 EditorMode::full(),
18896 multi_buffer.clone(),
18897 Some(project.clone()),
18898 window,
18899 cx,
18900 )
18901 });
18902
18903 assert_eq!(
18904 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18905 "\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",
18906 );
18907
18908 multi_buffer_editor.update(cx, |editor, cx| {
18909 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
18910 });
18911 assert_eq!(
18912 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18913 "\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",
18914 "After folding the first buffer, its text should not be displayed"
18915 );
18916
18917 multi_buffer_editor.update(cx, |editor, cx| {
18918 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
18919 });
18920 assert_eq!(
18921 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18922 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
18923 "After folding the second buffer, its text should not be displayed"
18924 );
18925
18926 multi_buffer_editor.update(cx, |editor, cx| {
18927 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
18928 });
18929 assert_eq!(
18930 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18931 "\n\n\n\n\n",
18932 "After folding the third buffer, its text should not be displayed"
18933 );
18934
18935 // Emulate selection inside the fold logic, that should work
18936 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18937 editor
18938 .snapshot(window, cx)
18939 .next_line_boundary(Point::new(0, 4));
18940 });
18941
18942 multi_buffer_editor.update(cx, |editor, cx| {
18943 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
18944 });
18945 assert_eq!(
18946 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18947 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
18948 "After unfolding the second buffer, its text should be displayed"
18949 );
18950
18951 // Typing inside of buffer 1 causes that buffer to be unfolded.
18952 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18953 assert_eq!(
18954 multi_buffer
18955 .read(cx)
18956 .snapshot(cx)
18957 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
18958 .collect::<String>(),
18959 "bbbb"
18960 );
18961 editor.change_selections(None, window, cx, |selections| {
18962 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
18963 });
18964 editor.handle_input("B", window, cx);
18965 });
18966
18967 assert_eq!(
18968 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18969 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
18970 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
18971 );
18972
18973 multi_buffer_editor.update(cx, |editor, cx| {
18974 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
18975 });
18976 assert_eq!(
18977 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18978 "\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",
18979 "After unfolding the all buffers, all original text should be displayed"
18980 );
18981}
18982
18983#[gpui::test]
18984async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
18985 init_test(cx, |_| {});
18986
18987 let sample_text_1 = "1111\n2222\n3333".to_string();
18988 let sample_text_2 = "4444\n5555\n6666".to_string();
18989 let sample_text_3 = "7777\n8888\n9999".to_string();
18990
18991 let fs = FakeFs::new(cx.executor());
18992 fs.insert_tree(
18993 path!("/a"),
18994 json!({
18995 "first.rs": sample_text_1,
18996 "second.rs": sample_text_2,
18997 "third.rs": sample_text_3,
18998 }),
18999 )
19000 .await;
19001 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19002 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19003 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19004 let worktree = project.update(cx, |project, cx| {
19005 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19006 assert_eq!(worktrees.len(), 1);
19007 worktrees.pop().unwrap()
19008 });
19009 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19010
19011 let buffer_1 = project
19012 .update(cx, |project, cx| {
19013 project.open_buffer((worktree_id, "first.rs"), cx)
19014 })
19015 .await
19016 .unwrap();
19017 let buffer_2 = project
19018 .update(cx, |project, cx| {
19019 project.open_buffer((worktree_id, "second.rs"), cx)
19020 })
19021 .await
19022 .unwrap();
19023 let buffer_3 = project
19024 .update(cx, |project, cx| {
19025 project.open_buffer((worktree_id, "third.rs"), cx)
19026 })
19027 .await
19028 .unwrap();
19029
19030 let multi_buffer = cx.new(|cx| {
19031 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19032 multi_buffer.push_excerpts(
19033 buffer_1.clone(),
19034 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19035 cx,
19036 );
19037 multi_buffer.push_excerpts(
19038 buffer_2.clone(),
19039 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19040 cx,
19041 );
19042 multi_buffer.push_excerpts(
19043 buffer_3.clone(),
19044 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19045 cx,
19046 );
19047 multi_buffer
19048 });
19049
19050 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19051 Editor::new(
19052 EditorMode::full(),
19053 multi_buffer,
19054 Some(project.clone()),
19055 window,
19056 cx,
19057 )
19058 });
19059
19060 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
19061 assert_eq!(
19062 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19063 full_text,
19064 );
19065
19066 multi_buffer_editor.update(cx, |editor, cx| {
19067 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
19068 });
19069 assert_eq!(
19070 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19071 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
19072 "After folding the first buffer, its text should not be displayed"
19073 );
19074
19075 multi_buffer_editor.update(cx, |editor, cx| {
19076 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
19077 });
19078
19079 assert_eq!(
19080 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19081 "\n\n\n\n\n\n7777\n8888\n9999",
19082 "After folding the second buffer, its text should not be displayed"
19083 );
19084
19085 multi_buffer_editor.update(cx, |editor, cx| {
19086 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
19087 });
19088 assert_eq!(
19089 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19090 "\n\n\n\n\n",
19091 "After folding the third buffer, its text should not be displayed"
19092 );
19093
19094 multi_buffer_editor.update(cx, |editor, cx| {
19095 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
19096 });
19097 assert_eq!(
19098 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19099 "\n\n\n\n4444\n5555\n6666\n\n",
19100 "After unfolding the second buffer, its text should be displayed"
19101 );
19102
19103 multi_buffer_editor.update(cx, |editor, cx| {
19104 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
19105 });
19106 assert_eq!(
19107 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19108 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
19109 "After unfolding the first buffer, its text should be displayed"
19110 );
19111
19112 multi_buffer_editor.update(cx, |editor, cx| {
19113 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
19114 });
19115 assert_eq!(
19116 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19117 full_text,
19118 "After unfolding all buffers, all original text should be displayed"
19119 );
19120}
19121
19122#[gpui::test]
19123async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
19124 init_test(cx, |_| {});
19125
19126 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
19127
19128 let fs = FakeFs::new(cx.executor());
19129 fs.insert_tree(
19130 path!("/a"),
19131 json!({
19132 "main.rs": sample_text,
19133 }),
19134 )
19135 .await;
19136 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19137 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19138 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19139 let worktree = project.update(cx, |project, cx| {
19140 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19141 assert_eq!(worktrees.len(), 1);
19142 worktrees.pop().unwrap()
19143 });
19144 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19145
19146 let buffer_1 = project
19147 .update(cx, |project, cx| {
19148 project.open_buffer((worktree_id, "main.rs"), cx)
19149 })
19150 .await
19151 .unwrap();
19152
19153 let multi_buffer = cx.new(|cx| {
19154 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19155 multi_buffer.push_excerpts(
19156 buffer_1.clone(),
19157 [ExcerptRange::new(
19158 Point::new(0, 0)
19159 ..Point::new(
19160 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
19161 0,
19162 ),
19163 )],
19164 cx,
19165 );
19166 multi_buffer
19167 });
19168 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19169 Editor::new(
19170 EditorMode::full(),
19171 multi_buffer,
19172 Some(project.clone()),
19173 window,
19174 cx,
19175 )
19176 });
19177
19178 let selection_range = Point::new(1, 0)..Point::new(2, 0);
19179 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19180 enum TestHighlight {}
19181 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
19182 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
19183 editor.highlight_text::<TestHighlight>(
19184 vec![highlight_range.clone()],
19185 HighlightStyle::color(Hsla::green()),
19186 cx,
19187 );
19188 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
19189 });
19190
19191 let full_text = format!("\n\n{sample_text}");
19192 assert_eq!(
19193 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19194 full_text,
19195 );
19196}
19197
19198#[gpui::test]
19199async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
19200 init_test(cx, |_| {});
19201 cx.update(|cx| {
19202 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
19203 "keymaps/default-linux.json",
19204 cx,
19205 )
19206 .unwrap();
19207 cx.bind_keys(default_key_bindings);
19208 });
19209
19210 let (editor, cx) = cx.add_window_view(|window, cx| {
19211 let multi_buffer = MultiBuffer::build_multi(
19212 [
19213 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
19214 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
19215 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
19216 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
19217 ],
19218 cx,
19219 );
19220 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
19221
19222 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
19223 // fold all but the second buffer, so that we test navigating between two
19224 // adjacent folded buffers, as well as folded buffers at the start and
19225 // end the multibuffer
19226 editor.fold_buffer(buffer_ids[0], cx);
19227 editor.fold_buffer(buffer_ids[2], cx);
19228 editor.fold_buffer(buffer_ids[3], cx);
19229
19230 editor
19231 });
19232 cx.simulate_resize(size(px(1000.), px(1000.)));
19233
19234 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
19235 cx.assert_excerpts_with_selections(indoc! {"
19236 [EXCERPT]
19237 ˇ[FOLDED]
19238 [EXCERPT]
19239 a1
19240 b1
19241 [EXCERPT]
19242 [FOLDED]
19243 [EXCERPT]
19244 [FOLDED]
19245 "
19246 });
19247 cx.simulate_keystroke("down");
19248 cx.assert_excerpts_with_selections(indoc! {"
19249 [EXCERPT]
19250 [FOLDED]
19251 [EXCERPT]
19252 ˇa1
19253 b1
19254 [EXCERPT]
19255 [FOLDED]
19256 [EXCERPT]
19257 [FOLDED]
19258 "
19259 });
19260 cx.simulate_keystroke("down");
19261 cx.assert_excerpts_with_selections(indoc! {"
19262 [EXCERPT]
19263 [FOLDED]
19264 [EXCERPT]
19265 a1
19266 ˇb1
19267 [EXCERPT]
19268 [FOLDED]
19269 [EXCERPT]
19270 [FOLDED]
19271 "
19272 });
19273 cx.simulate_keystroke("down");
19274 cx.assert_excerpts_with_selections(indoc! {"
19275 [EXCERPT]
19276 [FOLDED]
19277 [EXCERPT]
19278 a1
19279 b1
19280 ˇ[EXCERPT]
19281 [FOLDED]
19282 [EXCERPT]
19283 [FOLDED]
19284 "
19285 });
19286 cx.simulate_keystroke("down");
19287 cx.assert_excerpts_with_selections(indoc! {"
19288 [EXCERPT]
19289 [FOLDED]
19290 [EXCERPT]
19291 a1
19292 b1
19293 [EXCERPT]
19294 ˇ[FOLDED]
19295 [EXCERPT]
19296 [FOLDED]
19297 "
19298 });
19299 for _ in 0..5 {
19300 cx.simulate_keystroke("down");
19301 cx.assert_excerpts_with_selections(indoc! {"
19302 [EXCERPT]
19303 [FOLDED]
19304 [EXCERPT]
19305 a1
19306 b1
19307 [EXCERPT]
19308 [FOLDED]
19309 [EXCERPT]
19310 ˇ[FOLDED]
19311 "
19312 });
19313 }
19314
19315 cx.simulate_keystroke("up");
19316 cx.assert_excerpts_with_selections(indoc! {"
19317 [EXCERPT]
19318 [FOLDED]
19319 [EXCERPT]
19320 a1
19321 b1
19322 [EXCERPT]
19323 ˇ[FOLDED]
19324 [EXCERPT]
19325 [FOLDED]
19326 "
19327 });
19328 cx.simulate_keystroke("up");
19329 cx.assert_excerpts_with_selections(indoc! {"
19330 [EXCERPT]
19331 [FOLDED]
19332 [EXCERPT]
19333 a1
19334 b1
19335 ˇ[EXCERPT]
19336 [FOLDED]
19337 [EXCERPT]
19338 [FOLDED]
19339 "
19340 });
19341 cx.simulate_keystroke("up");
19342 cx.assert_excerpts_with_selections(indoc! {"
19343 [EXCERPT]
19344 [FOLDED]
19345 [EXCERPT]
19346 a1
19347 ˇb1
19348 [EXCERPT]
19349 [FOLDED]
19350 [EXCERPT]
19351 [FOLDED]
19352 "
19353 });
19354 cx.simulate_keystroke("up");
19355 cx.assert_excerpts_with_selections(indoc! {"
19356 [EXCERPT]
19357 [FOLDED]
19358 [EXCERPT]
19359 ˇa1
19360 b1
19361 [EXCERPT]
19362 [FOLDED]
19363 [EXCERPT]
19364 [FOLDED]
19365 "
19366 });
19367 for _ in 0..5 {
19368 cx.simulate_keystroke("up");
19369 cx.assert_excerpts_with_selections(indoc! {"
19370 [EXCERPT]
19371 ˇ[FOLDED]
19372 [EXCERPT]
19373 a1
19374 b1
19375 [EXCERPT]
19376 [FOLDED]
19377 [EXCERPT]
19378 [FOLDED]
19379 "
19380 });
19381 }
19382}
19383
19384#[gpui::test]
19385async fn test_inline_completion_text(cx: &mut TestAppContext) {
19386 init_test(cx, |_| {});
19387
19388 // Simple insertion
19389 assert_highlighted_edits(
19390 "Hello, world!",
19391 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
19392 true,
19393 cx,
19394 |highlighted_edits, cx| {
19395 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
19396 assert_eq!(highlighted_edits.highlights.len(), 1);
19397 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
19398 assert_eq!(
19399 highlighted_edits.highlights[0].1.background_color,
19400 Some(cx.theme().status().created_background)
19401 );
19402 },
19403 )
19404 .await;
19405
19406 // Replacement
19407 assert_highlighted_edits(
19408 "This is a test.",
19409 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
19410 false,
19411 cx,
19412 |highlighted_edits, cx| {
19413 assert_eq!(highlighted_edits.text, "That is a test.");
19414 assert_eq!(highlighted_edits.highlights.len(), 1);
19415 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
19416 assert_eq!(
19417 highlighted_edits.highlights[0].1.background_color,
19418 Some(cx.theme().status().created_background)
19419 );
19420 },
19421 )
19422 .await;
19423
19424 // Multiple edits
19425 assert_highlighted_edits(
19426 "Hello, world!",
19427 vec![
19428 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
19429 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
19430 ],
19431 false,
19432 cx,
19433 |highlighted_edits, cx| {
19434 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
19435 assert_eq!(highlighted_edits.highlights.len(), 2);
19436 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
19437 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
19438 assert_eq!(
19439 highlighted_edits.highlights[0].1.background_color,
19440 Some(cx.theme().status().created_background)
19441 );
19442 assert_eq!(
19443 highlighted_edits.highlights[1].1.background_color,
19444 Some(cx.theme().status().created_background)
19445 );
19446 },
19447 )
19448 .await;
19449
19450 // Multiple lines with edits
19451 assert_highlighted_edits(
19452 "First line\nSecond line\nThird line\nFourth line",
19453 vec![
19454 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
19455 (
19456 Point::new(2, 0)..Point::new(2, 10),
19457 "New third line".to_string(),
19458 ),
19459 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
19460 ],
19461 false,
19462 cx,
19463 |highlighted_edits, cx| {
19464 assert_eq!(
19465 highlighted_edits.text,
19466 "Second modified\nNew third line\nFourth updated line"
19467 );
19468 assert_eq!(highlighted_edits.highlights.len(), 3);
19469 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
19470 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
19471 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
19472 for highlight in &highlighted_edits.highlights {
19473 assert_eq!(
19474 highlight.1.background_color,
19475 Some(cx.theme().status().created_background)
19476 );
19477 }
19478 },
19479 )
19480 .await;
19481}
19482
19483#[gpui::test]
19484async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
19485 init_test(cx, |_| {});
19486
19487 // Deletion
19488 assert_highlighted_edits(
19489 "Hello, world!",
19490 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
19491 true,
19492 cx,
19493 |highlighted_edits, cx| {
19494 assert_eq!(highlighted_edits.text, "Hello, world!");
19495 assert_eq!(highlighted_edits.highlights.len(), 1);
19496 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
19497 assert_eq!(
19498 highlighted_edits.highlights[0].1.background_color,
19499 Some(cx.theme().status().deleted_background)
19500 );
19501 },
19502 )
19503 .await;
19504
19505 // Insertion
19506 assert_highlighted_edits(
19507 "Hello, world!",
19508 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
19509 true,
19510 cx,
19511 |highlighted_edits, cx| {
19512 assert_eq!(highlighted_edits.highlights.len(), 1);
19513 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
19514 assert_eq!(
19515 highlighted_edits.highlights[0].1.background_color,
19516 Some(cx.theme().status().created_background)
19517 );
19518 },
19519 )
19520 .await;
19521}
19522
19523async fn assert_highlighted_edits(
19524 text: &str,
19525 edits: Vec<(Range<Point>, String)>,
19526 include_deletions: bool,
19527 cx: &mut TestAppContext,
19528 assertion_fn: impl Fn(HighlightedText, &App),
19529) {
19530 let window = cx.add_window(|window, cx| {
19531 let buffer = MultiBuffer::build_simple(text, cx);
19532 Editor::new(EditorMode::full(), buffer, None, window, cx)
19533 });
19534 let cx = &mut VisualTestContext::from_window(*window, cx);
19535
19536 let (buffer, snapshot) = window
19537 .update(cx, |editor, _window, cx| {
19538 (
19539 editor.buffer().clone(),
19540 editor.buffer().read(cx).snapshot(cx),
19541 )
19542 })
19543 .unwrap();
19544
19545 let edits = edits
19546 .into_iter()
19547 .map(|(range, edit)| {
19548 (
19549 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
19550 edit,
19551 )
19552 })
19553 .collect::<Vec<_>>();
19554
19555 let text_anchor_edits = edits
19556 .clone()
19557 .into_iter()
19558 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
19559 .collect::<Vec<_>>();
19560
19561 let edit_preview = window
19562 .update(cx, |_, _window, cx| {
19563 buffer
19564 .read(cx)
19565 .as_singleton()
19566 .unwrap()
19567 .read(cx)
19568 .preview_edits(text_anchor_edits.into(), cx)
19569 })
19570 .unwrap()
19571 .await;
19572
19573 cx.update(|_window, cx| {
19574 let highlighted_edits = inline_completion_edit_text(
19575 &snapshot.as_singleton().unwrap().2,
19576 &edits,
19577 &edit_preview,
19578 include_deletions,
19579 cx,
19580 );
19581 assertion_fn(highlighted_edits, cx)
19582 });
19583}
19584
19585#[track_caller]
19586fn assert_breakpoint(
19587 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
19588 path: &Arc<Path>,
19589 expected: Vec<(u32, Breakpoint)>,
19590) {
19591 if expected.len() == 0usize {
19592 assert!(!breakpoints.contains_key(path), "{}", path.display());
19593 } else {
19594 let mut breakpoint = breakpoints
19595 .get(path)
19596 .unwrap()
19597 .into_iter()
19598 .map(|breakpoint| {
19599 (
19600 breakpoint.row,
19601 Breakpoint {
19602 message: breakpoint.message.clone(),
19603 state: breakpoint.state,
19604 condition: breakpoint.condition.clone(),
19605 hit_condition: breakpoint.hit_condition.clone(),
19606 },
19607 )
19608 })
19609 .collect::<Vec<_>>();
19610
19611 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
19612
19613 assert_eq!(expected, breakpoint);
19614 }
19615}
19616
19617fn add_log_breakpoint_at_cursor(
19618 editor: &mut Editor,
19619 log_message: &str,
19620 window: &mut Window,
19621 cx: &mut Context<Editor>,
19622) {
19623 let (anchor, bp) = editor
19624 .breakpoints_at_cursors(window, cx)
19625 .first()
19626 .and_then(|(anchor, bp)| {
19627 if let Some(bp) = bp {
19628 Some((*anchor, bp.clone()))
19629 } else {
19630 None
19631 }
19632 })
19633 .unwrap_or_else(|| {
19634 let cursor_position: Point = editor.selections.newest(cx).head();
19635
19636 let breakpoint_position = editor
19637 .snapshot(window, cx)
19638 .display_snapshot
19639 .buffer_snapshot
19640 .anchor_before(Point::new(cursor_position.row, 0));
19641
19642 (breakpoint_position, Breakpoint::new_log(&log_message))
19643 });
19644
19645 editor.edit_breakpoint_at_anchor(
19646 anchor,
19647 bp,
19648 BreakpointEditAction::EditLogMessage(log_message.into()),
19649 cx,
19650 );
19651}
19652
19653#[gpui::test]
19654async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
19655 init_test(cx, |_| {});
19656
19657 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19658 let fs = FakeFs::new(cx.executor());
19659 fs.insert_tree(
19660 path!("/a"),
19661 json!({
19662 "main.rs": sample_text,
19663 }),
19664 )
19665 .await;
19666 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19667 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19668 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19669
19670 let fs = FakeFs::new(cx.executor());
19671 fs.insert_tree(
19672 path!("/a"),
19673 json!({
19674 "main.rs": sample_text,
19675 }),
19676 )
19677 .await;
19678 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19679 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19680 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19681 let worktree_id = workspace
19682 .update(cx, |workspace, _window, cx| {
19683 workspace.project().update(cx, |project, cx| {
19684 project.worktrees(cx).next().unwrap().read(cx).id()
19685 })
19686 })
19687 .unwrap();
19688
19689 let buffer = project
19690 .update(cx, |project, cx| {
19691 project.open_buffer((worktree_id, "main.rs"), cx)
19692 })
19693 .await
19694 .unwrap();
19695
19696 let (editor, cx) = cx.add_window_view(|window, cx| {
19697 Editor::new(
19698 EditorMode::full(),
19699 MultiBuffer::build_from_buffer(buffer, cx),
19700 Some(project.clone()),
19701 window,
19702 cx,
19703 )
19704 });
19705
19706 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19707 let abs_path = project.read_with(cx, |project, cx| {
19708 project
19709 .absolute_path(&project_path, cx)
19710 .map(|path_buf| Arc::from(path_buf.to_owned()))
19711 .unwrap()
19712 });
19713
19714 // assert we can add breakpoint on the first line
19715 editor.update_in(cx, |editor, window, cx| {
19716 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19717 editor.move_to_end(&MoveToEnd, window, cx);
19718 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19719 });
19720
19721 let breakpoints = editor.update(cx, |editor, cx| {
19722 editor
19723 .breakpoint_store()
19724 .as_ref()
19725 .unwrap()
19726 .read(cx)
19727 .all_source_breakpoints(cx)
19728 .clone()
19729 });
19730
19731 assert_eq!(1, breakpoints.len());
19732 assert_breakpoint(
19733 &breakpoints,
19734 &abs_path,
19735 vec![
19736 (0, Breakpoint::new_standard()),
19737 (3, Breakpoint::new_standard()),
19738 ],
19739 );
19740
19741 editor.update_in(cx, |editor, window, cx| {
19742 editor.move_to_beginning(&MoveToBeginning, window, cx);
19743 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19744 });
19745
19746 let breakpoints = editor.update(cx, |editor, cx| {
19747 editor
19748 .breakpoint_store()
19749 .as_ref()
19750 .unwrap()
19751 .read(cx)
19752 .all_source_breakpoints(cx)
19753 .clone()
19754 });
19755
19756 assert_eq!(1, breakpoints.len());
19757 assert_breakpoint(
19758 &breakpoints,
19759 &abs_path,
19760 vec![(3, Breakpoint::new_standard())],
19761 );
19762
19763 editor.update_in(cx, |editor, window, cx| {
19764 editor.move_to_end(&MoveToEnd, window, cx);
19765 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19766 });
19767
19768 let breakpoints = editor.update(cx, |editor, cx| {
19769 editor
19770 .breakpoint_store()
19771 .as_ref()
19772 .unwrap()
19773 .read(cx)
19774 .all_source_breakpoints(cx)
19775 .clone()
19776 });
19777
19778 assert_eq!(0, breakpoints.len());
19779 assert_breakpoint(&breakpoints, &abs_path, vec![]);
19780}
19781
19782#[gpui::test]
19783async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
19784 init_test(cx, |_| {});
19785
19786 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19787
19788 let fs = FakeFs::new(cx.executor());
19789 fs.insert_tree(
19790 path!("/a"),
19791 json!({
19792 "main.rs": sample_text,
19793 }),
19794 )
19795 .await;
19796 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19797 let (workspace, cx) =
19798 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19799
19800 let worktree_id = workspace.update(cx, |workspace, cx| {
19801 workspace.project().update(cx, |project, cx| {
19802 project.worktrees(cx).next().unwrap().read(cx).id()
19803 })
19804 });
19805
19806 let buffer = project
19807 .update(cx, |project, cx| {
19808 project.open_buffer((worktree_id, "main.rs"), cx)
19809 })
19810 .await
19811 .unwrap();
19812
19813 let (editor, cx) = cx.add_window_view(|window, cx| {
19814 Editor::new(
19815 EditorMode::full(),
19816 MultiBuffer::build_from_buffer(buffer, cx),
19817 Some(project.clone()),
19818 window,
19819 cx,
19820 )
19821 });
19822
19823 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19824 let abs_path = project.read_with(cx, |project, cx| {
19825 project
19826 .absolute_path(&project_path, cx)
19827 .map(|path_buf| Arc::from(path_buf.to_owned()))
19828 .unwrap()
19829 });
19830
19831 editor.update_in(cx, |editor, window, cx| {
19832 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
19833 });
19834
19835 let breakpoints = editor.update(cx, |editor, cx| {
19836 editor
19837 .breakpoint_store()
19838 .as_ref()
19839 .unwrap()
19840 .read(cx)
19841 .all_source_breakpoints(cx)
19842 .clone()
19843 });
19844
19845 assert_breakpoint(
19846 &breakpoints,
19847 &abs_path,
19848 vec![(0, Breakpoint::new_log("hello world"))],
19849 );
19850
19851 // Removing a log message from a log breakpoint should remove it
19852 editor.update_in(cx, |editor, window, cx| {
19853 add_log_breakpoint_at_cursor(editor, "", window, cx);
19854 });
19855
19856 let breakpoints = editor.update(cx, |editor, cx| {
19857 editor
19858 .breakpoint_store()
19859 .as_ref()
19860 .unwrap()
19861 .read(cx)
19862 .all_source_breakpoints(cx)
19863 .clone()
19864 });
19865
19866 assert_breakpoint(&breakpoints, &abs_path, vec![]);
19867
19868 editor.update_in(cx, |editor, window, cx| {
19869 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19870 editor.move_to_end(&MoveToEnd, window, cx);
19871 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19872 // Not adding a log message to a standard breakpoint shouldn't remove it
19873 add_log_breakpoint_at_cursor(editor, "", window, cx);
19874 });
19875
19876 let breakpoints = editor.update(cx, |editor, cx| {
19877 editor
19878 .breakpoint_store()
19879 .as_ref()
19880 .unwrap()
19881 .read(cx)
19882 .all_source_breakpoints(cx)
19883 .clone()
19884 });
19885
19886 assert_breakpoint(
19887 &breakpoints,
19888 &abs_path,
19889 vec![
19890 (0, Breakpoint::new_standard()),
19891 (3, Breakpoint::new_standard()),
19892 ],
19893 );
19894
19895 editor.update_in(cx, |editor, window, cx| {
19896 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
19897 });
19898
19899 let breakpoints = editor.update(cx, |editor, cx| {
19900 editor
19901 .breakpoint_store()
19902 .as_ref()
19903 .unwrap()
19904 .read(cx)
19905 .all_source_breakpoints(cx)
19906 .clone()
19907 });
19908
19909 assert_breakpoint(
19910 &breakpoints,
19911 &abs_path,
19912 vec![
19913 (0, Breakpoint::new_standard()),
19914 (3, Breakpoint::new_log("hello world")),
19915 ],
19916 );
19917
19918 editor.update_in(cx, |editor, window, cx| {
19919 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
19920 });
19921
19922 let breakpoints = editor.update(cx, |editor, cx| {
19923 editor
19924 .breakpoint_store()
19925 .as_ref()
19926 .unwrap()
19927 .read(cx)
19928 .all_source_breakpoints(cx)
19929 .clone()
19930 });
19931
19932 assert_breakpoint(
19933 &breakpoints,
19934 &abs_path,
19935 vec![
19936 (0, Breakpoint::new_standard()),
19937 (3, Breakpoint::new_log("hello Earth!!")),
19938 ],
19939 );
19940}
19941
19942/// This also tests that Editor::breakpoint_at_cursor_head is working properly
19943/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
19944/// or when breakpoints were placed out of order. This tests for a regression too
19945#[gpui::test]
19946async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
19947 init_test(cx, |_| {});
19948
19949 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19950 let fs = FakeFs::new(cx.executor());
19951 fs.insert_tree(
19952 path!("/a"),
19953 json!({
19954 "main.rs": sample_text,
19955 }),
19956 )
19957 .await;
19958 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19959 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19960 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19961
19962 let fs = FakeFs::new(cx.executor());
19963 fs.insert_tree(
19964 path!("/a"),
19965 json!({
19966 "main.rs": sample_text,
19967 }),
19968 )
19969 .await;
19970 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19971 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19972 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19973 let worktree_id = workspace
19974 .update(cx, |workspace, _window, cx| {
19975 workspace.project().update(cx, |project, cx| {
19976 project.worktrees(cx).next().unwrap().read(cx).id()
19977 })
19978 })
19979 .unwrap();
19980
19981 let buffer = project
19982 .update(cx, |project, cx| {
19983 project.open_buffer((worktree_id, "main.rs"), cx)
19984 })
19985 .await
19986 .unwrap();
19987
19988 let (editor, cx) = cx.add_window_view(|window, cx| {
19989 Editor::new(
19990 EditorMode::full(),
19991 MultiBuffer::build_from_buffer(buffer, cx),
19992 Some(project.clone()),
19993 window,
19994 cx,
19995 )
19996 });
19997
19998 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19999 let abs_path = project.read_with(cx, |project, cx| {
20000 project
20001 .absolute_path(&project_path, cx)
20002 .map(|path_buf| Arc::from(path_buf.to_owned()))
20003 .unwrap()
20004 });
20005
20006 // assert we can add breakpoint on the first line
20007 editor.update_in(cx, |editor, window, cx| {
20008 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20009 editor.move_to_end(&MoveToEnd, window, cx);
20010 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20011 editor.move_up(&MoveUp, window, cx);
20012 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20013 });
20014
20015 let breakpoints = editor.update(cx, |editor, cx| {
20016 editor
20017 .breakpoint_store()
20018 .as_ref()
20019 .unwrap()
20020 .read(cx)
20021 .all_source_breakpoints(cx)
20022 .clone()
20023 });
20024
20025 assert_eq!(1, breakpoints.len());
20026 assert_breakpoint(
20027 &breakpoints,
20028 &abs_path,
20029 vec![
20030 (0, Breakpoint::new_standard()),
20031 (2, Breakpoint::new_standard()),
20032 (3, Breakpoint::new_standard()),
20033 ],
20034 );
20035
20036 editor.update_in(cx, |editor, window, cx| {
20037 editor.move_to_beginning(&MoveToBeginning, window, cx);
20038 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20039 editor.move_to_end(&MoveToEnd, window, cx);
20040 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20041 // Disabling a breakpoint that doesn't exist should do nothing
20042 editor.move_up(&MoveUp, window, cx);
20043 editor.move_up(&MoveUp, window, cx);
20044 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20045 });
20046
20047 let breakpoints = editor.update(cx, |editor, cx| {
20048 editor
20049 .breakpoint_store()
20050 .as_ref()
20051 .unwrap()
20052 .read(cx)
20053 .all_source_breakpoints(cx)
20054 .clone()
20055 });
20056
20057 let disable_breakpoint = {
20058 let mut bp = Breakpoint::new_standard();
20059 bp.state = BreakpointState::Disabled;
20060 bp
20061 };
20062
20063 assert_eq!(1, breakpoints.len());
20064 assert_breakpoint(
20065 &breakpoints,
20066 &abs_path,
20067 vec![
20068 (0, disable_breakpoint.clone()),
20069 (2, Breakpoint::new_standard()),
20070 (3, disable_breakpoint.clone()),
20071 ],
20072 );
20073
20074 editor.update_in(cx, |editor, window, cx| {
20075 editor.move_to_beginning(&MoveToBeginning, window, cx);
20076 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
20077 editor.move_to_end(&MoveToEnd, window, cx);
20078 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
20079 editor.move_up(&MoveUp, window, cx);
20080 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20081 });
20082
20083 let breakpoints = editor.update(cx, |editor, cx| {
20084 editor
20085 .breakpoint_store()
20086 .as_ref()
20087 .unwrap()
20088 .read(cx)
20089 .all_source_breakpoints(cx)
20090 .clone()
20091 });
20092
20093 assert_eq!(1, breakpoints.len());
20094 assert_breakpoint(
20095 &breakpoints,
20096 &abs_path,
20097 vec![
20098 (0, Breakpoint::new_standard()),
20099 (2, disable_breakpoint),
20100 (3, Breakpoint::new_standard()),
20101 ],
20102 );
20103}
20104
20105#[gpui::test]
20106async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
20107 init_test(cx, |_| {});
20108 let capabilities = lsp::ServerCapabilities {
20109 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
20110 prepare_provider: Some(true),
20111 work_done_progress_options: Default::default(),
20112 })),
20113 ..Default::default()
20114 };
20115 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
20116
20117 cx.set_state(indoc! {"
20118 struct Fˇoo {}
20119 "});
20120
20121 cx.update_editor(|editor, _, cx| {
20122 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
20123 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
20124 editor.highlight_background::<DocumentHighlightRead>(
20125 &[highlight_range],
20126 |c| c.editor_document_highlight_read_background,
20127 cx,
20128 );
20129 });
20130
20131 let mut prepare_rename_handler = cx
20132 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
20133 move |_, _, _| async move {
20134 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
20135 start: lsp::Position {
20136 line: 0,
20137 character: 7,
20138 },
20139 end: lsp::Position {
20140 line: 0,
20141 character: 10,
20142 },
20143 })))
20144 },
20145 );
20146 let prepare_rename_task = cx
20147 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
20148 .expect("Prepare rename was not started");
20149 prepare_rename_handler.next().await.unwrap();
20150 prepare_rename_task.await.expect("Prepare rename failed");
20151
20152 let mut rename_handler =
20153 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
20154 let edit = lsp::TextEdit {
20155 range: lsp::Range {
20156 start: lsp::Position {
20157 line: 0,
20158 character: 7,
20159 },
20160 end: lsp::Position {
20161 line: 0,
20162 character: 10,
20163 },
20164 },
20165 new_text: "FooRenamed".to_string(),
20166 };
20167 Ok(Some(lsp::WorkspaceEdit::new(
20168 // Specify the same edit twice
20169 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
20170 )))
20171 });
20172 let rename_task = cx
20173 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
20174 .expect("Confirm rename was not started");
20175 rename_handler.next().await.unwrap();
20176 rename_task.await.expect("Confirm rename failed");
20177 cx.run_until_parked();
20178
20179 // Despite two edits, only one is actually applied as those are identical
20180 cx.assert_editor_state(indoc! {"
20181 struct FooRenamedˇ {}
20182 "});
20183}
20184
20185#[gpui::test]
20186async fn test_rename_without_prepare(cx: &mut TestAppContext) {
20187 init_test(cx, |_| {});
20188 // These capabilities indicate that the server does not support prepare rename.
20189 let capabilities = lsp::ServerCapabilities {
20190 rename_provider: Some(lsp::OneOf::Left(true)),
20191 ..Default::default()
20192 };
20193 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
20194
20195 cx.set_state(indoc! {"
20196 struct Fˇoo {}
20197 "});
20198
20199 cx.update_editor(|editor, _window, cx| {
20200 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
20201 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
20202 editor.highlight_background::<DocumentHighlightRead>(
20203 &[highlight_range],
20204 |c| c.editor_document_highlight_read_background,
20205 cx,
20206 );
20207 });
20208
20209 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
20210 .expect("Prepare rename was not started")
20211 .await
20212 .expect("Prepare rename failed");
20213
20214 let mut rename_handler =
20215 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
20216 let edit = lsp::TextEdit {
20217 range: lsp::Range {
20218 start: lsp::Position {
20219 line: 0,
20220 character: 7,
20221 },
20222 end: lsp::Position {
20223 line: 0,
20224 character: 10,
20225 },
20226 },
20227 new_text: "FooRenamed".to_string(),
20228 };
20229 Ok(Some(lsp::WorkspaceEdit::new(
20230 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
20231 )))
20232 });
20233 let rename_task = cx
20234 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
20235 .expect("Confirm rename was not started");
20236 rename_handler.next().await.unwrap();
20237 rename_task.await.expect("Confirm rename failed");
20238 cx.run_until_parked();
20239
20240 // Correct range is renamed, as `surrounding_word` is used to find it.
20241 cx.assert_editor_state(indoc! {"
20242 struct FooRenamedˇ {}
20243 "});
20244}
20245
20246#[gpui::test]
20247async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
20248 init_test(cx, |_| {});
20249 let mut cx = EditorTestContext::new(cx).await;
20250
20251 let language = Arc::new(
20252 Language::new(
20253 LanguageConfig::default(),
20254 Some(tree_sitter_html::LANGUAGE.into()),
20255 )
20256 .with_brackets_query(
20257 r#"
20258 ("<" @open "/>" @close)
20259 ("</" @open ">" @close)
20260 ("<" @open ">" @close)
20261 ("\"" @open "\"" @close)
20262 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
20263 "#,
20264 )
20265 .unwrap(),
20266 );
20267 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20268
20269 cx.set_state(indoc! {"
20270 <span>ˇ</span>
20271 "});
20272 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
20273 cx.assert_editor_state(indoc! {"
20274 <span>
20275 ˇ
20276 </span>
20277 "});
20278
20279 cx.set_state(indoc! {"
20280 <span><span></span>ˇ</span>
20281 "});
20282 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
20283 cx.assert_editor_state(indoc! {"
20284 <span><span></span>
20285 ˇ</span>
20286 "});
20287
20288 cx.set_state(indoc! {"
20289 <span>ˇ
20290 </span>
20291 "});
20292 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
20293 cx.assert_editor_state(indoc! {"
20294 <span>
20295 ˇ
20296 </span>
20297 "});
20298}
20299
20300#[gpui::test(iterations = 10)]
20301async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
20302 init_test(cx, |_| {});
20303
20304 let fs = FakeFs::new(cx.executor());
20305 fs.insert_tree(
20306 path!("/dir"),
20307 json!({
20308 "a.ts": "a",
20309 }),
20310 )
20311 .await;
20312
20313 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
20314 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20315 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20316
20317 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20318 language_registry.add(Arc::new(Language::new(
20319 LanguageConfig {
20320 name: "TypeScript".into(),
20321 matcher: LanguageMatcher {
20322 path_suffixes: vec!["ts".to_string()],
20323 ..Default::default()
20324 },
20325 ..Default::default()
20326 },
20327 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
20328 )));
20329 let mut fake_language_servers = language_registry.register_fake_lsp(
20330 "TypeScript",
20331 FakeLspAdapter {
20332 capabilities: lsp::ServerCapabilities {
20333 code_lens_provider: Some(lsp::CodeLensOptions {
20334 resolve_provider: Some(true),
20335 }),
20336 execute_command_provider: Some(lsp::ExecuteCommandOptions {
20337 commands: vec!["_the/command".to_string()],
20338 ..lsp::ExecuteCommandOptions::default()
20339 }),
20340 ..lsp::ServerCapabilities::default()
20341 },
20342 ..FakeLspAdapter::default()
20343 },
20344 );
20345
20346 let (buffer, _handle) = project
20347 .update(cx, |p, cx| {
20348 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
20349 })
20350 .await
20351 .unwrap();
20352 cx.executor().run_until_parked();
20353
20354 let fake_server = fake_language_servers.next().await.unwrap();
20355
20356 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
20357 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
20358 drop(buffer_snapshot);
20359 let actions = cx
20360 .update_window(*workspace, |_, window, cx| {
20361 project.code_actions(&buffer, anchor..anchor, window, cx)
20362 })
20363 .unwrap();
20364
20365 fake_server
20366 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
20367 Ok(Some(vec![
20368 lsp::CodeLens {
20369 range: lsp::Range::default(),
20370 command: Some(lsp::Command {
20371 title: "Code lens command".to_owned(),
20372 command: "_the/command".to_owned(),
20373 arguments: None,
20374 }),
20375 data: None,
20376 },
20377 lsp::CodeLens {
20378 range: lsp::Range::default(),
20379 command: Some(lsp::Command {
20380 title: "Command not in capabilities".to_owned(),
20381 command: "not in capabilities".to_owned(),
20382 arguments: None,
20383 }),
20384 data: None,
20385 },
20386 lsp::CodeLens {
20387 range: lsp::Range {
20388 start: lsp::Position {
20389 line: 1,
20390 character: 1,
20391 },
20392 end: lsp::Position {
20393 line: 1,
20394 character: 1,
20395 },
20396 },
20397 command: Some(lsp::Command {
20398 title: "Command not in range".to_owned(),
20399 command: "_the/command".to_owned(),
20400 arguments: None,
20401 }),
20402 data: None,
20403 },
20404 ]))
20405 })
20406 .next()
20407 .await;
20408
20409 let actions = actions.await.unwrap();
20410 assert_eq!(
20411 actions.len(),
20412 1,
20413 "Should have only one valid action for the 0..0 range"
20414 );
20415 let action = actions[0].clone();
20416 let apply = project.update(cx, |project, cx| {
20417 project.apply_code_action(buffer.clone(), action, true, cx)
20418 });
20419
20420 // Resolving the code action does not populate its edits. In absence of
20421 // edits, we must execute the given command.
20422 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
20423 |mut lens, _| async move {
20424 let lens_command = lens.command.as_mut().expect("should have a command");
20425 assert_eq!(lens_command.title, "Code lens command");
20426 lens_command.arguments = Some(vec![json!("the-argument")]);
20427 Ok(lens)
20428 },
20429 );
20430
20431 // While executing the command, the language server sends the editor
20432 // a `workspaceEdit` request.
20433 fake_server
20434 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
20435 let fake = fake_server.clone();
20436 move |params, _| {
20437 assert_eq!(params.command, "_the/command");
20438 let fake = fake.clone();
20439 async move {
20440 fake.server
20441 .request::<lsp::request::ApplyWorkspaceEdit>(
20442 lsp::ApplyWorkspaceEditParams {
20443 label: None,
20444 edit: lsp::WorkspaceEdit {
20445 changes: Some(
20446 [(
20447 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
20448 vec![lsp::TextEdit {
20449 range: lsp::Range::new(
20450 lsp::Position::new(0, 0),
20451 lsp::Position::new(0, 0),
20452 ),
20453 new_text: "X".into(),
20454 }],
20455 )]
20456 .into_iter()
20457 .collect(),
20458 ),
20459 ..Default::default()
20460 },
20461 },
20462 )
20463 .await
20464 .into_response()
20465 .unwrap();
20466 Ok(Some(json!(null)))
20467 }
20468 }
20469 })
20470 .next()
20471 .await;
20472
20473 // Applying the code lens command returns a project transaction containing the edits
20474 // sent by the language server in its `workspaceEdit` request.
20475 let transaction = apply.await.unwrap();
20476 assert!(transaction.0.contains_key(&buffer));
20477 buffer.update(cx, |buffer, cx| {
20478 assert_eq!(buffer.text(), "Xa");
20479 buffer.undo(cx);
20480 assert_eq!(buffer.text(), "a");
20481 });
20482}
20483
20484#[gpui::test]
20485async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
20486 init_test(cx, |_| {});
20487
20488 let fs = FakeFs::new(cx.executor());
20489 let main_text = r#"fn main() {
20490println!("1");
20491println!("2");
20492println!("3");
20493println!("4");
20494println!("5");
20495}"#;
20496 let lib_text = "mod foo {}";
20497 fs.insert_tree(
20498 path!("/a"),
20499 json!({
20500 "lib.rs": lib_text,
20501 "main.rs": main_text,
20502 }),
20503 )
20504 .await;
20505
20506 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20507 let (workspace, cx) =
20508 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20509 let worktree_id = workspace.update(cx, |workspace, cx| {
20510 workspace.project().update(cx, |project, cx| {
20511 project.worktrees(cx).next().unwrap().read(cx).id()
20512 })
20513 });
20514
20515 let expected_ranges = vec![
20516 Point::new(0, 0)..Point::new(0, 0),
20517 Point::new(1, 0)..Point::new(1, 1),
20518 Point::new(2, 0)..Point::new(2, 2),
20519 Point::new(3, 0)..Point::new(3, 3),
20520 ];
20521
20522 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20523 let editor_1 = workspace
20524 .update_in(cx, |workspace, window, cx| {
20525 workspace.open_path(
20526 (worktree_id, "main.rs"),
20527 Some(pane_1.downgrade()),
20528 true,
20529 window,
20530 cx,
20531 )
20532 })
20533 .unwrap()
20534 .await
20535 .downcast::<Editor>()
20536 .unwrap();
20537 pane_1.update(cx, |pane, cx| {
20538 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20539 open_editor.update(cx, |editor, cx| {
20540 assert_eq!(
20541 editor.display_text(cx),
20542 main_text,
20543 "Original main.rs text on initial open",
20544 );
20545 assert_eq!(
20546 editor
20547 .selections
20548 .all::<Point>(cx)
20549 .into_iter()
20550 .map(|s| s.range())
20551 .collect::<Vec<_>>(),
20552 vec![Point::zero()..Point::zero()],
20553 "Default selections on initial open",
20554 );
20555 })
20556 });
20557 editor_1.update_in(cx, |editor, window, cx| {
20558 editor.change_selections(None, window, cx, |s| {
20559 s.select_ranges(expected_ranges.clone());
20560 });
20561 });
20562
20563 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
20564 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
20565 });
20566 let editor_2 = workspace
20567 .update_in(cx, |workspace, window, cx| {
20568 workspace.open_path(
20569 (worktree_id, "main.rs"),
20570 Some(pane_2.downgrade()),
20571 true,
20572 window,
20573 cx,
20574 )
20575 })
20576 .unwrap()
20577 .await
20578 .downcast::<Editor>()
20579 .unwrap();
20580 pane_2.update(cx, |pane, cx| {
20581 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20582 open_editor.update(cx, |editor, cx| {
20583 assert_eq!(
20584 editor.display_text(cx),
20585 main_text,
20586 "Original main.rs text on initial open in another panel",
20587 );
20588 assert_eq!(
20589 editor
20590 .selections
20591 .all::<Point>(cx)
20592 .into_iter()
20593 .map(|s| s.range())
20594 .collect::<Vec<_>>(),
20595 vec![Point::zero()..Point::zero()],
20596 "Default selections on initial open in another panel",
20597 );
20598 })
20599 });
20600
20601 editor_2.update_in(cx, |editor, window, cx| {
20602 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
20603 });
20604
20605 let _other_editor_1 = workspace
20606 .update_in(cx, |workspace, window, cx| {
20607 workspace.open_path(
20608 (worktree_id, "lib.rs"),
20609 Some(pane_1.downgrade()),
20610 true,
20611 window,
20612 cx,
20613 )
20614 })
20615 .unwrap()
20616 .await
20617 .downcast::<Editor>()
20618 .unwrap();
20619 pane_1
20620 .update_in(cx, |pane, window, cx| {
20621 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
20622 })
20623 .await
20624 .unwrap();
20625 drop(editor_1);
20626 pane_1.update(cx, |pane, cx| {
20627 pane.active_item()
20628 .unwrap()
20629 .downcast::<Editor>()
20630 .unwrap()
20631 .update(cx, |editor, cx| {
20632 assert_eq!(
20633 editor.display_text(cx),
20634 lib_text,
20635 "Other file should be open and active",
20636 );
20637 });
20638 assert_eq!(pane.items().count(), 1, "No other editors should be open");
20639 });
20640
20641 let _other_editor_2 = workspace
20642 .update_in(cx, |workspace, window, cx| {
20643 workspace.open_path(
20644 (worktree_id, "lib.rs"),
20645 Some(pane_2.downgrade()),
20646 true,
20647 window,
20648 cx,
20649 )
20650 })
20651 .unwrap()
20652 .await
20653 .downcast::<Editor>()
20654 .unwrap();
20655 pane_2
20656 .update_in(cx, |pane, window, cx| {
20657 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
20658 })
20659 .await
20660 .unwrap();
20661 drop(editor_2);
20662 pane_2.update(cx, |pane, cx| {
20663 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20664 open_editor.update(cx, |editor, cx| {
20665 assert_eq!(
20666 editor.display_text(cx),
20667 lib_text,
20668 "Other file should be open and active in another panel too",
20669 );
20670 });
20671 assert_eq!(
20672 pane.items().count(),
20673 1,
20674 "No other editors should be open in another pane",
20675 );
20676 });
20677
20678 let _editor_1_reopened = workspace
20679 .update_in(cx, |workspace, window, cx| {
20680 workspace.open_path(
20681 (worktree_id, "main.rs"),
20682 Some(pane_1.downgrade()),
20683 true,
20684 window,
20685 cx,
20686 )
20687 })
20688 .unwrap()
20689 .await
20690 .downcast::<Editor>()
20691 .unwrap();
20692 let _editor_2_reopened = workspace
20693 .update_in(cx, |workspace, window, cx| {
20694 workspace.open_path(
20695 (worktree_id, "main.rs"),
20696 Some(pane_2.downgrade()),
20697 true,
20698 window,
20699 cx,
20700 )
20701 })
20702 .unwrap()
20703 .await
20704 .downcast::<Editor>()
20705 .unwrap();
20706 pane_1.update(cx, |pane, cx| {
20707 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20708 open_editor.update(cx, |editor, cx| {
20709 assert_eq!(
20710 editor.display_text(cx),
20711 main_text,
20712 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
20713 );
20714 assert_eq!(
20715 editor
20716 .selections
20717 .all::<Point>(cx)
20718 .into_iter()
20719 .map(|s| s.range())
20720 .collect::<Vec<_>>(),
20721 expected_ranges,
20722 "Previous editor in the 1st panel had selections and should get them restored on reopen",
20723 );
20724 })
20725 });
20726 pane_2.update(cx, |pane, cx| {
20727 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20728 open_editor.update(cx, |editor, cx| {
20729 assert_eq!(
20730 editor.display_text(cx),
20731 r#"fn main() {
20732⋯rintln!("1");
20733⋯intln!("2");
20734⋯ntln!("3");
20735println!("4");
20736println!("5");
20737}"#,
20738 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
20739 );
20740 assert_eq!(
20741 editor
20742 .selections
20743 .all::<Point>(cx)
20744 .into_iter()
20745 .map(|s| s.range())
20746 .collect::<Vec<_>>(),
20747 vec![Point::zero()..Point::zero()],
20748 "Previous editor in the 2nd pane had no selections changed hence should restore none",
20749 );
20750 })
20751 });
20752}
20753
20754#[gpui::test]
20755async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
20756 init_test(cx, |_| {});
20757
20758 let fs = FakeFs::new(cx.executor());
20759 let main_text = r#"fn main() {
20760println!("1");
20761println!("2");
20762println!("3");
20763println!("4");
20764println!("5");
20765}"#;
20766 let lib_text = "mod foo {}";
20767 fs.insert_tree(
20768 path!("/a"),
20769 json!({
20770 "lib.rs": lib_text,
20771 "main.rs": main_text,
20772 }),
20773 )
20774 .await;
20775
20776 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20777 let (workspace, cx) =
20778 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20779 let worktree_id = workspace.update(cx, |workspace, cx| {
20780 workspace.project().update(cx, |project, cx| {
20781 project.worktrees(cx).next().unwrap().read(cx).id()
20782 })
20783 });
20784
20785 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20786 let editor = workspace
20787 .update_in(cx, |workspace, window, cx| {
20788 workspace.open_path(
20789 (worktree_id, "main.rs"),
20790 Some(pane.downgrade()),
20791 true,
20792 window,
20793 cx,
20794 )
20795 })
20796 .unwrap()
20797 .await
20798 .downcast::<Editor>()
20799 .unwrap();
20800 pane.update(cx, |pane, cx| {
20801 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20802 open_editor.update(cx, |editor, cx| {
20803 assert_eq!(
20804 editor.display_text(cx),
20805 main_text,
20806 "Original main.rs text on initial open",
20807 );
20808 })
20809 });
20810 editor.update_in(cx, |editor, window, cx| {
20811 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
20812 });
20813
20814 cx.update_global(|store: &mut SettingsStore, cx| {
20815 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
20816 s.restore_on_file_reopen = Some(false);
20817 });
20818 });
20819 editor.update_in(cx, |editor, window, cx| {
20820 editor.fold_ranges(
20821 vec![
20822 Point::new(1, 0)..Point::new(1, 1),
20823 Point::new(2, 0)..Point::new(2, 2),
20824 Point::new(3, 0)..Point::new(3, 3),
20825 ],
20826 false,
20827 window,
20828 cx,
20829 );
20830 });
20831 pane.update_in(cx, |pane, window, cx| {
20832 pane.close_all_items(&CloseAllItems::default(), window, cx)
20833 })
20834 .await
20835 .unwrap();
20836 pane.update(cx, |pane, _| {
20837 assert!(pane.active_item().is_none());
20838 });
20839 cx.update_global(|store: &mut SettingsStore, cx| {
20840 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
20841 s.restore_on_file_reopen = Some(true);
20842 });
20843 });
20844
20845 let _editor_reopened = workspace
20846 .update_in(cx, |workspace, window, cx| {
20847 workspace.open_path(
20848 (worktree_id, "main.rs"),
20849 Some(pane.downgrade()),
20850 true,
20851 window,
20852 cx,
20853 )
20854 })
20855 .unwrap()
20856 .await
20857 .downcast::<Editor>()
20858 .unwrap();
20859 pane.update(cx, |pane, cx| {
20860 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20861 open_editor.update(cx, |editor, cx| {
20862 assert_eq!(
20863 editor.display_text(cx),
20864 main_text,
20865 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
20866 );
20867 })
20868 });
20869}
20870
20871#[gpui::test]
20872async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
20873 struct EmptyModalView {
20874 focus_handle: gpui::FocusHandle,
20875 }
20876 impl EventEmitter<DismissEvent> for EmptyModalView {}
20877 impl Render for EmptyModalView {
20878 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
20879 div()
20880 }
20881 }
20882 impl Focusable for EmptyModalView {
20883 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
20884 self.focus_handle.clone()
20885 }
20886 }
20887 impl workspace::ModalView for EmptyModalView {}
20888 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
20889 EmptyModalView {
20890 focus_handle: cx.focus_handle(),
20891 }
20892 }
20893
20894 init_test(cx, |_| {});
20895
20896 let fs = FakeFs::new(cx.executor());
20897 let project = Project::test(fs, [], cx).await;
20898 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20899 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
20900 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20901 let editor = cx.new_window_entity(|window, cx| {
20902 Editor::new(
20903 EditorMode::full(),
20904 buffer,
20905 Some(project.clone()),
20906 window,
20907 cx,
20908 )
20909 });
20910 workspace
20911 .update(cx, |workspace, window, cx| {
20912 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
20913 })
20914 .unwrap();
20915 editor.update_in(cx, |editor, window, cx| {
20916 editor.open_context_menu(&OpenContextMenu, window, cx);
20917 assert!(editor.mouse_context_menu.is_some());
20918 });
20919 workspace
20920 .update(cx, |workspace, window, cx| {
20921 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
20922 })
20923 .unwrap();
20924 cx.read(|cx| {
20925 assert!(editor.read(cx).mouse_context_menu.is_none());
20926 });
20927}
20928
20929#[gpui::test]
20930async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
20931 init_test(cx, |_| {});
20932
20933 let fs = FakeFs::new(cx.executor());
20934 fs.insert_file(path!("/file.html"), Default::default())
20935 .await;
20936
20937 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
20938
20939 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20940 let html_language = Arc::new(Language::new(
20941 LanguageConfig {
20942 name: "HTML".into(),
20943 matcher: LanguageMatcher {
20944 path_suffixes: vec!["html".to_string()],
20945 ..LanguageMatcher::default()
20946 },
20947 brackets: BracketPairConfig {
20948 pairs: vec![BracketPair {
20949 start: "<".into(),
20950 end: ">".into(),
20951 close: true,
20952 ..Default::default()
20953 }],
20954 ..Default::default()
20955 },
20956 ..Default::default()
20957 },
20958 Some(tree_sitter_html::LANGUAGE.into()),
20959 ));
20960 language_registry.add(html_language);
20961 let mut fake_servers = language_registry.register_fake_lsp(
20962 "HTML",
20963 FakeLspAdapter {
20964 capabilities: lsp::ServerCapabilities {
20965 completion_provider: Some(lsp::CompletionOptions {
20966 resolve_provider: Some(true),
20967 ..Default::default()
20968 }),
20969 ..Default::default()
20970 },
20971 ..Default::default()
20972 },
20973 );
20974
20975 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20976 let cx = &mut VisualTestContext::from_window(*workspace, cx);
20977
20978 let worktree_id = workspace
20979 .update(cx, |workspace, _window, cx| {
20980 workspace.project().update(cx, |project, cx| {
20981 project.worktrees(cx).next().unwrap().read(cx).id()
20982 })
20983 })
20984 .unwrap();
20985 project
20986 .update(cx, |project, cx| {
20987 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
20988 })
20989 .await
20990 .unwrap();
20991 let editor = workspace
20992 .update(cx, |workspace, window, cx| {
20993 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
20994 })
20995 .unwrap()
20996 .await
20997 .unwrap()
20998 .downcast::<Editor>()
20999 .unwrap();
21000
21001 let fake_server = fake_servers.next().await.unwrap();
21002 editor.update_in(cx, |editor, window, cx| {
21003 editor.set_text("<ad></ad>", window, cx);
21004 editor.change_selections(None, window, cx, |selections| {
21005 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
21006 });
21007 let Some((buffer, _)) = editor
21008 .buffer
21009 .read(cx)
21010 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
21011 else {
21012 panic!("Failed to get buffer for selection position");
21013 };
21014 let buffer = buffer.read(cx);
21015 let buffer_id = buffer.remote_id();
21016 let opening_range =
21017 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
21018 let closing_range =
21019 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
21020 let mut linked_ranges = HashMap::default();
21021 linked_ranges.insert(
21022 buffer_id,
21023 vec![(opening_range.clone(), vec![closing_range.clone()])],
21024 );
21025 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
21026 });
21027 let mut completion_handle =
21028 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
21029 Ok(Some(lsp::CompletionResponse::Array(vec![
21030 lsp::CompletionItem {
21031 label: "head".to_string(),
21032 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
21033 lsp::InsertReplaceEdit {
21034 new_text: "head".to_string(),
21035 insert: lsp::Range::new(
21036 lsp::Position::new(0, 1),
21037 lsp::Position::new(0, 3),
21038 ),
21039 replace: lsp::Range::new(
21040 lsp::Position::new(0, 1),
21041 lsp::Position::new(0, 3),
21042 ),
21043 },
21044 )),
21045 ..Default::default()
21046 },
21047 ])))
21048 });
21049 editor.update_in(cx, |editor, window, cx| {
21050 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
21051 });
21052 cx.run_until_parked();
21053 completion_handle.next().await.unwrap();
21054 editor.update(cx, |editor, _| {
21055 assert!(
21056 editor.context_menu_visible(),
21057 "Completion menu should be visible"
21058 );
21059 });
21060 editor.update_in(cx, |editor, window, cx| {
21061 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
21062 });
21063 cx.executor().run_until_parked();
21064 editor.update(cx, |editor, cx| {
21065 assert_eq!(editor.text(cx), "<head></head>");
21066 });
21067}
21068
21069#[gpui::test]
21070async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
21071 init_test(cx, |_| {});
21072
21073 let fs = FakeFs::new(cx.executor());
21074 fs.insert_tree(
21075 path!("/root"),
21076 json!({
21077 "a": {
21078 "main.rs": "fn main() {}",
21079 },
21080 "foo": {
21081 "bar": {
21082 "external_file.rs": "pub mod external {}",
21083 }
21084 }
21085 }),
21086 )
21087 .await;
21088
21089 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
21090 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21091 language_registry.add(rust_lang());
21092 let _fake_servers = language_registry.register_fake_lsp(
21093 "Rust",
21094 FakeLspAdapter {
21095 ..FakeLspAdapter::default()
21096 },
21097 );
21098 let (workspace, cx) =
21099 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21100 let worktree_id = workspace.update(cx, |workspace, cx| {
21101 workspace.project().update(cx, |project, cx| {
21102 project.worktrees(cx).next().unwrap().read(cx).id()
21103 })
21104 });
21105
21106 let assert_language_servers_count =
21107 |expected: usize, context: &str, cx: &mut VisualTestContext| {
21108 project.update(cx, |project, cx| {
21109 let current = project
21110 .lsp_store()
21111 .read(cx)
21112 .as_local()
21113 .unwrap()
21114 .language_servers
21115 .len();
21116 assert_eq!(expected, current, "{context}");
21117 });
21118 };
21119
21120 assert_language_servers_count(
21121 0,
21122 "No servers should be running before any file is open",
21123 cx,
21124 );
21125 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21126 let main_editor = workspace
21127 .update_in(cx, |workspace, window, cx| {
21128 workspace.open_path(
21129 (worktree_id, "main.rs"),
21130 Some(pane.downgrade()),
21131 true,
21132 window,
21133 cx,
21134 )
21135 })
21136 .unwrap()
21137 .await
21138 .downcast::<Editor>()
21139 .unwrap();
21140 pane.update(cx, |pane, cx| {
21141 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21142 open_editor.update(cx, |editor, cx| {
21143 assert_eq!(
21144 editor.display_text(cx),
21145 "fn main() {}",
21146 "Original main.rs text on initial open",
21147 );
21148 });
21149 assert_eq!(open_editor, main_editor);
21150 });
21151 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
21152
21153 let external_editor = workspace
21154 .update_in(cx, |workspace, window, cx| {
21155 workspace.open_abs_path(
21156 PathBuf::from("/root/foo/bar/external_file.rs"),
21157 OpenOptions::default(),
21158 window,
21159 cx,
21160 )
21161 })
21162 .await
21163 .expect("opening external file")
21164 .downcast::<Editor>()
21165 .expect("downcasted external file's open element to editor");
21166 pane.update(cx, |pane, cx| {
21167 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21168 open_editor.update(cx, |editor, cx| {
21169 assert_eq!(
21170 editor.display_text(cx),
21171 "pub mod external {}",
21172 "External file is open now",
21173 );
21174 });
21175 assert_eq!(open_editor, external_editor);
21176 });
21177 assert_language_servers_count(
21178 1,
21179 "Second, external, *.rs file should join the existing server",
21180 cx,
21181 );
21182
21183 pane.update_in(cx, |pane, window, cx| {
21184 pane.close_active_item(&CloseActiveItem::default(), window, cx)
21185 })
21186 .await
21187 .unwrap();
21188 pane.update_in(cx, |pane, window, cx| {
21189 pane.navigate_backward(window, cx);
21190 });
21191 cx.run_until_parked();
21192 pane.update(cx, |pane, cx| {
21193 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21194 open_editor.update(cx, |editor, cx| {
21195 assert_eq!(
21196 editor.display_text(cx),
21197 "pub mod external {}",
21198 "External file is open now",
21199 );
21200 });
21201 });
21202 assert_language_servers_count(
21203 1,
21204 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
21205 cx,
21206 );
21207
21208 cx.update(|_, cx| {
21209 workspace::reload(&workspace::Reload::default(), cx);
21210 });
21211 assert_language_servers_count(
21212 1,
21213 "After reloading the worktree with local and external files opened, only one project should be started",
21214 cx,
21215 );
21216}
21217
21218#[gpui::test]
21219async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
21220 init_test(cx, |_| {});
21221
21222 let mut cx = EditorTestContext::new(cx).await;
21223 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
21224 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21225
21226 // test cursor move to start of each line on tab
21227 // for `if`, `elif`, `else`, `while`, `with` and `for`
21228 cx.set_state(indoc! {"
21229 def main():
21230 ˇ for item in items:
21231 ˇ while item.active:
21232 ˇ if item.value > 10:
21233 ˇ continue
21234 ˇ elif item.value < 0:
21235 ˇ break
21236 ˇ else:
21237 ˇ with item.context() as ctx:
21238 ˇ yield count
21239 ˇ else:
21240 ˇ log('while else')
21241 ˇ else:
21242 ˇ log('for else')
21243 "});
21244 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21245 cx.assert_editor_state(indoc! {"
21246 def main():
21247 ˇfor item in items:
21248 ˇwhile item.active:
21249 ˇif item.value > 10:
21250 ˇcontinue
21251 ˇelif item.value < 0:
21252 ˇbreak
21253 ˇelse:
21254 ˇwith item.context() as ctx:
21255 ˇyield count
21256 ˇelse:
21257 ˇlog('while else')
21258 ˇelse:
21259 ˇlog('for else')
21260 "});
21261 // test relative indent is preserved when tab
21262 // for `if`, `elif`, `else`, `while`, `with` and `for`
21263 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21264 cx.assert_editor_state(indoc! {"
21265 def main():
21266 ˇfor item in items:
21267 ˇwhile item.active:
21268 ˇif item.value > 10:
21269 ˇcontinue
21270 ˇelif item.value < 0:
21271 ˇbreak
21272 ˇelse:
21273 ˇwith item.context() as ctx:
21274 ˇyield count
21275 ˇelse:
21276 ˇlog('while else')
21277 ˇelse:
21278 ˇlog('for else')
21279 "});
21280
21281 // test cursor move to start of each line on tab
21282 // for `try`, `except`, `else`, `finally`, `match` and `def`
21283 cx.set_state(indoc! {"
21284 def main():
21285 ˇ try:
21286 ˇ fetch()
21287 ˇ except ValueError:
21288 ˇ handle_error()
21289 ˇ else:
21290 ˇ match value:
21291 ˇ case _:
21292 ˇ finally:
21293 ˇ def status():
21294 ˇ return 0
21295 "});
21296 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21297 cx.assert_editor_state(indoc! {"
21298 def main():
21299 ˇtry:
21300 ˇfetch()
21301 ˇexcept ValueError:
21302 ˇhandle_error()
21303 ˇelse:
21304 ˇmatch value:
21305 ˇcase _:
21306 ˇfinally:
21307 ˇdef status():
21308 ˇreturn 0
21309 "});
21310 // test relative indent is preserved when tab
21311 // for `try`, `except`, `else`, `finally`, `match` and `def`
21312 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21313 cx.assert_editor_state(indoc! {"
21314 def main():
21315 ˇtry:
21316 ˇfetch()
21317 ˇexcept ValueError:
21318 ˇhandle_error()
21319 ˇelse:
21320 ˇmatch value:
21321 ˇcase _:
21322 ˇfinally:
21323 ˇdef status():
21324 ˇreturn 0
21325 "});
21326}
21327
21328#[gpui::test]
21329async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
21330 init_test(cx, |_| {});
21331
21332 let mut cx = EditorTestContext::new(cx).await;
21333 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
21334 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21335
21336 // test `else` auto outdents when typed inside `if` block
21337 cx.set_state(indoc! {"
21338 def main():
21339 if i == 2:
21340 return
21341 ˇ
21342 "});
21343 cx.update_editor(|editor, window, cx| {
21344 editor.handle_input("else:", window, cx);
21345 });
21346 cx.assert_editor_state(indoc! {"
21347 def main():
21348 if i == 2:
21349 return
21350 else:ˇ
21351 "});
21352
21353 // test `except` auto outdents when typed inside `try` block
21354 cx.set_state(indoc! {"
21355 def main():
21356 try:
21357 i = 2
21358 ˇ
21359 "});
21360 cx.update_editor(|editor, window, cx| {
21361 editor.handle_input("except:", window, cx);
21362 });
21363 cx.assert_editor_state(indoc! {"
21364 def main():
21365 try:
21366 i = 2
21367 except:ˇ
21368 "});
21369
21370 // test `else` auto outdents when typed inside `except` block
21371 cx.set_state(indoc! {"
21372 def main():
21373 try:
21374 i = 2
21375 except:
21376 j = 2
21377 ˇ
21378 "});
21379 cx.update_editor(|editor, window, cx| {
21380 editor.handle_input("else:", window, cx);
21381 });
21382 cx.assert_editor_state(indoc! {"
21383 def main():
21384 try:
21385 i = 2
21386 except:
21387 j = 2
21388 else:ˇ
21389 "});
21390
21391 // test `finally` auto outdents when typed inside `else` block
21392 cx.set_state(indoc! {"
21393 def main():
21394 try:
21395 i = 2
21396 except:
21397 j = 2
21398 else:
21399 k = 2
21400 ˇ
21401 "});
21402 cx.update_editor(|editor, window, cx| {
21403 editor.handle_input("finally:", window, cx);
21404 });
21405 cx.assert_editor_state(indoc! {"
21406 def main():
21407 try:
21408 i = 2
21409 except:
21410 j = 2
21411 else:
21412 k = 2
21413 finally:ˇ
21414 "});
21415
21416 // TODO: test `except` auto outdents when typed inside `try` block right after for block
21417 // cx.set_state(indoc! {"
21418 // def main():
21419 // try:
21420 // for i in range(n):
21421 // pass
21422 // ˇ
21423 // "});
21424 // cx.update_editor(|editor, window, cx| {
21425 // editor.handle_input("except:", window, cx);
21426 // });
21427 // cx.assert_editor_state(indoc! {"
21428 // def main():
21429 // try:
21430 // for i in range(n):
21431 // pass
21432 // except:ˇ
21433 // "});
21434
21435 // TODO: test `else` auto outdents when typed inside `except` block right after for block
21436 // cx.set_state(indoc! {"
21437 // def main():
21438 // try:
21439 // i = 2
21440 // except:
21441 // for i in range(n):
21442 // pass
21443 // ˇ
21444 // "});
21445 // cx.update_editor(|editor, window, cx| {
21446 // editor.handle_input("else:", window, cx);
21447 // });
21448 // cx.assert_editor_state(indoc! {"
21449 // def main():
21450 // try:
21451 // i = 2
21452 // except:
21453 // for i in range(n):
21454 // pass
21455 // else:ˇ
21456 // "});
21457
21458 // TODO: test `finally` auto outdents when typed inside `else` block right after for block
21459 // cx.set_state(indoc! {"
21460 // def main():
21461 // try:
21462 // i = 2
21463 // except:
21464 // j = 2
21465 // else:
21466 // for i in range(n):
21467 // pass
21468 // ˇ
21469 // "});
21470 // cx.update_editor(|editor, window, cx| {
21471 // editor.handle_input("finally:", window, cx);
21472 // });
21473 // cx.assert_editor_state(indoc! {"
21474 // def main():
21475 // try:
21476 // i = 2
21477 // except:
21478 // j = 2
21479 // else:
21480 // for i in range(n):
21481 // pass
21482 // finally:ˇ
21483 // "});
21484
21485 // test `else` stays at correct indent when typed after `for` block
21486 cx.set_state(indoc! {"
21487 def main():
21488 for i in range(10):
21489 if i == 3:
21490 break
21491 ˇ
21492 "});
21493 cx.update_editor(|editor, window, cx| {
21494 editor.handle_input("else:", window, cx);
21495 });
21496 cx.assert_editor_state(indoc! {"
21497 def main():
21498 for i in range(10):
21499 if i == 3:
21500 break
21501 else:ˇ
21502 "});
21503
21504 // test does not outdent on typing after line with square brackets
21505 cx.set_state(indoc! {"
21506 def f() -> list[str]:
21507 ˇ
21508 "});
21509 cx.update_editor(|editor, window, cx| {
21510 editor.handle_input("a", window, cx);
21511 });
21512 cx.assert_editor_state(indoc! {"
21513 def f() -> list[str]:
21514 aˇ
21515 "});
21516}
21517
21518#[gpui::test]
21519async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
21520 init_test(cx, |_| {});
21521 update_test_language_settings(cx, |settings| {
21522 settings.defaults.extend_comment_on_newline = Some(false);
21523 });
21524 let mut cx = EditorTestContext::new(cx).await;
21525 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
21526 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21527
21528 // test correct indent after newline on comment
21529 cx.set_state(indoc! {"
21530 # COMMENT:ˇ
21531 "});
21532 cx.update_editor(|editor, window, cx| {
21533 editor.newline(&Newline, window, cx);
21534 });
21535 cx.assert_editor_state(indoc! {"
21536 # COMMENT:
21537 ˇ
21538 "});
21539
21540 // test correct indent after newline in brackets
21541 cx.set_state(indoc! {"
21542 {ˇ}
21543 "});
21544 cx.update_editor(|editor, window, cx| {
21545 editor.newline(&Newline, window, cx);
21546 });
21547 cx.run_until_parked();
21548 cx.assert_editor_state(indoc! {"
21549 {
21550 ˇ
21551 }
21552 "});
21553
21554 cx.set_state(indoc! {"
21555 (ˇ)
21556 "});
21557 cx.update_editor(|editor, window, cx| {
21558 editor.newline(&Newline, window, cx);
21559 });
21560 cx.run_until_parked();
21561 cx.assert_editor_state(indoc! {"
21562 (
21563 ˇ
21564 )
21565 "});
21566
21567 // do not indent after empty lists or dictionaries
21568 cx.set_state(indoc! {"
21569 a = []ˇ
21570 "});
21571 cx.update_editor(|editor, window, cx| {
21572 editor.newline(&Newline, window, cx);
21573 });
21574 cx.run_until_parked();
21575 cx.assert_editor_state(indoc! {"
21576 a = []
21577 ˇ
21578 "});
21579}
21580
21581fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
21582 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
21583 point..point
21584}
21585
21586#[track_caller]
21587fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
21588 let (text, ranges) = marked_text_ranges(marked_text, true);
21589 assert_eq!(editor.text(cx), text);
21590 assert_eq!(
21591 editor.selections.ranges(cx),
21592 ranges,
21593 "Assert selections are {}",
21594 marked_text
21595 );
21596}
21597
21598pub fn handle_signature_help_request(
21599 cx: &mut EditorLspTestContext,
21600 mocked_response: lsp::SignatureHelp,
21601) -> impl Future<Output = ()> + use<> {
21602 let mut request =
21603 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
21604 let mocked_response = mocked_response.clone();
21605 async move { Ok(Some(mocked_response)) }
21606 });
21607
21608 async move {
21609 request.next().await;
21610 }
21611}
21612
21613#[track_caller]
21614pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
21615 cx.update_editor(|editor, _, _| {
21616 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
21617 let entries = menu.entries.borrow();
21618 let entries = entries
21619 .iter()
21620 .map(|entry| entry.string.as_str())
21621 .collect::<Vec<_>>();
21622 assert_eq!(entries, expected);
21623 } else {
21624 panic!("Expected completions menu");
21625 }
21626 });
21627}
21628
21629/// Handle completion request passing a marked string specifying where the completion
21630/// should be triggered from using '|' character, what range should be replaced, and what completions
21631/// should be returned using '<' and '>' to delimit the range.
21632///
21633/// Also see `handle_completion_request_with_insert_and_replace`.
21634#[track_caller]
21635pub fn handle_completion_request(
21636 marked_string: &str,
21637 completions: Vec<&'static str>,
21638 is_incomplete: bool,
21639 counter: Arc<AtomicUsize>,
21640 cx: &mut EditorLspTestContext,
21641) -> impl Future<Output = ()> {
21642 let complete_from_marker: TextRangeMarker = '|'.into();
21643 let replace_range_marker: TextRangeMarker = ('<', '>').into();
21644 let (_, mut marked_ranges) = marked_text_ranges_by(
21645 marked_string,
21646 vec![complete_from_marker.clone(), replace_range_marker.clone()],
21647 );
21648
21649 let complete_from_position =
21650 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
21651 let replace_range =
21652 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
21653
21654 let mut request =
21655 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
21656 let completions = completions.clone();
21657 counter.fetch_add(1, atomic::Ordering::Release);
21658 async move {
21659 assert_eq!(params.text_document_position.text_document.uri, url.clone());
21660 assert_eq!(
21661 params.text_document_position.position,
21662 complete_from_position
21663 );
21664 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
21665 is_incomplete: is_incomplete,
21666 item_defaults: None,
21667 items: completions
21668 .iter()
21669 .map(|completion_text| lsp::CompletionItem {
21670 label: completion_text.to_string(),
21671 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
21672 range: replace_range,
21673 new_text: completion_text.to_string(),
21674 })),
21675 ..Default::default()
21676 })
21677 .collect(),
21678 })))
21679 }
21680 });
21681
21682 async move {
21683 request.next().await;
21684 }
21685}
21686
21687/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
21688/// given instead, which also contains an `insert` range.
21689///
21690/// This function uses markers to define ranges:
21691/// - `|` marks the cursor position
21692/// - `<>` marks the replace range
21693/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
21694pub fn handle_completion_request_with_insert_and_replace(
21695 cx: &mut EditorLspTestContext,
21696 marked_string: &str,
21697 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
21698 counter: Arc<AtomicUsize>,
21699) -> impl Future<Output = ()> {
21700 let complete_from_marker: TextRangeMarker = '|'.into();
21701 let replace_range_marker: TextRangeMarker = ('<', '>').into();
21702 let insert_range_marker: TextRangeMarker = ('{', '}').into();
21703
21704 let (_, mut marked_ranges) = marked_text_ranges_by(
21705 marked_string,
21706 vec![
21707 complete_from_marker.clone(),
21708 replace_range_marker.clone(),
21709 insert_range_marker.clone(),
21710 ],
21711 );
21712
21713 let complete_from_position =
21714 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
21715 let replace_range =
21716 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
21717
21718 let insert_range = match marked_ranges.remove(&insert_range_marker) {
21719 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
21720 _ => lsp::Range {
21721 start: replace_range.start,
21722 end: complete_from_position,
21723 },
21724 };
21725
21726 let mut request =
21727 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
21728 let completions = completions.clone();
21729 counter.fetch_add(1, atomic::Ordering::Release);
21730 async move {
21731 assert_eq!(params.text_document_position.text_document.uri, url.clone());
21732 assert_eq!(
21733 params.text_document_position.position, complete_from_position,
21734 "marker `|` position doesn't match",
21735 );
21736 Ok(Some(lsp::CompletionResponse::Array(
21737 completions
21738 .iter()
21739 .map(|(label, new_text)| lsp::CompletionItem {
21740 label: label.to_string(),
21741 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
21742 lsp::InsertReplaceEdit {
21743 insert: insert_range,
21744 replace: replace_range,
21745 new_text: new_text.to_string(),
21746 },
21747 )),
21748 ..Default::default()
21749 })
21750 .collect(),
21751 )))
21752 }
21753 });
21754
21755 async move {
21756 request.next().await;
21757 }
21758}
21759
21760fn handle_resolve_completion_request(
21761 cx: &mut EditorLspTestContext,
21762 edits: Option<Vec<(&'static str, &'static str)>>,
21763) -> impl Future<Output = ()> {
21764 let edits = edits.map(|edits| {
21765 edits
21766 .iter()
21767 .map(|(marked_string, new_text)| {
21768 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
21769 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
21770 lsp::TextEdit::new(replace_range, new_text.to_string())
21771 })
21772 .collect::<Vec<_>>()
21773 });
21774
21775 let mut request =
21776 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
21777 let edits = edits.clone();
21778 async move {
21779 Ok(lsp::CompletionItem {
21780 additional_text_edits: edits,
21781 ..Default::default()
21782 })
21783 }
21784 });
21785
21786 async move {
21787 request.next().await;
21788 }
21789}
21790
21791pub(crate) fn update_test_language_settings(
21792 cx: &mut TestAppContext,
21793 f: impl Fn(&mut AllLanguageSettingsContent),
21794) {
21795 cx.update(|cx| {
21796 SettingsStore::update_global(cx, |store, cx| {
21797 store.update_user_settings::<AllLanguageSettings>(cx, f);
21798 });
21799 });
21800}
21801
21802pub(crate) fn update_test_project_settings(
21803 cx: &mut TestAppContext,
21804 f: impl Fn(&mut ProjectSettings),
21805) {
21806 cx.update(|cx| {
21807 SettingsStore::update_global(cx, |store, cx| {
21808 store.update_user_settings::<ProjectSettings>(cx, f);
21809 });
21810 });
21811}
21812
21813pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
21814 cx.update(|cx| {
21815 assets::Assets.load_test_fonts(cx);
21816 let store = SettingsStore::test(cx);
21817 cx.set_global(store);
21818 theme::init(theme::LoadThemes::JustBase, cx);
21819 release_channel::init(SemanticVersion::default(), cx);
21820 client::init_settings(cx);
21821 language::init(cx);
21822 Project::init_settings(cx);
21823 workspace::init_settings(cx);
21824 crate::init(cx);
21825 });
21826
21827 update_test_language_settings(cx, f);
21828}
21829
21830#[track_caller]
21831fn assert_hunk_revert(
21832 not_reverted_text_with_selections: &str,
21833 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
21834 expected_reverted_text_with_selections: &str,
21835 base_text: &str,
21836 cx: &mut EditorLspTestContext,
21837) {
21838 cx.set_state(not_reverted_text_with_selections);
21839 cx.set_head_text(base_text);
21840 cx.executor().run_until_parked();
21841
21842 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
21843 let snapshot = editor.snapshot(window, cx);
21844 let reverted_hunk_statuses = snapshot
21845 .buffer_snapshot
21846 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
21847 .map(|hunk| hunk.status().kind)
21848 .collect::<Vec<_>>();
21849
21850 editor.git_restore(&Default::default(), window, cx);
21851 reverted_hunk_statuses
21852 });
21853 cx.executor().run_until_parked();
21854 cx.assert_editor_state(expected_reverted_text_with_selections);
21855 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
21856}
21857
21858#[gpui::test(iterations = 10)]
21859async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
21860 init_test(cx, |_| {});
21861
21862 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
21863 let counter = diagnostic_requests.clone();
21864
21865 let fs = FakeFs::new(cx.executor());
21866 fs.insert_tree(
21867 path!("/a"),
21868 json!({
21869 "first.rs": "fn main() { let a = 5; }",
21870 "second.rs": "// Test file",
21871 }),
21872 )
21873 .await;
21874
21875 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21876 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21877 let cx = &mut VisualTestContext::from_window(*workspace, cx);
21878
21879 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21880 language_registry.add(rust_lang());
21881 let mut fake_servers = language_registry.register_fake_lsp(
21882 "Rust",
21883 FakeLspAdapter {
21884 capabilities: lsp::ServerCapabilities {
21885 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
21886 lsp::DiagnosticOptions {
21887 identifier: None,
21888 inter_file_dependencies: true,
21889 workspace_diagnostics: true,
21890 work_done_progress_options: Default::default(),
21891 },
21892 )),
21893 ..Default::default()
21894 },
21895 ..Default::default()
21896 },
21897 );
21898
21899 let editor = workspace
21900 .update(cx, |workspace, window, cx| {
21901 workspace.open_abs_path(
21902 PathBuf::from(path!("/a/first.rs")),
21903 OpenOptions::default(),
21904 window,
21905 cx,
21906 )
21907 })
21908 .unwrap()
21909 .await
21910 .unwrap()
21911 .downcast::<Editor>()
21912 .unwrap();
21913 let fake_server = fake_servers.next().await.unwrap();
21914 let mut first_request = fake_server
21915 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
21916 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
21917 let result_id = Some(new_result_id.to_string());
21918 assert_eq!(
21919 params.text_document.uri,
21920 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
21921 );
21922 async move {
21923 Ok(lsp::DocumentDiagnosticReportResult::Report(
21924 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
21925 related_documents: None,
21926 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
21927 items: Vec::new(),
21928 result_id,
21929 },
21930 }),
21931 ))
21932 }
21933 });
21934
21935 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
21936 project.update(cx, |project, cx| {
21937 let buffer_id = editor
21938 .read(cx)
21939 .buffer()
21940 .read(cx)
21941 .as_singleton()
21942 .expect("created a singleton buffer")
21943 .read(cx)
21944 .remote_id();
21945 let buffer_result_id = project.lsp_store().read(cx).result_id(buffer_id);
21946 assert_eq!(expected, buffer_result_id);
21947 });
21948 };
21949
21950 ensure_result_id(None, cx);
21951 cx.executor().advance_clock(Duration::from_millis(60));
21952 cx.executor().run_until_parked();
21953 assert_eq!(
21954 diagnostic_requests.load(atomic::Ordering::Acquire),
21955 1,
21956 "Opening file should trigger diagnostic request"
21957 );
21958 first_request
21959 .next()
21960 .await
21961 .expect("should have sent the first diagnostics pull request");
21962 ensure_result_id(Some("1".to_string()), cx);
21963
21964 // Editing should trigger diagnostics
21965 editor.update_in(cx, |editor, window, cx| {
21966 editor.handle_input("2", window, cx)
21967 });
21968 cx.executor().advance_clock(Duration::from_millis(60));
21969 cx.executor().run_until_parked();
21970 assert_eq!(
21971 diagnostic_requests.load(atomic::Ordering::Acquire),
21972 2,
21973 "Editing should trigger diagnostic request"
21974 );
21975 ensure_result_id(Some("2".to_string()), cx);
21976
21977 // Moving cursor should not trigger diagnostic request
21978 editor.update_in(cx, |editor, window, cx| {
21979 editor.change_selections(None, window, cx, |s| {
21980 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
21981 });
21982 });
21983 cx.executor().advance_clock(Duration::from_millis(60));
21984 cx.executor().run_until_parked();
21985 assert_eq!(
21986 diagnostic_requests.load(atomic::Ordering::Acquire),
21987 2,
21988 "Cursor movement should not trigger diagnostic request"
21989 );
21990 ensure_result_id(Some("2".to_string()), cx);
21991
21992 // Multiple rapid edits should be debounced
21993 for _ in 0..5 {
21994 editor.update_in(cx, |editor, window, cx| {
21995 editor.handle_input("x", window, cx)
21996 });
21997 }
21998 cx.executor().advance_clock(Duration::from_millis(60));
21999 cx.executor().run_until_parked();
22000
22001 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
22002 assert!(
22003 final_requests <= 4,
22004 "Multiple rapid edits should be debounced (got {final_requests} requests)",
22005 );
22006 ensure_result_id(Some(final_requests.to_string()), cx);
22007}
22008
22009#[gpui::test]
22010async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
22011 // Regression test for issue #11671
22012 // Previously, adding a cursor after moving multiple cursors would reset
22013 // the cursor count instead of adding to the existing cursors.
22014 init_test(cx, |_| {});
22015 let mut cx = EditorTestContext::new(cx).await;
22016
22017 // Create a simple buffer with cursor at start
22018 cx.set_state(indoc! {"
22019 ˇaaaa
22020 bbbb
22021 cccc
22022 dddd
22023 eeee
22024 ffff
22025 gggg
22026 hhhh"});
22027
22028 // Add 2 cursors below (so we have 3 total)
22029 cx.update_editor(|editor, window, cx| {
22030 editor.add_selection_below(&Default::default(), window, cx);
22031 editor.add_selection_below(&Default::default(), window, cx);
22032 });
22033
22034 // Verify we have 3 cursors
22035 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
22036 assert_eq!(
22037 initial_count, 3,
22038 "Should have 3 cursors after adding 2 below"
22039 );
22040
22041 // Move down one line
22042 cx.update_editor(|editor, window, cx| {
22043 editor.move_down(&MoveDown, window, cx);
22044 });
22045
22046 // Add another cursor below
22047 cx.update_editor(|editor, window, cx| {
22048 editor.add_selection_below(&Default::default(), window, cx);
22049 });
22050
22051 // Should now have 4 cursors (3 original + 1 new)
22052 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
22053 assert_eq!(
22054 final_count, 4,
22055 "Should have 4 cursors after moving and adding another"
22056 );
22057}