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 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1911 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1912
1913 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1914 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1915
1916 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1917 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1918
1919 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1920 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1921
1922 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1923 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
1924
1925 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1926 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1927
1928 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1929 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1930
1931 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1932 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1933
1934 editor.move_right(&MoveRight, window, cx);
1935 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1936 assert_selection_ranges(
1937 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
1938 editor,
1939 cx,
1940 );
1941
1942 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1943 assert_selection_ranges(
1944 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
1945 editor,
1946 cx,
1947 );
1948
1949 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1950 assert_selection_ranges(
1951 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
1952 editor,
1953 cx,
1954 );
1955 });
1956}
1957
1958#[gpui::test]
1959fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1960 init_test(cx, |_| {});
1961
1962 let editor = cx.add_window(|window, cx| {
1963 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1964 build_editor(buffer, window, cx)
1965 });
1966
1967 _ = editor.update(cx, |editor, window, cx| {
1968 editor.set_wrap_width(Some(140.0.into()), cx);
1969 assert_eq!(
1970 editor.display_text(cx),
1971 "use one::{\n two::three::\n four::five\n};"
1972 );
1973
1974 editor.change_selections(None, window, cx, |s| {
1975 s.select_display_ranges([
1976 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1977 ]);
1978 });
1979
1980 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1981 assert_eq!(
1982 editor.selections.display_ranges(cx),
1983 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1984 );
1985
1986 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1987 assert_eq!(
1988 editor.selections.display_ranges(cx),
1989 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1990 );
1991
1992 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1993 assert_eq!(
1994 editor.selections.display_ranges(cx),
1995 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1996 );
1997
1998 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1999 assert_eq!(
2000 editor.selections.display_ranges(cx),
2001 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2002 );
2003
2004 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2005 assert_eq!(
2006 editor.selections.display_ranges(cx),
2007 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2008 );
2009
2010 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2011 assert_eq!(
2012 editor.selections.display_ranges(cx),
2013 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2014 );
2015 });
2016}
2017
2018#[gpui::test]
2019async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2020 init_test(cx, |_| {});
2021 let mut cx = EditorTestContext::new(cx).await;
2022
2023 let line_height = cx.editor(|editor, window, _| {
2024 editor
2025 .style()
2026 .unwrap()
2027 .text
2028 .line_height_in_pixels(window.rem_size())
2029 });
2030 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2031
2032 cx.set_state(
2033 &r#"ˇone
2034 two
2035
2036 three
2037 fourˇ
2038 five
2039
2040 six"#
2041 .unindent(),
2042 );
2043
2044 cx.update_editor(|editor, window, cx| {
2045 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2046 });
2047 cx.assert_editor_state(
2048 &r#"one
2049 two
2050 ˇ
2051 three
2052 four
2053 five
2054 ˇ
2055 six"#
2056 .unindent(),
2057 );
2058
2059 cx.update_editor(|editor, window, cx| {
2060 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2061 });
2062 cx.assert_editor_state(
2063 &r#"one
2064 two
2065
2066 three
2067 four
2068 five
2069 ˇ
2070 sixˇ"#
2071 .unindent(),
2072 );
2073
2074 cx.update_editor(|editor, window, cx| {
2075 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2076 });
2077 cx.assert_editor_state(
2078 &r#"one
2079 two
2080
2081 three
2082 four
2083 five
2084
2085 sixˇ"#
2086 .unindent(),
2087 );
2088
2089 cx.update_editor(|editor, window, cx| {
2090 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2091 });
2092 cx.assert_editor_state(
2093 &r#"one
2094 two
2095
2096 three
2097 four
2098 five
2099 ˇ
2100 six"#
2101 .unindent(),
2102 );
2103
2104 cx.update_editor(|editor, window, cx| {
2105 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2106 });
2107 cx.assert_editor_state(
2108 &r#"one
2109 two
2110 ˇ
2111 three
2112 four
2113 five
2114
2115 six"#
2116 .unindent(),
2117 );
2118
2119 cx.update_editor(|editor, window, cx| {
2120 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2121 });
2122 cx.assert_editor_state(
2123 &r#"ˇone
2124 two
2125
2126 three
2127 four
2128 five
2129
2130 six"#
2131 .unindent(),
2132 );
2133}
2134
2135#[gpui::test]
2136async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2137 init_test(cx, |_| {});
2138 let mut cx = EditorTestContext::new(cx).await;
2139 let line_height = cx.editor(|editor, window, _| {
2140 editor
2141 .style()
2142 .unwrap()
2143 .text
2144 .line_height_in_pixels(window.rem_size())
2145 });
2146 let window = cx.window;
2147 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2148
2149 cx.set_state(
2150 r#"ˇone
2151 two
2152 three
2153 four
2154 five
2155 six
2156 seven
2157 eight
2158 nine
2159 ten
2160 "#,
2161 );
2162
2163 cx.update_editor(|editor, window, cx| {
2164 assert_eq!(
2165 editor.snapshot(window, cx).scroll_position(),
2166 gpui::Point::new(0., 0.)
2167 );
2168 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2169 assert_eq!(
2170 editor.snapshot(window, cx).scroll_position(),
2171 gpui::Point::new(0., 3.)
2172 );
2173 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2174 assert_eq!(
2175 editor.snapshot(window, cx).scroll_position(),
2176 gpui::Point::new(0., 6.)
2177 );
2178 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2179 assert_eq!(
2180 editor.snapshot(window, cx).scroll_position(),
2181 gpui::Point::new(0., 3.)
2182 );
2183
2184 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2185 assert_eq!(
2186 editor.snapshot(window, cx).scroll_position(),
2187 gpui::Point::new(0., 1.)
2188 );
2189 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2190 assert_eq!(
2191 editor.snapshot(window, cx).scroll_position(),
2192 gpui::Point::new(0., 3.)
2193 );
2194 });
2195}
2196
2197#[gpui::test]
2198async fn test_autoscroll(cx: &mut TestAppContext) {
2199 init_test(cx, |_| {});
2200 let mut cx = EditorTestContext::new(cx).await;
2201
2202 let line_height = cx.update_editor(|editor, window, cx| {
2203 editor.set_vertical_scroll_margin(2, cx);
2204 editor
2205 .style()
2206 .unwrap()
2207 .text
2208 .line_height_in_pixels(window.rem_size())
2209 });
2210 let window = cx.window;
2211 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2212
2213 cx.set_state(
2214 r#"ˇone
2215 two
2216 three
2217 four
2218 five
2219 six
2220 seven
2221 eight
2222 nine
2223 ten
2224 "#,
2225 );
2226 cx.update_editor(|editor, window, cx| {
2227 assert_eq!(
2228 editor.snapshot(window, cx).scroll_position(),
2229 gpui::Point::new(0., 0.0)
2230 );
2231 });
2232
2233 // Add a cursor below the visible area. Since both cursors cannot fit
2234 // on screen, the editor autoscrolls to reveal the newest cursor, and
2235 // allows the vertical scroll margin below that cursor.
2236 cx.update_editor(|editor, window, cx| {
2237 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2238 selections.select_ranges([
2239 Point::new(0, 0)..Point::new(0, 0),
2240 Point::new(6, 0)..Point::new(6, 0),
2241 ]);
2242 })
2243 });
2244 cx.update_editor(|editor, window, cx| {
2245 assert_eq!(
2246 editor.snapshot(window, cx).scroll_position(),
2247 gpui::Point::new(0., 3.0)
2248 );
2249 });
2250
2251 // Move down. The editor cursor scrolls down to track the newest cursor.
2252 cx.update_editor(|editor, window, cx| {
2253 editor.move_down(&Default::default(), window, cx);
2254 });
2255 cx.update_editor(|editor, window, cx| {
2256 assert_eq!(
2257 editor.snapshot(window, cx).scroll_position(),
2258 gpui::Point::new(0., 4.0)
2259 );
2260 });
2261
2262 // Add a cursor above the visible area. Since both cursors fit on screen,
2263 // the editor scrolls to show both.
2264 cx.update_editor(|editor, window, cx| {
2265 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2266 selections.select_ranges([
2267 Point::new(1, 0)..Point::new(1, 0),
2268 Point::new(6, 0)..Point::new(6, 0),
2269 ]);
2270 })
2271 });
2272 cx.update_editor(|editor, window, cx| {
2273 assert_eq!(
2274 editor.snapshot(window, cx).scroll_position(),
2275 gpui::Point::new(0., 1.0)
2276 );
2277 });
2278}
2279
2280#[gpui::test]
2281async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2282 init_test(cx, |_| {});
2283 let mut cx = EditorTestContext::new(cx).await;
2284
2285 let line_height = cx.editor(|editor, window, _cx| {
2286 editor
2287 .style()
2288 .unwrap()
2289 .text
2290 .line_height_in_pixels(window.rem_size())
2291 });
2292 let window = cx.window;
2293 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2294 cx.set_state(
2295 &r#"
2296 ˇone
2297 two
2298 threeˇ
2299 four
2300 five
2301 six
2302 seven
2303 eight
2304 nine
2305 ten
2306 "#
2307 .unindent(),
2308 );
2309
2310 cx.update_editor(|editor, window, cx| {
2311 editor.move_page_down(&MovePageDown::default(), window, cx)
2312 });
2313 cx.assert_editor_state(
2314 &r#"
2315 one
2316 two
2317 three
2318 ˇfour
2319 five
2320 sixˇ
2321 seven
2322 eight
2323 nine
2324 ten
2325 "#
2326 .unindent(),
2327 );
2328
2329 cx.update_editor(|editor, window, cx| {
2330 editor.move_page_down(&MovePageDown::default(), window, cx)
2331 });
2332 cx.assert_editor_state(
2333 &r#"
2334 one
2335 two
2336 three
2337 four
2338 five
2339 six
2340 ˇseven
2341 eight
2342 nineˇ
2343 ten
2344 "#
2345 .unindent(),
2346 );
2347
2348 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2349 cx.assert_editor_state(
2350 &r#"
2351 one
2352 two
2353 three
2354 ˇfour
2355 five
2356 sixˇ
2357 seven
2358 eight
2359 nine
2360 ten
2361 "#
2362 .unindent(),
2363 );
2364
2365 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2366 cx.assert_editor_state(
2367 &r#"
2368 ˇone
2369 two
2370 threeˇ
2371 four
2372 five
2373 six
2374 seven
2375 eight
2376 nine
2377 ten
2378 "#
2379 .unindent(),
2380 );
2381
2382 // Test select collapsing
2383 cx.update_editor(|editor, window, cx| {
2384 editor.move_page_down(&MovePageDown::default(), window, cx);
2385 editor.move_page_down(&MovePageDown::default(), window, cx);
2386 editor.move_page_down(&MovePageDown::default(), window, cx);
2387 });
2388 cx.assert_editor_state(
2389 &r#"
2390 one
2391 two
2392 three
2393 four
2394 five
2395 six
2396 seven
2397 eight
2398 nine
2399 ˇten
2400 ˇ"#
2401 .unindent(),
2402 );
2403}
2404
2405#[gpui::test]
2406async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2407 init_test(cx, |_| {});
2408 let mut cx = EditorTestContext::new(cx).await;
2409 cx.set_state("one «two threeˇ» four");
2410 cx.update_editor(|editor, window, cx| {
2411 editor.delete_to_beginning_of_line(
2412 &DeleteToBeginningOfLine {
2413 stop_at_indent: false,
2414 },
2415 window,
2416 cx,
2417 );
2418 assert_eq!(editor.text(cx), " four");
2419 });
2420}
2421
2422#[gpui::test]
2423fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2424 init_test(cx, |_| {});
2425
2426 let editor = cx.add_window(|window, cx| {
2427 let buffer = MultiBuffer::build_simple("one two three four", cx);
2428 build_editor(buffer.clone(), window, cx)
2429 });
2430
2431 _ = editor.update(cx, |editor, window, cx| {
2432 editor.change_selections(None, window, cx, |s| {
2433 s.select_display_ranges([
2434 // an empty selection - the preceding word fragment is deleted
2435 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2436 // characters selected - they are deleted
2437 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2438 ])
2439 });
2440 editor.delete_to_previous_word_start(
2441 &DeleteToPreviousWordStart {
2442 ignore_newlines: false,
2443 },
2444 window,
2445 cx,
2446 );
2447 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2448 });
2449
2450 _ = editor.update(cx, |editor, window, cx| {
2451 editor.change_selections(None, window, cx, |s| {
2452 s.select_display_ranges([
2453 // an empty selection - the following word fragment is deleted
2454 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2455 // characters selected - they are deleted
2456 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2457 ])
2458 });
2459 editor.delete_to_next_word_end(
2460 &DeleteToNextWordEnd {
2461 ignore_newlines: false,
2462 },
2463 window,
2464 cx,
2465 );
2466 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2467 });
2468}
2469
2470#[gpui::test]
2471fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2472 init_test(cx, |_| {});
2473
2474 let editor = cx.add_window(|window, cx| {
2475 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2476 build_editor(buffer.clone(), window, cx)
2477 });
2478 let del_to_prev_word_start = DeleteToPreviousWordStart {
2479 ignore_newlines: false,
2480 };
2481 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2482 ignore_newlines: true,
2483 };
2484
2485 _ = editor.update(cx, |editor, window, cx| {
2486 editor.change_selections(None, window, cx, |s| {
2487 s.select_display_ranges([
2488 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2489 ])
2490 });
2491 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2492 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2493 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2494 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2495 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2496 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2497 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2498 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2499 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2500 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2501 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2502 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2503 });
2504}
2505
2506#[gpui::test]
2507fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2508 init_test(cx, |_| {});
2509
2510 let editor = cx.add_window(|window, cx| {
2511 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2512 build_editor(buffer.clone(), window, cx)
2513 });
2514 let del_to_next_word_end = DeleteToNextWordEnd {
2515 ignore_newlines: false,
2516 };
2517 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2518 ignore_newlines: true,
2519 };
2520
2521 _ = editor.update(cx, |editor, window, cx| {
2522 editor.change_selections(None, window, cx, |s| {
2523 s.select_display_ranges([
2524 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2525 ])
2526 });
2527 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2528 assert_eq!(
2529 editor.buffer.read(cx).read(cx).text(),
2530 "one\n two\nthree\n four"
2531 );
2532 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2533 assert_eq!(
2534 editor.buffer.read(cx).read(cx).text(),
2535 "\n two\nthree\n four"
2536 );
2537 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2538 assert_eq!(
2539 editor.buffer.read(cx).read(cx).text(),
2540 "two\nthree\n four"
2541 );
2542 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2543 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2544 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2545 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2546 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2547 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2548 });
2549}
2550
2551#[gpui::test]
2552fn test_newline(cx: &mut TestAppContext) {
2553 init_test(cx, |_| {});
2554
2555 let editor = cx.add_window(|window, cx| {
2556 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2557 build_editor(buffer.clone(), window, cx)
2558 });
2559
2560 _ = editor.update(cx, |editor, window, cx| {
2561 editor.change_selections(None, window, cx, |s| {
2562 s.select_display_ranges([
2563 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2564 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2565 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2566 ])
2567 });
2568
2569 editor.newline(&Newline, window, cx);
2570 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2571 });
2572}
2573
2574#[gpui::test]
2575fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2576 init_test(cx, |_| {});
2577
2578 let editor = cx.add_window(|window, cx| {
2579 let buffer = MultiBuffer::build_simple(
2580 "
2581 a
2582 b(
2583 X
2584 )
2585 c(
2586 X
2587 )
2588 "
2589 .unindent()
2590 .as_str(),
2591 cx,
2592 );
2593 let mut editor = build_editor(buffer.clone(), window, cx);
2594 editor.change_selections(None, window, cx, |s| {
2595 s.select_ranges([
2596 Point::new(2, 4)..Point::new(2, 5),
2597 Point::new(5, 4)..Point::new(5, 5),
2598 ])
2599 });
2600 editor
2601 });
2602
2603 _ = editor.update(cx, |editor, window, cx| {
2604 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2605 editor.buffer.update(cx, |buffer, cx| {
2606 buffer.edit(
2607 [
2608 (Point::new(1, 2)..Point::new(3, 0), ""),
2609 (Point::new(4, 2)..Point::new(6, 0), ""),
2610 ],
2611 None,
2612 cx,
2613 );
2614 assert_eq!(
2615 buffer.read(cx).text(),
2616 "
2617 a
2618 b()
2619 c()
2620 "
2621 .unindent()
2622 );
2623 });
2624 assert_eq!(
2625 editor.selections.ranges(cx),
2626 &[
2627 Point::new(1, 2)..Point::new(1, 2),
2628 Point::new(2, 2)..Point::new(2, 2),
2629 ],
2630 );
2631
2632 editor.newline(&Newline, window, cx);
2633 assert_eq!(
2634 editor.text(cx),
2635 "
2636 a
2637 b(
2638 )
2639 c(
2640 )
2641 "
2642 .unindent()
2643 );
2644
2645 // The selections are moved after the inserted newlines
2646 assert_eq!(
2647 editor.selections.ranges(cx),
2648 &[
2649 Point::new(2, 0)..Point::new(2, 0),
2650 Point::new(4, 0)..Point::new(4, 0),
2651 ],
2652 );
2653 });
2654}
2655
2656#[gpui::test]
2657async fn test_newline_above(cx: &mut TestAppContext) {
2658 init_test(cx, |settings| {
2659 settings.defaults.tab_size = NonZeroU32::new(4)
2660 });
2661
2662 let language = Arc::new(
2663 Language::new(
2664 LanguageConfig::default(),
2665 Some(tree_sitter_rust::LANGUAGE.into()),
2666 )
2667 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2668 .unwrap(),
2669 );
2670
2671 let mut cx = EditorTestContext::new(cx).await;
2672 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2673 cx.set_state(indoc! {"
2674 const a: ˇA = (
2675 (ˇ
2676 «const_functionˇ»(ˇ),
2677 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2678 )ˇ
2679 ˇ);ˇ
2680 "});
2681
2682 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2683 cx.assert_editor_state(indoc! {"
2684 ˇ
2685 const a: A = (
2686 ˇ
2687 (
2688 ˇ
2689 ˇ
2690 const_function(),
2691 ˇ
2692 ˇ
2693 ˇ
2694 ˇ
2695 something_else,
2696 ˇ
2697 )
2698 ˇ
2699 ˇ
2700 );
2701 "});
2702}
2703
2704#[gpui::test]
2705async fn test_newline_below(cx: &mut TestAppContext) {
2706 init_test(cx, |settings| {
2707 settings.defaults.tab_size = NonZeroU32::new(4)
2708 });
2709
2710 let language = Arc::new(
2711 Language::new(
2712 LanguageConfig::default(),
2713 Some(tree_sitter_rust::LANGUAGE.into()),
2714 )
2715 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2716 .unwrap(),
2717 );
2718
2719 let mut cx = EditorTestContext::new(cx).await;
2720 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2721 cx.set_state(indoc! {"
2722 const a: ˇA = (
2723 (ˇ
2724 «const_functionˇ»(ˇ),
2725 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2726 )ˇ
2727 ˇ);ˇ
2728 "});
2729
2730 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2731 cx.assert_editor_state(indoc! {"
2732 const a: A = (
2733 ˇ
2734 (
2735 ˇ
2736 const_function(),
2737 ˇ
2738 ˇ
2739 something_else,
2740 ˇ
2741 ˇ
2742 ˇ
2743 ˇ
2744 )
2745 ˇ
2746 );
2747 ˇ
2748 ˇ
2749 "});
2750}
2751
2752#[gpui::test]
2753async fn test_newline_comments(cx: &mut TestAppContext) {
2754 init_test(cx, |settings| {
2755 settings.defaults.tab_size = NonZeroU32::new(4)
2756 });
2757
2758 let language = Arc::new(Language::new(
2759 LanguageConfig {
2760 line_comments: vec!["// ".into()],
2761 ..LanguageConfig::default()
2762 },
2763 None,
2764 ));
2765 {
2766 let mut cx = EditorTestContext::new(cx).await;
2767 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2768 cx.set_state(indoc! {"
2769 // Fooˇ
2770 "});
2771
2772 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2773 cx.assert_editor_state(indoc! {"
2774 // Foo
2775 // ˇ
2776 "});
2777 // Ensure that we add comment prefix when existing line contains space
2778 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2779 cx.assert_editor_state(
2780 indoc! {"
2781 // Foo
2782 //s
2783 // ˇ
2784 "}
2785 .replace("s", " ") // s is used as space placeholder to prevent format on save
2786 .as_str(),
2787 );
2788 // Ensure that we add comment prefix when existing line does not contain space
2789 cx.set_state(indoc! {"
2790 // Foo
2791 //ˇ
2792 "});
2793 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2794 cx.assert_editor_state(indoc! {"
2795 // Foo
2796 //
2797 // ˇ
2798 "});
2799 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2800 cx.set_state(indoc! {"
2801 ˇ// Foo
2802 "});
2803 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2804 cx.assert_editor_state(indoc! {"
2805
2806 ˇ// Foo
2807 "});
2808 }
2809 // Ensure that comment continuations can be disabled.
2810 update_test_language_settings(cx, |settings| {
2811 settings.defaults.extend_comment_on_newline = Some(false);
2812 });
2813 let mut cx = EditorTestContext::new(cx).await;
2814 cx.set_state(indoc! {"
2815 // Fooˇ
2816 "});
2817 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2818 cx.assert_editor_state(indoc! {"
2819 // Foo
2820 ˇ
2821 "});
2822}
2823
2824#[gpui::test]
2825async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
2826 init_test(cx, |settings| {
2827 settings.defaults.tab_size = NonZeroU32::new(4)
2828 });
2829
2830 let language = Arc::new(Language::new(
2831 LanguageConfig {
2832 line_comments: vec!["// ".into(), "/// ".into()],
2833 ..LanguageConfig::default()
2834 },
2835 None,
2836 ));
2837 {
2838 let mut cx = EditorTestContext::new(cx).await;
2839 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2840 cx.set_state(indoc! {"
2841 //ˇ
2842 "});
2843 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2844 cx.assert_editor_state(indoc! {"
2845 //
2846 // ˇ
2847 "});
2848
2849 cx.set_state(indoc! {"
2850 ///ˇ
2851 "});
2852 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2853 cx.assert_editor_state(indoc! {"
2854 ///
2855 /// ˇ
2856 "});
2857 }
2858}
2859
2860#[gpui::test]
2861async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
2862 init_test(cx, |settings| {
2863 settings.defaults.tab_size = NonZeroU32::new(4)
2864 });
2865
2866 let language = Arc::new(
2867 Language::new(
2868 LanguageConfig {
2869 documentation: Some(language::DocumentationConfig {
2870 start: "/**".into(),
2871 end: "*/".into(),
2872 prefix: "* ".into(),
2873 tab_size: NonZeroU32::new(1).unwrap(),
2874 }),
2875
2876 ..LanguageConfig::default()
2877 },
2878 Some(tree_sitter_rust::LANGUAGE.into()),
2879 )
2880 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
2881 .unwrap(),
2882 );
2883
2884 {
2885 let mut cx = EditorTestContext::new(cx).await;
2886 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2887 cx.set_state(indoc! {"
2888 /**ˇ
2889 "});
2890
2891 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2892 cx.assert_editor_state(indoc! {"
2893 /**
2894 * ˇ
2895 "});
2896 // Ensure that if cursor is before the comment start,
2897 // we do not actually insert a comment prefix.
2898 cx.set_state(indoc! {"
2899 ˇ/**
2900 "});
2901 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2902 cx.assert_editor_state(indoc! {"
2903
2904 ˇ/**
2905 "});
2906 // Ensure that if cursor is between it doesn't add comment prefix.
2907 cx.set_state(indoc! {"
2908 /*ˇ*
2909 "});
2910 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2911 cx.assert_editor_state(indoc! {"
2912 /*
2913 ˇ*
2914 "});
2915 // Ensure that if suffix exists on same line after cursor it adds new line.
2916 cx.set_state(indoc! {"
2917 /**ˇ*/
2918 "});
2919 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2920 cx.assert_editor_state(indoc! {"
2921 /**
2922 * ˇ
2923 */
2924 "});
2925 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2926 cx.set_state(indoc! {"
2927 /**ˇ */
2928 "});
2929 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2930 cx.assert_editor_state(indoc! {"
2931 /**
2932 * ˇ
2933 */
2934 "});
2935 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2936 cx.set_state(indoc! {"
2937 /** ˇ*/
2938 "});
2939 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2940 cx.assert_editor_state(
2941 indoc! {"
2942 /**s
2943 * ˇ
2944 */
2945 "}
2946 .replace("s", " ") // s is used as space placeholder to prevent format on save
2947 .as_str(),
2948 );
2949 // Ensure that delimiter space is preserved when newline on already
2950 // spaced delimiter.
2951 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2952 cx.assert_editor_state(
2953 indoc! {"
2954 /**s
2955 *s
2956 * ˇ
2957 */
2958 "}
2959 .replace("s", " ") // s is used as space placeholder to prevent format on save
2960 .as_str(),
2961 );
2962 // Ensure that delimiter space is preserved when space is not
2963 // on existing delimiter.
2964 cx.set_state(indoc! {"
2965 /**
2966 *ˇ
2967 */
2968 "});
2969 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2970 cx.assert_editor_state(indoc! {"
2971 /**
2972 *
2973 * ˇ
2974 */
2975 "});
2976 // Ensure that if suffix exists on same line after cursor it
2977 // doesn't add extra new line if prefix is not on same line.
2978 cx.set_state(indoc! {"
2979 /**
2980 ˇ*/
2981 "});
2982 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2983 cx.assert_editor_state(indoc! {"
2984 /**
2985
2986 ˇ*/
2987 "});
2988 // Ensure that it detects suffix after existing prefix.
2989 cx.set_state(indoc! {"
2990 /**ˇ/
2991 "});
2992 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2993 cx.assert_editor_state(indoc! {"
2994 /**
2995 ˇ/
2996 "});
2997 // Ensure that if suffix exists on same line before
2998 // cursor it does not add comment prefix.
2999 cx.set_state(indoc! {"
3000 /** */ˇ
3001 "});
3002 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3003 cx.assert_editor_state(indoc! {"
3004 /** */
3005 ˇ
3006 "});
3007 // Ensure that if suffix exists on same line before
3008 // cursor it does not add comment prefix.
3009 cx.set_state(indoc! {"
3010 /**
3011 *
3012 */ˇ
3013 "});
3014 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3015 cx.assert_editor_state(indoc! {"
3016 /**
3017 *
3018 */
3019 ˇ
3020 "});
3021
3022 // Ensure that inline comment followed by code
3023 // doesn't add comment prefix on newline
3024 cx.set_state(indoc! {"
3025 /** */ textˇ
3026 "});
3027 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3028 cx.assert_editor_state(indoc! {"
3029 /** */ text
3030 ˇ
3031 "});
3032
3033 // Ensure that text after comment end tag
3034 // doesn't add comment prefix on newline
3035 cx.set_state(indoc! {"
3036 /**
3037 *
3038 */ˇtext
3039 "});
3040 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3041 cx.assert_editor_state(indoc! {"
3042 /**
3043 *
3044 */
3045 ˇtext
3046 "});
3047
3048 // Ensure if not comment block it doesn't
3049 // add comment prefix on newline
3050 cx.set_state(indoc! {"
3051 * textˇ
3052 "});
3053 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3054 cx.assert_editor_state(indoc! {"
3055 * text
3056 ˇ
3057 "});
3058 }
3059 // Ensure that comment continuations can be disabled.
3060 update_test_language_settings(cx, |settings| {
3061 settings.defaults.extend_comment_on_newline = Some(false);
3062 });
3063 let mut cx = EditorTestContext::new(cx).await;
3064 cx.set_state(indoc! {"
3065 /**ˇ
3066 "});
3067 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3068 cx.assert_editor_state(indoc! {"
3069 /**
3070 ˇ
3071 "});
3072}
3073
3074#[gpui::test]
3075fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3076 init_test(cx, |_| {});
3077
3078 let editor = cx.add_window(|window, cx| {
3079 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3080 let mut editor = build_editor(buffer.clone(), window, cx);
3081 editor.change_selections(None, window, cx, |s| {
3082 s.select_ranges([3..4, 11..12, 19..20])
3083 });
3084 editor
3085 });
3086
3087 _ = editor.update(cx, |editor, window, cx| {
3088 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3089 editor.buffer.update(cx, |buffer, cx| {
3090 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3091 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3092 });
3093 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3094
3095 editor.insert("Z", window, cx);
3096 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3097
3098 // The selections are moved after the inserted characters
3099 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3100 });
3101}
3102
3103#[gpui::test]
3104async fn test_tab(cx: &mut TestAppContext) {
3105 init_test(cx, |settings| {
3106 settings.defaults.tab_size = NonZeroU32::new(3)
3107 });
3108
3109 let mut cx = EditorTestContext::new(cx).await;
3110 cx.set_state(indoc! {"
3111 ˇabˇc
3112 ˇ🏀ˇ🏀ˇefg
3113 dˇ
3114 "});
3115 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3116 cx.assert_editor_state(indoc! {"
3117 ˇab ˇc
3118 ˇ🏀 ˇ🏀 ˇefg
3119 d ˇ
3120 "});
3121
3122 cx.set_state(indoc! {"
3123 a
3124 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3125 "});
3126 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3127 cx.assert_editor_state(indoc! {"
3128 a
3129 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3130 "});
3131}
3132
3133#[gpui::test]
3134async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3135 init_test(cx, |_| {});
3136
3137 let mut cx = EditorTestContext::new(cx).await;
3138 let language = Arc::new(
3139 Language::new(
3140 LanguageConfig::default(),
3141 Some(tree_sitter_rust::LANGUAGE.into()),
3142 )
3143 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3144 .unwrap(),
3145 );
3146 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3147
3148 // test when all cursors are not at suggested indent
3149 // then simply move to their suggested indent location
3150 cx.set_state(indoc! {"
3151 const a: B = (
3152 c(
3153 ˇ
3154 ˇ )
3155 );
3156 "});
3157 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3158 cx.assert_editor_state(indoc! {"
3159 const a: B = (
3160 c(
3161 ˇ
3162 ˇ)
3163 );
3164 "});
3165
3166 // test cursor already at suggested indent not moving when
3167 // other cursors are yet to reach their suggested indents
3168 cx.set_state(indoc! {"
3169 ˇ
3170 const a: B = (
3171 c(
3172 d(
3173 ˇ
3174 )
3175 ˇ
3176 ˇ )
3177 );
3178 "});
3179 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3180 cx.assert_editor_state(indoc! {"
3181 ˇ
3182 const a: B = (
3183 c(
3184 d(
3185 ˇ
3186 )
3187 ˇ
3188 ˇ)
3189 );
3190 "});
3191 // test when all cursors are at suggested indent then tab is inserted
3192 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3193 cx.assert_editor_state(indoc! {"
3194 ˇ
3195 const a: B = (
3196 c(
3197 d(
3198 ˇ
3199 )
3200 ˇ
3201 ˇ)
3202 );
3203 "});
3204
3205 // test when current indent is less than suggested indent,
3206 // we adjust line to match suggested indent and move cursor to it
3207 //
3208 // when no other cursor is at word boundary, all of them should move
3209 cx.set_state(indoc! {"
3210 const a: B = (
3211 c(
3212 d(
3213 ˇ
3214 ˇ )
3215 ˇ )
3216 );
3217 "});
3218 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3219 cx.assert_editor_state(indoc! {"
3220 const a: B = (
3221 c(
3222 d(
3223 ˇ
3224 ˇ)
3225 ˇ)
3226 );
3227 "});
3228
3229 // test when current indent is less than suggested indent,
3230 // we adjust line to match suggested indent and move cursor to it
3231 //
3232 // when some other cursor is at word boundary, it should not move
3233 cx.set_state(indoc! {"
3234 const a: B = (
3235 c(
3236 d(
3237 ˇ
3238 ˇ )
3239 ˇ)
3240 );
3241 "});
3242 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3243 cx.assert_editor_state(indoc! {"
3244 const a: B = (
3245 c(
3246 d(
3247 ˇ
3248 ˇ)
3249 ˇ)
3250 );
3251 "});
3252
3253 // test when current indent is more than suggested indent,
3254 // we just move cursor to current indent instead of suggested indent
3255 //
3256 // when no other cursor is at word boundary, all of them should move
3257 cx.set_state(indoc! {"
3258 const a: B = (
3259 c(
3260 d(
3261 ˇ
3262 ˇ )
3263 ˇ )
3264 );
3265 "});
3266 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3267 cx.assert_editor_state(indoc! {"
3268 const a: B = (
3269 c(
3270 d(
3271 ˇ
3272 ˇ)
3273 ˇ)
3274 );
3275 "});
3276 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3277 cx.assert_editor_state(indoc! {"
3278 const a: B = (
3279 c(
3280 d(
3281 ˇ
3282 ˇ)
3283 ˇ)
3284 );
3285 "});
3286
3287 // test when current indent is more than suggested indent,
3288 // we just move cursor to current indent instead of suggested indent
3289 //
3290 // when some other cursor is at word boundary, it doesn't move
3291 cx.set_state(indoc! {"
3292 const a: B = (
3293 c(
3294 d(
3295 ˇ
3296 ˇ )
3297 ˇ)
3298 );
3299 "});
3300 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3301 cx.assert_editor_state(indoc! {"
3302 const a: B = (
3303 c(
3304 d(
3305 ˇ
3306 ˇ)
3307 ˇ)
3308 );
3309 "});
3310
3311 // handle auto-indent when there are multiple cursors on the same line
3312 cx.set_state(indoc! {"
3313 const a: B = (
3314 c(
3315 ˇ ˇ
3316 ˇ )
3317 );
3318 "});
3319 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3320 cx.assert_editor_state(indoc! {"
3321 const a: B = (
3322 c(
3323 ˇ
3324 ˇ)
3325 );
3326 "});
3327}
3328
3329#[gpui::test]
3330async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3331 init_test(cx, |settings| {
3332 settings.defaults.tab_size = NonZeroU32::new(3)
3333 });
3334
3335 let mut cx = EditorTestContext::new(cx).await;
3336 cx.set_state(indoc! {"
3337 ˇ
3338 \t ˇ
3339 \t ˇ
3340 \t ˇ
3341 \t \t\t \t \t\t \t\t \t \t ˇ
3342 "});
3343
3344 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3345 cx.assert_editor_state(indoc! {"
3346 ˇ
3347 \t ˇ
3348 \t ˇ
3349 \t ˇ
3350 \t \t\t \t \t\t \t\t \t \t ˇ
3351 "});
3352}
3353
3354#[gpui::test]
3355async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3356 init_test(cx, |settings| {
3357 settings.defaults.tab_size = NonZeroU32::new(4)
3358 });
3359
3360 let language = Arc::new(
3361 Language::new(
3362 LanguageConfig::default(),
3363 Some(tree_sitter_rust::LANGUAGE.into()),
3364 )
3365 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3366 .unwrap(),
3367 );
3368
3369 let mut cx = EditorTestContext::new(cx).await;
3370 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3371 cx.set_state(indoc! {"
3372 fn a() {
3373 if b {
3374 \t ˇc
3375 }
3376 }
3377 "});
3378
3379 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3380 cx.assert_editor_state(indoc! {"
3381 fn a() {
3382 if b {
3383 ˇc
3384 }
3385 }
3386 "});
3387}
3388
3389#[gpui::test]
3390async fn test_indent_outdent(cx: &mut TestAppContext) {
3391 init_test(cx, |settings| {
3392 settings.defaults.tab_size = NonZeroU32::new(4);
3393 });
3394
3395 let mut cx = EditorTestContext::new(cx).await;
3396
3397 cx.set_state(indoc! {"
3398 «oneˇ» «twoˇ»
3399 three
3400 four
3401 "});
3402 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3403 cx.assert_editor_state(indoc! {"
3404 «oneˇ» «twoˇ»
3405 three
3406 four
3407 "});
3408
3409 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3410 cx.assert_editor_state(indoc! {"
3411 «oneˇ» «twoˇ»
3412 three
3413 four
3414 "});
3415
3416 // select across line ending
3417 cx.set_state(indoc! {"
3418 one two
3419 t«hree
3420 ˇ» four
3421 "});
3422 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3423 cx.assert_editor_state(indoc! {"
3424 one two
3425 t«hree
3426 ˇ» four
3427 "});
3428
3429 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3430 cx.assert_editor_state(indoc! {"
3431 one two
3432 t«hree
3433 ˇ» four
3434 "});
3435
3436 // Ensure that indenting/outdenting works when the cursor is at column 0.
3437 cx.set_state(indoc! {"
3438 one two
3439 ˇthree
3440 four
3441 "});
3442 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3443 cx.assert_editor_state(indoc! {"
3444 one two
3445 ˇthree
3446 four
3447 "});
3448
3449 cx.set_state(indoc! {"
3450 one two
3451 ˇ three
3452 four
3453 "});
3454 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3455 cx.assert_editor_state(indoc! {"
3456 one two
3457 ˇthree
3458 four
3459 "});
3460}
3461
3462#[gpui::test]
3463async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3464 init_test(cx, |settings| {
3465 settings.defaults.hard_tabs = Some(true);
3466 });
3467
3468 let mut cx = EditorTestContext::new(cx).await;
3469
3470 // select two ranges on one line
3471 cx.set_state(indoc! {"
3472 «oneˇ» «twoˇ»
3473 three
3474 four
3475 "});
3476 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3477 cx.assert_editor_state(indoc! {"
3478 \t«oneˇ» «twoˇ»
3479 three
3480 four
3481 "});
3482 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3483 cx.assert_editor_state(indoc! {"
3484 \t\t«oneˇ» «twoˇ»
3485 three
3486 four
3487 "});
3488 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3489 cx.assert_editor_state(indoc! {"
3490 \t«oneˇ» «twoˇ»
3491 three
3492 four
3493 "});
3494 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3495 cx.assert_editor_state(indoc! {"
3496 «oneˇ» «twoˇ»
3497 three
3498 four
3499 "});
3500
3501 // select across a line ending
3502 cx.set_state(indoc! {"
3503 one two
3504 t«hree
3505 ˇ»four
3506 "});
3507 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3508 cx.assert_editor_state(indoc! {"
3509 one two
3510 \tt«hree
3511 ˇ»four
3512 "});
3513 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3514 cx.assert_editor_state(indoc! {"
3515 one two
3516 \t\tt«hree
3517 ˇ»four
3518 "});
3519 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3520 cx.assert_editor_state(indoc! {"
3521 one two
3522 \tt«hree
3523 ˇ»four
3524 "});
3525 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3526 cx.assert_editor_state(indoc! {"
3527 one two
3528 t«hree
3529 ˇ»four
3530 "});
3531
3532 // Ensure that indenting/outdenting works when the cursor is at column 0.
3533 cx.set_state(indoc! {"
3534 one two
3535 ˇthree
3536 four
3537 "});
3538 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3539 cx.assert_editor_state(indoc! {"
3540 one two
3541 ˇthree
3542 four
3543 "});
3544 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3545 cx.assert_editor_state(indoc! {"
3546 one two
3547 \tˇthree
3548 four
3549 "});
3550 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3551 cx.assert_editor_state(indoc! {"
3552 one two
3553 ˇthree
3554 four
3555 "});
3556}
3557
3558#[gpui::test]
3559fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3560 init_test(cx, |settings| {
3561 settings.languages.extend([
3562 (
3563 "TOML".into(),
3564 LanguageSettingsContent {
3565 tab_size: NonZeroU32::new(2),
3566 ..Default::default()
3567 },
3568 ),
3569 (
3570 "Rust".into(),
3571 LanguageSettingsContent {
3572 tab_size: NonZeroU32::new(4),
3573 ..Default::default()
3574 },
3575 ),
3576 ]);
3577 });
3578
3579 let toml_language = Arc::new(Language::new(
3580 LanguageConfig {
3581 name: "TOML".into(),
3582 ..Default::default()
3583 },
3584 None,
3585 ));
3586 let rust_language = Arc::new(Language::new(
3587 LanguageConfig {
3588 name: "Rust".into(),
3589 ..Default::default()
3590 },
3591 None,
3592 ));
3593
3594 let toml_buffer =
3595 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3596 let rust_buffer =
3597 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3598 let multibuffer = cx.new(|cx| {
3599 let mut multibuffer = MultiBuffer::new(ReadWrite);
3600 multibuffer.push_excerpts(
3601 toml_buffer.clone(),
3602 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3603 cx,
3604 );
3605 multibuffer.push_excerpts(
3606 rust_buffer.clone(),
3607 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3608 cx,
3609 );
3610 multibuffer
3611 });
3612
3613 cx.add_window(|window, cx| {
3614 let mut editor = build_editor(multibuffer, window, cx);
3615
3616 assert_eq!(
3617 editor.text(cx),
3618 indoc! {"
3619 a = 1
3620 b = 2
3621
3622 const c: usize = 3;
3623 "}
3624 );
3625
3626 select_ranges(
3627 &mut editor,
3628 indoc! {"
3629 «aˇ» = 1
3630 b = 2
3631
3632 «const c:ˇ» usize = 3;
3633 "},
3634 window,
3635 cx,
3636 );
3637
3638 editor.tab(&Tab, window, cx);
3639 assert_text_with_selections(
3640 &mut editor,
3641 indoc! {"
3642 «aˇ» = 1
3643 b = 2
3644
3645 «const c:ˇ» usize = 3;
3646 "},
3647 cx,
3648 );
3649 editor.backtab(&Backtab, window, cx);
3650 assert_text_with_selections(
3651 &mut editor,
3652 indoc! {"
3653 «aˇ» = 1
3654 b = 2
3655
3656 «const c:ˇ» usize = 3;
3657 "},
3658 cx,
3659 );
3660
3661 editor
3662 });
3663}
3664
3665#[gpui::test]
3666async fn test_backspace(cx: &mut TestAppContext) {
3667 init_test(cx, |_| {});
3668
3669 let mut cx = EditorTestContext::new(cx).await;
3670
3671 // Basic backspace
3672 cx.set_state(indoc! {"
3673 onˇe two three
3674 fou«rˇ» five six
3675 seven «ˇeight nine
3676 »ten
3677 "});
3678 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3679 cx.assert_editor_state(indoc! {"
3680 oˇe two three
3681 fouˇ five six
3682 seven ˇten
3683 "});
3684
3685 // Test backspace inside and around indents
3686 cx.set_state(indoc! {"
3687 zero
3688 ˇone
3689 ˇtwo
3690 ˇ ˇ ˇ three
3691 ˇ ˇ four
3692 "});
3693 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3694 cx.assert_editor_state(indoc! {"
3695 zero
3696 ˇone
3697 ˇtwo
3698 ˇ threeˇ four
3699 "});
3700}
3701
3702#[gpui::test]
3703async fn test_delete(cx: &mut TestAppContext) {
3704 init_test(cx, |_| {});
3705
3706 let mut cx = EditorTestContext::new(cx).await;
3707 cx.set_state(indoc! {"
3708 onˇe two three
3709 fou«rˇ» five six
3710 seven «ˇeight nine
3711 »ten
3712 "});
3713 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3714 cx.assert_editor_state(indoc! {"
3715 onˇ two three
3716 fouˇ five six
3717 seven ˇten
3718 "});
3719}
3720
3721#[gpui::test]
3722fn test_delete_line(cx: &mut TestAppContext) {
3723 init_test(cx, |_| {});
3724
3725 let editor = cx.add_window(|window, cx| {
3726 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3727 build_editor(buffer, window, cx)
3728 });
3729 _ = editor.update(cx, |editor, window, cx| {
3730 editor.change_selections(None, window, cx, |s| {
3731 s.select_display_ranges([
3732 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3733 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3734 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3735 ])
3736 });
3737 editor.delete_line(&DeleteLine, window, cx);
3738 assert_eq!(editor.display_text(cx), "ghi");
3739 assert_eq!(
3740 editor.selections.display_ranges(cx),
3741 vec![
3742 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3743 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3744 ]
3745 );
3746 });
3747
3748 let editor = cx.add_window(|window, cx| {
3749 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3750 build_editor(buffer, window, cx)
3751 });
3752 _ = editor.update(cx, |editor, window, cx| {
3753 editor.change_selections(None, window, cx, |s| {
3754 s.select_display_ranges([
3755 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3756 ])
3757 });
3758 editor.delete_line(&DeleteLine, window, cx);
3759 assert_eq!(editor.display_text(cx), "ghi\n");
3760 assert_eq!(
3761 editor.selections.display_ranges(cx),
3762 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3763 );
3764 });
3765}
3766
3767#[gpui::test]
3768fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3769 init_test(cx, |_| {});
3770
3771 cx.add_window(|window, cx| {
3772 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3773 let mut editor = build_editor(buffer.clone(), window, cx);
3774 let buffer = buffer.read(cx).as_singleton().unwrap();
3775
3776 assert_eq!(
3777 editor.selections.ranges::<Point>(cx),
3778 &[Point::new(0, 0)..Point::new(0, 0)]
3779 );
3780
3781 // When on single line, replace newline at end by space
3782 editor.join_lines(&JoinLines, window, cx);
3783 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3784 assert_eq!(
3785 editor.selections.ranges::<Point>(cx),
3786 &[Point::new(0, 3)..Point::new(0, 3)]
3787 );
3788
3789 // When multiple lines are selected, remove newlines that are spanned by the selection
3790 editor.change_selections(None, window, cx, |s| {
3791 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3792 });
3793 editor.join_lines(&JoinLines, window, cx);
3794 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3795 assert_eq!(
3796 editor.selections.ranges::<Point>(cx),
3797 &[Point::new(0, 11)..Point::new(0, 11)]
3798 );
3799
3800 // Undo should be transactional
3801 editor.undo(&Undo, window, cx);
3802 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3803 assert_eq!(
3804 editor.selections.ranges::<Point>(cx),
3805 &[Point::new(0, 5)..Point::new(2, 2)]
3806 );
3807
3808 // When joining an empty line don't insert a space
3809 editor.change_selections(None, window, cx, |s| {
3810 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3811 });
3812 editor.join_lines(&JoinLines, window, cx);
3813 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3814 assert_eq!(
3815 editor.selections.ranges::<Point>(cx),
3816 [Point::new(2, 3)..Point::new(2, 3)]
3817 );
3818
3819 // We can remove trailing newlines
3820 editor.join_lines(&JoinLines, window, cx);
3821 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3822 assert_eq!(
3823 editor.selections.ranges::<Point>(cx),
3824 [Point::new(2, 3)..Point::new(2, 3)]
3825 );
3826
3827 // We don't blow up on the last line
3828 editor.join_lines(&JoinLines, window, cx);
3829 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3830 assert_eq!(
3831 editor.selections.ranges::<Point>(cx),
3832 [Point::new(2, 3)..Point::new(2, 3)]
3833 );
3834
3835 // reset to test indentation
3836 editor.buffer.update(cx, |buffer, cx| {
3837 buffer.edit(
3838 [
3839 (Point::new(1, 0)..Point::new(1, 2), " "),
3840 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3841 ],
3842 None,
3843 cx,
3844 )
3845 });
3846
3847 // We remove any leading spaces
3848 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3849 editor.change_selections(None, window, cx, |s| {
3850 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3851 });
3852 editor.join_lines(&JoinLines, window, cx);
3853 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3854
3855 // We don't insert a space for a line containing only spaces
3856 editor.join_lines(&JoinLines, window, cx);
3857 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3858
3859 // We ignore any leading tabs
3860 editor.join_lines(&JoinLines, window, cx);
3861 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3862
3863 editor
3864 });
3865}
3866
3867#[gpui::test]
3868fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3869 init_test(cx, |_| {});
3870
3871 cx.add_window(|window, cx| {
3872 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3873 let mut editor = build_editor(buffer.clone(), window, cx);
3874 let buffer = buffer.read(cx).as_singleton().unwrap();
3875
3876 editor.change_selections(None, window, cx, |s| {
3877 s.select_ranges([
3878 Point::new(0, 2)..Point::new(1, 1),
3879 Point::new(1, 2)..Point::new(1, 2),
3880 Point::new(3, 1)..Point::new(3, 2),
3881 ])
3882 });
3883
3884 editor.join_lines(&JoinLines, window, cx);
3885 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3886
3887 assert_eq!(
3888 editor.selections.ranges::<Point>(cx),
3889 [
3890 Point::new(0, 7)..Point::new(0, 7),
3891 Point::new(1, 3)..Point::new(1, 3)
3892 ]
3893 );
3894 editor
3895 });
3896}
3897
3898#[gpui::test]
3899async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3900 init_test(cx, |_| {});
3901
3902 let mut cx = EditorTestContext::new(cx).await;
3903
3904 let diff_base = r#"
3905 Line 0
3906 Line 1
3907 Line 2
3908 Line 3
3909 "#
3910 .unindent();
3911
3912 cx.set_state(
3913 &r#"
3914 ˇLine 0
3915 Line 1
3916 Line 2
3917 Line 3
3918 "#
3919 .unindent(),
3920 );
3921
3922 cx.set_head_text(&diff_base);
3923 executor.run_until_parked();
3924
3925 // Join lines
3926 cx.update_editor(|editor, window, cx| {
3927 editor.join_lines(&JoinLines, window, cx);
3928 });
3929 executor.run_until_parked();
3930
3931 cx.assert_editor_state(
3932 &r#"
3933 Line 0ˇ Line 1
3934 Line 2
3935 Line 3
3936 "#
3937 .unindent(),
3938 );
3939 // Join again
3940 cx.update_editor(|editor, window, cx| {
3941 editor.join_lines(&JoinLines, window, cx);
3942 });
3943 executor.run_until_parked();
3944
3945 cx.assert_editor_state(
3946 &r#"
3947 Line 0 Line 1ˇ Line 2
3948 Line 3
3949 "#
3950 .unindent(),
3951 );
3952}
3953
3954#[gpui::test]
3955async fn test_custom_newlines_cause_no_false_positive_diffs(
3956 executor: BackgroundExecutor,
3957 cx: &mut TestAppContext,
3958) {
3959 init_test(cx, |_| {});
3960 let mut cx = EditorTestContext::new(cx).await;
3961 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3962 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3963 executor.run_until_parked();
3964
3965 cx.update_editor(|editor, window, cx| {
3966 let snapshot = editor.snapshot(window, cx);
3967 assert_eq!(
3968 snapshot
3969 .buffer_snapshot
3970 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3971 .collect::<Vec<_>>(),
3972 Vec::new(),
3973 "Should not have any diffs for files with custom newlines"
3974 );
3975 });
3976}
3977
3978#[gpui::test]
3979async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3980 init_test(cx, |_| {});
3981
3982 let mut cx = EditorTestContext::new(cx).await;
3983
3984 // Test sort_lines_case_insensitive()
3985 cx.set_state(indoc! {"
3986 «z
3987 y
3988 x
3989 Z
3990 Y
3991 Xˇ»
3992 "});
3993 cx.update_editor(|e, window, cx| {
3994 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3995 });
3996 cx.assert_editor_state(indoc! {"
3997 «x
3998 X
3999 y
4000 Y
4001 z
4002 Zˇ»
4003 "});
4004
4005 // Test reverse_lines()
4006 cx.set_state(indoc! {"
4007 «5
4008 4
4009 3
4010 2
4011 1ˇ»
4012 "});
4013 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4014 cx.assert_editor_state(indoc! {"
4015 «1
4016 2
4017 3
4018 4
4019 5ˇ»
4020 "});
4021
4022 // Skip testing shuffle_line()
4023
4024 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
4025 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
4026
4027 // Don't manipulate when cursor is on single line, but expand the selection
4028 cx.set_state(indoc! {"
4029 ddˇdd
4030 ccc
4031 bb
4032 a
4033 "});
4034 cx.update_editor(|e, window, cx| {
4035 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4036 });
4037 cx.assert_editor_state(indoc! {"
4038 «ddddˇ»
4039 ccc
4040 bb
4041 a
4042 "});
4043
4044 // Basic manipulate case
4045 // Start selection moves to column 0
4046 // End of selection shrinks to fit shorter line
4047 cx.set_state(indoc! {"
4048 dd«d
4049 ccc
4050 bb
4051 aaaaaˇ»
4052 "});
4053 cx.update_editor(|e, window, cx| {
4054 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4055 });
4056 cx.assert_editor_state(indoc! {"
4057 «aaaaa
4058 bb
4059 ccc
4060 dddˇ»
4061 "});
4062
4063 // Manipulate case with newlines
4064 cx.set_state(indoc! {"
4065 dd«d
4066 ccc
4067
4068 bb
4069 aaaaa
4070
4071 ˇ»
4072 "});
4073 cx.update_editor(|e, window, cx| {
4074 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4075 });
4076 cx.assert_editor_state(indoc! {"
4077 «
4078
4079 aaaaa
4080 bb
4081 ccc
4082 dddˇ»
4083
4084 "});
4085
4086 // Adding new line
4087 cx.set_state(indoc! {"
4088 aa«a
4089 bbˇ»b
4090 "});
4091 cx.update_editor(|e, window, cx| {
4092 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
4093 });
4094 cx.assert_editor_state(indoc! {"
4095 «aaa
4096 bbb
4097 added_lineˇ»
4098 "});
4099
4100 // Removing line
4101 cx.set_state(indoc! {"
4102 aa«a
4103 bbbˇ»
4104 "});
4105 cx.update_editor(|e, window, cx| {
4106 e.manipulate_lines(window, cx, |lines| {
4107 lines.pop();
4108 })
4109 });
4110 cx.assert_editor_state(indoc! {"
4111 «aaaˇ»
4112 "});
4113
4114 // Removing all lines
4115 cx.set_state(indoc! {"
4116 aa«a
4117 bbbˇ»
4118 "});
4119 cx.update_editor(|e, window, cx| {
4120 e.manipulate_lines(window, cx, |lines| {
4121 lines.drain(..);
4122 })
4123 });
4124 cx.assert_editor_state(indoc! {"
4125 ˇ
4126 "});
4127}
4128
4129#[gpui::test]
4130async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4131 init_test(cx, |_| {});
4132
4133 let mut cx = EditorTestContext::new(cx).await;
4134
4135 // Consider continuous selection as single selection
4136 cx.set_state(indoc! {"
4137 Aaa«aa
4138 cˇ»c«c
4139 bb
4140 aaaˇ»aa
4141 "});
4142 cx.update_editor(|e, window, cx| {
4143 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4144 });
4145 cx.assert_editor_state(indoc! {"
4146 «Aaaaa
4147 ccc
4148 bb
4149 aaaaaˇ»
4150 "});
4151
4152 cx.set_state(indoc! {"
4153 Aaa«aa
4154 cˇ»c«c
4155 bb
4156 aaaˇ»aa
4157 "});
4158 cx.update_editor(|e, window, cx| {
4159 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4160 });
4161 cx.assert_editor_state(indoc! {"
4162 «Aaaaa
4163 ccc
4164 bbˇ»
4165 "});
4166
4167 // Consider non continuous selection as distinct dedup operations
4168 cx.set_state(indoc! {"
4169 «aaaaa
4170 bb
4171 aaaaa
4172 aaaaaˇ»
4173
4174 aaa«aaˇ»
4175 "});
4176 cx.update_editor(|e, window, cx| {
4177 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4178 });
4179 cx.assert_editor_state(indoc! {"
4180 «aaaaa
4181 bbˇ»
4182
4183 «aaaaaˇ»
4184 "});
4185}
4186
4187#[gpui::test]
4188async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4189 init_test(cx, |_| {});
4190
4191 let mut cx = EditorTestContext::new(cx).await;
4192
4193 cx.set_state(indoc! {"
4194 «Aaa
4195 aAa
4196 Aaaˇ»
4197 "});
4198 cx.update_editor(|e, window, cx| {
4199 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4200 });
4201 cx.assert_editor_state(indoc! {"
4202 «Aaa
4203 aAaˇ»
4204 "});
4205
4206 cx.set_state(indoc! {"
4207 «Aaa
4208 aAa
4209 aaAˇ»
4210 "});
4211 cx.update_editor(|e, window, cx| {
4212 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4213 });
4214 cx.assert_editor_state(indoc! {"
4215 «Aaaˇ»
4216 "});
4217}
4218
4219#[gpui::test]
4220async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
4221 init_test(cx, |_| {});
4222
4223 let mut cx = EditorTestContext::new(cx).await;
4224
4225 // Manipulate with multiple selections on a single line
4226 cx.set_state(indoc! {"
4227 dd«dd
4228 cˇ»c«c
4229 bb
4230 aaaˇ»aa
4231 "});
4232 cx.update_editor(|e, window, cx| {
4233 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4234 });
4235 cx.assert_editor_state(indoc! {"
4236 «aaaaa
4237 bb
4238 ccc
4239 ddddˇ»
4240 "});
4241
4242 // Manipulate with multiple disjoin selections
4243 cx.set_state(indoc! {"
4244 5«
4245 4
4246 3
4247 2
4248 1ˇ»
4249
4250 dd«dd
4251 ccc
4252 bb
4253 aaaˇ»aa
4254 "});
4255 cx.update_editor(|e, window, cx| {
4256 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4257 });
4258 cx.assert_editor_state(indoc! {"
4259 «1
4260 2
4261 3
4262 4
4263 5ˇ»
4264
4265 «aaaaa
4266 bb
4267 ccc
4268 ddddˇ»
4269 "});
4270
4271 // Adding lines on each selection
4272 cx.set_state(indoc! {"
4273 2«
4274 1ˇ»
4275
4276 bb«bb
4277 aaaˇ»aa
4278 "});
4279 cx.update_editor(|e, window, cx| {
4280 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
4281 });
4282 cx.assert_editor_state(indoc! {"
4283 «2
4284 1
4285 added lineˇ»
4286
4287 «bbbb
4288 aaaaa
4289 added lineˇ»
4290 "});
4291
4292 // Removing lines on each selection
4293 cx.set_state(indoc! {"
4294 2«
4295 1ˇ»
4296
4297 bb«bb
4298 aaaˇ»aa
4299 "});
4300 cx.update_editor(|e, window, cx| {
4301 e.manipulate_lines(window, cx, |lines| {
4302 lines.pop();
4303 })
4304 });
4305 cx.assert_editor_state(indoc! {"
4306 «2ˇ»
4307
4308 «bbbbˇ»
4309 "});
4310}
4311
4312#[gpui::test]
4313async fn test_toggle_case(cx: &mut TestAppContext) {
4314 init_test(cx, |_| {});
4315
4316 let mut cx = EditorTestContext::new(cx).await;
4317
4318 // If all lower case -> upper case
4319 cx.set_state(indoc! {"
4320 «hello worldˇ»
4321 "});
4322 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4323 cx.assert_editor_state(indoc! {"
4324 «HELLO WORLDˇ»
4325 "});
4326
4327 // If all upper case -> lower case
4328 cx.set_state(indoc! {"
4329 «HELLO WORLDˇ»
4330 "});
4331 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4332 cx.assert_editor_state(indoc! {"
4333 «hello worldˇ»
4334 "});
4335
4336 // If any upper case characters are identified -> lower case
4337 // This matches JetBrains IDEs
4338 cx.set_state(indoc! {"
4339 «hEllo worldˇ»
4340 "});
4341 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4342 cx.assert_editor_state(indoc! {"
4343 «hello worldˇ»
4344 "});
4345}
4346
4347#[gpui::test]
4348async fn test_manipulate_text(cx: &mut TestAppContext) {
4349 init_test(cx, |_| {});
4350
4351 let mut cx = EditorTestContext::new(cx).await;
4352
4353 // Test convert_to_upper_case()
4354 cx.set_state(indoc! {"
4355 «hello worldˇ»
4356 "});
4357 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4358 cx.assert_editor_state(indoc! {"
4359 «HELLO WORLDˇ»
4360 "});
4361
4362 // Test convert_to_lower_case()
4363 cx.set_state(indoc! {"
4364 «HELLO WORLDˇ»
4365 "});
4366 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4367 cx.assert_editor_state(indoc! {"
4368 «hello worldˇ»
4369 "});
4370
4371 // Test multiple line, single selection case
4372 cx.set_state(indoc! {"
4373 «The quick brown
4374 fox jumps over
4375 the lazy dogˇ»
4376 "});
4377 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4378 cx.assert_editor_state(indoc! {"
4379 «The Quick Brown
4380 Fox Jumps Over
4381 The Lazy Dogˇ»
4382 "});
4383
4384 // Test multiple line, single selection case
4385 cx.set_state(indoc! {"
4386 «The quick brown
4387 fox jumps over
4388 the lazy dogˇ»
4389 "});
4390 cx.update_editor(|e, window, cx| {
4391 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4392 });
4393 cx.assert_editor_state(indoc! {"
4394 «TheQuickBrown
4395 FoxJumpsOver
4396 TheLazyDogˇ»
4397 "});
4398
4399 // From here on out, test more complex cases of manipulate_text()
4400
4401 // Test no selection case - should affect words cursors are in
4402 // Cursor at beginning, middle, and end of word
4403 cx.set_state(indoc! {"
4404 ˇhello big beauˇtiful worldˇ
4405 "});
4406 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4407 cx.assert_editor_state(indoc! {"
4408 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4409 "});
4410
4411 // Test multiple selections on a single line and across multiple lines
4412 cx.set_state(indoc! {"
4413 «Theˇ» quick «brown
4414 foxˇ» jumps «overˇ»
4415 the «lazyˇ» dog
4416 "});
4417 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4418 cx.assert_editor_state(indoc! {"
4419 «THEˇ» quick «BROWN
4420 FOXˇ» jumps «OVERˇ»
4421 the «LAZYˇ» dog
4422 "});
4423
4424 // Test case where text length grows
4425 cx.set_state(indoc! {"
4426 «tschüߡ»
4427 "});
4428 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4429 cx.assert_editor_state(indoc! {"
4430 «TSCHÜSSˇ»
4431 "});
4432
4433 // Test to make sure we don't crash when text shrinks
4434 cx.set_state(indoc! {"
4435 aaa_bbbˇ
4436 "});
4437 cx.update_editor(|e, window, cx| {
4438 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4439 });
4440 cx.assert_editor_state(indoc! {"
4441 «aaaBbbˇ»
4442 "});
4443
4444 // Test to make sure we all aware of the fact that each word can grow and shrink
4445 // Final selections should be aware of this fact
4446 cx.set_state(indoc! {"
4447 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4448 "});
4449 cx.update_editor(|e, window, cx| {
4450 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4451 });
4452 cx.assert_editor_state(indoc! {"
4453 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4454 "});
4455
4456 cx.set_state(indoc! {"
4457 «hElLo, WoRld!ˇ»
4458 "});
4459 cx.update_editor(|e, window, cx| {
4460 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4461 });
4462 cx.assert_editor_state(indoc! {"
4463 «HeLlO, wOrLD!ˇ»
4464 "});
4465}
4466
4467#[gpui::test]
4468fn test_duplicate_line(cx: &mut TestAppContext) {
4469 init_test(cx, |_| {});
4470
4471 let editor = cx.add_window(|window, cx| {
4472 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4473 build_editor(buffer, window, cx)
4474 });
4475 _ = editor.update(cx, |editor, window, cx| {
4476 editor.change_selections(None, window, cx, |s| {
4477 s.select_display_ranges([
4478 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4479 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4480 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4481 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4482 ])
4483 });
4484 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4485 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4486 assert_eq!(
4487 editor.selections.display_ranges(cx),
4488 vec![
4489 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4490 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4491 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4492 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4493 ]
4494 );
4495 });
4496
4497 let editor = cx.add_window(|window, cx| {
4498 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4499 build_editor(buffer, window, cx)
4500 });
4501 _ = editor.update(cx, |editor, window, cx| {
4502 editor.change_selections(None, window, cx, |s| {
4503 s.select_display_ranges([
4504 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4505 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4506 ])
4507 });
4508 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4509 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4510 assert_eq!(
4511 editor.selections.display_ranges(cx),
4512 vec![
4513 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4514 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4515 ]
4516 );
4517 });
4518
4519 // With `move_upwards` the selections stay in place, except for
4520 // the lines inserted above them
4521 let editor = cx.add_window(|window, cx| {
4522 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4523 build_editor(buffer, window, cx)
4524 });
4525 _ = editor.update(cx, |editor, window, cx| {
4526 editor.change_selections(None, window, cx, |s| {
4527 s.select_display_ranges([
4528 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4529 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4530 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4531 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4532 ])
4533 });
4534 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4535 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4536 assert_eq!(
4537 editor.selections.display_ranges(cx),
4538 vec![
4539 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4540 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4541 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4542 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4543 ]
4544 );
4545 });
4546
4547 let editor = cx.add_window(|window, cx| {
4548 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4549 build_editor(buffer, window, cx)
4550 });
4551 _ = editor.update(cx, |editor, window, cx| {
4552 editor.change_selections(None, window, cx, |s| {
4553 s.select_display_ranges([
4554 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4555 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4556 ])
4557 });
4558 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4559 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4560 assert_eq!(
4561 editor.selections.display_ranges(cx),
4562 vec![
4563 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4564 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4565 ]
4566 );
4567 });
4568
4569 let editor = cx.add_window(|window, cx| {
4570 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4571 build_editor(buffer, window, cx)
4572 });
4573 _ = editor.update(cx, |editor, window, cx| {
4574 editor.change_selections(None, window, cx, |s| {
4575 s.select_display_ranges([
4576 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4577 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4578 ])
4579 });
4580 editor.duplicate_selection(&DuplicateSelection, window, cx);
4581 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4582 assert_eq!(
4583 editor.selections.display_ranges(cx),
4584 vec![
4585 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4586 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4587 ]
4588 );
4589 });
4590}
4591
4592#[gpui::test]
4593fn test_move_line_up_down(cx: &mut TestAppContext) {
4594 init_test(cx, |_| {});
4595
4596 let editor = cx.add_window(|window, cx| {
4597 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4598 build_editor(buffer, window, cx)
4599 });
4600 _ = editor.update(cx, |editor, window, cx| {
4601 editor.fold_creases(
4602 vec![
4603 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4604 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4605 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4606 ],
4607 true,
4608 window,
4609 cx,
4610 );
4611 editor.change_selections(None, window, cx, |s| {
4612 s.select_display_ranges([
4613 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4614 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4615 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4616 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4617 ])
4618 });
4619 assert_eq!(
4620 editor.display_text(cx),
4621 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4622 );
4623
4624 editor.move_line_up(&MoveLineUp, window, cx);
4625 assert_eq!(
4626 editor.display_text(cx),
4627 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4628 );
4629 assert_eq!(
4630 editor.selections.display_ranges(cx),
4631 vec![
4632 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4633 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4634 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4635 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4636 ]
4637 );
4638 });
4639
4640 _ = editor.update(cx, |editor, window, cx| {
4641 editor.move_line_down(&MoveLineDown, window, cx);
4642 assert_eq!(
4643 editor.display_text(cx),
4644 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4645 );
4646 assert_eq!(
4647 editor.selections.display_ranges(cx),
4648 vec![
4649 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4650 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4651 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4652 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4653 ]
4654 );
4655 });
4656
4657 _ = editor.update(cx, |editor, window, cx| {
4658 editor.move_line_down(&MoveLineDown, window, cx);
4659 assert_eq!(
4660 editor.display_text(cx),
4661 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4662 );
4663 assert_eq!(
4664 editor.selections.display_ranges(cx),
4665 vec![
4666 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4667 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4668 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4669 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4670 ]
4671 );
4672 });
4673
4674 _ = editor.update(cx, |editor, window, cx| {
4675 editor.move_line_up(&MoveLineUp, window, cx);
4676 assert_eq!(
4677 editor.display_text(cx),
4678 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4679 );
4680 assert_eq!(
4681 editor.selections.display_ranges(cx),
4682 vec![
4683 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4684 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4685 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4686 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4687 ]
4688 );
4689 });
4690}
4691
4692#[gpui::test]
4693fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4694 init_test(cx, |_| {});
4695
4696 let editor = cx.add_window(|window, cx| {
4697 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4698 build_editor(buffer, window, cx)
4699 });
4700 _ = editor.update(cx, |editor, window, cx| {
4701 let snapshot = editor.buffer.read(cx).snapshot(cx);
4702 editor.insert_blocks(
4703 [BlockProperties {
4704 style: BlockStyle::Fixed,
4705 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4706 height: Some(1),
4707 render: Arc::new(|_| div().into_any()),
4708 priority: 0,
4709 render_in_minimap: true,
4710 }],
4711 Some(Autoscroll::fit()),
4712 cx,
4713 );
4714 editor.change_selections(None, window, cx, |s| {
4715 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4716 });
4717 editor.move_line_down(&MoveLineDown, window, cx);
4718 });
4719}
4720
4721#[gpui::test]
4722async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4723 init_test(cx, |_| {});
4724
4725 let mut cx = EditorTestContext::new(cx).await;
4726 cx.set_state(
4727 &"
4728 ˇzero
4729 one
4730 two
4731 three
4732 four
4733 five
4734 "
4735 .unindent(),
4736 );
4737
4738 // Create a four-line block that replaces three lines of text.
4739 cx.update_editor(|editor, window, cx| {
4740 let snapshot = editor.snapshot(window, cx);
4741 let snapshot = &snapshot.buffer_snapshot;
4742 let placement = BlockPlacement::Replace(
4743 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4744 );
4745 editor.insert_blocks(
4746 [BlockProperties {
4747 placement,
4748 height: Some(4),
4749 style: BlockStyle::Sticky,
4750 render: Arc::new(|_| gpui::div().into_any_element()),
4751 priority: 0,
4752 render_in_minimap: true,
4753 }],
4754 None,
4755 cx,
4756 );
4757 });
4758
4759 // Move down so that the cursor touches the block.
4760 cx.update_editor(|editor, window, cx| {
4761 editor.move_down(&Default::default(), window, cx);
4762 });
4763 cx.assert_editor_state(
4764 &"
4765 zero
4766 «one
4767 two
4768 threeˇ»
4769 four
4770 five
4771 "
4772 .unindent(),
4773 );
4774
4775 // Move down past the block.
4776 cx.update_editor(|editor, window, cx| {
4777 editor.move_down(&Default::default(), window, cx);
4778 });
4779 cx.assert_editor_state(
4780 &"
4781 zero
4782 one
4783 two
4784 three
4785 ˇfour
4786 five
4787 "
4788 .unindent(),
4789 );
4790}
4791
4792#[gpui::test]
4793fn test_transpose(cx: &mut TestAppContext) {
4794 init_test(cx, |_| {});
4795
4796 _ = cx.add_window(|window, cx| {
4797 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4798 editor.set_style(EditorStyle::default(), window, cx);
4799 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4800 editor.transpose(&Default::default(), window, cx);
4801 assert_eq!(editor.text(cx), "bac");
4802 assert_eq!(editor.selections.ranges(cx), [2..2]);
4803
4804 editor.transpose(&Default::default(), window, cx);
4805 assert_eq!(editor.text(cx), "bca");
4806 assert_eq!(editor.selections.ranges(cx), [3..3]);
4807
4808 editor.transpose(&Default::default(), window, cx);
4809 assert_eq!(editor.text(cx), "bac");
4810 assert_eq!(editor.selections.ranges(cx), [3..3]);
4811
4812 editor
4813 });
4814
4815 _ = cx.add_window(|window, cx| {
4816 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4817 editor.set_style(EditorStyle::default(), window, cx);
4818 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4819 editor.transpose(&Default::default(), window, cx);
4820 assert_eq!(editor.text(cx), "acb\nde");
4821 assert_eq!(editor.selections.ranges(cx), [3..3]);
4822
4823 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4824 editor.transpose(&Default::default(), window, cx);
4825 assert_eq!(editor.text(cx), "acbd\ne");
4826 assert_eq!(editor.selections.ranges(cx), [5..5]);
4827
4828 editor.transpose(&Default::default(), window, cx);
4829 assert_eq!(editor.text(cx), "acbde\n");
4830 assert_eq!(editor.selections.ranges(cx), [6..6]);
4831
4832 editor.transpose(&Default::default(), window, cx);
4833 assert_eq!(editor.text(cx), "acbd\ne");
4834 assert_eq!(editor.selections.ranges(cx), [6..6]);
4835
4836 editor
4837 });
4838
4839 _ = cx.add_window(|window, cx| {
4840 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4841 editor.set_style(EditorStyle::default(), window, cx);
4842 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4843 editor.transpose(&Default::default(), window, cx);
4844 assert_eq!(editor.text(cx), "bacd\ne");
4845 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4846
4847 editor.transpose(&Default::default(), window, cx);
4848 assert_eq!(editor.text(cx), "bcade\n");
4849 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4850
4851 editor.transpose(&Default::default(), window, cx);
4852 assert_eq!(editor.text(cx), "bcda\ne");
4853 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4854
4855 editor.transpose(&Default::default(), window, cx);
4856 assert_eq!(editor.text(cx), "bcade\n");
4857 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4858
4859 editor.transpose(&Default::default(), window, cx);
4860 assert_eq!(editor.text(cx), "bcaed\n");
4861 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4862
4863 editor
4864 });
4865
4866 _ = cx.add_window(|window, cx| {
4867 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4868 editor.set_style(EditorStyle::default(), window, cx);
4869 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4870 editor.transpose(&Default::default(), window, cx);
4871 assert_eq!(editor.text(cx), "🏀🍐✋");
4872 assert_eq!(editor.selections.ranges(cx), [8..8]);
4873
4874 editor.transpose(&Default::default(), window, cx);
4875 assert_eq!(editor.text(cx), "🏀✋🍐");
4876 assert_eq!(editor.selections.ranges(cx), [11..11]);
4877
4878 editor.transpose(&Default::default(), window, cx);
4879 assert_eq!(editor.text(cx), "🏀🍐✋");
4880 assert_eq!(editor.selections.ranges(cx), [11..11]);
4881
4882 editor
4883 });
4884}
4885
4886#[gpui::test]
4887async fn test_rewrap(cx: &mut TestAppContext) {
4888 init_test(cx, |settings| {
4889 settings.languages.extend([
4890 (
4891 "Markdown".into(),
4892 LanguageSettingsContent {
4893 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4894 ..Default::default()
4895 },
4896 ),
4897 (
4898 "Plain Text".into(),
4899 LanguageSettingsContent {
4900 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4901 ..Default::default()
4902 },
4903 ),
4904 ])
4905 });
4906
4907 let mut cx = EditorTestContext::new(cx).await;
4908
4909 let language_with_c_comments = Arc::new(Language::new(
4910 LanguageConfig {
4911 line_comments: vec!["// ".into()],
4912 ..LanguageConfig::default()
4913 },
4914 None,
4915 ));
4916 let language_with_pound_comments = Arc::new(Language::new(
4917 LanguageConfig {
4918 line_comments: vec!["# ".into()],
4919 ..LanguageConfig::default()
4920 },
4921 None,
4922 ));
4923 let markdown_language = Arc::new(Language::new(
4924 LanguageConfig {
4925 name: "Markdown".into(),
4926 ..LanguageConfig::default()
4927 },
4928 None,
4929 ));
4930 let language_with_doc_comments = Arc::new(Language::new(
4931 LanguageConfig {
4932 line_comments: vec!["// ".into(), "/// ".into()],
4933 ..LanguageConfig::default()
4934 },
4935 Some(tree_sitter_rust::LANGUAGE.into()),
4936 ));
4937
4938 let plaintext_language = Arc::new(Language::new(
4939 LanguageConfig {
4940 name: "Plain Text".into(),
4941 ..LanguageConfig::default()
4942 },
4943 None,
4944 ));
4945
4946 assert_rewrap(
4947 indoc! {"
4948 // ˇ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.
4949 "},
4950 indoc! {"
4951 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4952 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4953 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4954 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4955 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4956 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4957 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4958 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4959 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4960 // porttitor id. Aliquam id accumsan eros.
4961 "},
4962 language_with_c_comments.clone(),
4963 &mut cx,
4964 );
4965
4966 // Test that rewrapping works inside of a selection
4967 assert_rewrap(
4968 indoc! {"
4969 «// 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.ˇ»
4970 "},
4971 indoc! {"
4972 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4973 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4974 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4975 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4976 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4977 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4978 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4979 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4980 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4981 // porttitor id. Aliquam id accumsan eros.ˇ»
4982 "},
4983 language_with_c_comments.clone(),
4984 &mut cx,
4985 );
4986
4987 // Test that cursors that expand to the same region are collapsed.
4988 assert_rewrap(
4989 indoc! {"
4990 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4991 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4992 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4993 // ˇ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.
4994 "},
4995 indoc! {"
4996 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4997 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4998 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
4999 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
5000 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
5001 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
5002 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
5003 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
5004 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
5005 // porttitor id. Aliquam id accumsan eros.
5006 "},
5007 language_with_c_comments.clone(),
5008 &mut cx,
5009 );
5010
5011 // Test that non-contiguous selections are treated separately.
5012 assert_rewrap(
5013 indoc! {"
5014 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
5015 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
5016 //
5017 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
5018 // ˇ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.
5019 "},
5020 indoc! {"
5021 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
5022 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
5023 // auctor, eu lacinia sapien scelerisque.
5024 //
5025 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
5026 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
5027 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
5028 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
5029 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
5030 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
5031 // vulputate turpis porttitor id. Aliquam id accumsan eros.
5032 "},
5033 language_with_c_comments.clone(),
5034 &mut cx,
5035 );
5036
5037 // Test that different comment prefixes are supported.
5038 assert_rewrap(
5039 indoc! {"
5040 # ˇ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.
5041 "},
5042 indoc! {"
5043 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
5044 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5045 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5046 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5047 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
5048 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
5049 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
5050 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
5051 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
5052 # accumsan eros.
5053 "},
5054 language_with_pound_comments.clone(),
5055 &mut cx,
5056 );
5057
5058 // Test that rewrapping is ignored outside of comments in most languages.
5059 assert_rewrap(
5060 indoc! {"
5061 /// Adds two numbers.
5062 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
5063 fn add(a: u32, b: u32) -> u32 {
5064 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ˇ
5065 }
5066 "},
5067 indoc! {"
5068 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
5069 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
5070 fn add(a: u32, b: u32) -> u32 {
5071 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ˇ
5072 }
5073 "},
5074 language_with_doc_comments.clone(),
5075 &mut cx,
5076 );
5077
5078 // Test that rewrapping works in Markdown and Plain Text languages.
5079 assert_rewrap(
5080 indoc! {"
5081 # Hello
5082
5083 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.
5084 "},
5085 indoc! {"
5086 # Hello
5087
5088 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
5089 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5090 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5091 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5092 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
5093 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
5094 Integer sit amet scelerisque nisi.
5095 "},
5096 markdown_language,
5097 &mut cx,
5098 );
5099
5100 assert_rewrap(
5101 indoc! {"
5102 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.
5103 "},
5104 indoc! {"
5105 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
5106 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5107 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5108 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5109 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
5110 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
5111 Integer sit amet scelerisque nisi.
5112 "},
5113 plaintext_language.clone(),
5114 &mut cx,
5115 );
5116
5117 // Test rewrapping unaligned comments in a selection.
5118 assert_rewrap(
5119 indoc! {"
5120 fn foo() {
5121 if true {
5122 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
5123 // Praesent semper egestas tellus id dignissim.ˇ»
5124 do_something();
5125 } else {
5126 //
5127 }
5128 }
5129 "},
5130 indoc! {"
5131 fn foo() {
5132 if true {
5133 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
5134 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
5135 // egestas tellus id dignissim.ˇ»
5136 do_something();
5137 } else {
5138 //
5139 }
5140 }
5141 "},
5142 language_with_doc_comments.clone(),
5143 &mut cx,
5144 );
5145
5146 assert_rewrap(
5147 indoc! {"
5148 fn foo() {
5149 if true {
5150 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
5151 // Praesent semper egestas tellus id dignissim.»
5152 do_something();
5153 } else {
5154 //
5155 }
5156
5157 }
5158 "},
5159 indoc! {"
5160 fn foo() {
5161 if true {
5162 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
5163 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
5164 // egestas tellus id dignissim.»
5165 do_something();
5166 } else {
5167 //
5168 }
5169
5170 }
5171 "},
5172 language_with_doc_comments.clone(),
5173 &mut cx,
5174 );
5175
5176 assert_rewrap(
5177 indoc! {"
5178 «ˇ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
5179
5180 two»
5181
5182 three
5183
5184 «ˇ\t
5185
5186 four four four four four four four four four four four four four four four four four four four four»
5187
5188 «ˇfive five five five five five five five five five five five five five five five five five five five
5189 \t»
5190 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
5191 "},
5192 indoc! {"
5193 «ˇone one one one one one one one one one one one one one one one one one one one
5194 one one one one one
5195
5196 two»
5197
5198 three
5199
5200 «ˇ\t
5201
5202 four four four four four four four four four four four four four four four four
5203 four four four four»
5204
5205 «ˇfive five five five five five five five five five five five five five five five
5206 five five five five
5207 \t»
5208 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
5209 "},
5210 plaintext_language.clone(),
5211 &mut cx,
5212 );
5213
5214 assert_rewrap(
5215 indoc! {"
5216 //ˇ 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
5217 //ˇ
5218 //ˇ 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
5219 //ˇ short short short
5220 int main(void) {
5221 return 17;
5222 }
5223 "},
5224 indoc! {"
5225 //ˇ long long long long long long long long long long long long long long long
5226 // long long long long long long long long long long long long long
5227 //ˇ
5228 //ˇ long long long long long long long long long long long long long long long
5229 //ˇ long long long long long long long long long long long long long short short
5230 // short
5231 int main(void) {
5232 return 17;
5233 }
5234 "},
5235 language_with_c_comments,
5236 &mut cx,
5237 );
5238
5239 #[track_caller]
5240 fn assert_rewrap(
5241 unwrapped_text: &str,
5242 wrapped_text: &str,
5243 language: Arc<Language>,
5244 cx: &mut EditorTestContext,
5245 ) {
5246 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5247 cx.set_state(unwrapped_text);
5248 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5249 cx.assert_editor_state(wrapped_text);
5250 }
5251}
5252
5253#[gpui::test]
5254async fn test_hard_wrap(cx: &mut TestAppContext) {
5255 init_test(cx, |_| {});
5256 let mut cx = EditorTestContext::new(cx).await;
5257
5258 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5259 cx.update_editor(|editor, _, cx| {
5260 editor.set_hard_wrap(Some(14), cx);
5261 });
5262
5263 cx.set_state(indoc!(
5264 "
5265 one two three ˇ
5266 "
5267 ));
5268 cx.simulate_input("four");
5269 cx.run_until_parked();
5270
5271 cx.assert_editor_state(indoc!(
5272 "
5273 one two three
5274 fourˇ
5275 "
5276 ));
5277
5278 cx.update_editor(|editor, window, cx| {
5279 editor.newline(&Default::default(), window, cx);
5280 });
5281 cx.run_until_parked();
5282 cx.assert_editor_state(indoc!(
5283 "
5284 one two three
5285 four
5286 ˇ
5287 "
5288 ));
5289
5290 cx.simulate_input("five");
5291 cx.run_until_parked();
5292 cx.assert_editor_state(indoc!(
5293 "
5294 one two three
5295 four
5296 fiveˇ
5297 "
5298 ));
5299
5300 cx.update_editor(|editor, window, cx| {
5301 editor.newline(&Default::default(), window, cx);
5302 });
5303 cx.run_until_parked();
5304 cx.simulate_input("# ");
5305 cx.run_until_parked();
5306 cx.assert_editor_state(indoc!(
5307 "
5308 one two three
5309 four
5310 five
5311 # ˇ
5312 "
5313 ));
5314
5315 cx.update_editor(|editor, window, cx| {
5316 editor.newline(&Default::default(), window, cx);
5317 });
5318 cx.run_until_parked();
5319 cx.assert_editor_state(indoc!(
5320 "
5321 one two three
5322 four
5323 five
5324 #\x20
5325 #ˇ
5326 "
5327 ));
5328
5329 cx.simulate_input(" 6");
5330 cx.run_until_parked();
5331 cx.assert_editor_state(indoc!(
5332 "
5333 one two three
5334 four
5335 five
5336 #
5337 # 6ˇ
5338 "
5339 ));
5340}
5341
5342#[gpui::test]
5343async fn test_clipboard(cx: &mut TestAppContext) {
5344 init_test(cx, |_| {});
5345
5346 let mut cx = EditorTestContext::new(cx).await;
5347
5348 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5349 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5350 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5351
5352 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5353 cx.set_state("two ˇfour ˇsix ˇ");
5354 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5355 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5356
5357 // Paste again but with only two cursors. Since the number of cursors doesn't
5358 // match the number of slices in the clipboard, the entire clipboard text
5359 // is pasted at each cursor.
5360 cx.set_state("ˇtwo one✅ four three six five ˇ");
5361 cx.update_editor(|e, window, cx| {
5362 e.handle_input("( ", window, cx);
5363 e.paste(&Paste, window, cx);
5364 e.handle_input(") ", window, cx);
5365 });
5366 cx.assert_editor_state(
5367 &([
5368 "( one✅ ",
5369 "three ",
5370 "five ) ˇtwo one✅ four three six five ( one✅ ",
5371 "three ",
5372 "five ) ˇ",
5373 ]
5374 .join("\n")),
5375 );
5376
5377 // Cut with three selections, one of which is full-line.
5378 cx.set_state(indoc! {"
5379 1«2ˇ»3
5380 4ˇ567
5381 «8ˇ»9"});
5382 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5383 cx.assert_editor_state(indoc! {"
5384 1ˇ3
5385 ˇ9"});
5386
5387 // Paste with three selections, noticing how the copied selection that was full-line
5388 // gets inserted before the second cursor.
5389 cx.set_state(indoc! {"
5390 1ˇ3
5391 9ˇ
5392 «oˇ»ne"});
5393 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5394 cx.assert_editor_state(indoc! {"
5395 12ˇ3
5396 4567
5397 9ˇ
5398 8ˇne"});
5399
5400 // Copy with a single cursor only, which writes the whole line into the clipboard.
5401 cx.set_state(indoc! {"
5402 The quick brown
5403 fox juˇmps over
5404 the lazy dog"});
5405 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5406 assert_eq!(
5407 cx.read_from_clipboard()
5408 .and_then(|item| item.text().as_deref().map(str::to_string)),
5409 Some("fox jumps over\n".to_string())
5410 );
5411
5412 // Paste with three selections, noticing how the copied full-line selection is inserted
5413 // before the empty selections but replaces the selection that is non-empty.
5414 cx.set_state(indoc! {"
5415 Tˇhe quick brown
5416 «foˇ»x jumps over
5417 tˇhe lazy dog"});
5418 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5419 cx.assert_editor_state(indoc! {"
5420 fox jumps over
5421 Tˇhe quick brown
5422 fox jumps over
5423 ˇx jumps over
5424 fox jumps over
5425 tˇhe lazy dog"});
5426}
5427
5428#[gpui::test]
5429async fn test_copy_trim(cx: &mut TestAppContext) {
5430 init_test(cx, |_| {});
5431
5432 let mut cx = EditorTestContext::new(cx).await;
5433 cx.set_state(
5434 r#" «for selection in selections.iter() {
5435 let mut start = selection.start;
5436 let mut end = selection.end;
5437 let is_entire_line = selection.is_empty();
5438 if is_entire_line {
5439 start = Point::new(start.row, 0);ˇ»
5440 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5441 }
5442 "#,
5443 );
5444 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5445 assert_eq!(
5446 cx.read_from_clipboard()
5447 .and_then(|item| item.text().as_deref().map(str::to_string)),
5448 Some(
5449 "for selection in selections.iter() {
5450 let mut start = selection.start;
5451 let mut end = selection.end;
5452 let is_entire_line = selection.is_empty();
5453 if is_entire_line {
5454 start = Point::new(start.row, 0);"
5455 .to_string()
5456 ),
5457 "Regular copying preserves all indentation selected",
5458 );
5459 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5460 assert_eq!(
5461 cx.read_from_clipboard()
5462 .and_then(|item| item.text().as_deref().map(str::to_string)),
5463 Some(
5464 "for selection in selections.iter() {
5465let mut start = selection.start;
5466let mut end = selection.end;
5467let is_entire_line = selection.is_empty();
5468if is_entire_line {
5469 start = Point::new(start.row, 0);"
5470 .to_string()
5471 ),
5472 "Copying with stripping should strip all leading whitespaces"
5473 );
5474
5475 cx.set_state(
5476 r#" « for selection in selections.iter() {
5477 let mut start = selection.start;
5478 let mut end = selection.end;
5479 let is_entire_line = selection.is_empty();
5480 if is_entire_line {
5481 start = Point::new(start.row, 0);ˇ»
5482 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5483 }
5484 "#,
5485 );
5486 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5487 assert_eq!(
5488 cx.read_from_clipboard()
5489 .and_then(|item| item.text().as_deref().map(str::to_string)),
5490 Some(
5491 " for selection in selections.iter() {
5492 let mut start = selection.start;
5493 let mut end = selection.end;
5494 let is_entire_line = selection.is_empty();
5495 if is_entire_line {
5496 start = Point::new(start.row, 0);"
5497 .to_string()
5498 ),
5499 "Regular copying preserves all indentation selected",
5500 );
5501 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5502 assert_eq!(
5503 cx.read_from_clipboard()
5504 .and_then(|item| item.text().as_deref().map(str::to_string)),
5505 Some(
5506 "for selection in selections.iter() {
5507let mut start = selection.start;
5508let mut end = selection.end;
5509let is_entire_line = selection.is_empty();
5510if is_entire_line {
5511 start = Point::new(start.row, 0);"
5512 .to_string()
5513 ),
5514 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5515 );
5516
5517 cx.set_state(
5518 r#" «ˇ for selection in selections.iter() {
5519 let mut start = selection.start;
5520 let mut end = selection.end;
5521 let is_entire_line = selection.is_empty();
5522 if is_entire_line {
5523 start = Point::new(start.row, 0);»
5524 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5525 }
5526 "#,
5527 );
5528 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5529 assert_eq!(
5530 cx.read_from_clipboard()
5531 .and_then(|item| item.text().as_deref().map(str::to_string)),
5532 Some(
5533 " for selection in selections.iter() {
5534 let mut start = selection.start;
5535 let mut end = selection.end;
5536 let is_entire_line = selection.is_empty();
5537 if is_entire_line {
5538 start = Point::new(start.row, 0);"
5539 .to_string()
5540 ),
5541 "Regular copying for reverse selection works the same",
5542 );
5543 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5544 assert_eq!(
5545 cx.read_from_clipboard()
5546 .and_then(|item| item.text().as_deref().map(str::to_string)),
5547 Some(
5548 "for selection in selections.iter() {
5549let mut start = selection.start;
5550let mut end = selection.end;
5551let is_entire_line = selection.is_empty();
5552if is_entire_line {
5553 start = Point::new(start.row, 0);"
5554 .to_string()
5555 ),
5556 "Copying with stripping for reverse selection works the same"
5557 );
5558
5559 cx.set_state(
5560 r#" for selection «in selections.iter() {
5561 let mut start = selection.start;
5562 let mut end = selection.end;
5563 let is_entire_line = selection.is_empty();
5564 if is_entire_line {
5565 start = Point::new(start.row, 0);ˇ»
5566 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5567 }
5568 "#,
5569 );
5570 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5571 assert_eq!(
5572 cx.read_from_clipboard()
5573 .and_then(|item| item.text().as_deref().map(str::to_string)),
5574 Some(
5575 "in selections.iter() {
5576 let mut start = selection.start;
5577 let mut end = selection.end;
5578 let is_entire_line = selection.is_empty();
5579 if is_entire_line {
5580 start = Point::new(start.row, 0);"
5581 .to_string()
5582 ),
5583 "When selecting past the indent, the copying works as usual",
5584 );
5585 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5586 assert_eq!(
5587 cx.read_from_clipboard()
5588 .and_then(|item| item.text().as_deref().map(str::to_string)),
5589 Some(
5590 "in selections.iter() {
5591 let mut start = selection.start;
5592 let mut end = selection.end;
5593 let is_entire_line = selection.is_empty();
5594 if is_entire_line {
5595 start = Point::new(start.row, 0);"
5596 .to_string()
5597 ),
5598 "When selecting past the indent, nothing is trimmed"
5599 );
5600
5601 cx.set_state(
5602 r#" «for selection in selections.iter() {
5603 let mut start = selection.start;
5604
5605 let mut end = selection.end;
5606 let is_entire_line = selection.is_empty();
5607 if is_entire_line {
5608 start = Point::new(start.row, 0);
5609ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
5610 }
5611 "#,
5612 );
5613 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5614 assert_eq!(
5615 cx.read_from_clipboard()
5616 .and_then(|item| item.text().as_deref().map(str::to_string)),
5617 Some(
5618 "for selection in selections.iter() {
5619let mut start = selection.start;
5620
5621let mut end = selection.end;
5622let is_entire_line = selection.is_empty();
5623if is_entire_line {
5624 start = Point::new(start.row, 0);
5625"
5626 .to_string()
5627 ),
5628 "Copying with stripping should ignore empty lines"
5629 );
5630}
5631
5632#[gpui::test]
5633async fn test_paste_multiline(cx: &mut TestAppContext) {
5634 init_test(cx, |_| {});
5635
5636 let mut cx = EditorTestContext::new(cx).await;
5637 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5638
5639 // Cut an indented block, without the leading whitespace.
5640 cx.set_state(indoc! {"
5641 const a: B = (
5642 c(),
5643 «d(
5644 e,
5645 f
5646 )ˇ»
5647 );
5648 "});
5649 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5650 cx.assert_editor_state(indoc! {"
5651 const a: B = (
5652 c(),
5653 ˇ
5654 );
5655 "});
5656
5657 // Paste it at the same position.
5658 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5659 cx.assert_editor_state(indoc! {"
5660 const a: B = (
5661 c(),
5662 d(
5663 e,
5664 f
5665 )ˇ
5666 );
5667 "});
5668
5669 // Paste it at a line with a lower indent level.
5670 cx.set_state(indoc! {"
5671 ˇ
5672 const a: B = (
5673 c(),
5674 );
5675 "});
5676 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5677 cx.assert_editor_state(indoc! {"
5678 d(
5679 e,
5680 f
5681 )ˇ
5682 const a: B = (
5683 c(),
5684 );
5685 "});
5686
5687 // Cut an indented block, with the leading whitespace.
5688 cx.set_state(indoc! {"
5689 const a: B = (
5690 c(),
5691 « d(
5692 e,
5693 f
5694 )
5695 ˇ»);
5696 "});
5697 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5698 cx.assert_editor_state(indoc! {"
5699 const a: B = (
5700 c(),
5701 ˇ);
5702 "});
5703
5704 // Paste it at the same position.
5705 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5706 cx.assert_editor_state(indoc! {"
5707 const a: B = (
5708 c(),
5709 d(
5710 e,
5711 f
5712 )
5713 ˇ);
5714 "});
5715
5716 // Paste it at a line with a higher indent level.
5717 cx.set_state(indoc! {"
5718 const a: B = (
5719 c(),
5720 d(
5721 e,
5722 fˇ
5723 )
5724 );
5725 "});
5726 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5727 cx.assert_editor_state(indoc! {"
5728 const a: B = (
5729 c(),
5730 d(
5731 e,
5732 f d(
5733 e,
5734 f
5735 )
5736 ˇ
5737 )
5738 );
5739 "});
5740
5741 // Copy an indented block, starting mid-line
5742 cx.set_state(indoc! {"
5743 const a: B = (
5744 c(),
5745 somethin«g(
5746 e,
5747 f
5748 )ˇ»
5749 );
5750 "});
5751 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5752
5753 // Paste it on a line with a lower indent level
5754 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
5755 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5756 cx.assert_editor_state(indoc! {"
5757 const a: B = (
5758 c(),
5759 something(
5760 e,
5761 f
5762 )
5763 );
5764 g(
5765 e,
5766 f
5767 )ˇ"});
5768}
5769
5770#[gpui::test]
5771async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
5772 init_test(cx, |_| {});
5773
5774 cx.write_to_clipboard(ClipboardItem::new_string(
5775 " d(\n e\n );\n".into(),
5776 ));
5777
5778 let mut cx = EditorTestContext::new(cx).await;
5779 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5780
5781 cx.set_state(indoc! {"
5782 fn a() {
5783 b();
5784 if c() {
5785 ˇ
5786 }
5787 }
5788 "});
5789
5790 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5791 cx.assert_editor_state(indoc! {"
5792 fn a() {
5793 b();
5794 if c() {
5795 d(
5796 e
5797 );
5798 ˇ
5799 }
5800 }
5801 "});
5802
5803 cx.set_state(indoc! {"
5804 fn a() {
5805 b();
5806 ˇ
5807 }
5808 "});
5809
5810 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5811 cx.assert_editor_state(indoc! {"
5812 fn a() {
5813 b();
5814 d(
5815 e
5816 );
5817 ˇ
5818 }
5819 "});
5820}
5821
5822#[gpui::test]
5823fn test_select_all(cx: &mut TestAppContext) {
5824 init_test(cx, |_| {});
5825
5826 let editor = cx.add_window(|window, cx| {
5827 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
5828 build_editor(buffer, window, cx)
5829 });
5830 _ = editor.update(cx, |editor, window, cx| {
5831 editor.select_all(&SelectAll, window, cx);
5832 assert_eq!(
5833 editor.selections.display_ranges(cx),
5834 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
5835 );
5836 });
5837}
5838
5839#[gpui::test]
5840fn test_select_line(cx: &mut TestAppContext) {
5841 init_test(cx, |_| {});
5842
5843 let editor = cx.add_window(|window, cx| {
5844 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
5845 build_editor(buffer, window, cx)
5846 });
5847 _ = editor.update(cx, |editor, window, cx| {
5848 editor.change_selections(None, window, cx, |s| {
5849 s.select_display_ranges([
5850 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5851 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5852 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5853 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
5854 ])
5855 });
5856 editor.select_line(&SelectLine, window, cx);
5857 assert_eq!(
5858 editor.selections.display_ranges(cx),
5859 vec![
5860 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
5861 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
5862 ]
5863 );
5864 });
5865
5866 _ = editor.update(cx, |editor, window, cx| {
5867 editor.select_line(&SelectLine, window, cx);
5868 assert_eq!(
5869 editor.selections.display_ranges(cx),
5870 vec![
5871 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
5872 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
5873 ]
5874 );
5875 });
5876
5877 _ = editor.update(cx, |editor, window, cx| {
5878 editor.select_line(&SelectLine, window, cx);
5879 assert_eq!(
5880 editor.selections.display_ranges(cx),
5881 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
5882 );
5883 });
5884}
5885
5886#[gpui::test]
5887async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
5888 init_test(cx, |_| {});
5889 let mut cx = EditorTestContext::new(cx).await;
5890
5891 #[track_caller]
5892 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
5893 cx.set_state(initial_state);
5894 cx.update_editor(|e, window, cx| {
5895 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
5896 });
5897 cx.assert_editor_state(expected_state);
5898 }
5899
5900 // Selection starts and ends at the middle of lines, left-to-right
5901 test(
5902 &mut cx,
5903 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
5904 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5905 );
5906 // Same thing, right-to-left
5907 test(
5908 &mut cx,
5909 "aa\nb«b\ncc\ndd\neˇ»e\nff",
5910 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5911 );
5912
5913 // Whole buffer, left-to-right, last line *doesn't* end with newline
5914 test(
5915 &mut cx,
5916 "«ˇaa\nbb\ncc\ndd\nee\nff»",
5917 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5918 );
5919 // Same thing, right-to-left
5920 test(
5921 &mut cx,
5922 "«aa\nbb\ncc\ndd\nee\nffˇ»",
5923 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5924 );
5925
5926 // Whole buffer, left-to-right, last line ends with newline
5927 test(
5928 &mut cx,
5929 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
5930 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5931 );
5932 // Same thing, right-to-left
5933 test(
5934 &mut cx,
5935 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
5936 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5937 );
5938
5939 // Starts at the end of a line, ends at the start of another
5940 test(
5941 &mut cx,
5942 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
5943 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
5944 );
5945}
5946
5947#[gpui::test]
5948async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
5949 init_test(cx, |_| {});
5950
5951 let editor = cx.add_window(|window, cx| {
5952 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
5953 build_editor(buffer, window, cx)
5954 });
5955
5956 // setup
5957 _ = editor.update(cx, |editor, window, cx| {
5958 editor.fold_creases(
5959 vec![
5960 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5961 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5962 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5963 ],
5964 true,
5965 window,
5966 cx,
5967 );
5968 assert_eq!(
5969 editor.display_text(cx),
5970 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5971 );
5972 });
5973
5974 _ = editor.update(cx, |editor, window, cx| {
5975 editor.change_selections(None, window, cx, |s| {
5976 s.select_display_ranges([
5977 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5978 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5979 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5980 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
5981 ])
5982 });
5983 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5984 assert_eq!(
5985 editor.display_text(cx),
5986 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5987 );
5988 });
5989 EditorTestContext::for_editor(editor, cx)
5990 .await
5991 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
5992
5993 _ = editor.update(cx, |editor, window, cx| {
5994 editor.change_selections(None, window, cx, |s| {
5995 s.select_display_ranges([
5996 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
5997 ])
5998 });
5999 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6000 assert_eq!(
6001 editor.display_text(cx),
6002 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
6003 );
6004 assert_eq!(
6005 editor.selections.display_ranges(cx),
6006 [
6007 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
6008 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
6009 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
6010 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
6011 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
6012 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
6013 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
6014 ]
6015 );
6016 });
6017 EditorTestContext::for_editor(editor, cx)
6018 .await
6019 .assert_editor_state(
6020 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
6021 );
6022}
6023
6024#[gpui::test]
6025async fn test_add_selection_above_below(cx: &mut TestAppContext) {
6026 init_test(cx, |_| {});
6027
6028 let mut cx = EditorTestContext::new(cx).await;
6029
6030 cx.set_state(indoc!(
6031 r#"abc
6032 defˇghi
6033
6034 jk
6035 nlmo
6036 "#
6037 ));
6038
6039 cx.update_editor(|editor, window, cx| {
6040 editor.add_selection_above(&Default::default(), window, cx);
6041 });
6042
6043 cx.assert_editor_state(indoc!(
6044 r#"abcˇ
6045 defˇghi
6046
6047 jk
6048 nlmo
6049 "#
6050 ));
6051
6052 cx.update_editor(|editor, window, cx| {
6053 editor.add_selection_above(&Default::default(), window, cx);
6054 });
6055
6056 cx.assert_editor_state(indoc!(
6057 r#"abcˇ
6058 defˇghi
6059
6060 jk
6061 nlmo
6062 "#
6063 ));
6064
6065 cx.update_editor(|editor, window, cx| {
6066 editor.add_selection_below(&Default::default(), window, cx);
6067 });
6068
6069 cx.assert_editor_state(indoc!(
6070 r#"abc
6071 defˇghi
6072
6073 jk
6074 nlmo
6075 "#
6076 ));
6077
6078 cx.update_editor(|editor, window, cx| {
6079 editor.undo_selection(&Default::default(), window, cx);
6080 });
6081
6082 cx.assert_editor_state(indoc!(
6083 r#"abcˇ
6084 defˇghi
6085
6086 jk
6087 nlmo
6088 "#
6089 ));
6090
6091 cx.update_editor(|editor, window, cx| {
6092 editor.redo_selection(&Default::default(), window, cx);
6093 });
6094
6095 cx.assert_editor_state(indoc!(
6096 r#"abc
6097 defˇghi
6098
6099 jk
6100 nlmo
6101 "#
6102 ));
6103
6104 cx.update_editor(|editor, window, cx| {
6105 editor.add_selection_below(&Default::default(), window, cx);
6106 });
6107
6108 cx.assert_editor_state(indoc!(
6109 r#"abc
6110 defˇghi
6111 ˇ
6112 jk
6113 nlmo
6114 "#
6115 ));
6116
6117 cx.update_editor(|editor, window, cx| {
6118 editor.add_selection_below(&Default::default(), window, cx);
6119 });
6120
6121 cx.assert_editor_state(indoc!(
6122 r#"abc
6123 defˇghi
6124 ˇ
6125 jkˇ
6126 nlmo
6127 "#
6128 ));
6129
6130 cx.update_editor(|editor, window, cx| {
6131 editor.add_selection_below(&Default::default(), window, cx);
6132 });
6133
6134 cx.assert_editor_state(indoc!(
6135 r#"abc
6136 defˇghi
6137 ˇ
6138 jkˇ
6139 nlmˇo
6140 "#
6141 ));
6142
6143 cx.update_editor(|editor, window, cx| {
6144 editor.add_selection_below(&Default::default(), window, cx);
6145 });
6146
6147 cx.assert_editor_state(indoc!(
6148 r#"abc
6149 defˇghi
6150 ˇ
6151 jkˇ
6152 nlmˇo
6153 ˇ"#
6154 ));
6155
6156 // change selections
6157 cx.set_state(indoc!(
6158 r#"abc
6159 def«ˇg»hi
6160
6161 jk
6162 nlmo
6163 "#
6164 ));
6165
6166 cx.update_editor(|editor, window, cx| {
6167 editor.add_selection_below(&Default::default(), window, cx);
6168 });
6169
6170 cx.assert_editor_state(indoc!(
6171 r#"abc
6172 def«ˇg»hi
6173
6174 jk
6175 nlm«ˇo»
6176 "#
6177 ));
6178
6179 cx.update_editor(|editor, window, cx| {
6180 editor.add_selection_below(&Default::default(), window, cx);
6181 });
6182
6183 cx.assert_editor_state(indoc!(
6184 r#"abc
6185 def«ˇg»hi
6186
6187 jk
6188 nlm«ˇo»
6189 "#
6190 ));
6191
6192 cx.update_editor(|editor, window, cx| {
6193 editor.add_selection_above(&Default::default(), window, cx);
6194 });
6195
6196 cx.assert_editor_state(indoc!(
6197 r#"abc
6198 def«ˇg»hi
6199
6200 jk
6201 nlmo
6202 "#
6203 ));
6204
6205 cx.update_editor(|editor, window, cx| {
6206 editor.add_selection_above(&Default::default(), window, cx);
6207 });
6208
6209 cx.assert_editor_state(indoc!(
6210 r#"abc
6211 def«ˇg»hi
6212
6213 jk
6214 nlmo
6215 "#
6216 ));
6217
6218 // Change selections again
6219 cx.set_state(indoc!(
6220 r#"a«bc
6221 defgˇ»hi
6222
6223 jk
6224 nlmo
6225 "#
6226 ));
6227
6228 cx.update_editor(|editor, window, cx| {
6229 editor.add_selection_below(&Default::default(), window, cx);
6230 });
6231
6232 cx.assert_editor_state(indoc!(
6233 r#"a«bcˇ»
6234 d«efgˇ»hi
6235
6236 j«kˇ»
6237 nlmo
6238 "#
6239 ));
6240
6241 cx.update_editor(|editor, window, cx| {
6242 editor.add_selection_below(&Default::default(), window, cx);
6243 });
6244 cx.assert_editor_state(indoc!(
6245 r#"a«bcˇ»
6246 d«efgˇ»hi
6247
6248 j«kˇ»
6249 n«lmoˇ»
6250 "#
6251 ));
6252 cx.update_editor(|editor, window, cx| {
6253 editor.add_selection_above(&Default::default(), window, cx);
6254 });
6255
6256 cx.assert_editor_state(indoc!(
6257 r#"a«bcˇ»
6258 d«efgˇ»hi
6259
6260 j«kˇ»
6261 nlmo
6262 "#
6263 ));
6264
6265 // Change selections again
6266 cx.set_state(indoc!(
6267 r#"abc
6268 d«ˇefghi
6269
6270 jk
6271 nlm»o
6272 "#
6273 ));
6274
6275 cx.update_editor(|editor, window, cx| {
6276 editor.add_selection_above(&Default::default(), window, cx);
6277 });
6278
6279 cx.assert_editor_state(indoc!(
6280 r#"a«ˇbc»
6281 d«ˇef»ghi
6282
6283 j«ˇk»
6284 n«ˇlm»o
6285 "#
6286 ));
6287
6288 cx.update_editor(|editor, window, cx| {
6289 editor.add_selection_below(&Default::default(), window, cx);
6290 });
6291
6292 cx.assert_editor_state(indoc!(
6293 r#"abc
6294 d«ˇef»ghi
6295
6296 j«ˇk»
6297 n«ˇlm»o
6298 "#
6299 ));
6300}
6301
6302#[gpui::test]
6303async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
6304 init_test(cx, |_| {});
6305 let mut cx = EditorTestContext::new(cx).await;
6306
6307 cx.set_state(indoc!(
6308 r#"line onˇe
6309 liˇne two
6310 line three
6311 line four"#
6312 ));
6313
6314 cx.update_editor(|editor, window, cx| {
6315 editor.add_selection_below(&Default::default(), window, cx);
6316 });
6317
6318 // test multiple cursors expand in the same direction
6319 cx.assert_editor_state(indoc!(
6320 r#"line onˇe
6321 liˇne twˇo
6322 liˇne three
6323 line four"#
6324 ));
6325
6326 cx.update_editor(|editor, window, cx| {
6327 editor.add_selection_below(&Default::default(), window, cx);
6328 });
6329
6330 cx.update_editor(|editor, window, cx| {
6331 editor.add_selection_below(&Default::default(), window, cx);
6332 });
6333
6334 // test multiple cursors expand below overflow
6335 cx.assert_editor_state(indoc!(
6336 r#"line onˇe
6337 liˇne twˇo
6338 liˇne thˇree
6339 liˇne foˇur"#
6340 ));
6341
6342 cx.update_editor(|editor, window, cx| {
6343 editor.add_selection_above(&Default::default(), window, cx);
6344 });
6345
6346 // test multiple cursors retrieves back correctly
6347 cx.assert_editor_state(indoc!(
6348 r#"line onˇe
6349 liˇne twˇo
6350 liˇne thˇree
6351 line four"#
6352 ));
6353
6354 cx.update_editor(|editor, window, cx| {
6355 editor.add_selection_above(&Default::default(), window, cx);
6356 });
6357
6358 cx.update_editor(|editor, window, cx| {
6359 editor.add_selection_above(&Default::default(), window, cx);
6360 });
6361
6362 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
6363 cx.assert_editor_state(indoc!(
6364 r#"liˇne onˇe
6365 liˇne two
6366 line three
6367 line four"#
6368 ));
6369
6370 cx.update_editor(|editor, window, cx| {
6371 editor.undo_selection(&Default::default(), window, cx);
6372 });
6373
6374 // test undo
6375 cx.assert_editor_state(indoc!(
6376 r#"line onˇe
6377 liˇne twˇo
6378 line three
6379 line four"#
6380 ));
6381
6382 cx.update_editor(|editor, window, cx| {
6383 editor.redo_selection(&Default::default(), window, cx);
6384 });
6385
6386 // test redo
6387 cx.assert_editor_state(indoc!(
6388 r#"liˇne onˇe
6389 liˇne two
6390 line three
6391 line four"#
6392 ));
6393
6394 cx.set_state(indoc!(
6395 r#"abcd
6396 ef«ghˇ»
6397 ijkl
6398 «mˇ»nop"#
6399 ));
6400
6401 cx.update_editor(|editor, window, cx| {
6402 editor.add_selection_above(&Default::default(), window, cx);
6403 });
6404
6405 // test multiple selections expand in the same direction
6406 cx.assert_editor_state(indoc!(
6407 r#"ab«cdˇ»
6408 ef«ghˇ»
6409 «iˇ»jkl
6410 «mˇ»nop"#
6411 ));
6412
6413 cx.update_editor(|editor, window, cx| {
6414 editor.add_selection_above(&Default::default(), window, cx);
6415 });
6416
6417 // test multiple selection upward overflow
6418 cx.assert_editor_state(indoc!(
6419 r#"ab«cdˇ»
6420 «eˇ»f«ghˇ»
6421 «iˇ»jkl
6422 «mˇ»nop"#
6423 ));
6424
6425 cx.update_editor(|editor, window, cx| {
6426 editor.add_selection_below(&Default::default(), window, cx);
6427 });
6428
6429 // test multiple selection retrieves back correctly
6430 cx.assert_editor_state(indoc!(
6431 r#"abcd
6432 ef«ghˇ»
6433 «iˇ»jkl
6434 «mˇ»nop"#
6435 ));
6436
6437 cx.update_editor(|editor, window, cx| {
6438 editor.add_selection_below(&Default::default(), window, cx);
6439 });
6440
6441 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
6442 cx.assert_editor_state(indoc!(
6443 r#"abcd
6444 ef«ghˇ»
6445 ij«klˇ»
6446 «mˇ»nop"#
6447 ));
6448
6449 cx.update_editor(|editor, window, cx| {
6450 editor.undo_selection(&Default::default(), window, cx);
6451 });
6452
6453 // test undo
6454 cx.assert_editor_state(indoc!(
6455 r#"abcd
6456 ef«ghˇ»
6457 «iˇ»jkl
6458 «mˇ»nop"#
6459 ));
6460
6461 cx.update_editor(|editor, window, cx| {
6462 editor.redo_selection(&Default::default(), window, cx);
6463 });
6464
6465 // test redo
6466 cx.assert_editor_state(indoc!(
6467 r#"abcd
6468 ef«ghˇ»
6469 ij«klˇ»
6470 «mˇ»nop"#
6471 ));
6472}
6473
6474#[gpui::test]
6475async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
6476 init_test(cx, |_| {});
6477 let mut cx = EditorTestContext::new(cx).await;
6478
6479 cx.set_state(indoc!(
6480 r#"line onˇe
6481 liˇne two
6482 line three
6483 line four"#
6484 ));
6485
6486 cx.update_editor(|editor, window, cx| {
6487 editor.add_selection_below(&Default::default(), window, cx);
6488 editor.add_selection_below(&Default::default(), window, cx);
6489 editor.add_selection_below(&Default::default(), window, cx);
6490 });
6491
6492 // initial state with two multi cursor groups
6493 cx.assert_editor_state(indoc!(
6494 r#"line onˇe
6495 liˇne twˇo
6496 liˇne thˇree
6497 liˇne foˇur"#
6498 ));
6499
6500 // add single cursor in middle - simulate opt click
6501 cx.update_editor(|editor, window, cx| {
6502 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
6503 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6504 editor.end_selection(window, cx);
6505 });
6506
6507 cx.assert_editor_state(indoc!(
6508 r#"line onˇe
6509 liˇne twˇo
6510 liˇneˇ thˇree
6511 liˇne foˇur"#
6512 ));
6513
6514 cx.update_editor(|editor, window, cx| {
6515 editor.add_selection_above(&Default::default(), window, cx);
6516 });
6517
6518 // test new added selection expands above and existing selection shrinks
6519 cx.assert_editor_state(indoc!(
6520 r#"line onˇe
6521 liˇneˇ twˇo
6522 liˇneˇ thˇree
6523 line four"#
6524 ));
6525
6526 cx.update_editor(|editor, window, cx| {
6527 editor.add_selection_above(&Default::default(), window, cx);
6528 });
6529
6530 // test new added selection expands above and existing selection shrinks
6531 cx.assert_editor_state(indoc!(
6532 r#"lineˇ onˇe
6533 liˇneˇ twˇo
6534 lineˇ three
6535 line four"#
6536 ));
6537
6538 // intial state with two selection groups
6539 cx.set_state(indoc!(
6540 r#"abcd
6541 ef«ghˇ»
6542 ijkl
6543 «mˇ»nop"#
6544 ));
6545
6546 cx.update_editor(|editor, window, cx| {
6547 editor.add_selection_above(&Default::default(), window, cx);
6548 editor.add_selection_above(&Default::default(), window, cx);
6549 });
6550
6551 cx.assert_editor_state(indoc!(
6552 r#"ab«cdˇ»
6553 «eˇ»f«ghˇ»
6554 «iˇ»jkl
6555 «mˇ»nop"#
6556 ));
6557
6558 // add single selection in middle - simulate opt drag
6559 cx.update_editor(|editor, window, cx| {
6560 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
6561 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6562 editor.update_selection(
6563 DisplayPoint::new(DisplayRow(2), 4),
6564 0,
6565 gpui::Point::<f32>::default(),
6566 window,
6567 cx,
6568 );
6569 editor.end_selection(window, cx);
6570 });
6571
6572 cx.assert_editor_state(indoc!(
6573 r#"ab«cdˇ»
6574 «eˇ»f«ghˇ»
6575 «iˇ»jk«lˇ»
6576 «mˇ»nop"#
6577 ));
6578
6579 cx.update_editor(|editor, window, cx| {
6580 editor.add_selection_below(&Default::default(), window, cx);
6581 });
6582
6583 // test new added selection expands below, others shrinks from above
6584 cx.assert_editor_state(indoc!(
6585 r#"abcd
6586 ef«ghˇ»
6587 «iˇ»jk«lˇ»
6588 «mˇ»no«pˇ»"#
6589 ));
6590}
6591
6592#[gpui::test]
6593async fn test_select_next(cx: &mut TestAppContext) {
6594 init_test(cx, |_| {});
6595
6596 let mut cx = EditorTestContext::new(cx).await;
6597 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6598
6599 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6600 .unwrap();
6601 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6602
6603 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6604 .unwrap();
6605 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6606
6607 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6608 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6609
6610 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6611 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6612
6613 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6614 .unwrap();
6615 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6616
6617 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6618 .unwrap();
6619 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6620
6621 // Test selection direction should be preserved
6622 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6623
6624 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6625 .unwrap();
6626 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
6627}
6628
6629#[gpui::test]
6630async fn test_select_all_matches(cx: &mut TestAppContext) {
6631 init_test(cx, |_| {});
6632
6633 let mut cx = EditorTestContext::new(cx).await;
6634
6635 // Test caret-only selections
6636 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6637 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6638 .unwrap();
6639 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6640
6641 // Test left-to-right selections
6642 cx.set_state("abc\n«abcˇ»\nabc");
6643 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6644 .unwrap();
6645 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
6646
6647 // Test right-to-left selections
6648 cx.set_state("abc\n«ˇabc»\nabc");
6649 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6650 .unwrap();
6651 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
6652
6653 // Test selecting whitespace with caret selection
6654 cx.set_state("abc\nˇ abc\nabc");
6655 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6656 .unwrap();
6657 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6658
6659 // Test selecting whitespace with left-to-right selection
6660 cx.set_state("abc\n«ˇ »abc\nabc");
6661 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6662 .unwrap();
6663 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
6664
6665 // Test no matches with right-to-left selection
6666 cx.set_state("abc\n« ˇ»abc\nabc");
6667 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6668 .unwrap();
6669 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6670}
6671
6672#[gpui::test]
6673async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
6674 init_test(cx, |_| {});
6675
6676 let mut cx = EditorTestContext::new(cx).await;
6677
6678 let large_body_1 = "\nd".repeat(200);
6679 let large_body_2 = "\ne".repeat(200);
6680
6681 cx.set_state(&format!(
6682 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
6683 ));
6684 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
6685 let scroll_position = editor.scroll_position(cx);
6686 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
6687 scroll_position
6688 });
6689
6690 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6691 .unwrap();
6692 cx.assert_editor_state(&format!(
6693 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
6694 ));
6695 let scroll_position_after_selection =
6696 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
6697 assert_eq!(
6698 initial_scroll_position, scroll_position_after_selection,
6699 "Scroll position should not change after selecting all matches"
6700 );
6701}
6702
6703#[gpui::test]
6704async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
6705 init_test(cx, |_| {});
6706
6707 let mut cx = EditorLspTestContext::new_rust(
6708 lsp::ServerCapabilities {
6709 document_formatting_provider: Some(lsp::OneOf::Left(true)),
6710 ..Default::default()
6711 },
6712 cx,
6713 )
6714 .await;
6715
6716 cx.set_state(indoc! {"
6717 line 1
6718 line 2
6719 linˇe 3
6720 line 4
6721 line 5
6722 "});
6723
6724 // Make an edit
6725 cx.update_editor(|editor, window, cx| {
6726 editor.handle_input("X", window, cx);
6727 });
6728
6729 // Move cursor to a different position
6730 cx.update_editor(|editor, window, cx| {
6731 editor.change_selections(None, window, cx, |s| {
6732 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
6733 });
6734 });
6735
6736 cx.assert_editor_state(indoc! {"
6737 line 1
6738 line 2
6739 linXe 3
6740 line 4
6741 liˇne 5
6742 "});
6743
6744 cx.lsp
6745 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
6746 Ok(Some(vec![lsp::TextEdit::new(
6747 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
6748 "PREFIX ".to_string(),
6749 )]))
6750 });
6751
6752 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
6753 .unwrap()
6754 .await
6755 .unwrap();
6756
6757 cx.assert_editor_state(indoc! {"
6758 PREFIX line 1
6759 line 2
6760 linXe 3
6761 line 4
6762 liˇne 5
6763 "});
6764
6765 // Undo formatting
6766 cx.update_editor(|editor, window, cx| {
6767 editor.undo(&Default::default(), window, cx);
6768 });
6769
6770 // Verify cursor moved back to position after edit
6771 cx.assert_editor_state(indoc! {"
6772 line 1
6773 line 2
6774 linXˇe 3
6775 line 4
6776 line 5
6777 "});
6778}
6779
6780#[gpui::test]
6781async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext) {
6782 init_test(cx, |_| {});
6783
6784 let mut cx = EditorTestContext::new(cx).await;
6785
6786 let provider = cx.new(|_| FakeInlineCompletionProvider::default());
6787 cx.update_editor(|editor, window, cx| {
6788 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
6789 });
6790
6791 cx.set_state(indoc! {"
6792 line 1
6793 line 2
6794 linˇe 3
6795 line 4
6796 line 5
6797 line 6
6798 line 7
6799 line 8
6800 line 9
6801 line 10
6802 "});
6803
6804 let snapshot = cx.buffer_snapshot();
6805 let edit_position = snapshot.anchor_after(Point::new(2, 4));
6806
6807 cx.update(|_, cx| {
6808 provider.update(cx, |provider, _| {
6809 provider.set_inline_completion(Some(inline_completion::InlineCompletion {
6810 id: None,
6811 edits: vec![(edit_position..edit_position, "X".into())],
6812 edit_preview: None,
6813 }))
6814 })
6815 });
6816
6817 cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx));
6818 cx.update_editor(|editor, window, cx| {
6819 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
6820 });
6821
6822 cx.assert_editor_state(indoc! {"
6823 line 1
6824 line 2
6825 lineXˇ 3
6826 line 4
6827 line 5
6828 line 6
6829 line 7
6830 line 8
6831 line 9
6832 line 10
6833 "});
6834
6835 cx.update_editor(|editor, window, cx| {
6836 editor.change_selections(None, window, cx, |s| {
6837 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
6838 });
6839 });
6840
6841 cx.assert_editor_state(indoc! {"
6842 line 1
6843 line 2
6844 lineX 3
6845 line 4
6846 line 5
6847 line 6
6848 line 7
6849 line 8
6850 line 9
6851 liˇne 10
6852 "});
6853
6854 cx.update_editor(|editor, window, cx| {
6855 editor.undo(&Default::default(), window, cx);
6856 });
6857
6858 cx.assert_editor_state(indoc! {"
6859 line 1
6860 line 2
6861 lineˇ 3
6862 line 4
6863 line 5
6864 line 6
6865 line 7
6866 line 8
6867 line 9
6868 line 10
6869 "});
6870}
6871
6872#[gpui::test]
6873async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
6874 init_test(cx, |_| {});
6875
6876 let mut cx = EditorTestContext::new(cx).await;
6877 cx.set_state(
6878 r#"let foo = 2;
6879lˇet foo = 2;
6880let fooˇ = 2;
6881let foo = 2;
6882let foo = ˇ2;"#,
6883 );
6884
6885 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6886 .unwrap();
6887 cx.assert_editor_state(
6888 r#"let foo = 2;
6889«letˇ» foo = 2;
6890let «fooˇ» = 2;
6891let foo = 2;
6892let foo = «2ˇ»;"#,
6893 );
6894
6895 // noop for multiple selections with different contents
6896 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6897 .unwrap();
6898 cx.assert_editor_state(
6899 r#"let foo = 2;
6900«letˇ» foo = 2;
6901let «fooˇ» = 2;
6902let foo = 2;
6903let foo = «2ˇ»;"#,
6904 );
6905
6906 // Test last selection direction should be preserved
6907 cx.set_state(
6908 r#"let foo = 2;
6909let foo = 2;
6910let «fooˇ» = 2;
6911let «ˇfoo» = 2;
6912let foo = 2;"#,
6913 );
6914
6915 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6916 .unwrap();
6917 cx.assert_editor_state(
6918 r#"let foo = 2;
6919let foo = 2;
6920let «fooˇ» = 2;
6921let «ˇfoo» = 2;
6922let «ˇfoo» = 2;"#,
6923 );
6924}
6925
6926#[gpui::test]
6927async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
6928 init_test(cx, |_| {});
6929
6930 let mut cx =
6931 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
6932
6933 cx.assert_editor_state(indoc! {"
6934 ˇbbb
6935 ccc
6936
6937 bbb
6938 ccc
6939 "});
6940 cx.dispatch_action(SelectPrevious::default());
6941 cx.assert_editor_state(indoc! {"
6942 «bbbˇ»
6943 ccc
6944
6945 bbb
6946 ccc
6947 "});
6948 cx.dispatch_action(SelectPrevious::default());
6949 cx.assert_editor_state(indoc! {"
6950 «bbbˇ»
6951 ccc
6952
6953 «bbbˇ»
6954 ccc
6955 "});
6956}
6957
6958#[gpui::test]
6959async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
6960 init_test(cx, |_| {});
6961
6962 let mut cx = EditorTestContext::new(cx).await;
6963 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6964
6965 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6966 .unwrap();
6967 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6968
6969 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6970 .unwrap();
6971 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6972
6973 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6974 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6975
6976 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6977 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6978
6979 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6980 .unwrap();
6981 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
6982
6983 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6984 .unwrap();
6985 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6986}
6987
6988#[gpui::test]
6989async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
6990 init_test(cx, |_| {});
6991
6992 let mut cx = EditorTestContext::new(cx).await;
6993 cx.set_state("aˇ");
6994
6995 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6996 .unwrap();
6997 cx.assert_editor_state("«aˇ»");
6998 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6999 .unwrap();
7000 cx.assert_editor_state("«aˇ»");
7001}
7002
7003#[gpui::test]
7004async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
7005 init_test(cx, |_| {});
7006
7007 let mut cx = EditorTestContext::new(cx).await;
7008 cx.set_state(
7009 r#"let foo = 2;
7010lˇet foo = 2;
7011let fooˇ = 2;
7012let foo = 2;
7013let foo = ˇ2;"#,
7014 );
7015
7016 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7017 .unwrap();
7018 cx.assert_editor_state(
7019 r#"let foo = 2;
7020«letˇ» foo = 2;
7021let «fooˇ» = 2;
7022let foo = 2;
7023let foo = «2ˇ»;"#,
7024 );
7025
7026 // noop for multiple selections with different contents
7027 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7028 .unwrap();
7029 cx.assert_editor_state(
7030 r#"let foo = 2;
7031«letˇ» foo = 2;
7032let «fooˇ» = 2;
7033let foo = 2;
7034let foo = «2ˇ»;"#,
7035 );
7036}
7037
7038#[gpui::test]
7039async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
7040 init_test(cx, |_| {});
7041
7042 let mut cx = EditorTestContext::new(cx).await;
7043 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7044
7045 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7046 .unwrap();
7047 // selection direction is preserved
7048 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7049
7050 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7051 .unwrap();
7052 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7053
7054 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7055 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7056
7057 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7058 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7059
7060 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7061 .unwrap();
7062 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
7063
7064 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7065 .unwrap();
7066 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
7067}
7068
7069#[gpui::test]
7070async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
7071 init_test(cx, |_| {});
7072
7073 let language = Arc::new(Language::new(
7074 LanguageConfig::default(),
7075 Some(tree_sitter_rust::LANGUAGE.into()),
7076 ));
7077
7078 let text = r#"
7079 use mod1::mod2::{mod3, mod4};
7080
7081 fn fn_1(param1: bool, param2: &str) {
7082 let var1 = "text";
7083 }
7084 "#
7085 .unindent();
7086
7087 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7088 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7089 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7090
7091 editor
7092 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7093 .await;
7094
7095 editor.update_in(cx, |editor, window, cx| {
7096 editor.change_selections(None, window, cx, |s| {
7097 s.select_display_ranges([
7098 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
7099 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
7100 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
7101 ]);
7102 });
7103 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7104 });
7105 editor.update(cx, |editor, cx| {
7106 assert_text_with_selections(
7107 editor,
7108 indoc! {r#"
7109 use mod1::mod2::{mod3, «mod4ˇ»};
7110
7111 fn fn_1«ˇ(param1: bool, param2: &str)» {
7112 let var1 = "«ˇtext»";
7113 }
7114 "#},
7115 cx,
7116 );
7117 });
7118
7119 editor.update_in(cx, |editor, window, cx| {
7120 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7121 });
7122 editor.update(cx, |editor, cx| {
7123 assert_text_with_selections(
7124 editor,
7125 indoc! {r#"
7126 use mod1::mod2::«{mod3, mod4}ˇ»;
7127
7128 «ˇfn fn_1(param1: bool, param2: &str) {
7129 let var1 = "text";
7130 }»
7131 "#},
7132 cx,
7133 );
7134 });
7135
7136 editor.update_in(cx, |editor, window, cx| {
7137 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7138 });
7139 assert_eq!(
7140 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7141 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7142 );
7143
7144 // Trying to expand the selected syntax node one more time has no effect.
7145 editor.update_in(cx, |editor, window, cx| {
7146 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7147 });
7148 assert_eq!(
7149 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7150 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7151 );
7152
7153 editor.update_in(cx, |editor, window, cx| {
7154 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7155 });
7156 editor.update(cx, |editor, cx| {
7157 assert_text_with_selections(
7158 editor,
7159 indoc! {r#"
7160 use mod1::mod2::«{mod3, mod4}ˇ»;
7161
7162 «ˇfn fn_1(param1: bool, param2: &str) {
7163 let var1 = "text";
7164 }»
7165 "#},
7166 cx,
7167 );
7168 });
7169
7170 editor.update_in(cx, |editor, window, cx| {
7171 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7172 });
7173 editor.update(cx, |editor, cx| {
7174 assert_text_with_selections(
7175 editor,
7176 indoc! {r#"
7177 use mod1::mod2::{mod3, «mod4ˇ»};
7178
7179 fn fn_1«ˇ(param1: bool, param2: &str)» {
7180 let var1 = "«ˇtext»";
7181 }
7182 "#},
7183 cx,
7184 );
7185 });
7186
7187 editor.update_in(cx, |editor, window, cx| {
7188 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7189 });
7190 editor.update(cx, |editor, cx| {
7191 assert_text_with_selections(
7192 editor,
7193 indoc! {r#"
7194 use mod1::mod2::{mod3, mo«ˇ»d4};
7195
7196 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7197 let var1 = "te«ˇ»xt";
7198 }
7199 "#},
7200 cx,
7201 );
7202 });
7203
7204 // Trying to shrink the selected syntax node one more time has no effect.
7205 editor.update_in(cx, |editor, window, cx| {
7206 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7207 });
7208 editor.update_in(cx, |editor, _, cx| {
7209 assert_text_with_selections(
7210 editor,
7211 indoc! {r#"
7212 use mod1::mod2::{mod3, mo«ˇ»d4};
7213
7214 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7215 let var1 = "te«ˇ»xt";
7216 }
7217 "#},
7218 cx,
7219 );
7220 });
7221
7222 // Ensure that we keep expanding the selection if the larger selection starts or ends within
7223 // a fold.
7224 editor.update_in(cx, |editor, window, cx| {
7225 editor.fold_creases(
7226 vec![
7227 Crease::simple(
7228 Point::new(0, 21)..Point::new(0, 24),
7229 FoldPlaceholder::test(),
7230 ),
7231 Crease::simple(
7232 Point::new(3, 20)..Point::new(3, 22),
7233 FoldPlaceholder::test(),
7234 ),
7235 ],
7236 true,
7237 window,
7238 cx,
7239 );
7240 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7241 });
7242 editor.update(cx, |editor, cx| {
7243 assert_text_with_selections(
7244 editor,
7245 indoc! {r#"
7246 use mod1::mod2::«{mod3, mod4}ˇ»;
7247
7248 fn fn_1«ˇ(param1: bool, param2: &str)» {
7249 let var1 = "«ˇtext»";
7250 }
7251 "#},
7252 cx,
7253 );
7254 });
7255}
7256
7257#[gpui::test]
7258async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
7259 init_test(cx, |_| {});
7260
7261 let language = Arc::new(Language::new(
7262 LanguageConfig::default(),
7263 Some(tree_sitter_rust::LANGUAGE.into()),
7264 ));
7265
7266 let text = "let a = 2;";
7267
7268 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7269 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7270 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7271
7272 editor
7273 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7274 .await;
7275
7276 // Test case 1: Cursor at end of word
7277 editor.update_in(cx, |editor, window, cx| {
7278 editor.change_selections(None, window, cx, |s| {
7279 s.select_display_ranges([
7280 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
7281 ]);
7282 });
7283 });
7284 editor.update(cx, |editor, cx| {
7285 assert_text_with_selections(editor, "let aˇ = 2;", cx);
7286 });
7287 editor.update_in(cx, |editor, window, cx| {
7288 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7289 });
7290 editor.update(cx, |editor, cx| {
7291 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
7292 });
7293 editor.update_in(cx, |editor, window, cx| {
7294 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7295 });
7296 editor.update(cx, |editor, cx| {
7297 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7298 });
7299
7300 // Test case 2: Cursor at end of statement
7301 editor.update_in(cx, |editor, window, cx| {
7302 editor.change_selections(None, window, cx, |s| {
7303 s.select_display_ranges([
7304 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
7305 ]);
7306 });
7307 });
7308 editor.update(cx, |editor, cx| {
7309 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
7310 });
7311 editor.update_in(cx, |editor, window, cx| {
7312 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7313 });
7314 editor.update(cx, |editor, cx| {
7315 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7316 });
7317}
7318
7319#[gpui::test]
7320async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
7321 init_test(cx, |_| {});
7322
7323 let language = Arc::new(Language::new(
7324 LanguageConfig::default(),
7325 Some(tree_sitter_rust::LANGUAGE.into()),
7326 ));
7327
7328 let text = r#"
7329 use mod1::mod2::{mod3, mod4};
7330
7331 fn fn_1(param1: bool, param2: &str) {
7332 let var1 = "hello world";
7333 }
7334 "#
7335 .unindent();
7336
7337 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7338 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7339 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7340
7341 editor
7342 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7343 .await;
7344
7345 // Test 1: Cursor on a letter of a string word
7346 editor.update_in(cx, |editor, window, cx| {
7347 editor.change_selections(None, window, cx, |s| {
7348 s.select_display_ranges([
7349 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
7350 ]);
7351 });
7352 });
7353 editor.update_in(cx, |editor, window, cx| {
7354 assert_text_with_selections(
7355 editor,
7356 indoc! {r#"
7357 use mod1::mod2::{mod3, mod4};
7358
7359 fn fn_1(param1: bool, param2: &str) {
7360 let var1 = "hˇello world";
7361 }
7362 "#},
7363 cx,
7364 );
7365 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7366 assert_text_with_selections(
7367 editor,
7368 indoc! {r#"
7369 use mod1::mod2::{mod3, mod4};
7370
7371 fn fn_1(param1: bool, param2: &str) {
7372 let var1 = "«ˇhello» world";
7373 }
7374 "#},
7375 cx,
7376 );
7377 });
7378
7379 // Test 2: Partial selection within a word
7380 editor.update_in(cx, |editor, window, cx| {
7381 editor.change_selections(None, window, cx, |s| {
7382 s.select_display_ranges([
7383 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
7384 ]);
7385 });
7386 });
7387 editor.update_in(cx, |editor, window, cx| {
7388 assert_text_with_selections(
7389 editor,
7390 indoc! {r#"
7391 use mod1::mod2::{mod3, mod4};
7392
7393 fn fn_1(param1: bool, param2: &str) {
7394 let var1 = "h«elˇ»lo world";
7395 }
7396 "#},
7397 cx,
7398 );
7399 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7400 assert_text_with_selections(
7401 editor,
7402 indoc! {r#"
7403 use mod1::mod2::{mod3, mod4};
7404
7405 fn fn_1(param1: bool, param2: &str) {
7406 let var1 = "«ˇhello» world";
7407 }
7408 "#},
7409 cx,
7410 );
7411 });
7412
7413 // Test 3: Complete word already selected
7414 editor.update_in(cx, |editor, window, cx| {
7415 editor.change_selections(None, window, cx, |s| {
7416 s.select_display_ranges([
7417 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
7418 ]);
7419 });
7420 });
7421 editor.update_in(cx, |editor, window, cx| {
7422 assert_text_with_selections(
7423 editor,
7424 indoc! {r#"
7425 use mod1::mod2::{mod3, mod4};
7426
7427 fn fn_1(param1: bool, param2: &str) {
7428 let var1 = "«helloˇ» world";
7429 }
7430 "#},
7431 cx,
7432 );
7433 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7434 assert_text_with_selections(
7435 editor,
7436 indoc! {r#"
7437 use mod1::mod2::{mod3, mod4};
7438
7439 fn fn_1(param1: bool, param2: &str) {
7440 let var1 = "«hello worldˇ»";
7441 }
7442 "#},
7443 cx,
7444 );
7445 });
7446
7447 // Test 4: Selection spanning across words
7448 editor.update_in(cx, |editor, window, cx| {
7449 editor.change_selections(None, window, cx, |s| {
7450 s.select_display_ranges([
7451 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
7452 ]);
7453 });
7454 });
7455 editor.update_in(cx, |editor, window, cx| {
7456 assert_text_with_selections(
7457 editor,
7458 indoc! {r#"
7459 use mod1::mod2::{mod3, mod4};
7460
7461 fn fn_1(param1: bool, param2: &str) {
7462 let var1 = "hel«lo woˇ»rld";
7463 }
7464 "#},
7465 cx,
7466 );
7467 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7468 assert_text_with_selections(
7469 editor,
7470 indoc! {r#"
7471 use mod1::mod2::{mod3, mod4};
7472
7473 fn fn_1(param1: bool, param2: &str) {
7474 let var1 = "«ˇhello world»";
7475 }
7476 "#},
7477 cx,
7478 );
7479 });
7480
7481 // Test 5: Expansion beyond string
7482 editor.update_in(cx, |editor, window, cx| {
7483 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7484 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7485 assert_text_with_selections(
7486 editor,
7487 indoc! {r#"
7488 use mod1::mod2::{mod3, mod4};
7489
7490 fn fn_1(param1: bool, param2: &str) {
7491 «ˇlet var1 = "hello world";»
7492 }
7493 "#},
7494 cx,
7495 );
7496 });
7497}
7498
7499#[gpui::test]
7500async fn test_fold_function_bodies(cx: &mut TestAppContext) {
7501 init_test(cx, |_| {});
7502
7503 let base_text = r#"
7504 impl A {
7505 // this is an uncommitted comment
7506
7507 fn b() {
7508 c();
7509 }
7510
7511 // this is another uncommitted comment
7512
7513 fn d() {
7514 // e
7515 // f
7516 }
7517 }
7518
7519 fn g() {
7520 // h
7521 }
7522 "#
7523 .unindent();
7524
7525 let text = r#"
7526 ˇimpl A {
7527
7528 fn b() {
7529 c();
7530 }
7531
7532 fn d() {
7533 // e
7534 // f
7535 }
7536 }
7537
7538 fn g() {
7539 // h
7540 }
7541 "#
7542 .unindent();
7543
7544 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7545 cx.set_state(&text);
7546 cx.set_head_text(&base_text);
7547 cx.update_editor(|editor, window, cx| {
7548 editor.expand_all_diff_hunks(&Default::default(), window, cx);
7549 });
7550
7551 cx.assert_state_with_diff(
7552 "
7553 ˇimpl A {
7554 - // this is an uncommitted comment
7555
7556 fn b() {
7557 c();
7558 }
7559
7560 - // this is another uncommitted comment
7561 -
7562 fn d() {
7563 // e
7564 // f
7565 }
7566 }
7567
7568 fn g() {
7569 // h
7570 }
7571 "
7572 .unindent(),
7573 );
7574
7575 let expected_display_text = "
7576 impl A {
7577 // this is an uncommitted comment
7578
7579 fn b() {
7580 ⋯
7581 }
7582
7583 // this is another uncommitted comment
7584
7585 fn d() {
7586 ⋯
7587 }
7588 }
7589
7590 fn g() {
7591 ⋯
7592 }
7593 "
7594 .unindent();
7595
7596 cx.update_editor(|editor, window, cx| {
7597 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
7598 assert_eq!(editor.display_text(cx), expected_display_text);
7599 });
7600}
7601
7602#[gpui::test]
7603async fn test_autoindent(cx: &mut TestAppContext) {
7604 init_test(cx, |_| {});
7605
7606 let language = Arc::new(
7607 Language::new(
7608 LanguageConfig {
7609 brackets: BracketPairConfig {
7610 pairs: vec![
7611 BracketPair {
7612 start: "{".to_string(),
7613 end: "}".to_string(),
7614 close: false,
7615 surround: false,
7616 newline: true,
7617 },
7618 BracketPair {
7619 start: "(".to_string(),
7620 end: ")".to_string(),
7621 close: false,
7622 surround: false,
7623 newline: true,
7624 },
7625 ],
7626 ..Default::default()
7627 },
7628 ..Default::default()
7629 },
7630 Some(tree_sitter_rust::LANGUAGE.into()),
7631 )
7632 .with_indents_query(
7633 r#"
7634 (_ "(" ")" @end) @indent
7635 (_ "{" "}" @end) @indent
7636 "#,
7637 )
7638 .unwrap(),
7639 );
7640
7641 let text = "fn a() {}";
7642
7643 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7644 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7645 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7646 editor
7647 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7648 .await;
7649
7650 editor.update_in(cx, |editor, window, cx| {
7651 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
7652 editor.newline(&Newline, window, cx);
7653 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
7654 assert_eq!(
7655 editor.selections.ranges(cx),
7656 &[
7657 Point::new(1, 4)..Point::new(1, 4),
7658 Point::new(3, 4)..Point::new(3, 4),
7659 Point::new(5, 0)..Point::new(5, 0)
7660 ]
7661 );
7662 });
7663}
7664
7665#[gpui::test]
7666async fn test_autoindent_selections(cx: &mut TestAppContext) {
7667 init_test(cx, |_| {});
7668
7669 {
7670 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7671 cx.set_state(indoc! {"
7672 impl A {
7673
7674 fn b() {}
7675
7676 «fn c() {
7677
7678 }ˇ»
7679 }
7680 "});
7681
7682 cx.update_editor(|editor, window, cx| {
7683 editor.autoindent(&Default::default(), window, cx);
7684 });
7685
7686 cx.assert_editor_state(indoc! {"
7687 impl A {
7688
7689 fn b() {}
7690
7691 «fn c() {
7692
7693 }ˇ»
7694 }
7695 "});
7696 }
7697
7698 {
7699 let mut cx = EditorTestContext::new_multibuffer(
7700 cx,
7701 [indoc! { "
7702 impl A {
7703 «
7704 // a
7705 fn b(){}
7706 »
7707 «
7708 }
7709 fn c(){}
7710 »
7711 "}],
7712 );
7713
7714 let buffer = cx.update_editor(|editor, _, cx| {
7715 let buffer = editor.buffer().update(cx, |buffer, _| {
7716 buffer.all_buffers().iter().next().unwrap().clone()
7717 });
7718 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7719 buffer
7720 });
7721
7722 cx.run_until_parked();
7723 cx.update_editor(|editor, window, cx| {
7724 editor.select_all(&Default::default(), window, cx);
7725 editor.autoindent(&Default::default(), window, cx)
7726 });
7727 cx.run_until_parked();
7728
7729 cx.update(|_, cx| {
7730 assert_eq!(
7731 buffer.read(cx).text(),
7732 indoc! { "
7733 impl A {
7734
7735 // a
7736 fn b(){}
7737
7738
7739 }
7740 fn c(){}
7741
7742 " }
7743 )
7744 });
7745 }
7746}
7747
7748#[gpui::test]
7749async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
7750 init_test(cx, |_| {});
7751
7752 let mut cx = EditorTestContext::new(cx).await;
7753
7754 let language = Arc::new(Language::new(
7755 LanguageConfig {
7756 brackets: BracketPairConfig {
7757 pairs: vec![
7758 BracketPair {
7759 start: "{".to_string(),
7760 end: "}".to_string(),
7761 close: true,
7762 surround: true,
7763 newline: true,
7764 },
7765 BracketPair {
7766 start: "(".to_string(),
7767 end: ")".to_string(),
7768 close: true,
7769 surround: true,
7770 newline: true,
7771 },
7772 BracketPair {
7773 start: "/*".to_string(),
7774 end: " */".to_string(),
7775 close: true,
7776 surround: true,
7777 newline: true,
7778 },
7779 BracketPair {
7780 start: "[".to_string(),
7781 end: "]".to_string(),
7782 close: false,
7783 surround: false,
7784 newline: true,
7785 },
7786 BracketPair {
7787 start: "\"".to_string(),
7788 end: "\"".to_string(),
7789 close: true,
7790 surround: true,
7791 newline: false,
7792 },
7793 BracketPair {
7794 start: "<".to_string(),
7795 end: ">".to_string(),
7796 close: false,
7797 surround: true,
7798 newline: true,
7799 },
7800 ],
7801 ..Default::default()
7802 },
7803 autoclose_before: "})]".to_string(),
7804 ..Default::default()
7805 },
7806 Some(tree_sitter_rust::LANGUAGE.into()),
7807 ));
7808
7809 cx.language_registry().add(language.clone());
7810 cx.update_buffer(|buffer, cx| {
7811 buffer.set_language(Some(language), cx);
7812 });
7813
7814 cx.set_state(
7815 &r#"
7816 🏀ˇ
7817 εˇ
7818 ❤️ˇ
7819 "#
7820 .unindent(),
7821 );
7822
7823 // autoclose multiple nested brackets at multiple cursors
7824 cx.update_editor(|editor, window, cx| {
7825 editor.handle_input("{", window, cx);
7826 editor.handle_input("{", window, cx);
7827 editor.handle_input("{", window, cx);
7828 });
7829 cx.assert_editor_state(
7830 &"
7831 🏀{{{ˇ}}}
7832 ε{{{ˇ}}}
7833 ❤️{{{ˇ}}}
7834 "
7835 .unindent(),
7836 );
7837
7838 // insert a different closing bracket
7839 cx.update_editor(|editor, window, cx| {
7840 editor.handle_input(")", window, cx);
7841 });
7842 cx.assert_editor_state(
7843 &"
7844 🏀{{{)ˇ}}}
7845 ε{{{)ˇ}}}
7846 ❤️{{{)ˇ}}}
7847 "
7848 .unindent(),
7849 );
7850
7851 // skip over the auto-closed brackets when typing a closing bracket
7852 cx.update_editor(|editor, window, cx| {
7853 editor.move_right(&MoveRight, window, cx);
7854 editor.handle_input("}", window, cx);
7855 editor.handle_input("}", window, cx);
7856 editor.handle_input("}", window, cx);
7857 });
7858 cx.assert_editor_state(
7859 &"
7860 🏀{{{)}}}}ˇ
7861 ε{{{)}}}}ˇ
7862 ❤️{{{)}}}}ˇ
7863 "
7864 .unindent(),
7865 );
7866
7867 // autoclose multi-character pairs
7868 cx.set_state(
7869 &"
7870 ˇ
7871 ˇ
7872 "
7873 .unindent(),
7874 );
7875 cx.update_editor(|editor, window, cx| {
7876 editor.handle_input("/", window, cx);
7877 editor.handle_input("*", window, cx);
7878 });
7879 cx.assert_editor_state(
7880 &"
7881 /*ˇ */
7882 /*ˇ */
7883 "
7884 .unindent(),
7885 );
7886
7887 // one cursor autocloses a multi-character pair, one cursor
7888 // does not autoclose.
7889 cx.set_state(
7890 &"
7891 /ˇ
7892 ˇ
7893 "
7894 .unindent(),
7895 );
7896 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
7897 cx.assert_editor_state(
7898 &"
7899 /*ˇ */
7900 *ˇ
7901 "
7902 .unindent(),
7903 );
7904
7905 // Don't autoclose if the next character isn't whitespace and isn't
7906 // listed in the language's "autoclose_before" section.
7907 cx.set_state("ˇa b");
7908 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7909 cx.assert_editor_state("{ˇa b");
7910
7911 // Don't autoclose if `close` is false for the bracket pair
7912 cx.set_state("ˇ");
7913 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
7914 cx.assert_editor_state("[ˇ");
7915
7916 // Surround with brackets if text is selected
7917 cx.set_state("«aˇ» b");
7918 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7919 cx.assert_editor_state("{«aˇ»} b");
7920
7921 // Autoclose when not immediately after a word character
7922 cx.set_state("a ˇ");
7923 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7924 cx.assert_editor_state("a \"ˇ\"");
7925
7926 // Autoclose pair where the start and end characters are the same
7927 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7928 cx.assert_editor_state("a \"\"ˇ");
7929
7930 // Don't autoclose when immediately after a word character
7931 cx.set_state("aˇ");
7932 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7933 cx.assert_editor_state("a\"ˇ");
7934
7935 // Do autoclose when after a non-word character
7936 cx.set_state("{ˇ");
7937 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7938 cx.assert_editor_state("{\"ˇ\"");
7939
7940 // Non identical pairs autoclose regardless of preceding character
7941 cx.set_state("aˇ");
7942 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7943 cx.assert_editor_state("a{ˇ}");
7944
7945 // Don't autoclose pair if autoclose is disabled
7946 cx.set_state("ˇ");
7947 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7948 cx.assert_editor_state("<ˇ");
7949
7950 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
7951 cx.set_state("«aˇ» b");
7952 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7953 cx.assert_editor_state("<«aˇ»> b");
7954}
7955
7956#[gpui::test]
7957async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
7958 init_test(cx, |settings| {
7959 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7960 });
7961
7962 let mut cx = EditorTestContext::new(cx).await;
7963
7964 let language = Arc::new(Language::new(
7965 LanguageConfig {
7966 brackets: BracketPairConfig {
7967 pairs: vec![
7968 BracketPair {
7969 start: "{".to_string(),
7970 end: "}".to_string(),
7971 close: true,
7972 surround: true,
7973 newline: true,
7974 },
7975 BracketPair {
7976 start: "(".to_string(),
7977 end: ")".to_string(),
7978 close: true,
7979 surround: true,
7980 newline: true,
7981 },
7982 BracketPair {
7983 start: "[".to_string(),
7984 end: "]".to_string(),
7985 close: false,
7986 surround: false,
7987 newline: true,
7988 },
7989 ],
7990 ..Default::default()
7991 },
7992 autoclose_before: "})]".to_string(),
7993 ..Default::default()
7994 },
7995 Some(tree_sitter_rust::LANGUAGE.into()),
7996 ));
7997
7998 cx.language_registry().add(language.clone());
7999 cx.update_buffer(|buffer, cx| {
8000 buffer.set_language(Some(language), cx);
8001 });
8002
8003 cx.set_state(
8004 &"
8005 ˇ
8006 ˇ
8007 ˇ
8008 "
8009 .unindent(),
8010 );
8011
8012 // ensure only matching closing brackets are skipped over
8013 cx.update_editor(|editor, window, cx| {
8014 editor.handle_input("}", window, cx);
8015 editor.move_left(&MoveLeft, window, cx);
8016 editor.handle_input(")", window, cx);
8017 editor.move_left(&MoveLeft, window, cx);
8018 });
8019 cx.assert_editor_state(
8020 &"
8021 ˇ)}
8022 ˇ)}
8023 ˇ)}
8024 "
8025 .unindent(),
8026 );
8027
8028 // skip-over closing brackets at multiple cursors
8029 cx.update_editor(|editor, window, cx| {
8030 editor.handle_input(")", window, cx);
8031 editor.handle_input("}", window, cx);
8032 });
8033 cx.assert_editor_state(
8034 &"
8035 )}ˇ
8036 )}ˇ
8037 )}ˇ
8038 "
8039 .unindent(),
8040 );
8041
8042 // ignore non-close brackets
8043 cx.update_editor(|editor, window, cx| {
8044 editor.handle_input("]", window, cx);
8045 editor.move_left(&MoveLeft, window, cx);
8046 editor.handle_input("]", window, cx);
8047 });
8048 cx.assert_editor_state(
8049 &"
8050 )}]ˇ]
8051 )}]ˇ]
8052 )}]ˇ]
8053 "
8054 .unindent(),
8055 );
8056}
8057
8058#[gpui::test]
8059async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
8060 init_test(cx, |_| {});
8061
8062 let mut cx = EditorTestContext::new(cx).await;
8063
8064 let html_language = Arc::new(
8065 Language::new(
8066 LanguageConfig {
8067 name: "HTML".into(),
8068 brackets: BracketPairConfig {
8069 pairs: vec![
8070 BracketPair {
8071 start: "<".into(),
8072 end: ">".into(),
8073 close: true,
8074 ..Default::default()
8075 },
8076 BracketPair {
8077 start: "{".into(),
8078 end: "}".into(),
8079 close: true,
8080 ..Default::default()
8081 },
8082 BracketPair {
8083 start: "(".into(),
8084 end: ")".into(),
8085 close: true,
8086 ..Default::default()
8087 },
8088 ],
8089 ..Default::default()
8090 },
8091 autoclose_before: "})]>".into(),
8092 ..Default::default()
8093 },
8094 Some(tree_sitter_html::LANGUAGE.into()),
8095 )
8096 .with_injection_query(
8097 r#"
8098 (script_element
8099 (raw_text) @injection.content
8100 (#set! injection.language "javascript"))
8101 "#,
8102 )
8103 .unwrap(),
8104 );
8105
8106 let javascript_language = Arc::new(Language::new(
8107 LanguageConfig {
8108 name: "JavaScript".into(),
8109 brackets: BracketPairConfig {
8110 pairs: vec![
8111 BracketPair {
8112 start: "/*".into(),
8113 end: " */".into(),
8114 close: true,
8115 ..Default::default()
8116 },
8117 BracketPair {
8118 start: "{".into(),
8119 end: "}".into(),
8120 close: true,
8121 ..Default::default()
8122 },
8123 BracketPair {
8124 start: "(".into(),
8125 end: ")".into(),
8126 close: true,
8127 ..Default::default()
8128 },
8129 ],
8130 ..Default::default()
8131 },
8132 autoclose_before: "})]>".into(),
8133 ..Default::default()
8134 },
8135 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8136 ));
8137
8138 cx.language_registry().add(html_language.clone());
8139 cx.language_registry().add(javascript_language.clone());
8140
8141 cx.update_buffer(|buffer, cx| {
8142 buffer.set_language(Some(html_language), cx);
8143 });
8144
8145 cx.set_state(
8146 &r#"
8147 <body>ˇ
8148 <script>
8149 var x = 1;ˇ
8150 </script>
8151 </body>ˇ
8152 "#
8153 .unindent(),
8154 );
8155
8156 // Precondition: different languages are active at different locations.
8157 cx.update_editor(|editor, window, cx| {
8158 let snapshot = editor.snapshot(window, cx);
8159 let cursors = editor.selections.ranges::<usize>(cx);
8160 let languages = cursors
8161 .iter()
8162 .map(|c| snapshot.language_at(c.start).unwrap().name())
8163 .collect::<Vec<_>>();
8164 assert_eq!(
8165 languages,
8166 &["HTML".into(), "JavaScript".into(), "HTML".into()]
8167 );
8168 });
8169
8170 // Angle brackets autoclose in HTML, but not JavaScript.
8171 cx.update_editor(|editor, window, cx| {
8172 editor.handle_input("<", window, cx);
8173 editor.handle_input("a", window, cx);
8174 });
8175 cx.assert_editor_state(
8176 &r#"
8177 <body><aˇ>
8178 <script>
8179 var x = 1;<aˇ
8180 </script>
8181 </body><aˇ>
8182 "#
8183 .unindent(),
8184 );
8185
8186 // Curly braces and parens autoclose in both HTML and JavaScript.
8187 cx.update_editor(|editor, window, cx| {
8188 editor.handle_input(" b=", window, cx);
8189 editor.handle_input("{", window, cx);
8190 editor.handle_input("c", window, cx);
8191 editor.handle_input("(", window, cx);
8192 });
8193 cx.assert_editor_state(
8194 &r#"
8195 <body><a b={c(ˇ)}>
8196 <script>
8197 var x = 1;<a b={c(ˇ)}
8198 </script>
8199 </body><a b={c(ˇ)}>
8200 "#
8201 .unindent(),
8202 );
8203
8204 // Brackets that were already autoclosed are skipped.
8205 cx.update_editor(|editor, window, cx| {
8206 editor.handle_input(")", window, cx);
8207 editor.handle_input("d", window, cx);
8208 editor.handle_input("}", window, cx);
8209 });
8210 cx.assert_editor_state(
8211 &r#"
8212 <body><a b={c()d}ˇ>
8213 <script>
8214 var x = 1;<a b={c()d}ˇ
8215 </script>
8216 </body><a b={c()d}ˇ>
8217 "#
8218 .unindent(),
8219 );
8220 cx.update_editor(|editor, window, cx| {
8221 editor.handle_input(">", window, cx);
8222 });
8223 cx.assert_editor_state(
8224 &r#"
8225 <body><a b={c()d}>ˇ
8226 <script>
8227 var x = 1;<a b={c()d}>ˇ
8228 </script>
8229 </body><a b={c()d}>ˇ
8230 "#
8231 .unindent(),
8232 );
8233
8234 // Reset
8235 cx.set_state(
8236 &r#"
8237 <body>ˇ
8238 <script>
8239 var x = 1;ˇ
8240 </script>
8241 </body>ˇ
8242 "#
8243 .unindent(),
8244 );
8245
8246 cx.update_editor(|editor, window, cx| {
8247 editor.handle_input("<", window, cx);
8248 });
8249 cx.assert_editor_state(
8250 &r#"
8251 <body><ˇ>
8252 <script>
8253 var x = 1;<ˇ
8254 </script>
8255 </body><ˇ>
8256 "#
8257 .unindent(),
8258 );
8259
8260 // When backspacing, the closing angle brackets are removed.
8261 cx.update_editor(|editor, window, cx| {
8262 editor.backspace(&Backspace, window, cx);
8263 });
8264 cx.assert_editor_state(
8265 &r#"
8266 <body>ˇ
8267 <script>
8268 var x = 1;ˇ
8269 </script>
8270 </body>ˇ
8271 "#
8272 .unindent(),
8273 );
8274
8275 // Block comments autoclose in JavaScript, but not HTML.
8276 cx.update_editor(|editor, window, cx| {
8277 editor.handle_input("/", window, cx);
8278 editor.handle_input("*", window, cx);
8279 });
8280 cx.assert_editor_state(
8281 &r#"
8282 <body>/*ˇ
8283 <script>
8284 var x = 1;/*ˇ */
8285 </script>
8286 </body>/*ˇ
8287 "#
8288 .unindent(),
8289 );
8290}
8291
8292#[gpui::test]
8293async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
8294 init_test(cx, |_| {});
8295
8296 let mut cx = EditorTestContext::new(cx).await;
8297
8298 let rust_language = Arc::new(
8299 Language::new(
8300 LanguageConfig {
8301 name: "Rust".into(),
8302 brackets: serde_json::from_value(json!([
8303 { "start": "{", "end": "}", "close": true, "newline": true },
8304 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
8305 ]))
8306 .unwrap(),
8307 autoclose_before: "})]>".into(),
8308 ..Default::default()
8309 },
8310 Some(tree_sitter_rust::LANGUAGE.into()),
8311 )
8312 .with_override_query("(string_literal) @string")
8313 .unwrap(),
8314 );
8315
8316 cx.language_registry().add(rust_language.clone());
8317 cx.update_buffer(|buffer, cx| {
8318 buffer.set_language(Some(rust_language), cx);
8319 });
8320
8321 cx.set_state(
8322 &r#"
8323 let x = ˇ
8324 "#
8325 .unindent(),
8326 );
8327
8328 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
8329 cx.update_editor(|editor, window, cx| {
8330 editor.handle_input("\"", window, cx);
8331 });
8332 cx.assert_editor_state(
8333 &r#"
8334 let x = "ˇ"
8335 "#
8336 .unindent(),
8337 );
8338
8339 // Inserting another quotation mark. The cursor moves across the existing
8340 // automatically-inserted quotation mark.
8341 cx.update_editor(|editor, window, cx| {
8342 editor.handle_input("\"", window, cx);
8343 });
8344 cx.assert_editor_state(
8345 &r#"
8346 let x = ""ˇ
8347 "#
8348 .unindent(),
8349 );
8350
8351 // Reset
8352 cx.set_state(
8353 &r#"
8354 let x = ˇ
8355 "#
8356 .unindent(),
8357 );
8358
8359 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
8360 cx.update_editor(|editor, window, cx| {
8361 editor.handle_input("\"", window, cx);
8362 editor.handle_input(" ", window, cx);
8363 editor.move_left(&Default::default(), window, cx);
8364 editor.handle_input("\\", window, cx);
8365 editor.handle_input("\"", window, cx);
8366 });
8367 cx.assert_editor_state(
8368 &r#"
8369 let x = "\"ˇ "
8370 "#
8371 .unindent(),
8372 );
8373
8374 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
8375 // mark. Nothing is inserted.
8376 cx.update_editor(|editor, window, cx| {
8377 editor.move_right(&Default::default(), window, cx);
8378 editor.handle_input("\"", window, cx);
8379 });
8380 cx.assert_editor_state(
8381 &r#"
8382 let x = "\" "ˇ
8383 "#
8384 .unindent(),
8385 );
8386}
8387
8388#[gpui::test]
8389async fn test_surround_with_pair(cx: &mut TestAppContext) {
8390 init_test(cx, |_| {});
8391
8392 let language = Arc::new(Language::new(
8393 LanguageConfig {
8394 brackets: BracketPairConfig {
8395 pairs: vec![
8396 BracketPair {
8397 start: "{".to_string(),
8398 end: "}".to_string(),
8399 close: true,
8400 surround: true,
8401 newline: true,
8402 },
8403 BracketPair {
8404 start: "/* ".to_string(),
8405 end: "*/".to_string(),
8406 close: true,
8407 surround: true,
8408 ..Default::default()
8409 },
8410 ],
8411 ..Default::default()
8412 },
8413 ..Default::default()
8414 },
8415 Some(tree_sitter_rust::LANGUAGE.into()),
8416 ));
8417
8418 let text = r#"
8419 a
8420 b
8421 c
8422 "#
8423 .unindent();
8424
8425 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8426 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8427 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8428 editor
8429 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8430 .await;
8431
8432 editor.update_in(cx, |editor, window, cx| {
8433 editor.change_selections(None, window, cx, |s| {
8434 s.select_display_ranges([
8435 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8436 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8437 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
8438 ])
8439 });
8440
8441 editor.handle_input("{", window, cx);
8442 editor.handle_input("{", window, cx);
8443 editor.handle_input("{", window, cx);
8444 assert_eq!(
8445 editor.text(cx),
8446 "
8447 {{{a}}}
8448 {{{b}}}
8449 {{{c}}}
8450 "
8451 .unindent()
8452 );
8453 assert_eq!(
8454 editor.selections.display_ranges(cx),
8455 [
8456 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
8457 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
8458 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
8459 ]
8460 );
8461
8462 editor.undo(&Undo, window, cx);
8463 editor.undo(&Undo, window, cx);
8464 editor.undo(&Undo, window, cx);
8465 assert_eq!(
8466 editor.text(cx),
8467 "
8468 a
8469 b
8470 c
8471 "
8472 .unindent()
8473 );
8474 assert_eq!(
8475 editor.selections.display_ranges(cx),
8476 [
8477 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8478 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8479 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8480 ]
8481 );
8482
8483 // Ensure inserting the first character of a multi-byte bracket pair
8484 // doesn't surround the selections with the bracket.
8485 editor.handle_input("/", window, cx);
8486 assert_eq!(
8487 editor.text(cx),
8488 "
8489 /
8490 /
8491 /
8492 "
8493 .unindent()
8494 );
8495 assert_eq!(
8496 editor.selections.display_ranges(cx),
8497 [
8498 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8499 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8500 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8501 ]
8502 );
8503
8504 editor.undo(&Undo, window, cx);
8505 assert_eq!(
8506 editor.text(cx),
8507 "
8508 a
8509 b
8510 c
8511 "
8512 .unindent()
8513 );
8514 assert_eq!(
8515 editor.selections.display_ranges(cx),
8516 [
8517 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8518 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8519 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8520 ]
8521 );
8522
8523 // Ensure inserting the last character of a multi-byte bracket pair
8524 // doesn't surround the selections with the bracket.
8525 editor.handle_input("*", window, cx);
8526 assert_eq!(
8527 editor.text(cx),
8528 "
8529 *
8530 *
8531 *
8532 "
8533 .unindent()
8534 );
8535 assert_eq!(
8536 editor.selections.display_ranges(cx),
8537 [
8538 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8539 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8540 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8541 ]
8542 );
8543 });
8544}
8545
8546#[gpui::test]
8547async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
8548 init_test(cx, |_| {});
8549
8550 let language = Arc::new(Language::new(
8551 LanguageConfig {
8552 brackets: BracketPairConfig {
8553 pairs: vec![BracketPair {
8554 start: "{".to_string(),
8555 end: "}".to_string(),
8556 close: true,
8557 surround: true,
8558 newline: true,
8559 }],
8560 ..Default::default()
8561 },
8562 autoclose_before: "}".to_string(),
8563 ..Default::default()
8564 },
8565 Some(tree_sitter_rust::LANGUAGE.into()),
8566 ));
8567
8568 let text = r#"
8569 a
8570 b
8571 c
8572 "#
8573 .unindent();
8574
8575 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8576 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8577 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8578 editor
8579 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8580 .await;
8581
8582 editor.update_in(cx, |editor, window, cx| {
8583 editor.change_selections(None, window, cx, |s| {
8584 s.select_ranges([
8585 Point::new(0, 1)..Point::new(0, 1),
8586 Point::new(1, 1)..Point::new(1, 1),
8587 Point::new(2, 1)..Point::new(2, 1),
8588 ])
8589 });
8590
8591 editor.handle_input("{", window, cx);
8592 editor.handle_input("{", window, cx);
8593 editor.handle_input("_", window, cx);
8594 assert_eq!(
8595 editor.text(cx),
8596 "
8597 a{{_}}
8598 b{{_}}
8599 c{{_}}
8600 "
8601 .unindent()
8602 );
8603 assert_eq!(
8604 editor.selections.ranges::<Point>(cx),
8605 [
8606 Point::new(0, 4)..Point::new(0, 4),
8607 Point::new(1, 4)..Point::new(1, 4),
8608 Point::new(2, 4)..Point::new(2, 4)
8609 ]
8610 );
8611
8612 editor.backspace(&Default::default(), window, cx);
8613 editor.backspace(&Default::default(), window, cx);
8614 assert_eq!(
8615 editor.text(cx),
8616 "
8617 a{}
8618 b{}
8619 c{}
8620 "
8621 .unindent()
8622 );
8623 assert_eq!(
8624 editor.selections.ranges::<Point>(cx),
8625 [
8626 Point::new(0, 2)..Point::new(0, 2),
8627 Point::new(1, 2)..Point::new(1, 2),
8628 Point::new(2, 2)..Point::new(2, 2)
8629 ]
8630 );
8631
8632 editor.delete_to_previous_word_start(&Default::default(), window, cx);
8633 assert_eq!(
8634 editor.text(cx),
8635 "
8636 a
8637 b
8638 c
8639 "
8640 .unindent()
8641 );
8642 assert_eq!(
8643 editor.selections.ranges::<Point>(cx),
8644 [
8645 Point::new(0, 1)..Point::new(0, 1),
8646 Point::new(1, 1)..Point::new(1, 1),
8647 Point::new(2, 1)..Point::new(2, 1)
8648 ]
8649 );
8650 });
8651}
8652
8653#[gpui::test]
8654async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
8655 init_test(cx, |settings| {
8656 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8657 });
8658
8659 let mut cx = EditorTestContext::new(cx).await;
8660
8661 let language = Arc::new(Language::new(
8662 LanguageConfig {
8663 brackets: BracketPairConfig {
8664 pairs: vec![
8665 BracketPair {
8666 start: "{".to_string(),
8667 end: "}".to_string(),
8668 close: true,
8669 surround: true,
8670 newline: true,
8671 },
8672 BracketPair {
8673 start: "(".to_string(),
8674 end: ")".to_string(),
8675 close: true,
8676 surround: true,
8677 newline: true,
8678 },
8679 BracketPair {
8680 start: "[".to_string(),
8681 end: "]".to_string(),
8682 close: false,
8683 surround: true,
8684 newline: true,
8685 },
8686 ],
8687 ..Default::default()
8688 },
8689 autoclose_before: "})]".to_string(),
8690 ..Default::default()
8691 },
8692 Some(tree_sitter_rust::LANGUAGE.into()),
8693 ));
8694
8695 cx.language_registry().add(language.clone());
8696 cx.update_buffer(|buffer, cx| {
8697 buffer.set_language(Some(language), cx);
8698 });
8699
8700 cx.set_state(
8701 &"
8702 {(ˇ)}
8703 [[ˇ]]
8704 {(ˇ)}
8705 "
8706 .unindent(),
8707 );
8708
8709 cx.update_editor(|editor, window, cx| {
8710 editor.backspace(&Default::default(), window, cx);
8711 editor.backspace(&Default::default(), window, cx);
8712 });
8713
8714 cx.assert_editor_state(
8715 &"
8716 ˇ
8717 ˇ]]
8718 ˇ
8719 "
8720 .unindent(),
8721 );
8722
8723 cx.update_editor(|editor, window, cx| {
8724 editor.handle_input("{", window, cx);
8725 editor.handle_input("{", window, cx);
8726 editor.move_right(&MoveRight, window, cx);
8727 editor.move_right(&MoveRight, window, cx);
8728 editor.move_left(&MoveLeft, window, cx);
8729 editor.move_left(&MoveLeft, window, cx);
8730 editor.backspace(&Default::default(), window, cx);
8731 });
8732
8733 cx.assert_editor_state(
8734 &"
8735 {ˇ}
8736 {ˇ}]]
8737 {ˇ}
8738 "
8739 .unindent(),
8740 );
8741
8742 cx.update_editor(|editor, window, cx| {
8743 editor.backspace(&Default::default(), window, cx);
8744 });
8745
8746 cx.assert_editor_state(
8747 &"
8748 ˇ
8749 ˇ]]
8750 ˇ
8751 "
8752 .unindent(),
8753 );
8754}
8755
8756#[gpui::test]
8757async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
8758 init_test(cx, |_| {});
8759
8760 let language = Arc::new(Language::new(
8761 LanguageConfig::default(),
8762 Some(tree_sitter_rust::LANGUAGE.into()),
8763 ));
8764
8765 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
8766 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8767 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8768 editor
8769 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8770 .await;
8771
8772 editor.update_in(cx, |editor, window, cx| {
8773 editor.set_auto_replace_emoji_shortcode(true);
8774
8775 editor.handle_input("Hello ", window, cx);
8776 editor.handle_input(":wave", window, cx);
8777 assert_eq!(editor.text(cx), "Hello :wave".unindent());
8778
8779 editor.handle_input(":", window, cx);
8780 assert_eq!(editor.text(cx), "Hello 👋".unindent());
8781
8782 editor.handle_input(" :smile", window, cx);
8783 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
8784
8785 editor.handle_input(":", window, cx);
8786 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
8787
8788 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
8789 editor.handle_input(":wave", window, cx);
8790 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
8791
8792 editor.handle_input(":", window, cx);
8793 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
8794
8795 editor.handle_input(":1", window, cx);
8796 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
8797
8798 editor.handle_input(":", window, cx);
8799 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
8800
8801 // Ensure shortcode does not get replaced when it is part of a word
8802 editor.handle_input(" Test:wave", window, cx);
8803 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
8804
8805 editor.handle_input(":", window, cx);
8806 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
8807
8808 editor.set_auto_replace_emoji_shortcode(false);
8809
8810 // Ensure shortcode does not get replaced when auto replace is off
8811 editor.handle_input(" :wave", window, cx);
8812 assert_eq!(
8813 editor.text(cx),
8814 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
8815 );
8816
8817 editor.handle_input(":", window, cx);
8818 assert_eq!(
8819 editor.text(cx),
8820 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
8821 );
8822 });
8823}
8824
8825#[gpui::test]
8826async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
8827 init_test(cx, |_| {});
8828
8829 let (text, insertion_ranges) = marked_text_ranges(
8830 indoc! {"
8831 ˇ
8832 "},
8833 false,
8834 );
8835
8836 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
8837 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8838
8839 _ = editor.update_in(cx, |editor, window, cx| {
8840 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
8841
8842 editor
8843 .insert_snippet(&insertion_ranges, snippet, window, cx)
8844 .unwrap();
8845
8846 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
8847 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
8848 assert_eq!(editor.text(cx), expected_text);
8849 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
8850 }
8851
8852 assert(
8853 editor,
8854 cx,
8855 indoc! {"
8856 type «» =•
8857 "},
8858 );
8859
8860 assert!(editor.context_menu_visible(), "There should be a matches");
8861 });
8862}
8863
8864#[gpui::test]
8865async fn test_snippets(cx: &mut TestAppContext) {
8866 init_test(cx, |_| {});
8867
8868 let mut cx = EditorTestContext::new(cx).await;
8869
8870 cx.set_state(indoc! {"
8871 a.ˇ b
8872 a.ˇ b
8873 a.ˇ b
8874 "});
8875
8876 cx.update_editor(|editor, window, cx| {
8877 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
8878 let insertion_ranges = editor
8879 .selections
8880 .all(cx)
8881 .iter()
8882 .map(|s| s.range().clone())
8883 .collect::<Vec<_>>();
8884 editor
8885 .insert_snippet(&insertion_ranges, snippet, window, cx)
8886 .unwrap();
8887 });
8888
8889 cx.assert_editor_state(indoc! {"
8890 a.f(«oneˇ», two, «threeˇ») b
8891 a.f(«oneˇ», two, «threeˇ») b
8892 a.f(«oneˇ», two, «threeˇ») b
8893 "});
8894
8895 // Can't move earlier than the first tab stop
8896 cx.update_editor(|editor, window, cx| {
8897 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
8898 });
8899 cx.assert_editor_state(indoc! {"
8900 a.f(«oneˇ», two, «threeˇ») b
8901 a.f(«oneˇ», two, «threeˇ») b
8902 a.f(«oneˇ», two, «threeˇ») b
8903 "});
8904
8905 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
8906 cx.assert_editor_state(indoc! {"
8907 a.f(one, «twoˇ», three) b
8908 a.f(one, «twoˇ», three) b
8909 a.f(one, «twoˇ», three) b
8910 "});
8911
8912 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
8913 cx.assert_editor_state(indoc! {"
8914 a.f(«oneˇ», two, «threeˇ») b
8915 a.f(«oneˇ», two, «threeˇ») b
8916 a.f(«oneˇ», two, «threeˇ») b
8917 "});
8918
8919 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
8920 cx.assert_editor_state(indoc! {"
8921 a.f(one, «twoˇ», three) b
8922 a.f(one, «twoˇ», three) b
8923 a.f(one, «twoˇ», three) b
8924 "});
8925 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
8926 cx.assert_editor_state(indoc! {"
8927 a.f(one, two, three)ˇ b
8928 a.f(one, two, three)ˇ b
8929 a.f(one, two, three)ˇ b
8930 "});
8931
8932 // As soon as the last tab stop is reached, snippet state is gone
8933 cx.update_editor(|editor, window, cx| {
8934 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
8935 });
8936 cx.assert_editor_state(indoc! {"
8937 a.f(one, two, three)ˇ b
8938 a.f(one, two, three)ˇ b
8939 a.f(one, two, three)ˇ b
8940 "});
8941}
8942
8943#[gpui::test]
8944async fn test_snippet_indentation(cx: &mut TestAppContext) {
8945 init_test(cx, |_| {});
8946
8947 let mut cx = EditorTestContext::new(cx).await;
8948
8949 cx.update_editor(|editor, window, cx| {
8950 let snippet = Snippet::parse(indoc! {"
8951 /*
8952 * Multiline comment with leading indentation
8953 *
8954 * $1
8955 */
8956 $0"})
8957 .unwrap();
8958 let insertion_ranges = editor
8959 .selections
8960 .all(cx)
8961 .iter()
8962 .map(|s| s.range().clone())
8963 .collect::<Vec<_>>();
8964 editor
8965 .insert_snippet(&insertion_ranges, snippet, window, cx)
8966 .unwrap();
8967 });
8968
8969 cx.assert_editor_state(indoc! {"
8970 /*
8971 * Multiline comment with leading indentation
8972 *
8973 * ˇ
8974 */
8975 "});
8976
8977 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
8978 cx.assert_editor_state(indoc! {"
8979 /*
8980 * Multiline comment with leading indentation
8981 *
8982 *•
8983 */
8984 ˇ"});
8985}
8986
8987#[gpui::test]
8988async fn test_document_format_during_save(cx: &mut TestAppContext) {
8989 init_test(cx, |_| {});
8990
8991 let fs = FakeFs::new(cx.executor());
8992 fs.insert_file(path!("/file.rs"), Default::default()).await;
8993
8994 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
8995
8996 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8997 language_registry.add(rust_lang());
8998 let mut fake_servers = language_registry.register_fake_lsp(
8999 "Rust",
9000 FakeLspAdapter {
9001 capabilities: lsp::ServerCapabilities {
9002 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9003 ..Default::default()
9004 },
9005 ..Default::default()
9006 },
9007 );
9008
9009 let buffer = project
9010 .update(cx, |project, cx| {
9011 project.open_local_buffer(path!("/file.rs"), cx)
9012 })
9013 .await
9014 .unwrap();
9015
9016 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9017 let (editor, cx) = cx.add_window_view(|window, cx| {
9018 build_editor_with_project(project.clone(), buffer, window, cx)
9019 });
9020 editor.update_in(cx, |editor, window, cx| {
9021 editor.set_text("one\ntwo\nthree\n", window, cx)
9022 });
9023 assert!(cx.read(|cx| editor.is_dirty(cx)));
9024
9025 cx.executor().start_waiting();
9026 let fake_server = fake_servers.next().await.unwrap();
9027
9028 {
9029 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9030 move |params, _| async move {
9031 assert_eq!(
9032 params.text_document.uri,
9033 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9034 );
9035 assert_eq!(params.options.tab_size, 4);
9036 Ok(Some(vec![lsp::TextEdit::new(
9037 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9038 ", ".to_string(),
9039 )]))
9040 },
9041 );
9042 let save = editor
9043 .update_in(cx, |editor, window, cx| {
9044 editor.save(true, project.clone(), window, cx)
9045 })
9046 .unwrap();
9047 cx.executor().start_waiting();
9048 save.await;
9049
9050 assert_eq!(
9051 editor.update(cx, |editor, cx| editor.text(cx)),
9052 "one, two\nthree\n"
9053 );
9054 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9055 }
9056
9057 {
9058 editor.update_in(cx, |editor, window, cx| {
9059 editor.set_text("one\ntwo\nthree\n", window, cx)
9060 });
9061 assert!(cx.read(|cx| editor.is_dirty(cx)));
9062
9063 // Ensure we can still save even if formatting hangs.
9064 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9065 move |params, _| async move {
9066 assert_eq!(
9067 params.text_document.uri,
9068 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9069 );
9070 futures::future::pending::<()>().await;
9071 unreachable!()
9072 },
9073 );
9074 let save = editor
9075 .update_in(cx, |editor, window, cx| {
9076 editor.save(true, project.clone(), window, cx)
9077 })
9078 .unwrap();
9079 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9080 cx.executor().start_waiting();
9081 save.await;
9082 assert_eq!(
9083 editor.update(cx, |editor, cx| editor.text(cx)),
9084 "one\ntwo\nthree\n"
9085 );
9086 }
9087
9088 // For non-dirty buffer and the corresponding settings, no formatting request should be sent
9089 {
9090 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9091 cx.update_global::<SettingsStore, _>(|settings, cx| {
9092 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9093 settings.save_non_dirty_buffers = Some(false);
9094 });
9095 });
9096
9097 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
9098 panic!("Should not be invoked on non-dirty buffer when configured so");
9099 });
9100 let save = editor
9101 .update_in(cx, |editor, window, cx| {
9102 editor.save(true, project.clone(), window, cx)
9103 })
9104 .unwrap();
9105 cx.executor().start_waiting();
9106 save.await;
9107
9108 assert_eq!(
9109 editor.update(cx, |editor, cx| editor.text(cx)),
9110 "one\ntwo\nthree\n"
9111 );
9112 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9113 }
9114
9115 cx.update_global::<SettingsStore, _>(|settings, cx| {
9116 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9117 settings.save_non_dirty_buffers = Some(false);
9118 });
9119 });
9120 // Set rust language override and assert overridden tabsize is sent to language server
9121 update_test_language_settings(cx, |settings| {
9122 settings.languages.insert(
9123 "Rust".into(),
9124 LanguageSettingsContent {
9125 tab_size: NonZeroU32::new(8),
9126 ..Default::default()
9127 },
9128 );
9129 });
9130
9131 {
9132 editor.update_in(cx, |editor, window, cx| {
9133 editor.set_text("somehting_new\n", window, cx)
9134 });
9135 assert!(cx.read(|cx| editor.is_dirty(cx)));
9136 let _formatting_request_signal = fake_server
9137 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9138 assert_eq!(
9139 params.text_document.uri,
9140 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9141 );
9142 assert_eq!(params.options.tab_size, 8);
9143 Ok(Some(vec![]))
9144 });
9145 let save = editor
9146 .update_in(cx, |editor, window, cx| {
9147 editor.save(true, project.clone(), window, cx)
9148 })
9149 .unwrap();
9150 cx.executor().start_waiting();
9151 save.await;
9152 }
9153}
9154
9155#[gpui::test]
9156async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
9157 init_test(cx, |_| {});
9158
9159 let cols = 4;
9160 let rows = 10;
9161 let sample_text_1 = sample_text(rows, cols, 'a');
9162 assert_eq!(
9163 sample_text_1,
9164 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
9165 );
9166 let sample_text_2 = sample_text(rows, cols, 'l');
9167 assert_eq!(
9168 sample_text_2,
9169 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
9170 );
9171 let sample_text_3 = sample_text(rows, cols, 'v');
9172 assert_eq!(
9173 sample_text_3,
9174 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
9175 );
9176
9177 let fs = FakeFs::new(cx.executor());
9178 fs.insert_tree(
9179 path!("/a"),
9180 json!({
9181 "main.rs": sample_text_1,
9182 "other.rs": sample_text_2,
9183 "lib.rs": sample_text_3,
9184 }),
9185 )
9186 .await;
9187
9188 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
9189 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9190 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9191
9192 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9193 language_registry.add(rust_lang());
9194 let mut fake_servers = language_registry.register_fake_lsp(
9195 "Rust",
9196 FakeLspAdapter {
9197 capabilities: lsp::ServerCapabilities {
9198 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9199 ..Default::default()
9200 },
9201 ..Default::default()
9202 },
9203 );
9204
9205 let worktree = project.update(cx, |project, cx| {
9206 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
9207 assert_eq!(worktrees.len(), 1);
9208 worktrees.pop().unwrap()
9209 });
9210 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9211
9212 let buffer_1 = project
9213 .update(cx, |project, cx| {
9214 project.open_buffer((worktree_id, "main.rs"), cx)
9215 })
9216 .await
9217 .unwrap();
9218 let buffer_2 = project
9219 .update(cx, |project, cx| {
9220 project.open_buffer((worktree_id, "other.rs"), cx)
9221 })
9222 .await
9223 .unwrap();
9224 let buffer_3 = project
9225 .update(cx, |project, cx| {
9226 project.open_buffer((worktree_id, "lib.rs"), cx)
9227 })
9228 .await
9229 .unwrap();
9230
9231 let multi_buffer = cx.new(|cx| {
9232 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9233 multi_buffer.push_excerpts(
9234 buffer_1.clone(),
9235 [
9236 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9237 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9238 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9239 ],
9240 cx,
9241 );
9242 multi_buffer.push_excerpts(
9243 buffer_2.clone(),
9244 [
9245 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9246 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9247 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9248 ],
9249 cx,
9250 );
9251 multi_buffer.push_excerpts(
9252 buffer_3.clone(),
9253 [
9254 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9255 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9256 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9257 ],
9258 cx,
9259 );
9260 multi_buffer
9261 });
9262 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
9263 Editor::new(
9264 EditorMode::full(),
9265 multi_buffer,
9266 Some(project.clone()),
9267 window,
9268 cx,
9269 )
9270 });
9271
9272 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9273 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
9274 s.select_ranges(Some(1..2))
9275 });
9276 editor.insert("|one|two|three|", window, cx);
9277 });
9278 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9279 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9280 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
9281 s.select_ranges(Some(60..70))
9282 });
9283 editor.insert("|four|five|six|", window, cx);
9284 });
9285 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9286
9287 // First two buffers should be edited, but not the third one.
9288 assert_eq!(
9289 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9290 "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}",
9291 );
9292 buffer_1.update(cx, |buffer, _| {
9293 assert!(buffer.is_dirty());
9294 assert_eq!(
9295 buffer.text(),
9296 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
9297 )
9298 });
9299 buffer_2.update(cx, |buffer, _| {
9300 assert!(buffer.is_dirty());
9301 assert_eq!(
9302 buffer.text(),
9303 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
9304 )
9305 });
9306 buffer_3.update(cx, |buffer, _| {
9307 assert!(!buffer.is_dirty());
9308 assert_eq!(buffer.text(), sample_text_3,)
9309 });
9310 cx.executor().run_until_parked();
9311
9312 cx.executor().start_waiting();
9313 let save = multi_buffer_editor
9314 .update_in(cx, |editor, window, cx| {
9315 editor.save(true, project.clone(), window, cx)
9316 })
9317 .unwrap();
9318
9319 let fake_server = fake_servers.next().await.unwrap();
9320 fake_server
9321 .server
9322 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
9323 Ok(Some(vec![lsp::TextEdit::new(
9324 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9325 format!("[{} formatted]", params.text_document.uri),
9326 )]))
9327 })
9328 .detach();
9329 save.await;
9330
9331 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
9332 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
9333 assert_eq!(
9334 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9335 uri!(
9336 "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}"
9337 ),
9338 );
9339 buffer_1.update(cx, |buffer, _| {
9340 assert!(!buffer.is_dirty());
9341 assert_eq!(
9342 buffer.text(),
9343 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
9344 )
9345 });
9346 buffer_2.update(cx, |buffer, _| {
9347 assert!(!buffer.is_dirty());
9348 assert_eq!(
9349 buffer.text(),
9350 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
9351 )
9352 });
9353 buffer_3.update(cx, |buffer, _| {
9354 assert!(!buffer.is_dirty());
9355 assert_eq!(buffer.text(), sample_text_3,)
9356 });
9357}
9358
9359#[gpui::test]
9360async fn test_range_format_during_save(cx: &mut TestAppContext) {
9361 init_test(cx, |_| {});
9362
9363 let fs = FakeFs::new(cx.executor());
9364 fs.insert_file(path!("/file.rs"), Default::default()).await;
9365
9366 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9367
9368 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9369 language_registry.add(rust_lang());
9370 let mut fake_servers = language_registry.register_fake_lsp(
9371 "Rust",
9372 FakeLspAdapter {
9373 capabilities: lsp::ServerCapabilities {
9374 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
9375 ..Default::default()
9376 },
9377 ..Default::default()
9378 },
9379 );
9380
9381 let buffer = project
9382 .update(cx, |project, cx| {
9383 project.open_local_buffer(path!("/file.rs"), cx)
9384 })
9385 .await
9386 .unwrap();
9387
9388 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9389 let (editor, cx) = cx.add_window_view(|window, cx| {
9390 build_editor_with_project(project.clone(), buffer, window, cx)
9391 });
9392 editor.update_in(cx, |editor, window, cx| {
9393 editor.set_text("one\ntwo\nthree\n", window, cx)
9394 });
9395 assert!(cx.read(|cx| editor.is_dirty(cx)));
9396
9397 cx.executor().start_waiting();
9398 let fake_server = fake_servers.next().await.unwrap();
9399
9400 let save = editor
9401 .update_in(cx, |editor, window, cx| {
9402 editor.save(true, project.clone(), window, cx)
9403 })
9404 .unwrap();
9405 fake_server
9406 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
9407 assert_eq!(
9408 params.text_document.uri,
9409 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9410 );
9411 assert_eq!(params.options.tab_size, 4);
9412 Ok(Some(vec![lsp::TextEdit::new(
9413 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9414 ", ".to_string(),
9415 )]))
9416 })
9417 .next()
9418 .await;
9419 cx.executor().start_waiting();
9420 save.await;
9421 assert_eq!(
9422 editor.update(cx, |editor, cx| editor.text(cx)),
9423 "one, two\nthree\n"
9424 );
9425 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9426
9427 editor.update_in(cx, |editor, window, cx| {
9428 editor.set_text("one\ntwo\nthree\n", window, cx)
9429 });
9430 assert!(cx.read(|cx| editor.is_dirty(cx)));
9431
9432 // Ensure we can still save even if formatting hangs.
9433 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
9434 move |params, _| async move {
9435 assert_eq!(
9436 params.text_document.uri,
9437 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9438 );
9439 futures::future::pending::<()>().await;
9440 unreachable!()
9441 },
9442 );
9443 let save = editor
9444 .update_in(cx, |editor, window, cx| {
9445 editor.save(true, project.clone(), window, cx)
9446 })
9447 .unwrap();
9448 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9449 cx.executor().start_waiting();
9450 save.await;
9451 assert_eq!(
9452 editor.update(cx, |editor, cx| editor.text(cx)),
9453 "one\ntwo\nthree\n"
9454 );
9455 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9456
9457 // For non-dirty buffer, a formatting request should be sent anyway with the default settings
9458 // where non-dirty singleton buffers are saved and formatted anyway.
9459 let save = editor
9460 .update_in(cx, |editor, window, cx| {
9461 editor.save(true, project.clone(), window, cx)
9462 })
9463 .unwrap();
9464 fake_server
9465 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
9466 assert_eq!(
9467 params.text_document.uri,
9468 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9469 );
9470 assert_eq!(params.options.tab_size, 4);
9471 Ok(Some(vec![lsp::TextEdit::new(
9472 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9473 ", ".to_string(),
9474 )]))
9475 })
9476 .next()
9477 .await;
9478 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9479 cx.executor().start_waiting();
9480 save.await;
9481 assert_eq!(
9482 editor.update(cx, |editor, cx| editor.text(cx)),
9483 "one, two\nthree\n"
9484 );
9485 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9486
9487 // Set Rust language override and assert overridden tabsize is sent to language server
9488 update_test_language_settings(cx, |settings| {
9489 settings.languages.insert(
9490 "Rust".into(),
9491 LanguageSettingsContent {
9492 tab_size: NonZeroU32::new(8),
9493 ..Default::default()
9494 },
9495 );
9496 });
9497
9498 editor.update_in(cx, |editor, window, cx| {
9499 editor.set_text("somehting_new\n", window, cx)
9500 });
9501 assert!(cx.read(|cx| editor.is_dirty(cx)));
9502 let save = editor
9503 .update_in(cx, |editor, window, cx| {
9504 editor.save(true, project.clone(), window, cx)
9505 })
9506 .unwrap();
9507 fake_server
9508 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
9509 assert_eq!(
9510 params.text_document.uri,
9511 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9512 );
9513 assert_eq!(params.options.tab_size, 8);
9514 Ok(Some(Vec::new()))
9515 })
9516 .next()
9517 .await;
9518 save.await;
9519}
9520
9521#[gpui::test]
9522async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
9523 init_test(cx, |settings| {
9524 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
9525 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
9526 ))
9527 });
9528
9529 let fs = FakeFs::new(cx.executor());
9530 fs.insert_file(path!("/file.rs"), Default::default()).await;
9531
9532 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9533
9534 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9535 language_registry.add(Arc::new(Language::new(
9536 LanguageConfig {
9537 name: "Rust".into(),
9538 matcher: LanguageMatcher {
9539 path_suffixes: vec!["rs".to_string()],
9540 ..Default::default()
9541 },
9542 ..LanguageConfig::default()
9543 },
9544 Some(tree_sitter_rust::LANGUAGE.into()),
9545 )));
9546 update_test_language_settings(cx, |settings| {
9547 // Enable Prettier formatting for the same buffer, and ensure
9548 // LSP is called instead of Prettier.
9549 settings.defaults.prettier = Some(PrettierSettings {
9550 allowed: true,
9551 ..PrettierSettings::default()
9552 });
9553 });
9554 let mut fake_servers = language_registry.register_fake_lsp(
9555 "Rust",
9556 FakeLspAdapter {
9557 capabilities: lsp::ServerCapabilities {
9558 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9559 ..Default::default()
9560 },
9561 ..Default::default()
9562 },
9563 );
9564
9565 let buffer = project
9566 .update(cx, |project, cx| {
9567 project.open_local_buffer(path!("/file.rs"), cx)
9568 })
9569 .await
9570 .unwrap();
9571
9572 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9573 let (editor, cx) = cx.add_window_view(|window, cx| {
9574 build_editor_with_project(project.clone(), buffer, window, cx)
9575 });
9576 editor.update_in(cx, |editor, window, cx| {
9577 editor.set_text("one\ntwo\nthree\n", window, cx)
9578 });
9579
9580 cx.executor().start_waiting();
9581 let fake_server = fake_servers.next().await.unwrap();
9582
9583 let format = editor
9584 .update_in(cx, |editor, window, cx| {
9585 editor.perform_format(
9586 project.clone(),
9587 FormatTrigger::Manual,
9588 FormatTarget::Buffers,
9589 window,
9590 cx,
9591 )
9592 })
9593 .unwrap();
9594 fake_server
9595 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9596 assert_eq!(
9597 params.text_document.uri,
9598 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9599 );
9600 assert_eq!(params.options.tab_size, 4);
9601 Ok(Some(vec![lsp::TextEdit::new(
9602 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9603 ", ".to_string(),
9604 )]))
9605 })
9606 .next()
9607 .await;
9608 cx.executor().start_waiting();
9609 format.await;
9610 assert_eq!(
9611 editor.update(cx, |editor, cx| editor.text(cx)),
9612 "one, two\nthree\n"
9613 );
9614
9615 editor.update_in(cx, |editor, window, cx| {
9616 editor.set_text("one\ntwo\nthree\n", window, cx)
9617 });
9618 // Ensure we don't lock if formatting hangs.
9619 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9620 move |params, _| async move {
9621 assert_eq!(
9622 params.text_document.uri,
9623 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9624 );
9625 futures::future::pending::<()>().await;
9626 unreachable!()
9627 },
9628 );
9629 let format = editor
9630 .update_in(cx, |editor, window, cx| {
9631 editor.perform_format(
9632 project,
9633 FormatTrigger::Manual,
9634 FormatTarget::Buffers,
9635 window,
9636 cx,
9637 )
9638 })
9639 .unwrap();
9640 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9641 cx.executor().start_waiting();
9642 format.await;
9643 assert_eq!(
9644 editor.update(cx, |editor, cx| editor.text(cx)),
9645 "one\ntwo\nthree\n"
9646 );
9647}
9648
9649#[gpui::test]
9650async fn test_multiple_formatters(cx: &mut TestAppContext) {
9651 init_test(cx, |settings| {
9652 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
9653 settings.defaults.formatter =
9654 Some(language_settings::SelectedFormatter::List(FormatterList(
9655 vec![
9656 Formatter::LanguageServer { name: None },
9657 Formatter::CodeActions(
9658 [
9659 ("code-action-1".into(), true),
9660 ("code-action-2".into(), true),
9661 ]
9662 .into_iter()
9663 .collect(),
9664 ),
9665 ]
9666 .into(),
9667 )))
9668 });
9669
9670 let fs = FakeFs::new(cx.executor());
9671 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
9672 .await;
9673
9674 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9675 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9676 language_registry.add(rust_lang());
9677
9678 let mut fake_servers = language_registry.register_fake_lsp(
9679 "Rust",
9680 FakeLspAdapter {
9681 capabilities: lsp::ServerCapabilities {
9682 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9683 execute_command_provider: Some(lsp::ExecuteCommandOptions {
9684 commands: vec!["the-command-for-code-action-1".into()],
9685 ..Default::default()
9686 }),
9687 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9688 ..Default::default()
9689 },
9690 ..Default::default()
9691 },
9692 );
9693
9694 let buffer = project
9695 .update(cx, |project, cx| {
9696 project.open_local_buffer(path!("/file.rs"), cx)
9697 })
9698 .await
9699 .unwrap();
9700
9701 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9702 let (editor, cx) = cx.add_window_view(|window, cx| {
9703 build_editor_with_project(project.clone(), buffer, window, cx)
9704 });
9705
9706 cx.executor().start_waiting();
9707
9708 let fake_server = fake_servers.next().await.unwrap();
9709 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9710 move |_params, _| async move {
9711 Ok(Some(vec![lsp::TextEdit::new(
9712 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9713 "applied-formatting\n".to_string(),
9714 )]))
9715 },
9716 );
9717 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9718 move |params, _| async move {
9719 assert_eq!(
9720 params.context.only,
9721 Some(vec!["code-action-1".into(), "code-action-2".into()])
9722 );
9723 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
9724 Ok(Some(vec![
9725 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9726 kind: Some("code-action-1".into()),
9727 edit: Some(lsp::WorkspaceEdit::new(
9728 [(
9729 uri.clone(),
9730 vec![lsp::TextEdit::new(
9731 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9732 "applied-code-action-1-edit\n".to_string(),
9733 )],
9734 )]
9735 .into_iter()
9736 .collect(),
9737 )),
9738 command: Some(lsp::Command {
9739 command: "the-command-for-code-action-1".into(),
9740 ..Default::default()
9741 }),
9742 ..Default::default()
9743 }),
9744 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9745 kind: Some("code-action-2".into()),
9746 edit: Some(lsp::WorkspaceEdit::new(
9747 [(
9748 uri.clone(),
9749 vec![lsp::TextEdit::new(
9750 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9751 "applied-code-action-2-edit\n".to_string(),
9752 )],
9753 )]
9754 .into_iter()
9755 .collect(),
9756 )),
9757 ..Default::default()
9758 }),
9759 ]))
9760 },
9761 );
9762
9763 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
9764 move |params, _| async move { Ok(params) }
9765 });
9766
9767 let command_lock = Arc::new(futures::lock::Mutex::new(()));
9768 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
9769 let fake = fake_server.clone();
9770 let lock = command_lock.clone();
9771 move |params, _| {
9772 assert_eq!(params.command, "the-command-for-code-action-1");
9773 let fake = fake.clone();
9774 let lock = lock.clone();
9775 async move {
9776 lock.lock().await;
9777 fake.server
9778 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
9779 label: None,
9780 edit: lsp::WorkspaceEdit {
9781 changes: Some(
9782 [(
9783 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
9784 vec![lsp::TextEdit {
9785 range: lsp::Range::new(
9786 lsp::Position::new(0, 0),
9787 lsp::Position::new(0, 0),
9788 ),
9789 new_text: "applied-code-action-1-command\n".into(),
9790 }],
9791 )]
9792 .into_iter()
9793 .collect(),
9794 ),
9795 ..Default::default()
9796 },
9797 })
9798 .await
9799 .into_response()
9800 .unwrap();
9801 Ok(Some(json!(null)))
9802 }
9803 }
9804 });
9805
9806 cx.executor().start_waiting();
9807 editor
9808 .update_in(cx, |editor, window, cx| {
9809 editor.perform_format(
9810 project.clone(),
9811 FormatTrigger::Manual,
9812 FormatTarget::Buffers,
9813 window,
9814 cx,
9815 )
9816 })
9817 .unwrap()
9818 .await;
9819 editor.update(cx, |editor, cx| {
9820 assert_eq!(
9821 editor.text(cx),
9822 r#"
9823 applied-code-action-2-edit
9824 applied-code-action-1-command
9825 applied-code-action-1-edit
9826 applied-formatting
9827 one
9828 two
9829 three
9830 "#
9831 .unindent()
9832 );
9833 });
9834
9835 editor.update_in(cx, |editor, window, cx| {
9836 editor.undo(&Default::default(), window, cx);
9837 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9838 });
9839
9840 // Perform a manual edit while waiting for an LSP command
9841 // that's being run as part of a formatting code action.
9842 let lock_guard = command_lock.lock().await;
9843 let format = editor
9844 .update_in(cx, |editor, window, cx| {
9845 editor.perform_format(
9846 project.clone(),
9847 FormatTrigger::Manual,
9848 FormatTarget::Buffers,
9849 window,
9850 cx,
9851 )
9852 })
9853 .unwrap();
9854 cx.run_until_parked();
9855 editor.update(cx, |editor, cx| {
9856 assert_eq!(
9857 editor.text(cx),
9858 r#"
9859 applied-code-action-1-edit
9860 applied-formatting
9861 one
9862 two
9863 three
9864 "#
9865 .unindent()
9866 );
9867
9868 editor.buffer.update(cx, |buffer, cx| {
9869 let ix = buffer.len(cx);
9870 buffer.edit([(ix..ix, "edited\n")], None, cx);
9871 });
9872 });
9873
9874 // Allow the LSP command to proceed. Because the buffer was edited,
9875 // the second code action will not be run.
9876 drop(lock_guard);
9877 format.await;
9878 editor.update_in(cx, |editor, window, cx| {
9879 assert_eq!(
9880 editor.text(cx),
9881 r#"
9882 applied-code-action-1-command
9883 applied-code-action-1-edit
9884 applied-formatting
9885 one
9886 two
9887 three
9888 edited
9889 "#
9890 .unindent()
9891 );
9892
9893 // The manual edit is undone first, because it is the last thing the user did
9894 // (even though the command completed afterwards).
9895 editor.undo(&Default::default(), window, cx);
9896 assert_eq!(
9897 editor.text(cx),
9898 r#"
9899 applied-code-action-1-command
9900 applied-code-action-1-edit
9901 applied-formatting
9902 one
9903 two
9904 three
9905 "#
9906 .unindent()
9907 );
9908
9909 // All the formatting (including the command, which completed after the manual edit)
9910 // is undone together.
9911 editor.undo(&Default::default(), window, cx);
9912 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9913 });
9914}
9915
9916#[gpui::test]
9917async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
9918 init_test(cx, |settings| {
9919 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
9920 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
9921 ))
9922 });
9923
9924 let fs = FakeFs::new(cx.executor());
9925 fs.insert_file(path!("/file.ts"), Default::default()).await;
9926
9927 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9928
9929 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9930 language_registry.add(Arc::new(Language::new(
9931 LanguageConfig {
9932 name: "TypeScript".into(),
9933 matcher: LanguageMatcher {
9934 path_suffixes: vec!["ts".to_string()],
9935 ..Default::default()
9936 },
9937 ..LanguageConfig::default()
9938 },
9939 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
9940 )));
9941 update_test_language_settings(cx, |settings| {
9942 settings.defaults.prettier = Some(PrettierSettings {
9943 allowed: true,
9944 ..PrettierSettings::default()
9945 });
9946 });
9947 let mut fake_servers = language_registry.register_fake_lsp(
9948 "TypeScript",
9949 FakeLspAdapter {
9950 capabilities: lsp::ServerCapabilities {
9951 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9952 ..Default::default()
9953 },
9954 ..Default::default()
9955 },
9956 );
9957
9958 let buffer = project
9959 .update(cx, |project, cx| {
9960 project.open_local_buffer(path!("/file.ts"), cx)
9961 })
9962 .await
9963 .unwrap();
9964
9965 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9966 let (editor, cx) = cx.add_window_view(|window, cx| {
9967 build_editor_with_project(project.clone(), buffer, window, cx)
9968 });
9969 editor.update_in(cx, |editor, window, cx| {
9970 editor.set_text(
9971 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9972 window,
9973 cx,
9974 )
9975 });
9976
9977 cx.executor().start_waiting();
9978 let fake_server = fake_servers.next().await.unwrap();
9979
9980 let format = editor
9981 .update_in(cx, |editor, window, cx| {
9982 editor.perform_code_action_kind(
9983 project.clone(),
9984 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9985 window,
9986 cx,
9987 )
9988 })
9989 .unwrap();
9990 fake_server
9991 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
9992 assert_eq!(
9993 params.text_document.uri,
9994 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9995 );
9996 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
9997 lsp::CodeAction {
9998 title: "Organize Imports".to_string(),
9999 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
10000 edit: Some(lsp::WorkspaceEdit {
10001 changes: Some(
10002 [(
10003 params.text_document.uri.clone(),
10004 vec![lsp::TextEdit::new(
10005 lsp::Range::new(
10006 lsp::Position::new(1, 0),
10007 lsp::Position::new(2, 0),
10008 ),
10009 "".to_string(),
10010 )],
10011 )]
10012 .into_iter()
10013 .collect(),
10014 ),
10015 ..Default::default()
10016 }),
10017 ..Default::default()
10018 },
10019 )]))
10020 })
10021 .next()
10022 .await;
10023 cx.executor().start_waiting();
10024 format.await;
10025 assert_eq!(
10026 editor.update(cx, |editor, cx| editor.text(cx)),
10027 "import { a } from 'module';\n\nconst x = a;\n"
10028 );
10029
10030 editor.update_in(cx, |editor, window, cx| {
10031 editor.set_text(
10032 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10033 window,
10034 cx,
10035 )
10036 });
10037 // Ensure we don't lock if code action hangs.
10038 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10039 move |params, _| async move {
10040 assert_eq!(
10041 params.text_document.uri,
10042 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10043 );
10044 futures::future::pending::<()>().await;
10045 unreachable!()
10046 },
10047 );
10048 let format = editor
10049 .update_in(cx, |editor, window, cx| {
10050 editor.perform_code_action_kind(
10051 project,
10052 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10053 window,
10054 cx,
10055 )
10056 })
10057 .unwrap();
10058 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
10059 cx.executor().start_waiting();
10060 format.await;
10061 assert_eq!(
10062 editor.update(cx, |editor, cx| editor.text(cx)),
10063 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
10064 );
10065}
10066
10067#[gpui::test]
10068async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
10069 init_test(cx, |_| {});
10070
10071 let mut cx = EditorLspTestContext::new_rust(
10072 lsp::ServerCapabilities {
10073 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10074 ..Default::default()
10075 },
10076 cx,
10077 )
10078 .await;
10079
10080 cx.set_state(indoc! {"
10081 one.twoˇ
10082 "});
10083
10084 // The format request takes a long time. When it completes, it inserts
10085 // a newline and an indent before the `.`
10086 cx.lsp
10087 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
10088 let executor = cx.background_executor().clone();
10089 async move {
10090 executor.timer(Duration::from_millis(100)).await;
10091 Ok(Some(vec![lsp::TextEdit {
10092 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
10093 new_text: "\n ".into(),
10094 }]))
10095 }
10096 });
10097
10098 // Submit a format request.
10099 let format_1 = cx
10100 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10101 .unwrap();
10102 cx.executor().run_until_parked();
10103
10104 // Submit a second format request.
10105 let format_2 = cx
10106 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10107 .unwrap();
10108 cx.executor().run_until_parked();
10109
10110 // Wait for both format requests to complete
10111 cx.executor().advance_clock(Duration::from_millis(200));
10112 cx.executor().start_waiting();
10113 format_1.await.unwrap();
10114 cx.executor().start_waiting();
10115 format_2.await.unwrap();
10116
10117 // The formatting edits only happens once.
10118 cx.assert_editor_state(indoc! {"
10119 one
10120 .twoˇ
10121 "});
10122}
10123
10124#[gpui::test]
10125async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
10126 init_test(cx, |settings| {
10127 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
10128 });
10129
10130 let mut cx = EditorLspTestContext::new_rust(
10131 lsp::ServerCapabilities {
10132 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10133 ..Default::default()
10134 },
10135 cx,
10136 )
10137 .await;
10138
10139 // Set up a buffer white some trailing whitespace and no trailing newline.
10140 cx.set_state(
10141 &[
10142 "one ", //
10143 "twoˇ", //
10144 "three ", //
10145 "four", //
10146 ]
10147 .join("\n"),
10148 );
10149
10150 // Submit a format request.
10151 let format = cx
10152 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10153 .unwrap();
10154
10155 // Record which buffer changes have been sent to the language server
10156 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
10157 cx.lsp
10158 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
10159 let buffer_changes = buffer_changes.clone();
10160 move |params, _| {
10161 buffer_changes.lock().extend(
10162 params
10163 .content_changes
10164 .into_iter()
10165 .map(|e| (e.range.unwrap(), e.text)),
10166 );
10167 }
10168 });
10169
10170 // Handle formatting requests to the language server.
10171 cx.lsp
10172 .set_request_handler::<lsp::request::Formatting, _, _>({
10173 let buffer_changes = buffer_changes.clone();
10174 move |_, _| {
10175 // When formatting is requested, trailing whitespace has already been stripped,
10176 // and the trailing newline has already been added.
10177 assert_eq!(
10178 &buffer_changes.lock()[1..],
10179 &[
10180 (
10181 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
10182 "".into()
10183 ),
10184 (
10185 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
10186 "".into()
10187 ),
10188 (
10189 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
10190 "\n".into()
10191 ),
10192 ]
10193 );
10194
10195 // Insert blank lines between each line of the buffer.
10196 async move {
10197 Ok(Some(vec![
10198 lsp::TextEdit {
10199 range: lsp::Range::new(
10200 lsp::Position::new(1, 0),
10201 lsp::Position::new(1, 0),
10202 ),
10203 new_text: "\n".into(),
10204 },
10205 lsp::TextEdit {
10206 range: lsp::Range::new(
10207 lsp::Position::new(2, 0),
10208 lsp::Position::new(2, 0),
10209 ),
10210 new_text: "\n".into(),
10211 },
10212 ]))
10213 }
10214 }
10215 });
10216
10217 // After formatting the buffer, the trailing whitespace is stripped,
10218 // a newline is appended, and the edits provided by the language server
10219 // have been applied.
10220 format.await.unwrap();
10221 cx.assert_editor_state(
10222 &[
10223 "one", //
10224 "", //
10225 "twoˇ", //
10226 "", //
10227 "three", //
10228 "four", //
10229 "", //
10230 ]
10231 .join("\n"),
10232 );
10233
10234 // Undoing the formatting undoes the trailing whitespace removal, the
10235 // trailing newline, and the LSP edits.
10236 cx.update_buffer(|buffer, cx| buffer.undo(cx));
10237 cx.assert_editor_state(
10238 &[
10239 "one ", //
10240 "twoˇ", //
10241 "three ", //
10242 "four", //
10243 ]
10244 .join("\n"),
10245 );
10246}
10247
10248#[gpui::test]
10249async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
10250 cx: &mut TestAppContext,
10251) {
10252 init_test(cx, |_| {});
10253
10254 cx.update(|cx| {
10255 cx.update_global::<SettingsStore, _>(|settings, cx| {
10256 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10257 settings.auto_signature_help = Some(true);
10258 });
10259 });
10260 });
10261
10262 let mut cx = EditorLspTestContext::new_rust(
10263 lsp::ServerCapabilities {
10264 signature_help_provider: Some(lsp::SignatureHelpOptions {
10265 ..Default::default()
10266 }),
10267 ..Default::default()
10268 },
10269 cx,
10270 )
10271 .await;
10272
10273 let language = Language::new(
10274 LanguageConfig {
10275 name: "Rust".into(),
10276 brackets: BracketPairConfig {
10277 pairs: vec![
10278 BracketPair {
10279 start: "{".to_string(),
10280 end: "}".to_string(),
10281 close: true,
10282 surround: true,
10283 newline: true,
10284 },
10285 BracketPair {
10286 start: "(".to_string(),
10287 end: ")".to_string(),
10288 close: true,
10289 surround: true,
10290 newline: true,
10291 },
10292 BracketPair {
10293 start: "/*".to_string(),
10294 end: " */".to_string(),
10295 close: true,
10296 surround: true,
10297 newline: true,
10298 },
10299 BracketPair {
10300 start: "[".to_string(),
10301 end: "]".to_string(),
10302 close: false,
10303 surround: false,
10304 newline: true,
10305 },
10306 BracketPair {
10307 start: "\"".to_string(),
10308 end: "\"".to_string(),
10309 close: true,
10310 surround: true,
10311 newline: false,
10312 },
10313 BracketPair {
10314 start: "<".to_string(),
10315 end: ">".to_string(),
10316 close: false,
10317 surround: true,
10318 newline: true,
10319 },
10320 ],
10321 ..Default::default()
10322 },
10323 autoclose_before: "})]".to_string(),
10324 ..Default::default()
10325 },
10326 Some(tree_sitter_rust::LANGUAGE.into()),
10327 );
10328 let language = Arc::new(language);
10329
10330 cx.language_registry().add(language.clone());
10331 cx.update_buffer(|buffer, cx| {
10332 buffer.set_language(Some(language), cx);
10333 });
10334
10335 cx.set_state(
10336 &r#"
10337 fn main() {
10338 sampleˇ
10339 }
10340 "#
10341 .unindent(),
10342 );
10343
10344 cx.update_editor(|editor, window, cx| {
10345 editor.handle_input("(", window, cx);
10346 });
10347 cx.assert_editor_state(
10348 &"
10349 fn main() {
10350 sample(ˇ)
10351 }
10352 "
10353 .unindent(),
10354 );
10355
10356 let mocked_response = lsp::SignatureHelp {
10357 signatures: vec![lsp::SignatureInformation {
10358 label: "fn sample(param1: u8, param2: u8)".to_string(),
10359 documentation: None,
10360 parameters: Some(vec![
10361 lsp::ParameterInformation {
10362 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10363 documentation: None,
10364 },
10365 lsp::ParameterInformation {
10366 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10367 documentation: None,
10368 },
10369 ]),
10370 active_parameter: None,
10371 }],
10372 active_signature: Some(0),
10373 active_parameter: Some(0),
10374 };
10375 handle_signature_help_request(&mut cx, mocked_response).await;
10376
10377 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10378 .await;
10379
10380 cx.editor(|editor, _, _| {
10381 let signature_help_state = editor.signature_help_state.popover().cloned();
10382 assert_eq!(
10383 signature_help_state.unwrap().label,
10384 "param1: u8, param2: u8"
10385 );
10386 });
10387}
10388
10389#[gpui::test]
10390async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
10391 init_test(cx, |_| {});
10392
10393 cx.update(|cx| {
10394 cx.update_global::<SettingsStore, _>(|settings, cx| {
10395 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10396 settings.auto_signature_help = Some(false);
10397 settings.show_signature_help_after_edits = Some(false);
10398 });
10399 });
10400 });
10401
10402 let mut cx = EditorLspTestContext::new_rust(
10403 lsp::ServerCapabilities {
10404 signature_help_provider: Some(lsp::SignatureHelpOptions {
10405 ..Default::default()
10406 }),
10407 ..Default::default()
10408 },
10409 cx,
10410 )
10411 .await;
10412
10413 let language = Language::new(
10414 LanguageConfig {
10415 name: "Rust".into(),
10416 brackets: BracketPairConfig {
10417 pairs: vec![
10418 BracketPair {
10419 start: "{".to_string(),
10420 end: "}".to_string(),
10421 close: true,
10422 surround: true,
10423 newline: true,
10424 },
10425 BracketPair {
10426 start: "(".to_string(),
10427 end: ")".to_string(),
10428 close: true,
10429 surround: true,
10430 newline: true,
10431 },
10432 BracketPair {
10433 start: "/*".to_string(),
10434 end: " */".to_string(),
10435 close: true,
10436 surround: true,
10437 newline: true,
10438 },
10439 BracketPair {
10440 start: "[".to_string(),
10441 end: "]".to_string(),
10442 close: false,
10443 surround: false,
10444 newline: true,
10445 },
10446 BracketPair {
10447 start: "\"".to_string(),
10448 end: "\"".to_string(),
10449 close: true,
10450 surround: true,
10451 newline: false,
10452 },
10453 BracketPair {
10454 start: "<".to_string(),
10455 end: ">".to_string(),
10456 close: false,
10457 surround: true,
10458 newline: true,
10459 },
10460 ],
10461 ..Default::default()
10462 },
10463 autoclose_before: "})]".to_string(),
10464 ..Default::default()
10465 },
10466 Some(tree_sitter_rust::LANGUAGE.into()),
10467 );
10468 let language = Arc::new(language);
10469
10470 cx.language_registry().add(language.clone());
10471 cx.update_buffer(|buffer, cx| {
10472 buffer.set_language(Some(language), cx);
10473 });
10474
10475 // Ensure that signature_help is not called when no signature help is enabled.
10476 cx.set_state(
10477 &r#"
10478 fn main() {
10479 sampleˇ
10480 }
10481 "#
10482 .unindent(),
10483 );
10484 cx.update_editor(|editor, window, cx| {
10485 editor.handle_input("(", window, cx);
10486 });
10487 cx.assert_editor_state(
10488 &"
10489 fn main() {
10490 sample(ˇ)
10491 }
10492 "
10493 .unindent(),
10494 );
10495 cx.editor(|editor, _, _| {
10496 assert!(editor.signature_help_state.task().is_none());
10497 });
10498
10499 let mocked_response = lsp::SignatureHelp {
10500 signatures: vec![lsp::SignatureInformation {
10501 label: "fn sample(param1: u8, param2: u8)".to_string(),
10502 documentation: None,
10503 parameters: Some(vec![
10504 lsp::ParameterInformation {
10505 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10506 documentation: None,
10507 },
10508 lsp::ParameterInformation {
10509 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10510 documentation: None,
10511 },
10512 ]),
10513 active_parameter: None,
10514 }],
10515 active_signature: Some(0),
10516 active_parameter: Some(0),
10517 };
10518
10519 // Ensure that signature_help is called when enabled afte edits
10520 cx.update(|_, cx| {
10521 cx.update_global::<SettingsStore, _>(|settings, cx| {
10522 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10523 settings.auto_signature_help = Some(false);
10524 settings.show_signature_help_after_edits = Some(true);
10525 });
10526 });
10527 });
10528 cx.set_state(
10529 &r#"
10530 fn main() {
10531 sampleˇ
10532 }
10533 "#
10534 .unindent(),
10535 );
10536 cx.update_editor(|editor, window, cx| {
10537 editor.handle_input("(", window, cx);
10538 });
10539 cx.assert_editor_state(
10540 &"
10541 fn main() {
10542 sample(ˇ)
10543 }
10544 "
10545 .unindent(),
10546 );
10547 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10548 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10549 .await;
10550 cx.update_editor(|editor, _, _| {
10551 let signature_help_state = editor.signature_help_state.popover().cloned();
10552 assert!(signature_help_state.is_some());
10553 assert_eq!(
10554 signature_help_state.unwrap().label,
10555 "param1: u8, param2: u8"
10556 );
10557 editor.signature_help_state = SignatureHelpState::default();
10558 });
10559
10560 // Ensure that signature_help is called when auto signature help override is enabled
10561 cx.update(|_, cx| {
10562 cx.update_global::<SettingsStore, _>(|settings, cx| {
10563 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10564 settings.auto_signature_help = Some(true);
10565 settings.show_signature_help_after_edits = Some(false);
10566 });
10567 });
10568 });
10569 cx.set_state(
10570 &r#"
10571 fn main() {
10572 sampleˇ
10573 }
10574 "#
10575 .unindent(),
10576 );
10577 cx.update_editor(|editor, window, cx| {
10578 editor.handle_input("(", window, cx);
10579 });
10580 cx.assert_editor_state(
10581 &"
10582 fn main() {
10583 sample(ˇ)
10584 }
10585 "
10586 .unindent(),
10587 );
10588 handle_signature_help_request(&mut cx, mocked_response).await;
10589 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10590 .await;
10591 cx.editor(|editor, _, _| {
10592 let signature_help_state = editor.signature_help_state.popover().cloned();
10593 assert!(signature_help_state.is_some());
10594 assert_eq!(
10595 signature_help_state.unwrap().label,
10596 "param1: u8, param2: u8"
10597 );
10598 });
10599}
10600
10601#[gpui::test]
10602async fn test_signature_help(cx: &mut TestAppContext) {
10603 init_test(cx, |_| {});
10604 cx.update(|cx| {
10605 cx.update_global::<SettingsStore, _>(|settings, cx| {
10606 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10607 settings.auto_signature_help = Some(true);
10608 });
10609 });
10610 });
10611
10612 let mut cx = EditorLspTestContext::new_rust(
10613 lsp::ServerCapabilities {
10614 signature_help_provider: Some(lsp::SignatureHelpOptions {
10615 ..Default::default()
10616 }),
10617 ..Default::default()
10618 },
10619 cx,
10620 )
10621 .await;
10622
10623 // A test that directly calls `show_signature_help`
10624 cx.update_editor(|editor, window, cx| {
10625 editor.show_signature_help(&ShowSignatureHelp, window, cx);
10626 });
10627
10628 let mocked_response = lsp::SignatureHelp {
10629 signatures: vec![lsp::SignatureInformation {
10630 label: "fn sample(param1: u8, param2: u8)".to_string(),
10631 documentation: None,
10632 parameters: Some(vec![
10633 lsp::ParameterInformation {
10634 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10635 documentation: None,
10636 },
10637 lsp::ParameterInformation {
10638 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10639 documentation: None,
10640 },
10641 ]),
10642 active_parameter: None,
10643 }],
10644 active_signature: Some(0),
10645 active_parameter: Some(0),
10646 };
10647 handle_signature_help_request(&mut cx, mocked_response).await;
10648
10649 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10650 .await;
10651
10652 cx.editor(|editor, _, _| {
10653 let signature_help_state = editor.signature_help_state.popover().cloned();
10654 assert!(signature_help_state.is_some());
10655 assert_eq!(
10656 signature_help_state.unwrap().label,
10657 "param1: u8, param2: u8"
10658 );
10659 });
10660
10661 // When exiting outside from inside the brackets, `signature_help` is closed.
10662 cx.set_state(indoc! {"
10663 fn main() {
10664 sample(ˇ);
10665 }
10666
10667 fn sample(param1: u8, param2: u8) {}
10668 "});
10669
10670 cx.update_editor(|editor, window, cx| {
10671 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
10672 });
10673
10674 let mocked_response = lsp::SignatureHelp {
10675 signatures: Vec::new(),
10676 active_signature: None,
10677 active_parameter: None,
10678 };
10679 handle_signature_help_request(&mut cx, mocked_response).await;
10680
10681 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10682 .await;
10683
10684 cx.editor(|editor, _, _| {
10685 assert!(!editor.signature_help_state.is_shown());
10686 });
10687
10688 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
10689 cx.set_state(indoc! {"
10690 fn main() {
10691 sample(ˇ);
10692 }
10693
10694 fn sample(param1: u8, param2: u8) {}
10695 "});
10696
10697 let mocked_response = lsp::SignatureHelp {
10698 signatures: vec![lsp::SignatureInformation {
10699 label: "fn sample(param1: u8, param2: u8)".to_string(),
10700 documentation: None,
10701 parameters: Some(vec![
10702 lsp::ParameterInformation {
10703 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10704 documentation: None,
10705 },
10706 lsp::ParameterInformation {
10707 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10708 documentation: None,
10709 },
10710 ]),
10711 active_parameter: None,
10712 }],
10713 active_signature: Some(0),
10714 active_parameter: Some(0),
10715 };
10716 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10717 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10718 .await;
10719 cx.editor(|editor, _, _| {
10720 assert!(editor.signature_help_state.is_shown());
10721 });
10722
10723 // Restore the popover with more parameter input
10724 cx.set_state(indoc! {"
10725 fn main() {
10726 sample(param1, param2ˇ);
10727 }
10728
10729 fn sample(param1: u8, param2: u8) {}
10730 "});
10731
10732 let mocked_response = lsp::SignatureHelp {
10733 signatures: vec![lsp::SignatureInformation {
10734 label: "fn sample(param1: u8, param2: u8)".to_string(),
10735 documentation: None,
10736 parameters: Some(vec![
10737 lsp::ParameterInformation {
10738 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10739 documentation: None,
10740 },
10741 lsp::ParameterInformation {
10742 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10743 documentation: None,
10744 },
10745 ]),
10746 active_parameter: None,
10747 }],
10748 active_signature: Some(0),
10749 active_parameter: Some(1),
10750 };
10751 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10752 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10753 .await;
10754
10755 // When selecting a range, the popover is gone.
10756 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
10757 cx.update_editor(|editor, window, cx| {
10758 editor.change_selections(None, window, cx, |s| {
10759 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10760 })
10761 });
10762 cx.assert_editor_state(indoc! {"
10763 fn main() {
10764 sample(param1, «ˇparam2»);
10765 }
10766
10767 fn sample(param1: u8, param2: u8) {}
10768 "});
10769 cx.editor(|editor, _, _| {
10770 assert!(!editor.signature_help_state.is_shown());
10771 });
10772
10773 // When unselecting again, the popover is back if within the brackets.
10774 cx.update_editor(|editor, window, cx| {
10775 editor.change_selections(None, window, cx, |s| {
10776 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10777 })
10778 });
10779 cx.assert_editor_state(indoc! {"
10780 fn main() {
10781 sample(param1, ˇparam2);
10782 }
10783
10784 fn sample(param1: u8, param2: u8) {}
10785 "});
10786 handle_signature_help_request(&mut cx, mocked_response).await;
10787 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10788 .await;
10789 cx.editor(|editor, _, _| {
10790 assert!(editor.signature_help_state.is_shown());
10791 });
10792
10793 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
10794 cx.update_editor(|editor, window, cx| {
10795 editor.change_selections(None, window, cx, |s| {
10796 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
10797 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10798 })
10799 });
10800 cx.assert_editor_state(indoc! {"
10801 fn main() {
10802 sample(param1, ˇparam2);
10803 }
10804
10805 fn sample(param1: u8, param2: u8) {}
10806 "});
10807
10808 let mocked_response = lsp::SignatureHelp {
10809 signatures: vec![lsp::SignatureInformation {
10810 label: "fn sample(param1: u8, param2: u8)".to_string(),
10811 documentation: None,
10812 parameters: Some(vec![
10813 lsp::ParameterInformation {
10814 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10815 documentation: None,
10816 },
10817 lsp::ParameterInformation {
10818 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10819 documentation: None,
10820 },
10821 ]),
10822 active_parameter: None,
10823 }],
10824 active_signature: Some(0),
10825 active_parameter: Some(1),
10826 };
10827 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10828 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10829 .await;
10830 cx.update_editor(|editor, _, cx| {
10831 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
10832 });
10833 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10834 .await;
10835 cx.update_editor(|editor, window, cx| {
10836 editor.change_selections(None, window, cx, |s| {
10837 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10838 })
10839 });
10840 cx.assert_editor_state(indoc! {"
10841 fn main() {
10842 sample(param1, «ˇparam2»);
10843 }
10844
10845 fn sample(param1: u8, param2: u8) {}
10846 "});
10847 cx.update_editor(|editor, window, cx| {
10848 editor.change_selections(None, window, cx, |s| {
10849 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10850 })
10851 });
10852 cx.assert_editor_state(indoc! {"
10853 fn main() {
10854 sample(param1, ˇparam2);
10855 }
10856
10857 fn sample(param1: u8, param2: u8) {}
10858 "});
10859 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
10860 .await;
10861}
10862
10863#[gpui::test]
10864async fn test_completion_mode(cx: &mut TestAppContext) {
10865 init_test(cx, |_| {});
10866 let mut cx = EditorLspTestContext::new_rust(
10867 lsp::ServerCapabilities {
10868 completion_provider: Some(lsp::CompletionOptions {
10869 resolve_provider: Some(true),
10870 ..Default::default()
10871 }),
10872 ..Default::default()
10873 },
10874 cx,
10875 )
10876 .await;
10877
10878 struct Run {
10879 run_description: &'static str,
10880 initial_state: String,
10881 buffer_marked_text: String,
10882 completion_label: &'static str,
10883 completion_text: &'static str,
10884 expected_with_insert_mode: String,
10885 expected_with_replace_mode: String,
10886 expected_with_replace_subsequence_mode: String,
10887 expected_with_replace_suffix_mode: String,
10888 }
10889
10890 let runs = [
10891 Run {
10892 run_description: "Start of word matches completion text",
10893 initial_state: "before ediˇ after".into(),
10894 buffer_marked_text: "before <edi|> after".into(),
10895 completion_label: "editor",
10896 completion_text: "editor",
10897 expected_with_insert_mode: "before editorˇ after".into(),
10898 expected_with_replace_mode: "before editorˇ after".into(),
10899 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10900 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10901 },
10902 Run {
10903 run_description: "Accept same text at the middle of the word",
10904 initial_state: "before ediˇtor after".into(),
10905 buffer_marked_text: "before <edi|tor> after".into(),
10906 completion_label: "editor",
10907 completion_text: "editor",
10908 expected_with_insert_mode: "before editorˇtor after".into(),
10909 expected_with_replace_mode: "before editorˇ after".into(),
10910 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10911 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10912 },
10913 Run {
10914 run_description: "End of word matches completion text -- cursor at end",
10915 initial_state: "before torˇ after".into(),
10916 buffer_marked_text: "before <tor|> after".into(),
10917 completion_label: "editor",
10918 completion_text: "editor",
10919 expected_with_insert_mode: "before editorˇ after".into(),
10920 expected_with_replace_mode: "before editorˇ after".into(),
10921 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10922 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10923 },
10924 Run {
10925 run_description: "End of word matches completion text -- cursor at start",
10926 initial_state: "before ˇtor after".into(),
10927 buffer_marked_text: "before <|tor> after".into(),
10928 completion_label: "editor",
10929 completion_text: "editor",
10930 expected_with_insert_mode: "before editorˇtor after".into(),
10931 expected_with_replace_mode: "before editorˇ after".into(),
10932 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10933 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10934 },
10935 Run {
10936 run_description: "Prepend text containing whitespace",
10937 initial_state: "pˇfield: bool".into(),
10938 buffer_marked_text: "<p|field>: bool".into(),
10939 completion_label: "pub ",
10940 completion_text: "pub ",
10941 expected_with_insert_mode: "pub ˇfield: bool".into(),
10942 expected_with_replace_mode: "pub ˇ: bool".into(),
10943 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
10944 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
10945 },
10946 Run {
10947 run_description: "Add element to start of list",
10948 initial_state: "[element_ˇelement_2]".into(),
10949 buffer_marked_text: "[<element_|element_2>]".into(),
10950 completion_label: "element_1",
10951 completion_text: "element_1",
10952 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
10953 expected_with_replace_mode: "[element_1ˇ]".into(),
10954 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
10955 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
10956 },
10957 Run {
10958 run_description: "Add element to start of list -- first and second elements are equal",
10959 initial_state: "[elˇelement]".into(),
10960 buffer_marked_text: "[<el|element>]".into(),
10961 completion_label: "element",
10962 completion_text: "element",
10963 expected_with_insert_mode: "[elementˇelement]".into(),
10964 expected_with_replace_mode: "[elementˇ]".into(),
10965 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
10966 expected_with_replace_suffix_mode: "[elementˇ]".into(),
10967 },
10968 Run {
10969 run_description: "Ends with matching suffix",
10970 initial_state: "SubˇError".into(),
10971 buffer_marked_text: "<Sub|Error>".into(),
10972 completion_label: "SubscriptionError",
10973 completion_text: "SubscriptionError",
10974 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
10975 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10976 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10977 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
10978 },
10979 Run {
10980 run_description: "Suffix is a subsequence -- contiguous",
10981 initial_state: "SubˇErr".into(),
10982 buffer_marked_text: "<Sub|Err>".into(),
10983 completion_label: "SubscriptionError",
10984 completion_text: "SubscriptionError",
10985 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
10986 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10987 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10988 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
10989 },
10990 Run {
10991 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
10992 initial_state: "Suˇscrirr".into(),
10993 buffer_marked_text: "<Su|scrirr>".into(),
10994 completion_label: "SubscriptionError",
10995 completion_text: "SubscriptionError",
10996 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
10997 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10998 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10999 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
11000 },
11001 Run {
11002 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
11003 initial_state: "foo(indˇix)".into(),
11004 buffer_marked_text: "foo(<ind|ix>)".into(),
11005 completion_label: "node_index",
11006 completion_text: "node_index",
11007 expected_with_insert_mode: "foo(node_indexˇix)".into(),
11008 expected_with_replace_mode: "foo(node_indexˇ)".into(),
11009 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
11010 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
11011 },
11012 Run {
11013 run_description: "Replace range ends before cursor - should extend to cursor",
11014 initial_state: "before editˇo after".into(),
11015 buffer_marked_text: "before <{ed}>it|o after".into(),
11016 completion_label: "editor",
11017 completion_text: "editor",
11018 expected_with_insert_mode: "before editorˇo after".into(),
11019 expected_with_replace_mode: "before editorˇo after".into(),
11020 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
11021 expected_with_replace_suffix_mode: "before editorˇo after".into(),
11022 },
11023 Run {
11024 run_description: "Uses label for suffix matching",
11025 initial_state: "before ediˇtor after".into(),
11026 buffer_marked_text: "before <edi|tor> after".into(),
11027 completion_label: "editor",
11028 completion_text: "editor()",
11029 expected_with_insert_mode: "before editor()ˇtor after".into(),
11030 expected_with_replace_mode: "before editor()ˇ after".into(),
11031 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
11032 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
11033 },
11034 Run {
11035 run_description: "Case insensitive subsequence and suffix matching",
11036 initial_state: "before EDiˇtoR after".into(),
11037 buffer_marked_text: "before <EDi|toR> after".into(),
11038 completion_label: "editor",
11039 completion_text: "editor",
11040 expected_with_insert_mode: "before editorˇtoR after".into(),
11041 expected_with_replace_mode: "before editorˇ after".into(),
11042 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11043 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11044 },
11045 ];
11046
11047 for run in runs {
11048 let run_variations = [
11049 (LspInsertMode::Insert, run.expected_with_insert_mode),
11050 (LspInsertMode::Replace, run.expected_with_replace_mode),
11051 (
11052 LspInsertMode::ReplaceSubsequence,
11053 run.expected_with_replace_subsequence_mode,
11054 ),
11055 (
11056 LspInsertMode::ReplaceSuffix,
11057 run.expected_with_replace_suffix_mode,
11058 ),
11059 ];
11060
11061 for (lsp_insert_mode, expected_text) in run_variations {
11062 eprintln!(
11063 "run = {:?}, mode = {lsp_insert_mode:.?}",
11064 run.run_description,
11065 );
11066
11067 update_test_language_settings(&mut cx, |settings| {
11068 settings.defaults.completions = Some(CompletionSettings {
11069 lsp_insert_mode,
11070 words: WordsCompletionMode::Disabled,
11071 lsp: true,
11072 lsp_fetch_timeout_ms: 0,
11073 });
11074 });
11075
11076 cx.set_state(&run.initial_state);
11077 cx.update_editor(|editor, window, cx| {
11078 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11079 });
11080
11081 let counter = Arc::new(AtomicUsize::new(0));
11082 handle_completion_request_with_insert_and_replace(
11083 &mut cx,
11084 &run.buffer_marked_text,
11085 vec![(run.completion_label, run.completion_text)],
11086 counter.clone(),
11087 )
11088 .await;
11089 cx.condition(|editor, _| editor.context_menu_visible())
11090 .await;
11091 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11092
11093 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11094 editor
11095 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11096 .unwrap()
11097 });
11098 cx.assert_editor_state(&expected_text);
11099 handle_resolve_completion_request(&mut cx, None).await;
11100 apply_additional_edits.await.unwrap();
11101 }
11102 }
11103}
11104
11105#[gpui::test]
11106async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
11107 init_test(cx, |_| {});
11108 let mut cx = EditorLspTestContext::new_rust(
11109 lsp::ServerCapabilities {
11110 completion_provider: Some(lsp::CompletionOptions {
11111 resolve_provider: Some(true),
11112 ..Default::default()
11113 }),
11114 ..Default::default()
11115 },
11116 cx,
11117 )
11118 .await;
11119
11120 let initial_state = "SubˇError";
11121 let buffer_marked_text = "<Sub|Error>";
11122 let completion_text = "SubscriptionError";
11123 let expected_with_insert_mode = "SubscriptionErrorˇError";
11124 let expected_with_replace_mode = "SubscriptionErrorˇ";
11125
11126 update_test_language_settings(&mut cx, |settings| {
11127 settings.defaults.completions = Some(CompletionSettings {
11128 words: WordsCompletionMode::Disabled,
11129 // set the opposite here to ensure that the action is overriding the default behavior
11130 lsp_insert_mode: LspInsertMode::Insert,
11131 lsp: true,
11132 lsp_fetch_timeout_ms: 0,
11133 });
11134 });
11135
11136 cx.set_state(initial_state);
11137 cx.update_editor(|editor, window, cx| {
11138 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11139 });
11140
11141 let counter = Arc::new(AtomicUsize::new(0));
11142 handle_completion_request_with_insert_and_replace(
11143 &mut cx,
11144 &buffer_marked_text,
11145 vec![(completion_text, completion_text)],
11146 counter.clone(),
11147 )
11148 .await;
11149 cx.condition(|editor, _| editor.context_menu_visible())
11150 .await;
11151 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11152
11153 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11154 editor
11155 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11156 .unwrap()
11157 });
11158 cx.assert_editor_state(&expected_with_replace_mode);
11159 handle_resolve_completion_request(&mut cx, None).await;
11160 apply_additional_edits.await.unwrap();
11161
11162 update_test_language_settings(&mut cx, |settings| {
11163 settings.defaults.completions = Some(CompletionSettings {
11164 words: WordsCompletionMode::Disabled,
11165 // set the opposite here to ensure that the action is overriding the default behavior
11166 lsp_insert_mode: LspInsertMode::Replace,
11167 lsp: true,
11168 lsp_fetch_timeout_ms: 0,
11169 });
11170 });
11171
11172 cx.set_state(initial_state);
11173 cx.update_editor(|editor, window, cx| {
11174 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11175 });
11176 handle_completion_request_with_insert_and_replace(
11177 &mut cx,
11178 &buffer_marked_text,
11179 vec![(completion_text, completion_text)],
11180 counter.clone(),
11181 )
11182 .await;
11183 cx.condition(|editor, _| editor.context_menu_visible())
11184 .await;
11185 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11186
11187 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11188 editor
11189 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
11190 .unwrap()
11191 });
11192 cx.assert_editor_state(&expected_with_insert_mode);
11193 handle_resolve_completion_request(&mut cx, None).await;
11194 apply_additional_edits.await.unwrap();
11195}
11196
11197#[gpui::test]
11198async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
11199 init_test(cx, |_| {});
11200 let mut cx = EditorLspTestContext::new_rust(
11201 lsp::ServerCapabilities {
11202 completion_provider: Some(lsp::CompletionOptions {
11203 resolve_provider: Some(true),
11204 ..Default::default()
11205 }),
11206 ..Default::default()
11207 },
11208 cx,
11209 )
11210 .await;
11211
11212 // scenario: surrounding text matches completion text
11213 let completion_text = "to_offset";
11214 let initial_state = indoc! {"
11215 1. buf.to_offˇsuffix
11216 2. buf.to_offˇsuf
11217 3. buf.to_offˇfix
11218 4. buf.to_offˇ
11219 5. into_offˇensive
11220 6. ˇsuffix
11221 7. let ˇ //
11222 8. aaˇzz
11223 9. buf.to_off«zzzzzˇ»suffix
11224 10. buf.«ˇzzzzz»suffix
11225 11. to_off«ˇzzzzz»
11226
11227 buf.to_offˇsuffix // newest cursor
11228 "};
11229 let completion_marked_buffer = indoc! {"
11230 1. buf.to_offsuffix
11231 2. buf.to_offsuf
11232 3. buf.to_offfix
11233 4. buf.to_off
11234 5. into_offensive
11235 6. suffix
11236 7. let //
11237 8. aazz
11238 9. buf.to_offzzzzzsuffix
11239 10. buf.zzzzzsuffix
11240 11. to_offzzzzz
11241
11242 buf.<to_off|suffix> // newest cursor
11243 "};
11244 let expected = indoc! {"
11245 1. buf.to_offsetˇ
11246 2. buf.to_offsetˇsuf
11247 3. buf.to_offsetˇfix
11248 4. buf.to_offsetˇ
11249 5. into_offsetˇensive
11250 6. to_offsetˇsuffix
11251 7. let to_offsetˇ //
11252 8. aato_offsetˇzz
11253 9. buf.to_offsetˇ
11254 10. buf.to_offsetˇsuffix
11255 11. to_offsetˇ
11256
11257 buf.to_offsetˇ // newest cursor
11258 "};
11259 cx.set_state(initial_state);
11260 cx.update_editor(|editor, window, cx| {
11261 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11262 });
11263 handle_completion_request_with_insert_and_replace(
11264 &mut cx,
11265 completion_marked_buffer,
11266 vec![(completion_text, completion_text)],
11267 Arc::new(AtomicUsize::new(0)),
11268 )
11269 .await;
11270 cx.condition(|editor, _| editor.context_menu_visible())
11271 .await;
11272 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11273 editor
11274 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11275 .unwrap()
11276 });
11277 cx.assert_editor_state(expected);
11278 handle_resolve_completion_request(&mut cx, None).await;
11279 apply_additional_edits.await.unwrap();
11280
11281 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
11282 let completion_text = "foo_and_bar";
11283 let initial_state = indoc! {"
11284 1. ooanbˇ
11285 2. zooanbˇ
11286 3. ooanbˇz
11287 4. zooanbˇz
11288 5. ooanˇ
11289 6. oanbˇ
11290
11291 ooanbˇ
11292 "};
11293 let completion_marked_buffer = indoc! {"
11294 1. ooanb
11295 2. zooanb
11296 3. ooanbz
11297 4. zooanbz
11298 5. ooan
11299 6. oanb
11300
11301 <ooanb|>
11302 "};
11303 let expected = indoc! {"
11304 1. foo_and_barˇ
11305 2. zfoo_and_barˇ
11306 3. foo_and_barˇz
11307 4. zfoo_and_barˇz
11308 5. ooanfoo_and_barˇ
11309 6. oanbfoo_and_barˇ
11310
11311 foo_and_barˇ
11312 "};
11313 cx.set_state(initial_state);
11314 cx.update_editor(|editor, window, cx| {
11315 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11316 });
11317 handle_completion_request_with_insert_and_replace(
11318 &mut cx,
11319 completion_marked_buffer,
11320 vec![(completion_text, completion_text)],
11321 Arc::new(AtomicUsize::new(0)),
11322 )
11323 .await;
11324 cx.condition(|editor, _| editor.context_menu_visible())
11325 .await;
11326 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11327 editor
11328 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11329 .unwrap()
11330 });
11331 cx.assert_editor_state(expected);
11332 handle_resolve_completion_request(&mut cx, None).await;
11333 apply_additional_edits.await.unwrap();
11334
11335 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
11336 // (expects the same as if it was inserted at the end)
11337 let completion_text = "foo_and_bar";
11338 let initial_state = indoc! {"
11339 1. ooˇanb
11340 2. zooˇanb
11341 3. ooˇanbz
11342 4. zooˇanbz
11343
11344 ooˇanb
11345 "};
11346 let completion_marked_buffer = indoc! {"
11347 1. ooanb
11348 2. zooanb
11349 3. ooanbz
11350 4. zooanbz
11351
11352 <oo|anb>
11353 "};
11354 let expected = indoc! {"
11355 1. foo_and_barˇ
11356 2. zfoo_and_barˇ
11357 3. foo_and_barˇz
11358 4. zfoo_and_barˇz
11359
11360 foo_and_barˇ
11361 "};
11362 cx.set_state(initial_state);
11363 cx.update_editor(|editor, window, cx| {
11364 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11365 });
11366 handle_completion_request_with_insert_and_replace(
11367 &mut cx,
11368 completion_marked_buffer,
11369 vec![(completion_text, completion_text)],
11370 Arc::new(AtomicUsize::new(0)),
11371 )
11372 .await;
11373 cx.condition(|editor, _| editor.context_menu_visible())
11374 .await;
11375 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11376 editor
11377 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11378 .unwrap()
11379 });
11380 cx.assert_editor_state(expected);
11381 handle_resolve_completion_request(&mut cx, None).await;
11382 apply_additional_edits.await.unwrap();
11383}
11384
11385// This used to crash
11386#[gpui::test]
11387async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
11388 init_test(cx, |_| {});
11389
11390 let buffer_text = indoc! {"
11391 fn main() {
11392 10.satu;
11393
11394 //
11395 // separate cursors so they open in different excerpts (manually reproducible)
11396 //
11397
11398 10.satu20;
11399 }
11400 "};
11401 let multibuffer_text_with_selections = indoc! {"
11402 fn main() {
11403 10.satuˇ;
11404
11405 //
11406
11407 //
11408
11409 10.satuˇ20;
11410 }
11411 "};
11412 let expected_multibuffer = indoc! {"
11413 fn main() {
11414 10.saturating_sub()ˇ;
11415
11416 //
11417
11418 //
11419
11420 10.saturating_sub()ˇ;
11421 }
11422 "};
11423
11424 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
11425 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
11426
11427 let fs = FakeFs::new(cx.executor());
11428 fs.insert_tree(
11429 path!("/a"),
11430 json!({
11431 "main.rs": buffer_text,
11432 }),
11433 )
11434 .await;
11435
11436 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11437 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11438 language_registry.add(rust_lang());
11439 let mut fake_servers = language_registry.register_fake_lsp(
11440 "Rust",
11441 FakeLspAdapter {
11442 capabilities: lsp::ServerCapabilities {
11443 completion_provider: Some(lsp::CompletionOptions {
11444 resolve_provider: None,
11445 ..lsp::CompletionOptions::default()
11446 }),
11447 ..lsp::ServerCapabilities::default()
11448 },
11449 ..FakeLspAdapter::default()
11450 },
11451 );
11452 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11453 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11454 let buffer = project
11455 .update(cx, |project, cx| {
11456 project.open_local_buffer(path!("/a/main.rs"), cx)
11457 })
11458 .await
11459 .unwrap();
11460
11461 let multi_buffer = cx.new(|cx| {
11462 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
11463 multi_buffer.push_excerpts(
11464 buffer.clone(),
11465 [ExcerptRange::new(0..first_excerpt_end)],
11466 cx,
11467 );
11468 multi_buffer.push_excerpts(
11469 buffer.clone(),
11470 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
11471 cx,
11472 );
11473 multi_buffer
11474 });
11475
11476 let editor = workspace
11477 .update(cx, |_, window, cx| {
11478 cx.new(|cx| {
11479 Editor::new(
11480 EditorMode::Full {
11481 scale_ui_elements_with_buffer_font_size: false,
11482 show_active_line_background: false,
11483 sized_by_content: false,
11484 },
11485 multi_buffer.clone(),
11486 Some(project.clone()),
11487 window,
11488 cx,
11489 )
11490 })
11491 })
11492 .unwrap();
11493
11494 let pane = workspace
11495 .update(cx, |workspace, _, _| workspace.active_pane().clone())
11496 .unwrap();
11497 pane.update_in(cx, |pane, window, cx| {
11498 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
11499 });
11500
11501 let fake_server = fake_servers.next().await.unwrap();
11502
11503 editor.update_in(cx, |editor, window, cx| {
11504 editor.change_selections(None, window, cx, |s| {
11505 s.select_ranges([
11506 Point::new(1, 11)..Point::new(1, 11),
11507 Point::new(7, 11)..Point::new(7, 11),
11508 ])
11509 });
11510
11511 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
11512 });
11513
11514 editor.update_in(cx, |editor, window, cx| {
11515 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11516 });
11517
11518 fake_server
11519 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11520 let completion_item = lsp::CompletionItem {
11521 label: "saturating_sub()".into(),
11522 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
11523 lsp::InsertReplaceEdit {
11524 new_text: "saturating_sub()".to_owned(),
11525 insert: lsp::Range::new(
11526 lsp::Position::new(7, 7),
11527 lsp::Position::new(7, 11),
11528 ),
11529 replace: lsp::Range::new(
11530 lsp::Position::new(7, 7),
11531 lsp::Position::new(7, 13),
11532 ),
11533 },
11534 )),
11535 ..lsp::CompletionItem::default()
11536 };
11537
11538 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
11539 })
11540 .next()
11541 .await
11542 .unwrap();
11543
11544 cx.condition(&editor, |editor, _| editor.context_menu_visible())
11545 .await;
11546
11547 editor
11548 .update_in(cx, |editor, window, cx| {
11549 editor
11550 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11551 .unwrap()
11552 })
11553 .await
11554 .unwrap();
11555
11556 editor.update(cx, |editor, cx| {
11557 assert_text_with_selections(editor, expected_multibuffer, cx);
11558 })
11559}
11560
11561#[gpui::test]
11562async fn test_completion(cx: &mut TestAppContext) {
11563 init_test(cx, |_| {});
11564
11565 let mut cx = EditorLspTestContext::new_rust(
11566 lsp::ServerCapabilities {
11567 completion_provider: Some(lsp::CompletionOptions {
11568 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11569 resolve_provider: Some(true),
11570 ..Default::default()
11571 }),
11572 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11573 ..Default::default()
11574 },
11575 cx,
11576 )
11577 .await;
11578 let counter = Arc::new(AtomicUsize::new(0));
11579
11580 cx.set_state(indoc! {"
11581 oneˇ
11582 two
11583 three
11584 "});
11585 cx.simulate_keystroke(".");
11586 handle_completion_request(
11587 indoc! {"
11588 one.|<>
11589 two
11590 three
11591 "},
11592 vec!["first_completion", "second_completion"],
11593 true,
11594 counter.clone(),
11595 &mut cx,
11596 )
11597 .await;
11598 cx.condition(|editor, _| editor.context_menu_visible())
11599 .await;
11600 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11601
11602 let _handler = handle_signature_help_request(
11603 &mut cx,
11604 lsp::SignatureHelp {
11605 signatures: vec![lsp::SignatureInformation {
11606 label: "test signature".to_string(),
11607 documentation: None,
11608 parameters: Some(vec![lsp::ParameterInformation {
11609 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
11610 documentation: None,
11611 }]),
11612 active_parameter: None,
11613 }],
11614 active_signature: None,
11615 active_parameter: None,
11616 },
11617 );
11618 cx.update_editor(|editor, window, cx| {
11619 assert!(
11620 !editor.signature_help_state.is_shown(),
11621 "No signature help was called for"
11622 );
11623 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11624 });
11625 cx.run_until_parked();
11626 cx.update_editor(|editor, _, _| {
11627 assert!(
11628 !editor.signature_help_state.is_shown(),
11629 "No signature help should be shown when completions menu is open"
11630 );
11631 });
11632
11633 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11634 editor.context_menu_next(&Default::default(), window, cx);
11635 editor
11636 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11637 .unwrap()
11638 });
11639 cx.assert_editor_state(indoc! {"
11640 one.second_completionˇ
11641 two
11642 three
11643 "});
11644
11645 handle_resolve_completion_request(
11646 &mut cx,
11647 Some(vec![
11648 (
11649 //This overlaps with the primary completion edit which is
11650 //misbehavior from the LSP spec, test that we filter it out
11651 indoc! {"
11652 one.second_ˇcompletion
11653 two
11654 threeˇ
11655 "},
11656 "overlapping additional edit",
11657 ),
11658 (
11659 indoc! {"
11660 one.second_completion
11661 two
11662 threeˇ
11663 "},
11664 "\nadditional edit",
11665 ),
11666 ]),
11667 )
11668 .await;
11669 apply_additional_edits.await.unwrap();
11670 cx.assert_editor_state(indoc! {"
11671 one.second_completionˇ
11672 two
11673 three
11674 additional edit
11675 "});
11676
11677 cx.set_state(indoc! {"
11678 one.second_completion
11679 twoˇ
11680 threeˇ
11681 additional edit
11682 "});
11683 cx.simulate_keystroke(" ");
11684 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11685 cx.simulate_keystroke("s");
11686 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11687
11688 cx.assert_editor_state(indoc! {"
11689 one.second_completion
11690 two sˇ
11691 three sˇ
11692 additional edit
11693 "});
11694 handle_completion_request(
11695 indoc! {"
11696 one.second_completion
11697 two s
11698 three <s|>
11699 additional edit
11700 "},
11701 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11702 true,
11703 counter.clone(),
11704 &mut cx,
11705 )
11706 .await;
11707 cx.condition(|editor, _| editor.context_menu_visible())
11708 .await;
11709 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11710
11711 cx.simulate_keystroke("i");
11712
11713 handle_completion_request(
11714 indoc! {"
11715 one.second_completion
11716 two si
11717 three <si|>
11718 additional edit
11719 "},
11720 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11721 true,
11722 counter.clone(),
11723 &mut cx,
11724 )
11725 .await;
11726 cx.condition(|editor, _| editor.context_menu_visible())
11727 .await;
11728 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
11729
11730 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11731 editor
11732 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11733 .unwrap()
11734 });
11735 cx.assert_editor_state(indoc! {"
11736 one.second_completion
11737 two sixth_completionˇ
11738 three sixth_completionˇ
11739 additional edit
11740 "});
11741
11742 apply_additional_edits.await.unwrap();
11743
11744 update_test_language_settings(&mut cx, |settings| {
11745 settings.defaults.show_completions_on_input = Some(false);
11746 });
11747 cx.set_state("editorˇ");
11748 cx.simulate_keystroke(".");
11749 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11750 cx.simulate_keystrokes("c l o");
11751 cx.assert_editor_state("editor.cloˇ");
11752 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11753 cx.update_editor(|editor, window, cx| {
11754 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11755 });
11756 handle_completion_request(
11757 "editor.<clo|>",
11758 vec!["close", "clobber"],
11759 true,
11760 counter.clone(),
11761 &mut cx,
11762 )
11763 .await;
11764 cx.condition(|editor, _| editor.context_menu_visible())
11765 .await;
11766 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
11767
11768 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11769 editor
11770 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11771 .unwrap()
11772 });
11773 cx.assert_editor_state("editor.closeˇ");
11774 handle_resolve_completion_request(&mut cx, None).await;
11775 apply_additional_edits.await.unwrap();
11776}
11777
11778#[gpui::test]
11779async fn test_completion_reuse(cx: &mut TestAppContext) {
11780 init_test(cx, |_| {});
11781
11782 let mut cx = EditorLspTestContext::new_rust(
11783 lsp::ServerCapabilities {
11784 completion_provider: Some(lsp::CompletionOptions {
11785 trigger_characters: Some(vec![".".to_string()]),
11786 ..Default::default()
11787 }),
11788 ..Default::default()
11789 },
11790 cx,
11791 )
11792 .await;
11793
11794 let counter = Arc::new(AtomicUsize::new(0));
11795 cx.set_state("objˇ");
11796 cx.simulate_keystroke(".");
11797
11798 // Initial completion request returns complete results
11799 let is_incomplete = false;
11800 handle_completion_request(
11801 "obj.|<>",
11802 vec!["a", "ab", "abc"],
11803 is_incomplete,
11804 counter.clone(),
11805 &mut cx,
11806 )
11807 .await;
11808 cx.run_until_parked();
11809 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11810 cx.assert_editor_state("obj.ˇ");
11811 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
11812
11813 // Type "a" - filters existing completions
11814 cx.simulate_keystroke("a");
11815 cx.run_until_parked();
11816 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11817 cx.assert_editor_state("obj.aˇ");
11818 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
11819
11820 // Type "b" - filters existing completions
11821 cx.simulate_keystroke("b");
11822 cx.run_until_parked();
11823 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11824 cx.assert_editor_state("obj.abˇ");
11825 check_displayed_completions(vec!["ab", "abc"], &mut cx);
11826
11827 // Type "c" - filters existing completions
11828 cx.simulate_keystroke("c");
11829 cx.run_until_parked();
11830 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11831 cx.assert_editor_state("obj.abcˇ");
11832 check_displayed_completions(vec!["abc"], &mut cx);
11833
11834 // Backspace to delete "c" - filters existing completions
11835 cx.update_editor(|editor, window, cx| {
11836 editor.backspace(&Backspace, window, cx);
11837 });
11838 cx.run_until_parked();
11839 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11840 cx.assert_editor_state("obj.abˇ");
11841 check_displayed_completions(vec!["ab", "abc"], &mut cx);
11842
11843 // Moving cursor to the left dismisses menu.
11844 cx.update_editor(|editor, window, cx| {
11845 editor.move_left(&MoveLeft, window, cx);
11846 });
11847 cx.run_until_parked();
11848 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11849 cx.assert_editor_state("obj.aˇb");
11850 cx.update_editor(|editor, _, _| {
11851 assert_eq!(editor.context_menu_visible(), false);
11852 });
11853
11854 // Type "b" - new request
11855 cx.simulate_keystroke("b");
11856 let is_incomplete = false;
11857 handle_completion_request(
11858 "obj.<ab|>a",
11859 vec!["ab", "abc"],
11860 is_incomplete,
11861 counter.clone(),
11862 &mut cx,
11863 )
11864 .await;
11865 cx.run_until_parked();
11866 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11867 cx.assert_editor_state("obj.abˇb");
11868 check_displayed_completions(vec!["ab", "abc"], &mut cx);
11869
11870 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
11871 cx.update_editor(|editor, window, cx| {
11872 editor.backspace(&Backspace, window, cx);
11873 });
11874 let is_incomplete = false;
11875 handle_completion_request(
11876 "obj.<a|>b",
11877 vec!["a", "ab", "abc"],
11878 is_incomplete,
11879 counter.clone(),
11880 &mut cx,
11881 )
11882 .await;
11883 cx.run_until_parked();
11884 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
11885 cx.assert_editor_state("obj.aˇb");
11886 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
11887
11888 // Backspace to delete "a" - dismisses menu.
11889 cx.update_editor(|editor, window, cx| {
11890 editor.backspace(&Backspace, window, cx);
11891 });
11892 cx.run_until_parked();
11893 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
11894 cx.assert_editor_state("obj.ˇb");
11895 cx.update_editor(|editor, _, _| {
11896 assert_eq!(editor.context_menu_visible(), false);
11897 });
11898}
11899
11900#[gpui::test]
11901async fn test_word_completion(cx: &mut TestAppContext) {
11902 let lsp_fetch_timeout_ms = 10;
11903 init_test(cx, |language_settings| {
11904 language_settings.defaults.completions = Some(CompletionSettings {
11905 words: WordsCompletionMode::Fallback,
11906 lsp: true,
11907 lsp_fetch_timeout_ms: 10,
11908 lsp_insert_mode: LspInsertMode::Insert,
11909 });
11910 });
11911
11912 let mut cx = EditorLspTestContext::new_rust(
11913 lsp::ServerCapabilities {
11914 completion_provider: Some(lsp::CompletionOptions {
11915 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11916 ..lsp::CompletionOptions::default()
11917 }),
11918 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11919 ..lsp::ServerCapabilities::default()
11920 },
11921 cx,
11922 )
11923 .await;
11924
11925 let throttle_completions = Arc::new(AtomicBool::new(false));
11926
11927 let lsp_throttle_completions = throttle_completions.clone();
11928 let _completion_requests_handler =
11929 cx.lsp
11930 .server
11931 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
11932 let lsp_throttle_completions = lsp_throttle_completions.clone();
11933 let cx = cx.clone();
11934 async move {
11935 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
11936 cx.background_executor()
11937 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
11938 .await;
11939 }
11940 Ok(Some(lsp::CompletionResponse::Array(vec![
11941 lsp::CompletionItem {
11942 label: "first".into(),
11943 ..lsp::CompletionItem::default()
11944 },
11945 lsp::CompletionItem {
11946 label: "last".into(),
11947 ..lsp::CompletionItem::default()
11948 },
11949 ])))
11950 }
11951 });
11952
11953 cx.set_state(indoc! {"
11954 oneˇ
11955 two
11956 three
11957 "});
11958 cx.simulate_keystroke(".");
11959 cx.executor().run_until_parked();
11960 cx.condition(|editor, _| editor.context_menu_visible())
11961 .await;
11962 cx.update_editor(|editor, window, cx| {
11963 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11964 {
11965 assert_eq!(
11966 completion_menu_entries(&menu),
11967 &["first", "last"],
11968 "When LSP server is fast to reply, no fallback word completions are used"
11969 );
11970 } else {
11971 panic!("expected completion menu to be open");
11972 }
11973 editor.cancel(&Cancel, window, cx);
11974 });
11975 cx.executor().run_until_parked();
11976 cx.condition(|editor, _| !editor.context_menu_visible())
11977 .await;
11978
11979 throttle_completions.store(true, atomic::Ordering::Release);
11980 cx.simulate_keystroke(".");
11981 cx.executor()
11982 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
11983 cx.executor().run_until_parked();
11984 cx.condition(|editor, _| editor.context_menu_visible())
11985 .await;
11986 cx.update_editor(|editor, _, _| {
11987 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11988 {
11989 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
11990 "When LSP server is slow, document words can be shown instead, if configured accordingly");
11991 } else {
11992 panic!("expected completion menu to be open");
11993 }
11994 });
11995}
11996
11997#[gpui::test]
11998async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
11999 init_test(cx, |language_settings| {
12000 language_settings.defaults.completions = Some(CompletionSettings {
12001 words: WordsCompletionMode::Enabled,
12002 lsp: true,
12003 lsp_fetch_timeout_ms: 0,
12004 lsp_insert_mode: LspInsertMode::Insert,
12005 });
12006 });
12007
12008 let mut cx = EditorLspTestContext::new_rust(
12009 lsp::ServerCapabilities {
12010 completion_provider: Some(lsp::CompletionOptions {
12011 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12012 ..lsp::CompletionOptions::default()
12013 }),
12014 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12015 ..lsp::ServerCapabilities::default()
12016 },
12017 cx,
12018 )
12019 .await;
12020
12021 let _completion_requests_handler =
12022 cx.lsp
12023 .server
12024 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12025 Ok(Some(lsp::CompletionResponse::Array(vec![
12026 lsp::CompletionItem {
12027 label: "first".into(),
12028 ..lsp::CompletionItem::default()
12029 },
12030 lsp::CompletionItem {
12031 label: "last".into(),
12032 ..lsp::CompletionItem::default()
12033 },
12034 ])))
12035 });
12036
12037 cx.set_state(indoc! {"ˇ
12038 first
12039 last
12040 second
12041 "});
12042 cx.simulate_keystroke(".");
12043 cx.executor().run_until_parked();
12044 cx.condition(|editor, _| editor.context_menu_visible())
12045 .await;
12046 cx.update_editor(|editor, _, _| {
12047 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12048 {
12049 assert_eq!(
12050 completion_menu_entries(&menu),
12051 &["first", "last", "second"],
12052 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
12053 );
12054 } else {
12055 panic!("expected completion menu to be open");
12056 }
12057 });
12058}
12059
12060#[gpui::test]
12061async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
12062 init_test(cx, |language_settings| {
12063 language_settings.defaults.completions = Some(CompletionSettings {
12064 words: WordsCompletionMode::Disabled,
12065 lsp: true,
12066 lsp_fetch_timeout_ms: 0,
12067 lsp_insert_mode: LspInsertMode::Insert,
12068 });
12069 });
12070
12071 let mut cx = EditorLspTestContext::new_rust(
12072 lsp::ServerCapabilities {
12073 completion_provider: Some(lsp::CompletionOptions {
12074 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12075 ..lsp::CompletionOptions::default()
12076 }),
12077 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12078 ..lsp::ServerCapabilities::default()
12079 },
12080 cx,
12081 )
12082 .await;
12083
12084 let _completion_requests_handler =
12085 cx.lsp
12086 .server
12087 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12088 panic!("LSP completions should not be queried when dealing with word completions")
12089 });
12090
12091 cx.set_state(indoc! {"ˇ
12092 first
12093 last
12094 second
12095 "});
12096 cx.update_editor(|editor, window, cx| {
12097 editor.show_word_completions(&ShowWordCompletions, window, cx);
12098 });
12099 cx.executor().run_until_parked();
12100 cx.condition(|editor, _| editor.context_menu_visible())
12101 .await;
12102 cx.update_editor(|editor, _, _| {
12103 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12104 {
12105 assert_eq!(
12106 completion_menu_entries(&menu),
12107 &["first", "last", "second"],
12108 "`ShowWordCompletions` action should show word completions"
12109 );
12110 } else {
12111 panic!("expected completion menu to be open");
12112 }
12113 });
12114
12115 cx.simulate_keystroke("l");
12116 cx.executor().run_until_parked();
12117 cx.condition(|editor, _| editor.context_menu_visible())
12118 .await;
12119 cx.update_editor(|editor, _, _| {
12120 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12121 {
12122 assert_eq!(
12123 completion_menu_entries(&menu),
12124 &["last"],
12125 "After showing word completions, further editing should filter them and not query the LSP"
12126 );
12127 } else {
12128 panic!("expected completion menu to be open");
12129 }
12130 });
12131}
12132
12133#[gpui::test]
12134async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
12135 init_test(cx, |language_settings| {
12136 language_settings.defaults.completions = Some(CompletionSettings {
12137 words: WordsCompletionMode::Fallback,
12138 lsp: false,
12139 lsp_fetch_timeout_ms: 0,
12140 lsp_insert_mode: LspInsertMode::Insert,
12141 });
12142 });
12143
12144 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12145
12146 cx.set_state(indoc! {"ˇ
12147 0_usize
12148 let
12149 33
12150 4.5f32
12151 "});
12152 cx.update_editor(|editor, window, cx| {
12153 editor.show_completions(&ShowCompletions::default(), window, cx);
12154 });
12155 cx.executor().run_until_parked();
12156 cx.condition(|editor, _| editor.context_menu_visible())
12157 .await;
12158 cx.update_editor(|editor, window, cx| {
12159 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12160 {
12161 assert_eq!(
12162 completion_menu_entries(&menu),
12163 &["let"],
12164 "With no digits in the completion query, no digits should be in the word completions"
12165 );
12166 } else {
12167 panic!("expected completion menu to be open");
12168 }
12169 editor.cancel(&Cancel, window, cx);
12170 });
12171
12172 cx.set_state(indoc! {"3ˇ
12173 0_usize
12174 let
12175 3
12176 33.35f32
12177 "});
12178 cx.update_editor(|editor, window, cx| {
12179 editor.show_completions(&ShowCompletions::default(), window, cx);
12180 });
12181 cx.executor().run_until_parked();
12182 cx.condition(|editor, _| editor.context_menu_visible())
12183 .await;
12184 cx.update_editor(|editor, _, _| {
12185 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12186 {
12187 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
12188 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
12189 } else {
12190 panic!("expected completion menu to be open");
12191 }
12192 });
12193}
12194
12195fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
12196 let position = || lsp::Position {
12197 line: params.text_document_position.position.line,
12198 character: params.text_document_position.position.character,
12199 };
12200 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12201 range: lsp::Range {
12202 start: position(),
12203 end: position(),
12204 },
12205 new_text: text.to_string(),
12206 }))
12207}
12208
12209#[gpui::test]
12210async fn test_multiline_completion(cx: &mut TestAppContext) {
12211 init_test(cx, |_| {});
12212
12213 let fs = FakeFs::new(cx.executor());
12214 fs.insert_tree(
12215 path!("/a"),
12216 json!({
12217 "main.ts": "a",
12218 }),
12219 )
12220 .await;
12221
12222 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12223 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12224 let typescript_language = Arc::new(Language::new(
12225 LanguageConfig {
12226 name: "TypeScript".into(),
12227 matcher: LanguageMatcher {
12228 path_suffixes: vec!["ts".to_string()],
12229 ..LanguageMatcher::default()
12230 },
12231 line_comments: vec!["// ".into()],
12232 ..LanguageConfig::default()
12233 },
12234 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12235 ));
12236 language_registry.add(typescript_language.clone());
12237 let mut fake_servers = language_registry.register_fake_lsp(
12238 "TypeScript",
12239 FakeLspAdapter {
12240 capabilities: lsp::ServerCapabilities {
12241 completion_provider: Some(lsp::CompletionOptions {
12242 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12243 ..lsp::CompletionOptions::default()
12244 }),
12245 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12246 ..lsp::ServerCapabilities::default()
12247 },
12248 // Emulate vtsls label generation
12249 label_for_completion: Some(Box::new(|item, _| {
12250 let text = if let Some(description) = item
12251 .label_details
12252 .as_ref()
12253 .and_then(|label_details| label_details.description.as_ref())
12254 {
12255 format!("{} {}", item.label, description)
12256 } else if let Some(detail) = &item.detail {
12257 format!("{} {}", item.label, detail)
12258 } else {
12259 item.label.clone()
12260 };
12261 let len = text.len();
12262 Some(language::CodeLabel {
12263 text,
12264 runs: Vec::new(),
12265 filter_range: 0..len,
12266 })
12267 })),
12268 ..FakeLspAdapter::default()
12269 },
12270 );
12271 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12272 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12273 let worktree_id = workspace
12274 .update(cx, |workspace, _window, cx| {
12275 workspace.project().update(cx, |project, cx| {
12276 project.worktrees(cx).next().unwrap().read(cx).id()
12277 })
12278 })
12279 .unwrap();
12280 let _buffer = project
12281 .update(cx, |project, cx| {
12282 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
12283 })
12284 .await
12285 .unwrap();
12286 let editor = workspace
12287 .update(cx, |workspace, window, cx| {
12288 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
12289 })
12290 .unwrap()
12291 .await
12292 .unwrap()
12293 .downcast::<Editor>()
12294 .unwrap();
12295 let fake_server = fake_servers.next().await.unwrap();
12296
12297 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
12298 let multiline_label_2 = "a\nb\nc\n";
12299 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
12300 let multiline_description = "d\ne\nf\n";
12301 let multiline_detail_2 = "g\nh\ni\n";
12302
12303 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
12304 move |params, _| async move {
12305 Ok(Some(lsp::CompletionResponse::Array(vec![
12306 lsp::CompletionItem {
12307 label: multiline_label.to_string(),
12308 text_edit: gen_text_edit(¶ms, "new_text_1"),
12309 ..lsp::CompletionItem::default()
12310 },
12311 lsp::CompletionItem {
12312 label: "single line label 1".to_string(),
12313 detail: Some(multiline_detail.to_string()),
12314 text_edit: gen_text_edit(¶ms, "new_text_2"),
12315 ..lsp::CompletionItem::default()
12316 },
12317 lsp::CompletionItem {
12318 label: "single line label 2".to_string(),
12319 label_details: Some(lsp::CompletionItemLabelDetails {
12320 description: Some(multiline_description.to_string()),
12321 detail: None,
12322 }),
12323 text_edit: gen_text_edit(¶ms, "new_text_2"),
12324 ..lsp::CompletionItem::default()
12325 },
12326 lsp::CompletionItem {
12327 label: multiline_label_2.to_string(),
12328 detail: Some(multiline_detail_2.to_string()),
12329 text_edit: gen_text_edit(¶ms, "new_text_3"),
12330 ..lsp::CompletionItem::default()
12331 },
12332 lsp::CompletionItem {
12333 label: "Label with many spaces and \t but without newlines".to_string(),
12334 detail: Some(
12335 "Details with many spaces and \t but without newlines".to_string(),
12336 ),
12337 text_edit: gen_text_edit(¶ms, "new_text_4"),
12338 ..lsp::CompletionItem::default()
12339 },
12340 ])))
12341 },
12342 );
12343
12344 editor.update_in(cx, |editor, window, cx| {
12345 cx.focus_self(window);
12346 editor.move_to_end(&MoveToEnd, window, cx);
12347 editor.handle_input(".", window, cx);
12348 });
12349 cx.run_until_parked();
12350 completion_handle.next().await.unwrap();
12351
12352 editor.update(cx, |editor, _| {
12353 assert!(editor.context_menu_visible());
12354 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12355 {
12356 let completion_labels = menu
12357 .completions
12358 .borrow()
12359 .iter()
12360 .map(|c| c.label.text.clone())
12361 .collect::<Vec<_>>();
12362 assert_eq!(
12363 completion_labels,
12364 &[
12365 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
12366 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
12367 "single line label 2 d e f ",
12368 "a b c g h i ",
12369 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
12370 ],
12371 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
12372 );
12373
12374 for completion in menu
12375 .completions
12376 .borrow()
12377 .iter() {
12378 assert_eq!(
12379 completion.label.filter_range,
12380 0..completion.label.text.len(),
12381 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
12382 );
12383 }
12384 } else {
12385 panic!("expected completion menu to be open");
12386 }
12387 });
12388}
12389
12390#[gpui::test]
12391async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
12392 init_test(cx, |_| {});
12393 let mut cx = EditorLspTestContext::new_rust(
12394 lsp::ServerCapabilities {
12395 completion_provider: Some(lsp::CompletionOptions {
12396 trigger_characters: Some(vec![".".to_string()]),
12397 ..Default::default()
12398 }),
12399 ..Default::default()
12400 },
12401 cx,
12402 )
12403 .await;
12404 cx.lsp
12405 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12406 Ok(Some(lsp::CompletionResponse::Array(vec![
12407 lsp::CompletionItem {
12408 label: "first".into(),
12409 ..Default::default()
12410 },
12411 lsp::CompletionItem {
12412 label: "last".into(),
12413 ..Default::default()
12414 },
12415 ])))
12416 });
12417 cx.set_state("variableˇ");
12418 cx.simulate_keystroke(".");
12419 cx.executor().run_until_parked();
12420
12421 cx.update_editor(|editor, _, _| {
12422 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12423 {
12424 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
12425 } else {
12426 panic!("expected completion menu to be open");
12427 }
12428 });
12429
12430 cx.update_editor(|editor, window, cx| {
12431 editor.move_page_down(&MovePageDown::default(), window, cx);
12432 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12433 {
12434 assert!(
12435 menu.selected_item == 1,
12436 "expected PageDown to select the last item from the context menu"
12437 );
12438 } else {
12439 panic!("expected completion menu to stay open after PageDown");
12440 }
12441 });
12442
12443 cx.update_editor(|editor, window, cx| {
12444 editor.move_page_up(&MovePageUp::default(), window, cx);
12445 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12446 {
12447 assert!(
12448 menu.selected_item == 0,
12449 "expected PageUp to select the first item from the context menu"
12450 );
12451 } else {
12452 panic!("expected completion menu to stay open after PageUp");
12453 }
12454 });
12455}
12456
12457#[gpui::test]
12458async fn test_as_is_completions(cx: &mut TestAppContext) {
12459 init_test(cx, |_| {});
12460 let mut cx = EditorLspTestContext::new_rust(
12461 lsp::ServerCapabilities {
12462 completion_provider: Some(lsp::CompletionOptions {
12463 ..Default::default()
12464 }),
12465 ..Default::default()
12466 },
12467 cx,
12468 )
12469 .await;
12470 cx.lsp
12471 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12472 Ok(Some(lsp::CompletionResponse::Array(vec![
12473 lsp::CompletionItem {
12474 label: "unsafe".into(),
12475 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12476 range: lsp::Range {
12477 start: lsp::Position {
12478 line: 1,
12479 character: 2,
12480 },
12481 end: lsp::Position {
12482 line: 1,
12483 character: 3,
12484 },
12485 },
12486 new_text: "unsafe".to_string(),
12487 })),
12488 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
12489 ..Default::default()
12490 },
12491 ])))
12492 });
12493 cx.set_state("fn a() {}\n nˇ");
12494 cx.executor().run_until_parked();
12495 cx.update_editor(|editor, window, cx| {
12496 editor.show_completions(
12497 &ShowCompletions {
12498 trigger: Some("\n".into()),
12499 },
12500 window,
12501 cx,
12502 );
12503 });
12504 cx.executor().run_until_parked();
12505
12506 cx.update_editor(|editor, window, cx| {
12507 editor.confirm_completion(&Default::default(), window, cx)
12508 });
12509 cx.executor().run_until_parked();
12510 cx.assert_editor_state("fn a() {}\n unsafeˇ");
12511}
12512
12513#[gpui::test]
12514async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
12515 init_test(cx, |_| {});
12516
12517 let mut cx = EditorLspTestContext::new_rust(
12518 lsp::ServerCapabilities {
12519 completion_provider: Some(lsp::CompletionOptions {
12520 trigger_characters: Some(vec![".".to_string()]),
12521 resolve_provider: Some(true),
12522 ..Default::default()
12523 }),
12524 ..Default::default()
12525 },
12526 cx,
12527 )
12528 .await;
12529
12530 cx.set_state("fn main() { let a = 2ˇ; }");
12531 cx.simulate_keystroke(".");
12532 let completion_item = lsp::CompletionItem {
12533 label: "Some".into(),
12534 kind: Some(lsp::CompletionItemKind::SNIPPET),
12535 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
12536 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
12537 kind: lsp::MarkupKind::Markdown,
12538 value: "```rust\nSome(2)\n```".to_string(),
12539 })),
12540 deprecated: Some(false),
12541 sort_text: Some("Some".to_string()),
12542 filter_text: Some("Some".to_string()),
12543 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
12544 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12545 range: lsp::Range {
12546 start: lsp::Position {
12547 line: 0,
12548 character: 22,
12549 },
12550 end: lsp::Position {
12551 line: 0,
12552 character: 22,
12553 },
12554 },
12555 new_text: "Some(2)".to_string(),
12556 })),
12557 additional_text_edits: Some(vec![lsp::TextEdit {
12558 range: lsp::Range {
12559 start: lsp::Position {
12560 line: 0,
12561 character: 20,
12562 },
12563 end: lsp::Position {
12564 line: 0,
12565 character: 22,
12566 },
12567 },
12568 new_text: "".to_string(),
12569 }]),
12570 ..Default::default()
12571 };
12572
12573 let closure_completion_item = completion_item.clone();
12574 let counter = Arc::new(AtomicUsize::new(0));
12575 let counter_clone = counter.clone();
12576 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
12577 let task_completion_item = closure_completion_item.clone();
12578 counter_clone.fetch_add(1, atomic::Ordering::Release);
12579 async move {
12580 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
12581 is_incomplete: true,
12582 item_defaults: None,
12583 items: vec![task_completion_item],
12584 })))
12585 }
12586 });
12587
12588 cx.condition(|editor, _| editor.context_menu_visible())
12589 .await;
12590 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
12591 assert!(request.next().await.is_some());
12592 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12593
12594 cx.simulate_keystrokes("S o m");
12595 cx.condition(|editor, _| editor.context_menu_visible())
12596 .await;
12597 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
12598 assert!(request.next().await.is_some());
12599 assert!(request.next().await.is_some());
12600 assert!(request.next().await.is_some());
12601 request.close();
12602 assert!(request.next().await.is_none());
12603 assert_eq!(
12604 counter.load(atomic::Ordering::Acquire),
12605 4,
12606 "With the completions menu open, only one LSP request should happen per input"
12607 );
12608}
12609
12610#[gpui::test]
12611async fn test_toggle_comment(cx: &mut TestAppContext) {
12612 init_test(cx, |_| {});
12613 let mut cx = EditorTestContext::new(cx).await;
12614 let language = Arc::new(Language::new(
12615 LanguageConfig {
12616 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
12617 ..Default::default()
12618 },
12619 Some(tree_sitter_rust::LANGUAGE.into()),
12620 ));
12621 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
12622
12623 // If multiple selections intersect a line, the line is only toggled once.
12624 cx.set_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 // The comment prefix is inserted at the same column for every line in a
12643 // selection.
12644 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12645
12646 cx.assert_editor_state(indoc! {"
12647 fn a() {
12648 // «b();
12649 // c();
12650 ˇ»// d();
12651 }
12652 "});
12653
12654 // If a selection ends at the beginning of a line, that line is not toggled.
12655 cx.set_selections_state(indoc! {"
12656 fn a() {
12657 // b();
12658 «// c();
12659 ˇ» // d();
12660 }
12661 "});
12662
12663 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12664
12665 cx.assert_editor_state(indoc! {"
12666 fn a() {
12667 // b();
12668 «c();
12669 ˇ» // d();
12670 }
12671 "});
12672
12673 // If a selection span a single line and is empty, the line is toggled.
12674 cx.set_state(indoc! {"
12675 fn a() {
12676 a();
12677 b();
12678 ˇ
12679 }
12680 "});
12681
12682 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12683
12684 cx.assert_editor_state(indoc! {"
12685 fn a() {
12686 a();
12687 b();
12688 //•ˇ
12689 }
12690 "});
12691
12692 // If a selection span multiple lines, empty lines are not toggled.
12693 cx.set_state(indoc! {"
12694 fn a() {
12695 «a();
12696
12697 c();ˇ»
12698 }
12699 "});
12700
12701 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12702
12703 cx.assert_editor_state(indoc! {"
12704 fn a() {
12705 // «a();
12706
12707 // c();ˇ»
12708 }
12709 "});
12710
12711 // If a selection includes multiple comment prefixes, all lines are uncommented.
12712 cx.set_state(indoc! {"
12713 fn a() {
12714 «// a();
12715 /// b();
12716 //! c();ˇ»
12717 }
12718 "});
12719
12720 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12721
12722 cx.assert_editor_state(indoc! {"
12723 fn a() {
12724 «a();
12725 b();
12726 c();ˇ»
12727 }
12728 "});
12729}
12730
12731#[gpui::test]
12732async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
12733 init_test(cx, |_| {});
12734 let mut cx = EditorTestContext::new(cx).await;
12735 let language = Arc::new(Language::new(
12736 LanguageConfig {
12737 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
12738 ..Default::default()
12739 },
12740 Some(tree_sitter_rust::LANGUAGE.into()),
12741 ));
12742 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
12743
12744 let toggle_comments = &ToggleComments {
12745 advance_downwards: false,
12746 ignore_indent: true,
12747 };
12748
12749 // If multiple selections intersect a line, the line is only toggled once.
12750 cx.set_state(indoc! {"
12751 fn a() {
12752 // «b();
12753 // c();
12754 // ˇ» d();
12755 }
12756 "});
12757
12758 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12759
12760 cx.assert_editor_state(indoc! {"
12761 fn a() {
12762 «b();
12763 c();
12764 ˇ» d();
12765 }
12766 "});
12767
12768 // The comment prefix is inserted at the beginning of each line
12769 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12770
12771 cx.assert_editor_state(indoc! {"
12772 fn a() {
12773 // «b();
12774 // c();
12775 // ˇ» d();
12776 }
12777 "});
12778
12779 // If a selection ends at the beginning of a line, that line is not toggled.
12780 cx.set_selections_state(indoc! {"
12781 fn a() {
12782 // b();
12783 // «c();
12784 ˇ»// d();
12785 }
12786 "});
12787
12788 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12789
12790 cx.assert_editor_state(indoc! {"
12791 fn a() {
12792 // b();
12793 «c();
12794 ˇ»// d();
12795 }
12796 "});
12797
12798 // If a selection span a single line and is empty, the line is toggled.
12799 cx.set_state(indoc! {"
12800 fn a() {
12801 a();
12802 b();
12803 ˇ
12804 }
12805 "});
12806
12807 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12808
12809 cx.assert_editor_state(indoc! {"
12810 fn a() {
12811 a();
12812 b();
12813 //ˇ
12814 }
12815 "});
12816
12817 // If a selection span multiple lines, empty lines are not toggled.
12818 cx.set_state(indoc! {"
12819 fn a() {
12820 «a();
12821
12822 c();ˇ»
12823 }
12824 "});
12825
12826 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12827
12828 cx.assert_editor_state(indoc! {"
12829 fn a() {
12830 // «a();
12831
12832 // c();ˇ»
12833 }
12834 "});
12835
12836 // If a selection includes multiple comment prefixes, all lines are uncommented.
12837 cx.set_state(indoc! {"
12838 fn a() {
12839 // «a();
12840 /// b();
12841 //! c();ˇ»
12842 }
12843 "});
12844
12845 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12846
12847 cx.assert_editor_state(indoc! {"
12848 fn a() {
12849 «a();
12850 b();
12851 c();ˇ»
12852 }
12853 "});
12854}
12855
12856#[gpui::test]
12857async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
12858 init_test(cx, |_| {});
12859
12860 let language = Arc::new(Language::new(
12861 LanguageConfig {
12862 line_comments: vec!["// ".into()],
12863 ..Default::default()
12864 },
12865 Some(tree_sitter_rust::LANGUAGE.into()),
12866 ));
12867
12868 let mut cx = EditorTestContext::new(cx).await;
12869
12870 cx.language_registry().add(language.clone());
12871 cx.update_buffer(|buffer, cx| {
12872 buffer.set_language(Some(language), cx);
12873 });
12874
12875 let toggle_comments = &ToggleComments {
12876 advance_downwards: true,
12877 ignore_indent: false,
12878 };
12879
12880 // Single cursor on one line -> advance
12881 // Cursor moves horizontally 3 characters as well on non-blank line
12882 cx.set_state(indoc!(
12883 "fn a() {
12884 ˇdog();
12885 cat();
12886 }"
12887 ));
12888 cx.update_editor(|editor, window, cx| {
12889 editor.toggle_comments(toggle_comments, window, cx);
12890 });
12891 cx.assert_editor_state(indoc!(
12892 "fn a() {
12893 // dog();
12894 catˇ();
12895 }"
12896 ));
12897
12898 // Single selection on one line -> don't advance
12899 cx.set_state(indoc!(
12900 "fn a() {
12901 «dog()ˇ»;
12902 cat();
12903 }"
12904 ));
12905 cx.update_editor(|editor, window, cx| {
12906 editor.toggle_comments(toggle_comments, window, cx);
12907 });
12908 cx.assert_editor_state(indoc!(
12909 "fn a() {
12910 // «dog()ˇ»;
12911 cat();
12912 }"
12913 ));
12914
12915 // Multiple cursors on one line -> advance
12916 cx.set_state(indoc!(
12917 "fn a() {
12918 ˇdˇog();
12919 cat();
12920 }"
12921 ));
12922 cx.update_editor(|editor, window, cx| {
12923 editor.toggle_comments(toggle_comments, window, cx);
12924 });
12925 cx.assert_editor_state(indoc!(
12926 "fn a() {
12927 // dog();
12928 catˇ(ˇ);
12929 }"
12930 ));
12931
12932 // Multiple cursors on one line, with selection -> don't advance
12933 cx.set_state(indoc!(
12934 "fn a() {
12935 ˇdˇog«()ˇ»;
12936 cat();
12937 }"
12938 ));
12939 cx.update_editor(|editor, window, cx| {
12940 editor.toggle_comments(toggle_comments, window, cx);
12941 });
12942 cx.assert_editor_state(indoc!(
12943 "fn a() {
12944 // ˇdˇog«()ˇ»;
12945 cat();
12946 }"
12947 ));
12948
12949 // Single cursor on one line -> advance
12950 // Cursor moves to column 0 on blank line
12951 cx.set_state(indoc!(
12952 "fn a() {
12953 ˇdog();
12954
12955 cat();
12956 }"
12957 ));
12958 cx.update_editor(|editor, window, cx| {
12959 editor.toggle_comments(toggle_comments, window, cx);
12960 });
12961 cx.assert_editor_state(indoc!(
12962 "fn a() {
12963 // dog();
12964 ˇ
12965 cat();
12966 }"
12967 ));
12968
12969 // Single cursor on one line -> advance
12970 // Cursor starts and ends at column 0
12971 cx.set_state(indoc!(
12972 "fn a() {
12973 ˇ dog();
12974 cat();
12975 }"
12976 ));
12977 cx.update_editor(|editor, window, cx| {
12978 editor.toggle_comments(toggle_comments, window, cx);
12979 });
12980 cx.assert_editor_state(indoc!(
12981 "fn a() {
12982 // dog();
12983 ˇ cat();
12984 }"
12985 ));
12986}
12987
12988#[gpui::test]
12989async fn test_toggle_block_comment(cx: &mut TestAppContext) {
12990 init_test(cx, |_| {});
12991
12992 let mut cx = EditorTestContext::new(cx).await;
12993
12994 let html_language = Arc::new(
12995 Language::new(
12996 LanguageConfig {
12997 name: "HTML".into(),
12998 block_comment: Some(("<!-- ".into(), " -->".into())),
12999 ..Default::default()
13000 },
13001 Some(tree_sitter_html::LANGUAGE.into()),
13002 )
13003 .with_injection_query(
13004 r#"
13005 (script_element
13006 (raw_text) @injection.content
13007 (#set! injection.language "javascript"))
13008 "#,
13009 )
13010 .unwrap(),
13011 );
13012
13013 let javascript_language = Arc::new(Language::new(
13014 LanguageConfig {
13015 name: "JavaScript".into(),
13016 line_comments: vec!["// ".into()],
13017 ..Default::default()
13018 },
13019 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
13020 ));
13021
13022 cx.language_registry().add(html_language.clone());
13023 cx.language_registry().add(javascript_language.clone());
13024 cx.update_buffer(|buffer, cx| {
13025 buffer.set_language(Some(html_language), cx);
13026 });
13027
13028 // Toggle comments for empty selections
13029 cx.set_state(
13030 &r#"
13031 <p>A</p>ˇ
13032 <p>B</p>ˇ
13033 <p>C</p>ˇ
13034 "#
13035 .unindent(),
13036 );
13037 cx.update_editor(|editor, window, cx| {
13038 editor.toggle_comments(&ToggleComments::default(), window, cx)
13039 });
13040 cx.assert_editor_state(
13041 &r#"
13042 <!-- <p>A</p>ˇ -->
13043 <!-- <p>B</p>ˇ -->
13044 <!-- <p>C</p>ˇ -->
13045 "#
13046 .unindent(),
13047 );
13048 cx.update_editor(|editor, window, cx| {
13049 editor.toggle_comments(&ToggleComments::default(), window, cx)
13050 });
13051 cx.assert_editor_state(
13052 &r#"
13053 <p>A</p>ˇ
13054 <p>B</p>ˇ
13055 <p>C</p>ˇ
13056 "#
13057 .unindent(),
13058 );
13059
13060 // Toggle comments for mixture of empty and non-empty selections, where
13061 // multiple selections occupy a given line.
13062 cx.set_state(
13063 &r#"
13064 <p>A«</p>
13065 <p>ˇ»B</p>ˇ
13066 <p>C«</p>
13067 <p>ˇ»D</p>ˇ
13068 "#
13069 .unindent(),
13070 );
13071
13072 cx.update_editor(|editor, window, cx| {
13073 editor.toggle_comments(&ToggleComments::default(), window, cx)
13074 });
13075 cx.assert_editor_state(
13076 &r#"
13077 <!-- <p>A«</p>
13078 <p>ˇ»B</p>ˇ -->
13079 <!-- <p>C«</p>
13080 <p>ˇ»D</p>ˇ -->
13081 "#
13082 .unindent(),
13083 );
13084 cx.update_editor(|editor, window, cx| {
13085 editor.toggle_comments(&ToggleComments::default(), window, cx)
13086 });
13087 cx.assert_editor_state(
13088 &r#"
13089 <p>A«</p>
13090 <p>ˇ»B</p>ˇ
13091 <p>C«</p>
13092 <p>ˇ»D</p>ˇ
13093 "#
13094 .unindent(),
13095 );
13096
13097 // Toggle comments when different languages are active for different
13098 // selections.
13099 cx.set_state(
13100 &r#"
13101 ˇ<script>
13102 ˇvar x = new Y();
13103 ˇ</script>
13104 "#
13105 .unindent(),
13106 );
13107 cx.executor().run_until_parked();
13108 cx.update_editor(|editor, window, cx| {
13109 editor.toggle_comments(&ToggleComments::default(), window, cx)
13110 });
13111 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
13112 // Uncommenting and commenting from this position brings in even more wrong artifacts.
13113 cx.assert_editor_state(
13114 &r#"
13115 <!-- ˇ<script> -->
13116 // ˇvar x = new Y();
13117 <!-- ˇ</script> -->
13118 "#
13119 .unindent(),
13120 );
13121}
13122
13123#[gpui::test]
13124fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
13125 init_test(cx, |_| {});
13126
13127 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13128 let multibuffer = cx.new(|cx| {
13129 let mut multibuffer = MultiBuffer::new(ReadWrite);
13130 multibuffer.push_excerpts(
13131 buffer.clone(),
13132 [
13133 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
13134 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
13135 ],
13136 cx,
13137 );
13138 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
13139 multibuffer
13140 });
13141
13142 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
13143 editor.update_in(cx, |editor, window, cx| {
13144 assert_eq!(editor.text(cx), "aaaa\nbbbb");
13145 editor.change_selections(None, window, cx, |s| {
13146 s.select_ranges([
13147 Point::new(0, 0)..Point::new(0, 0),
13148 Point::new(1, 0)..Point::new(1, 0),
13149 ])
13150 });
13151
13152 editor.handle_input("X", window, cx);
13153 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
13154 assert_eq!(
13155 editor.selections.ranges(cx),
13156 [
13157 Point::new(0, 1)..Point::new(0, 1),
13158 Point::new(1, 1)..Point::new(1, 1),
13159 ]
13160 );
13161
13162 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
13163 editor.change_selections(None, window, cx, |s| {
13164 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
13165 });
13166 editor.backspace(&Default::default(), window, cx);
13167 assert_eq!(editor.text(cx), "Xa\nbbb");
13168 assert_eq!(
13169 editor.selections.ranges(cx),
13170 [Point::new(1, 0)..Point::new(1, 0)]
13171 );
13172
13173 editor.change_selections(None, window, cx, |s| {
13174 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
13175 });
13176 editor.backspace(&Default::default(), window, cx);
13177 assert_eq!(editor.text(cx), "X\nbb");
13178 assert_eq!(
13179 editor.selections.ranges(cx),
13180 [Point::new(0, 1)..Point::new(0, 1)]
13181 );
13182 });
13183}
13184
13185#[gpui::test]
13186fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
13187 init_test(cx, |_| {});
13188
13189 let markers = vec![('[', ']').into(), ('(', ')').into()];
13190 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
13191 indoc! {"
13192 [aaaa
13193 (bbbb]
13194 cccc)",
13195 },
13196 markers.clone(),
13197 );
13198 let excerpt_ranges = markers.into_iter().map(|marker| {
13199 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
13200 ExcerptRange::new(context.clone())
13201 });
13202 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
13203 let multibuffer = cx.new(|cx| {
13204 let mut multibuffer = MultiBuffer::new(ReadWrite);
13205 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
13206 multibuffer
13207 });
13208
13209 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
13210 editor.update_in(cx, |editor, window, cx| {
13211 let (expected_text, selection_ranges) = marked_text_ranges(
13212 indoc! {"
13213 aaaa
13214 bˇbbb
13215 bˇbbˇb
13216 cccc"
13217 },
13218 true,
13219 );
13220 assert_eq!(editor.text(cx), expected_text);
13221 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
13222
13223 editor.handle_input("X", window, cx);
13224
13225 let (expected_text, expected_selections) = marked_text_ranges(
13226 indoc! {"
13227 aaaa
13228 bXˇbbXb
13229 bXˇbbXˇb
13230 cccc"
13231 },
13232 false,
13233 );
13234 assert_eq!(editor.text(cx), expected_text);
13235 assert_eq!(editor.selections.ranges(cx), expected_selections);
13236
13237 editor.newline(&Newline, window, cx);
13238 let (expected_text, expected_selections) = marked_text_ranges(
13239 indoc! {"
13240 aaaa
13241 bX
13242 ˇbbX
13243 b
13244 bX
13245 ˇbbX
13246 ˇb
13247 cccc"
13248 },
13249 false,
13250 );
13251 assert_eq!(editor.text(cx), expected_text);
13252 assert_eq!(editor.selections.ranges(cx), expected_selections);
13253 });
13254}
13255
13256#[gpui::test]
13257fn test_refresh_selections(cx: &mut TestAppContext) {
13258 init_test(cx, |_| {});
13259
13260 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13261 let mut excerpt1_id = None;
13262 let multibuffer = cx.new(|cx| {
13263 let mut multibuffer = MultiBuffer::new(ReadWrite);
13264 excerpt1_id = multibuffer
13265 .push_excerpts(
13266 buffer.clone(),
13267 [
13268 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
13269 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
13270 ],
13271 cx,
13272 )
13273 .into_iter()
13274 .next();
13275 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
13276 multibuffer
13277 });
13278
13279 let editor = cx.add_window(|window, cx| {
13280 let mut editor = build_editor(multibuffer.clone(), window, cx);
13281 let snapshot = editor.snapshot(window, cx);
13282 editor.change_selections(None, window, cx, |s| {
13283 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
13284 });
13285 editor.begin_selection(
13286 Point::new(2, 1).to_display_point(&snapshot),
13287 true,
13288 1,
13289 window,
13290 cx,
13291 );
13292 assert_eq!(
13293 editor.selections.ranges(cx),
13294 [
13295 Point::new(1, 3)..Point::new(1, 3),
13296 Point::new(2, 1)..Point::new(2, 1),
13297 ]
13298 );
13299 editor
13300 });
13301
13302 // Refreshing selections is a no-op when excerpts haven't changed.
13303 _ = editor.update(cx, |editor, window, cx| {
13304 editor.change_selections(None, window, cx, |s| s.refresh());
13305 assert_eq!(
13306 editor.selections.ranges(cx),
13307 [
13308 Point::new(1, 3)..Point::new(1, 3),
13309 Point::new(2, 1)..Point::new(2, 1),
13310 ]
13311 );
13312 });
13313
13314 multibuffer.update(cx, |multibuffer, cx| {
13315 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
13316 });
13317 _ = editor.update(cx, |editor, window, cx| {
13318 // Removing an excerpt causes the first selection to become degenerate.
13319 assert_eq!(
13320 editor.selections.ranges(cx),
13321 [
13322 Point::new(0, 0)..Point::new(0, 0),
13323 Point::new(0, 1)..Point::new(0, 1)
13324 ]
13325 );
13326
13327 // Refreshing selections will relocate the first selection to the original buffer
13328 // location.
13329 editor.change_selections(None, window, cx, |s| s.refresh());
13330 assert_eq!(
13331 editor.selections.ranges(cx),
13332 [
13333 Point::new(0, 1)..Point::new(0, 1),
13334 Point::new(0, 3)..Point::new(0, 3)
13335 ]
13336 );
13337 assert!(editor.selections.pending_anchor().is_some());
13338 });
13339}
13340
13341#[gpui::test]
13342fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
13343 init_test(cx, |_| {});
13344
13345 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13346 let mut excerpt1_id = None;
13347 let multibuffer = cx.new(|cx| {
13348 let mut multibuffer = MultiBuffer::new(ReadWrite);
13349 excerpt1_id = multibuffer
13350 .push_excerpts(
13351 buffer.clone(),
13352 [
13353 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
13354 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
13355 ],
13356 cx,
13357 )
13358 .into_iter()
13359 .next();
13360 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
13361 multibuffer
13362 });
13363
13364 let editor = cx.add_window(|window, cx| {
13365 let mut editor = build_editor(multibuffer.clone(), window, cx);
13366 let snapshot = editor.snapshot(window, cx);
13367 editor.begin_selection(
13368 Point::new(1, 3).to_display_point(&snapshot),
13369 false,
13370 1,
13371 window,
13372 cx,
13373 );
13374 assert_eq!(
13375 editor.selections.ranges(cx),
13376 [Point::new(1, 3)..Point::new(1, 3)]
13377 );
13378 editor
13379 });
13380
13381 multibuffer.update(cx, |multibuffer, cx| {
13382 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
13383 });
13384 _ = editor.update(cx, |editor, window, cx| {
13385 assert_eq!(
13386 editor.selections.ranges(cx),
13387 [Point::new(0, 0)..Point::new(0, 0)]
13388 );
13389
13390 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
13391 editor.change_selections(None, window, cx, |s| s.refresh());
13392 assert_eq!(
13393 editor.selections.ranges(cx),
13394 [Point::new(0, 3)..Point::new(0, 3)]
13395 );
13396 assert!(editor.selections.pending_anchor().is_some());
13397 });
13398}
13399
13400#[gpui::test]
13401async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
13402 init_test(cx, |_| {});
13403
13404 let language = Arc::new(
13405 Language::new(
13406 LanguageConfig {
13407 brackets: BracketPairConfig {
13408 pairs: vec![
13409 BracketPair {
13410 start: "{".to_string(),
13411 end: "}".to_string(),
13412 close: true,
13413 surround: true,
13414 newline: true,
13415 },
13416 BracketPair {
13417 start: "/* ".to_string(),
13418 end: " */".to_string(),
13419 close: true,
13420 surround: true,
13421 newline: true,
13422 },
13423 ],
13424 ..Default::default()
13425 },
13426 ..Default::default()
13427 },
13428 Some(tree_sitter_rust::LANGUAGE.into()),
13429 )
13430 .with_indents_query("")
13431 .unwrap(),
13432 );
13433
13434 let text = concat!(
13435 "{ }\n", //
13436 " x\n", //
13437 " /* */\n", //
13438 "x\n", //
13439 "{{} }\n", //
13440 );
13441
13442 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
13443 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13444 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
13445 editor
13446 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
13447 .await;
13448
13449 editor.update_in(cx, |editor, window, cx| {
13450 editor.change_selections(None, window, cx, |s| {
13451 s.select_display_ranges([
13452 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
13453 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
13454 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
13455 ])
13456 });
13457 editor.newline(&Newline, window, cx);
13458
13459 assert_eq!(
13460 editor.buffer().read(cx).read(cx).text(),
13461 concat!(
13462 "{ \n", // Suppress rustfmt
13463 "\n", //
13464 "}\n", //
13465 " x\n", //
13466 " /* \n", //
13467 " \n", //
13468 " */\n", //
13469 "x\n", //
13470 "{{} \n", //
13471 "}\n", //
13472 )
13473 );
13474 });
13475}
13476
13477#[gpui::test]
13478fn test_highlighted_ranges(cx: &mut TestAppContext) {
13479 init_test(cx, |_| {});
13480
13481 let editor = cx.add_window(|window, cx| {
13482 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
13483 build_editor(buffer.clone(), window, cx)
13484 });
13485
13486 _ = editor.update(cx, |editor, window, cx| {
13487 struct Type1;
13488 struct Type2;
13489
13490 let buffer = editor.buffer.read(cx).snapshot(cx);
13491
13492 let anchor_range =
13493 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
13494
13495 editor.highlight_background::<Type1>(
13496 &[
13497 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
13498 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
13499 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
13500 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
13501 ],
13502 |_| Hsla::red(),
13503 cx,
13504 );
13505 editor.highlight_background::<Type2>(
13506 &[
13507 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
13508 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
13509 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
13510 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
13511 ],
13512 |_| Hsla::green(),
13513 cx,
13514 );
13515
13516 let snapshot = editor.snapshot(window, cx);
13517 let mut highlighted_ranges = editor.background_highlights_in_range(
13518 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
13519 &snapshot,
13520 cx.theme().colors(),
13521 );
13522 // Enforce a consistent ordering based on color without relying on the ordering of the
13523 // highlight's `TypeId` which is non-executor.
13524 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
13525 assert_eq!(
13526 highlighted_ranges,
13527 &[
13528 (
13529 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
13530 Hsla::red(),
13531 ),
13532 (
13533 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
13534 Hsla::red(),
13535 ),
13536 (
13537 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
13538 Hsla::green(),
13539 ),
13540 (
13541 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
13542 Hsla::green(),
13543 ),
13544 ]
13545 );
13546 assert_eq!(
13547 editor.background_highlights_in_range(
13548 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
13549 &snapshot,
13550 cx.theme().colors(),
13551 ),
13552 &[(
13553 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
13554 Hsla::red(),
13555 )]
13556 );
13557 });
13558}
13559
13560#[gpui::test]
13561async fn test_following(cx: &mut TestAppContext) {
13562 init_test(cx, |_| {});
13563
13564 let fs = FakeFs::new(cx.executor());
13565 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
13566
13567 let buffer = project.update(cx, |project, cx| {
13568 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
13569 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
13570 });
13571 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
13572 let follower = cx.update(|cx| {
13573 cx.open_window(
13574 WindowOptions {
13575 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
13576 gpui::Point::new(px(0.), px(0.)),
13577 gpui::Point::new(px(10.), px(80.)),
13578 ))),
13579 ..Default::default()
13580 },
13581 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
13582 )
13583 .unwrap()
13584 });
13585
13586 let is_still_following = Rc::new(RefCell::new(true));
13587 let follower_edit_event_count = Rc::new(RefCell::new(0));
13588 let pending_update = Rc::new(RefCell::new(None));
13589 let leader_entity = leader.root(cx).unwrap();
13590 let follower_entity = follower.root(cx).unwrap();
13591 _ = follower.update(cx, {
13592 let update = pending_update.clone();
13593 let is_still_following = is_still_following.clone();
13594 let follower_edit_event_count = follower_edit_event_count.clone();
13595 |_, window, cx| {
13596 cx.subscribe_in(
13597 &leader_entity,
13598 window,
13599 move |_, leader, event, window, cx| {
13600 leader.read(cx).add_event_to_update_proto(
13601 event,
13602 &mut update.borrow_mut(),
13603 window,
13604 cx,
13605 );
13606 },
13607 )
13608 .detach();
13609
13610 cx.subscribe_in(
13611 &follower_entity,
13612 window,
13613 move |_, _, event: &EditorEvent, _window, _cx| {
13614 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
13615 *is_still_following.borrow_mut() = false;
13616 }
13617
13618 if let EditorEvent::BufferEdited = event {
13619 *follower_edit_event_count.borrow_mut() += 1;
13620 }
13621 },
13622 )
13623 .detach();
13624 }
13625 });
13626
13627 // Update the selections only
13628 _ = leader.update(cx, |leader, window, cx| {
13629 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
13630 });
13631 follower
13632 .update(cx, |follower, window, cx| {
13633 follower.apply_update_proto(
13634 &project,
13635 pending_update.borrow_mut().take().unwrap(),
13636 window,
13637 cx,
13638 )
13639 })
13640 .unwrap()
13641 .await
13642 .unwrap();
13643 _ = follower.update(cx, |follower, _, cx| {
13644 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
13645 });
13646 assert!(*is_still_following.borrow());
13647 assert_eq!(*follower_edit_event_count.borrow(), 0);
13648
13649 // Update the scroll position only
13650 _ = leader.update(cx, |leader, window, cx| {
13651 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
13652 });
13653 follower
13654 .update(cx, |follower, window, cx| {
13655 follower.apply_update_proto(
13656 &project,
13657 pending_update.borrow_mut().take().unwrap(),
13658 window,
13659 cx,
13660 )
13661 })
13662 .unwrap()
13663 .await
13664 .unwrap();
13665 assert_eq!(
13666 follower
13667 .update(cx, |follower, _, cx| follower.scroll_position(cx))
13668 .unwrap(),
13669 gpui::Point::new(1.5, 3.5)
13670 );
13671 assert!(*is_still_following.borrow());
13672 assert_eq!(*follower_edit_event_count.borrow(), 0);
13673
13674 // Update the selections and scroll position. The follower's scroll position is updated
13675 // via autoscroll, not via the leader's exact scroll position.
13676 _ = leader.update(cx, |leader, window, cx| {
13677 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
13678 leader.request_autoscroll(Autoscroll::newest(), cx);
13679 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
13680 });
13681 follower
13682 .update(cx, |follower, window, cx| {
13683 follower.apply_update_proto(
13684 &project,
13685 pending_update.borrow_mut().take().unwrap(),
13686 window,
13687 cx,
13688 )
13689 })
13690 .unwrap()
13691 .await
13692 .unwrap();
13693 _ = follower.update(cx, |follower, _, cx| {
13694 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
13695 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
13696 });
13697 assert!(*is_still_following.borrow());
13698
13699 // Creating a pending selection that precedes another selection
13700 _ = leader.update(cx, |leader, window, cx| {
13701 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
13702 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
13703 });
13704 follower
13705 .update(cx, |follower, window, cx| {
13706 follower.apply_update_proto(
13707 &project,
13708 pending_update.borrow_mut().take().unwrap(),
13709 window,
13710 cx,
13711 )
13712 })
13713 .unwrap()
13714 .await
13715 .unwrap();
13716 _ = follower.update(cx, |follower, _, cx| {
13717 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
13718 });
13719 assert!(*is_still_following.borrow());
13720
13721 // Extend the pending selection so that it surrounds another selection
13722 _ = leader.update(cx, |leader, window, cx| {
13723 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
13724 });
13725 follower
13726 .update(cx, |follower, window, cx| {
13727 follower.apply_update_proto(
13728 &project,
13729 pending_update.borrow_mut().take().unwrap(),
13730 window,
13731 cx,
13732 )
13733 })
13734 .unwrap()
13735 .await
13736 .unwrap();
13737 _ = follower.update(cx, |follower, _, cx| {
13738 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
13739 });
13740
13741 // Scrolling locally breaks the follow
13742 _ = follower.update(cx, |follower, window, cx| {
13743 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
13744 follower.set_scroll_anchor(
13745 ScrollAnchor {
13746 anchor: top_anchor,
13747 offset: gpui::Point::new(0.0, 0.5),
13748 },
13749 window,
13750 cx,
13751 );
13752 });
13753 assert!(!(*is_still_following.borrow()));
13754}
13755
13756#[gpui::test]
13757async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
13758 init_test(cx, |_| {});
13759
13760 let fs = FakeFs::new(cx.executor());
13761 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
13762 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13763 let pane = workspace
13764 .update(cx, |workspace, _, _| workspace.active_pane().clone())
13765 .unwrap();
13766
13767 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
13768
13769 let leader = pane.update_in(cx, |_, window, cx| {
13770 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
13771 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
13772 });
13773
13774 // Start following the editor when it has no excerpts.
13775 let mut state_message =
13776 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
13777 let workspace_entity = workspace.root(cx).unwrap();
13778 let follower_1 = cx
13779 .update_window(*workspace.deref(), |_, window, cx| {
13780 Editor::from_state_proto(
13781 workspace_entity,
13782 ViewId {
13783 creator: CollaboratorId::PeerId(PeerId::default()),
13784 id: 0,
13785 },
13786 &mut state_message,
13787 window,
13788 cx,
13789 )
13790 })
13791 .unwrap()
13792 .unwrap()
13793 .await
13794 .unwrap();
13795
13796 let update_message = Rc::new(RefCell::new(None));
13797 follower_1.update_in(cx, {
13798 let update = update_message.clone();
13799 |_, window, cx| {
13800 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
13801 leader.read(cx).add_event_to_update_proto(
13802 event,
13803 &mut update.borrow_mut(),
13804 window,
13805 cx,
13806 );
13807 })
13808 .detach();
13809 }
13810 });
13811
13812 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
13813 (
13814 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
13815 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
13816 )
13817 });
13818
13819 // Insert some excerpts.
13820 leader.update(cx, |leader, cx| {
13821 leader.buffer.update(cx, |multibuffer, cx| {
13822 multibuffer.set_excerpts_for_path(
13823 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
13824 buffer_1.clone(),
13825 vec![
13826 Point::row_range(0..3),
13827 Point::row_range(1..6),
13828 Point::row_range(12..15),
13829 ],
13830 0,
13831 cx,
13832 );
13833 multibuffer.set_excerpts_for_path(
13834 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
13835 buffer_2.clone(),
13836 vec![Point::row_range(0..6), Point::row_range(8..12)],
13837 0,
13838 cx,
13839 );
13840 });
13841 });
13842
13843 // Apply the update of adding the excerpts.
13844 follower_1
13845 .update_in(cx, |follower, window, cx| {
13846 follower.apply_update_proto(
13847 &project,
13848 update_message.borrow().clone().unwrap(),
13849 window,
13850 cx,
13851 )
13852 })
13853 .await
13854 .unwrap();
13855 assert_eq!(
13856 follower_1.update(cx, |editor, cx| editor.text(cx)),
13857 leader.update(cx, |editor, cx| editor.text(cx))
13858 );
13859 update_message.borrow_mut().take();
13860
13861 // Start following separately after it already has excerpts.
13862 let mut state_message =
13863 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
13864 let workspace_entity = workspace.root(cx).unwrap();
13865 let follower_2 = cx
13866 .update_window(*workspace.deref(), |_, window, cx| {
13867 Editor::from_state_proto(
13868 workspace_entity,
13869 ViewId {
13870 creator: CollaboratorId::PeerId(PeerId::default()),
13871 id: 0,
13872 },
13873 &mut state_message,
13874 window,
13875 cx,
13876 )
13877 })
13878 .unwrap()
13879 .unwrap()
13880 .await
13881 .unwrap();
13882 assert_eq!(
13883 follower_2.update(cx, |editor, cx| editor.text(cx)),
13884 leader.update(cx, |editor, cx| editor.text(cx))
13885 );
13886
13887 // Remove some excerpts.
13888 leader.update(cx, |leader, cx| {
13889 leader.buffer.update(cx, |multibuffer, cx| {
13890 let excerpt_ids = multibuffer.excerpt_ids();
13891 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
13892 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
13893 });
13894 });
13895
13896 // Apply the update of removing the excerpts.
13897 follower_1
13898 .update_in(cx, |follower, window, cx| {
13899 follower.apply_update_proto(
13900 &project,
13901 update_message.borrow().clone().unwrap(),
13902 window,
13903 cx,
13904 )
13905 })
13906 .await
13907 .unwrap();
13908 follower_2
13909 .update_in(cx, |follower, window, cx| {
13910 follower.apply_update_proto(
13911 &project,
13912 update_message.borrow().clone().unwrap(),
13913 window,
13914 cx,
13915 )
13916 })
13917 .await
13918 .unwrap();
13919 update_message.borrow_mut().take();
13920 assert_eq!(
13921 follower_1.update(cx, |editor, cx| editor.text(cx)),
13922 leader.update(cx, |editor, cx| editor.text(cx))
13923 );
13924}
13925
13926#[gpui::test]
13927async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13928 init_test(cx, |_| {});
13929
13930 let mut cx = EditorTestContext::new(cx).await;
13931 let lsp_store =
13932 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
13933
13934 cx.set_state(indoc! {"
13935 ˇfn func(abc def: i32) -> u32 {
13936 }
13937 "});
13938
13939 cx.update(|_, cx| {
13940 lsp_store.update(cx, |lsp_store, cx| {
13941 lsp_store
13942 .update_diagnostics(
13943 LanguageServerId(0),
13944 lsp::PublishDiagnosticsParams {
13945 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
13946 version: None,
13947 diagnostics: vec![
13948 lsp::Diagnostic {
13949 range: lsp::Range::new(
13950 lsp::Position::new(0, 11),
13951 lsp::Position::new(0, 12),
13952 ),
13953 severity: Some(lsp::DiagnosticSeverity::ERROR),
13954 ..Default::default()
13955 },
13956 lsp::Diagnostic {
13957 range: lsp::Range::new(
13958 lsp::Position::new(0, 12),
13959 lsp::Position::new(0, 15),
13960 ),
13961 severity: Some(lsp::DiagnosticSeverity::ERROR),
13962 ..Default::default()
13963 },
13964 lsp::Diagnostic {
13965 range: lsp::Range::new(
13966 lsp::Position::new(0, 25),
13967 lsp::Position::new(0, 28),
13968 ),
13969 severity: Some(lsp::DiagnosticSeverity::ERROR),
13970 ..Default::default()
13971 },
13972 ],
13973 },
13974 None,
13975 DiagnosticSourceKind::Pushed,
13976 &[],
13977 cx,
13978 )
13979 .unwrap()
13980 });
13981 });
13982
13983 executor.run_until_parked();
13984
13985 cx.update_editor(|editor, window, cx| {
13986 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13987 });
13988
13989 cx.assert_editor_state(indoc! {"
13990 fn func(abc def: i32) -> ˇu32 {
13991 }
13992 "});
13993
13994 cx.update_editor(|editor, window, cx| {
13995 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13996 });
13997
13998 cx.assert_editor_state(indoc! {"
13999 fn func(abc ˇdef: i32) -> u32 {
14000 }
14001 "});
14002
14003 cx.update_editor(|editor, window, cx| {
14004 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14005 });
14006
14007 cx.assert_editor_state(indoc! {"
14008 fn func(abcˇ def: i32) -> u32 {
14009 }
14010 "});
14011
14012 cx.update_editor(|editor, window, cx| {
14013 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14014 });
14015
14016 cx.assert_editor_state(indoc! {"
14017 fn func(abc def: i32) -> ˇu32 {
14018 }
14019 "});
14020}
14021
14022#[gpui::test]
14023async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14024 init_test(cx, |_| {});
14025
14026 let mut cx = EditorTestContext::new(cx).await;
14027
14028 let diff_base = r#"
14029 use some::mod;
14030
14031 const A: u32 = 42;
14032
14033 fn main() {
14034 println!("hello");
14035
14036 println!("world");
14037 }
14038 "#
14039 .unindent();
14040
14041 // Edits are modified, removed, modified, added
14042 cx.set_state(
14043 &r#"
14044 use some::modified;
14045
14046 ˇ
14047 fn main() {
14048 println!("hello there");
14049
14050 println!("around the");
14051 println!("world");
14052 }
14053 "#
14054 .unindent(),
14055 );
14056
14057 cx.set_head_text(&diff_base);
14058 executor.run_until_parked();
14059
14060 cx.update_editor(|editor, window, cx| {
14061 //Wrap around the bottom of the buffer
14062 for _ in 0..3 {
14063 editor.go_to_next_hunk(&GoToHunk, window, cx);
14064 }
14065 });
14066
14067 cx.assert_editor_state(
14068 &r#"
14069 ˇuse some::modified;
14070
14071
14072 fn main() {
14073 println!("hello there");
14074
14075 println!("around the");
14076 println!("world");
14077 }
14078 "#
14079 .unindent(),
14080 );
14081
14082 cx.update_editor(|editor, window, cx| {
14083 //Wrap around the top of the buffer
14084 for _ in 0..2 {
14085 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14086 }
14087 });
14088
14089 cx.assert_editor_state(
14090 &r#"
14091 use some::modified;
14092
14093
14094 fn main() {
14095 ˇ println!("hello there");
14096
14097 println!("around the");
14098 println!("world");
14099 }
14100 "#
14101 .unindent(),
14102 );
14103
14104 cx.update_editor(|editor, window, cx| {
14105 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14106 });
14107
14108 cx.assert_editor_state(
14109 &r#"
14110 use some::modified;
14111
14112 ˇ
14113 fn main() {
14114 println!("hello there");
14115
14116 println!("around the");
14117 println!("world");
14118 }
14119 "#
14120 .unindent(),
14121 );
14122
14123 cx.update_editor(|editor, window, cx| {
14124 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14125 });
14126
14127 cx.assert_editor_state(
14128 &r#"
14129 ˇuse some::modified;
14130
14131
14132 fn main() {
14133 println!("hello there");
14134
14135 println!("around the");
14136 println!("world");
14137 }
14138 "#
14139 .unindent(),
14140 );
14141
14142 cx.update_editor(|editor, window, cx| {
14143 for _ in 0..2 {
14144 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14145 }
14146 });
14147
14148 cx.assert_editor_state(
14149 &r#"
14150 use some::modified;
14151
14152
14153 fn main() {
14154 ˇ println!("hello there");
14155
14156 println!("around the");
14157 println!("world");
14158 }
14159 "#
14160 .unindent(),
14161 );
14162
14163 cx.update_editor(|editor, window, cx| {
14164 editor.fold(&Fold, window, cx);
14165 });
14166
14167 cx.update_editor(|editor, window, cx| {
14168 editor.go_to_next_hunk(&GoToHunk, window, cx);
14169 });
14170
14171 cx.assert_editor_state(
14172 &r#"
14173 ˇuse some::modified;
14174
14175
14176 fn main() {
14177 println!("hello there");
14178
14179 println!("around the");
14180 println!("world");
14181 }
14182 "#
14183 .unindent(),
14184 );
14185}
14186
14187#[test]
14188fn test_split_words() {
14189 fn split(text: &str) -> Vec<&str> {
14190 split_words(text).collect()
14191 }
14192
14193 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
14194 assert_eq!(split("hello_world"), &["hello_", "world"]);
14195 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
14196 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
14197 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
14198 assert_eq!(split("helloworld"), &["helloworld"]);
14199
14200 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
14201}
14202
14203#[gpui::test]
14204async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
14205 init_test(cx, |_| {});
14206
14207 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
14208 let mut assert = |before, after| {
14209 let _state_context = cx.set_state(before);
14210 cx.run_until_parked();
14211 cx.update_editor(|editor, window, cx| {
14212 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
14213 });
14214 cx.run_until_parked();
14215 cx.assert_editor_state(after);
14216 };
14217
14218 // Outside bracket jumps to outside of matching bracket
14219 assert("console.logˇ(var);", "console.log(var)ˇ;");
14220 assert("console.log(var)ˇ;", "console.logˇ(var);");
14221
14222 // Inside bracket jumps to inside of matching bracket
14223 assert("console.log(ˇvar);", "console.log(varˇ);");
14224 assert("console.log(varˇ);", "console.log(ˇvar);");
14225
14226 // When outside a bracket and inside, favor jumping to the inside bracket
14227 assert(
14228 "console.log('foo', [1, 2, 3]ˇ);",
14229 "console.log(ˇ'foo', [1, 2, 3]);",
14230 );
14231 assert(
14232 "console.log(ˇ'foo', [1, 2, 3]);",
14233 "console.log('foo', [1, 2, 3]ˇ);",
14234 );
14235
14236 // Bias forward if two options are equally likely
14237 assert(
14238 "let result = curried_fun()ˇ();",
14239 "let result = curried_fun()()ˇ;",
14240 );
14241
14242 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
14243 assert(
14244 indoc! {"
14245 function test() {
14246 console.log('test')ˇ
14247 }"},
14248 indoc! {"
14249 function test() {
14250 console.logˇ('test')
14251 }"},
14252 );
14253}
14254
14255#[gpui::test]
14256async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
14257 init_test(cx, |_| {});
14258
14259 let fs = FakeFs::new(cx.executor());
14260 fs.insert_tree(
14261 path!("/a"),
14262 json!({
14263 "main.rs": "fn main() { let a = 5; }",
14264 "other.rs": "// Test file",
14265 }),
14266 )
14267 .await;
14268 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14269
14270 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14271 language_registry.add(Arc::new(Language::new(
14272 LanguageConfig {
14273 name: "Rust".into(),
14274 matcher: LanguageMatcher {
14275 path_suffixes: vec!["rs".to_string()],
14276 ..Default::default()
14277 },
14278 brackets: BracketPairConfig {
14279 pairs: vec![BracketPair {
14280 start: "{".to_string(),
14281 end: "}".to_string(),
14282 close: true,
14283 surround: true,
14284 newline: true,
14285 }],
14286 disabled_scopes_by_bracket_ix: Vec::new(),
14287 },
14288 ..Default::default()
14289 },
14290 Some(tree_sitter_rust::LANGUAGE.into()),
14291 )));
14292 let mut fake_servers = language_registry.register_fake_lsp(
14293 "Rust",
14294 FakeLspAdapter {
14295 capabilities: lsp::ServerCapabilities {
14296 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
14297 first_trigger_character: "{".to_string(),
14298 more_trigger_character: None,
14299 }),
14300 ..Default::default()
14301 },
14302 ..Default::default()
14303 },
14304 );
14305
14306 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14307
14308 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14309
14310 let worktree_id = workspace
14311 .update(cx, |workspace, _, cx| {
14312 workspace.project().update(cx, |project, cx| {
14313 project.worktrees(cx).next().unwrap().read(cx).id()
14314 })
14315 })
14316 .unwrap();
14317
14318 let buffer = project
14319 .update(cx, |project, cx| {
14320 project.open_local_buffer(path!("/a/main.rs"), cx)
14321 })
14322 .await
14323 .unwrap();
14324 let editor_handle = workspace
14325 .update(cx, |workspace, window, cx| {
14326 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
14327 })
14328 .unwrap()
14329 .await
14330 .unwrap()
14331 .downcast::<Editor>()
14332 .unwrap();
14333
14334 cx.executor().start_waiting();
14335 let fake_server = fake_servers.next().await.unwrap();
14336
14337 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
14338 |params, _| async move {
14339 assert_eq!(
14340 params.text_document_position.text_document.uri,
14341 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
14342 );
14343 assert_eq!(
14344 params.text_document_position.position,
14345 lsp::Position::new(0, 21),
14346 );
14347
14348 Ok(Some(vec![lsp::TextEdit {
14349 new_text: "]".to_string(),
14350 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14351 }]))
14352 },
14353 );
14354
14355 editor_handle.update_in(cx, |editor, window, cx| {
14356 window.focus(&editor.focus_handle(cx));
14357 editor.change_selections(None, window, cx, |s| {
14358 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
14359 });
14360 editor.handle_input("{", window, cx);
14361 });
14362
14363 cx.executor().run_until_parked();
14364
14365 buffer.update(cx, |buffer, _| {
14366 assert_eq!(
14367 buffer.text(),
14368 "fn main() { let a = {5}; }",
14369 "No extra braces from on type formatting should appear in the buffer"
14370 )
14371 });
14372}
14373
14374#[gpui::test]
14375async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
14376 init_test(cx, |_| {});
14377
14378 let fs = FakeFs::new(cx.executor());
14379 fs.insert_tree(
14380 path!("/a"),
14381 json!({
14382 "main.rs": "fn main() { let a = 5; }",
14383 "other.rs": "// Test file",
14384 }),
14385 )
14386 .await;
14387
14388 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14389
14390 let server_restarts = Arc::new(AtomicUsize::new(0));
14391 let closure_restarts = Arc::clone(&server_restarts);
14392 let language_server_name = "test language server";
14393 let language_name: LanguageName = "Rust".into();
14394
14395 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14396 language_registry.add(Arc::new(Language::new(
14397 LanguageConfig {
14398 name: language_name.clone(),
14399 matcher: LanguageMatcher {
14400 path_suffixes: vec!["rs".to_string()],
14401 ..Default::default()
14402 },
14403 ..Default::default()
14404 },
14405 Some(tree_sitter_rust::LANGUAGE.into()),
14406 )));
14407 let mut fake_servers = language_registry.register_fake_lsp(
14408 "Rust",
14409 FakeLspAdapter {
14410 name: language_server_name,
14411 initialization_options: Some(json!({
14412 "testOptionValue": true
14413 })),
14414 initializer: Some(Box::new(move |fake_server| {
14415 let task_restarts = Arc::clone(&closure_restarts);
14416 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
14417 task_restarts.fetch_add(1, atomic::Ordering::Release);
14418 futures::future::ready(Ok(()))
14419 });
14420 })),
14421 ..Default::default()
14422 },
14423 );
14424
14425 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14426 let _buffer = project
14427 .update(cx, |project, cx| {
14428 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
14429 })
14430 .await
14431 .unwrap();
14432 let _fake_server = fake_servers.next().await.unwrap();
14433 update_test_language_settings(cx, |language_settings| {
14434 language_settings.languages.insert(
14435 language_name.clone(),
14436 LanguageSettingsContent {
14437 tab_size: NonZeroU32::new(8),
14438 ..Default::default()
14439 },
14440 );
14441 });
14442 cx.executor().run_until_parked();
14443 assert_eq!(
14444 server_restarts.load(atomic::Ordering::Acquire),
14445 0,
14446 "Should not restart LSP server on an unrelated change"
14447 );
14448
14449 update_test_project_settings(cx, |project_settings| {
14450 project_settings.lsp.insert(
14451 "Some other server name".into(),
14452 LspSettings {
14453 binary: None,
14454 settings: None,
14455 initialization_options: Some(json!({
14456 "some other init value": false
14457 })),
14458 enable_lsp_tasks: false,
14459 },
14460 );
14461 });
14462 cx.executor().run_until_parked();
14463 assert_eq!(
14464 server_restarts.load(atomic::Ordering::Acquire),
14465 0,
14466 "Should not restart LSP server on an unrelated LSP settings change"
14467 );
14468
14469 update_test_project_settings(cx, |project_settings| {
14470 project_settings.lsp.insert(
14471 language_server_name.into(),
14472 LspSettings {
14473 binary: None,
14474 settings: None,
14475 initialization_options: Some(json!({
14476 "anotherInitValue": false
14477 })),
14478 enable_lsp_tasks: false,
14479 },
14480 );
14481 });
14482 cx.executor().run_until_parked();
14483 assert_eq!(
14484 server_restarts.load(atomic::Ordering::Acquire),
14485 1,
14486 "Should restart LSP server on a related LSP settings change"
14487 );
14488
14489 update_test_project_settings(cx, |project_settings| {
14490 project_settings.lsp.insert(
14491 language_server_name.into(),
14492 LspSettings {
14493 binary: None,
14494 settings: None,
14495 initialization_options: Some(json!({
14496 "anotherInitValue": false
14497 })),
14498 enable_lsp_tasks: false,
14499 },
14500 );
14501 });
14502 cx.executor().run_until_parked();
14503 assert_eq!(
14504 server_restarts.load(atomic::Ordering::Acquire),
14505 1,
14506 "Should not restart LSP server on a related LSP settings change that is the same"
14507 );
14508
14509 update_test_project_settings(cx, |project_settings| {
14510 project_settings.lsp.insert(
14511 language_server_name.into(),
14512 LspSettings {
14513 binary: None,
14514 settings: None,
14515 initialization_options: None,
14516 enable_lsp_tasks: false,
14517 },
14518 );
14519 });
14520 cx.executor().run_until_parked();
14521 assert_eq!(
14522 server_restarts.load(atomic::Ordering::Acquire),
14523 2,
14524 "Should restart LSP server on another related LSP settings change"
14525 );
14526}
14527
14528#[gpui::test]
14529async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
14530 init_test(cx, |_| {});
14531
14532 let mut cx = EditorLspTestContext::new_rust(
14533 lsp::ServerCapabilities {
14534 completion_provider: Some(lsp::CompletionOptions {
14535 trigger_characters: Some(vec![".".to_string()]),
14536 resolve_provider: Some(true),
14537 ..Default::default()
14538 }),
14539 ..Default::default()
14540 },
14541 cx,
14542 )
14543 .await;
14544
14545 cx.set_state("fn main() { let a = 2ˇ; }");
14546 cx.simulate_keystroke(".");
14547 let completion_item = lsp::CompletionItem {
14548 label: "some".into(),
14549 kind: Some(lsp::CompletionItemKind::SNIPPET),
14550 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
14551 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
14552 kind: lsp::MarkupKind::Markdown,
14553 value: "```rust\nSome(2)\n```".to_string(),
14554 })),
14555 deprecated: Some(false),
14556 sort_text: Some("fffffff2".to_string()),
14557 filter_text: Some("some".to_string()),
14558 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
14559 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14560 range: lsp::Range {
14561 start: lsp::Position {
14562 line: 0,
14563 character: 22,
14564 },
14565 end: lsp::Position {
14566 line: 0,
14567 character: 22,
14568 },
14569 },
14570 new_text: "Some(2)".to_string(),
14571 })),
14572 additional_text_edits: Some(vec![lsp::TextEdit {
14573 range: lsp::Range {
14574 start: lsp::Position {
14575 line: 0,
14576 character: 20,
14577 },
14578 end: lsp::Position {
14579 line: 0,
14580 character: 22,
14581 },
14582 },
14583 new_text: "".to_string(),
14584 }]),
14585 ..Default::default()
14586 };
14587
14588 let closure_completion_item = completion_item.clone();
14589 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14590 let task_completion_item = closure_completion_item.clone();
14591 async move {
14592 Ok(Some(lsp::CompletionResponse::Array(vec![
14593 task_completion_item,
14594 ])))
14595 }
14596 });
14597
14598 request.next().await;
14599
14600 cx.condition(|editor, _| editor.context_menu_visible())
14601 .await;
14602 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14603 editor
14604 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14605 .unwrap()
14606 });
14607 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
14608
14609 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
14610 let task_completion_item = completion_item.clone();
14611 async move { Ok(task_completion_item) }
14612 })
14613 .next()
14614 .await
14615 .unwrap();
14616 apply_additional_edits.await.unwrap();
14617 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
14618}
14619
14620#[gpui::test]
14621async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
14622 init_test(cx, |_| {});
14623
14624 let mut cx = EditorLspTestContext::new_rust(
14625 lsp::ServerCapabilities {
14626 completion_provider: Some(lsp::CompletionOptions {
14627 trigger_characters: Some(vec![".".to_string()]),
14628 resolve_provider: Some(true),
14629 ..Default::default()
14630 }),
14631 ..Default::default()
14632 },
14633 cx,
14634 )
14635 .await;
14636
14637 cx.set_state("fn main() { let a = 2ˇ; }");
14638 cx.simulate_keystroke(".");
14639
14640 let item1 = lsp::CompletionItem {
14641 label: "method id()".to_string(),
14642 filter_text: Some("id".to_string()),
14643 detail: None,
14644 documentation: None,
14645 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14646 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14647 new_text: ".id".to_string(),
14648 })),
14649 ..lsp::CompletionItem::default()
14650 };
14651
14652 let item2 = lsp::CompletionItem {
14653 label: "other".to_string(),
14654 filter_text: Some("other".to_string()),
14655 detail: None,
14656 documentation: None,
14657 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14658 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14659 new_text: ".other".to_string(),
14660 })),
14661 ..lsp::CompletionItem::default()
14662 };
14663
14664 let item1 = item1.clone();
14665 cx.set_request_handler::<lsp::request::Completion, _, _>({
14666 let item1 = item1.clone();
14667 move |_, _, _| {
14668 let item1 = item1.clone();
14669 let item2 = item2.clone();
14670 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
14671 }
14672 })
14673 .next()
14674 .await;
14675
14676 cx.condition(|editor, _| editor.context_menu_visible())
14677 .await;
14678 cx.update_editor(|editor, _, _| {
14679 let context_menu = editor.context_menu.borrow_mut();
14680 let context_menu = context_menu
14681 .as_ref()
14682 .expect("Should have the context menu deployed");
14683 match context_menu {
14684 CodeContextMenu::Completions(completions_menu) => {
14685 let completions = completions_menu.completions.borrow_mut();
14686 assert_eq!(
14687 completions
14688 .iter()
14689 .map(|completion| &completion.label.text)
14690 .collect::<Vec<_>>(),
14691 vec!["method id()", "other"]
14692 )
14693 }
14694 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14695 }
14696 });
14697
14698 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
14699 let item1 = item1.clone();
14700 move |_, item_to_resolve, _| {
14701 let item1 = item1.clone();
14702 async move {
14703 if item1 == item_to_resolve {
14704 Ok(lsp::CompletionItem {
14705 label: "method id()".to_string(),
14706 filter_text: Some("id".to_string()),
14707 detail: Some("Now resolved!".to_string()),
14708 documentation: Some(lsp::Documentation::String("Docs".to_string())),
14709 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14710 range: lsp::Range::new(
14711 lsp::Position::new(0, 22),
14712 lsp::Position::new(0, 22),
14713 ),
14714 new_text: ".id".to_string(),
14715 })),
14716 ..lsp::CompletionItem::default()
14717 })
14718 } else {
14719 Ok(item_to_resolve)
14720 }
14721 }
14722 }
14723 })
14724 .next()
14725 .await
14726 .unwrap();
14727 cx.run_until_parked();
14728
14729 cx.update_editor(|editor, window, cx| {
14730 editor.context_menu_next(&Default::default(), window, cx);
14731 });
14732
14733 cx.update_editor(|editor, _, _| {
14734 let context_menu = editor.context_menu.borrow_mut();
14735 let context_menu = context_menu
14736 .as_ref()
14737 .expect("Should have the context menu deployed");
14738 match context_menu {
14739 CodeContextMenu::Completions(completions_menu) => {
14740 let completions = completions_menu.completions.borrow_mut();
14741 assert_eq!(
14742 completions
14743 .iter()
14744 .map(|completion| &completion.label.text)
14745 .collect::<Vec<_>>(),
14746 vec!["method id() Now resolved!", "other"],
14747 "Should update first completion label, but not second as the filter text did not match."
14748 );
14749 }
14750 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14751 }
14752 });
14753}
14754
14755#[gpui::test]
14756async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
14757 init_test(cx, |_| {});
14758 let mut cx = EditorLspTestContext::new_rust(
14759 lsp::ServerCapabilities {
14760 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
14761 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
14762 completion_provider: Some(lsp::CompletionOptions {
14763 resolve_provider: Some(true),
14764 ..Default::default()
14765 }),
14766 ..Default::default()
14767 },
14768 cx,
14769 )
14770 .await;
14771 cx.set_state(indoc! {"
14772 struct TestStruct {
14773 field: i32
14774 }
14775
14776 fn mainˇ() {
14777 let unused_var = 42;
14778 let test_struct = TestStruct { field: 42 };
14779 }
14780 "});
14781 let symbol_range = cx.lsp_range(indoc! {"
14782 struct TestStruct {
14783 field: i32
14784 }
14785
14786 «fn main»() {
14787 let unused_var = 42;
14788 let test_struct = TestStruct { field: 42 };
14789 }
14790 "});
14791 let mut hover_requests =
14792 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
14793 Ok(Some(lsp::Hover {
14794 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
14795 kind: lsp::MarkupKind::Markdown,
14796 value: "Function documentation".to_string(),
14797 }),
14798 range: Some(symbol_range),
14799 }))
14800 });
14801
14802 // Case 1: Test that code action menu hide hover popover
14803 cx.dispatch_action(Hover);
14804 hover_requests.next().await;
14805 cx.condition(|editor, _| editor.hover_state.visible()).await;
14806 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
14807 move |_, _, _| async move {
14808 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
14809 lsp::CodeAction {
14810 title: "Remove unused variable".to_string(),
14811 kind: Some(CodeActionKind::QUICKFIX),
14812 edit: Some(lsp::WorkspaceEdit {
14813 changes: Some(
14814 [(
14815 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
14816 vec![lsp::TextEdit {
14817 range: lsp::Range::new(
14818 lsp::Position::new(5, 4),
14819 lsp::Position::new(5, 27),
14820 ),
14821 new_text: "".to_string(),
14822 }],
14823 )]
14824 .into_iter()
14825 .collect(),
14826 ),
14827 ..Default::default()
14828 }),
14829 ..Default::default()
14830 },
14831 )]))
14832 },
14833 );
14834 cx.update_editor(|editor, window, cx| {
14835 editor.toggle_code_actions(
14836 &ToggleCodeActions {
14837 deployed_from: None,
14838 quick_launch: false,
14839 },
14840 window,
14841 cx,
14842 );
14843 });
14844 code_action_requests.next().await;
14845 cx.run_until_parked();
14846 cx.condition(|editor, _| editor.context_menu_visible())
14847 .await;
14848 cx.update_editor(|editor, _, _| {
14849 assert!(
14850 !editor.hover_state.visible(),
14851 "Hover popover should be hidden when code action menu is shown"
14852 );
14853 // Hide code actions
14854 editor.context_menu.take();
14855 });
14856
14857 // Case 2: Test that code completions hide hover popover
14858 cx.dispatch_action(Hover);
14859 hover_requests.next().await;
14860 cx.condition(|editor, _| editor.hover_state.visible()).await;
14861 let counter = Arc::new(AtomicUsize::new(0));
14862 let mut completion_requests =
14863 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14864 let counter = counter.clone();
14865 async move {
14866 counter.fetch_add(1, atomic::Ordering::Release);
14867 Ok(Some(lsp::CompletionResponse::Array(vec![
14868 lsp::CompletionItem {
14869 label: "main".into(),
14870 kind: Some(lsp::CompletionItemKind::FUNCTION),
14871 detail: Some("() -> ()".to_string()),
14872 ..Default::default()
14873 },
14874 lsp::CompletionItem {
14875 label: "TestStruct".into(),
14876 kind: Some(lsp::CompletionItemKind::STRUCT),
14877 detail: Some("struct TestStruct".to_string()),
14878 ..Default::default()
14879 },
14880 ])))
14881 }
14882 });
14883 cx.update_editor(|editor, window, cx| {
14884 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14885 });
14886 completion_requests.next().await;
14887 cx.condition(|editor, _| editor.context_menu_visible())
14888 .await;
14889 cx.update_editor(|editor, _, _| {
14890 assert!(
14891 !editor.hover_state.visible(),
14892 "Hover popover should be hidden when completion menu is shown"
14893 );
14894 });
14895}
14896
14897#[gpui::test]
14898async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
14899 init_test(cx, |_| {});
14900
14901 let mut cx = EditorLspTestContext::new_rust(
14902 lsp::ServerCapabilities {
14903 completion_provider: Some(lsp::CompletionOptions {
14904 trigger_characters: Some(vec![".".to_string()]),
14905 resolve_provider: Some(true),
14906 ..Default::default()
14907 }),
14908 ..Default::default()
14909 },
14910 cx,
14911 )
14912 .await;
14913
14914 cx.set_state("fn main() { let a = 2ˇ; }");
14915 cx.simulate_keystroke(".");
14916
14917 let unresolved_item_1 = lsp::CompletionItem {
14918 label: "id".to_string(),
14919 filter_text: Some("id".to_string()),
14920 detail: None,
14921 documentation: None,
14922 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14923 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14924 new_text: ".id".to_string(),
14925 })),
14926 ..lsp::CompletionItem::default()
14927 };
14928 let resolved_item_1 = lsp::CompletionItem {
14929 additional_text_edits: Some(vec![lsp::TextEdit {
14930 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
14931 new_text: "!!".to_string(),
14932 }]),
14933 ..unresolved_item_1.clone()
14934 };
14935 let unresolved_item_2 = lsp::CompletionItem {
14936 label: "other".to_string(),
14937 filter_text: Some("other".to_string()),
14938 detail: None,
14939 documentation: None,
14940 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14941 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14942 new_text: ".other".to_string(),
14943 })),
14944 ..lsp::CompletionItem::default()
14945 };
14946 let resolved_item_2 = lsp::CompletionItem {
14947 additional_text_edits: Some(vec![lsp::TextEdit {
14948 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
14949 new_text: "??".to_string(),
14950 }]),
14951 ..unresolved_item_2.clone()
14952 };
14953
14954 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
14955 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
14956 cx.lsp
14957 .server
14958 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
14959 let unresolved_item_1 = unresolved_item_1.clone();
14960 let resolved_item_1 = resolved_item_1.clone();
14961 let unresolved_item_2 = unresolved_item_2.clone();
14962 let resolved_item_2 = resolved_item_2.clone();
14963 let resolve_requests_1 = resolve_requests_1.clone();
14964 let resolve_requests_2 = resolve_requests_2.clone();
14965 move |unresolved_request, _| {
14966 let unresolved_item_1 = unresolved_item_1.clone();
14967 let resolved_item_1 = resolved_item_1.clone();
14968 let unresolved_item_2 = unresolved_item_2.clone();
14969 let resolved_item_2 = resolved_item_2.clone();
14970 let resolve_requests_1 = resolve_requests_1.clone();
14971 let resolve_requests_2 = resolve_requests_2.clone();
14972 async move {
14973 if unresolved_request == unresolved_item_1 {
14974 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
14975 Ok(resolved_item_1.clone())
14976 } else if unresolved_request == unresolved_item_2 {
14977 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
14978 Ok(resolved_item_2.clone())
14979 } else {
14980 panic!("Unexpected completion item {unresolved_request:?}")
14981 }
14982 }
14983 }
14984 })
14985 .detach();
14986
14987 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14988 let unresolved_item_1 = unresolved_item_1.clone();
14989 let unresolved_item_2 = unresolved_item_2.clone();
14990 async move {
14991 Ok(Some(lsp::CompletionResponse::Array(vec![
14992 unresolved_item_1,
14993 unresolved_item_2,
14994 ])))
14995 }
14996 })
14997 .next()
14998 .await;
14999
15000 cx.condition(|editor, _| editor.context_menu_visible())
15001 .await;
15002 cx.update_editor(|editor, _, _| {
15003 let context_menu = editor.context_menu.borrow_mut();
15004 let context_menu = context_menu
15005 .as_ref()
15006 .expect("Should have the context menu deployed");
15007 match context_menu {
15008 CodeContextMenu::Completions(completions_menu) => {
15009 let completions = completions_menu.completions.borrow_mut();
15010 assert_eq!(
15011 completions
15012 .iter()
15013 .map(|completion| &completion.label.text)
15014 .collect::<Vec<_>>(),
15015 vec!["id", "other"]
15016 )
15017 }
15018 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15019 }
15020 });
15021 cx.run_until_parked();
15022
15023 cx.update_editor(|editor, window, cx| {
15024 editor.context_menu_next(&ContextMenuNext, window, cx);
15025 });
15026 cx.run_until_parked();
15027 cx.update_editor(|editor, window, cx| {
15028 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
15029 });
15030 cx.run_until_parked();
15031 cx.update_editor(|editor, window, cx| {
15032 editor.context_menu_next(&ContextMenuNext, window, cx);
15033 });
15034 cx.run_until_parked();
15035 cx.update_editor(|editor, window, cx| {
15036 editor
15037 .compose_completion(&ComposeCompletion::default(), window, cx)
15038 .expect("No task returned")
15039 })
15040 .await
15041 .expect("Completion failed");
15042 cx.run_until_parked();
15043
15044 cx.update_editor(|editor, _, cx| {
15045 assert_eq!(
15046 resolve_requests_1.load(atomic::Ordering::Acquire),
15047 1,
15048 "Should always resolve once despite multiple selections"
15049 );
15050 assert_eq!(
15051 resolve_requests_2.load(atomic::Ordering::Acquire),
15052 1,
15053 "Should always resolve once after multiple selections and applying the completion"
15054 );
15055 assert_eq!(
15056 editor.text(cx),
15057 "fn main() { let a = ??.other; }",
15058 "Should use resolved data when applying the completion"
15059 );
15060 });
15061}
15062
15063#[gpui::test]
15064async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
15065 init_test(cx, |_| {});
15066
15067 let item_0 = lsp::CompletionItem {
15068 label: "abs".into(),
15069 insert_text: Some("abs".into()),
15070 data: Some(json!({ "very": "special"})),
15071 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
15072 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
15073 lsp::InsertReplaceEdit {
15074 new_text: "abs".to_string(),
15075 insert: lsp::Range::default(),
15076 replace: lsp::Range::default(),
15077 },
15078 )),
15079 ..lsp::CompletionItem::default()
15080 };
15081 let items = iter::once(item_0.clone())
15082 .chain((11..51).map(|i| lsp::CompletionItem {
15083 label: format!("item_{}", i),
15084 insert_text: Some(format!("item_{}", i)),
15085 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15086 ..lsp::CompletionItem::default()
15087 }))
15088 .collect::<Vec<_>>();
15089
15090 let default_commit_characters = vec!["?".to_string()];
15091 let default_data = json!({ "default": "data"});
15092 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
15093 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
15094 let default_edit_range = lsp::Range {
15095 start: lsp::Position {
15096 line: 0,
15097 character: 5,
15098 },
15099 end: lsp::Position {
15100 line: 0,
15101 character: 5,
15102 },
15103 };
15104
15105 let mut cx = EditorLspTestContext::new_rust(
15106 lsp::ServerCapabilities {
15107 completion_provider: Some(lsp::CompletionOptions {
15108 trigger_characters: Some(vec![".".to_string()]),
15109 resolve_provider: Some(true),
15110 ..Default::default()
15111 }),
15112 ..Default::default()
15113 },
15114 cx,
15115 )
15116 .await;
15117
15118 cx.set_state("fn main() { let a = 2ˇ; }");
15119 cx.simulate_keystroke(".");
15120
15121 let completion_data = default_data.clone();
15122 let completion_characters = default_commit_characters.clone();
15123 let completion_items = items.clone();
15124 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15125 let default_data = completion_data.clone();
15126 let default_commit_characters = completion_characters.clone();
15127 let items = completion_items.clone();
15128 async move {
15129 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15130 items,
15131 item_defaults: Some(lsp::CompletionListItemDefaults {
15132 data: Some(default_data.clone()),
15133 commit_characters: Some(default_commit_characters.clone()),
15134 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
15135 default_edit_range,
15136 )),
15137 insert_text_format: Some(default_insert_text_format),
15138 insert_text_mode: Some(default_insert_text_mode),
15139 }),
15140 ..lsp::CompletionList::default()
15141 })))
15142 }
15143 })
15144 .next()
15145 .await;
15146
15147 let resolved_items = Arc::new(Mutex::new(Vec::new()));
15148 cx.lsp
15149 .server
15150 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
15151 let closure_resolved_items = resolved_items.clone();
15152 move |item_to_resolve, _| {
15153 let closure_resolved_items = closure_resolved_items.clone();
15154 async move {
15155 closure_resolved_items.lock().push(item_to_resolve.clone());
15156 Ok(item_to_resolve)
15157 }
15158 }
15159 })
15160 .detach();
15161
15162 cx.condition(|editor, _| editor.context_menu_visible())
15163 .await;
15164 cx.run_until_parked();
15165 cx.update_editor(|editor, _, _| {
15166 let menu = editor.context_menu.borrow_mut();
15167 match menu.as_ref().expect("should have the completions menu") {
15168 CodeContextMenu::Completions(completions_menu) => {
15169 assert_eq!(
15170 completions_menu
15171 .entries
15172 .borrow()
15173 .iter()
15174 .map(|mat| mat.string.clone())
15175 .collect::<Vec<String>>(),
15176 items
15177 .iter()
15178 .map(|completion| completion.label.clone())
15179 .collect::<Vec<String>>()
15180 );
15181 }
15182 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
15183 }
15184 });
15185 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
15186 // with 4 from the end.
15187 assert_eq!(
15188 *resolved_items.lock(),
15189 [&items[0..16], &items[items.len() - 4..items.len()]]
15190 .concat()
15191 .iter()
15192 .cloned()
15193 .map(|mut item| {
15194 if item.data.is_none() {
15195 item.data = Some(default_data.clone());
15196 }
15197 item
15198 })
15199 .collect::<Vec<lsp::CompletionItem>>(),
15200 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
15201 );
15202 resolved_items.lock().clear();
15203
15204 cx.update_editor(|editor, window, cx| {
15205 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
15206 });
15207 cx.run_until_parked();
15208 // Completions that have already been resolved are skipped.
15209 assert_eq!(
15210 *resolved_items.lock(),
15211 items[items.len() - 16..items.len() - 4]
15212 .iter()
15213 .cloned()
15214 .map(|mut item| {
15215 if item.data.is_none() {
15216 item.data = Some(default_data.clone());
15217 }
15218 item
15219 })
15220 .collect::<Vec<lsp::CompletionItem>>()
15221 );
15222 resolved_items.lock().clear();
15223}
15224
15225#[gpui::test]
15226async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
15227 init_test(cx, |_| {});
15228
15229 let mut cx = EditorLspTestContext::new(
15230 Language::new(
15231 LanguageConfig {
15232 matcher: LanguageMatcher {
15233 path_suffixes: vec!["jsx".into()],
15234 ..Default::default()
15235 },
15236 overrides: [(
15237 "element".into(),
15238 LanguageConfigOverride {
15239 completion_query_characters: Override::Set(['-'].into_iter().collect()),
15240 ..Default::default()
15241 },
15242 )]
15243 .into_iter()
15244 .collect(),
15245 ..Default::default()
15246 },
15247 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15248 )
15249 .with_override_query("(jsx_self_closing_element) @element")
15250 .unwrap(),
15251 lsp::ServerCapabilities {
15252 completion_provider: Some(lsp::CompletionOptions {
15253 trigger_characters: Some(vec![":".to_string()]),
15254 ..Default::default()
15255 }),
15256 ..Default::default()
15257 },
15258 cx,
15259 )
15260 .await;
15261
15262 cx.lsp
15263 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15264 Ok(Some(lsp::CompletionResponse::Array(vec![
15265 lsp::CompletionItem {
15266 label: "bg-blue".into(),
15267 ..Default::default()
15268 },
15269 lsp::CompletionItem {
15270 label: "bg-red".into(),
15271 ..Default::default()
15272 },
15273 lsp::CompletionItem {
15274 label: "bg-yellow".into(),
15275 ..Default::default()
15276 },
15277 ])))
15278 });
15279
15280 cx.set_state(r#"<p class="bgˇ" />"#);
15281
15282 // Trigger completion when typing a dash, because the dash is an extra
15283 // word character in the 'element' scope, which contains the cursor.
15284 cx.simulate_keystroke("-");
15285 cx.executor().run_until_parked();
15286 cx.update_editor(|editor, _, _| {
15287 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15288 {
15289 assert_eq!(
15290 completion_menu_entries(&menu),
15291 &["bg-red", "bg-blue", "bg-yellow"]
15292 );
15293 } else {
15294 panic!("expected completion menu to be open");
15295 }
15296 });
15297
15298 cx.simulate_keystroke("l");
15299 cx.executor().run_until_parked();
15300 cx.update_editor(|editor, _, _| {
15301 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15302 {
15303 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
15304 } else {
15305 panic!("expected completion menu to be open");
15306 }
15307 });
15308
15309 // When filtering completions, consider the character after the '-' to
15310 // be the start of a subword.
15311 cx.set_state(r#"<p class="yelˇ" />"#);
15312 cx.simulate_keystroke("l");
15313 cx.executor().run_until_parked();
15314 cx.update_editor(|editor, _, _| {
15315 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15316 {
15317 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
15318 } else {
15319 panic!("expected completion menu to be open");
15320 }
15321 });
15322}
15323
15324fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
15325 let entries = menu.entries.borrow();
15326 entries.iter().map(|mat| mat.string.clone()).collect()
15327}
15328
15329#[gpui::test]
15330async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
15331 init_test(cx, |settings| {
15332 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
15333 FormatterList(vec![Formatter::Prettier].into()),
15334 ))
15335 });
15336
15337 let fs = FakeFs::new(cx.executor());
15338 fs.insert_file(path!("/file.ts"), Default::default()).await;
15339
15340 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
15341 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15342
15343 language_registry.add(Arc::new(Language::new(
15344 LanguageConfig {
15345 name: "TypeScript".into(),
15346 matcher: LanguageMatcher {
15347 path_suffixes: vec!["ts".to_string()],
15348 ..Default::default()
15349 },
15350 ..Default::default()
15351 },
15352 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
15353 )));
15354 update_test_language_settings(cx, |settings| {
15355 settings.defaults.prettier = Some(PrettierSettings {
15356 allowed: true,
15357 ..PrettierSettings::default()
15358 });
15359 });
15360
15361 let test_plugin = "test_plugin";
15362 let _ = language_registry.register_fake_lsp(
15363 "TypeScript",
15364 FakeLspAdapter {
15365 prettier_plugins: vec![test_plugin],
15366 ..Default::default()
15367 },
15368 );
15369
15370 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
15371 let buffer = project
15372 .update(cx, |project, cx| {
15373 project.open_local_buffer(path!("/file.ts"), cx)
15374 })
15375 .await
15376 .unwrap();
15377
15378 let buffer_text = "one\ntwo\nthree\n";
15379 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
15380 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
15381 editor.update_in(cx, |editor, window, cx| {
15382 editor.set_text(buffer_text, window, cx)
15383 });
15384
15385 editor
15386 .update_in(cx, |editor, window, cx| {
15387 editor.perform_format(
15388 project.clone(),
15389 FormatTrigger::Manual,
15390 FormatTarget::Buffers,
15391 window,
15392 cx,
15393 )
15394 })
15395 .unwrap()
15396 .await;
15397 assert_eq!(
15398 editor.update(cx, |editor, cx| editor.text(cx)),
15399 buffer_text.to_string() + prettier_format_suffix,
15400 "Test prettier formatting was not applied to the original buffer text",
15401 );
15402
15403 update_test_language_settings(cx, |settings| {
15404 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
15405 });
15406 let format = editor.update_in(cx, |editor, window, cx| {
15407 editor.perform_format(
15408 project.clone(),
15409 FormatTrigger::Manual,
15410 FormatTarget::Buffers,
15411 window,
15412 cx,
15413 )
15414 });
15415 format.await.unwrap();
15416 assert_eq!(
15417 editor.update(cx, |editor, cx| editor.text(cx)),
15418 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
15419 "Autoformatting (via test prettier) was not applied to the original buffer text",
15420 );
15421}
15422
15423#[gpui::test]
15424async fn test_addition_reverts(cx: &mut TestAppContext) {
15425 init_test(cx, |_| {});
15426 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15427 let base_text = indoc! {r#"
15428 struct Row;
15429 struct Row1;
15430 struct Row2;
15431
15432 struct Row4;
15433 struct Row5;
15434 struct Row6;
15435
15436 struct Row8;
15437 struct Row9;
15438 struct Row10;"#};
15439
15440 // When addition hunks are not adjacent to carets, no hunk revert is performed
15441 assert_hunk_revert(
15442 indoc! {r#"struct Row;
15443 struct Row1;
15444 struct Row1.1;
15445 struct Row1.2;
15446 struct Row2;ˇ
15447
15448 struct Row4;
15449 struct Row5;
15450 struct Row6;
15451
15452 struct Row8;
15453 ˇstruct Row9;
15454 struct Row9.1;
15455 struct Row9.2;
15456 struct Row9.3;
15457 struct Row10;"#},
15458 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
15459 indoc! {r#"struct Row;
15460 struct Row1;
15461 struct Row1.1;
15462 struct Row1.2;
15463 struct Row2;ˇ
15464
15465 struct Row4;
15466 struct Row5;
15467 struct Row6;
15468
15469 struct Row8;
15470 ˇstruct Row9;
15471 struct Row9.1;
15472 struct Row9.2;
15473 struct Row9.3;
15474 struct Row10;"#},
15475 base_text,
15476 &mut cx,
15477 );
15478 // Same for selections
15479 assert_hunk_revert(
15480 indoc! {r#"struct Row;
15481 struct Row1;
15482 struct Row2;
15483 struct Row2.1;
15484 struct Row2.2;
15485 «ˇ
15486 struct Row4;
15487 struct» Row5;
15488 «struct Row6;
15489 ˇ»
15490 struct Row9.1;
15491 struct Row9.2;
15492 struct Row9.3;
15493 struct Row8;
15494 struct Row9;
15495 struct Row10;"#},
15496 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
15497 indoc! {r#"struct Row;
15498 struct Row1;
15499 struct Row2;
15500 struct Row2.1;
15501 struct Row2.2;
15502 «ˇ
15503 struct Row4;
15504 struct» Row5;
15505 «struct Row6;
15506 ˇ»
15507 struct Row9.1;
15508 struct Row9.2;
15509 struct Row9.3;
15510 struct Row8;
15511 struct Row9;
15512 struct Row10;"#},
15513 base_text,
15514 &mut cx,
15515 );
15516
15517 // When carets and selections intersect the addition hunks, those are reverted.
15518 // Adjacent carets got merged.
15519 assert_hunk_revert(
15520 indoc! {r#"struct Row;
15521 ˇ// something on the top
15522 struct Row1;
15523 struct Row2;
15524 struct Roˇw3.1;
15525 struct Row2.2;
15526 struct Row2.3;ˇ
15527
15528 struct Row4;
15529 struct ˇRow5.1;
15530 struct Row5.2;
15531 struct «Rowˇ»5.3;
15532 struct Row5;
15533 struct Row6;
15534 ˇ
15535 struct Row9.1;
15536 struct «Rowˇ»9.2;
15537 struct «ˇRow»9.3;
15538 struct Row8;
15539 struct Row9;
15540 «ˇ// something on bottom»
15541 struct Row10;"#},
15542 vec![
15543 DiffHunkStatusKind::Added,
15544 DiffHunkStatusKind::Added,
15545 DiffHunkStatusKind::Added,
15546 DiffHunkStatusKind::Added,
15547 DiffHunkStatusKind::Added,
15548 ],
15549 indoc! {r#"struct Row;
15550 ˇstruct Row1;
15551 struct Row2;
15552 ˇ
15553 struct Row4;
15554 ˇstruct Row5;
15555 struct Row6;
15556 ˇ
15557 ˇstruct Row8;
15558 struct Row9;
15559 ˇstruct Row10;"#},
15560 base_text,
15561 &mut cx,
15562 );
15563}
15564
15565#[gpui::test]
15566async fn test_modification_reverts(cx: &mut TestAppContext) {
15567 init_test(cx, |_| {});
15568 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15569 let base_text = indoc! {r#"
15570 struct Row;
15571 struct Row1;
15572 struct Row2;
15573
15574 struct Row4;
15575 struct Row5;
15576 struct Row6;
15577
15578 struct Row8;
15579 struct Row9;
15580 struct Row10;"#};
15581
15582 // Modification hunks behave the same as the addition ones.
15583 assert_hunk_revert(
15584 indoc! {r#"struct Row;
15585 struct Row1;
15586 struct Row33;
15587 ˇ
15588 struct Row4;
15589 struct Row5;
15590 struct Row6;
15591 ˇ
15592 struct Row99;
15593 struct Row9;
15594 struct Row10;"#},
15595 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
15596 indoc! {r#"struct Row;
15597 struct Row1;
15598 struct Row33;
15599 ˇ
15600 struct Row4;
15601 struct Row5;
15602 struct Row6;
15603 ˇ
15604 struct Row99;
15605 struct Row9;
15606 struct Row10;"#},
15607 base_text,
15608 &mut cx,
15609 );
15610 assert_hunk_revert(
15611 indoc! {r#"struct Row;
15612 struct Row1;
15613 struct Row33;
15614 «ˇ
15615 struct Row4;
15616 struct» Row5;
15617 «struct Row6;
15618 ˇ»
15619 struct Row99;
15620 struct Row9;
15621 struct Row10;"#},
15622 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
15623 indoc! {r#"struct Row;
15624 struct Row1;
15625 struct Row33;
15626 «ˇ
15627 struct Row4;
15628 struct» Row5;
15629 «struct Row6;
15630 ˇ»
15631 struct Row99;
15632 struct Row9;
15633 struct Row10;"#},
15634 base_text,
15635 &mut cx,
15636 );
15637
15638 assert_hunk_revert(
15639 indoc! {r#"ˇstruct Row1.1;
15640 struct Row1;
15641 «ˇstr»uct Row22;
15642
15643 struct ˇRow44;
15644 struct Row5;
15645 struct «Rˇ»ow66;ˇ
15646
15647 «struˇ»ct Row88;
15648 struct Row9;
15649 struct Row1011;ˇ"#},
15650 vec![
15651 DiffHunkStatusKind::Modified,
15652 DiffHunkStatusKind::Modified,
15653 DiffHunkStatusKind::Modified,
15654 DiffHunkStatusKind::Modified,
15655 DiffHunkStatusKind::Modified,
15656 DiffHunkStatusKind::Modified,
15657 ],
15658 indoc! {r#"struct Row;
15659 ˇstruct Row1;
15660 struct Row2;
15661 ˇ
15662 struct Row4;
15663 ˇstruct Row5;
15664 struct Row6;
15665 ˇ
15666 struct Row8;
15667 ˇstruct Row9;
15668 struct Row10;ˇ"#},
15669 base_text,
15670 &mut cx,
15671 );
15672}
15673
15674#[gpui::test]
15675async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
15676 init_test(cx, |_| {});
15677 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15678 let base_text = indoc! {r#"
15679 one
15680
15681 two
15682 three
15683 "#};
15684
15685 cx.set_head_text(base_text);
15686 cx.set_state("\nˇ\n");
15687 cx.executor().run_until_parked();
15688 cx.update_editor(|editor, _window, cx| {
15689 editor.expand_selected_diff_hunks(cx);
15690 });
15691 cx.executor().run_until_parked();
15692 cx.update_editor(|editor, window, cx| {
15693 editor.backspace(&Default::default(), window, cx);
15694 });
15695 cx.run_until_parked();
15696 cx.assert_state_with_diff(
15697 indoc! {r#"
15698
15699 - two
15700 - threeˇ
15701 +
15702 "#}
15703 .to_string(),
15704 );
15705}
15706
15707#[gpui::test]
15708async fn test_deletion_reverts(cx: &mut TestAppContext) {
15709 init_test(cx, |_| {});
15710 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15711 let base_text = indoc! {r#"struct Row;
15712struct Row1;
15713struct Row2;
15714
15715struct Row4;
15716struct Row5;
15717struct Row6;
15718
15719struct Row8;
15720struct Row9;
15721struct Row10;"#};
15722
15723 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
15724 assert_hunk_revert(
15725 indoc! {r#"struct Row;
15726 struct Row2;
15727
15728 ˇstruct Row4;
15729 struct Row5;
15730 struct Row6;
15731 ˇ
15732 struct Row8;
15733 struct Row10;"#},
15734 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15735 indoc! {r#"struct Row;
15736 struct Row2;
15737
15738 ˇstruct Row4;
15739 struct Row5;
15740 struct Row6;
15741 ˇ
15742 struct Row8;
15743 struct Row10;"#},
15744 base_text,
15745 &mut cx,
15746 );
15747 assert_hunk_revert(
15748 indoc! {r#"struct Row;
15749 struct Row2;
15750
15751 «ˇstruct Row4;
15752 struct» Row5;
15753 «struct Row6;
15754 ˇ»
15755 struct Row8;
15756 struct Row10;"#},
15757 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15758 indoc! {r#"struct Row;
15759 struct Row2;
15760
15761 «ˇstruct Row4;
15762 struct» Row5;
15763 «struct Row6;
15764 ˇ»
15765 struct Row8;
15766 struct Row10;"#},
15767 base_text,
15768 &mut cx,
15769 );
15770
15771 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
15772 assert_hunk_revert(
15773 indoc! {r#"struct Row;
15774 ˇstruct Row2;
15775
15776 struct Row4;
15777 struct Row5;
15778 struct Row6;
15779
15780 struct Row8;ˇ
15781 struct Row10;"#},
15782 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15783 indoc! {r#"struct Row;
15784 struct Row1;
15785 ˇstruct Row2;
15786
15787 struct Row4;
15788 struct Row5;
15789 struct Row6;
15790
15791 struct Row8;ˇ
15792 struct Row9;
15793 struct Row10;"#},
15794 base_text,
15795 &mut cx,
15796 );
15797 assert_hunk_revert(
15798 indoc! {r#"struct Row;
15799 struct Row2«ˇ;
15800 struct Row4;
15801 struct» Row5;
15802 «struct Row6;
15803
15804 struct Row8;ˇ»
15805 struct Row10;"#},
15806 vec![
15807 DiffHunkStatusKind::Deleted,
15808 DiffHunkStatusKind::Deleted,
15809 DiffHunkStatusKind::Deleted,
15810 ],
15811 indoc! {r#"struct Row;
15812 struct Row1;
15813 struct Row2«ˇ;
15814
15815 struct Row4;
15816 struct» Row5;
15817 «struct Row6;
15818
15819 struct Row8;ˇ»
15820 struct Row9;
15821 struct Row10;"#},
15822 base_text,
15823 &mut cx,
15824 );
15825}
15826
15827#[gpui::test]
15828async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
15829 init_test(cx, |_| {});
15830
15831 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
15832 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
15833 let base_text_3 =
15834 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
15835
15836 let text_1 = edit_first_char_of_every_line(base_text_1);
15837 let text_2 = edit_first_char_of_every_line(base_text_2);
15838 let text_3 = edit_first_char_of_every_line(base_text_3);
15839
15840 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
15841 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
15842 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
15843
15844 let multibuffer = cx.new(|cx| {
15845 let mut multibuffer = MultiBuffer::new(ReadWrite);
15846 multibuffer.push_excerpts(
15847 buffer_1.clone(),
15848 [
15849 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15850 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15851 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15852 ],
15853 cx,
15854 );
15855 multibuffer.push_excerpts(
15856 buffer_2.clone(),
15857 [
15858 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15859 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15860 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15861 ],
15862 cx,
15863 );
15864 multibuffer.push_excerpts(
15865 buffer_3.clone(),
15866 [
15867 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15868 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15869 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15870 ],
15871 cx,
15872 );
15873 multibuffer
15874 });
15875
15876 let fs = FakeFs::new(cx.executor());
15877 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
15878 let (editor, cx) = cx
15879 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
15880 editor.update_in(cx, |editor, _window, cx| {
15881 for (buffer, diff_base) in [
15882 (buffer_1.clone(), base_text_1),
15883 (buffer_2.clone(), base_text_2),
15884 (buffer_3.clone(), base_text_3),
15885 ] {
15886 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
15887 editor
15888 .buffer
15889 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
15890 }
15891 });
15892 cx.executor().run_until_parked();
15893
15894 editor.update_in(cx, |editor, window, cx| {
15895 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}");
15896 editor.select_all(&SelectAll, window, cx);
15897 editor.git_restore(&Default::default(), window, cx);
15898 });
15899 cx.executor().run_until_parked();
15900
15901 // When all ranges are selected, all buffer hunks are reverted.
15902 editor.update(cx, |editor, cx| {
15903 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");
15904 });
15905 buffer_1.update(cx, |buffer, _| {
15906 assert_eq!(buffer.text(), base_text_1);
15907 });
15908 buffer_2.update(cx, |buffer, _| {
15909 assert_eq!(buffer.text(), base_text_2);
15910 });
15911 buffer_3.update(cx, |buffer, _| {
15912 assert_eq!(buffer.text(), base_text_3);
15913 });
15914
15915 editor.update_in(cx, |editor, window, cx| {
15916 editor.undo(&Default::default(), window, cx);
15917 });
15918
15919 editor.update_in(cx, |editor, window, cx| {
15920 editor.change_selections(None, window, cx, |s| {
15921 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
15922 });
15923 editor.git_restore(&Default::default(), window, cx);
15924 });
15925
15926 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
15927 // but not affect buffer_2 and its related excerpts.
15928 editor.update(cx, |editor, cx| {
15929 assert_eq!(
15930 editor.text(cx),
15931 "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}"
15932 );
15933 });
15934 buffer_1.update(cx, |buffer, _| {
15935 assert_eq!(buffer.text(), base_text_1);
15936 });
15937 buffer_2.update(cx, |buffer, _| {
15938 assert_eq!(
15939 buffer.text(),
15940 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
15941 );
15942 });
15943 buffer_3.update(cx, |buffer, _| {
15944 assert_eq!(
15945 buffer.text(),
15946 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
15947 );
15948 });
15949
15950 fn edit_first_char_of_every_line(text: &str) -> String {
15951 text.split('\n')
15952 .map(|line| format!("X{}", &line[1..]))
15953 .collect::<Vec<_>>()
15954 .join("\n")
15955 }
15956}
15957
15958#[gpui::test]
15959async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
15960 init_test(cx, |_| {});
15961
15962 let cols = 4;
15963 let rows = 10;
15964 let sample_text_1 = sample_text(rows, cols, 'a');
15965 assert_eq!(
15966 sample_text_1,
15967 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
15968 );
15969 let sample_text_2 = sample_text(rows, cols, 'l');
15970 assert_eq!(
15971 sample_text_2,
15972 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
15973 );
15974 let sample_text_3 = sample_text(rows, cols, 'v');
15975 assert_eq!(
15976 sample_text_3,
15977 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
15978 );
15979
15980 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
15981 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
15982 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
15983
15984 let multi_buffer = cx.new(|cx| {
15985 let mut multibuffer = MultiBuffer::new(ReadWrite);
15986 multibuffer.push_excerpts(
15987 buffer_1.clone(),
15988 [
15989 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15990 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15991 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15992 ],
15993 cx,
15994 );
15995 multibuffer.push_excerpts(
15996 buffer_2.clone(),
15997 [
15998 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15999 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16000 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16001 ],
16002 cx,
16003 );
16004 multibuffer.push_excerpts(
16005 buffer_3.clone(),
16006 [
16007 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16008 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16009 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16010 ],
16011 cx,
16012 );
16013 multibuffer
16014 });
16015
16016 let fs = FakeFs::new(cx.executor());
16017 fs.insert_tree(
16018 "/a",
16019 json!({
16020 "main.rs": sample_text_1,
16021 "other.rs": sample_text_2,
16022 "lib.rs": sample_text_3,
16023 }),
16024 )
16025 .await;
16026 let project = Project::test(fs, ["/a".as_ref()], cx).await;
16027 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16028 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16029 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16030 Editor::new(
16031 EditorMode::full(),
16032 multi_buffer,
16033 Some(project.clone()),
16034 window,
16035 cx,
16036 )
16037 });
16038 let multibuffer_item_id = workspace
16039 .update(cx, |workspace, window, cx| {
16040 assert!(
16041 workspace.active_item(cx).is_none(),
16042 "active item should be None before the first item is added"
16043 );
16044 workspace.add_item_to_active_pane(
16045 Box::new(multi_buffer_editor.clone()),
16046 None,
16047 true,
16048 window,
16049 cx,
16050 );
16051 let active_item = workspace
16052 .active_item(cx)
16053 .expect("should have an active item after adding the multi buffer");
16054 assert!(
16055 !active_item.is_singleton(cx),
16056 "A multi buffer was expected to active after adding"
16057 );
16058 active_item.item_id()
16059 })
16060 .unwrap();
16061 cx.executor().run_until_parked();
16062
16063 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16064 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
16065 s.select_ranges(Some(1..2))
16066 });
16067 editor.open_excerpts(&OpenExcerpts, window, cx);
16068 });
16069 cx.executor().run_until_parked();
16070 let first_item_id = workspace
16071 .update(cx, |workspace, window, cx| {
16072 let active_item = workspace
16073 .active_item(cx)
16074 .expect("should have an active item after navigating into the 1st buffer");
16075 let first_item_id = active_item.item_id();
16076 assert_ne!(
16077 first_item_id, multibuffer_item_id,
16078 "Should navigate into the 1st buffer and activate it"
16079 );
16080 assert!(
16081 active_item.is_singleton(cx),
16082 "New active item should be a singleton buffer"
16083 );
16084 assert_eq!(
16085 active_item
16086 .act_as::<Editor>(cx)
16087 .expect("should have navigated into an editor for the 1st buffer")
16088 .read(cx)
16089 .text(cx),
16090 sample_text_1
16091 );
16092
16093 workspace
16094 .go_back(workspace.active_pane().downgrade(), window, cx)
16095 .detach_and_log_err(cx);
16096
16097 first_item_id
16098 })
16099 .unwrap();
16100 cx.executor().run_until_parked();
16101 workspace
16102 .update(cx, |workspace, _, cx| {
16103 let active_item = workspace
16104 .active_item(cx)
16105 .expect("should have an active item after navigating back");
16106 assert_eq!(
16107 active_item.item_id(),
16108 multibuffer_item_id,
16109 "Should navigate back to the multi buffer"
16110 );
16111 assert!(!active_item.is_singleton(cx));
16112 })
16113 .unwrap();
16114
16115 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16116 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
16117 s.select_ranges(Some(39..40))
16118 });
16119 editor.open_excerpts(&OpenExcerpts, window, cx);
16120 });
16121 cx.executor().run_until_parked();
16122 let second_item_id = workspace
16123 .update(cx, |workspace, window, cx| {
16124 let active_item = workspace
16125 .active_item(cx)
16126 .expect("should have an active item after navigating into the 2nd buffer");
16127 let second_item_id = active_item.item_id();
16128 assert_ne!(
16129 second_item_id, multibuffer_item_id,
16130 "Should navigate away from the multibuffer"
16131 );
16132 assert_ne!(
16133 second_item_id, first_item_id,
16134 "Should navigate into the 2nd buffer and activate it"
16135 );
16136 assert!(
16137 active_item.is_singleton(cx),
16138 "New active item should be a singleton buffer"
16139 );
16140 assert_eq!(
16141 active_item
16142 .act_as::<Editor>(cx)
16143 .expect("should have navigated into an editor")
16144 .read(cx)
16145 .text(cx),
16146 sample_text_2
16147 );
16148
16149 workspace
16150 .go_back(workspace.active_pane().downgrade(), window, cx)
16151 .detach_and_log_err(cx);
16152
16153 second_item_id
16154 })
16155 .unwrap();
16156 cx.executor().run_until_parked();
16157 workspace
16158 .update(cx, |workspace, _, cx| {
16159 let active_item = workspace
16160 .active_item(cx)
16161 .expect("should have an active item after navigating back from the 2nd buffer");
16162 assert_eq!(
16163 active_item.item_id(),
16164 multibuffer_item_id,
16165 "Should navigate back from the 2nd buffer to the multi buffer"
16166 );
16167 assert!(!active_item.is_singleton(cx));
16168 })
16169 .unwrap();
16170
16171 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16172 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
16173 s.select_ranges(Some(70..70))
16174 });
16175 editor.open_excerpts(&OpenExcerpts, window, cx);
16176 });
16177 cx.executor().run_until_parked();
16178 workspace
16179 .update(cx, |workspace, window, cx| {
16180 let active_item = workspace
16181 .active_item(cx)
16182 .expect("should have an active item after navigating into the 3rd buffer");
16183 let third_item_id = active_item.item_id();
16184 assert_ne!(
16185 third_item_id, multibuffer_item_id,
16186 "Should navigate into the 3rd buffer and activate it"
16187 );
16188 assert_ne!(third_item_id, first_item_id);
16189 assert_ne!(third_item_id, second_item_id);
16190 assert!(
16191 active_item.is_singleton(cx),
16192 "New active item should be a singleton buffer"
16193 );
16194 assert_eq!(
16195 active_item
16196 .act_as::<Editor>(cx)
16197 .expect("should have navigated into an editor")
16198 .read(cx)
16199 .text(cx),
16200 sample_text_3
16201 );
16202
16203 workspace
16204 .go_back(workspace.active_pane().downgrade(), window, cx)
16205 .detach_and_log_err(cx);
16206 })
16207 .unwrap();
16208 cx.executor().run_until_parked();
16209 workspace
16210 .update(cx, |workspace, _, cx| {
16211 let active_item = workspace
16212 .active_item(cx)
16213 .expect("should have an active item after navigating back from the 3rd buffer");
16214 assert_eq!(
16215 active_item.item_id(),
16216 multibuffer_item_id,
16217 "Should navigate back from the 3rd buffer to the multi buffer"
16218 );
16219 assert!(!active_item.is_singleton(cx));
16220 })
16221 .unwrap();
16222}
16223
16224#[gpui::test]
16225async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16226 init_test(cx, |_| {});
16227
16228 let mut cx = EditorTestContext::new(cx).await;
16229
16230 let diff_base = r#"
16231 use some::mod;
16232
16233 const A: u32 = 42;
16234
16235 fn main() {
16236 println!("hello");
16237
16238 println!("world");
16239 }
16240 "#
16241 .unindent();
16242
16243 cx.set_state(
16244 &r#"
16245 use some::modified;
16246
16247 ˇ
16248 fn main() {
16249 println!("hello there");
16250
16251 println!("around the");
16252 println!("world");
16253 }
16254 "#
16255 .unindent(),
16256 );
16257
16258 cx.set_head_text(&diff_base);
16259 executor.run_until_parked();
16260
16261 cx.update_editor(|editor, window, cx| {
16262 editor.go_to_next_hunk(&GoToHunk, window, cx);
16263 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16264 });
16265 executor.run_until_parked();
16266 cx.assert_state_with_diff(
16267 r#"
16268 use some::modified;
16269
16270
16271 fn main() {
16272 - println!("hello");
16273 + ˇ println!("hello there");
16274
16275 println!("around the");
16276 println!("world");
16277 }
16278 "#
16279 .unindent(),
16280 );
16281
16282 cx.update_editor(|editor, window, cx| {
16283 for _ in 0..2 {
16284 editor.go_to_next_hunk(&GoToHunk, window, cx);
16285 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16286 }
16287 });
16288 executor.run_until_parked();
16289 cx.assert_state_with_diff(
16290 r#"
16291 - use some::mod;
16292 + ˇuse some::modified;
16293
16294
16295 fn main() {
16296 - println!("hello");
16297 + println!("hello there");
16298
16299 + println!("around the");
16300 println!("world");
16301 }
16302 "#
16303 .unindent(),
16304 );
16305
16306 cx.update_editor(|editor, window, cx| {
16307 editor.go_to_next_hunk(&GoToHunk, window, cx);
16308 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16309 });
16310 executor.run_until_parked();
16311 cx.assert_state_with_diff(
16312 r#"
16313 - use some::mod;
16314 + use some::modified;
16315
16316 - const A: u32 = 42;
16317 ˇ
16318 fn main() {
16319 - println!("hello");
16320 + println!("hello there");
16321
16322 + println!("around the");
16323 println!("world");
16324 }
16325 "#
16326 .unindent(),
16327 );
16328
16329 cx.update_editor(|editor, window, cx| {
16330 editor.cancel(&Cancel, window, cx);
16331 });
16332
16333 cx.assert_state_with_diff(
16334 r#"
16335 use some::modified;
16336
16337 ˇ
16338 fn main() {
16339 println!("hello there");
16340
16341 println!("around the");
16342 println!("world");
16343 }
16344 "#
16345 .unindent(),
16346 );
16347}
16348
16349#[gpui::test]
16350async fn test_diff_base_change_with_expanded_diff_hunks(
16351 executor: BackgroundExecutor,
16352 cx: &mut TestAppContext,
16353) {
16354 init_test(cx, |_| {});
16355
16356 let mut cx = EditorTestContext::new(cx).await;
16357
16358 let diff_base = r#"
16359 use some::mod1;
16360 use some::mod2;
16361
16362 const A: u32 = 42;
16363 const B: u32 = 42;
16364 const C: u32 = 42;
16365
16366 fn main() {
16367 println!("hello");
16368
16369 println!("world");
16370 }
16371 "#
16372 .unindent();
16373
16374 cx.set_state(
16375 &r#"
16376 use some::mod2;
16377
16378 const A: u32 = 42;
16379 const C: u32 = 42;
16380
16381 fn main(ˇ) {
16382 //println!("hello");
16383
16384 println!("world");
16385 //
16386 //
16387 }
16388 "#
16389 .unindent(),
16390 );
16391
16392 cx.set_head_text(&diff_base);
16393 executor.run_until_parked();
16394
16395 cx.update_editor(|editor, window, cx| {
16396 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16397 });
16398 executor.run_until_parked();
16399 cx.assert_state_with_diff(
16400 r#"
16401 - use some::mod1;
16402 use some::mod2;
16403
16404 const A: u32 = 42;
16405 - const B: u32 = 42;
16406 const C: u32 = 42;
16407
16408 fn main(ˇ) {
16409 - println!("hello");
16410 + //println!("hello");
16411
16412 println!("world");
16413 + //
16414 + //
16415 }
16416 "#
16417 .unindent(),
16418 );
16419
16420 cx.set_head_text("new diff base!");
16421 executor.run_until_parked();
16422 cx.assert_state_with_diff(
16423 r#"
16424 - new diff base!
16425 + use some::mod2;
16426 +
16427 + const A: u32 = 42;
16428 + const C: u32 = 42;
16429 +
16430 + fn main(ˇ) {
16431 + //println!("hello");
16432 +
16433 + println!("world");
16434 + //
16435 + //
16436 + }
16437 "#
16438 .unindent(),
16439 );
16440}
16441
16442#[gpui::test]
16443async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
16444 init_test(cx, |_| {});
16445
16446 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
16447 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
16448 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
16449 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
16450 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
16451 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
16452
16453 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
16454 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
16455 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
16456
16457 let multi_buffer = cx.new(|cx| {
16458 let mut multibuffer = MultiBuffer::new(ReadWrite);
16459 multibuffer.push_excerpts(
16460 buffer_1.clone(),
16461 [
16462 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16463 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16464 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16465 ],
16466 cx,
16467 );
16468 multibuffer.push_excerpts(
16469 buffer_2.clone(),
16470 [
16471 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16472 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16473 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16474 ],
16475 cx,
16476 );
16477 multibuffer.push_excerpts(
16478 buffer_3.clone(),
16479 [
16480 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16481 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16482 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16483 ],
16484 cx,
16485 );
16486 multibuffer
16487 });
16488
16489 let editor =
16490 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
16491 editor
16492 .update(cx, |editor, _window, cx| {
16493 for (buffer, diff_base) in [
16494 (buffer_1.clone(), file_1_old),
16495 (buffer_2.clone(), file_2_old),
16496 (buffer_3.clone(), file_3_old),
16497 ] {
16498 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
16499 editor
16500 .buffer
16501 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
16502 }
16503 })
16504 .unwrap();
16505
16506 let mut cx = EditorTestContext::for_editor(editor, cx).await;
16507 cx.run_until_parked();
16508
16509 cx.assert_editor_state(
16510 &"
16511 ˇaaa
16512 ccc
16513 ddd
16514
16515 ggg
16516 hhh
16517
16518
16519 lll
16520 mmm
16521 NNN
16522
16523 qqq
16524 rrr
16525
16526 uuu
16527 111
16528 222
16529 333
16530
16531 666
16532 777
16533
16534 000
16535 !!!"
16536 .unindent(),
16537 );
16538
16539 cx.update_editor(|editor, window, cx| {
16540 editor.select_all(&SelectAll, window, cx);
16541 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16542 });
16543 cx.executor().run_until_parked();
16544
16545 cx.assert_state_with_diff(
16546 "
16547 «aaa
16548 - bbb
16549 ccc
16550 ddd
16551
16552 ggg
16553 hhh
16554
16555
16556 lll
16557 mmm
16558 - nnn
16559 + NNN
16560
16561 qqq
16562 rrr
16563
16564 uuu
16565 111
16566 222
16567 333
16568
16569 + 666
16570 777
16571
16572 000
16573 !!!ˇ»"
16574 .unindent(),
16575 );
16576}
16577
16578#[gpui::test]
16579async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
16580 init_test(cx, |_| {});
16581
16582 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
16583 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
16584
16585 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
16586 let multi_buffer = cx.new(|cx| {
16587 let mut multibuffer = MultiBuffer::new(ReadWrite);
16588 multibuffer.push_excerpts(
16589 buffer.clone(),
16590 [
16591 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
16592 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
16593 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
16594 ],
16595 cx,
16596 );
16597 multibuffer
16598 });
16599
16600 let editor =
16601 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
16602 editor
16603 .update(cx, |editor, _window, cx| {
16604 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
16605 editor
16606 .buffer
16607 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
16608 })
16609 .unwrap();
16610
16611 let mut cx = EditorTestContext::for_editor(editor, cx).await;
16612 cx.run_until_parked();
16613
16614 cx.update_editor(|editor, window, cx| {
16615 editor.expand_all_diff_hunks(&Default::default(), window, cx)
16616 });
16617 cx.executor().run_until_parked();
16618
16619 // When the start of a hunk coincides with the start of its excerpt,
16620 // the hunk is expanded. When the start of a a hunk is earlier than
16621 // the start of its excerpt, the hunk is not expanded.
16622 cx.assert_state_with_diff(
16623 "
16624 ˇaaa
16625 - bbb
16626 + BBB
16627
16628 - ddd
16629 - eee
16630 + DDD
16631 + EEE
16632 fff
16633
16634 iii
16635 "
16636 .unindent(),
16637 );
16638}
16639
16640#[gpui::test]
16641async fn test_edits_around_expanded_insertion_hunks(
16642 executor: BackgroundExecutor,
16643 cx: &mut TestAppContext,
16644) {
16645 init_test(cx, |_| {});
16646
16647 let mut cx = EditorTestContext::new(cx).await;
16648
16649 let diff_base = r#"
16650 use some::mod1;
16651 use some::mod2;
16652
16653 const A: u32 = 42;
16654
16655 fn main() {
16656 println!("hello");
16657
16658 println!("world");
16659 }
16660 "#
16661 .unindent();
16662 executor.run_until_parked();
16663 cx.set_state(
16664 &r#"
16665 use some::mod1;
16666 use some::mod2;
16667
16668 const A: u32 = 42;
16669 const B: u32 = 42;
16670 const C: u32 = 42;
16671 ˇ
16672
16673 fn main() {
16674 println!("hello");
16675
16676 println!("world");
16677 }
16678 "#
16679 .unindent(),
16680 );
16681
16682 cx.set_head_text(&diff_base);
16683 executor.run_until_parked();
16684
16685 cx.update_editor(|editor, window, cx| {
16686 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16687 });
16688 executor.run_until_parked();
16689
16690 cx.assert_state_with_diff(
16691 r#"
16692 use some::mod1;
16693 use some::mod2;
16694
16695 const A: u32 = 42;
16696 + const B: u32 = 42;
16697 + const C: u32 = 42;
16698 + ˇ
16699
16700 fn main() {
16701 println!("hello");
16702
16703 println!("world");
16704 }
16705 "#
16706 .unindent(),
16707 );
16708
16709 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
16710 executor.run_until_parked();
16711
16712 cx.assert_state_with_diff(
16713 r#"
16714 use some::mod1;
16715 use some::mod2;
16716
16717 const A: u32 = 42;
16718 + const B: u32 = 42;
16719 + const C: u32 = 42;
16720 + const D: u32 = 42;
16721 + ˇ
16722
16723 fn main() {
16724 println!("hello");
16725
16726 println!("world");
16727 }
16728 "#
16729 .unindent(),
16730 );
16731
16732 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
16733 executor.run_until_parked();
16734
16735 cx.assert_state_with_diff(
16736 r#"
16737 use some::mod1;
16738 use some::mod2;
16739
16740 const A: u32 = 42;
16741 + const B: u32 = 42;
16742 + const C: u32 = 42;
16743 + const D: u32 = 42;
16744 + const E: u32 = 42;
16745 + ˇ
16746
16747 fn main() {
16748 println!("hello");
16749
16750 println!("world");
16751 }
16752 "#
16753 .unindent(),
16754 );
16755
16756 cx.update_editor(|editor, window, cx| {
16757 editor.delete_line(&DeleteLine, window, cx);
16758 });
16759 executor.run_until_parked();
16760
16761 cx.assert_state_with_diff(
16762 r#"
16763 use some::mod1;
16764 use some::mod2;
16765
16766 const A: u32 = 42;
16767 + const B: u32 = 42;
16768 + const C: u32 = 42;
16769 + const D: u32 = 42;
16770 + const E: u32 = 42;
16771 ˇ
16772 fn main() {
16773 println!("hello");
16774
16775 println!("world");
16776 }
16777 "#
16778 .unindent(),
16779 );
16780
16781 cx.update_editor(|editor, window, cx| {
16782 editor.move_up(&MoveUp, window, cx);
16783 editor.delete_line(&DeleteLine, window, cx);
16784 editor.move_up(&MoveUp, window, cx);
16785 editor.delete_line(&DeleteLine, window, cx);
16786 editor.move_up(&MoveUp, window, cx);
16787 editor.delete_line(&DeleteLine, window, cx);
16788 });
16789 executor.run_until_parked();
16790 cx.assert_state_with_diff(
16791 r#"
16792 use some::mod1;
16793 use some::mod2;
16794
16795 const A: u32 = 42;
16796 + const B: u32 = 42;
16797 ˇ
16798 fn main() {
16799 println!("hello");
16800
16801 println!("world");
16802 }
16803 "#
16804 .unindent(),
16805 );
16806
16807 cx.update_editor(|editor, window, cx| {
16808 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
16809 editor.delete_line(&DeleteLine, window, cx);
16810 });
16811 executor.run_until_parked();
16812 cx.assert_state_with_diff(
16813 r#"
16814 ˇ
16815 fn main() {
16816 println!("hello");
16817
16818 println!("world");
16819 }
16820 "#
16821 .unindent(),
16822 );
16823}
16824
16825#[gpui::test]
16826async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
16827 init_test(cx, |_| {});
16828
16829 let mut cx = EditorTestContext::new(cx).await;
16830 cx.set_head_text(indoc! { "
16831 one
16832 two
16833 three
16834 four
16835 five
16836 "
16837 });
16838 cx.set_state(indoc! { "
16839 one
16840 ˇthree
16841 five
16842 "});
16843 cx.run_until_parked();
16844 cx.update_editor(|editor, window, cx| {
16845 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16846 });
16847 cx.assert_state_with_diff(
16848 indoc! { "
16849 one
16850 - two
16851 ˇthree
16852 - four
16853 five
16854 "}
16855 .to_string(),
16856 );
16857 cx.update_editor(|editor, window, cx| {
16858 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16859 });
16860
16861 cx.assert_state_with_diff(
16862 indoc! { "
16863 one
16864 ˇthree
16865 five
16866 "}
16867 .to_string(),
16868 );
16869
16870 cx.set_state(indoc! { "
16871 one
16872 ˇTWO
16873 three
16874 four
16875 five
16876 "});
16877 cx.run_until_parked();
16878 cx.update_editor(|editor, window, cx| {
16879 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16880 });
16881
16882 cx.assert_state_with_diff(
16883 indoc! { "
16884 one
16885 - two
16886 + ˇTWO
16887 three
16888 four
16889 five
16890 "}
16891 .to_string(),
16892 );
16893 cx.update_editor(|editor, window, cx| {
16894 editor.move_up(&Default::default(), window, cx);
16895 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16896 });
16897 cx.assert_state_with_diff(
16898 indoc! { "
16899 one
16900 ˇTWO
16901 three
16902 four
16903 five
16904 "}
16905 .to_string(),
16906 );
16907}
16908
16909#[gpui::test]
16910async fn test_edits_around_expanded_deletion_hunks(
16911 executor: BackgroundExecutor,
16912 cx: &mut TestAppContext,
16913) {
16914 init_test(cx, |_| {});
16915
16916 let mut cx = EditorTestContext::new(cx).await;
16917
16918 let diff_base = r#"
16919 use some::mod1;
16920 use some::mod2;
16921
16922 const A: u32 = 42;
16923 const B: u32 = 42;
16924 const C: u32 = 42;
16925
16926
16927 fn main() {
16928 println!("hello");
16929
16930 println!("world");
16931 }
16932 "#
16933 .unindent();
16934 executor.run_until_parked();
16935 cx.set_state(
16936 &r#"
16937 use some::mod1;
16938 use some::mod2;
16939
16940 ˇconst B: u32 = 42;
16941 const C: u32 = 42;
16942
16943
16944 fn main() {
16945 println!("hello");
16946
16947 println!("world");
16948 }
16949 "#
16950 .unindent(),
16951 );
16952
16953 cx.set_head_text(&diff_base);
16954 executor.run_until_parked();
16955
16956 cx.update_editor(|editor, window, cx| {
16957 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16958 });
16959 executor.run_until_parked();
16960
16961 cx.assert_state_with_diff(
16962 r#"
16963 use some::mod1;
16964 use some::mod2;
16965
16966 - const A: u32 = 42;
16967 ˇconst B: u32 = 42;
16968 const C: u32 = 42;
16969
16970
16971 fn main() {
16972 println!("hello");
16973
16974 println!("world");
16975 }
16976 "#
16977 .unindent(),
16978 );
16979
16980 cx.update_editor(|editor, window, cx| {
16981 editor.delete_line(&DeleteLine, window, cx);
16982 });
16983 executor.run_until_parked();
16984 cx.assert_state_with_diff(
16985 r#"
16986 use some::mod1;
16987 use some::mod2;
16988
16989 - const A: u32 = 42;
16990 - const B: u32 = 42;
16991 ˇconst C: u32 = 42;
16992
16993
16994 fn main() {
16995 println!("hello");
16996
16997 println!("world");
16998 }
16999 "#
17000 .unindent(),
17001 );
17002
17003 cx.update_editor(|editor, window, cx| {
17004 editor.delete_line(&DeleteLine, window, cx);
17005 });
17006 executor.run_until_parked();
17007 cx.assert_state_with_diff(
17008 r#"
17009 use some::mod1;
17010 use some::mod2;
17011
17012 - const A: u32 = 42;
17013 - const B: u32 = 42;
17014 - const C: u32 = 42;
17015 ˇ
17016
17017 fn main() {
17018 println!("hello");
17019
17020 println!("world");
17021 }
17022 "#
17023 .unindent(),
17024 );
17025
17026 cx.update_editor(|editor, window, cx| {
17027 editor.handle_input("replacement", window, cx);
17028 });
17029 executor.run_until_parked();
17030 cx.assert_state_with_diff(
17031 r#"
17032 use some::mod1;
17033 use some::mod2;
17034
17035 - const A: u32 = 42;
17036 - const B: u32 = 42;
17037 - const C: u32 = 42;
17038 -
17039 + replacementˇ
17040
17041 fn main() {
17042 println!("hello");
17043
17044 println!("world");
17045 }
17046 "#
17047 .unindent(),
17048 );
17049}
17050
17051#[gpui::test]
17052async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17053 init_test(cx, |_| {});
17054
17055 let mut cx = EditorTestContext::new(cx).await;
17056
17057 let base_text = r#"
17058 one
17059 two
17060 three
17061 four
17062 five
17063 "#
17064 .unindent();
17065 executor.run_until_parked();
17066 cx.set_state(
17067 &r#"
17068 one
17069 two
17070 fˇour
17071 five
17072 "#
17073 .unindent(),
17074 );
17075
17076 cx.set_head_text(&base_text);
17077 executor.run_until_parked();
17078
17079 cx.update_editor(|editor, window, cx| {
17080 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17081 });
17082 executor.run_until_parked();
17083
17084 cx.assert_state_with_diff(
17085 r#"
17086 one
17087 two
17088 - three
17089 fˇour
17090 five
17091 "#
17092 .unindent(),
17093 );
17094
17095 cx.update_editor(|editor, window, cx| {
17096 editor.backspace(&Backspace, window, cx);
17097 editor.backspace(&Backspace, window, cx);
17098 });
17099 executor.run_until_parked();
17100 cx.assert_state_with_diff(
17101 r#"
17102 one
17103 two
17104 - threeˇ
17105 - four
17106 + our
17107 five
17108 "#
17109 .unindent(),
17110 );
17111}
17112
17113#[gpui::test]
17114async fn test_edit_after_expanded_modification_hunk(
17115 executor: BackgroundExecutor,
17116 cx: &mut TestAppContext,
17117) {
17118 init_test(cx, |_| {});
17119
17120 let mut cx = EditorTestContext::new(cx).await;
17121
17122 let diff_base = r#"
17123 use some::mod1;
17124 use some::mod2;
17125
17126 const A: u32 = 42;
17127 const B: u32 = 42;
17128 const C: u32 = 42;
17129 const D: u32 = 42;
17130
17131
17132 fn main() {
17133 println!("hello");
17134
17135 println!("world");
17136 }"#
17137 .unindent();
17138
17139 cx.set_state(
17140 &r#"
17141 use some::mod1;
17142 use some::mod2;
17143
17144 const A: u32 = 42;
17145 const B: u32 = 42;
17146 const C: u32 = 43ˇ
17147 const D: u32 = 42;
17148
17149
17150 fn main() {
17151 println!("hello");
17152
17153 println!("world");
17154 }"#
17155 .unindent(),
17156 );
17157
17158 cx.set_head_text(&diff_base);
17159 executor.run_until_parked();
17160 cx.update_editor(|editor, window, cx| {
17161 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17162 });
17163 executor.run_until_parked();
17164
17165 cx.assert_state_with_diff(
17166 r#"
17167 use some::mod1;
17168 use some::mod2;
17169
17170 const A: u32 = 42;
17171 const B: u32 = 42;
17172 - const C: u32 = 42;
17173 + const C: u32 = 43ˇ
17174 const D: u32 = 42;
17175
17176
17177 fn main() {
17178 println!("hello");
17179
17180 println!("world");
17181 }"#
17182 .unindent(),
17183 );
17184
17185 cx.update_editor(|editor, window, cx| {
17186 editor.handle_input("\nnew_line\n", window, cx);
17187 });
17188 executor.run_until_parked();
17189
17190 cx.assert_state_with_diff(
17191 r#"
17192 use some::mod1;
17193 use some::mod2;
17194
17195 const A: u32 = 42;
17196 const B: u32 = 42;
17197 - const C: u32 = 42;
17198 + const C: u32 = 43
17199 + new_line
17200 + ˇ
17201 const D: u32 = 42;
17202
17203
17204 fn main() {
17205 println!("hello");
17206
17207 println!("world");
17208 }"#
17209 .unindent(),
17210 );
17211}
17212
17213#[gpui::test]
17214async fn test_stage_and_unstage_added_file_hunk(
17215 executor: BackgroundExecutor,
17216 cx: &mut TestAppContext,
17217) {
17218 init_test(cx, |_| {});
17219
17220 let mut cx = EditorTestContext::new(cx).await;
17221 cx.update_editor(|editor, _, cx| {
17222 editor.set_expand_all_diff_hunks(cx);
17223 });
17224
17225 let working_copy = r#"
17226 ˇfn main() {
17227 println!("hello, world!");
17228 }
17229 "#
17230 .unindent();
17231
17232 cx.set_state(&working_copy);
17233 executor.run_until_parked();
17234
17235 cx.assert_state_with_diff(
17236 r#"
17237 + ˇfn main() {
17238 + println!("hello, world!");
17239 + }
17240 "#
17241 .unindent(),
17242 );
17243 cx.assert_index_text(None);
17244
17245 cx.update_editor(|editor, window, cx| {
17246 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17247 });
17248 executor.run_until_parked();
17249 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
17250 cx.assert_state_with_diff(
17251 r#"
17252 + ˇfn main() {
17253 + println!("hello, world!");
17254 + }
17255 "#
17256 .unindent(),
17257 );
17258
17259 cx.update_editor(|editor, window, cx| {
17260 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17261 });
17262 executor.run_until_parked();
17263 cx.assert_index_text(None);
17264}
17265
17266async fn setup_indent_guides_editor(
17267 text: &str,
17268 cx: &mut TestAppContext,
17269) -> (BufferId, EditorTestContext) {
17270 init_test(cx, |_| {});
17271
17272 let mut cx = EditorTestContext::new(cx).await;
17273
17274 let buffer_id = cx.update_editor(|editor, window, cx| {
17275 editor.set_text(text, window, cx);
17276 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
17277
17278 buffer_ids[0]
17279 });
17280
17281 (buffer_id, cx)
17282}
17283
17284fn assert_indent_guides(
17285 range: Range<u32>,
17286 expected: Vec<IndentGuide>,
17287 active_indices: Option<Vec<usize>>,
17288 cx: &mut EditorTestContext,
17289) {
17290 let indent_guides = cx.update_editor(|editor, window, cx| {
17291 let snapshot = editor.snapshot(window, cx).display_snapshot;
17292 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
17293 editor,
17294 MultiBufferRow(range.start)..MultiBufferRow(range.end),
17295 true,
17296 &snapshot,
17297 cx,
17298 );
17299
17300 indent_guides.sort_by(|a, b| {
17301 a.depth.cmp(&b.depth).then(
17302 a.start_row
17303 .cmp(&b.start_row)
17304 .then(a.end_row.cmp(&b.end_row)),
17305 )
17306 });
17307 indent_guides
17308 });
17309
17310 if let Some(expected) = active_indices {
17311 let active_indices = cx.update_editor(|editor, window, cx| {
17312 let snapshot = editor.snapshot(window, cx).display_snapshot;
17313 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
17314 });
17315
17316 assert_eq!(
17317 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
17318 expected,
17319 "Active indent guide indices do not match"
17320 );
17321 }
17322
17323 assert_eq!(indent_guides, expected, "Indent guides do not match");
17324}
17325
17326fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
17327 IndentGuide {
17328 buffer_id,
17329 start_row: MultiBufferRow(start_row),
17330 end_row: MultiBufferRow(end_row),
17331 depth,
17332 tab_size: 4,
17333 settings: IndentGuideSettings {
17334 enabled: true,
17335 line_width: 1,
17336 active_line_width: 1,
17337 ..Default::default()
17338 },
17339 }
17340}
17341
17342#[gpui::test]
17343async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
17344 let (buffer_id, mut cx) = setup_indent_guides_editor(
17345 &"
17346 fn main() {
17347 let a = 1;
17348 }"
17349 .unindent(),
17350 cx,
17351 )
17352 .await;
17353
17354 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
17355}
17356
17357#[gpui::test]
17358async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
17359 let (buffer_id, mut cx) = setup_indent_guides_editor(
17360 &"
17361 fn main() {
17362 let a = 1;
17363 let b = 2;
17364 }"
17365 .unindent(),
17366 cx,
17367 )
17368 .await;
17369
17370 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
17371}
17372
17373#[gpui::test]
17374async fn test_indent_guide_nested(cx: &mut TestAppContext) {
17375 let (buffer_id, mut cx) = setup_indent_guides_editor(
17376 &"
17377 fn main() {
17378 let a = 1;
17379 if a == 3 {
17380 let b = 2;
17381 } else {
17382 let c = 3;
17383 }
17384 }"
17385 .unindent(),
17386 cx,
17387 )
17388 .await;
17389
17390 assert_indent_guides(
17391 0..8,
17392 vec![
17393 indent_guide(buffer_id, 1, 6, 0),
17394 indent_guide(buffer_id, 3, 3, 1),
17395 indent_guide(buffer_id, 5, 5, 1),
17396 ],
17397 None,
17398 &mut cx,
17399 );
17400}
17401
17402#[gpui::test]
17403async fn test_indent_guide_tab(cx: &mut TestAppContext) {
17404 let (buffer_id, mut cx) = setup_indent_guides_editor(
17405 &"
17406 fn main() {
17407 let a = 1;
17408 let b = 2;
17409 let c = 3;
17410 }"
17411 .unindent(),
17412 cx,
17413 )
17414 .await;
17415
17416 assert_indent_guides(
17417 0..5,
17418 vec![
17419 indent_guide(buffer_id, 1, 3, 0),
17420 indent_guide(buffer_id, 2, 2, 1),
17421 ],
17422 None,
17423 &mut cx,
17424 );
17425}
17426
17427#[gpui::test]
17428async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
17429 let (buffer_id, mut cx) = setup_indent_guides_editor(
17430 &"
17431 fn main() {
17432 let a = 1;
17433
17434 let c = 3;
17435 }"
17436 .unindent(),
17437 cx,
17438 )
17439 .await;
17440
17441 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
17442}
17443
17444#[gpui::test]
17445async fn test_indent_guide_complex(cx: &mut TestAppContext) {
17446 let (buffer_id, mut cx) = setup_indent_guides_editor(
17447 &"
17448 fn main() {
17449 let a = 1;
17450
17451 let c = 3;
17452
17453 if a == 3 {
17454 let b = 2;
17455 } else {
17456 let c = 3;
17457 }
17458 }"
17459 .unindent(),
17460 cx,
17461 )
17462 .await;
17463
17464 assert_indent_guides(
17465 0..11,
17466 vec![
17467 indent_guide(buffer_id, 1, 9, 0),
17468 indent_guide(buffer_id, 6, 6, 1),
17469 indent_guide(buffer_id, 8, 8, 1),
17470 ],
17471 None,
17472 &mut cx,
17473 );
17474}
17475
17476#[gpui::test]
17477async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
17478 let (buffer_id, mut cx) = setup_indent_guides_editor(
17479 &"
17480 fn main() {
17481 let a = 1;
17482
17483 let c = 3;
17484
17485 if a == 3 {
17486 let b = 2;
17487 } else {
17488 let c = 3;
17489 }
17490 }"
17491 .unindent(),
17492 cx,
17493 )
17494 .await;
17495
17496 assert_indent_guides(
17497 1..11,
17498 vec![
17499 indent_guide(buffer_id, 1, 9, 0),
17500 indent_guide(buffer_id, 6, 6, 1),
17501 indent_guide(buffer_id, 8, 8, 1),
17502 ],
17503 None,
17504 &mut cx,
17505 );
17506}
17507
17508#[gpui::test]
17509async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
17510 let (buffer_id, mut cx) = setup_indent_guides_editor(
17511 &"
17512 fn main() {
17513 let a = 1;
17514
17515 let c = 3;
17516
17517 if a == 3 {
17518 let b = 2;
17519 } else {
17520 let c = 3;
17521 }
17522 }"
17523 .unindent(),
17524 cx,
17525 )
17526 .await;
17527
17528 assert_indent_guides(
17529 1..10,
17530 vec![
17531 indent_guide(buffer_id, 1, 9, 0),
17532 indent_guide(buffer_id, 6, 6, 1),
17533 indent_guide(buffer_id, 8, 8, 1),
17534 ],
17535 None,
17536 &mut cx,
17537 );
17538}
17539
17540#[gpui::test]
17541async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
17542 let (buffer_id, mut cx) = setup_indent_guides_editor(
17543 &"
17544 fn main() {
17545 if a {
17546 b(
17547 c,
17548 d,
17549 )
17550 } else {
17551 e(
17552 f
17553 )
17554 }
17555 }"
17556 .unindent(),
17557 cx,
17558 )
17559 .await;
17560
17561 assert_indent_guides(
17562 0..11,
17563 vec![
17564 indent_guide(buffer_id, 1, 10, 0),
17565 indent_guide(buffer_id, 2, 5, 1),
17566 indent_guide(buffer_id, 7, 9, 1),
17567 indent_guide(buffer_id, 3, 4, 2),
17568 indent_guide(buffer_id, 8, 8, 2),
17569 ],
17570 None,
17571 &mut cx,
17572 );
17573
17574 cx.update_editor(|editor, window, cx| {
17575 editor.fold_at(MultiBufferRow(2), window, cx);
17576 assert_eq!(
17577 editor.display_text(cx),
17578 "
17579 fn main() {
17580 if a {
17581 b(⋯
17582 )
17583 } else {
17584 e(
17585 f
17586 )
17587 }
17588 }"
17589 .unindent()
17590 );
17591 });
17592
17593 assert_indent_guides(
17594 0..11,
17595 vec![
17596 indent_guide(buffer_id, 1, 10, 0),
17597 indent_guide(buffer_id, 2, 5, 1),
17598 indent_guide(buffer_id, 7, 9, 1),
17599 indent_guide(buffer_id, 8, 8, 2),
17600 ],
17601 None,
17602 &mut cx,
17603 );
17604}
17605
17606#[gpui::test]
17607async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
17608 let (buffer_id, mut cx) = setup_indent_guides_editor(
17609 &"
17610 block1
17611 block2
17612 block3
17613 block4
17614 block2
17615 block1
17616 block1"
17617 .unindent(),
17618 cx,
17619 )
17620 .await;
17621
17622 assert_indent_guides(
17623 1..10,
17624 vec![
17625 indent_guide(buffer_id, 1, 4, 0),
17626 indent_guide(buffer_id, 2, 3, 1),
17627 indent_guide(buffer_id, 3, 3, 2),
17628 ],
17629 None,
17630 &mut cx,
17631 );
17632}
17633
17634#[gpui::test]
17635async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
17636 let (buffer_id, mut cx) = setup_indent_guides_editor(
17637 &"
17638 block1
17639 block2
17640 block3
17641
17642 block1
17643 block1"
17644 .unindent(),
17645 cx,
17646 )
17647 .await;
17648
17649 assert_indent_guides(
17650 0..6,
17651 vec![
17652 indent_guide(buffer_id, 1, 2, 0),
17653 indent_guide(buffer_id, 2, 2, 1),
17654 ],
17655 None,
17656 &mut cx,
17657 );
17658}
17659
17660#[gpui::test]
17661async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
17662 let (buffer_id, mut cx) = setup_indent_guides_editor(
17663 &"
17664 function component() {
17665 \treturn (
17666 \t\t\t
17667 \t\t<div>
17668 \t\t\t<abc></abc>
17669 \t\t</div>
17670 \t)
17671 }"
17672 .unindent(),
17673 cx,
17674 )
17675 .await;
17676
17677 assert_indent_guides(
17678 0..8,
17679 vec![
17680 indent_guide(buffer_id, 1, 6, 0),
17681 indent_guide(buffer_id, 2, 5, 1),
17682 indent_guide(buffer_id, 4, 4, 2),
17683 ],
17684 None,
17685 &mut cx,
17686 );
17687}
17688
17689#[gpui::test]
17690async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
17691 let (buffer_id, mut cx) = setup_indent_guides_editor(
17692 &"
17693 function component() {
17694 \treturn (
17695 \t
17696 \t\t<div>
17697 \t\t\t<abc></abc>
17698 \t\t</div>
17699 \t)
17700 }"
17701 .unindent(),
17702 cx,
17703 )
17704 .await;
17705
17706 assert_indent_guides(
17707 0..8,
17708 vec![
17709 indent_guide(buffer_id, 1, 6, 0),
17710 indent_guide(buffer_id, 2, 5, 1),
17711 indent_guide(buffer_id, 4, 4, 2),
17712 ],
17713 None,
17714 &mut cx,
17715 );
17716}
17717
17718#[gpui::test]
17719async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
17720 let (buffer_id, mut cx) = setup_indent_guides_editor(
17721 &"
17722 block1
17723
17724
17725
17726 block2
17727 "
17728 .unindent(),
17729 cx,
17730 )
17731 .await;
17732
17733 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
17734}
17735
17736#[gpui::test]
17737async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
17738 let (buffer_id, mut cx) = setup_indent_guides_editor(
17739 &"
17740 def a:
17741 \tb = 3
17742 \tif True:
17743 \t\tc = 4
17744 \t\td = 5
17745 \tprint(b)
17746 "
17747 .unindent(),
17748 cx,
17749 )
17750 .await;
17751
17752 assert_indent_guides(
17753 0..6,
17754 vec![
17755 indent_guide(buffer_id, 1, 5, 0),
17756 indent_guide(buffer_id, 3, 4, 1),
17757 ],
17758 None,
17759 &mut cx,
17760 );
17761}
17762
17763#[gpui::test]
17764async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
17765 let (buffer_id, mut cx) = setup_indent_guides_editor(
17766 &"
17767 fn main() {
17768 let a = 1;
17769 }"
17770 .unindent(),
17771 cx,
17772 )
17773 .await;
17774
17775 cx.update_editor(|editor, window, cx| {
17776 editor.change_selections(None, window, cx, |s| {
17777 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17778 });
17779 });
17780
17781 assert_indent_guides(
17782 0..3,
17783 vec![indent_guide(buffer_id, 1, 1, 0)],
17784 Some(vec![0]),
17785 &mut cx,
17786 );
17787}
17788
17789#[gpui::test]
17790async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
17791 let (buffer_id, mut cx) = setup_indent_guides_editor(
17792 &"
17793 fn main() {
17794 if 1 == 2 {
17795 let a = 1;
17796 }
17797 }"
17798 .unindent(),
17799 cx,
17800 )
17801 .await;
17802
17803 cx.update_editor(|editor, window, cx| {
17804 editor.change_selections(None, window, cx, |s| {
17805 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17806 });
17807 });
17808
17809 assert_indent_guides(
17810 0..4,
17811 vec![
17812 indent_guide(buffer_id, 1, 3, 0),
17813 indent_guide(buffer_id, 2, 2, 1),
17814 ],
17815 Some(vec![1]),
17816 &mut cx,
17817 );
17818
17819 cx.update_editor(|editor, window, cx| {
17820 editor.change_selections(None, window, cx, |s| {
17821 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
17822 });
17823 });
17824
17825 assert_indent_guides(
17826 0..4,
17827 vec![
17828 indent_guide(buffer_id, 1, 3, 0),
17829 indent_guide(buffer_id, 2, 2, 1),
17830 ],
17831 Some(vec![1]),
17832 &mut cx,
17833 );
17834
17835 cx.update_editor(|editor, window, cx| {
17836 editor.change_selections(None, window, cx, |s| {
17837 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
17838 });
17839 });
17840
17841 assert_indent_guides(
17842 0..4,
17843 vec![
17844 indent_guide(buffer_id, 1, 3, 0),
17845 indent_guide(buffer_id, 2, 2, 1),
17846 ],
17847 Some(vec![0]),
17848 &mut cx,
17849 );
17850}
17851
17852#[gpui::test]
17853async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
17854 let (buffer_id, mut cx) = setup_indent_guides_editor(
17855 &"
17856 fn main() {
17857 let a = 1;
17858
17859 let b = 2;
17860 }"
17861 .unindent(),
17862 cx,
17863 )
17864 .await;
17865
17866 cx.update_editor(|editor, window, cx| {
17867 editor.change_selections(None, window, cx, |s| {
17868 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
17869 });
17870 });
17871
17872 assert_indent_guides(
17873 0..5,
17874 vec![indent_guide(buffer_id, 1, 3, 0)],
17875 Some(vec![0]),
17876 &mut cx,
17877 );
17878}
17879
17880#[gpui::test]
17881async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
17882 let (buffer_id, mut cx) = setup_indent_guides_editor(
17883 &"
17884 def m:
17885 a = 1
17886 pass"
17887 .unindent(),
17888 cx,
17889 )
17890 .await;
17891
17892 cx.update_editor(|editor, window, cx| {
17893 editor.change_selections(None, window, cx, |s| {
17894 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17895 });
17896 });
17897
17898 assert_indent_guides(
17899 0..3,
17900 vec![indent_guide(buffer_id, 1, 2, 0)],
17901 Some(vec![0]),
17902 &mut cx,
17903 );
17904}
17905
17906#[gpui::test]
17907async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
17908 init_test(cx, |_| {});
17909 let mut cx = EditorTestContext::new(cx).await;
17910 let text = indoc! {
17911 "
17912 impl A {
17913 fn b() {
17914 0;
17915 3;
17916 5;
17917 6;
17918 7;
17919 }
17920 }
17921 "
17922 };
17923 let base_text = indoc! {
17924 "
17925 impl A {
17926 fn b() {
17927 0;
17928 1;
17929 2;
17930 3;
17931 4;
17932 }
17933 fn c() {
17934 5;
17935 6;
17936 7;
17937 }
17938 }
17939 "
17940 };
17941
17942 cx.update_editor(|editor, window, cx| {
17943 editor.set_text(text, window, cx);
17944
17945 editor.buffer().update(cx, |multibuffer, cx| {
17946 let buffer = multibuffer.as_singleton().unwrap();
17947 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
17948
17949 multibuffer.set_all_diff_hunks_expanded(cx);
17950 multibuffer.add_diff(diff, cx);
17951
17952 buffer.read(cx).remote_id()
17953 })
17954 });
17955 cx.run_until_parked();
17956
17957 cx.assert_state_with_diff(
17958 indoc! { "
17959 impl A {
17960 fn b() {
17961 0;
17962 - 1;
17963 - 2;
17964 3;
17965 - 4;
17966 - }
17967 - fn c() {
17968 5;
17969 6;
17970 7;
17971 }
17972 }
17973 ˇ"
17974 }
17975 .to_string(),
17976 );
17977
17978 let mut actual_guides = cx.update_editor(|editor, window, cx| {
17979 editor
17980 .snapshot(window, cx)
17981 .buffer_snapshot
17982 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
17983 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
17984 .collect::<Vec<_>>()
17985 });
17986 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
17987 assert_eq!(
17988 actual_guides,
17989 vec![
17990 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
17991 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
17992 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
17993 ]
17994 );
17995}
17996
17997#[gpui::test]
17998async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17999 init_test(cx, |_| {});
18000 let mut cx = EditorTestContext::new(cx).await;
18001
18002 let diff_base = r#"
18003 a
18004 b
18005 c
18006 "#
18007 .unindent();
18008
18009 cx.set_state(
18010 &r#"
18011 ˇA
18012 b
18013 C
18014 "#
18015 .unindent(),
18016 );
18017 cx.set_head_text(&diff_base);
18018 cx.update_editor(|editor, window, cx| {
18019 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18020 });
18021 executor.run_until_parked();
18022
18023 let both_hunks_expanded = r#"
18024 - a
18025 + ˇA
18026 b
18027 - c
18028 + C
18029 "#
18030 .unindent();
18031
18032 cx.assert_state_with_diff(both_hunks_expanded.clone());
18033
18034 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18035 let snapshot = editor.snapshot(window, cx);
18036 let hunks = editor
18037 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18038 .collect::<Vec<_>>();
18039 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18040 let buffer_id = hunks[0].buffer_id;
18041 hunks
18042 .into_iter()
18043 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18044 .collect::<Vec<_>>()
18045 });
18046 assert_eq!(hunk_ranges.len(), 2);
18047
18048 cx.update_editor(|editor, _, cx| {
18049 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18050 });
18051 executor.run_until_parked();
18052
18053 let second_hunk_expanded = r#"
18054 ˇA
18055 b
18056 - c
18057 + C
18058 "#
18059 .unindent();
18060
18061 cx.assert_state_with_diff(second_hunk_expanded);
18062
18063 cx.update_editor(|editor, _, cx| {
18064 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18065 });
18066 executor.run_until_parked();
18067
18068 cx.assert_state_with_diff(both_hunks_expanded.clone());
18069
18070 cx.update_editor(|editor, _, cx| {
18071 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18072 });
18073 executor.run_until_parked();
18074
18075 let first_hunk_expanded = r#"
18076 - a
18077 + ˇA
18078 b
18079 C
18080 "#
18081 .unindent();
18082
18083 cx.assert_state_with_diff(first_hunk_expanded);
18084
18085 cx.update_editor(|editor, _, cx| {
18086 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18087 });
18088 executor.run_until_parked();
18089
18090 cx.assert_state_with_diff(both_hunks_expanded);
18091
18092 cx.set_state(
18093 &r#"
18094 ˇA
18095 b
18096 "#
18097 .unindent(),
18098 );
18099 cx.run_until_parked();
18100
18101 // TODO this cursor position seems bad
18102 cx.assert_state_with_diff(
18103 r#"
18104 - ˇa
18105 + A
18106 b
18107 "#
18108 .unindent(),
18109 );
18110
18111 cx.update_editor(|editor, window, cx| {
18112 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18113 });
18114
18115 cx.assert_state_with_diff(
18116 r#"
18117 - ˇa
18118 + A
18119 b
18120 - c
18121 "#
18122 .unindent(),
18123 );
18124
18125 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18126 let snapshot = editor.snapshot(window, cx);
18127 let hunks = editor
18128 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18129 .collect::<Vec<_>>();
18130 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18131 let buffer_id = hunks[0].buffer_id;
18132 hunks
18133 .into_iter()
18134 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18135 .collect::<Vec<_>>()
18136 });
18137 assert_eq!(hunk_ranges.len(), 2);
18138
18139 cx.update_editor(|editor, _, cx| {
18140 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18141 });
18142 executor.run_until_parked();
18143
18144 cx.assert_state_with_diff(
18145 r#"
18146 - ˇa
18147 + A
18148 b
18149 "#
18150 .unindent(),
18151 );
18152}
18153
18154#[gpui::test]
18155async fn test_toggle_deletion_hunk_at_start_of_file(
18156 executor: BackgroundExecutor,
18157 cx: &mut TestAppContext,
18158) {
18159 init_test(cx, |_| {});
18160 let mut cx = EditorTestContext::new(cx).await;
18161
18162 let diff_base = r#"
18163 a
18164 b
18165 c
18166 "#
18167 .unindent();
18168
18169 cx.set_state(
18170 &r#"
18171 ˇb
18172 c
18173 "#
18174 .unindent(),
18175 );
18176 cx.set_head_text(&diff_base);
18177 cx.update_editor(|editor, window, cx| {
18178 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18179 });
18180 executor.run_until_parked();
18181
18182 let hunk_expanded = r#"
18183 - a
18184 ˇb
18185 c
18186 "#
18187 .unindent();
18188
18189 cx.assert_state_with_diff(hunk_expanded.clone());
18190
18191 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18192 let snapshot = editor.snapshot(window, cx);
18193 let hunks = editor
18194 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18195 .collect::<Vec<_>>();
18196 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18197 let buffer_id = hunks[0].buffer_id;
18198 hunks
18199 .into_iter()
18200 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18201 .collect::<Vec<_>>()
18202 });
18203 assert_eq!(hunk_ranges.len(), 1);
18204
18205 cx.update_editor(|editor, _, cx| {
18206 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18207 });
18208 executor.run_until_parked();
18209
18210 let hunk_collapsed = r#"
18211 ˇb
18212 c
18213 "#
18214 .unindent();
18215
18216 cx.assert_state_with_diff(hunk_collapsed);
18217
18218 cx.update_editor(|editor, _, cx| {
18219 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18220 });
18221 executor.run_until_parked();
18222
18223 cx.assert_state_with_diff(hunk_expanded.clone());
18224}
18225
18226#[gpui::test]
18227async fn test_display_diff_hunks(cx: &mut TestAppContext) {
18228 init_test(cx, |_| {});
18229
18230 let fs = FakeFs::new(cx.executor());
18231 fs.insert_tree(
18232 path!("/test"),
18233 json!({
18234 ".git": {},
18235 "file-1": "ONE\n",
18236 "file-2": "TWO\n",
18237 "file-3": "THREE\n",
18238 }),
18239 )
18240 .await;
18241
18242 fs.set_head_for_repo(
18243 path!("/test/.git").as_ref(),
18244 &[
18245 ("file-1".into(), "one\n".into()),
18246 ("file-2".into(), "two\n".into()),
18247 ("file-3".into(), "three\n".into()),
18248 ],
18249 "deadbeef",
18250 );
18251
18252 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
18253 let mut buffers = vec![];
18254 for i in 1..=3 {
18255 let buffer = project
18256 .update(cx, |project, cx| {
18257 let path = format!(path!("/test/file-{}"), i);
18258 project.open_local_buffer(path, cx)
18259 })
18260 .await
18261 .unwrap();
18262 buffers.push(buffer);
18263 }
18264
18265 let multibuffer = cx.new(|cx| {
18266 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
18267 multibuffer.set_all_diff_hunks_expanded(cx);
18268 for buffer in &buffers {
18269 let snapshot = buffer.read(cx).snapshot();
18270 multibuffer.set_excerpts_for_path(
18271 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
18272 buffer.clone(),
18273 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
18274 DEFAULT_MULTIBUFFER_CONTEXT,
18275 cx,
18276 );
18277 }
18278 multibuffer
18279 });
18280
18281 let editor = cx.add_window(|window, cx| {
18282 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
18283 });
18284 cx.run_until_parked();
18285
18286 let snapshot = editor
18287 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18288 .unwrap();
18289 let hunks = snapshot
18290 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
18291 .map(|hunk| match hunk {
18292 DisplayDiffHunk::Unfolded {
18293 display_row_range, ..
18294 } => display_row_range,
18295 DisplayDiffHunk::Folded { .. } => unreachable!(),
18296 })
18297 .collect::<Vec<_>>();
18298 assert_eq!(
18299 hunks,
18300 [
18301 DisplayRow(2)..DisplayRow(4),
18302 DisplayRow(7)..DisplayRow(9),
18303 DisplayRow(12)..DisplayRow(14),
18304 ]
18305 );
18306}
18307
18308#[gpui::test]
18309async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
18310 init_test(cx, |_| {});
18311
18312 let mut cx = EditorTestContext::new(cx).await;
18313 cx.set_head_text(indoc! { "
18314 one
18315 two
18316 three
18317 four
18318 five
18319 "
18320 });
18321 cx.set_index_text(indoc! { "
18322 one
18323 two
18324 three
18325 four
18326 five
18327 "
18328 });
18329 cx.set_state(indoc! {"
18330 one
18331 TWO
18332 ˇTHREE
18333 FOUR
18334 five
18335 "});
18336 cx.run_until_parked();
18337 cx.update_editor(|editor, window, cx| {
18338 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18339 });
18340 cx.run_until_parked();
18341 cx.assert_index_text(Some(indoc! {"
18342 one
18343 TWO
18344 THREE
18345 FOUR
18346 five
18347 "}));
18348 cx.set_state(indoc! { "
18349 one
18350 TWO
18351 ˇTHREE-HUNDRED
18352 FOUR
18353 five
18354 "});
18355 cx.run_until_parked();
18356 cx.update_editor(|editor, window, cx| {
18357 let snapshot = editor.snapshot(window, cx);
18358 let hunks = editor
18359 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18360 .collect::<Vec<_>>();
18361 assert_eq!(hunks.len(), 1);
18362 assert_eq!(
18363 hunks[0].status(),
18364 DiffHunkStatus {
18365 kind: DiffHunkStatusKind::Modified,
18366 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
18367 }
18368 );
18369
18370 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18371 });
18372 cx.run_until_parked();
18373 cx.assert_index_text(Some(indoc! {"
18374 one
18375 TWO
18376 THREE-HUNDRED
18377 FOUR
18378 five
18379 "}));
18380}
18381
18382#[gpui::test]
18383fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
18384 init_test(cx, |_| {});
18385
18386 let editor = cx.add_window(|window, cx| {
18387 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
18388 build_editor(buffer, window, cx)
18389 });
18390
18391 let render_args = Arc::new(Mutex::new(None));
18392 let snapshot = editor
18393 .update(cx, |editor, window, cx| {
18394 let snapshot = editor.buffer().read(cx).snapshot(cx);
18395 let range =
18396 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
18397
18398 struct RenderArgs {
18399 row: MultiBufferRow,
18400 folded: bool,
18401 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
18402 }
18403
18404 let crease = Crease::inline(
18405 range,
18406 FoldPlaceholder::test(),
18407 {
18408 let toggle_callback = render_args.clone();
18409 move |row, folded, callback, _window, _cx| {
18410 *toggle_callback.lock() = Some(RenderArgs {
18411 row,
18412 folded,
18413 callback,
18414 });
18415 div()
18416 }
18417 },
18418 |_row, _folded, _window, _cx| div(),
18419 );
18420
18421 editor.insert_creases(Some(crease), cx);
18422 let snapshot = editor.snapshot(window, cx);
18423 let _div = snapshot.render_crease_toggle(
18424 MultiBufferRow(1),
18425 false,
18426 cx.entity().clone(),
18427 window,
18428 cx,
18429 );
18430 snapshot
18431 })
18432 .unwrap();
18433
18434 let render_args = render_args.lock().take().unwrap();
18435 assert_eq!(render_args.row, MultiBufferRow(1));
18436 assert!(!render_args.folded);
18437 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
18438
18439 cx.update_window(*editor, |_, window, cx| {
18440 (render_args.callback)(true, window, cx)
18441 })
18442 .unwrap();
18443 let snapshot = editor
18444 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18445 .unwrap();
18446 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
18447
18448 cx.update_window(*editor, |_, window, cx| {
18449 (render_args.callback)(false, window, cx)
18450 })
18451 .unwrap();
18452 let snapshot = editor
18453 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18454 .unwrap();
18455 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
18456}
18457
18458#[gpui::test]
18459async fn test_input_text(cx: &mut TestAppContext) {
18460 init_test(cx, |_| {});
18461 let mut cx = EditorTestContext::new(cx).await;
18462
18463 cx.set_state(
18464 &r#"ˇone
18465 two
18466
18467 three
18468 fourˇ
18469 five
18470
18471 siˇx"#
18472 .unindent(),
18473 );
18474
18475 cx.dispatch_action(HandleInput(String::new()));
18476 cx.assert_editor_state(
18477 &r#"ˇone
18478 two
18479
18480 three
18481 fourˇ
18482 five
18483
18484 siˇx"#
18485 .unindent(),
18486 );
18487
18488 cx.dispatch_action(HandleInput("AAAA".to_string()));
18489 cx.assert_editor_state(
18490 &r#"AAAAˇone
18491 two
18492
18493 three
18494 fourAAAAˇ
18495 five
18496
18497 siAAAAˇx"#
18498 .unindent(),
18499 );
18500}
18501
18502#[gpui::test]
18503async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
18504 init_test(cx, |_| {});
18505
18506 let mut cx = EditorTestContext::new(cx).await;
18507 cx.set_state(
18508 r#"let foo = 1;
18509let foo = 2;
18510let foo = 3;
18511let fooˇ = 4;
18512let foo = 5;
18513let foo = 6;
18514let foo = 7;
18515let foo = 8;
18516let foo = 9;
18517let foo = 10;
18518let foo = 11;
18519let foo = 12;
18520let foo = 13;
18521let foo = 14;
18522let foo = 15;"#,
18523 );
18524
18525 cx.update_editor(|e, window, cx| {
18526 assert_eq!(
18527 e.next_scroll_position,
18528 NextScrollCursorCenterTopBottom::Center,
18529 "Default next scroll direction is center",
18530 );
18531
18532 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18533 assert_eq!(
18534 e.next_scroll_position,
18535 NextScrollCursorCenterTopBottom::Top,
18536 "After center, next scroll direction should be top",
18537 );
18538
18539 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18540 assert_eq!(
18541 e.next_scroll_position,
18542 NextScrollCursorCenterTopBottom::Bottom,
18543 "After top, next scroll direction should be bottom",
18544 );
18545
18546 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18547 assert_eq!(
18548 e.next_scroll_position,
18549 NextScrollCursorCenterTopBottom::Center,
18550 "After bottom, scrolling should start over",
18551 );
18552
18553 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18554 assert_eq!(
18555 e.next_scroll_position,
18556 NextScrollCursorCenterTopBottom::Top,
18557 "Scrolling continues if retriggered fast enough"
18558 );
18559 });
18560
18561 cx.executor()
18562 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
18563 cx.executor().run_until_parked();
18564 cx.update_editor(|e, _, _| {
18565 assert_eq!(
18566 e.next_scroll_position,
18567 NextScrollCursorCenterTopBottom::Center,
18568 "If scrolling is not triggered fast enough, it should reset"
18569 );
18570 });
18571}
18572
18573#[gpui::test]
18574async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
18575 init_test(cx, |_| {});
18576 let mut cx = EditorLspTestContext::new_rust(
18577 lsp::ServerCapabilities {
18578 definition_provider: Some(lsp::OneOf::Left(true)),
18579 references_provider: Some(lsp::OneOf::Left(true)),
18580 ..lsp::ServerCapabilities::default()
18581 },
18582 cx,
18583 )
18584 .await;
18585
18586 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
18587 let go_to_definition = cx
18588 .lsp
18589 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
18590 move |params, _| async move {
18591 if empty_go_to_definition {
18592 Ok(None)
18593 } else {
18594 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
18595 uri: params.text_document_position_params.text_document.uri,
18596 range: lsp::Range::new(
18597 lsp::Position::new(4, 3),
18598 lsp::Position::new(4, 6),
18599 ),
18600 })))
18601 }
18602 },
18603 );
18604 let references = cx
18605 .lsp
18606 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
18607 Ok(Some(vec![lsp::Location {
18608 uri: params.text_document_position.text_document.uri,
18609 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
18610 }]))
18611 });
18612 (go_to_definition, references)
18613 };
18614
18615 cx.set_state(
18616 &r#"fn one() {
18617 let mut a = ˇtwo();
18618 }
18619
18620 fn two() {}"#
18621 .unindent(),
18622 );
18623 set_up_lsp_handlers(false, &mut cx);
18624 let navigated = cx
18625 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
18626 .await
18627 .expect("Failed to navigate to definition");
18628 assert_eq!(
18629 navigated,
18630 Navigated::Yes,
18631 "Should have navigated to definition from the GetDefinition response"
18632 );
18633 cx.assert_editor_state(
18634 &r#"fn one() {
18635 let mut a = two();
18636 }
18637
18638 fn «twoˇ»() {}"#
18639 .unindent(),
18640 );
18641
18642 let editors = cx.update_workspace(|workspace, _, cx| {
18643 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
18644 });
18645 cx.update_editor(|_, _, test_editor_cx| {
18646 assert_eq!(
18647 editors.len(),
18648 1,
18649 "Initially, only one, test, editor should be open in the workspace"
18650 );
18651 assert_eq!(
18652 test_editor_cx.entity(),
18653 editors.last().expect("Asserted len is 1").clone()
18654 );
18655 });
18656
18657 set_up_lsp_handlers(true, &mut cx);
18658 let navigated = cx
18659 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
18660 .await
18661 .expect("Failed to navigate to lookup references");
18662 assert_eq!(
18663 navigated,
18664 Navigated::Yes,
18665 "Should have navigated to references as a fallback after empty GoToDefinition response"
18666 );
18667 // We should not change the selections in the existing file,
18668 // if opening another milti buffer with the references
18669 cx.assert_editor_state(
18670 &r#"fn one() {
18671 let mut a = two();
18672 }
18673
18674 fn «twoˇ»() {}"#
18675 .unindent(),
18676 );
18677 let editors = cx.update_workspace(|workspace, _, cx| {
18678 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
18679 });
18680 cx.update_editor(|_, _, test_editor_cx| {
18681 assert_eq!(
18682 editors.len(),
18683 2,
18684 "After falling back to references search, we open a new editor with the results"
18685 );
18686 let references_fallback_text = editors
18687 .into_iter()
18688 .find(|new_editor| *new_editor != test_editor_cx.entity())
18689 .expect("Should have one non-test editor now")
18690 .read(test_editor_cx)
18691 .text(test_editor_cx);
18692 assert_eq!(
18693 references_fallback_text, "fn one() {\n let mut a = two();\n}",
18694 "Should use the range from the references response and not the GoToDefinition one"
18695 );
18696 });
18697}
18698
18699#[gpui::test]
18700async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
18701 init_test(cx, |_| {});
18702 cx.update(|cx| {
18703 let mut editor_settings = EditorSettings::get_global(cx).clone();
18704 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
18705 EditorSettings::override_global(editor_settings, cx);
18706 });
18707 let mut cx = EditorLspTestContext::new_rust(
18708 lsp::ServerCapabilities {
18709 definition_provider: Some(lsp::OneOf::Left(true)),
18710 references_provider: Some(lsp::OneOf::Left(true)),
18711 ..lsp::ServerCapabilities::default()
18712 },
18713 cx,
18714 )
18715 .await;
18716 let original_state = r#"fn one() {
18717 let mut a = ˇtwo();
18718 }
18719
18720 fn two() {}"#
18721 .unindent();
18722 cx.set_state(&original_state);
18723
18724 let mut go_to_definition = cx
18725 .lsp
18726 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
18727 move |_, _| async move { Ok(None) },
18728 );
18729 let _references = cx
18730 .lsp
18731 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
18732 panic!("Should not call for references with no go to definition fallback")
18733 });
18734
18735 let navigated = cx
18736 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
18737 .await
18738 .expect("Failed to navigate to lookup references");
18739 go_to_definition
18740 .next()
18741 .await
18742 .expect("Should have called the go_to_definition handler");
18743
18744 assert_eq!(
18745 navigated,
18746 Navigated::No,
18747 "Should have navigated to references as a fallback after empty GoToDefinition response"
18748 );
18749 cx.assert_editor_state(&original_state);
18750 let editors = cx.update_workspace(|workspace, _, cx| {
18751 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
18752 });
18753 cx.update_editor(|_, _, _| {
18754 assert_eq!(
18755 editors.len(),
18756 1,
18757 "After unsuccessful fallback, no other editor should have been opened"
18758 );
18759 });
18760}
18761
18762#[gpui::test]
18763async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
18764 init_test(cx, |_| {});
18765
18766 let language = Arc::new(Language::new(
18767 LanguageConfig::default(),
18768 Some(tree_sitter_rust::LANGUAGE.into()),
18769 ));
18770
18771 let text = r#"
18772 #[cfg(test)]
18773 mod tests() {
18774 #[test]
18775 fn runnable_1() {
18776 let a = 1;
18777 }
18778
18779 #[test]
18780 fn runnable_2() {
18781 let a = 1;
18782 let b = 2;
18783 }
18784 }
18785 "#
18786 .unindent();
18787
18788 let fs = FakeFs::new(cx.executor());
18789 fs.insert_file("/file.rs", Default::default()).await;
18790
18791 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18792 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18793 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18794 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
18795 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
18796
18797 let editor = cx.new_window_entity(|window, cx| {
18798 Editor::new(
18799 EditorMode::full(),
18800 multi_buffer,
18801 Some(project.clone()),
18802 window,
18803 cx,
18804 )
18805 });
18806
18807 editor.update_in(cx, |editor, window, cx| {
18808 let snapshot = editor.buffer().read(cx).snapshot(cx);
18809 editor.tasks.insert(
18810 (buffer.read(cx).remote_id(), 3),
18811 RunnableTasks {
18812 templates: vec![],
18813 offset: snapshot.anchor_before(43),
18814 column: 0,
18815 extra_variables: HashMap::default(),
18816 context_range: BufferOffset(43)..BufferOffset(85),
18817 },
18818 );
18819 editor.tasks.insert(
18820 (buffer.read(cx).remote_id(), 8),
18821 RunnableTasks {
18822 templates: vec![],
18823 offset: snapshot.anchor_before(86),
18824 column: 0,
18825 extra_variables: HashMap::default(),
18826 context_range: BufferOffset(86)..BufferOffset(191),
18827 },
18828 );
18829
18830 // Test finding task when cursor is inside function body
18831 editor.change_selections(None, window, cx, |s| {
18832 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
18833 });
18834 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
18835 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
18836
18837 // Test finding task when cursor is on function name
18838 editor.change_selections(None, window, cx, |s| {
18839 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
18840 });
18841 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
18842 assert_eq!(row, 8, "Should find task when cursor is on function name");
18843 });
18844}
18845
18846#[gpui::test]
18847async fn test_folding_buffers(cx: &mut TestAppContext) {
18848 init_test(cx, |_| {});
18849
18850 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
18851 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
18852 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
18853
18854 let fs = FakeFs::new(cx.executor());
18855 fs.insert_tree(
18856 path!("/a"),
18857 json!({
18858 "first.rs": sample_text_1,
18859 "second.rs": sample_text_2,
18860 "third.rs": sample_text_3,
18861 }),
18862 )
18863 .await;
18864 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18865 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18866 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18867 let worktree = project.update(cx, |project, cx| {
18868 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18869 assert_eq!(worktrees.len(), 1);
18870 worktrees.pop().unwrap()
18871 });
18872 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18873
18874 let buffer_1 = project
18875 .update(cx, |project, cx| {
18876 project.open_buffer((worktree_id, "first.rs"), cx)
18877 })
18878 .await
18879 .unwrap();
18880 let buffer_2 = project
18881 .update(cx, |project, cx| {
18882 project.open_buffer((worktree_id, "second.rs"), cx)
18883 })
18884 .await
18885 .unwrap();
18886 let buffer_3 = project
18887 .update(cx, |project, cx| {
18888 project.open_buffer((worktree_id, "third.rs"), cx)
18889 })
18890 .await
18891 .unwrap();
18892
18893 let multi_buffer = cx.new(|cx| {
18894 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18895 multi_buffer.push_excerpts(
18896 buffer_1.clone(),
18897 [
18898 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18899 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18900 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18901 ],
18902 cx,
18903 );
18904 multi_buffer.push_excerpts(
18905 buffer_2.clone(),
18906 [
18907 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18908 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18909 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18910 ],
18911 cx,
18912 );
18913 multi_buffer.push_excerpts(
18914 buffer_3.clone(),
18915 [
18916 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18917 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18918 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18919 ],
18920 cx,
18921 );
18922 multi_buffer
18923 });
18924 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18925 Editor::new(
18926 EditorMode::full(),
18927 multi_buffer.clone(),
18928 Some(project.clone()),
18929 window,
18930 cx,
18931 )
18932 });
18933
18934 assert_eq!(
18935 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18936 "\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",
18937 );
18938
18939 multi_buffer_editor.update(cx, |editor, cx| {
18940 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
18941 });
18942 assert_eq!(
18943 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18944 "\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",
18945 "After folding the first buffer, its text should not be displayed"
18946 );
18947
18948 multi_buffer_editor.update(cx, |editor, cx| {
18949 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
18950 });
18951 assert_eq!(
18952 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18953 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
18954 "After folding the second buffer, its text should not be displayed"
18955 );
18956
18957 multi_buffer_editor.update(cx, |editor, cx| {
18958 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
18959 });
18960 assert_eq!(
18961 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18962 "\n\n\n\n\n",
18963 "After folding the third buffer, its text should not be displayed"
18964 );
18965
18966 // Emulate selection inside the fold logic, that should work
18967 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18968 editor
18969 .snapshot(window, cx)
18970 .next_line_boundary(Point::new(0, 4));
18971 });
18972
18973 multi_buffer_editor.update(cx, |editor, cx| {
18974 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
18975 });
18976 assert_eq!(
18977 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18978 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
18979 "After unfolding the second buffer, its text should be displayed"
18980 );
18981
18982 // Typing inside of buffer 1 causes that buffer to be unfolded.
18983 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18984 assert_eq!(
18985 multi_buffer
18986 .read(cx)
18987 .snapshot(cx)
18988 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
18989 .collect::<String>(),
18990 "bbbb"
18991 );
18992 editor.change_selections(None, window, cx, |selections| {
18993 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
18994 });
18995 editor.handle_input("B", window, cx);
18996 });
18997
18998 assert_eq!(
18999 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19000 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19001 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
19002 );
19003
19004 multi_buffer_editor.update(cx, |editor, cx| {
19005 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
19006 });
19007 assert_eq!(
19008 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19009 "\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",
19010 "After unfolding the all buffers, all original text should be displayed"
19011 );
19012}
19013
19014#[gpui::test]
19015async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
19016 init_test(cx, |_| {});
19017
19018 let sample_text_1 = "1111\n2222\n3333".to_string();
19019 let sample_text_2 = "4444\n5555\n6666".to_string();
19020 let sample_text_3 = "7777\n8888\n9999".to_string();
19021
19022 let fs = FakeFs::new(cx.executor());
19023 fs.insert_tree(
19024 path!("/a"),
19025 json!({
19026 "first.rs": sample_text_1,
19027 "second.rs": sample_text_2,
19028 "third.rs": sample_text_3,
19029 }),
19030 )
19031 .await;
19032 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19033 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19034 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19035 let worktree = project.update(cx, |project, cx| {
19036 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19037 assert_eq!(worktrees.len(), 1);
19038 worktrees.pop().unwrap()
19039 });
19040 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19041
19042 let buffer_1 = project
19043 .update(cx, |project, cx| {
19044 project.open_buffer((worktree_id, "first.rs"), cx)
19045 })
19046 .await
19047 .unwrap();
19048 let buffer_2 = project
19049 .update(cx, |project, cx| {
19050 project.open_buffer((worktree_id, "second.rs"), cx)
19051 })
19052 .await
19053 .unwrap();
19054 let buffer_3 = project
19055 .update(cx, |project, cx| {
19056 project.open_buffer((worktree_id, "third.rs"), cx)
19057 })
19058 .await
19059 .unwrap();
19060
19061 let multi_buffer = cx.new(|cx| {
19062 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19063 multi_buffer.push_excerpts(
19064 buffer_1.clone(),
19065 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19066 cx,
19067 );
19068 multi_buffer.push_excerpts(
19069 buffer_2.clone(),
19070 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19071 cx,
19072 );
19073 multi_buffer.push_excerpts(
19074 buffer_3.clone(),
19075 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19076 cx,
19077 );
19078 multi_buffer
19079 });
19080
19081 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19082 Editor::new(
19083 EditorMode::full(),
19084 multi_buffer,
19085 Some(project.clone()),
19086 window,
19087 cx,
19088 )
19089 });
19090
19091 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
19092 assert_eq!(
19093 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19094 full_text,
19095 );
19096
19097 multi_buffer_editor.update(cx, |editor, cx| {
19098 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
19099 });
19100 assert_eq!(
19101 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19102 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
19103 "After folding the first buffer, its text should not be displayed"
19104 );
19105
19106 multi_buffer_editor.update(cx, |editor, cx| {
19107 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
19108 });
19109
19110 assert_eq!(
19111 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19112 "\n\n\n\n\n\n7777\n8888\n9999",
19113 "After folding the second buffer, its text should not be displayed"
19114 );
19115
19116 multi_buffer_editor.update(cx, |editor, cx| {
19117 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
19118 });
19119 assert_eq!(
19120 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19121 "\n\n\n\n\n",
19122 "After folding the third buffer, its text should not be displayed"
19123 );
19124
19125 multi_buffer_editor.update(cx, |editor, cx| {
19126 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
19127 });
19128 assert_eq!(
19129 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19130 "\n\n\n\n4444\n5555\n6666\n\n",
19131 "After unfolding the second buffer, its text should be displayed"
19132 );
19133
19134 multi_buffer_editor.update(cx, |editor, cx| {
19135 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
19136 });
19137 assert_eq!(
19138 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19139 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
19140 "After unfolding the first buffer, its text should be displayed"
19141 );
19142
19143 multi_buffer_editor.update(cx, |editor, cx| {
19144 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
19145 });
19146 assert_eq!(
19147 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19148 full_text,
19149 "After unfolding all buffers, all original text should be displayed"
19150 );
19151}
19152
19153#[gpui::test]
19154async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
19155 init_test(cx, |_| {});
19156
19157 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
19158
19159 let fs = FakeFs::new(cx.executor());
19160 fs.insert_tree(
19161 path!("/a"),
19162 json!({
19163 "main.rs": sample_text,
19164 }),
19165 )
19166 .await;
19167 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19168 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19169 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19170 let worktree = project.update(cx, |project, cx| {
19171 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19172 assert_eq!(worktrees.len(), 1);
19173 worktrees.pop().unwrap()
19174 });
19175 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19176
19177 let buffer_1 = project
19178 .update(cx, |project, cx| {
19179 project.open_buffer((worktree_id, "main.rs"), cx)
19180 })
19181 .await
19182 .unwrap();
19183
19184 let multi_buffer = cx.new(|cx| {
19185 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19186 multi_buffer.push_excerpts(
19187 buffer_1.clone(),
19188 [ExcerptRange::new(
19189 Point::new(0, 0)
19190 ..Point::new(
19191 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
19192 0,
19193 ),
19194 )],
19195 cx,
19196 );
19197 multi_buffer
19198 });
19199 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19200 Editor::new(
19201 EditorMode::full(),
19202 multi_buffer,
19203 Some(project.clone()),
19204 window,
19205 cx,
19206 )
19207 });
19208
19209 let selection_range = Point::new(1, 0)..Point::new(2, 0);
19210 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19211 enum TestHighlight {}
19212 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
19213 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
19214 editor.highlight_text::<TestHighlight>(
19215 vec![highlight_range.clone()],
19216 HighlightStyle::color(Hsla::green()),
19217 cx,
19218 );
19219 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
19220 });
19221
19222 let full_text = format!("\n\n{sample_text}");
19223 assert_eq!(
19224 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19225 full_text,
19226 );
19227}
19228
19229#[gpui::test]
19230async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
19231 init_test(cx, |_| {});
19232 cx.update(|cx| {
19233 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
19234 "keymaps/default-linux.json",
19235 cx,
19236 )
19237 .unwrap();
19238 cx.bind_keys(default_key_bindings);
19239 });
19240
19241 let (editor, cx) = cx.add_window_view(|window, cx| {
19242 let multi_buffer = MultiBuffer::build_multi(
19243 [
19244 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
19245 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
19246 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
19247 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
19248 ],
19249 cx,
19250 );
19251 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
19252
19253 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
19254 // fold all but the second buffer, so that we test navigating between two
19255 // adjacent folded buffers, as well as folded buffers at the start and
19256 // end the multibuffer
19257 editor.fold_buffer(buffer_ids[0], cx);
19258 editor.fold_buffer(buffer_ids[2], cx);
19259 editor.fold_buffer(buffer_ids[3], cx);
19260
19261 editor
19262 });
19263 cx.simulate_resize(size(px(1000.), px(1000.)));
19264
19265 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
19266 cx.assert_excerpts_with_selections(indoc! {"
19267 [EXCERPT]
19268 ˇ[FOLDED]
19269 [EXCERPT]
19270 a1
19271 b1
19272 [EXCERPT]
19273 [FOLDED]
19274 [EXCERPT]
19275 [FOLDED]
19276 "
19277 });
19278 cx.simulate_keystroke("down");
19279 cx.assert_excerpts_with_selections(indoc! {"
19280 [EXCERPT]
19281 [FOLDED]
19282 [EXCERPT]
19283 ˇa1
19284 b1
19285 [EXCERPT]
19286 [FOLDED]
19287 [EXCERPT]
19288 [FOLDED]
19289 "
19290 });
19291 cx.simulate_keystroke("down");
19292 cx.assert_excerpts_with_selections(indoc! {"
19293 [EXCERPT]
19294 [FOLDED]
19295 [EXCERPT]
19296 a1
19297 ˇb1
19298 [EXCERPT]
19299 [FOLDED]
19300 [EXCERPT]
19301 [FOLDED]
19302 "
19303 });
19304 cx.simulate_keystroke("down");
19305 cx.assert_excerpts_with_selections(indoc! {"
19306 [EXCERPT]
19307 [FOLDED]
19308 [EXCERPT]
19309 a1
19310 b1
19311 ˇ[EXCERPT]
19312 [FOLDED]
19313 [EXCERPT]
19314 [FOLDED]
19315 "
19316 });
19317 cx.simulate_keystroke("down");
19318 cx.assert_excerpts_with_selections(indoc! {"
19319 [EXCERPT]
19320 [FOLDED]
19321 [EXCERPT]
19322 a1
19323 b1
19324 [EXCERPT]
19325 ˇ[FOLDED]
19326 [EXCERPT]
19327 [FOLDED]
19328 "
19329 });
19330 for _ in 0..5 {
19331 cx.simulate_keystroke("down");
19332 cx.assert_excerpts_with_selections(indoc! {"
19333 [EXCERPT]
19334 [FOLDED]
19335 [EXCERPT]
19336 a1
19337 b1
19338 [EXCERPT]
19339 [FOLDED]
19340 [EXCERPT]
19341 ˇ[FOLDED]
19342 "
19343 });
19344 }
19345
19346 cx.simulate_keystroke("up");
19347 cx.assert_excerpts_with_selections(indoc! {"
19348 [EXCERPT]
19349 [FOLDED]
19350 [EXCERPT]
19351 a1
19352 b1
19353 [EXCERPT]
19354 ˇ[FOLDED]
19355 [EXCERPT]
19356 [FOLDED]
19357 "
19358 });
19359 cx.simulate_keystroke("up");
19360 cx.assert_excerpts_with_selections(indoc! {"
19361 [EXCERPT]
19362 [FOLDED]
19363 [EXCERPT]
19364 a1
19365 b1
19366 ˇ[EXCERPT]
19367 [FOLDED]
19368 [EXCERPT]
19369 [FOLDED]
19370 "
19371 });
19372 cx.simulate_keystroke("up");
19373 cx.assert_excerpts_with_selections(indoc! {"
19374 [EXCERPT]
19375 [FOLDED]
19376 [EXCERPT]
19377 a1
19378 ˇb1
19379 [EXCERPT]
19380 [FOLDED]
19381 [EXCERPT]
19382 [FOLDED]
19383 "
19384 });
19385 cx.simulate_keystroke("up");
19386 cx.assert_excerpts_with_selections(indoc! {"
19387 [EXCERPT]
19388 [FOLDED]
19389 [EXCERPT]
19390 ˇa1
19391 b1
19392 [EXCERPT]
19393 [FOLDED]
19394 [EXCERPT]
19395 [FOLDED]
19396 "
19397 });
19398 for _ in 0..5 {
19399 cx.simulate_keystroke("up");
19400 cx.assert_excerpts_with_selections(indoc! {"
19401 [EXCERPT]
19402 ˇ[FOLDED]
19403 [EXCERPT]
19404 a1
19405 b1
19406 [EXCERPT]
19407 [FOLDED]
19408 [EXCERPT]
19409 [FOLDED]
19410 "
19411 });
19412 }
19413}
19414
19415#[gpui::test]
19416async fn test_inline_completion_text(cx: &mut TestAppContext) {
19417 init_test(cx, |_| {});
19418
19419 // Simple insertion
19420 assert_highlighted_edits(
19421 "Hello, world!",
19422 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
19423 true,
19424 cx,
19425 |highlighted_edits, cx| {
19426 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
19427 assert_eq!(highlighted_edits.highlights.len(), 1);
19428 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
19429 assert_eq!(
19430 highlighted_edits.highlights[0].1.background_color,
19431 Some(cx.theme().status().created_background)
19432 );
19433 },
19434 )
19435 .await;
19436
19437 // Replacement
19438 assert_highlighted_edits(
19439 "This is a test.",
19440 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
19441 false,
19442 cx,
19443 |highlighted_edits, cx| {
19444 assert_eq!(highlighted_edits.text, "That is a test.");
19445 assert_eq!(highlighted_edits.highlights.len(), 1);
19446 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
19447 assert_eq!(
19448 highlighted_edits.highlights[0].1.background_color,
19449 Some(cx.theme().status().created_background)
19450 );
19451 },
19452 )
19453 .await;
19454
19455 // Multiple edits
19456 assert_highlighted_edits(
19457 "Hello, world!",
19458 vec![
19459 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
19460 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
19461 ],
19462 false,
19463 cx,
19464 |highlighted_edits, cx| {
19465 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
19466 assert_eq!(highlighted_edits.highlights.len(), 2);
19467 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
19468 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
19469 assert_eq!(
19470 highlighted_edits.highlights[0].1.background_color,
19471 Some(cx.theme().status().created_background)
19472 );
19473 assert_eq!(
19474 highlighted_edits.highlights[1].1.background_color,
19475 Some(cx.theme().status().created_background)
19476 );
19477 },
19478 )
19479 .await;
19480
19481 // Multiple lines with edits
19482 assert_highlighted_edits(
19483 "First line\nSecond line\nThird line\nFourth line",
19484 vec![
19485 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
19486 (
19487 Point::new(2, 0)..Point::new(2, 10),
19488 "New third line".to_string(),
19489 ),
19490 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
19491 ],
19492 false,
19493 cx,
19494 |highlighted_edits, cx| {
19495 assert_eq!(
19496 highlighted_edits.text,
19497 "Second modified\nNew third line\nFourth updated line"
19498 );
19499 assert_eq!(highlighted_edits.highlights.len(), 3);
19500 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
19501 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
19502 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
19503 for highlight in &highlighted_edits.highlights {
19504 assert_eq!(
19505 highlight.1.background_color,
19506 Some(cx.theme().status().created_background)
19507 );
19508 }
19509 },
19510 )
19511 .await;
19512}
19513
19514#[gpui::test]
19515async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
19516 init_test(cx, |_| {});
19517
19518 // Deletion
19519 assert_highlighted_edits(
19520 "Hello, world!",
19521 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
19522 true,
19523 cx,
19524 |highlighted_edits, cx| {
19525 assert_eq!(highlighted_edits.text, "Hello, world!");
19526 assert_eq!(highlighted_edits.highlights.len(), 1);
19527 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
19528 assert_eq!(
19529 highlighted_edits.highlights[0].1.background_color,
19530 Some(cx.theme().status().deleted_background)
19531 );
19532 },
19533 )
19534 .await;
19535
19536 // Insertion
19537 assert_highlighted_edits(
19538 "Hello, world!",
19539 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
19540 true,
19541 cx,
19542 |highlighted_edits, cx| {
19543 assert_eq!(highlighted_edits.highlights.len(), 1);
19544 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
19545 assert_eq!(
19546 highlighted_edits.highlights[0].1.background_color,
19547 Some(cx.theme().status().created_background)
19548 );
19549 },
19550 )
19551 .await;
19552}
19553
19554async fn assert_highlighted_edits(
19555 text: &str,
19556 edits: Vec<(Range<Point>, String)>,
19557 include_deletions: bool,
19558 cx: &mut TestAppContext,
19559 assertion_fn: impl Fn(HighlightedText, &App),
19560) {
19561 let window = cx.add_window(|window, cx| {
19562 let buffer = MultiBuffer::build_simple(text, cx);
19563 Editor::new(EditorMode::full(), buffer, None, window, cx)
19564 });
19565 let cx = &mut VisualTestContext::from_window(*window, cx);
19566
19567 let (buffer, snapshot) = window
19568 .update(cx, |editor, _window, cx| {
19569 (
19570 editor.buffer().clone(),
19571 editor.buffer().read(cx).snapshot(cx),
19572 )
19573 })
19574 .unwrap();
19575
19576 let edits = edits
19577 .into_iter()
19578 .map(|(range, edit)| {
19579 (
19580 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
19581 edit,
19582 )
19583 })
19584 .collect::<Vec<_>>();
19585
19586 let text_anchor_edits = edits
19587 .clone()
19588 .into_iter()
19589 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
19590 .collect::<Vec<_>>();
19591
19592 let edit_preview = window
19593 .update(cx, |_, _window, cx| {
19594 buffer
19595 .read(cx)
19596 .as_singleton()
19597 .unwrap()
19598 .read(cx)
19599 .preview_edits(text_anchor_edits.into(), cx)
19600 })
19601 .unwrap()
19602 .await;
19603
19604 cx.update(|_window, cx| {
19605 let highlighted_edits = inline_completion_edit_text(
19606 &snapshot.as_singleton().unwrap().2,
19607 &edits,
19608 &edit_preview,
19609 include_deletions,
19610 cx,
19611 );
19612 assertion_fn(highlighted_edits, cx)
19613 });
19614}
19615
19616#[track_caller]
19617fn assert_breakpoint(
19618 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
19619 path: &Arc<Path>,
19620 expected: Vec<(u32, Breakpoint)>,
19621) {
19622 if expected.len() == 0usize {
19623 assert!(!breakpoints.contains_key(path), "{}", path.display());
19624 } else {
19625 let mut breakpoint = breakpoints
19626 .get(path)
19627 .unwrap()
19628 .into_iter()
19629 .map(|breakpoint| {
19630 (
19631 breakpoint.row,
19632 Breakpoint {
19633 message: breakpoint.message.clone(),
19634 state: breakpoint.state,
19635 condition: breakpoint.condition.clone(),
19636 hit_condition: breakpoint.hit_condition.clone(),
19637 },
19638 )
19639 })
19640 .collect::<Vec<_>>();
19641
19642 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
19643
19644 assert_eq!(expected, breakpoint);
19645 }
19646}
19647
19648fn add_log_breakpoint_at_cursor(
19649 editor: &mut Editor,
19650 log_message: &str,
19651 window: &mut Window,
19652 cx: &mut Context<Editor>,
19653) {
19654 let (anchor, bp) = editor
19655 .breakpoints_at_cursors(window, cx)
19656 .first()
19657 .and_then(|(anchor, bp)| {
19658 if let Some(bp) = bp {
19659 Some((*anchor, bp.clone()))
19660 } else {
19661 None
19662 }
19663 })
19664 .unwrap_or_else(|| {
19665 let cursor_position: Point = editor.selections.newest(cx).head();
19666
19667 let breakpoint_position = editor
19668 .snapshot(window, cx)
19669 .display_snapshot
19670 .buffer_snapshot
19671 .anchor_before(Point::new(cursor_position.row, 0));
19672
19673 (breakpoint_position, Breakpoint::new_log(&log_message))
19674 });
19675
19676 editor.edit_breakpoint_at_anchor(
19677 anchor,
19678 bp,
19679 BreakpointEditAction::EditLogMessage(log_message.into()),
19680 cx,
19681 );
19682}
19683
19684#[gpui::test]
19685async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
19686 init_test(cx, |_| {});
19687
19688 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19689 let fs = FakeFs::new(cx.executor());
19690 fs.insert_tree(
19691 path!("/a"),
19692 json!({
19693 "main.rs": sample_text,
19694 }),
19695 )
19696 .await;
19697 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19698 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19699 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19700
19701 let fs = FakeFs::new(cx.executor());
19702 fs.insert_tree(
19703 path!("/a"),
19704 json!({
19705 "main.rs": sample_text,
19706 }),
19707 )
19708 .await;
19709 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19710 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19711 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19712 let worktree_id = workspace
19713 .update(cx, |workspace, _window, cx| {
19714 workspace.project().update(cx, |project, cx| {
19715 project.worktrees(cx).next().unwrap().read(cx).id()
19716 })
19717 })
19718 .unwrap();
19719
19720 let buffer = project
19721 .update(cx, |project, cx| {
19722 project.open_buffer((worktree_id, "main.rs"), cx)
19723 })
19724 .await
19725 .unwrap();
19726
19727 let (editor, cx) = cx.add_window_view(|window, cx| {
19728 Editor::new(
19729 EditorMode::full(),
19730 MultiBuffer::build_from_buffer(buffer, cx),
19731 Some(project.clone()),
19732 window,
19733 cx,
19734 )
19735 });
19736
19737 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19738 let abs_path = project.read_with(cx, |project, cx| {
19739 project
19740 .absolute_path(&project_path, cx)
19741 .map(|path_buf| Arc::from(path_buf.to_owned()))
19742 .unwrap()
19743 });
19744
19745 // assert we can add breakpoint on the first line
19746 editor.update_in(cx, |editor, window, cx| {
19747 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19748 editor.move_to_end(&MoveToEnd, window, cx);
19749 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19750 });
19751
19752 let breakpoints = editor.update(cx, |editor, cx| {
19753 editor
19754 .breakpoint_store()
19755 .as_ref()
19756 .unwrap()
19757 .read(cx)
19758 .all_source_breakpoints(cx)
19759 .clone()
19760 });
19761
19762 assert_eq!(1, breakpoints.len());
19763 assert_breakpoint(
19764 &breakpoints,
19765 &abs_path,
19766 vec![
19767 (0, Breakpoint::new_standard()),
19768 (3, Breakpoint::new_standard()),
19769 ],
19770 );
19771
19772 editor.update_in(cx, |editor, window, cx| {
19773 editor.move_to_beginning(&MoveToBeginning, window, cx);
19774 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19775 });
19776
19777 let breakpoints = editor.update(cx, |editor, cx| {
19778 editor
19779 .breakpoint_store()
19780 .as_ref()
19781 .unwrap()
19782 .read(cx)
19783 .all_source_breakpoints(cx)
19784 .clone()
19785 });
19786
19787 assert_eq!(1, breakpoints.len());
19788 assert_breakpoint(
19789 &breakpoints,
19790 &abs_path,
19791 vec![(3, Breakpoint::new_standard())],
19792 );
19793
19794 editor.update_in(cx, |editor, window, cx| {
19795 editor.move_to_end(&MoveToEnd, window, cx);
19796 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19797 });
19798
19799 let breakpoints = editor.update(cx, |editor, cx| {
19800 editor
19801 .breakpoint_store()
19802 .as_ref()
19803 .unwrap()
19804 .read(cx)
19805 .all_source_breakpoints(cx)
19806 .clone()
19807 });
19808
19809 assert_eq!(0, breakpoints.len());
19810 assert_breakpoint(&breakpoints, &abs_path, vec![]);
19811}
19812
19813#[gpui::test]
19814async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
19815 init_test(cx, |_| {});
19816
19817 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19818
19819 let fs = FakeFs::new(cx.executor());
19820 fs.insert_tree(
19821 path!("/a"),
19822 json!({
19823 "main.rs": sample_text,
19824 }),
19825 )
19826 .await;
19827 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19828 let (workspace, cx) =
19829 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19830
19831 let worktree_id = workspace.update(cx, |workspace, cx| {
19832 workspace.project().update(cx, |project, cx| {
19833 project.worktrees(cx).next().unwrap().read(cx).id()
19834 })
19835 });
19836
19837 let buffer = project
19838 .update(cx, |project, cx| {
19839 project.open_buffer((worktree_id, "main.rs"), cx)
19840 })
19841 .await
19842 .unwrap();
19843
19844 let (editor, cx) = cx.add_window_view(|window, cx| {
19845 Editor::new(
19846 EditorMode::full(),
19847 MultiBuffer::build_from_buffer(buffer, cx),
19848 Some(project.clone()),
19849 window,
19850 cx,
19851 )
19852 });
19853
19854 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19855 let abs_path = project.read_with(cx, |project, cx| {
19856 project
19857 .absolute_path(&project_path, cx)
19858 .map(|path_buf| Arc::from(path_buf.to_owned()))
19859 .unwrap()
19860 });
19861
19862 editor.update_in(cx, |editor, window, cx| {
19863 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
19864 });
19865
19866 let breakpoints = editor.update(cx, |editor, cx| {
19867 editor
19868 .breakpoint_store()
19869 .as_ref()
19870 .unwrap()
19871 .read(cx)
19872 .all_source_breakpoints(cx)
19873 .clone()
19874 });
19875
19876 assert_breakpoint(
19877 &breakpoints,
19878 &abs_path,
19879 vec![(0, Breakpoint::new_log("hello world"))],
19880 );
19881
19882 // Removing a log message from a log breakpoint should remove it
19883 editor.update_in(cx, |editor, window, cx| {
19884 add_log_breakpoint_at_cursor(editor, "", window, cx);
19885 });
19886
19887 let breakpoints = editor.update(cx, |editor, cx| {
19888 editor
19889 .breakpoint_store()
19890 .as_ref()
19891 .unwrap()
19892 .read(cx)
19893 .all_source_breakpoints(cx)
19894 .clone()
19895 });
19896
19897 assert_breakpoint(&breakpoints, &abs_path, vec![]);
19898
19899 editor.update_in(cx, |editor, window, cx| {
19900 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19901 editor.move_to_end(&MoveToEnd, window, cx);
19902 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19903 // Not adding a log message to a standard breakpoint shouldn't remove it
19904 add_log_breakpoint_at_cursor(editor, "", window, cx);
19905 });
19906
19907 let breakpoints = editor.update(cx, |editor, cx| {
19908 editor
19909 .breakpoint_store()
19910 .as_ref()
19911 .unwrap()
19912 .read(cx)
19913 .all_source_breakpoints(cx)
19914 .clone()
19915 });
19916
19917 assert_breakpoint(
19918 &breakpoints,
19919 &abs_path,
19920 vec![
19921 (0, Breakpoint::new_standard()),
19922 (3, Breakpoint::new_standard()),
19923 ],
19924 );
19925
19926 editor.update_in(cx, |editor, window, cx| {
19927 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
19928 });
19929
19930 let breakpoints = editor.update(cx, |editor, cx| {
19931 editor
19932 .breakpoint_store()
19933 .as_ref()
19934 .unwrap()
19935 .read(cx)
19936 .all_source_breakpoints(cx)
19937 .clone()
19938 });
19939
19940 assert_breakpoint(
19941 &breakpoints,
19942 &abs_path,
19943 vec![
19944 (0, Breakpoint::new_standard()),
19945 (3, Breakpoint::new_log("hello world")),
19946 ],
19947 );
19948
19949 editor.update_in(cx, |editor, window, cx| {
19950 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
19951 });
19952
19953 let breakpoints = editor.update(cx, |editor, cx| {
19954 editor
19955 .breakpoint_store()
19956 .as_ref()
19957 .unwrap()
19958 .read(cx)
19959 .all_source_breakpoints(cx)
19960 .clone()
19961 });
19962
19963 assert_breakpoint(
19964 &breakpoints,
19965 &abs_path,
19966 vec![
19967 (0, Breakpoint::new_standard()),
19968 (3, Breakpoint::new_log("hello Earth!!")),
19969 ],
19970 );
19971}
19972
19973/// This also tests that Editor::breakpoint_at_cursor_head is working properly
19974/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
19975/// or when breakpoints were placed out of order. This tests for a regression too
19976#[gpui::test]
19977async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
19978 init_test(cx, |_| {});
19979
19980 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19981 let fs = FakeFs::new(cx.executor());
19982 fs.insert_tree(
19983 path!("/a"),
19984 json!({
19985 "main.rs": sample_text,
19986 }),
19987 )
19988 .await;
19989 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19990 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19991 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19992
19993 let fs = FakeFs::new(cx.executor());
19994 fs.insert_tree(
19995 path!("/a"),
19996 json!({
19997 "main.rs": sample_text,
19998 }),
19999 )
20000 .await;
20001 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20002 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20003 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20004 let worktree_id = workspace
20005 .update(cx, |workspace, _window, cx| {
20006 workspace.project().update(cx, |project, cx| {
20007 project.worktrees(cx).next().unwrap().read(cx).id()
20008 })
20009 })
20010 .unwrap();
20011
20012 let buffer = project
20013 .update(cx, |project, cx| {
20014 project.open_buffer((worktree_id, "main.rs"), cx)
20015 })
20016 .await
20017 .unwrap();
20018
20019 let (editor, cx) = cx.add_window_view(|window, cx| {
20020 Editor::new(
20021 EditorMode::full(),
20022 MultiBuffer::build_from_buffer(buffer, cx),
20023 Some(project.clone()),
20024 window,
20025 cx,
20026 )
20027 });
20028
20029 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20030 let abs_path = project.read_with(cx, |project, cx| {
20031 project
20032 .absolute_path(&project_path, cx)
20033 .map(|path_buf| Arc::from(path_buf.to_owned()))
20034 .unwrap()
20035 });
20036
20037 // assert we can add breakpoint on the first line
20038 editor.update_in(cx, |editor, window, cx| {
20039 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20040 editor.move_to_end(&MoveToEnd, window, cx);
20041 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20042 editor.move_up(&MoveUp, window, cx);
20043 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20044 });
20045
20046 let breakpoints = editor.update(cx, |editor, cx| {
20047 editor
20048 .breakpoint_store()
20049 .as_ref()
20050 .unwrap()
20051 .read(cx)
20052 .all_source_breakpoints(cx)
20053 .clone()
20054 });
20055
20056 assert_eq!(1, breakpoints.len());
20057 assert_breakpoint(
20058 &breakpoints,
20059 &abs_path,
20060 vec![
20061 (0, Breakpoint::new_standard()),
20062 (2, Breakpoint::new_standard()),
20063 (3, Breakpoint::new_standard()),
20064 ],
20065 );
20066
20067 editor.update_in(cx, |editor, window, cx| {
20068 editor.move_to_beginning(&MoveToBeginning, window, cx);
20069 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20070 editor.move_to_end(&MoveToEnd, window, cx);
20071 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20072 // Disabling a breakpoint that doesn't exist should do nothing
20073 editor.move_up(&MoveUp, window, cx);
20074 editor.move_up(&MoveUp, window, cx);
20075 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20076 });
20077
20078 let breakpoints = editor.update(cx, |editor, cx| {
20079 editor
20080 .breakpoint_store()
20081 .as_ref()
20082 .unwrap()
20083 .read(cx)
20084 .all_source_breakpoints(cx)
20085 .clone()
20086 });
20087
20088 let disable_breakpoint = {
20089 let mut bp = Breakpoint::new_standard();
20090 bp.state = BreakpointState::Disabled;
20091 bp
20092 };
20093
20094 assert_eq!(1, breakpoints.len());
20095 assert_breakpoint(
20096 &breakpoints,
20097 &abs_path,
20098 vec![
20099 (0, disable_breakpoint.clone()),
20100 (2, Breakpoint::new_standard()),
20101 (3, disable_breakpoint.clone()),
20102 ],
20103 );
20104
20105 editor.update_in(cx, |editor, window, cx| {
20106 editor.move_to_beginning(&MoveToBeginning, window, cx);
20107 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
20108 editor.move_to_end(&MoveToEnd, window, cx);
20109 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
20110 editor.move_up(&MoveUp, window, cx);
20111 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20112 });
20113
20114 let breakpoints = editor.update(cx, |editor, cx| {
20115 editor
20116 .breakpoint_store()
20117 .as_ref()
20118 .unwrap()
20119 .read(cx)
20120 .all_source_breakpoints(cx)
20121 .clone()
20122 });
20123
20124 assert_eq!(1, breakpoints.len());
20125 assert_breakpoint(
20126 &breakpoints,
20127 &abs_path,
20128 vec![
20129 (0, Breakpoint::new_standard()),
20130 (2, disable_breakpoint),
20131 (3, Breakpoint::new_standard()),
20132 ],
20133 );
20134}
20135
20136#[gpui::test]
20137async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
20138 init_test(cx, |_| {});
20139 let capabilities = lsp::ServerCapabilities {
20140 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
20141 prepare_provider: Some(true),
20142 work_done_progress_options: Default::default(),
20143 })),
20144 ..Default::default()
20145 };
20146 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
20147
20148 cx.set_state(indoc! {"
20149 struct Fˇoo {}
20150 "});
20151
20152 cx.update_editor(|editor, _, cx| {
20153 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
20154 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
20155 editor.highlight_background::<DocumentHighlightRead>(
20156 &[highlight_range],
20157 |c| c.editor_document_highlight_read_background,
20158 cx,
20159 );
20160 });
20161
20162 let mut prepare_rename_handler = cx
20163 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
20164 move |_, _, _| async move {
20165 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
20166 start: lsp::Position {
20167 line: 0,
20168 character: 7,
20169 },
20170 end: lsp::Position {
20171 line: 0,
20172 character: 10,
20173 },
20174 })))
20175 },
20176 );
20177 let prepare_rename_task = cx
20178 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
20179 .expect("Prepare rename was not started");
20180 prepare_rename_handler.next().await.unwrap();
20181 prepare_rename_task.await.expect("Prepare rename failed");
20182
20183 let mut rename_handler =
20184 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
20185 let edit = lsp::TextEdit {
20186 range: lsp::Range {
20187 start: lsp::Position {
20188 line: 0,
20189 character: 7,
20190 },
20191 end: lsp::Position {
20192 line: 0,
20193 character: 10,
20194 },
20195 },
20196 new_text: "FooRenamed".to_string(),
20197 };
20198 Ok(Some(lsp::WorkspaceEdit::new(
20199 // Specify the same edit twice
20200 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
20201 )))
20202 });
20203 let rename_task = cx
20204 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
20205 .expect("Confirm rename was not started");
20206 rename_handler.next().await.unwrap();
20207 rename_task.await.expect("Confirm rename failed");
20208 cx.run_until_parked();
20209
20210 // Despite two edits, only one is actually applied as those are identical
20211 cx.assert_editor_state(indoc! {"
20212 struct FooRenamedˇ {}
20213 "});
20214}
20215
20216#[gpui::test]
20217async fn test_rename_without_prepare(cx: &mut TestAppContext) {
20218 init_test(cx, |_| {});
20219 // These capabilities indicate that the server does not support prepare rename.
20220 let capabilities = lsp::ServerCapabilities {
20221 rename_provider: Some(lsp::OneOf::Left(true)),
20222 ..Default::default()
20223 };
20224 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
20225
20226 cx.set_state(indoc! {"
20227 struct Fˇoo {}
20228 "});
20229
20230 cx.update_editor(|editor, _window, cx| {
20231 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
20232 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
20233 editor.highlight_background::<DocumentHighlightRead>(
20234 &[highlight_range],
20235 |c| c.editor_document_highlight_read_background,
20236 cx,
20237 );
20238 });
20239
20240 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
20241 .expect("Prepare rename was not started")
20242 .await
20243 .expect("Prepare rename failed");
20244
20245 let mut rename_handler =
20246 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
20247 let edit = lsp::TextEdit {
20248 range: lsp::Range {
20249 start: lsp::Position {
20250 line: 0,
20251 character: 7,
20252 },
20253 end: lsp::Position {
20254 line: 0,
20255 character: 10,
20256 },
20257 },
20258 new_text: "FooRenamed".to_string(),
20259 };
20260 Ok(Some(lsp::WorkspaceEdit::new(
20261 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
20262 )))
20263 });
20264 let rename_task = cx
20265 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
20266 .expect("Confirm rename was not started");
20267 rename_handler.next().await.unwrap();
20268 rename_task.await.expect("Confirm rename failed");
20269 cx.run_until_parked();
20270
20271 // Correct range is renamed, as `surrounding_word` is used to find it.
20272 cx.assert_editor_state(indoc! {"
20273 struct FooRenamedˇ {}
20274 "});
20275}
20276
20277#[gpui::test]
20278async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
20279 init_test(cx, |_| {});
20280 let mut cx = EditorTestContext::new(cx).await;
20281
20282 let language = Arc::new(
20283 Language::new(
20284 LanguageConfig::default(),
20285 Some(tree_sitter_html::LANGUAGE.into()),
20286 )
20287 .with_brackets_query(
20288 r#"
20289 ("<" @open "/>" @close)
20290 ("</" @open ">" @close)
20291 ("<" @open ">" @close)
20292 ("\"" @open "\"" @close)
20293 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
20294 "#,
20295 )
20296 .unwrap(),
20297 );
20298 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20299
20300 cx.set_state(indoc! {"
20301 <span>ˇ</span>
20302 "});
20303 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
20304 cx.assert_editor_state(indoc! {"
20305 <span>
20306 ˇ
20307 </span>
20308 "});
20309
20310 cx.set_state(indoc! {"
20311 <span><span></span>ˇ</span>
20312 "});
20313 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
20314 cx.assert_editor_state(indoc! {"
20315 <span><span></span>
20316 ˇ</span>
20317 "});
20318
20319 cx.set_state(indoc! {"
20320 <span>ˇ
20321 </span>
20322 "});
20323 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
20324 cx.assert_editor_state(indoc! {"
20325 <span>
20326 ˇ
20327 </span>
20328 "});
20329}
20330
20331#[gpui::test(iterations = 10)]
20332async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
20333 init_test(cx, |_| {});
20334
20335 let fs = FakeFs::new(cx.executor());
20336 fs.insert_tree(
20337 path!("/dir"),
20338 json!({
20339 "a.ts": "a",
20340 }),
20341 )
20342 .await;
20343
20344 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
20345 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20346 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20347
20348 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20349 language_registry.add(Arc::new(Language::new(
20350 LanguageConfig {
20351 name: "TypeScript".into(),
20352 matcher: LanguageMatcher {
20353 path_suffixes: vec!["ts".to_string()],
20354 ..Default::default()
20355 },
20356 ..Default::default()
20357 },
20358 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
20359 )));
20360 let mut fake_language_servers = language_registry.register_fake_lsp(
20361 "TypeScript",
20362 FakeLspAdapter {
20363 capabilities: lsp::ServerCapabilities {
20364 code_lens_provider: Some(lsp::CodeLensOptions {
20365 resolve_provider: Some(true),
20366 }),
20367 execute_command_provider: Some(lsp::ExecuteCommandOptions {
20368 commands: vec!["_the/command".to_string()],
20369 ..lsp::ExecuteCommandOptions::default()
20370 }),
20371 ..lsp::ServerCapabilities::default()
20372 },
20373 ..FakeLspAdapter::default()
20374 },
20375 );
20376
20377 let (buffer, _handle) = project
20378 .update(cx, |p, cx| {
20379 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
20380 })
20381 .await
20382 .unwrap();
20383 cx.executor().run_until_parked();
20384
20385 let fake_server = fake_language_servers.next().await.unwrap();
20386
20387 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
20388 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
20389 drop(buffer_snapshot);
20390 let actions = cx
20391 .update_window(*workspace, |_, window, cx| {
20392 project.code_actions(&buffer, anchor..anchor, window, cx)
20393 })
20394 .unwrap();
20395
20396 fake_server
20397 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
20398 Ok(Some(vec![
20399 lsp::CodeLens {
20400 range: lsp::Range::default(),
20401 command: Some(lsp::Command {
20402 title: "Code lens command".to_owned(),
20403 command: "_the/command".to_owned(),
20404 arguments: None,
20405 }),
20406 data: None,
20407 },
20408 lsp::CodeLens {
20409 range: lsp::Range::default(),
20410 command: Some(lsp::Command {
20411 title: "Command not in capabilities".to_owned(),
20412 command: "not in capabilities".to_owned(),
20413 arguments: None,
20414 }),
20415 data: None,
20416 },
20417 lsp::CodeLens {
20418 range: lsp::Range {
20419 start: lsp::Position {
20420 line: 1,
20421 character: 1,
20422 },
20423 end: lsp::Position {
20424 line: 1,
20425 character: 1,
20426 },
20427 },
20428 command: Some(lsp::Command {
20429 title: "Command not in range".to_owned(),
20430 command: "_the/command".to_owned(),
20431 arguments: None,
20432 }),
20433 data: None,
20434 },
20435 ]))
20436 })
20437 .next()
20438 .await;
20439
20440 let actions = actions.await.unwrap();
20441 assert_eq!(
20442 actions.len(),
20443 1,
20444 "Should have only one valid action for the 0..0 range"
20445 );
20446 let action = actions[0].clone();
20447 let apply = project.update(cx, |project, cx| {
20448 project.apply_code_action(buffer.clone(), action, true, cx)
20449 });
20450
20451 // Resolving the code action does not populate its edits. In absence of
20452 // edits, we must execute the given command.
20453 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
20454 |mut lens, _| async move {
20455 let lens_command = lens.command.as_mut().expect("should have a command");
20456 assert_eq!(lens_command.title, "Code lens command");
20457 lens_command.arguments = Some(vec![json!("the-argument")]);
20458 Ok(lens)
20459 },
20460 );
20461
20462 // While executing the command, the language server sends the editor
20463 // a `workspaceEdit` request.
20464 fake_server
20465 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
20466 let fake = fake_server.clone();
20467 move |params, _| {
20468 assert_eq!(params.command, "_the/command");
20469 let fake = fake.clone();
20470 async move {
20471 fake.server
20472 .request::<lsp::request::ApplyWorkspaceEdit>(
20473 lsp::ApplyWorkspaceEditParams {
20474 label: None,
20475 edit: lsp::WorkspaceEdit {
20476 changes: Some(
20477 [(
20478 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
20479 vec![lsp::TextEdit {
20480 range: lsp::Range::new(
20481 lsp::Position::new(0, 0),
20482 lsp::Position::new(0, 0),
20483 ),
20484 new_text: "X".into(),
20485 }],
20486 )]
20487 .into_iter()
20488 .collect(),
20489 ),
20490 ..Default::default()
20491 },
20492 },
20493 )
20494 .await
20495 .into_response()
20496 .unwrap();
20497 Ok(Some(json!(null)))
20498 }
20499 }
20500 })
20501 .next()
20502 .await;
20503
20504 // Applying the code lens command returns a project transaction containing the edits
20505 // sent by the language server in its `workspaceEdit` request.
20506 let transaction = apply.await.unwrap();
20507 assert!(transaction.0.contains_key(&buffer));
20508 buffer.update(cx, |buffer, cx| {
20509 assert_eq!(buffer.text(), "Xa");
20510 buffer.undo(cx);
20511 assert_eq!(buffer.text(), "a");
20512 });
20513}
20514
20515#[gpui::test]
20516async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
20517 init_test(cx, |_| {});
20518
20519 let fs = FakeFs::new(cx.executor());
20520 let main_text = r#"fn main() {
20521println!("1");
20522println!("2");
20523println!("3");
20524println!("4");
20525println!("5");
20526}"#;
20527 let lib_text = "mod foo {}";
20528 fs.insert_tree(
20529 path!("/a"),
20530 json!({
20531 "lib.rs": lib_text,
20532 "main.rs": main_text,
20533 }),
20534 )
20535 .await;
20536
20537 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20538 let (workspace, cx) =
20539 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20540 let worktree_id = workspace.update(cx, |workspace, cx| {
20541 workspace.project().update(cx, |project, cx| {
20542 project.worktrees(cx).next().unwrap().read(cx).id()
20543 })
20544 });
20545
20546 let expected_ranges = vec![
20547 Point::new(0, 0)..Point::new(0, 0),
20548 Point::new(1, 0)..Point::new(1, 1),
20549 Point::new(2, 0)..Point::new(2, 2),
20550 Point::new(3, 0)..Point::new(3, 3),
20551 ];
20552
20553 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20554 let editor_1 = workspace
20555 .update_in(cx, |workspace, window, cx| {
20556 workspace.open_path(
20557 (worktree_id, "main.rs"),
20558 Some(pane_1.downgrade()),
20559 true,
20560 window,
20561 cx,
20562 )
20563 })
20564 .unwrap()
20565 .await
20566 .downcast::<Editor>()
20567 .unwrap();
20568 pane_1.update(cx, |pane, cx| {
20569 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20570 open_editor.update(cx, |editor, cx| {
20571 assert_eq!(
20572 editor.display_text(cx),
20573 main_text,
20574 "Original main.rs text on initial open",
20575 );
20576 assert_eq!(
20577 editor
20578 .selections
20579 .all::<Point>(cx)
20580 .into_iter()
20581 .map(|s| s.range())
20582 .collect::<Vec<_>>(),
20583 vec![Point::zero()..Point::zero()],
20584 "Default selections on initial open",
20585 );
20586 })
20587 });
20588 editor_1.update_in(cx, |editor, window, cx| {
20589 editor.change_selections(None, window, cx, |s| {
20590 s.select_ranges(expected_ranges.clone());
20591 });
20592 });
20593
20594 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
20595 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
20596 });
20597 let editor_2 = workspace
20598 .update_in(cx, |workspace, window, cx| {
20599 workspace.open_path(
20600 (worktree_id, "main.rs"),
20601 Some(pane_2.downgrade()),
20602 true,
20603 window,
20604 cx,
20605 )
20606 })
20607 .unwrap()
20608 .await
20609 .downcast::<Editor>()
20610 .unwrap();
20611 pane_2.update(cx, |pane, cx| {
20612 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20613 open_editor.update(cx, |editor, cx| {
20614 assert_eq!(
20615 editor.display_text(cx),
20616 main_text,
20617 "Original main.rs text on initial open in another panel",
20618 );
20619 assert_eq!(
20620 editor
20621 .selections
20622 .all::<Point>(cx)
20623 .into_iter()
20624 .map(|s| s.range())
20625 .collect::<Vec<_>>(),
20626 vec![Point::zero()..Point::zero()],
20627 "Default selections on initial open in another panel",
20628 );
20629 })
20630 });
20631
20632 editor_2.update_in(cx, |editor, window, cx| {
20633 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
20634 });
20635
20636 let _other_editor_1 = workspace
20637 .update_in(cx, |workspace, window, cx| {
20638 workspace.open_path(
20639 (worktree_id, "lib.rs"),
20640 Some(pane_1.downgrade()),
20641 true,
20642 window,
20643 cx,
20644 )
20645 })
20646 .unwrap()
20647 .await
20648 .downcast::<Editor>()
20649 .unwrap();
20650 pane_1
20651 .update_in(cx, |pane, window, cx| {
20652 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
20653 })
20654 .await
20655 .unwrap();
20656 drop(editor_1);
20657 pane_1.update(cx, |pane, cx| {
20658 pane.active_item()
20659 .unwrap()
20660 .downcast::<Editor>()
20661 .unwrap()
20662 .update(cx, |editor, cx| {
20663 assert_eq!(
20664 editor.display_text(cx),
20665 lib_text,
20666 "Other file should be open and active",
20667 );
20668 });
20669 assert_eq!(pane.items().count(), 1, "No other editors should be open");
20670 });
20671
20672 let _other_editor_2 = workspace
20673 .update_in(cx, |workspace, window, cx| {
20674 workspace.open_path(
20675 (worktree_id, "lib.rs"),
20676 Some(pane_2.downgrade()),
20677 true,
20678 window,
20679 cx,
20680 )
20681 })
20682 .unwrap()
20683 .await
20684 .downcast::<Editor>()
20685 .unwrap();
20686 pane_2
20687 .update_in(cx, |pane, window, cx| {
20688 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
20689 })
20690 .await
20691 .unwrap();
20692 drop(editor_2);
20693 pane_2.update(cx, |pane, cx| {
20694 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20695 open_editor.update(cx, |editor, cx| {
20696 assert_eq!(
20697 editor.display_text(cx),
20698 lib_text,
20699 "Other file should be open and active in another panel too",
20700 );
20701 });
20702 assert_eq!(
20703 pane.items().count(),
20704 1,
20705 "No other editors should be open in another pane",
20706 );
20707 });
20708
20709 let _editor_1_reopened = workspace
20710 .update_in(cx, |workspace, window, cx| {
20711 workspace.open_path(
20712 (worktree_id, "main.rs"),
20713 Some(pane_1.downgrade()),
20714 true,
20715 window,
20716 cx,
20717 )
20718 })
20719 .unwrap()
20720 .await
20721 .downcast::<Editor>()
20722 .unwrap();
20723 let _editor_2_reopened = workspace
20724 .update_in(cx, |workspace, window, cx| {
20725 workspace.open_path(
20726 (worktree_id, "main.rs"),
20727 Some(pane_2.downgrade()),
20728 true,
20729 window,
20730 cx,
20731 )
20732 })
20733 .unwrap()
20734 .await
20735 .downcast::<Editor>()
20736 .unwrap();
20737 pane_1.update(cx, |pane, cx| {
20738 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20739 open_editor.update(cx, |editor, cx| {
20740 assert_eq!(
20741 editor.display_text(cx),
20742 main_text,
20743 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
20744 );
20745 assert_eq!(
20746 editor
20747 .selections
20748 .all::<Point>(cx)
20749 .into_iter()
20750 .map(|s| s.range())
20751 .collect::<Vec<_>>(),
20752 expected_ranges,
20753 "Previous editor in the 1st panel had selections and should get them restored on reopen",
20754 );
20755 })
20756 });
20757 pane_2.update(cx, |pane, cx| {
20758 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20759 open_editor.update(cx, |editor, cx| {
20760 assert_eq!(
20761 editor.display_text(cx),
20762 r#"fn main() {
20763⋯rintln!("1");
20764⋯intln!("2");
20765⋯ntln!("3");
20766println!("4");
20767println!("5");
20768}"#,
20769 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
20770 );
20771 assert_eq!(
20772 editor
20773 .selections
20774 .all::<Point>(cx)
20775 .into_iter()
20776 .map(|s| s.range())
20777 .collect::<Vec<_>>(),
20778 vec![Point::zero()..Point::zero()],
20779 "Previous editor in the 2nd pane had no selections changed hence should restore none",
20780 );
20781 })
20782 });
20783}
20784
20785#[gpui::test]
20786async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
20787 init_test(cx, |_| {});
20788
20789 let fs = FakeFs::new(cx.executor());
20790 let main_text = r#"fn main() {
20791println!("1");
20792println!("2");
20793println!("3");
20794println!("4");
20795println!("5");
20796}"#;
20797 let lib_text = "mod foo {}";
20798 fs.insert_tree(
20799 path!("/a"),
20800 json!({
20801 "lib.rs": lib_text,
20802 "main.rs": main_text,
20803 }),
20804 )
20805 .await;
20806
20807 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20808 let (workspace, cx) =
20809 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20810 let worktree_id = workspace.update(cx, |workspace, cx| {
20811 workspace.project().update(cx, |project, cx| {
20812 project.worktrees(cx).next().unwrap().read(cx).id()
20813 })
20814 });
20815
20816 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20817 let editor = workspace
20818 .update_in(cx, |workspace, window, cx| {
20819 workspace.open_path(
20820 (worktree_id, "main.rs"),
20821 Some(pane.downgrade()),
20822 true,
20823 window,
20824 cx,
20825 )
20826 })
20827 .unwrap()
20828 .await
20829 .downcast::<Editor>()
20830 .unwrap();
20831 pane.update(cx, |pane, cx| {
20832 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20833 open_editor.update(cx, |editor, cx| {
20834 assert_eq!(
20835 editor.display_text(cx),
20836 main_text,
20837 "Original main.rs text on initial open",
20838 );
20839 })
20840 });
20841 editor.update_in(cx, |editor, window, cx| {
20842 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
20843 });
20844
20845 cx.update_global(|store: &mut SettingsStore, cx| {
20846 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
20847 s.restore_on_file_reopen = Some(false);
20848 });
20849 });
20850 editor.update_in(cx, |editor, window, cx| {
20851 editor.fold_ranges(
20852 vec![
20853 Point::new(1, 0)..Point::new(1, 1),
20854 Point::new(2, 0)..Point::new(2, 2),
20855 Point::new(3, 0)..Point::new(3, 3),
20856 ],
20857 false,
20858 window,
20859 cx,
20860 );
20861 });
20862 pane.update_in(cx, |pane, window, cx| {
20863 pane.close_all_items(&CloseAllItems::default(), window, cx)
20864 })
20865 .await
20866 .unwrap();
20867 pane.update(cx, |pane, _| {
20868 assert!(pane.active_item().is_none());
20869 });
20870 cx.update_global(|store: &mut SettingsStore, cx| {
20871 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
20872 s.restore_on_file_reopen = Some(true);
20873 });
20874 });
20875
20876 let _editor_reopened = workspace
20877 .update_in(cx, |workspace, window, cx| {
20878 workspace.open_path(
20879 (worktree_id, "main.rs"),
20880 Some(pane.downgrade()),
20881 true,
20882 window,
20883 cx,
20884 )
20885 })
20886 .unwrap()
20887 .await
20888 .downcast::<Editor>()
20889 .unwrap();
20890 pane.update(cx, |pane, cx| {
20891 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20892 open_editor.update(cx, |editor, cx| {
20893 assert_eq!(
20894 editor.display_text(cx),
20895 main_text,
20896 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
20897 );
20898 })
20899 });
20900}
20901
20902#[gpui::test]
20903async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
20904 struct EmptyModalView {
20905 focus_handle: gpui::FocusHandle,
20906 }
20907 impl EventEmitter<DismissEvent> for EmptyModalView {}
20908 impl Render for EmptyModalView {
20909 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
20910 div()
20911 }
20912 }
20913 impl Focusable for EmptyModalView {
20914 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
20915 self.focus_handle.clone()
20916 }
20917 }
20918 impl workspace::ModalView for EmptyModalView {}
20919 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
20920 EmptyModalView {
20921 focus_handle: cx.focus_handle(),
20922 }
20923 }
20924
20925 init_test(cx, |_| {});
20926
20927 let fs = FakeFs::new(cx.executor());
20928 let project = Project::test(fs, [], cx).await;
20929 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20930 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
20931 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20932 let editor = cx.new_window_entity(|window, cx| {
20933 Editor::new(
20934 EditorMode::full(),
20935 buffer,
20936 Some(project.clone()),
20937 window,
20938 cx,
20939 )
20940 });
20941 workspace
20942 .update(cx, |workspace, window, cx| {
20943 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
20944 })
20945 .unwrap();
20946 editor.update_in(cx, |editor, window, cx| {
20947 editor.open_context_menu(&OpenContextMenu, window, cx);
20948 assert!(editor.mouse_context_menu.is_some());
20949 });
20950 workspace
20951 .update(cx, |workspace, window, cx| {
20952 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
20953 })
20954 .unwrap();
20955 cx.read(|cx| {
20956 assert!(editor.read(cx).mouse_context_menu.is_none());
20957 });
20958}
20959
20960#[gpui::test]
20961async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
20962 init_test(cx, |_| {});
20963
20964 let fs = FakeFs::new(cx.executor());
20965 fs.insert_file(path!("/file.html"), Default::default())
20966 .await;
20967
20968 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
20969
20970 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20971 let html_language = Arc::new(Language::new(
20972 LanguageConfig {
20973 name: "HTML".into(),
20974 matcher: LanguageMatcher {
20975 path_suffixes: vec!["html".to_string()],
20976 ..LanguageMatcher::default()
20977 },
20978 brackets: BracketPairConfig {
20979 pairs: vec![BracketPair {
20980 start: "<".into(),
20981 end: ">".into(),
20982 close: true,
20983 ..Default::default()
20984 }],
20985 ..Default::default()
20986 },
20987 ..Default::default()
20988 },
20989 Some(tree_sitter_html::LANGUAGE.into()),
20990 ));
20991 language_registry.add(html_language);
20992 let mut fake_servers = language_registry.register_fake_lsp(
20993 "HTML",
20994 FakeLspAdapter {
20995 capabilities: lsp::ServerCapabilities {
20996 completion_provider: Some(lsp::CompletionOptions {
20997 resolve_provider: Some(true),
20998 ..Default::default()
20999 }),
21000 ..Default::default()
21001 },
21002 ..Default::default()
21003 },
21004 );
21005
21006 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21007 let cx = &mut VisualTestContext::from_window(*workspace, cx);
21008
21009 let worktree_id = workspace
21010 .update(cx, |workspace, _window, cx| {
21011 workspace.project().update(cx, |project, cx| {
21012 project.worktrees(cx).next().unwrap().read(cx).id()
21013 })
21014 })
21015 .unwrap();
21016 project
21017 .update(cx, |project, cx| {
21018 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
21019 })
21020 .await
21021 .unwrap();
21022 let editor = workspace
21023 .update(cx, |workspace, window, cx| {
21024 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
21025 })
21026 .unwrap()
21027 .await
21028 .unwrap()
21029 .downcast::<Editor>()
21030 .unwrap();
21031
21032 let fake_server = fake_servers.next().await.unwrap();
21033 editor.update_in(cx, |editor, window, cx| {
21034 editor.set_text("<ad></ad>", window, cx);
21035 editor.change_selections(None, window, cx, |selections| {
21036 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
21037 });
21038 let Some((buffer, _)) = editor
21039 .buffer
21040 .read(cx)
21041 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
21042 else {
21043 panic!("Failed to get buffer for selection position");
21044 };
21045 let buffer = buffer.read(cx);
21046 let buffer_id = buffer.remote_id();
21047 let opening_range =
21048 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
21049 let closing_range =
21050 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
21051 let mut linked_ranges = HashMap::default();
21052 linked_ranges.insert(
21053 buffer_id,
21054 vec![(opening_range.clone(), vec![closing_range.clone()])],
21055 );
21056 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
21057 });
21058 let mut completion_handle =
21059 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
21060 Ok(Some(lsp::CompletionResponse::Array(vec![
21061 lsp::CompletionItem {
21062 label: "head".to_string(),
21063 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
21064 lsp::InsertReplaceEdit {
21065 new_text: "head".to_string(),
21066 insert: lsp::Range::new(
21067 lsp::Position::new(0, 1),
21068 lsp::Position::new(0, 3),
21069 ),
21070 replace: lsp::Range::new(
21071 lsp::Position::new(0, 1),
21072 lsp::Position::new(0, 3),
21073 ),
21074 },
21075 )),
21076 ..Default::default()
21077 },
21078 ])))
21079 });
21080 editor.update_in(cx, |editor, window, cx| {
21081 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
21082 });
21083 cx.run_until_parked();
21084 completion_handle.next().await.unwrap();
21085 editor.update(cx, |editor, _| {
21086 assert!(
21087 editor.context_menu_visible(),
21088 "Completion menu should be visible"
21089 );
21090 });
21091 editor.update_in(cx, |editor, window, cx| {
21092 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
21093 });
21094 cx.executor().run_until_parked();
21095 editor.update(cx, |editor, cx| {
21096 assert_eq!(editor.text(cx), "<head></head>");
21097 });
21098}
21099
21100#[gpui::test]
21101async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
21102 init_test(cx, |_| {});
21103
21104 let fs = FakeFs::new(cx.executor());
21105 fs.insert_tree(
21106 path!("/root"),
21107 json!({
21108 "a": {
21109 "main.rs": "fn main() {}",
21110 },
21111 "foo": {
21112 "bar": {
21113 "external_file.rs": "pub mod external {}",
21114 }
21115 }
21116 }),
21117 )
21118 .await;
21119
21120 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
21121 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21122 language_registry.add(rust_lang());
21123 let _fake_servers = language_registry.register_fake_lsp(
21124 "Rust",
21125 FakeLspAdapter {
21126 ..FakeLspAdapter::default()
21127 },
21128 );
21129 let (workspace, cx) =
21130 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21131 let worktree_id = workspace.update(cx, |workspace, cx| {
21132 workspace.project().update(cx, |project, cx| {
21133 project.worktrees(cx).next().unwrap().read(cx).id()
21134 })
21135 });
21136
21137 let assert_language_servers_count =
21138 |expected: usize, context: &str, cx: &mut VisualTestContext| {
21139 project.update(cx, |project, cx| {
21140 let current = project
21141 .lsp_store()
21142 .read(cx)
21143 .as_local()
21144 .unwrap()
21145 .language_servers
21146 .len();
21147 assert_eq!(expected, current, "{context}");
21148 });
21149 };
21150
21151 assert_language_servers_count(
21152 0,
21153 "No servers should be running before any file is open",
21154 cx,
21155 );
21156 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21157 let main_editor = workspace
21158 .update_in(cx, |workspace, window, cx| {
21159 workspace.open_path(
21160 (worktree_id, "main.rs"),
21161 Some(pane.downgrade()),
21162 true,
21163 window,
21164 cx,
21165 )
21166 })
21167 .unwrap()
21168 .await
21169 .downcast::<Editor>()
21170 .unwrap();
21171 pane.update(cx, |pane, cx| {
21172 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21173 open_editor.update(cx, |editor, cx| {
21174 assert_eq!(
21175 editor.display_text(cx),
21176 "fn main() {}",
21177 "Original main.rs text on initial open",
21178 );
21179 });
21180 assert_eq!(open_editor, main_editor);
21181 });
21182 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
21183
21184 let external_editor = workspace
21185 .update_in(cx, |workspace, window, cx| {
21186 workspace.open_abs_path(
21187 PathBuf::from("/root/foo/bar/external_file.rs"),
21188 OpenOptions::default(),
21189 window,
21190 cx,
21191 )
21192 })
21193 .await
21194 .expect("opening external file")
21195 .downcast::<Editor>()
21196 .expect("downcasted external file's open element to editor");
21197 pane.update(cx, |pane, cx| {
21198 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21199 open_editor.update(cx, |editor, cx| {
21200 assert_eq!(
21201 editor.display_text(cx),
21202 "pub mod external {}",
21203 "External file is open now",
21204 );
21205 });
21206 assert_eq!(open_editor, external_editor);
21207 });
21208 assert_language_servers_count(
21209 1,
21210 "Second, external, *.rs file should join the existing server",
21211 cx,
21212 );
21213
21214 pane.update_in(cx, |pane, window, cx| {
21215 pane.close_active_item(&CloseActiveItem::default(), window, cx)
21216 })
21217 .await
21218 .unwrap();
21219 pane.update_in(cx, |pane, window, cx| {
21220 pane.navigate_backward(window, cx);
21221 });
21222 cx.run_until_parked();
21223 pane.update(cx, |pane, cx| {
21224 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21225 open_editor.update(cx, |editor, cx| {
21226 assert_eq!(
21227 editor.display_text(cx),
21228 "pub mod external {}",
21229 "External file is open now",
21230 );
21231 });
21232 });
21233 assert_language_servers_count(
21234 1,
21235 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
21236 cx,
21237 );
21238
21239 cx.update(|_, cx| {
21240 workspace::reload(&workspace::Reload::default(), cx);
21241 });
21242 assert_language_servers_count(
21243 1,
21244 "After reloading the worktree with local and external files opened, only one project should be started",
21245 cx,
21246 );
21247}
21248
21249#[gpui::test]
21250async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
21251 init_test(cx, |_| {});
21252
21253 let mut cx = EditorTestContext::new(cx).await;
21254 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
21255 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21256
21257 // test cursor move to start of each line on tab
21258 // for `if`, `elif`, `else`, `while`, `with` and `for`
21259 cx.set_state(indoc! {"
21260 def main():
21261 ˇ for item in items:
21262 ˇ while item.active:
21263 ˇ if item.value > 10:
21264 ˇ continue
21265 ˇ elif item.value < 0:
21266 ˇ break
21267 ˇ else:
21268 ˇ with item.context() as ctx:
21269 ˇ yield count
21270 ˇ else:
21271 ˇ log('while else')
21272 ˇ else:
21273 ˇ log('for else')
21274 "});
21275 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21276 cx.assert_editor_state(indoc! {"
21277 def main():
21278 ˇfor item in items:
21279 ˇwhile item.active:
21280 ˇif item.value > 10:
21281 ˇcontinue
21282 ˇelif item.value < 0:
21283 ˇbreak
21284 ˇelse:
21285 ˇwith item.context() as ctx:
21286 ˇyield count
21287 ˇelse:
21288 ˇlog('while else')
21289 ˇelse:
21290 ˇlog('for else')
21291 "});
21292 // test relative indent is preserved when tab
21293 // for `if`, `elif`, `else`, `while`, `with` and `for`
21294 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21295 cx.assert_editor_state(indoc! {"
21296 def main():
21297 ˇfor item in items:
21298 ˇwhile item.active:
21299 ˇif item.value > 10:
21300 ˇcontinue
21301 ˇelif item.value < 0:
21302 ˇbreak
21303 ˇelse:
21304 ˇwith item.context() as ctx:
21305 ˇyield count
21306 ˇelse:
21307 ˇlog('while else')
21308 ˇelse:
21309 ˇlog('for else')
21310 "});
21311
21312 // test cursor move to start of each line on tab
21313 // for `try`, `except`, `else`, `finally`, `match` and `def`
21314 cx.set_state(indoc! {"
21315 def main():
21316 ˇ try:
21317 ˇ fetch()
21318 ˇ except ValueError:
21319 ˇ handle_error()
21320 ˇ else:
21321 ˇ match value:
21322 ˇ case _:
21323 ˇ finally:
21324 ˇ def status():
21325 ˇ return 0
21326 "});
21327 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21328 cx.assert_editor_state(indoc! {"
21329 def main():
21330 ˇtry:
21331 ˇfetch()
21332 ˇexcept ValueError:
21333 ˇhandle_error()
21334 ˇelse:
21335 ˇmatch value:
21336 ˇcase _:
21337 ˇfinally:
21338 ˇdef status():
21339 ˇreturn 0
21340 "});
21341 // test relative indent is preserved when tab
21342 // for `try`, `except`, `else`, `finally`, `match` and `def`
21343 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21344 cx.assert_editor_state(indoc! {"
21345 def main():
21346 ˇtry:
21347 ˇfetch()
21348 ˇexcept ValueError:
21349 ˇhandle_error()
21350 ˇelse:
21351 ˇmatch value:
21352 ˇcase _:
21353 ˇfinally:
21354 ˇdef status():
21355 ˇreturn 0
21356 "});
21357}
21358
21359#[gpui::test]
21360async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
21361 init_test(cx, |_| {});
21362
21363 let mut cx = EditorTestContext::new(cx).await;
21364 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
21365 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21366
21367 // test `else` auto outdents when typed inside `if` block
21368 cx.set_state(indoc! {"
21369 def main():
21370 if i == 2:
21371 return
21372 ˇ
21373 "});
21374 cx.update_editor(|editor, window, cx| {
21375 editor.handle_input("else:", window, cx);
21376 });
21377 cx.assert_editor_state(indoc! {"
21378 def main():
21379 if i == 2:
21380 return
21381 else:ˇ
21382 "});
21383
21384 // test `except` auto outdents when typed inside `try` block
21385 cx.set_state(indoc! {"
21386 def main():
21387 try:
21388 i = 2
21389 ˇ
21390 "});
21391 cx.update_editor(|editor, window, cx| {
21392 editor.handle_input("except:", window, cx);
21393 });
21394 cx.assert_editor_state(indoc! {"
21395 def main():
21396 try:
21397 i = 2
21398 except:ˇ
21399 "});
21400
21401 // test `else` auto outdents when typed inside `except` block
21402 cx.set_state(indoc! {"
21403 def main():
21404 try:
21405 i = 2
21406 except:
21407 j = 2
21408 ˇ
21409 "});
21410 cx.update_editor(|editor, window, cx| {
21411 editor.handle_input("else:", window, cx);
21412 });
21413 cx.assert_editor_state(indoc! {"
21414 def main():
21415 try:
21416 i = 2
21417 except:
21418 j = 2
21419 else:ˇ
21420 "});
21421
21422 // test `finally` auto outdents when typed inside `else` block
21423 cx.set_state(indoc! {"
21424 def main():
21425 try:
21426 i = 2
21427 except:
21428 j = 2
21429 else:
21430 k = 2
21431 ˇ
21432 "});
21433 cx.update_editor(|editor, window, cx| {
21434 editor.handle_input("finally:", window, cx);
21435 });
21436 cx.assert_editor_state(indoc! {"
21437 def main():
21438 try:
21439 i = 2
21440 except:
21441 j = 2
21442 else:
21443 k = 2
21444 finally:ˇ
21445 "});
21446
21447 // TODO: test `except` auto outdents when typed inside `try` block right after for block
21448 // cx.set_state(indoc! {"
21449 // def main():
21450 // try:
21451 // for i in range(n):
21452 // pass
21453 // ˇ
21454 // "});
21455 // cx.update_editor(|editor, window, cx| {
21456 // editor.handle_input("except:", window, cx);
21457 // });
21458 // cx.assert_editor_state(indoc! {"
21459 // def main():
21460 // try:
21461 // for i in range(n):
21462 // pass
21463 // except:ˇ
21464 // "});
21465
21466 // TODO: test `else` auto outdents when typed inside `except` block right after for block
21467 // cx.set_state(indoc! {"
21468 // def main():
21469 // try:
21470 // i = 2
21471 // except:
21472 // for i in range(n):
21473 // pass
21474 // ˇ
21475 // "});
21476 // cx.update_editor(|editor, window, cx| {
21477 // editor.handle_input("else:", window, cx);
21478 // });
21479 // cx.assert_editor_state(indoc! {"
21480 // def main():
21481 // try:
21482 // i = 2
21483 // except:
21484 // for i in range(n):
21485 // pass
21486 // else:ˇ
21487 // "});
21488
21489 // TODO: test `finally` auto outdents when typed inside `else` block right after for block
21490 // cx.set_state(indoc! {"
21491 // def main():
21492 // try:
21493 // i = 2
21494 // except:
21495 // j = 2
21496 // else:
21497 // for i in range(n):
21498 // pass
21499 // ˇ
21500 // "});
21501 // cx.update_editor(|editor, window, cx| {
21502 // editor.handle_input("finally:", window, cx);
21503 // });
21504 // cx.assert_editor_state(indoc! {"
21505 // def main():
21506 // try:
21507 // i = 2
21508 // except:
21509 // j = 2
21510 // else:
21511 // for i in range(n):
21512 // pass
21513 // finally:ˇ
21514 // "});
21515
21516 // test `else` stays at correct indent when typed after `for` block
21517 cx.set_state(indoc! {"
21518 def main():
21519 for i in range(10):
21520 if i == 3:
21521 break
21522 ˇ
21523 "});
21524 cx.update_editor(|editor, window, cx| {
21525 editor.handle_input("else:", window, cx);
21526 });
21527 cx.assert_editor_state(indoc! {"
21528 def main():
21529 for i in range(10):
21530 if i == 3:
21531 break
21532 else:ˇ
21533 "});
21534
21535 // test does not outdent on typing after line with square brackets
21536 cx.set_state(indoc! {"
21537 def f() -> list[str]:
21538 ˇ
21539 "});
21540 cx.update_editor(|editor, window, cx| {
21541 editor.handle_input("a", window, cx);
21542 });
21543 cx.assert_editor_state(indoc! {"
21544 def f() -> list[str]:
21545 aˇ
21546 "});
21547}
21548
21549#[gpui::test]
21550async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
21551 init_test(cx, |_| {});
21552 update_test_language_settings(cx, |settings| {
21553 settings.defaults.extend_comment_on_newline = Some(false);
21554 });
21555 let mut cx = EditorTestContext::new(cx).await;
21556 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
21557 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21558
21559 // test correct indent after newline on comment
21560 cx.set_state(indoc! {"
21561 # COMMENT:ˇ
21562 "});
21563 cx.update_editor(|editor, window, cx| {
21564 editor.newline(&Newline, window, cx);
21565 });
21566 cx.assert_editor_state(indoc! {"
21567 # COMMENT:
21568 ˇ
21569 "});
21570
21571 // test correct indent after newline in brackets
21572 cx.set_state(indoc! {"
21573 {ˇ}
21574 "});
21575 cx.update_editor(|editor, window, cx| {
21576 editor.newline(&Newline, window, cx);
21577 });
21578 cx.run_until_parked();
21579 cx.assert_editor_state(indoc! {"
21580 {
21581 ˇ
21582 }
21583 "});
21584
21585 cx.set_state(indoc! {"
21586 (ˇ)
21587 "});
21588 cx.update_editor(|editor, window, cx| {
21589 editor.newline(&Newline, window, cx);
21590 });
21591 cx.run_until_parked();
21592 cx.assert_editor_state(indoc! {"
21593 (
21594 ˇ
21595 )
21596 "});
21597
21598 // do not indent after empty lists or dictionaries
21599 cx.set_state(indoc! {"
21600 a = []ˇ
21601 "});
21602 cx.update_editor(|editor, window, cx| {
21603 editor.newline(&Newline, window, cx);
21604 });
21605 cx.run_until_parked();
21606 cx.assert_editor_state(indoc! {"
21607 a = []
21608 ˇ
21609 "});
21610}
21611
21612fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
21613 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
21614 point..point
21615}
21616
21617#[track_caller]
21618fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
21619 let (text, ranges) = marked_text_ranges(marked_text, true);
21620 assert_eq!(editor.text(cx), text);
21621 assert_eq!(
21622 editor.selections.ranges(cx),
21623 ranges,
21624 "Assert selections are {}",
21625 marked_text
21626 );
21627}
21628
21629pub fn handle_signature_help_request(
21630 cx: &mut EditorLspTestContext,
21631 mocked_response: lsp::SignatureHelp,
21632) -> impl Future<Output = ()> + use<> {
21633 let mut request =
21634 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
21635 let mocked_response = mocked_response.clone();
21636 async move { Ok(Some(mocked_response)) }
21637 });
21638
21639 async move {
21640 request.next().await;
21641 }
21642}
21643
21644#[track_caller]
21645pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
21646 cx.update_editor(|editor, _, _| {
21647 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
21648 let entries = menu.entries.borrow();
21649 let entries = entries
21650 .iter()
21651 .map(|entry| entry.string.as_str())
21652 .collect::<Vec<_>>();
21653 assert_eq!(entries, expected);
21654 } else {
21655 panic!("Expected completions menu");
21656 }
21657 });
21658}
21659
21660/// Handle completion request passing a marked string specifying where the completion
21661/// should be triggered from using '|' character, what range should be replaced, and what completions
21662/// should be returned using '<' and '>' to delimit the range.
21663///
21664/// Also see `handle_completion_request_with_insert_and_replace`.
21665#[track_caller]
21666pub fn handle_completion_request(
21667 marked_string: &str,
21668 completions: Vec<&'static str>,
21669 is_incomplete: bool,
21670 counter: Arc<AtomicUsize>,
21671 cx: &mut EditorLspTestContext,
21672) -> impl Future<Output = ()> {
21673 let complete_from_marker: TextRangeMarker = '|'.into();
21674 let replace_range_marker: TextRangeMarker = ('<', '>').into();
21675 let (_, mut marked_ranges) = marked_text_ranges_by(
21676 marked_string,
21677 vec![complete_from_marker.clone(), replace_range_marker.clone()],
21678 );
21679
21680 let complete_from_position =
21681 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
21682 let replace_range =
21683 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
21684
21685 let mut request =
21686 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
21687 let completions = completions.clone();
21688 counter.fetch_add(1, atomic::Ordering::Release);
21689 async move {
21690 assert_eq!(params.text_document_position.text_document.uri, url.clone());
21691 assert_eq!(
21692 params.text_document_position.position,
21693 complete_from_position
21694 );
21695 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
21696 is_incomplete: is_incomplete,
21697 item_defaults: None,
21698 items: completions
21699 .iter()
21700 .map(|completion_text| lsp::CompletionItem {
21701 label: completion_text.to_string(),
21702 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
21703 range: replace_range,
21704 new_text: completion_text.to_string(),
21705 })),
21706 ..Default::default()
21707 })
21708 .collect(),
21709 })))
21710 }
21711 });
21712
21713 async move {
21714 request.next().await;
21715 }
21716}
21717
21718/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
21719/// given instead, which also contains an `insert` range.
21720///
21721/// This function uses markers to define ranges:
21722/// - `|` marks the cursor position
21723/// - `<>` marks the replace range
21724/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
21725pub fn handle_completion_request_with_insert_and_replace(
21726 cx: &mut EditorLspTestContext,
21727 marked_string: &str,
21728 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
21729 counter: Arc<AtomicUsize>,
21730) -> impl Future<Output = ()> {
21731 let complete_from_marker: TextRangeMarker = '|'.into();
21732 let replace_range_marker: TextRangeMarker = ('<', '>').into();
21733 let insert_range_marker: TextRangeMarker = ('{', '}').into();
21734
21735 let (_, mut marked_ranges) = marked_text_ranges_by(
21736 marked_string,
21737 vec![
21738 complete_from_marker.clone(),
21739 replace_range_marker.clone(),
21740 insert_range_marker.clone(),
21741 ],
21742 );
21743
21744 let complete_from_position =
21745 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
21746 let replace_range =
21747 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
21748
21749 let insert_range = match marked_ranges.remove(&insert_range_marker) {
21750 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
21751 _ => lsp::Range {
21752 start: replace_range.start,
21753 end: complete_from_position,
21754 },
21755 };
21756
21757 let mut request =
21758 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
21759 let completions = completions.clone();
21760 counter.fetch_add(1, atomic::Ordering::Release);
21761 async move {
21762 assert_eq!(params.text_document_position.text_document.uri, url.clone());
21763 assert_eq!(
21764 params.text_document_position.position, complete_from_position,
21765 "marker `|` position doesn't match",
21766 );
21767 Ok(Some(lsp::CompletionResponse::Array(
21768 completions
21769 .iter()
21770 .map(|(label, new_text)| lsp::CompletionItem {
21771 label: label.to_string(),
21772 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
21773 lsp::InsertReplaceEdit {
21774 insert: insert_range,
21775 replace: replace_range,
21776 new_text: new_text.to_string(),
21777 },
21778 )),
21779 ..Default::default()
21780 })
21781 .collect(),
21782 )))
21783 }
21784 });
21785
21786 async move {
21787 request.next().await;
21788 }
21789}
21790
21791fn handle_resolve_completion_request(
21792 cx: &mut EditorLspTestContext,
21793 edits: Option<Vec<(&'static str, &'static str)>>,
21794) -> impl Future<Output = ()> {
21795 let edits = edits.map(|edits| {
21796 edits
21797 .iter()
21798 .map(|(marked_string, new_text)| {
21799 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
21800 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
21801 lsp::TextEdit::new(replace_range, new_text.to_string())
21802 })
21803 .collect::<Vec<_>>()
21804 });
21805
21806 let mut request =
21807 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
21808 let edits = edits.clone();
21809 async move {
21810 Ok(lsp::CompletionItem {
21811 additional_text_edits: edits,
21812 ..Default::default()
21813 })
21814 }
21815 });
21816
21817 async move {
21818 request.next().await;
21819 }
21820}
21821
21822pub(crate) fn update_test_language_settings(
21823 cx: &mut TestAppContext,
21824 f: impl Fn(&mut AllLanguageSettingsContent),
21825) {
21826 cx.update(|cx| {
21827 SettingsStore::update_global(cx, |store, cx| {
21828 store.update_user_settings::<AllLanguageSettings>(cx, f);
21829 });
21830 });
21831}
21832
21833pub(crate) fn update_test_project_settings(
21834 cx: &mut TestAppContext,
21835 f: impl Fn(&mut ProjectSettings),
21836) {
21837 cx.update(|cx| {
21838 SettingsStore::update_global(cx, |store, cx| {
21839 store.update_user_settings::<ProjectSettings>(cx, f);
21840 });
21841 });
21842}
21843
21844pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
21845 cx.update(|cx| {
21846 assets::Assets.load_test_fonts(cx);
21847 let store = SettingsStore::test(cx);
21848 cx.set_global(store);
21849 theme::init(theme::LoadThemes::JustBase, cx);
21850 release_channel::init(SemanticVersion::default(), cx);
21851 client::init_settings(cx);
21852 language::init(cx);
21853 Project::init_settings(cx);
21854 workspace::init_settings(cx);
21855 crate::init(cx);
21856 });
21857
21858 update_test_language_settings(cx, f);
21859}
21860
21861#[track_caller]
21862fn assert_hunk_revert(
21863 not_reverted_text_with_selections: &str,
21864 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
21865 expected_reverted_text_with_selections: &str,
21866 base_text: &str,
21867 cx: &mut EditorLspTestContext,
21868) {
21869 cx.set_state(not_reverted_text_with_selections);
21870 cx.set_head_text(base_text);
21871 cx.executor().run_until_parked();
21872
21873 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
21874 let snapshot = editor.snapshot(window, cx);
21875 let reverted_hunk_statuses = snapshot
21876 .buffer_snapshot
21877 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
21878 .map(|hunk| hunk.status().kind)
21879 .collect::<Vec<_>>();
21880
21881 editor.git_restore(&Default::default(), window, cx);
21882 reverted_hunk_statuses
21883 });
21884 cx.executor().run_until_parked();
21885 cx.assert_editor_state(expected_reverted_text_with_selections);
21886 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
21887}
21888
21889#[gpui::test(iterations = 10)]
21890async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
21891 init_test(cx, |_| {});
21892
21893 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
21894 let counter = diagnostic_requests.clone();
21895
21896 let fs = FakeFs::new(cx.executor());
21897 fs.insert_tree(
21898 path!("/a"),
21899 json!({
21900 "first.rs": "fn main() { let a = 5; }",
21901 "second.rs": "// Test file",
21902 }),
21903 )
21904 .await;
21905
21906 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21907 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21908 let cx = &mut VisualTestContext::from_window(*workspace, cx);
21909
21910 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21911 language_registry.add(rust_lang());
21912 let mut fake_servers = language_registry.register_fake_lsp(
21913 "Rust",
21914 FakeLspAdapter {
21915 capabilities: lsp::ServerCapabilities {
21916 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
21917 lsp::DiagnosticOptions {
21918 identifier: None,
21919 inter_file_dependencies: true,
21920 workspace_diagnostics: true,
21921 work_done_progress_options: Default::default(),
21922 },
21923 )),
21924 ..Default::default()
21925 },
21926 ..Default::default()
21927 },
21928 );
21929
21930 let editor = workspace
21931 .update(cx, |workspace, window, cx| {
21932 workspace.open_abs_path(
21933 PathBuf::from(path!("/a/first.rs")),
21934 OpenOptions::default(),
21935 window,
21936 cx,
21937 )
21938 })
21939 .unwrap()
21940 .await
21941 .unwrap()
21942 .downcast::<Editor>()
21943 .unwrap();
21944 let fake_server = fake_servers.next().await.unwrap();
21945 let mut first_request = fake_server
21946 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
21947 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
21948 let result_id = Some(new_result_id.to_string());
21949 assert_eq!(
21950 params.text_document.uri,
21951 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
21952 );
21953 async move {
21954 Ok(lsp::DocumentDiagnosticReportResult::Report(
21955 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
21956 related_documents: None,
21957 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
21958 items: Vec::new(),
21959 result_id,
21960 },
21961 }),
21962 ))
21963 }
21964 });
21965
21966 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
21967 project.update(cx, |project, cx| {
21968 let buffer_id = editor
21969 .read(cx)
21970 .buffer()
21971 .read(cx)
21972 .as_singleton()
21973 .expect("created a singleton buffer")
21974 .read(cx)
21975 .remote_id();
21976 let buffer_result_id = project.lsp_store().read(cx).result_id(buffer_id, cx);
21977 assert_eq!(expected, buffer_result_id);
21978 });
21979 };
21980
21981 ensure_result_id(None, cx);
21982 cx.executor().advance_clock(Duration::from_millis(60));
21983 cx.executor().run_until_parked();
21984 assert_eq!(
21985 diagnostic_requests.load(atomic::Ordering::Acquire),
21986 1,
21987 "Opening file should trigger diagnostic request"
21988 );
21989 first_request
21990 .next()
21991 .await
21992 .expect("should have sent the first diagnostics pull request");
21993 ensure_result_id(Some("1".to_string()), cx);
21994
21995 // Editing should trigger diagnostics
21996 editor.update_in(cx, |editor, window, cx| {
21997 editor.handle_input("2", window, cx)
21998 });
21999 cx.executor().advance_clock(Duration::from_millis(60));
22000 cx.executor().run_until_parked();
22001 assert_eq!(
22002 diagnostic_requests.load(atomic::Ordering::Acquire),
22003 2,
22004 "Editing should trigger diagnostic request"
22005 );
22006 ensure_result_id(Some("2".to_string()), cx);
22007
22008 // Moving cursor should not trigger diagnostic request
22009 editor.update_in(cx, |editor, window, cx| {
22010 editor.change_selections(None, window, cx, |s| {
22011 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
22012 });
22013 });
22014 cx.executor().advance_clock(Duration::from_millis(60));
22015 cx.executor().run_until_parked();
22016 assert_eq!(
22017 diagnostic_requests.load(atomic::Ordering::Acquire),
22018 2,
22019 "Cursor movement should not trigger diagnostic request"
22020 );
22021 ensure_result_id(Some("2".to_string()), cx);
22022 // Multiple rapid edits should be debounced
22023 for _ in 0..5 {
22024 editor.update_in(cx, |editor, window, cx| {
22025 editor.handle_input("x", window, cx)
22026 });
22027 }
22028 cx.executor().advance_clock(Duration::from_millis(60));
22029 cx.executor().run_until_parked();
22030
22031 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
22032 assert!(
22033 final_requests <= 4,
22034 "Multiple rapid edits should be debounced (got {final_requests} requests)",
22035 );
22036 ensure_result_id(Some(final_requests.to_string()), cx);
22037}
22038
22039#[gpui::test]
22040async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
22041 // Regression test for issue #11671
22042 // Previously, adding a cursor after moving multiple cursors would reset
22043 // the cursor count instead of adding to the existing cursors.
22044 init_test(cx, |_| {});
22045 let mut cx = EditorTestContext::new(cx).await;
22046
22047 // Create a simple buffer with cursor at start
22048 cx.set_state(indoc! {"
22049 ˇaaaa
22050 bbbb
22051 cccc
22052 dddd
22053 eeee
22054 ffff
22055 gggg
22056 hhhh"});
22057
22058 // Add 2 cursors below (so we have 3 total)
22059 cx.update_editor(|editor, window, cx| {
22060 editor.add_selection_below(&Default::default(), window, cx);
22061 editor.add_selection_below(&Default::default(), window, cx);
22062 });
22063
22064 // Verify we have 3 cursors
22065 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
22066 assert_eq!(
22067 initial_count, 3,
22068 "Should have 3 cursors after adding 2 below"
22069 );
22070
22071 // Move down one line
22072 cx.update_editor(|editor, window, cx| {
22073 editor.move_down(&MoveDown, window, cx);
22074 });
22075
22076 // Add another cursor below
22077 cx.update_editor(|editor, window, cx| {
22078 editor.add_selection_below(&Default::default(), window, cx);
22079 });
22080
22081 // Should now have 4 cursors (3 original + 1 new)
22082 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
22083 assert_eq!(
22084 final_count, 4,
22085 "Should have 4 cursors after moving and adding another"
22086 );
22087}