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, SaveOptions},
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(
9045 SaveOptions {
9046 format: true,
9047 autosave: false,
9048 },
9049 project.clone(),
9050 window,
9051 cx,
9052 )
9053 })
9054 .unwrap();
9055 cx.executor().start_waiting();
9056 save.await;
9057
9058 assert_eq!(
9059 editor.update(cx, |editor, cx| editor.text(cx)),
9060 "one, two\nthree\n"
9061 );
9062 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9063 }
9064
9065 {
9066 editor.update_in(cx, |editor, window, cx| {
9067 editor.set_text("one\ntwo\nthree\n", window, cx)
9068 });
9069 assert!(cx.read(|cx| editor.is_dirty(cx)));
9070
9071 // Ensure we can still save even if formatting hangs.
9072 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9073 move |params, _| async move {
9074 assert_eq!(
9075 params.text_document.uri,
9076 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9077 );
9078 futures::future::pending::<()>().await;
9079 unreachable!()
9080 },
9081 );
9082 let save = editor
9083 .update_in(cx, |editor, window, cx| {
9084 editor.save(
9085 SaveOptions {
9086 format: true,
9087 autosave: false,
9088 },
9089 project.clone(),
9090 window,
9091 cx,
9092 )
9093 })
9094 .unwrap();
9095 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9096 cx.executor().start_waiting();
9097 save.await;
9098 assert_eq!(
9099 editor.update(cx, |editor, cx| editor.text(cx)),
9100 "one\ntwo\nthree\n"
9101 );
9102 }
9103
9104 // Set rust language override and assert overridden tabsize is sent to language server
9105 update_test_language_settings(cx, |settings| {
9106 settings.languages.insert(
9107 "Rust".into(),
9108 LanguageSettingsContent {
9109 tab_size: NonZeroU32::new(8),
9110 ..Default::default()
9111 },
9112 );
9113 });
9114
9115 {
9116 editor.update_in(cx, |editor, window, cx| {
9117 editor.set_text("somehting_new\n", window, cx)
9118 });
9119 assert!(cx.read(|cx| editor.is_dirty(cx)));
9120 let _formatting_request_signal = fake_server
9121 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9122 assert_eq!(
9123 params.text_document.uri,
9124 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9125 );
9126 assert_eq!(params.options.tab_size, 8);
9127 Ok(Some(vec![]))
9128 });
9129 let save = editor
9130 .update_in(cx, |editor, window, cx| {
9131 editor.save(
9132 SaveOptions {
9133 format: true,
9134 autosave: false,
9135 },
9136 project.clone(),
9137 window,
9138 cx,
9139 )
9140 })
9141 .unwrap();
9142 cx.executor().start_waiting();
9143 save.await;
9144 }
9145}
9146
9147#[gpui::test]
9148async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
9149 init_test(cx, |_| {});
9150
9151 let cols = 4;
9152 let rows = 10;
9153 let sample_text_1 = sample_text(rows, cols, 'a');
9154 assert_eq!(
9155 sample_text_1,
9156 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
9157 );
9158 let sample_text_2 = sample_text(rows, cols, 'l');
9159 assert_eq!(
9160 sample_text_2,
9161 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
9162 );
9163 let sample_text_3 = sample_text(rows, cols, 'v');
9164 assert_eq!(
9165 sample_text_3,
9166 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
9167 );
9168
9169 let fs = FakeFs::new(cx.executor());
9170 fs.insert_tree(
9171 path!("/a"),
9172 json!({
9173 "main.rs": sample_text_1,
9174 "other.rs": sample_text_2,
9175 "lib.rs": sample_text_3,
9176 }),
9177 )
9178 .await;
9179
9180 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
9181 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9182 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9183
9184 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9185 language_registry.add(rust_lang());
9186 let mut fake_servers = language_registry.register_fake_lsp(
9187 "Rust",
9188 FakeLspAdapter {
9189 capabilities: lsp::ServerCapabilities {
9190 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9191 ..Default::default()
9192 },
9193 ..Default::default()
9194 },
9195 );
9196
9197 let worktree = project.update(cx, |project, cx| {
9198 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
9199 assert_eq!(worktrees.len(), 1);
9200 worktrees.pop().unwrap()
9201 });
9202 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9203
9204 let buffer_1 = project
9205 .update(cx, |project, cx| {
9206 project.open_buffer((worktree_id, "main.rs"), cx)
9207 })
9208 .await
9209 .unwrap();
9210 let buffer_2 = project
9211 .update(cx, |project, cx| {
9212 project.open_buffer((worktree_id, "other.rs"), cx)
9213 })
9214 .await
9215 .unwrap();
9216 let buffer_3 = project
9217 .update(cx, |project, cx| {
9218 project.open_buffer((worktree_id, "lib.rs"), cx)
9219 })
9220 .await
9221 .unwrap();
9222
9223 let multi_buffer = cx.new(|cx| {
9224 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9225 multi_buffer.push_excerpts(
9226 buffer_1.clone(),
9227 [
9228 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9229 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9230 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9231 ],
9232 cx,
9233 );
9234 multi_buffer.push_excerpts(
9235 buffer_2.clone(),
9236 [
9237 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9238 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9239 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9240 ],
9241 cx,
9242 );
9243 multi_buffer.push_excerpts(
9244 buffer_3.clone(),
9245 [
9246 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9247 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9248 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9249 ],
9250 cx,
9251 );
9252 multi_buffer
9253 });
9254 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
9255 Editor::new(
9256 EditorMode::full(),
9257 multi_buffer,
9258 Some(project.clone()),
9259 window,
9260 cx,
9261 )
9262 });
9263
9264 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9265 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
9266 s.select_ranges(Some(1..2))
9267 });
9268 editor.insert("|one|two|three|", window, cx);
9269 });
9270 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9271 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9272 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
9273 s.select_ranges(Some(60..70))
9274 });
9275 editor.insert("|four|five|six|", window, cx);
9276 });
9277 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9278
9279 // First two buffers should be edited, but not the third one.
9280 assert_eq!(
9281 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9282 "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}",
9283 );
9284 buffer_1.update(cx, |buffer, _| {
9285 assert!(buffer.is_dirty());
9286 assert_eq!(
9287 buffer.text(),
9288 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
9289 )
9290 });
9291 buffer_2.update(cx, |buffer, _| {
9292 assert!(buffer.is_dirty());
9293 assert_eq!(
9294 buffer.text(),
9295 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
9296 )
9297 });
9298 buffer_3.update(cx, |buffer, _| {
9299 assert!(!buffer.is_dirty());
9300 assert_eq!(buffer.text(), sample_text_3,)
9301 });
9302 cx.executor().run_until_parked();
9303
9304 cx.executor().start_waiting();
9305 let save = multi_buffer_editor
9306 .update_in(cx, |editor, window, cx| {
9307 editor.save(
9308 SaveOptions {
9309 format: true,
9310 autosave: false,
9311 },
9312 project.clone(),
9313 window,
9314 cx,
9315 )
9316 })
9317 .unwrap();
9318
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_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
9361 init_test(cx, |_| {});
9362
9363 let fs = FakeFs::new(cx.executor());
9364 fs.insert_tree(
9365 path!("/dir"),
9366 json!({
9367 "file1.rs": "fn main() { println!(\"hello\"); }",
9368 "file2.rs": "fn test() { println!(\"test\"); }",
9369 "file3.rs": "fn other() { println!(\"other\"); }\n",
9370 }),
9371 )
9372 .await;
9373
9374 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
9375 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9376 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9377
9378 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9379 language_registry.add(rust_lang());
9380
9381 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
9382 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9383
9384 // Open three buffers
9385 let buffer_1 = project
9386 .update(cx, |project, cx| {
9387 project.open_buffer((worktree_id, "file1.rs"), cx)
9388 })
9389 .await
9390 .unwrap();
9391 let buffer_2 = project
9392 .update(cx, |project, cx| {
9393 project.open_buffer((worktree_id, "file2.rs"), cx)
9394 })
9395 .await
9396 .unwrap();
9397 let buffer_3 = project
9398 .update(cx, |project, cx| {
9399 project.open_buffer((worktree_id, "file3.rs"), cx)
9400 })
9401 .await
9402 .unwrap();
9403
9404 // Create a multi-buffer with all three buffers
9405 let multi_buffer = cx.new(|cx| {
9406 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9407 multi_buffer.push_excerpts(
9408 buffer_1.clone(),
9409 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9410 cx,
9411 );
9412 multi_buffer.push_excerpts(
9413 buffer_2.clone(),
9414 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9415 cx,
9416 );
9417 multi_buffer.push_excerpts(
9418 buffer_3.clone(),
9419 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9420 cx,
9421 );
9422 multi_buffer
9423 });
9424
9425 let editor = cx.new_window_entity(|window, cx| {
9426 Editor::new(
9427 EditorMode::full(),
9428 multi_buffer,
9429 Some(project.clone()),
9430 window,
9431 cx,
9432 )
9433 });
9434
9435 // Edit only the first buffer
9436 editor.update_in(cx, |editor, window, cx| {
9437 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
9438 s.select_ranges(Some(10..10))
9439 });
9440 editor.insert("// edited", window, cx);
9441 });
9442
9443 // Verify that only buffer 1 is dirty
9444 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
9445 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9446 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9447
9448 // Get write counts after file creation (files were created with initial content)
9449 // We expect each file to have been written once during creation
9450 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
9451 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
9452 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
9453
9454 // Perform autosave
9455 let save_task = editor.update_in(cx, |editor, window, cx| {
9456 editor.save(
9457 SaveOptions {
9458 format: true,
9459 autosave: true,
9460 },
9461 project.clone(),
9462 window,
9463 cx,
9464 )
9465 });
9466 save_task.await.unwrap();
9467
9468 // Only the dirty buffer should have been saved
9469 assert_eq!(
9470 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
9471 1,
9472 "Buffer 1 was dirty, so it should have been written once during autosave"
9473 );
9474 assert_eq!(
9475 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
9476 0,
9477 "Buffer 2 was clean, so it should not have been written during autosave"
9478 );
9479 assert_eq!(
9480 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
9481 0,
9482 "Buffer 3 was clean, so it should not have been written during autosave"
9483 );
9484
9485 // Verify buffer states after autosave
9486 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9487 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9488 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9489
9490 // Now perform a manual save (format = true)
9491 let save_task = editor.update_in(cx, |editor, window, cx| {
9492 editor.save(
9493 SaveOptions {
9494 format: true,
9495 autosave: false,
9496 },
9497 project.clone(),
9498 window,
9499 cx,
9500 )
9501 });
9502 save_task.await.unwrap();
9503
9504 // During manual save, clean buffers don't get written to disk
9505 // They just get did_save called for language server notifications
9506 assert_eq!(
9507 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
9508 1,
9509 "Buffer 1 should only have been written once total (during autosave, not manual save)"
9510 );
9511 assert_eq!(
9512 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
9513 0,
9514 "Buffer 2 should not have been written at all"
9515 );
9516 assert_eq!(
9517 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
9518 0,
9519 "Buffer 3 should not have been written at all"
9520 );
9521}
9522
9523#[gpui::test]
9524async fn test_range_format_during_save(cx: &mut TestAppContext) {
9525 init_test(cx, |_| {});
9526
9527 let fs = FakeFs::new(cx.executor());
9528 fs.insert_file(path!("/file.rs"), Default::default()).await;
9529
9530 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9531
9532 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9533 language_registry.add(rust_lang());
9534 let mut fake_servers = language_registry.register_fake_lsp(
9535 "Rust",
9536 FakeLspAdapter {
9537 capabilities: lsp::ServerCapabilities {
9538 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
9539 ..Default::default()
9540 },
9541 ..Default::default()
9542 },
9543 );
9544
9545 let buffer = project
9546 .update(cx, |project, cx| {
9547 project.open_local_buffer(path!("/file.rs"), cx)
9548 })
9549 .await
9550 .unwrap();
9551
9552 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9553 let (editor, cx) = cx.add_window_view(|window, cx| {
9554 build_editor_with_project(project.clone(), buffer, window, cx)
9555 });
9556 editor.update_in(cx, |editor, window, cx| {
9557 editor.set_text("one\ntwo\nthree\n", window, cx)
9558 });
9559 assert!(cx.read(|cx| editor.is_dirty(cx)));
9560
9561 cx.executor().start_waiting();
9562 let fake_server = fake_servers.next().await.unwrap();
9563
9564 let save = editor
9565 .update_in(cx, |editor, window, cx| {
9566 editor.save(
9567 SaveOptions {
9568 format: true,
9569 autosave: false,
9570 },
9571 project.clone(),
9572 window,
9573 cx,
9574 )
9575 })
9576 .unwrap();
9577 fake_server
9578 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
9579 assert_eq!(
9580 params.text_document.uri,
9581 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9582 );
9583 assert_eq!(params.options.tab_size, 4);
9584 Ok(Some(vec![lsp::TextEdit::new(
9585 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9586 ", ".to_string(),
9587 )]))
9588 })
9589 .next()
9590 .await;
9591 cx.executor().start_waiting();
9592 save.await;
9593 assert_eq!(
9594 editor.update(cx, |editor, cx| editor.text(cx)),
9595 "one, two\nthree\n"
9596 );
9597 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9598
9599 editor.update_in(cx, |editor, window, cx| {
9600 editor.set_text("one\ntwo\nthree\n", window, cx)
9601 });
9602 assert!(cx.read(|cx| editor.is_dirty(cx)));
9603
9604 // Ensure we can still save even if formatting hangs.
9605 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
9606 move |params, _| async move {
9607 assert_eq!(
9608 params.text_document.uri,
9609 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9610 );
9611 futures::future::pending::<()>().await;
9612 unreachable!()
9613 },
9614 );
9615 let save = editor
9616 .update_in(cx, |editor, window, cx| {
9617 editor.save(
9618 SaveOptions {
9619 format: true,
9620 autosave: false,
9621 },
9622 project.clone(),
9623 window,
9624 cx,
9625 )
9626 })
9627 .unwrap();
9628 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9629 cx.executor().start_waiting();
9630 save.await;
9631 assert_eq!(
9632 editor.update(cx, |editor, cx| editor.text(cx)),
9633 "one\ntwo\nthree\n"
9634 );
9635 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9636
9637 // For non-dirty buffer, no formatting request should be sent
9638 let save = editor
9639 .update_in(cx, |editor, window, cx| {
9640 editor.save(
9641 SaveOptions {
9642 format: false,
9643 autosave: false,
9644 },
9645 project.clone(),
9646 window,
9647 cx,
9648 )
9649 })
9650 .unwrap();
9651 let _pending_format_request = fake_server
9652 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
9653 panic!("Should not be invoked");
9654 })
9655 .next();
9656 cx.executor().start_waiting();
9657 save.await;
9658
9659 // Set Rust language override and assert overridden tabsize is sent to language server
9660 update_test_language_settings(cx, |settings| {
9661 settings.languages.insert(
9662 "Rust".into(),
9663 LanguageSettingsContent {
9664 tab_size: NonZeroU32::new(8),
9665 ..Default::default()
9666 },
9667 );
9668 });
9669
9670 editor.update_in(cx, |editor, window, cx| {
9671 editor.set_text("somehting_new\n", window, cx)
9672 });
9673 assert!(cx.read(|cx| editor.is_dirty(cx)));
9674 let save = editor
9675 .update_in(cx, |editor, window, cx| {
9676 editor.save(
9677 SaveOptions {
9678 format: true,
9679 autosave: false,
9680 },
9681 project.clone(),
9682 window,
9683 cx,
9684 )
9685 })
9686 .unwrap();
9687 fake_server
9688 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
9689 assert_eq!(
9690 params.text_document.uri,
9691 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9692 );
9693 assert_eq!(params.options.tab_size, 8);
9694 Ok(Some(Vec::new()))
9695 })
9696 .next()
9697 .await;
9698 save.await;
9699}
9700
9701#[gpui::test]
9702async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
9703 init_test(cx, |settings| {
9704 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
9705 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
9706 ))
9707 });
9708
9709 let fs = FakeFs::new(cx.executor());
9710 fs.insert_file(path!("/file.rs"), Default::default()).await;
9711
9712 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9713
9714 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9715 language_registry.add(Arc::new(Language::new(
9716 LanguageConfig {
9717 name: "Rust".into(),
9718 matcher: LanguageMatcher {
9719 path_suffixes: vec!["rs".to_string()],
9720 ..Default::default()
9721 },
9722 ..LanguageConfig::default()
9723 },
9724 Some(tree_sitter_rust::LANGUAGE.into()),
9725 )));
9726 update_test_language_settings(cx, |settings| {
9727 // Enable Prettier formatting for the same buffer, and ensure
9728 // LSP is called instead of Prettier.
9729 settings.defaults.prettier = Some(PrettierSettings {
9730 allowed: true,
9731 ..PrettierSettings::default()
9732 });
9733 });
9734 let mut fake_servers = language_registry.register_fake_lsp(
9735 "Rust",
9736 FakeLspAdapter {
9737 capabilities: lsp::ServerCapabilities {
9738 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9739 ..Default::default()
9740 },
9741 ..Default::default()
9742 },
9743 );
9744
9745 let buffer = project
9746 .update(cx, |project, cx| {
9747 project.open_local_buffer(path!("/file.rs"), cx)
9748 })
9749 .await
9750 .unwrap();
9751
9752 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9753 let (editor, cx) = cx.add_window_view(|window, cx| {
9754 build_editor_with_project(project.clone(), buffer, window, cx)
9755 });
9756 editor.update_in(cx, |editor, window, cx| {
9757 editor.set_text("one\ntwo\nthree\n", window, cx)
9758 });
9759
9760 cx.executor().start_waiting();
9761 let fake_server = fake_servers.next().await.unwrap();
9762
9763 let format = editor
9764 .update_in(cx, |editor, window, cx| {
9765 editor.perform_format(
9766 project.clone(),
9767 FormatTrigger::Manual,
9768 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
9769 window,
9770 cx,
9771 )
9772 })
9773 .unwrap();
9774 fake_server
9775 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9776 assert_eq!(
9777 params.text_document.uri,
9778 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9779 );
9780 assert_eq!(params.options.tab_size, 4);
9781 Ok(Some(vec![lsp::TextEdit::new(
9782 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9783 ", ".to_string(),
9784 )]))
9785 })
9786 .next()
9787 .await;
9788 cx.executor().start_waiting();
9789 format.await;
9790 assert_eq!(
9791 editor.update(cx, |editor, cx| editor.text(cx)),
9792 "one, two\nthree\n"
9793 );
9794
9795 editor.update_in(cx, |editor, window, cx| {
9796 editor.set_text("one\ntwo\nthree\n", window, cx)
9797 });
9798 // Ensure we don't lock if formatting hangs.
9799 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9800 move |params, _| async move {
9801 assert_eq!(
9802 params.text_document.uri,
9803 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9804 );
9805 futures::future::pending::<()>().await;
9806 unreachable!()
9807 },
9808 );
9809 let format = editor
9810 .update_in(cx, |editor, window, cx| {
9811 editor.perform_format(
9812 project,
9813 FormatTrigger::Manual,
9814 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
9815 window,
9816 cx,
9817 )
9818 })
9819 .unwrap();
9820 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9821 cx.executor().start_waiting();
9822 format.await;
9823 assert_eq!(
9824 editor.update(cx, |editor, cx| editor.text(cx)),
9825 "one\ntwo\nthree\n"
9826 );
9827}
9828
9829#[gpui::test]
9830async fn test_multiple_formatters(cx: &mut TestAppContext) {
9831 init_test(cx, |settings| {
9832 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
9833 settings.defaults.formatter =
9834 Some(language_settings::SelectedFormatter::List(FormatterList(
9835 vec![
9836 Formatter::LanguageServer { name: None },
9837 Formatter::CodeActions(
9838 [
9839 ("code-action-1".into(), true),
9840 ("code-action-2".into(), true),
9841 ]
9842 .into_iter()
9843 .collect(),
9844 ),
9845 ]
9846 .into(),
9847 )))
9848 });
9849
9850 let fs = FakeFs::new(cx.executor());
9851 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
9852 .await;
9853
9854 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9855 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9856 language_registry.add(rust_lang());
9857
9858 let mut fake_servers = language_registry.register_fake_lsp(
9859 "Rust",
9860 FakeLspAdapter {
9861 capabilities: lsp::ServerCapabilities {
9862 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9863 execute_command_provider: Some(lsp::ExecuteCommandOptions {
9864 commands: vec!["the-command-for-code-action-1".into()],
9865 ..Default::default()
9866 }),
9867 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9868 ..Default::default()
9869 },
9870 ..Default::default()
9871 },
9872 );
9873
9874 let buffer = project
9875 .update(cx, |project, cx| {
9876 project.open_local_buffer(path!("/file.rs"), cx)
9877 })
9878 .await
9879 .unwrap();
9880
9881 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9882 let (editor, cx) = cx.add_window_view(|window, cx| {
9883 build_editor_with_project(project.clone(), buffer, window, cx)
9884 });
9885
9886 cx.executor().start_waiting();
9887
9888 let fake_server = fake_servers.next().await.unwrap();
9889 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9890 move |_params, _| async move {
9891 Ok(Some(vec![lsp::TextEdit::new(
9892 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9893 "applied-formatting\n".to_string(),
9894 )]))
9895 },
9896 );
9897 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9898 move |params, _| async move {
9899 assert_eq!(
9900 params.context.only,
9901 Some(vec!["code-action-1".into(), "code-action-2".into()])
9902 );
9903 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
9904 Ok(Some(vec![
9905 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9906 kind: Some("code-action-1".into()),
9907 edit: Some(lsp::WorkspaceEdit::new(
9908 [(
9909 uri.clone(),
9910 vec![lsp::TextEdit::new(
9911 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9912 "applied-code-action-1-edit\n".to_string(),
9913 )],
9914 )]
9915 .into_iter()
9916 .collect(),
9917 )),
9918 command: Some(lsp::Command {
9919 command: "the-command-for-code-action-1".into(),
9920 ..Default::default()
9921 }),
9922 ..Default::default()
9923 }),
9924 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9925 kind: Some("code-action-2".into()),
9926 edit: Some(lsp::WorkspaceEdit::new(
9927 [(
9928 uri.clone(),
9929 vec![lsp::TextEdit::new(
9930 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9931 "applied-code-action-2-edit\n".to_string(),
9932 )],
9933 )]
9934 .into_iter()
9935 .collect(),
9936 )),
9937 ..Default::default()
9938 }),
9939 ]))
9940 },
9941 );
9942
9943 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
9944 move |params, _| async move { Ok(params) }
9945 });
9946
9947 let command_lock = Arc::new(futures::lock::Mutex::new(()));
9948 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
9949 let fake = fake_server.clone();
9950 let lock = command_lock.clone();
9951 move |params, _| {
9952 assert_eq!(params.command, "the-command-for-code-action-1");
9953 let fake = fake.clone();
9954 let lock = lock.clone();
9955 async move {
9956 lock.lock().await;
9957 fake.server
9958 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
9959 label: None,
9960 edit: lsp::WorkspaceEdit {
9961 changes: Some(
9962 [(
9963 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
9964 vec![lsp::TextEdit {
9965 range: lsp::Range::new(
9966 lsp::Position::new(0, 0),
9967 lsp::Position::new(0, 0),
9968 ),
9969 new_text: "applied-code-action-1-command\n".into(),
9970 }],
9971 )]
9972 .into_iter()
9973 .collect(),
9974 ),
9975 ..Default::default()
9976 },
9977 })
9978 .await
9979 .into_response()
9980 .unwrap();
9981 Ok(Some(json!(null)))
9982 }
9983 }
9984 });
9985
9986 cx.executor().start_waiting();
9987 editor
9988 .update_in(cx, |editor, window, cx| {
9989 editor.perform_format(
9990 project.clone(),
9991 FormatTrigger::Manual,
9992 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
9993 window,
9994 cx,
9995 )
9996 })
9997 .unwrap()
9998 .await;
9999 editor.update(cx, |editor, cx| {
10000 assert_eq!(
10001 editor.text(cx),
10002 r#"
10003 applied-code-action-2-edit
10004 applied-code-action-1-command
10005 applied-code-action-1-edit
10006 applied-formatting
10007 one
10008 two
10009 three
10010 "#
10011 .unindent()
10012 );
10013 });
10014
10015 editor.update_in(cx, |editor, window, cx| {
10016 editor.undo(&Default::default(), window, cx);
10017 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10018 });
10019
10020 // Perform a manual edit while waiting for an LSP command
10021 // that's being run as part of a formatting code action.
10022 let lock_guard = command_lock.lock().await;
10023 let format = editor
10024 .update_in(cx, |editor, window, cx| {
10025 editor.perform_format(
10026 project.clone(),
10027 FormatTrigger::Manual,
10028 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10029 window,
10030 cx,
10031 )
10032 })
10033 .unwrap();
10034 cx.run_until_parked();
10035 editor.update(cx, |editor, cx| {
10036 assert_eq!(
10037 editor.text(cx),
10038 r#"
10039 applied-code-action-1-edit
10040 applied-formatting
10041 one
10042 two
10043 three
10044 "#
10045 .unindent()
10046 );
10047
10048 editor.buffer.update(cx, |buffer, cx| {
10049 let ix = buffer.len(cx);
10050 buffer.edit([(ix..ix, "edited\n")], None, cx);
10051 });
10052 });
10053
10054 // Allow the LSP command to proceed. Because the buffer was edited,
10055 // the second code action will not be run.
10056 drop(lock_guard);
10057 format.await;
10058 editor.update_in(cx, |editor, window, cx| {
10059 assert_eq!(
10060 editor.text(cx),
10061 r#"
10062 applied-code-action-1-command
10063 applied-code-action-1-edit
10064 applied-formatting
10065 one
10066 two
10067 three
10068 edited
10069 "#
10070 .unindent()
10071 );
10072
10073 // The manual edit is undone first, because it is the last thing the user did
10074 // (even though the command completed afterwards).
10075 editor.undo(&Default::default(), window, cx);
10076 assert_eq!(
10077 editor.text(cx),
10078 r#"
10079 applied-code-action-1-command
10080 applied-code-action-1-edit
10081 applied-formatting
10082 one
10083 two
10084 three
10085 "#
10086 .unindent()
10087 );
10088
10089 // All the formatting (including the command, which completed after the manual edit)
10090 // is undone together.
10091 editor.undo(&Default::default(), window, cx);
10092 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10093 });
10094}
10095
10096#[gpui::test]
10097async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
10098 init_test(cx, |settings| {
10099 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
10100 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
10101 ))
10102 });
10103
10104 let fs = FakeFs::new(cx.executor());
10105 fs.insert_file(path!("/file.ts"), Default::default()).await;
10106
10107 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10108
10109 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10110 language_registry.add(Arc::new(Language::new(
10111 LanguageConfig {
10112 name: "TypeScript".into(),
10113 matcher: LanguageMatcher {
10114 path_suffixes: vec!["ts".to_string()],
10115 ..Default::default()
10116 },
10117 ..LanguageConfig::default()
10118 },
10119 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
10120 )));
10121 update_test_language_settings(cx, |settings| {
10122 settings.defaults.prettier = Some(PrettierSettings {
10123 allowed: true,
10124 ..PrettierSettings::default()
10125 });
10126 });
10127 let mut fake_servers = language_registry.register_fake_lsp(
10128 "TypeScript",
10129 FakeLspAdapter {
10130 capabilities: lsp::ServerCapabilities {
10131 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10132 ..Default::default()
10133 },
10134 ..Default::default()
10135 },
10136 );
10137
10138 let buffer = project
10139 .update(cx, |project, cx| {
10140 project.open_local_buffer(path!("/file.ts"), cx)
10141 })
10142 .await
10143 .unwrap();
10144
10145 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10146 let (editor, cx) = cx.add_window_view(|window, cx| {
10147 build_editor_with_project(project.clone(), buffer, window, cx)
10148 });
10149 editor.update_in(cx, |editor, window, cx| {
10150 editor.set_text(
10151 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10152 window,
10153 cx,
10154 )
10155 });
10156
10157 cx.executor().start_waiting();
10158 let fake_server = fake_servers.next().await.unwrap();
10159
10160 let format = editor
10161 .update_in(cx, |editor, window, cx| {
10162 editor.perform_code_action_kind(
10163 project.clone(),
10164 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10165 window,
10166 cx,
10167 )
10168 })
10169 .unwrap();
10170 fake_server
10171 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
10172 assert_eq!(
10173 params.text_document.uri,
10174 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10175 );
10176 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
10177 lsp::CodeAction {
10178 title: "Organize Imports".to_string(),
10179 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
10180 edit: Some(lsp::WorkspaceEdit {
10181 changes: Some(
10182 [(
10183 params.text_document.uri.clone(),
10184 vec![lsp::TextEdit::new(
10185 lsp::Range::new(
10186 lsp::Position::new(1, 0),
10187 lsp::Position::new(2, 0),
10188 ),
10189 "".to_string(),
10190 )],
10191 )]
10192 .into_iter()
10193 .collect(),
10194 ),
10195 ..Default::default()
10196 }),
10197 ..Default::default()
10198 },
10199 )]))
10200 })
10201 .next()
10202 .await;
10203 cx.executor().start_waiting();
10204 format.await;
10205 assert_eq!(
10206 editor.update(cx, |editor, cx| editor.text(cx)),
10207 "import { a } from 'module';\n\nconst x = a;\n"
10208 );
10209
10210 editor.update_in(cx, |editor, window, cx| {
10211 editor.set_text(
10212 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10213 window,
10214 cx,
10215 )
10216 });
10217 // Ensure we don't lock if code action hangs.
10218 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10219 move |params, _| async move {
10220 assert_eq!(
10221 params.text_document.uri,
10222 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10223 );
10224 futures::future::pending::<()>().await;
10225 unreachable!()
10226 },
10227 );
10228 let format = editor
10229 .update_in(cx, |editor, window, cx| {
10230 editor.perform_code_action_kind(
10231 project,
10232 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10233 window,
10234 cx,
10235 )
10236 })
10237 .unwrap();
10238 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
10239 cx.executor().start_waiting();
10240 format.await;
10241 assert_eq!(
10242 editor.update(cx, |editor, cx| editor.text(cx)),
10243 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
10244 );
10245}
10246
10247#[gpui::test]
10248async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
10249 init_test(cx, |_| {});
10250
10251 let mut cx = EditorLspTestContext::new_rust(
10252 lsp::ServerCapabilities {
10253 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10254 ..Default::default()
10255 },
10256 cx,
10257 )
10258 .await;
10259
10260 cx.set_state(indoc! {"
10261 one.twoˇ
10262 "});
10263
10264 // The format request takes a long time. When it completes, it inserts
10265 // a newline and an indent before the `.`
10266 cx.lsp
10267 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
10268 let executor = cx.background_executor().clone();
10269 async move {
10270 executor.timer(Duration::from_millis(100)).await;
10271 Ok(Some(vec![lsp::TextEdit {
10272 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
10273 new_text: "\n ".into(),
10274 }]))
10275 }
10276 });
10277
10278 // Submit a format request.
10279 let format_1 = cx
10280 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10281 .unwrap();
10282 cx.executor().run_until_parked();
10283
10284 // Submit a second format request.
10285 let format_2 = cx
10286 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10287 .unwrap();
10288 cx.executor().run_until_parked();
10289
10290 // Wait for both format requests to complete
10291 cx.executor().advance_clock(Duration::from_millis(200));
10292 cx.executor().start_waiting();
10293 format_1.await.unwrap();
10294 cx.executor().start_waiting();
10295 format_2.await.unwrap();
10296
10297 // The formatting edits only happens once.
10298 cx.assert_editor_state(indoc! {"
10299 one
10300 .twoˇ
10301 "});
10302}
10303
10304#[gpui::test]
10305async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
10306 init_test(cx, |settings| {
10307 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
10308 });
10309
10310 let mut cx = EditorLspTestContext::new_rust(
10311 lsp::ServerCapabilities {
10312 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10313 ..Default::default()
10314 },
10315 cx,
10316 )
10317 .await;
10318
10319 // Set up a buffer white some trailing whitespace and no trailing newline.
10320 cx.set_state(
10321 &[
10322 "one ", //
10323 "twoˇ", //
10324 "three ", //
10325 "four", //
10326 ]
10327 .join("\n"),
10328 );
10329
10330 // Submit a format request.
10331 let format = cx
10332 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10333 .unwrap();
10334
10335 // Record which buffer changes have been sent to the language server
10336 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
10337 cx.lsp
10338 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
10339 let buffer_changes = buffer_changes.clone();
10340 move |params, _| {
10341 buffer_changes.lock().extend(
10342 params
10343 .content_changes
10344 .into_iter()
10345 .map(|e| (e.range.unwrap(), e.text)),
10346 );
10347 }
10348 });
10349
10350 // Handle formatting requests to the language server.
10351 cx.lsp
10352 .set_request_handler::<lsp::request::Formatting, _, _>({
10353 let buffer_changes = buffer_changes.clone();
10354 move |_, _| {
10355 // When formatting is requested, trailing whitespace has already been stripped,
10356 // and the trailing newline has already been added.
10357 assert_eq!(
10358 &buffer_changes.lock()[1..],
10359 &[
10360 (
10361 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
10362 "".into()
10363 ),
10364 (
10365 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
10366 "".into()
10367 ),
10368 (
10369 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
10370 "\n".into()
10371 ),
10372 ]
10373 );
10374
10375 // Insert blank lines between each line of the buffer.
10376 async move {
10377 Ok(Some(vec![
10378 lsp::TextEdit {
10379 range: lsp::Range::new(
10380 lsp::Position::new(1, 0),
10381 lsp::Position::new(1, 0),
10382 ),
10383 new_text: "\n".into(),
10384 },
10385 lsp::TextEdit {
10386 range: lsp::Range::new(
10387 lsp::Position::new(2, 0),
10388 lsp::Position::new(2, 0),
10389 ),
10390 new_text: "\n".into(),
10391 },
10392 ]))
10393 }
10394 }
10395 });
10396
10397 // After formatting the buffer, the trailing whitespace is stripped,
10398 // a newline is appended, and the edits provided by the language server
10399 // have been applied.
10400 format.await.unwrap();
10401 cx.assert_editor_state(
10402 &[
10403 "one", //
10404 "", //
10405 "twoˇ", //
10406 "", //
10407 "three", //
10408 "four", //
10409 "", //
10410 ]
10411 .join("\n"),
10412 );
10413
10414 // Undoing the formatting undoes the trailing whitespace removal, the
10415 // trailing newline, and the LSP edits.
10416 cx.update_buffer(|buffer, cx| buffer.undo(cx));
10417 cx.assert_editor_state(
10418 &[
10419 "one ", //
10420 "twoˇ", //
10421 "three ", //
10422 "four", //
10423 ]
10424 .join("\n"),
10425 );
10426}
10427
10428#[gpui::test]
10429async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
10430 cx: &mut TestAppContext,
10431) {
10432 init_test(cx, |_| {});
10433
10434 cx.update(|cx| {
10435 cx.update_global::<SettingsStore, _>(|settings, cx| {
10436 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10437 settings.auto_signature_help = Some(true);
10438 });
10439 });
10440 });
10441
10442 let mut cx = EditorLspTestContext::new_rust(
10443 lsp::ServerCapabilities {
10444 signature_help_provider: Some(lsp::SignatureHelpOptions {
10445 ..Default::default()
10446 }),
10447 ..Default::default()
10448 },
10449 cx,
10450 )
10451 .await;
10452
10453 let language = Language::new(
10454 LanguageConfig {
10455 name: "Rust".into(),
10456 brackets: BracketPairConfig {
10457 pairs: vec![
10458 BracketPair {
10459 start: "{".to_string(),
10460 end: "}".to_string(),
10461 close: true,
10462 surround: true,
10463 newline: true,
10464 },
10465 BracketPair {
10466 start: "(".to_string(),
10467 end: ")".to_string(),
10468 close: true,
10469 surround: true,
10470 newline: true,
10471 },
10472 BracketPair {
10473 start: "/*".to_string(),
10474 end: " */".to_string(),
10475 close: true,
10476 surround: true,
10477 newline: true,
10478 },
10479 BracketPair {
10480 start: "[".to_string(),
10481 end: "]".to_string(),
10482 close: false,
10483 surround: false,
10484 newline: true,
10485 },
10486 BracketPair {
10487 start: "\"".to_string(),
10488 end: "\"".to_string(),
10489 close: true,
10490 surround: true,
10491 newline: false,
10492 },
10493 BracketPair {
10494 start: "<".to_string(),
10495 end: ">".to_string(),
10496 close: false,
10497 surround: true,
10498 newline: true,
10499 },
10500 ],
10501 ..Default::default()
10502 },
10503 autoclose_before: "})]".to_string(),
10504 ..Default::default()
10505 },
10506 Some(tree_sitter_rust::LANGUAGE.into()),
10507 );
10508 let language = Arc::new(language);
10509
10510 cx.language_registry().add(language.clone());
10511 cx.update_buffer(|buffer, cx| {
10512 buffer.set_language(Some(language), cx);
10513 });
10514
10515 cx.set_state(
10516 &r#"
10517 fn main() {
10518 sampleˇ
10519 }
10520 "#
10521 .unindent(),
10522 );
10523
10524 cx.update_editor(|editor, window, cx| {
10525 editor.handle_input("(", window, cx);
10526 });
10527 cx.assert_editor_state(
10528 &"
10529 fn main() {
10530 sample(ˇ)
10531 }
10532 "
10533 .unindent(),
10534 );
10535
10536 let mocked_response = lsp::SignatureHelp {
10537 signatures: vec![lsp::SignatureInformation {
10538 label: "fn sample(param1: u8, param2: u8)".to_string(),
10539 documentation: None,
10540 parameters: Some(vec![
10541 lsp::ParameterInformation {
10542 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10543 documentation: None,
10544 },
10545 lsp::ParameterInformation {
10546 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10547 documentation: None,
10548 },
10549 ]),
10550 active_parameter: None,
10551 }],
10552 active_signature: Some(0),
10553 active_parameter: Some(0),
10554 };
10555 handle_signature_help_request(&mut cx, mocked_response).await;
10556
10557 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10558 .await;
10559
10560 cx.editor(|editor, _, _| {
10561 let signature_help_state = editor.signature_help_state.popover().cloned();
10562 assert_eq!(
10563 signature_help_state.unwrap().label,
10564 "param1: u8, param2: u8"
10565 );
10566 });
10567}
10568
10569#[gpui::test]
10570async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
10571 init_test(cx, |_| {});
10572
10573 cx.update(|cx| {
10574 cx.update_global::<SettingsStore, _>(|settings, cx| {
10575 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10576 settings.auto_signature_help = Some(false);
10577 settings.show_signature_help_after_edits = Some(false);
10578 });
10579 });
10580 });
10581
10582 let mut cx = EditorLspTestContext::new_rust(
10583 lsp::ServerCapabilities {
10584 signature_help_provider: Some(lsp::SignatureHelpOptions {
10585 ..Default::default()
10586 }),
10587 ..Default::default()
10588 },
10589 cx,
10590 )
10591 .await;
10592
10593 let language = Language::new(
10594 LanguageConfig {
10595 name: "Rust".into(),
10596 brackets: BracketPairConfig {
10597 pairs: vec![
10598 BracketPair {
10599 start: "{".to_string(),
10600 end: "}".to_string(),
10601 close: true,
10602 surround: true,
10603 newline: true,
10604 },
10605 BracketPair {
10606 start: "(".to_string(),
10607 end: ")".to_string(),
10608 close: true,
10609 surround: true,
10610 newline: true,
10611 },
10612 BracketPair {
10613 start: "/*".to_string(),
10614 end: " */".to_string(),
10615 close: true,
10616 surround: true,
10617 newline: true,
10618 },
10619 BracketPair {
10620 start: "[".to_string(),
10621 end: "]".to_string(),
10622 close: false,
10623 surround: false,
10624 newline: true,
10625 },
10626 BracketPair {
10627 start: "\"".to_string(),
10628 end: "\"".to_string(),
10629 close: true,
10630 surround: true,
10631 newline: false,
10632 },
10633 BracketPair {
10634 start: "<".to_string(),
10635 end: ">".to_string(),
10636 close: false,
10637 surround: true,
10638 newline: true,
10639 },
10640 ],
10641 ..Default::default()
10642 },
10643 autoclose_before: "})]".to_string(),
10644 ..Default::default()
10645 },
10646 Some(tree_sitter_rust::LANGUAGE.into()),
10647 );
10648 let language = Arc::new(language);
10649
10650 cx.language_registry().add(language.clone());
10651 cx.update_buffer(|buffer, cx| {
10652 buffer.set_language(Some(language), cx);
10653 });
10654
10655 // Ensure that signature_help is not called when no signature help is enabled.
10656 cx.set_state(
10657 &r#"
10658 fn main() {
10659 sampleˇ
10660 }
10661 "#
10662 .unindent(),
10663 );
10664 cx.update_editor(|editor, window, cx| {
10665 editor.handle_input("(", window, cx);
10666 });
10667 cx.assert_editor_state(
10668 &"
10669 fn main() {
10670 sample(ˇ)
10671 }
10672 "
10673 .unindent(),
10674 );
10675 cx.editor(|editor, _, _| {
10676 assert!(editor.signature_help_state.task().is_none());
10677 });
10678
10679 let mocked_response = lsp::SignatureHelp {
10680 signatures: vec![lsp::SignatureInformation {
10681 label: "fn sample(param1: u8, param2: u8)".to_string(),
10682 documentation: None,
10683 parameters: Some(vec![
10684 lsp::ParameterInformation {
10685 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10686 documentation: None,
10687 },
10688 lsp::ParameterInformation {
10689 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10690 documentation: None,
10691 },
10692 ]),
10693 active_parameter: None,
10694 }],
10695 active_signature: Some(0),
10696 active_parameter: Some(0),
10697 };
10698
10699 // Ensure that signature_help is called when enabled afte edits
10700 cx.update(|_, cx| {
10701 cx.update_global::<SettingsStore, _>(|settings, cx| {
10702 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10703 settings.auto_signature_help = Some(false);
10704 settings.show_signature_help_after_edits = Some(true);
10705 });
10706 });
10707 });
10708 cx.set_state(
10709 &r#"
10710 fn main() {
10711 sampleˇ
10712 }
10713 "#
10714 .unindent(),
10715 );
10716 cx.update_editor(|editor, window, cx| {
10717 editor.handle_input("(", window, cx);
10718 });
10719 cx.assert_editor_state(
10720 &"
10721 fn main() {
10722 sample(ˇ)
10723 }
10724 "
10725 .unindent(),
10726 );
10727 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10728 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10729 .await;
10730 cx.update_editor(|editor, _, _| {
10731 let signature_help_state = editor.signature_help_state.popover().cloned();
10732 assert!(signature_help_state.is_some());
10733 assert_eq!(
10734 signature_help_state.unwrap().label,
10735 "param1: u8, param2: u8"
10736 );
10737 editor.signature_help_state = SignatureHelpState::default();
10738 });
10739
10740 // Ensure that signature_help is called when auto signature help override is enabled
10741 cx.update(|_, cx| {
10742 cx.update_global::<SettingsStore, _>(|settings, cx| {
10743 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10744 settings.auto_signature_help = Some(true);
10745 settings.show_signature_help_after_edits = Some(false);
10746 });
10747 });
10748 });
10749 cx.set_state(
10750 &r#"
10751 fn main() {
10752 sampleˇ
10753 }
10754 "#
10755 .unindent(),
10756 );
10757 cx.update_editor(|editor, window, cx| {
10758 editor.handle_input("(", window, cx);
10759 });
10760 cx.assert_editor_state(
10761 &"
10762 fn main() {
10763 sample(ˇ)
10764 }
10765 "
10766 .unindent(),
10767 );
10768 handle_signature_help_request(&mut cx, mocked_response).await;
10769 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10770 .await;
10771 cx.editor(|editor, _, _| {
10772 let signature_help_state = editor.signature_help_state.popover().cloned();
10773 assert!(signature_help_state.is_some());
10774 assert_eq!(
10775 signature_help_state.unwrap().label,
10776 "param1: u8, param2: u8"
10777 );
10778 });
10779}
10780
10781#[gpui::test]
10782async fn test_signature_help(cx: &mut TestAppContext) {
10783 init_test(cx, |_| {});
10784 cx.update(|cx| {
10785 cx.update_global::<SettingsStore, _>(|settings, cx| {
10786 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10787 settings.auto_signature_help = Some(true);
10788 });
10789 });
10790 });
10791
10792 let mut cx = EditorLspTestContext::new_rust(
10793 lsp::ServerCapabilities {
10794 signature_help_provider: Some(lsp::SignatureHelpOptions {
10795 ..Default::default()
10796 }),
10797 ..Default::default()
10798 },
10799 cx,
10800 )
10801 .await;
10802
10803 // A test that directly calls `show_signature_help`
10804 cx.update_editor(|editor, window, cx| {
10805 editor.show_signature_help(&ShowSignatureHelp, window, cx);
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(0),
10826 };
10827 handle_signature_help_request(&mut cx, mocked_response).await;
10828
10829 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10830 .await;
10831
10832 cx.editor(|editor, _, _| {
10833 let signature_help_state = editor.signature_help_state.popover().cloned();
10834 assert!(signature_help_state.is_some());
10835 assert_eq!(
10836 signature_help_state.unwrap().label,
10837 "param1: u8, param2: u8"
10838 );
10839 });
10840
10841 // When exiting outside from inside the brackets, `signature_help` is closed.
10842 cx.set_state(indoc! {"
10843 fn main() {
10844 sample(ˇ);
10845 }
10846
10847 fn sample(param1: u8, param2: u8) {}
10848 "});
10849
10850 cx.update_editor(|editor, window, cx| {
10851 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
10852 });
10853
10854 let mocked_response = lsp::SignatureHelp {
10855 signatures: Vec::new(),
10856 active_signature: None,
10857 active_parameter: None,
10858 };
10859 handle_signature_help_request(&mut cx, mocked_response).await;
10860
10861 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10862 .await;
10863
10864 cx.editor(|editor, _, _| {
10865 assert!(!editor.signature_help_state.is_shown());
10866 });
10867
10868 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
10869 cx.set_state(indoc! {"
10870 fn main() {
10871 sample(ˇ);
10872 }
10873
10874 fn sample(param1: u8, param2: u8) {}
10875 "});
10876
10877 let mocked_response = lsp::SignatureHelp {
10878 signatures: vec![lsp::SignatureInformation {
10879 label: "fn sample(param1: u8, param2: u8)".to_string(),
10880 documentation: None,
10881 parameters: Some(vec![
10882 lsp::ParameterInformation {
10883 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10884 documentation: None,
10885 },
10886 lsp::ParameterInformation {
10887 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10888 documentation: None,
10889 },
10890 ]),
10891 active_parameter: None,
10892 }],
10893 active_signature: Some(0),
10894 active_parameter: Some(0),
10895 };
10896 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10897 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10898 .await;
10899 cx.editor(|editor, _, _| {
10900 assert!(editor.signature_help_state.is_shown());
10901 });
10902
10903 // Restore the popover with more parameter input
10904 cx.set_state(indoc! {"
10905 fn main() {
10906 sample(param1, param2ˇ);
10907 }
10908
10909 fn sample(param1: u8, param2: u8) {}
10910 "});
10911
10912 let mocked_response = lsp::SignatureHelp {
10913 signatures: vec![lsp::SignatureInformation {
10914 label: "fn sample(param1: u8, param2: u8)".to_string(),
10915 documentation: None,
10916 parameters: Some(vec![
10917 lsp::ParameterInformation {
10918 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10919 documentation: None,
10920 },
10921 lsp::ParameterInformation {
10922 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10923 documentation: None,
10924 },
10925 ]),
10926 active_parameter: None,
10927 }],
10928 active_signature: Some(0),
10929 active_parameter: Some(1),
10930 };
10931 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10932 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10933 .await;
10934
10935 // When selecting a range, the popover is gone.
10936 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
10937 cx.update_editor(|editor, window, cx| {
10938 editor.change_selections(None, window, cx, |s| {
10939 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10940 })
10941 });
10942 cx.assert_editor_state(indoc! {"
10943 fn main() {
10944 sample(param1, «ˇparam2»);
10945 }
10946
10947 fn sample(param1: u8, param2: u8) {}
10948 "});
10949 cx.editor(|editor, _, _| {
10950 assert!(!editor.signature_help_state.is_shown());
10951 });
10952
10953 // When unselecting again, the popover is back if within the brackets.
10954 cx.update_editor(|editor, window, cx| {
10955 editor.change_selections(None, window, cx, |s| {
10956 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10957 })
10958 });
10959 cx.assert_editor_state(indoc! {"
10960 fn main() {
10961 sample(param1, ˇparam2);
10962 }
10963
10964 fn sample(param1: u8, param2: u8) {}
10965 "});
10966 handle_signature_help_request(&mut cx, mocked_response).await;
10967 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10968 .await;
10969 cx.editor(|editor, _, _| {
10970 assert!(editor.signature_help_state.is_shown());
10971 });
10972
10973 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
10974 cx.update_editor(|editor, window, cx| {
10975 editor.change_selections(None, window, cx, |s| {
10976 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
10977 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10978 })
10979 });
10980 cx.assert_editor_state(indoc! {"
10981 fn main() {
10982 sample(param1, ˇparam2);
10983 }
10984
10985 fn sample(param1: u8, param2: u8) {}
10986 "});
10987
10988 let mocked_response = lsp::SignatureHelp {
10989 signatures: vec![lsp::SignatureInformation {
10990 label: "fn sample(param1: u8, param2: u8)".to_string(),
10991 documentation: None,
10992 parameters: Some(vec![
10993 lsp::ParameterInformation {
10994 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10995 documentation: None,
10996 },
10997 lsp::ParameterInformation {
10998 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10999 documentation: None,
11000 },
11001 ]),
11002 active_parameter: None,
11003 }],
11004 active_signature: Some(0),
11005 active_parameter: Some(1),
11006 };
11007 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11008 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11009 .await;
11010 cx.update_editor(|editor, _, cx| {
11011 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
11012 });
11013 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11014 .await;
11015 cx.update_editor(|editor, window, cx| {
11016 editor.change_selections(None, window, cx, |s| {
11017 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11018 })
11019 });
11020 cx.assert_editor_state(indoc! {"
11021 fn main() {
11022 sample(param1, «ˇparam2»);
11023 }
11024
11025 fn sample(param1: u8, param2: u8) {}
11026 "});
11027 cx.update_editor(|editor, window, cx| {
11028 editor.change_selections(None, window, cx, |s| {
11029 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11030 })
11031 });
11032 cx.assert_editor_state(indoc! {"
11033 fn main() {
11034 sample(param1, ˇparam2);
11035 }
11036
11037 fn sample(param1: u8, param2: u8) {}
11038 "});
11039 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
11040 .await;
11041}
11042
11043#[gpui::test]
11044async fn test_completion_mode(cx: &mut TestAppContext) {
11045 init_test(cx, |_| {});
11046 let mut cx = EditorLspTestContext::new_rust(
11047 lsp::ServerCapabilities {
11048 completion_provider: Some(lsp::CompletionOptions {
11049 resolve_provider: Some(true),
11050 ..Default::default()
11051 }),
11052 ..Default::default()
11053 },
11054 cx,
11055 )
11056 .await;
11057
11058 struct Run {
11059 run_description: &'static str,
11060 initial_state: String,
11061 buffer_marked_text: String,
11062 completion_label: &'static str,
11063 completion_text: &'static str,
11064 expected_with_insert_mode: String,
11065 expected_with_replace_mode: String,
11066 expected_with_replace_subsequence_mode: String,
11067 expected_with_replace_suffix_mode: String,
11068 }
11069
11070 let runs = [
11071 Run {
11072 run_description: "Start of word matches completion text",
11073 initial_state: "before ediˇ after".into(),
11074 buffer_marked_text: "before <edi|> after".into(),
11075 completion_label: "editor",
11076 completion_text: "editor",
11077 expected_with_insert_mode: "before editorˇ after".into(),
11078 expected_with_replace_mode: "before editorˇ after".into(),
11079 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11080 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11081 },
11082 Run {
11083 run_description: "Accept same text at the middle of the word",
11084 initial_state: "before ediˇtor after".into(),
11085 buffer_marked_text: "before <edi|tor> after".into(),
11086 completion_label: "editor",
11087 completion_text: "editor",
11088 expected_with_insert_mode: "before editorˇtor after".into(),
11089 expected_with_replace_mode: "before editorˇ after".into(),
11090 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11091 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11092 },
11093 Run {
11094 run_description: "End of word matches completion text -- cursor at end",
11095 initial_state: "before torˇ after".into(),
11096 buffer_marked_text: "before <tor|> after".into(),
11097 completion_label: "editor",
11098 completion_text: "editor",
11099 expected_with_insert_mode: "before editorˇ after".into(),
11100 expected_with_replace_mode: "before editorˇ after".into(),
11101 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11102 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11103 },
11104 Run {
11105 run_description: "End of word matches completion text -- cursor at start",
11106 initial_state: "before ˇtor after".into(),
11107 buffer_marked_text: "before <|tor> after".into(),
11108 completion_label: "editor",
11109 completion_text: "editor",
11110 expected_with_insert_mode: "before editorˇtor after".into(),
11111 expected_with_replace_mode: "before editorˇ after".into(),
11112 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11113 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11114 },
11115 Run {
11116 run_description: "Prepend text containing whitespace",
11117 initial_state: "pˇfield: bool".into(),
11118 buffer_marked_text: "<p|field>: bool".into(),
11119 completion_label: "pub ",
11120 completion_text: "pub ",
11121 expected_with_insert_mode: "pub ˇfield: bool".into(),
11122 expected_with_replace_mode: "pub ˇ: bool".into(),
11123 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
11124 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
11125 },
11126 Run {
11127 run_description: "Add element to start of list",
11128 initial_state: "[element_ˇelement_2]".into(),
11129 buffer_marked_text: "[<element_|element_2>]".into(),
11130 completion_label: "element_1",
11131 completion_text: "element_1",
11132 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
11133 expected_with_replace_mode: "[element_1ˇ]".into(),
11134 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
11135 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
11136 },
11137 Run {
11138 run_description: "Add element to start of list -- first and second elements are equal",
11139 initial_state: "[elˇelement]".into(),
11140 buffer_marked_text: "[<el|element>]".into(),
11141 completion_label: "element",
11142 completion_text: "element",
11143 expected_with_insert_mode: "[elementˇelement]".into(),
11144 expected_with_replace_mode: "[elementˇ]".into(),
11145 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
11146 expected_with_replace_suffix_mode: "[elementˇ]".into(),
11147 },
11148 Run {
11149 run_description: "Ends with matching suffix",
11150 initial_state: "SubˇError".into(),
11151 buffer_marked_text: "<Sub|Error>".into(),
11152 completion_label: "SubscriptionError",
11153 completion_text: "SubscriptionError",
11154 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
11155 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11156 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11157 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
11158 },
11159 Run {
11160 run_description: "Suffix is a subsequence -- contiguous",
11161 initial_state: "SubˇErr".into(),
11162 buffer_marked_text: "<Sub|Err>".into(),
11163 completion_label: "SubscriptionError",
11164 completion_text: "SubscriptionError",
11165 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
11166 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11167 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11168 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
11169 },
11170 Run {
11171 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
11172 initial_state: "Suˇscrirr".into(),
11173 buffer_marked_text: "<Su|scrirr>".into(),
11174 completion_label: "SubscriptionError",
11175 completion_text: "SubscriptionError",
11176 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
11177 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11178 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11179 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
11180 },
11181 Run {
11182 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
11183 initial_state: "foo(indˇix)".into(),
11184 buffer_marked_text: "foo(<ind|ix>)".into(),
11185 completion_label: "node_index",
11186 completion_text: "node_index",
11187 expected_with_insert_mode: "foo(node_indexˇix)".into(),
11188 expected_with_replace_mode: "foo(node_indexˇ)".into(),
11189 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
11190 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
11191 },
11192 Run {
11193 run_description: "Replace range ends before cursor - should extend to cursor",
11194 initial_state: "before editˇo after".into(),
11195 buffer_marked_text: "before <{ed}>it|o after".into(),
11196 completion_label: "editor",
11197 completion_text: "editor",
11198 expected_with_insert_mode: "before editorˇo after".into(),
11199 expected_with_replace_mode: "before editorˇo after".into(),
11200 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
11201 expected_with_replace_suffix_mode: "before editorˇo after".into(),
11202 },
11203 Run {
11204 run_description: "Uses label for suffix matching",
11205 initial_state: "before ediˇtor after".into(),
11206 buffer_marked_text: "before <edi|tor> after".into(),
11207 completion_label: "editor",
11208 completion_text: "editor()",
11209 expected_with_insert_mode: "before editor()ˇtor after".into(),
11210 expected_with_replace_mode: "before editor()ˇ after".into(),
11211 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
11212 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
11213 },
11214 Run {
11215 run_description: "Case insensitive subsequence and suffix matching",
11216 initial_state: "before EDiˇtoR after".into(),
11217 buffer_marked_text: "before <EDi|toR> after".into(),
11218 completion_label: "editor",
11219 completion_text: "editor",
11220 expected_with_insert_mode: "before editorˇtoR after".into(),
11221 expected_with_replace_mode: "before editorˇ after".into(),
11222 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11223 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11224 },
11225 ];
11226
11227 for run in runs {
11228 let run_variations = [
11229 (LspInsertMode::Insert, run.expected_with_insert_mode),
11230 (LspInsertMode::Replace, run.expected_with_replace_mode),
11231 (
11232 LspInsertMode::ReplaceSubsequence,
11233 run.expected_with_replace_subsequence_mode,
11234 ),
11235 (
11236 LspInsertMode::ReplaceSuffix,
11237 run.expected_with_replace_suffix_mode,
11238 ),
11239 ];
11240
11241 for (lsp_insert_mode, expected_text) in run_variations {
11242 eprintln!(
11243 "run = {:?}, mode = {lsp_insert_mode:.?}",
11244 run.run_description,
11245 );
11246
11247 update_test_language_settings(&mut cx, |settings| {
11248 settings.defaults.completions = Some(CompletionSettings {
11249 lsp_insert_mode,
11250 words: WordsCompletionMode::Disabled,
11251 lsp: true,
11252 lsp_fetch_timeout_ms: 0,
11253 });
11254 });
11255
11256 cx.set_state(&run.initial_state);
11257 cx.update_editor(|editor, window, cx| {
11258 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11259 });
11260
11261 let counter = Arc::new(AtomicUsize::new(0));
11262 handle_completion_request_with_insert_and_replace(
11263 &mut cx,
11264 &run.buffer_marked_text,
11265 vec![(run.completion_label, run.completion_text)],
11266 counter.clone(),
11267 )
11268 .await;
11269 cx.condition(|editor, _| editor.context_menu_visible())
11270 .await;
11271 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11272
11273 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11274 editor
11275 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11276 .unwrap()
11277 });
11278 cx.assert_editor_state(&expected_text);
11279 handle_resolve_completion_request(&mut cx, None).await;
11280 apply_additional_edits.await.unwrap();
11281 }
11282 }
11283}
11284
11285#[gpui::test]
11286async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
11287 init_test(cx, |_| {});
11288 let mut cx = EditorLspTestContext::new_rust(
11289 lsp::ServerCapabilities {
11290 completion_provider: Some(lsp::CompletionOptions {
11291 resolve_provider: Some(true),
11292 ..Default::default()
11293 }),
11294 ..Default::default()
11295 },
11296 cx,
11297 )
11298 .await;
11299
11300 let initial_state = "SubˇError";
11301 let buffer_marked_text = "<Sub|Error>";
11302 let completion_text = "SubscriptionError";
11303 let expected_with_insert_mode = "SubscriptionErrorˇError";
11304 let expected_with_replace_mode = "SubscriptionErrorˇ";
11305
11306 update_test_language_settings(&mut cx, |settings| {
11307 settings.defaults.completions = Some(CompletionSettings {
11308 words: WordsCompletionMode::Disabled,
11309 // set the opposite here to ensure that the action is overriding the default behavior
11310 lsp_insert_mode: LspInsertMode::Insert,
11311 lsp: true,
11312 lsp_fetch_timeout_ms: 0,
11313 });
11314 });
11315
11316 cx.set_state(initial_state);
11317 cx.update_editor(|editor, window, cx| {
11318 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11319 });
11320
11321 let counter = Arc::new(AtomicUsize::new(0));
11322 handle_completion_request_with_insert_and_replace(
11323 &mut cx,
11324 &buffer_marked_text,
11325 vec![(completion_text, completion_text)],
11326 counter.clone(),
11327 )
11328 .await;
11329 cx.condition(|editor, _| editor.context_menu_visible())
11330 .await;
11331 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11332
11333 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11334 editor
11335 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11336 .unwrap()
11337 });
11338 cx.assert_editor_state(&expected_with_replace_mode);
11339 handle_resolve_completion_request(&mut cx, None).await;
11340 apply_additional_edits.await.unwrap();
11341
11342 update_test_language_settings(&mut cx, |settings| {
11343 settings.defaults.completions = Some(CompletionSettings {
11344 words: WordsCompletionMode::Disabled,
11345 // set the opposite here to ensure that the action is overriding the default behavior
11346 lsp_insert_mode: LspInsertMode::Replace,
11347 lsp: true,
11348 lsp_fetch_timeout_ms: 0,
11349 });
11350 });
11351
11352 cx.set_state(initial_state);
11353 cx.update_editor(|editor, window, cx| {
11354 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11355 });
11356 handle_completion_request_with_insert_and_replace(
11357 &mut cx,
11358 &buffer_marked_text,
11359 vec![(completion_text, completion_text)],
11360 counter.clone(),
11361 )
11362 .await;
11363 cx.condition(|editor, _| editor.context_menu_visible())
11364 .await;
11365 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11366
11367 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11368 editor
11369 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
11370 .unwrap()
11371 });
11372 cx.assert_editor_state(&expected_with_insert_mode);
11373 handle_resolve_completion_request(&mut cx, None).await;
11374 apply_additional_edits.await.unwrap();
11375}
11376
11377#[gpui::test]
11378async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
11379 init_test(cx, |_| {});
11380 let mut cx = EditorLspTestContext::new_rust(
11381 lsp::ServerCapabilities {
11382 completion_provider: Some(lsp::CompletionOptions {
11383 resolve_provider: Some(true),
11384 ..Default::default()
11385 }),
11386 ..Default::default()
11387 },
11388 cx,
11389 )
11390 .await;
11391
11392 // scenario: surrounding text matches completion text
11393 let completion_text = "to_offset";
11394 let initial_state = indoc! {"
11395 1. buf.to_offˇsuffix
11396 2. buf.to_offˇsuf
11397 3. buf.to_offˇfix
11398 4. buf.to_offˇ
11399 5. into_offˇensive
11400 6. ˇsuffix
11401 7. let ˇ //
11402 8. aaˇzz
11403 9. buf.to_off«zzzzzˇ»suffix
11404 10. buf.«ˇzzzzz»suffix
11405 11. to_off«ˇzzzzz»
11406
11407 buf.to_offˇsuffix // newest cursor
11408 "};
11409 let completion_marked_buffer = indoc! {"
11410 1. buf.to_offsuffix
11411 2. buf.to_offsuf
11412 3. buf.to_offfix
11413 4. buf.to_off
11414 5. into_offensive
11415 6. suffix
11416 7. let //
11417 8. aazz
11418 9. buf.to_offzzzzzsuffix
11419 10. buf.zzzzzsuffix
11420 11. to_offzzzzz
11421
11422 buf.<to_off|suffix> // newest cursor
11423 "};
11424 let expected = indoc! {"
11425 1. buf.to_offsetˇ
11426 2. buf.to_offsetˇsuf
11427 3. buf.to_offsetˇfix
11428 4. buf.to_offsetˇ
11429 5. into_offsetˇensive
11430 6. to_offsetˇsuffix
11431 7. let to_offsetˇ //
11432 8. aato_offsetˇzz
11433 9. buf.to_offsetˇ
11434 10. buf.to_offsetˇsuffix
11435 11. to_offsetˇ
11436
11437 buf.to_offsetˇ // newest cursor
11438 "};
11439 cx.set_state(initial_state);
11440 cx.update_editor(|editor, window, cx| {
11441 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11442 });
11443 handle_completion_request_with_insert_and_replace(
11444 &mut cx,
11445 completion_marked_buffer,
11446 vec![(completion_text, completion_text)],
11447 Arc::new(AtomicUsize::new(0)),
11448 )
11449 .await;
11450 cx.condition(|editor, _| editor.context_menu_visible())
11451 .await;
11452 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11453 editor
11454 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11455 .unwrap()
11456 });
11457 cx.assert_editor_state(expected);
11458 handle_resolve_completion_request(&mut cx, None).await;
11459 apply_additional_edits.await.unwrap();
11460
11461 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
11462 let completion_text = "foo_and_bar";
11463 let initial_state = indoc! {"
11464 1. ooanbˇ
11465 2. zooanbˇ
11466 3. ooanbˇz
11467 4. zooanbˇz
11468 5. ooanˇ
11469 6. oanbˇ
11470
11471 ooanbˇ
11472 "};
11473 let completion_marked_buffer = indoc! {"
11474 1. ooanb
11475 2. zooanb
11476 3. ooanbz
11477 4. zooanbz
11478 5. ooan
11479 6. oanb
11480
11481 <ooanb|>
11482 "};
11483 let expected = indoc! {"
11484 1. foo_and_barˇ
11485 2. zfoo_and_barˇ
11486 3. foo_and_barˇz
11487 4. zfoo_and_barˇz
11488 5. ooanfoo_and_barˇ
11489 6. oanbfoo_and_barˇ
11490
11491 foo_and_barˇ
11492 "};
11493 cx.set_state(initial_state);
11494 cx.update_editor(|editor, window, cx| {
11495 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11496 });
11497 handle_completion_request_with_insert_and_replace(
11498 &mut cx,
11499 completion_marked_buffer,
11500 vec![(completion_text, completion_text)],
11501 Arc::new(AtomicUsize::new(0)),
11502 )
11503 .await;
11504 cx.condition(|editor, _| editor.context_menu_visible())
11505 .await;
11506 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11507 editor
11508 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11509 .unwrap()
11510 });
11511 cx.assert_editor_state(expected);
11512 handle_resolve_completion_request(&mut cx, None).await;
11513 apply_additional_edits.await.unwrap();
11514
11515 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
11516 // (expects the same as if it was inserted at the end)
11517 let completion_text = "foo_and_bar";
11518 let initial_state = indoc! {"
11519 1. ooˇanb
11520 2. zooˇanb
11521 3. ooˇanbz
11522 4. zooˇanbz
11523
11524 ooˇanb
11525 "};
11526 let completion_marked_buffer = indoc! {"
11527 1. ooanb
11528 2. zooanb
11529 3. ooanbz
11530 4. zooanbz
11531
11532 <oo|anb>
11533 "};
11534 let expected = indoc! {"
11535 1. foo_and_barˇ
11536 2. zfoo_and_barˇ
11537 3. foo_and_barˇz
11538 4. zfoo_and_barˇz
11539
11540 foo_and_barˇ
11541 "};
11542 cx.set_state(initial_state);
11543 cx.update_editor(|editor, window, cx| {
11544 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11545 });
11546 handle_completion_request_with_insert_and_replace(
11547 &mut cx,
11548 completion_marked_buffer,
11549 vec![(completion_text, completion_text)],
11550 Arc::new(AtomicUsize::new(0)),
11551 )
11552 .await;
11553 cx.condition(|editor, _| editor.context_menu_visible())
11554 .await;
11555 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11556 editor
11557 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11558 .unwrap()
11559 });
11560 cx.assert_editor_state(expected);
11561 handle_resolve_completion_request(&mut cx, None).await;
11562 apply_additional_edits.await.unwrap();
11563}
11564
11565// This used to crash
11566#[gpui::test]
11567async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
11568 init_test(cx, |_| {});
11569
11570 let buffer_text = indoc! {"
11571 fn main() {
11572 10.satu;
11573
11574 //
11575 // separate cursors so they open in different excerpts (manually reproducible)
11576 //
11577
11578 10.satu20;
11579 }
11580 "};
11581 let multibuffer_text_with_selections = indoc! {"
11582 fn main() {
11583 10.satuˇ;
11584
11585 //
11586
11587 //
11588
11589 10.satuˇ20;
11590 }
11591 "};
11592 let expected_multibuffer = indoc! {"
11593 fn main() {
11594 10.saturating_sub()ˇ;
11595
11596 //
11597
11598 //
11599
11600 10.saturating_sub()ˇ;
11601 }
11602 "};
11603
11604 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
11605 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
11606
11607 let fs = FakeFs::new(cx.executor());
11608 fs.insert_tree(
11609 path!("/a"),
11610 json!({
11611 "main.rs": buffer_text,
11612 }),
11613 )
11614 .await;
11615
11616 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11617 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11618 language_registry.add(rust_lang());
11619 let mut fake_servers = language_registry.register_fake_lsp(
11620 "Rust",
11621 FakeLspAdapter {
11622 capabilities: lsp::ServerCapabilities {
11623 completion_provider: Some(lsp::CompletionOptions {
11624 resolve_provider: None,
11625 ..lsp::CompletionOptions::default()
11626 }),
11627 ..lsp::ServerCapabilities::default()
11628 },
11629 ..FakeLspAdapter::default()
11630 },
11631 );
11632 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11633 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11634 let buffer = project
11635 .update(cx, |project, cx| {
11636 project.open_local_buffer(path!("/a/main.rs"), cx)
11637 })
11638 .await
11639 .unwrap();
11640
11641 let multi_buffer = cx.new(|cx| {
11642 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
11643 multi_buffer.push_excerpts(
11644 buffer.clone(),
11645 [ExcerptRange::new(0..first_excerpt_end)],
11646 cx,
11647 );
11648 multi_buffer.push_excerpts(
11649 buffer.clone(),
11650 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
11651 cx,
11652 );
11653 multi_buffer
11654 });
11655
11656 let editor = workspace
11657 .update(cx, |_, window, cx| {
11658 cx.new(|cx| {
11659 Editor::new(
11660 EditorMode::Full {
11661 scale_ui_elements_with_buffer_font_size: false,
11662 show_active_line_background: false,
11663 sized_by_content: false,
11664 },
11665 multi_buffer.clone(),
11666 Some(project.clone()),
11667 window,
11668 cx,
11669 )
11670 })
11671 })
11672 .unwrap();
11673
11674 let pane = workspace
11675 .update(cx, |workspace, _, _| workspace.active_pane().clone())
11676 .unwrap();
11677 pane.update_in(cx, |pane, window, cx| {
11678 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
11679 });
11680
11681 let fake_server = fake_servers.next().await.unwrap();
11682
11683 editor.update_in(cx, |editor, window, cx| {
11684 editor.change_selections(None, window, cx, |s| {
11685 s.select_ranges([
11686 Point::new(1, 11)..Point::new(1, 11),
11687 Point::new(7, 11)..Point::new(7, 11),
11688 ])
11689 });
11690
11691 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
11692 });
11693
11694 editor.update_in(cx, |editor, window, cx| {
11695 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11696 });
11697
11698 fake_server
11699 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11700 let completion_item = lsp::CompletionItem {
11701 label: "saturating_sub()".into(),
11702 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
11703 lsp::InsertReplaceEdit {
11704 new_text: "saturating_sub()".to_owned(),
11705 insert: lsp::Range::new(
11706 lsp::Position::new(7, 7),
11707 lsp::Position::new(7, 11),
11708 ),
11709 replace: lsp::Range::new(
11710 lsp::Position::new(7, 7),
11711 lsp::Position::new(7, 13),
11712 ),
11713 },
11714 )),
11715 ..lsp::CompletionItem::default()
11716 };
11717
11718 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
11719 })
11720 .next()
11721 .await
11722 .unwrap();
11723
11724 cx.condition(&editor, |editor, _| editor.context_menu_visible())
11725 .await;
11726
11727 editor
11728 .update_in(cx, |editor, window, cx| {
11729 editor
11730 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11731 .unwrap()
11732 })
11733 .await
11734 .unwrap();
11735
11736 editor.update(cx, |editor, cx| {
11737 assert_text_with_selections(editor, expected_multibuffer, cx);
11738 })
11739}
11740
11741#[gpui::test]
11742async fn test_completion(cx: &mut TestAppContext) {
11743 init_test(cx, |_| {});
11744
11745 let mut cx = EditorLspTestContext::new_rust(
11746 lsp::ServerCapabilities {
11747 completion_provider: Some(lsp::CompletionOptions {
11748 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11749 resolve_provider: Some(true),
11750 ..Default::default()
11751 }),
11752 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11753 ..Default::default()
11754 },
11755 cx,
11756 )
11757 .await;
11758 let counter = Arc::new(AtomicUsize::new(0));
11759
11760 cx.set_state(indoc! {"
11761 oneˇ
11762 two
11763 three
11764 "});
11765 cx.simulate_keystroke(".");
11766 handle_completion_request(
11767 indoc! {"
11768 one.|<>
11769 two
11770 three
11771 "},
11772 vec!["first_completion", "second_completion"],
11773 true,
11774 counter.clone(),
11775 &mut cx,
11776 )
11777 .await;
11778 cx.condition(|editor, _| editor.context_menu_visible())
11779 .await;
11780 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11781
11782 let _handler = handle_signature_help_request(
11783 &mut cx,
11784 lsp::SignatureHelp {
11785 signatures: vec![lsp::SignatureInformation {
11786 label: "test signature".to_string(),
11787 documentation: None,
11788 parameters: Some(vec![lsp::ParameterInformation {
11789 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
11790 documentation: None,
11791 }]),
11792 active_parameter: None,
11793 }],
11794 active_signature: None,
11795 active_parameter: None,
11796 },
11797 );
11798 cx.update_editor(|editor, window, cx| {
11799 assert!(
11800 !editor.signature_help_state.is_shown(),
11801 "No signature help was called for"
11802 );
11803 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11804 });
11805 cx.run_until_parked();
11806 cx.update_editor(|editor, _, _| {
11807 assert!(
11808 !editor.signature_help_state.is_shown(),
11809 "No signature help should be shown when completions menu is open"
11810 );
11811 });
11812
11813 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11814 editor.context_menu_next(&Default::default(), window, cx);
11815 editor
11816 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11817 .unwrap()
11818 });
11819 cx.assert_editor_state(indoc! {"
11820 one.second_completionˇ
11821 two
11822 three
11823 "});
11824
11825 handle_resolve_completion_request(
11826 &mut cx,
11827 Some(vec![
11828 (
11829 //This overlaps with the primary completion edit which is
11830 //misbehavior from the LSP spec, test that we filter it out
11831 indoc! {"
11832 one.second_ˇcompletion
11833 two
11834 threeˇ
11835 "},
11836 "overlapping additional edit",
11837 ),
11838 (
11839 indoc! {"
11840 one.second_completion
11841 two
11842 threeˇ
11843 "},
11844 "\nadditional edit",
11845 ),
11846 ]),
11847 )
11848 .await;
11849 apply_additional_edits.await.unwrap();
11850 cx.assert_editor_state(indoc! {"
11851 one.second_completionˇ
11852 two
11853 three
11854 additional edit
11855 "});
11856
11857 cx.set_state(indoc! {"
11858 one.second_completion
11859 twoˇ
11860 threeˇ
11861 additional edit
11862 "});
11863 cx.simulate_keystroke(" ");
11864 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11865 cx.simulate_keystroke("s");
11866 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11867
11868 cx.assert_editor_state(indoc! {"
11869 one.second_completion
11870 two sˇ
11871 three sˇ
11872 additional edit
11873 "});
11874 handle_completion_request(
11875 indoc! {"
11876 one.second_completion
11877 two s
11878 three <s|>
11879 additional edit
11880 "},
11881 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11882 true,
11883 counter.clone(),
11884 &mut cx,
11885 )
11886 .await;
11887 cx.condition(|editor, _| editor.context_menu_visible())
11888 .await;
11889 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11890
11891 cx.simulate_keystroke("i");
11892
11893 handle_completion_request(
11894 indoc! {"
11895 one.second_completion
11896 two si
11897 three <si|>
11898 additional edit
11899 "},
11900 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11901 true,
11902 counter.clone(),
11903 &mut cx,
11904 )
11905 .await;
11906 cx.condition(|editor, _| editor.context_menu_visible())
11907 .await;
11908 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
11909
11910 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11911 editor
11912 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11913 .unwrap()
11914 });
11915 cx.assert_editor_state(indoc! {"
11916 one.second_completion
11917 two sixth_completionˇ
11918 three sixth_completionˇ
11919 additional edit
11920 "});
11921
11922 apply_additional_edits.await.unwrap();
11923
11924 update_test_language_settings(&mut cx, |settings| {
11925 settings.defaults.show_completions_on_input = Some(false);
11926 });
11927 cx.set_state("editorˇ");
11928 cx.simulate_keystroke(".");
11929 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11930 cx.simulate_keystrokes("c l o");
11931 cx.assert_editor_state("editor.cloˇ");
11932 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11933 cx.update_editor(|editor, window, cx| {
11934 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11935 });
11936 handle_completion_request(
11937 "editor.<clo|>",
11938 vec!["close", "clobber"],
11939 true,
11940 counter.clone(),
11941 &mut cx,
11942 )
11943 .await;
11944 cx.condition(|editor, _| editor.context_menu_visible())
11945 .await;
11946 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
11947
11948 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11949 editor
11950 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11951 .unwrap()
11952 });
11953 cx.assert_editor_state("editor.closeˇ");
11954 handle_resolve_completion_request(&mut cx, None).await;
11955 apply_additional_edits.await.unwrap();
11956}
11957
11958#[gpui::test]
11959async fn test_completion_reuse(cx: &mut TestAppContext) {
11960 init_test(cx, |_| {});
11961
11962 let mut cx = EditorLspTestContext::new_rust(
11963 lsp::ServerCapabilities {
11964 completion_provider: Some(lsp::CompletionOptions {
11965 trigger_characters: Some(vec![".".to_string()]),
11966 ..Default::default()
11967 }),
11968 ..Default::default()
11969 },
11970 cx,
11971 )
11972 .await;
11973
11974 let counter = Arc::new(AtomicUsize::new(0));
11975 cx.set_state("objˇ");
11976 cx.simulate_keystroke(".");
11977
11978 // Initial completion request returns complete results
11979 let is_incomplete = false;
11980 handle_completion_request(
11981 "obj.|<>",
11982 vec!["a", "ab", "abc"],
11983 is_incomplete,
11984 counter.clone(),
11985 &mut cx,
11986 )
11987 .await;
11988 cx.run_until_parked();
11989 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11990 cx.assert_editor_state("obj.ˇ");
11991 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
11992
11993 // Type "a" - filters existing completions
11994 cx.simulate_keystroke("a");
11995 cx.run_until_parked();
11996 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11997 cx.assert_editor_state("obj.aˇ");
11998 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
11999
12000 // Type "b" - filters existing completions
12001 cx.simulate_keystroke("b");
12002 cx.run_until_parked();
12003 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12004 cx.assert_editor_state("obj.abˇ");
12005 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12006
12007 // Type "c" - filters existing completions
12008 cx.simulate_keystroke("c");
12009 cx.run_until_parked();
12010 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12011 cx.assert_editor_state("obj.abcˇ");
12012 check_displayed_completions(vec!["abc"], &mut cx);
12013
12014 // Backspace to delete "c" - filters existing completions
12015 cx.update_editor(|editor, window, cx| {
12016 editor.backspace(&Backspace, window, cx);
12017 });
12018 cx.run_until_parked();
12019 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12020 cx.assert_editor_state("obj.abˇ");
12021 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12022
12023 // Moving cursor to the left dismisses menu.
12024 cx.update_editor(|editor, window, cx| {
12025 editor.move_left(&MoveLeft, window, cx);
12026 });
12027 cx.run_until_parked();
12028 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12029 cx.assert_editor_state("obj.aˇb");
12030 cx.update_editor(|editor, _, _| {
12031 assert_eq!(editor.context_menu_visible(), false);
12032 });
12033
12034 // Type "b" - new request
12035 cx.simulate_keystroke("b");
12036 let is_incomplete = false;
12037 handle_completion_request(
12038 "obj.<ab|>a",
12039 vec!["ab", "abc"],
12040 is_incomplete,
12041 counter.clone(),
12042 &mut cx,
12043 )
12044 .await;
12045 cx.run_until_parked();
12046 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12047 cx.assert_editor_state("obj.abˇb");
12048 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12049
12050 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
12051 cx.update_editor(|editor, window, cx| {
12052 editor.backspace(&Backspace, window, cx);
12053 });
12054 let is_incomplete = false;
12055 handle_completion_request(
12056 "obj.<a|>b",
12057 vec!["a", "ab", "abc"],
12058 is_incomplete,
12059 counter.clone(),
12060 &mut cx,
12061 )
12062 .await;
12063 cx.run_until_parked();
12064 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12065 cx.assert_editor_state("obj.aˇb");
12066 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12067
12068 // Backspace to delete "a" - dismisses menu.
12069 cx.update_editor(|editor, window, cx| {
12070 editor.backspace(&Backspace, window, cx);
12071 });
12072 cx.run_until_parked();
12073 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12074 cx.assert_editor_state("obj.ˇb");
12075 cx.update_editor(|editor, _, _| {
12076 assert_eq!(editor.context_menu_visible(), false);
12077 });
12078}
12079
12080#[gpui::test]
12081async fn test_word_completion(cx: &mut TestAppContext) {
12082 let lsp_fetch_timeout_ms = 10;
12083 init_test(cx, |language_settings| {
12084 language_settings.defaults.completions = Some(CompletionSettings {
12085 words: WordsCompletionMode::Fallback,
12086 lsp: true,
12087 lsp_fetch_timeout_ms: 10,
12088 lsp_insert_mode: LspInsertMode::Insert,
12089 });
12090 });
12091
12092 let mut cx = EditorLspTestContext::new_rust(
12093 lsp::ServerCapabilities {
12094 completion_provider: Some(lsp::CompletionOptions {
12095 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12096 ..lsp::CompletionOptions::default()
12097 }),
12098 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12099 ..lsp::ServerCapabilities::default()
12100 },
12101 cx,
12102 )
12103 .await;
12104
12105 let throttle_completions = Arc::new(AtomicBool::new(false));
12106
12107 let lsp_throttle_completions = throttle_completions.clone();
12108 let _completion_requests_handler =
12109 cx.lsp
12110 .server
12111 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
12112 let lsp_throttle_completions = lsp_throttle_completions.clone();
12113 let cx = cx.clone();
12114 async move {
12115 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
12116 cx.background_executor()
12117 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
12118 .await;
12119 }
12120 Ok(Some(lsp::CompletionResponse::Array(vec![
12121 lsp::CompletionItem {
12122 label: "first".into(),
12123 ..lsp::CompletionItem::default()
12124 },
12125 lsp::CompletionItem {
12126 label: "last".into(),
12127 ..lsp::CompletionItem::default()
12128 },
12129 ])))
12130 }
12131 });
12132
12133 cx.set_state(indoc! {"
12134 oneˇ
12135 two
12136 three
12137 "});
12138 cx.simulate_keystroke(".");
12139 cx.executor().run_until_parked();
12140 cx.condition(|editor, _| editor.context_menu_visible())
12141 .await;
12142 cx.update_editor(|editor, window, cx| {
12143 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12144 {
12145 assert_eq!(
12146 completion_menu_entries(&menu),
12147 &["first", "last"],
12148 "When LSP server is fast to reply, no fallback word completions are used"
12149 );
12150 } else {
12151 panic!("expected completion menu to be open");
12152 }
12153 editor.cancel(&Cancel, window, cx);
12154 });
12155 cx.executor().run_until_parked();
12156 cx.condition(|editor, _| !editor.context_menu_visible())
12157 .await;
12158
12159 throttle_completions.store(true, atomic::Ordering::Release);
12160 cx.simulate_keystroke(".");
12161 cx.executor()
12162 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
12163 cx.executor().run_until_parked();
12164 cx.condition(|editor, _| editor.context_menu_visible())
12165 .await;
12166 cx.update_editor(|editor, _, _| {
12167 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12168 {
12169 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
12170 "When LSP server is slow, document words can be shown instead, if configured accordingly");
12171 } else {
12172 panic!("expected completion menu to be open");
12173 }
12174 });
12175}
12176
12177#[gpui::test]
12178async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
12179 init_test(cx, |language_settings| {
12180 language_settings.defaults.completions = Some(CompletionSettings {
12181 words: WordsCompletionMode::Enabled,
12182 lsp: true,
12183 lsp_fetch_timeout_ms: 0,
12184 lsp_insert_mode: LspInsertMode::Insert,
12185 });
12186 });
12187
12188 let mut cx = EditorLspTestContext::new_rust(
12189 lsp::ServerCapabilities {
12190 completion_provider: Some(lsp::CompletionOptions {
12191 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12192 ..lsp::CompletionOptions::default()
12193 }),
12194 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12195 ..lsp::ServerCapabilities::default()
12196 },
12197 cx,
12198 )
12199 .await;
12200
12201 let _completion_requests_handler =
12202 cx.lsp
12203 .server
12204 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12205 Ok(Some(lsp::CompletionResponse::Array(vec![
12206 lsp::CompletionItem {
12207 label: "first".into(),
12208 ..lsp::CompletionItem::default()
12209 },
12210 lsp::CompletionItem {
12211 label: "last".into(),
12212 ..lsp::CompletionItem::default()
12213 },
12214 ])))
12215 });
12216
12217 cx.set_state(indoc! {"ˇ
12218 first
12219 last
12220 second
12221 "});
12222 cx.simulate_keystroke(".");
12223 cx.executor().run_until_parked();
12224 cx.condition(|editor, _| editor.context_menu_visible())
12225 .await;
12226 cx.update_editor(|editor, _, _| {
12227 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12228 {
12229 assert_eq!(
12230 completion_menu_entries(&menu),
12231 &["first", "last", "second"],
12232 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
12233 );
12234 } else {
12235 panic!("expected completion menu to be open");
12236 }
12237 });
12238}
12239
12240#[gpui::test]
12241async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
12242 init_test(cx, |language_settings| {
12243 language_settings.defaults.completions = Some(CompletionSettings {
12244 words: WordsCompletionMode::Disabled,
12245 lsp: true,
12246 lsp_fetch_timeout_ms: 0,
12247 lsp_insert_mode: LspInsertMode::Insert,
12248 });
12249 });
12250
12251 let mut cx = EditorLspTestContext::new_rust(
12252 lsp::ServerCapabilities {
12253 completion_provider: Some(lsp::CompletionOptions {
12254 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12255 ..lsp::CompletionOptions::default()
12256 }),
12257 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12258 ..lsp::ServerCapabilities::default()
12259 },
12260 cx,
12261 )
12262 .await;
12263
12264 let _completion_requests_handler =
12265 cx.lsp
12266 .server
12267 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12268 panic!("LSP completions should not be queried when dealing with word completions")
12269 });
12270
12271 cx.set_state(indoc! {"ˇ
12272 first
12273 last
12274 second
12275 "});
12276 cx.update_editor(|editor, window, cx| {
12277 editor.show_word_completions(&ShowWordCompletions, window, cx);
12278 });
12279 cx.executor().run_until_parked();
12280 cx.condition(|editor, _| editor.context_menu_visible())
12281 .await;
12282 cx.update_editor(|editor, _, _| {
12283 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12284 {
12285 assert_eq!(
12286 completion_menu_entries(&menu),
12287 &["first", "last", "second"],
12288 "`ShowWordCompletions` action should show word completions"
12289 );
12290 } else {
12291 panic!("expected completion menu to be open");
12292 }
12293 });
12294
12295 cx.simulate_keystroke("l");
12296 cx.executor().run_until_parked();
12297 cx.condition(|editor, _| editor.context_menu_visible())
12298 .await;
12299 cx.update_editor(|editor, _, _| {
12300 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12301 {
12302 assert_eq!(
12303 completion_menu_entries(&menu),
12304 &["last"],
12305 "After showing word completions, further editing should filter them and not query the LSP"
12306 );
12307 } else {
12308 panic!("expected completion menu to be open");
12309 }
12310 });
12311}
12312
12313#[gpui::test]
12314async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
12315 init_test(cx, |language_settings| {
12316 language_settings.defaults.completions = Some(CompletionSettings {
12317 words: WordsCompletionMode::Fallback,
12318 lsp: false,
12319 lsp_fetch_timeout_ms: 0,
12320 lsp_insert_mode: LspInsertMode::Insert,
12321 });
12322 });
12323
12324 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12325
12326 cx.set_state(indoc! {"ˇ
12327 0_usize
12328 let
12329 33
12330 4.5f32
12331 "});
12332 cx.update_editor(|editor, window, cx| {
12333 editor.show_completions(&ShowCompletions::default(), window, cx);
12334 });
12335 cx.executor().run_until_parked();
12336 cx.condition(|editor, _| editor.context_menu_visible())
12337 .await;
12338 cx.update_editor(|editor, window, cx| {
12339 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12340 {
12341 assert_eq!(
12342 completion_menu_entries(&menu),
12343 &["let"],
12344 "With no digits in the completion query, no digits should be in the word completions"
12345 );
12346 } else {
12347 panic!("expected completion menu to be open");
12348 }
12349 editor.cancel(&Cancel, window, cx);
12350 });
12351
12352 cx.set_state(indoc! {"3ˇ
12353 0_usize
12354 let
12355 3
12356 33.35f32
12357 "});
12358 cx.update_editor(|editor, window, cx| {
12359 editor.show_completions(&ShowCompletions::default(), window, cx);
12360 });
12361 cx.executor().run_until_parked();
12362 cx.condition(|editor, _| editor.context_menu_visible())
12363 .await;
12364 cx.update_editor(|editor, _, _| {
12365 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12366 {
12367 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
12368 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
12369 } else {
12370 panic!("expected completion menu to be open");
12371 }
12372 });
12373}
12374
12375fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
12376 let position = || lsp::Position {
12377 line: params.text_document_position.position.line,
12378 character: params.text_document_position.position.character,
12379 };
12380 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12381 range: lsp::Range {
12382 start: position(),
12383 end: position(),
12384 },
12385 new_text: text.to_string(),
12386 }))
12387}
12388
12389#[gpui::test]
12390async fn test_multiline_completion(cx: &mut TestAppContext) {
12391 init_test(cx, |_| {});
12392
12393 let fs = FakeFs::new(cx.executor());
12394 fs.insert_tree(
12395 path!("/a"),
12396 json!({
12397 "main.ts": "a",
12398 }),
12399 )
12400 .await;
12401
12402 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12403 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12404 let typescript_language = Arc::new(Language::new(
12405 LanguageConfig {
12406 name: "TypeScript".into(),
12407 matcher: LanguageMatcher {
12408 path_suffixes: vec!["ts".to_string()],
12409 ..LanguageMatcher::default()
12410 },
12411 line_comments: vec!["// ".into()],
12412 ..LanguageConfig::default()
12413 },
12414 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12415 ));
12416 language_registry.add(typescript_language.clone());
12417 let mut fake_servers = language_registry.register_fake_lsp(
12418 "TypeScript",
12419 FakeLspAdapter {
12420 capabilities: lsp::ServerCapabilities {
12421 completion_provider: Some(lsp::CompletionOptions {
12422 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12423 ..lsp::CompletionOptions::default()
12424 }),
12425 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12426 ..lsp::ServerCapabilities::default()
12427 },
12428 // Emulate vtsls label generation
12429 label_for_completion: Some(Box::new(|item, _| {
12430 let text = if let Some(description) = item
12431 .label_details
12432 .as_ref()
12433 .and_then(|label_details| label_details.description.as_ref())
12434 {
12435 format!("{} {}", item.label, description)
12436 } else if let Some(detail) = &item.detail {
12437 format!("{} {}", item.label, detail)
12438 } else {
12439 item.label.clone()
12440 };
12441 let len = text.len();
12442 Some(language::CodeLabel {
12443 text,
12444 runs: Vec::new(),
12445 filter_range: 0..len,
12446 })
12447 })),
12448 ..FakeLspAdapter::default()
12449 },
12450 );
12451 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12452 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12453 let worktree_id = workspace
12454 .update(cx, |workspace, _window, cx| {
12455 workspace.project().update(cx, |project, cx| {
12456 project.worktrees(cx).next().unwrap().read(cx).id()
12457 })
12458 })
12459 .unwrap();
12460 let _buffer = project
12461 .update(cx, |project, cx| {
12462 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
12463 })
12464 .await
12465 .unwrap();
12466 let editor = workspace
12467 .update(cx, |workspace, window, cx| {
12468 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
12469 })
12470 .unwrap()
12471 .await
12472 .unwrap()
12473 .downcast::<Editor>()
12474 .unwrap();
12475 let fake_server = fake_servers.next().await.unwrap();
12476
12477 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
12478 let multiline_label_2 = "a\nb\nc\n";
12479 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
12480 let multiline_description = "d\ne\nf\n";
12481 let multiline_detail_2 = "g\nh\ni\n";
12482
12483 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
12484 move |params, _| async move {
12485 Ok(Some(lsp::CompletionResponse::Array(vec![
12486 lsp::CompletionItem {
12487 label: multiline_label.to_string(),
12488 text_edit: gen_text_edit(¶ms, "new_text_1"),
12489 ..lsp::CompletionItem::default()
12490 },
12491 lsp::CompletionItem {
12492 label: "single line label 1".to_string(),
12493 detail: Some(multiline_detail.to_string()),
12494 text_edit: gen_text_edit(¶ms, "new_text_2"),
12495 ..lsp::CompletionItem::default()
12496 },
12497 lsp::CompletionItem {
12498 label: "single line label 2".to_string(),
12499 label_details: Some(lsp::CompletionItemLabelDetails {
12500 description: Some(multiline_description.to_string()),
12501 detail: None,
12502 }),
12503 text_edit: gen_text_edit(¶ms, "new_text_2"),
12504 ..lsp::CompletionItem::default()
12505 },
12506 lsp::CompletionItem {
12507 label: multiline_label_2.to_string(),
12508 detail: Some(multiline_detail_2.to_string()),
12509 text_edit: gen_text_edit(¶ms, "new_text_3"),
12510 ..lsp::CompletionItem::default()
12511 },
12512 lsp::CompletionItem {
12513 label: "Label with many spaces and \t but without newlines".to_string(),
12514 detail: Some(
12515 "Details with many spaces and \t but without newlines".to_string(),
12516 ),
12517 text_edit: gen_text_edit(¶ms, "new_text_4"),
12518 ..lsp::CompletionItem::default()
12519 },
12520 ])))
12521 },
12522 );
12523
12524 editor.update_in(cx, |editor, window, cx| {
12525 cx.focus_self(window);
12526 editor.move_to_end(&MoveToEnd, window, cx);
12527 editor.handle_input(".", window, cx);
12528 });
12529 cx.run_until_parked();
12530 completion_handle.next().await.unwrap();
12531
12532 editor.update(cx, |editor, _| {
12533 assert!(editor.context_menu_visible());
12534 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12535 {
12536 let completion_labels = menu
12537 .completions
12538 .borrow()
12539 .iter()
12540 .map(|c| c.label.text.clone())
12541 .collect::<Vec<_>>();
12542 assert_eq!(
12543 completion_labels,
12544 &[
12545 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
12546 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
12547 "single line label 2 d e f ",
12548 "a b c g h i ",
12549 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
12550 ],
12551 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
12552 );
12553
12554 for completion in menu
12555 .completions
12556 .borrow()
12557 .iter() {
12558 assert_eq!(
12559 completion.label.filter_range,
12560 0..completion.label.text.len(),
12561 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
12562 );
12563 }
12564 } else {
12565 panic!("expected completion menu to be open");
12566 }
12567 });
12568}
12569
12570#[gpui::test]
12571async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
12572 init_test(cx, |_| {});
12573 let mut cx = EditorLspTestContext::new_rust(
12574 lsp::ServerCapabilities {
12575 completion_provider: Some(lsp::CompletionOptions {
12576 trigger_characters: Some(vec![".".to_string()]),
12577 ..Default::default()
12578 }),
12579 ..Default::default()
12580 },
12581 cx,
12582 )
12583 .await;
12584 cx.lsp
12585 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12586 Ok(Some(lsp::CompletionResponse::Array(vec![
12587 lsp::CompletionItem {
12588 label: "first".into(),
12589 ..Default::default()
12590 },
12591 lsp::CompletionItem {
12592 label: "last".into(),
12593 ..Default::default()
12594 },
12595 ])))
12596 });
12597 cx.set_state("variableˇ");
12598 cx.simulate_keystroke(".");
12599 cx.executor().run_until_parked();
12600
12601 cx.update_editor(|editor, _, _| {
12602 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12603 {
12604 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
12605 } else {
12606 panic!("expected completion menu to be open");
12607 }
12608 });
12609
12610 cx.update_editor(|editor, window, cx| {
12611 editor.move_page_down(&MovePageDown::default(), window, cx);
12612 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12613 {
12614 assert!(
12615 menu.selected_item == 1,
12616 "expected PageDown to select the last item from the context menu"
12617 );
12618 } else {
12619 panic!("expected completion menu to stay open after PageDown");
12620 }
12621 });
12622
12623 cx.update_editor(|editor, window, cx| {
12624 editor.move_page_up(&MovePageUp::default(), window, cx);
12625 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12626 {
12627 assert!(
12628 menu.selected_item == 0,
12629 "expected PageUp to select the first item from the context menu"
12630 );
12631 } else {
12632 panic!("expected completion menu to stay open after PageUp");
12633 }
12634 });
12635}
12636
12637#[gpui::test]
12638async fn test_as_is_completions(cx: &mut TestAppContext) {
12639 init_test(cx, |_| {});
12640 let mut cx = EditorLspTestContext::new_rust(
12641 lsp::ServerCapabilities {
12642 completion_provider: Some(lsp::CompletionOptions {
12643 ..Default::default()
12644 }),
12645 ..Default::default()
12646 },
12647 cx,
12648 )
12649 .await;
12650 cx.lsp
12651 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12652 Ok(Some(lsp::CompletionResponse::Array(vec![
12653 lsp::CompletionItem {
12654 label: "unsafe".into(),
12655 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12656 range: lsp::Range {
12657 start: lsp::Position {
12658 line: 1,
12659 character: 2,
12660 },
12661 end: lsp::Position {
12662 line: 1,
12663 character: 3,
12664 },
12665 },
12666 new_text: "unsafe".to_string(),
12667 })),
12668 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
12669 ..Default::default()
12670 },
12671 ])))
12672 });
12673 cx.set_state("fn a() {}\n nˇ");
12674 cx.executor().run_until_parked();
12675 cx.update_editor(|editor, window, cx| {
12676 editor.show_completions(
12677 &ShowCompletions {
12678 trigger: Some("\n".into()),
12679 },
12680 window,
12681 cx,
12682 );
12683 });
12684 cx.executor().run_until_parked();
12685
12686 cx.update_editor(|editor, window, cx| {
12687 editor.confirm_completion(&Default::default(), window, cx)
12688 });
12689 cx.executor().run_until_parked();
12690 cx.assert_editor_state("fn a() {}\n unsafeˇ");
12691}
12692
12693#[gpui::test]
12694async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
12695 init_test(cx, |_| {});
12696
12697 let mut cx = EditorLspTestContext::new_rust(
12698 lsp::ServerCapabilities {
12699 completion_provider: Some(lsp::CompletionOptions {
12700 trigger_characters: Some(vec![".".to_string()]),
12701 resolve_provider: Some(true),
12702 ..Default::default()
12703 }),
12704 ..Default::default()
12705 },
12706 cx,
12707 )
12708 .await;
12709
12710 cx.set_state("fn main() { let a = 2ˇ; }");
12711 cx.simulate_keystroke(".");
12712 let completion_item = lsp::CompletionItem {
12713 label: "Some".into(),
12714 kind: Some(lsp::CompletionItemKind::SNIPPET),
12715 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
12716 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
12717 kind: lsp::MarkupKind::Markdown,
12718 value: "```rust\nSome(2)\n```".to_string(),
12719 })),
12720 deprecated: Some(false),
12721 sort_text: Some("Some".to_string()),
12722 filter_text: Some("Some".to_string()),
12723 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
12724 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12725 range: lsp::Range {
12726 start: lsp::Position {
12727 line: 0,
12728 character: 22,
12729 },
12730 end: lsp::Position {
12731 line: 0,
12732 character: 22,
12733 },
12734 },
12735 new_text: "Some(2)".to_string(),
12736 })),
12737 additional_text_edits: Some(vec![lsp::TextEdit {
12738 range: lsp::Range {
12739 start: lsp::Position {
12740 line: 0,
12741 character: 20,
12742 },
12743 end: lsp::Position {
12744 line: 0,
12745 character: 22,
12746 },
12747 },
12748 new_text: "".to_string(),
12749 }]),
12750 ..Default::default()
12751 };
12752
12753 let closure_completion_item = completion_item.clone();
12754 let counter = Arc::new(AtomicUsize::new(0));
12755 let counter_clone = counter.clone();
12756 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
12757 let task_completion_item = closure_completion_item.clone();
12758 counter_clone.fetch_add(1, atomic::Ordering::Release);
12759 async move {
12760 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
12761 is_incomplete: true,
12762 item_defaults: None,
12763 items: vec![task_completion_item],
12764 })))
12765 }
12766 });
12767
12768 cx.condition(|editor, _| editor.context_menu_visible())
12769 .await;
12770 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
12771 assert!(request.next().await.is_some());
12772 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12773
12774 cx.simulate_keystrokes("S o m");
12775 cx.condition(|editor, _| editor.context_menu_visible())
12776 .await;
12777 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
12778 assert!(request.next().await.is_some());
12779 assert!(request.next().await.is_some());
12780 assert!(request.next().await.is_some());
12781 request.close();
12782 assert!(request.next().await.is_none());
12783 assert_eq!(
12784 counter.load(atomic::Ordering::Acquire),
12785 4,
12786 "With the completions menu open, only one LSP request should happen per input"
12787 );
12788}
12789
12790#[gpui::test]
12791async fn test_toggle_comment(cx: &mut TestAppContext) {
12792 init_test(cx, |_| {});
12793 let mut cx = EditorTestContext::new(cx).await;
12794 let language = Arc::new(Language::new(
12795 LanguageConfig {
12796 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
12797 ..Default::default()
12798 },
12799 Some(tree_sitter_rust::LANGUAGE.into()),
12800 ));
12801 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
12802
12803 // If multiple selections intersect a line, the line is only toggled once.
12804 cx.set_state(indoc! {"
12805 fn a() {
12806 «//b();
12807 ˇ»// «c();
12808 //ˇ» d();
12809 }
12810 "});
12811
12812 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12813
12814 cx.assert_editor_state(indoc! {"
12815 fn a() {
12816 «b();
12817 c();
12818 ˇ» d();
12819 }
12820 "});
12821
12822 // The comment prefix is inserted at the same column for every line in a
12823 // selection.
12824 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12825
12826 cx.assert_editor_state(indoc! {"
12827 fn a() {
12828 // «b();
12829 // c();
12830 ˇ»// d();
12831 }
12832 "});
12833
12834 // If a selection ends at the beginning of a line, that line is not toggled.
12835 cx.set_selections_state(indoc! {"
12836 fn a() {
12837 // b();
12838 «// c();
12839 ˇ» // d();
12840 }
12841 "});
12842
12843 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12844
12845 cx.assert_editor_state(indoc! {"
12846 fn a() {
12847 // b();
12848 «c();
12849 ˇ» // d();
12850 }
12851 "});
12852
12853 // If a selection span a single line and is empty, the line is toggled.
12854 cx.set_state(indoc! {"
12855 fn a() {
12856 a();
12857 b();
12858 ˇ
12859 }
12860 "});
12861
12862 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12863
12864 cx.assert_editor_state(indoc! {"
12865 fn a() {
12866 a();
12867 b();
12868 //•ˇ
12869 }
12870 "});
12871
12872 // If a selection span multiple lines, empty lines are not toggled.
12873 cx.set_state(indoc! {"
12874 fn a() {
12875 «a();
12876
12877 c();ˇ»
12878 }
12879 "});
12880
12881 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12882
12883 cx.assert_editor_state(indoc! {"
12884 fn a() {
12885 // «a();
12886
12887 // c();ˇ»
12888 }
12889 "});
12890
12891 // If a selection includes multiple comment prefixes, all lines are uncommented.
12892 cx.set_state(indoc! {"
12893 fn a() {
12894 «// a();
12895 /// b();
12896 //! c();ˇ»
12897 }
12898 "});
12899
12900 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12901
12902 cx.assert_editor_state(indoc! {"
12903 fn a() {
12904 «a();
12905 b();
12906 c();ˇ»
12907 }
12908 "});
12909}
12910
12911#[gpui::test]
12912async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
12913 init_test(cx, |_| {});
12914 let mut cx = EditorTestContext::new(cx).await;
12915 let language = Arc::new(Language::new(
12916 LanguageConfig {
12917 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
12918 ..Default::default()
12919 },
12920 Some(tree_sitter_rust::LANGUAGE.into()),
12921 ));
12922 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
12923
12924 let toggle_comments = &ToggleComments {
12925 advance_downwards: false,
12926 ignore_indent: true,
12927 };
12928
12929 // If multiple selections intersect a line, the line is only toggled once.
12930 cx.set_state(indoc! {"
12931 fn a() {
12932 // «b();
12933 // c();
12934 // ˇ» d();
12935 }
12936 "});
12937
12938 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12939
12940 cx.assert_editor_state(indoc! {"
12941 fn a() {
12942 «b();
12943 c();
12944 ˇ» d();
12945 }
12946 "});
12947
12948 // The comment prefix is inserted at the beginning of each line
12949 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12950
12951 cx.assert_editor_state(indoc! {"
12952 fn a() {
12953 // «b();
12954 // c();
12955 // ˇ» d();
12956 }
12957 "});
12958
12959 // If a selection ends at the beginning of a line, that line is not toggled.
12960 cx.set_selections_state(indoc! {"
12961 fn a() {
12962 // b();
12963 // «c();
12964 ˇ»// d();
12965 }
12966 "});
12967
12968 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12969
12970 cx.assert_editor_state(indoc! {"
12971 fn a() {
12972 // b();
12973 «c();
12974 ˇ»// d();
12975 }
12976 "});
12977
12978 // If a selection span a single line and is empty, the line is toggled.
12979 cx.set_state(indoc! {"
12980 fn a() {
12981 a();
12982 b();
12983 ˇ
12984 }
12985 "});
12986
12987 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12988
12989 cx.assert_editor_state(indoc! {"
12990 fn a() {
12991 a();
12992 b();
12993 //ˇ
12994 }
12995 "});
12996
12997 // If a selection span multiple lines, empty lines are not toggled.
12998 cx.set_state(indoc! {"
12999 fn a() {
13000 «a();
13001
13002 c();ˇ»
13003 }
13004 "});
13005
13006 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13007
13008 cx.assert_editor_state(indoc! {"
13009 fn a() {
13010 // «a();
13011
13012 // c();ˇ»
13013 }
13014 "});
13015
13016 // If a selection includes multiple comment prefixes, all lines are uncommented.
13017 cx.set_state(indoc! {"
13018 fn a() {
13019 // «a();
13020 /// b();
13021 //! c();ˇ»
13022 }
13023 "});
13024
13025 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13026
13027 cx.assert_editor_state(indoc! {"
13028 fn a() {
13029 «a();
13030 b();
13031 c();ˇ»
13032 }
13033 "});
13034}
13035
13036#[gpui::test]
13037async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
13038 init_test(cx, |_| {});
13039
13040 let language = Arc::new(Language::new(
13041 LanguageConfig {
13042 line_comments: vec!["// ".into()],
13043 ..Default::default()
13044 },
13045 Some(tree_sitter_rust::LANGUAGE.into()),
13046 ));
13047
13048 let mut cx = EditorTestContext::new(cx).await;
13049
13050 cx.language_registry().add(language.clone());
13051 cx.update_buffer(|buffer, cx| {
13052 buffer.set_language(Some(language), cx);
13053 });
13054
13055 let toggle_comments = &ToggleComments {
13056 advance_downwards: true,
13057 ignore_indent: false,
13058 };
13059
13060 // Single cursor on one line -> advance
13061 // Cursor moves horizontally 3 characters as well on non-blank line
13062 cx.set_state(indoc!(
13063 "fn a() {
13064 ˇdog();
13065 cat();
13066 }"
13067 ));
13068 cx.update_editor(|editor, window, cx| {
13069 editor.toggle_comments(toggle_comments, window, cx);
13070 });
13071 cx.assert_editor_state(indoc!(
13072 "fn a() {
13073 // dog();
13074 catˇ();
13075 }"
13076 ));
13077
13078 // Single selection on one line -> don't advance
13079 cx.set_state(indoc!(
13080 "fn a() {
13081 «dog()ˇ»;
13082 cat();
13083 }"
13084 ));
13085 cx.update_editor(|editor, window, cx| {
13086 editor.toggle_comments(toggle_comments, window, cx);
13087 });
13088 cx.assert_editor_state(indoc!(
13089 "fn a() {
13090 // «dog()ˇ»;
13091 cat();
13092 }"
13093 ));
13094
13095 // Multiple cursors on one line -> advance
13096 cx.set_state(indoc!(
13097 "fn a() {
13098 ˇdˇog();
13099 cat();
13100 }"
13101 ));
13102 cx.update_editor(|editor, window, cx| {
13103 editor.toggle_comments(toggle_comments, window, cx);
13104 });
13105 cx.assert_editor_state(indoc!(
13106 "fn a() {
13107 // dog();
13108 catˇ(ˇ);
13109 }"
13110 ));
13111
13112 // Multiple cursors on one line, with selection -> don't advance
13113 cx.set_state(indoc!(
13114 "fn a() {
13115 ˇdˇog«()ˇ»;
13116 cat();
13117 }"
13118 ));
13119 cx.update_editor(|editor, window, cx| {
13120 editor.toggle_comments(toggle_comments, window, cx);
13121 });
13122 cx.assert_editor_state(indoc!(
13123 "fn a() {
13124 // ˇdˇog«()ˇ»;
13125 cat();
13126 }"
13127 ));
13128
13129 // Single cursor on one line -> advance
13130 // Cursor moves to column 0 on blank line
13131 cx.set_state(indoc!(
13132 "fn a() {
13133 ˇdog();
13134
13135 cat();
13136 }"
13137 ));
13138 cx.update_editor(|editor, window, cx| {
13139 editor.toggle_comments(toggle_comments, window, cx);
13140 });
13141 cx.assert_editor_state(indoc!(
13142 "fn a() {
13143 // dog();
13144 ˇ
13145 cat();
13146 }"
13147 ));
13148
13149 // Single cursor on one line -> advance
13150 // Cursor starts and ends at column 0
13151 cx.set_state(indoc!(
13152 "fn a() {
13153 ˇ dog();
13154 cat();
13155 }"
13156 ));
13157 cx.update_editor(|editor, window, cx| {
13158 editor.toggle_comments(toggle_comments, window, cx);
13159 });
13160 cx.assert_editor_state(indoc!(
13161 "fn a() {
13162 // dog();
13163 ˇ cat();
13164 }"
13165 ));
13166}
13167
13168#[gpui::test]
13169async fn test_toggle_block_comment(cx: &mut TestAppContext) {
13170 init_test(cx, |_| {});
13171
13172 let mut cx = EditorTestContext::new(cx).await;
13173
13174 let html_language = Arc::new(
13175 Language::new(
13176 LanguageConfig {
13177 name: "HTML".into(),
13178 block_comment: Some(("<!-- ".into(), " -->".into())),
13179 ..Default::default()
13180 },
13181 Some(tree_sitter_html::LANGUAGE.into()),
13182 )
13183 .with_injection_query(
13184 r#"
13185 (script_element
13186 (raw_text) @injection.content
13187 (#set! injection.language "javascript"))
13188 "#,
13189 )
13190 .unwrap(),
13191 );
13192
13193 let javascript_language = Arc::new(Language::new(
13194 LanguageConfig {
13195 name: "JavaScript".into(),
13196 line_comments: vec!["// ".into()],
13197 ..Default::default()
13198 },
13199 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
13200 ));
13201
13202 cx.language_registry().add(html_language.clone());
13203 cx.language_registry().add(javascript_language.clone());
13204 cx.update_buffer(|buffer, cx| {
13205 buffer.set_language(Some(html_language), cx);
13206 });
13207
13208 // Toggle comments for empty selections
13209 cx.set_state(
13210 &r#"
13211 <p>A</p>ˇ
13212 <p>B</p>ˇ
13213 <p>C</p>ˇ
13214 "#
13215 .unindent(),
13216 );
13217 cx.update_editor(|editor, window, cx| {
13218 editor.toggle_comments(&ToggleComments::default(), window, cx)
13219 });
13220 cx.assert_editor_state(
13221 &r#"
13222 <!-- <p>A</p>ˇ -->
13223 <!-- <p>B</p>ˇ -->
13224 <!-- <p>C</p>ˇ -->
13225 "#
13226 .unindent(),
13227 );
13228 cx.update_editor(|editor, window, cx| {
13229 editor.toggle_comments(&ToggleComments::default(), window, cx)
13230 });
13231 cx.assert_editor_state(
13232 &r#"
13233 <p>A</p>ˇ
13234 <p>B</p>ˇ
13235 <p>C</p>ˇ
13236 "#
13237 .unindent(),
13238 );
13239
13240 // Toggle comments for mixture of empty and non-empty selections, where
13241 // multiple selections occupy a given line.
13242 cx.set_state(
13243 &r#"
13244 <p>A«</p>
13245 <p>ˇ»B</p>ˇ
13246 <p>C«</p>
13247 <p>ˇ»D</p>ˇ
13248 "#
13249 .unindent(),
13250 );
13251
13252 cx.update_editor(|editor, window, cx| {
13253 editor.toggle_comments(&ToggleComments::default(), window, cx)
13254 });
13255 cx.assert_editor_state(
13256 &r#"
13257 <!-- <p>A«</p>
13258 <p>ˇ»B</p>ˇ -->
13259 <!-- <p>C«</p>
13260 <p>ˇ»D</p>ˇ -->
13261 "#
13262 .unindent(),
13263 );
13264 cx.update_editor(|editor, window, cx| {
13265 editor.toggle_comments(&ToggleComments::default(), window, cx)
13266 });
13267 cx.assert_editor_state(
13268 &r#"
13269 <p>A«</p>
13270 <p>ˇ»B</p>ˇ
13271 <p>C«</p>
13272 <p>ˇ»D</p>ˇ
13273 "#
13274 .unindent(),
13275 );
13276
13277 // Toggle comments when different languages are active for different
13278 // selections.
13279 cx.set_state(
13280 &r#"
13281 ˇ<script>
13282 ˇvar x = new Y();
13283 ˇ</script>
13284 "#
13285 .unindent(),
13286 );
13287 cx.executor().run_until_parked();
13288 cx.update_editor(|editor, window, cx| {
13289 editor.toggle_comments(&ToggleComments::default(), window, cx)
13290 });
13291 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
13292 // Uncommenting and commenting from this position brings in even more wrong artifacts.
13293 cx.assert_editor_state(
13294 &r#"
13295 <!-- ˇ<script> -->
13296 // ˇvar x = new Y();
13297 <!-- ˇ</script> -->
13298 "#
13299 .unindent(),
13300 );
13301}
13302
13303#[gpui::test]
13304fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
13305 init_test(cx, |_| {});
13306
13307 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13308 let multibuffer = cx.new(|cx| {
13309 let mut multibuffer = MultiBuffer::new(ReadWrite);
13310 multibuffer.push_excerpts(
13311 buffer.clone(),
13312 [
13313 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
13314 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
13315 ],
13316 cx,
13317 );
13318 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
13319 multibuffer
13320 });
13321
13322 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
13323 editor.update_in(cx, |editor, window, cx| {
13324 assert_eq!(editor.text(cx), "aaaa\nbbbb");
13325 editor.change_selections(None, window, cx, |s| {
13326 s.select_ranges([
13327 Point::new(0, 0)..Point::new(0, 0),
13328 Point::new(1, 0)..Point::new(1, 0),
13329 ])
13330 });
13331
13332 editor.handle_input("X", window, cx);
13333 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
13334 assert_eq!(
13335 editor.selections.ranges(cx),
13336 [
13337 Point::new(0, 1)..Point::new(0, 1),
13338 Point::new(1, 1)..Point::new(1, 1),
13339 ]
13340 );
13341
13342 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
13343 editor.change_selections(None, window, cx, |s| {
13344 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
13345 });
13346 editor.backspace(&Default::default(), window, cx);
13347 assert_eq!(editor.text(cx), "Xa\nbbb");
13348 assert_eq!(
13349 editor.selections.ranges(cx),
13350 [Point::new(1, 0)..Point::new(1, 0)]
13351 );
13352
13353 editor.change_selections(None, window, cx, |s| {
13354 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
13355 });
13356 editor.backspace(&Default::default(), window, cx);
13357 assert_eq!(editor.text(cx), "X\nbb");
13358 assert_eq!(
13359 editor.selections.ranges(cx),
13360 [Point::new(0, 1)..Point::new(0, 1)]
13361 );
13362 });
13363}
13364
13365#[gpui::test]
13366fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
13367 init_test(cx, |_| {});
13368
13369 let markers = vec![('[', ']').into(), ('(', ')').into()];
13370 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
13371 indoc! {"
13372 [aaaa
13373 (bbbb]
13374 cccc)",
13375 },
13376 markers.clone(),
13377 );
13378 let excerpt_ranges = markers.into_iter().map(|marker| {
13379 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
13380 ExcerptRange::new(context.clone())
13381 });
13382 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
13383 let multibuffer = cx.new(|cx| {
13384 let mut multibuffer = MultiBuffer::new(ReadWrite);
13385 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
13386 multibuffer
13387 });
13388
13389 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
13390 editor.update_in(cx, |editor, window, cx| {
13391 let (expected_text, selection_ranges) = marked_text_ranges(
13392 indoc! {"
13393 aaaa
13394 bˇbbb
13395 bˇbbˇb
13396 cccc"
13397 },
13398 true,
13399 );
13400 assert_eq!(editor.text(cx), expected_text);
13401 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
13402
13403 editor.handle_input("X", window, cx);
13404
13405 let (expected_text, expected_selections) = marked_text_ranges(
13406 indoc! {"
13407 aaaa
13408 bXˇbbXb
13409 bXˇbbXˇb
13410 cccc"
13411 },
13412 false,
13413 );
13414 assert_eq!(editor.text(cx), expected_text);
13415 assert_eq!(editor.selections.ranges(cx), expected_selections);
13416
13417 editor.newline(&Newline, window, cx);
13418 let (expected_text, expected_selections) = marked_text_ranges(
13419 indoc! {"
13420 aaaa
13421 bX
13422 ˇbbX
13423 b
13424 bX
13425 ˇbbX
13426 ˇb
13427 cccc"
13428 },
13429 false,
13430 );
13431 assert_eq!(editor.text(cx), expected_text);
13432 assert_eq!(editor.selections.ranges(cx), expected_selections);
13433 });
13434}
13435
13436#[gpui::test]
13437fn test_refresh_selections(cx: &mut TestAppContext) {
13438 init_test(cx, |_| {});
13439
13440 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13441 let mut excerpt1_id = None;
13442 let multibuffer = cx.new(|cx| {
13443 let mut multibuffer = MultiBuffer::new(ReadWrite);
13444 excerpt1_id = multibuffer
13445 .push_excerpts(
13446 buffer.clone(),
13447 [
13448 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
13449 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
13450 ],
13451 cx,
13452 )
13453 .into_iter()
13454 .next();
13455 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
13456 multibuffer
13457 });
13458
13459 let editor = cx.add_window(|window, cx| {
13460 let mut editor = build_editor(multibuffer.clone(), window, cx);
13461 let snapshot = editor.snapshot(window, cx);
13462 editor.change_selections(None, window, cx, |s| {
13463 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
13464 });
13465 editor.begin_selection(
13466 Point::new(2, 1).to_display_point(&snapshot),
13467 true,
13468 1,
13469 window,
13470 cx,
13471 );
13472 assert_eq!(
13473 editor.selections.ranges(cx),
13474 [
13475 Point::new(1, 3)..Point::new(1, 3),
13476 Point::new(2, 1)..Point::new(2, 1),
13477 ]
13478 );
13479 editor
13480 });
13481
13482 // Refreshing selections is a no-op when excerpts haven't changed.
13483 _ = editor.update(cx, |editor, window, cx| {
13484 editor.change_selections(None, window, cx, |s| s.refresh());
13485 assert_eq!(
13486 editor.selections.ranges(cx),
13487 [
13488 Point::new(1, 3)..Point::new(1, 3),
13489 Point::new(2, 1)..Point::new(2, 1),
13490 ]
13491 );
13492 });
13493
13494 multibuffer.update(cx, |multibuffer, cx| {
13495 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
13496 });
13497 _ = editor.update(cx, |editor, window, cx| {
13498 // Removing an excerpt causes the first selection to become degenerate.
13499 assert_eq!(
13500 editor.selections.ranges(cx),
13501 [
13502 Point::new(0, 0)..Point::new(0, 0),
13503 Point::new(0, 1)..Point::new(0, 1)
13504 ]
13505 );
13506
13507 // Refreshing selections will relocate the first selection to the original buffer
13508 // location.
13509 editor.change_selections(None, window, cx, |s| s.refresh());
13510 assert_eq!(
13511 editor.selections.ranges(cx),
13512 [
13513 Point::new(0, 1)..Point::new(0, 1),
13514 Point::new(0, 3)..Point::new(0, 3)
13515 ]
13516 );
13517 assert!(editor.selections.pending_anchor().is_some());
13518 });
13519}
13520
13521#[gpui::test]
13522fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
13523 init_test(cx, |_| {});
13524
13525 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13526 let mut excerpt1_id = None;
13527 let multibuffer = cx.new(|cx| {
13528 let mut multibuffer = MultiBuffer::new(ReadWrite);
13529 excerpt1_id = multibuffer
13530 .push_excerpts(
13531 buffer.clone(),
13532 [
13533 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
13534 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
13535 ],
13536 cx,
13537 )
13538 .into_iter()
13539 .next();
13540 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
13541 multibuffer
13542 });
13543
13544 let editor = cx.add_window(|window, cx| {
13545 let mut editor = build_editor(multibuffer.clone(), window, cx);
13546 let snapshot = editor.snapshot(window, cx);
13547 editor.begin_selection(
13548 Point::new(1, 3).to_display_point(&snapshot),
13549 false,
13550 1,
13551 window,
13552 cx,
13553 );
13554 assert_eq!(
13555 editor.selections.ranges(cx),
13556 [Point::new(1, 3)..Point::new(1, 3)]
13557 );
13558 editor
13559 });
13560
13561 multibuffer.update(cx, |multibuffer, cx| {
13562 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
13563 });
13564 _ = editor.update(cx, |editor, window, cx| {
13565 assert_eq!(
13566 editor.selections.ranges(cx),
13567 [Point::new(0, 0)..Point::new(0, 0)]
13568 );
13569
13570 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
13571 editor.change_selections(None, window, cx, |s| s.refresh());
13572 assert_eq!(
13573 editor.selections.ranges(cx),
13574 [Point::new(0, 3)..Point::new(0, 3)]
13575 );
13576 assert!(editor.selections.pending_anchor().is_some());
13577 });
13578}
13579
13580#[gpui::test]
13581async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
13582 init_test(cx, |_| {});
13583
13584 let language = Arc::new(
13585 Language::new(
13586 LanguageConfig {
13587 brackets: BracketPairConfig {
13588 pairs: vec![
13589 BracketPair {
13590 start: "{".to_string(),
13591 end: "}".to_string(),
13592 close: true,
13593 surround: true,
13594 newline: true,
13595 },
13596 BracketPair {
13597 start: "/* ".to_string(),
13598 end: " */".to_string(),
13599 close: true,
13600 surround: true,
13601 newline: true,
13602 },
13603 ],
13604 ..Default::default()
13605 },
13606 ..Default::default()
13607 },
13608 Some(tree_sitter_rust::LANGUAGE.into()),
13609 )
13610 .with_indents_query("")
13611 .unwrap(),
13612 );
13613
13614 let text = concat!(
13615 "{ }\n", //
13616 " x\n", //
13617 " /* */\n", //
13618 "x\n", //
13619 "{{} }\n", //
13620 );
13621
13622 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
13623 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13624 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
13625 editor
13626 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
13627 .await;
13628
13629 editor.update_in(cx, |editor, window, cx| {
13630 editor.change_selections(None, window, cx, |s| {
13631 s.select_display_ranges([
13632 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
13633 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
13634 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
13635 ])
13636 });
13637 editor.newline(&Newline, window, cx);
13638
13639 assert_eq!(
13640 editor.buffer().read(cx).read(cx).text(),
13641 concat!(
13642 "{ \n", // Suppress rustfmt
13643 "\n", //
13644 "}\n", //
13645 " x\n", //
13646 " /* \n", //
13647 " \n", //
13648 " */\n", //
13649 "x\n", //
13650 "{{} \n", //
13651 "}\n", //
13652 )
13653 );
13654 });
13655}
13656
13657#[gpui::test]
13658fn test_highlighted_ranges(cx: &mut TestAppContext) {
13659 init_test(cx, |_| {});
13660
13661 let editor = cx.add_window(|window, cx| {
13662 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
13663 build_editor(buffer.clone(), window, cx)
13664 });
13665
13666 _ = editor.update(cx, |editor, window, cx| {
13667 struct Type1;
13668 struct Type2;
13669
13670 let buffer = editor.buffer.read(cx).snapshot(cx);
13671
13672 let anchor_range =
13673 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
13674
13675 editor.highlight_background::<Type1>(
13676 &[
13677 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
13678 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
13679 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
13680 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
13681 ],
13682 |_| Hsla::red(),
13683 cx,
13684 );
13685 editor.highlight_background::<Type2>(
13686 &[
13687 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
13688 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
13689 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
13690 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
13691 ],
13692 |_| Hsla::green(),
13693 cx,
13694 );
13695
13696 let snapshot = editor.snapshot(window, cx);
13697 let mut highlighted_ranges = editor.background_highlights_in_range(
13698 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
13699 &snapshot,
13700 cx.theme(),
13701 );
13702 // Enforce a consistent ordering based on color without relying on the ordering of the
13703 // highlight's `TypeId` which is non-executor.
13704 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
13705 assert_eq!(
13706 highlighted_ranges,
13707 &[
13708 (
13709 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
13710 Hsla::red(),
13711 ),
13712 (
13713 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
13714 Hsla::red(),
13715 ),
13716 (
13717 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
13718 Hsla::green(),
13719 ),
13720 (
13721 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
13722 Hsla::green(),
13723 ),
13724 ]
13725 );
13726 assert_eq!(
13727 editor.background_highlights_in_range(
13728 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
13729 &snapshot,
13730 cx.theme(),
13731 ),
13732 &[(
13733 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
13734 Hsla::red(),
13735 )]
13736 );
13737 });
13738}
13739
13740#[gpui::test]
13741async fn test_following(cx: &mut TestAppContext) {
13742 init_test(cx, |_| {});
13743
13744 let fs = FakeFs::new(cx.executor());
13745 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
13746
13747 let buffer = project.update(cx, |project, cx| {
13748 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
13749 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
13750 });
13751 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
13752 let follower = cx.update(|cx| {
13753 cx.open_window(
13754 WindowOptions {
13755 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
13756 gpui::Point::new(px(0.), px(0.)),
13757 gpui::Point::new(px(10.), px(80.)),
13758 ))),
13759 ..Default::default()
13760 },
13761 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
13762 )
13763 .unwrap()
13764 });
13765
13766 let is_still_following = Rc::new(RefCell::new(true));
13767 let follower_edit_event_count = Rc::new(RefCell::new(0));
13768 let pending_update = Rc::new(RefCell::new(None));
13769 let leader_entity = leader.root(cx).unwrap();
13770 let follower_entity = follower.root(cx).unwrap();
13771 _ = follower.update(cx, {
13772 let update = pending_update.clone();
13773 let is_still_following = is_still_following.clone();
13774 let follower_edit_event_count = follower_edit_event_count.clone();
13775 |_, window, cx| {
13776 cx.subscribe_in(
13777 &leader_entity,
13778 window,
13779 move |_, leader, event, window, cx| {
13780 leader.read(cx).add_event_to_update_proto(
13781 event,
13782 &mut update.borrow_mut(),
13783 window,
13784 cx,
13785 );
13786 },
13787 )
13788 .detach();
13789
13790 cx.subscribe_in(
13791 &follower_entity,
13792 window,
13793 move |_, _, event: &EditorEvent, _window, _cx| {
13794 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
13795 *is_still_following.borrow_mut() = false;
13796 }
13797
13798 if let EditorEvent::BufferEdited = event {
13799 *follower_edit_event_count.borrow_mut() += 1;
13800 }
13801 },
13802 )
13803 .detach();
13804 }
13805 });
13806
13807 // Update the selections only
13808 _ = leader.update(cx, |leader, window, cx| {
13809 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
13810 });
13811 follower
13812 .update(cx, |follower, window, cx| {
13813 follower.apply_update_proto(
13814 &project,
13815 pending_update.borrow_mut().take().unwrap(),
13816 window,
13817 cx,
13818 )
13819 })
13820 .unwrap()
13821 .await
13822 .unwrap();
13823 _ = follower.update(cx, |follower, _, cx| {
13824 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
13825 });
13826 assert!(*is_still_following.borrow());
13827 assert_eq!(*follower_edit_event_count.borrow(), 0);
13828
13829 // Update the scroll position only
13830 _ = leader.update(cx, |leader, window, cx| {
13831 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
13832 });
13833 follower
13834 .update(cx, |follower, window, cx| {
13835 follower.apply_update_proto(
13836 &project,
13837 pending_update.borrow_mut().take().unwrap(),
13838 window,
13839 cx,
13840 )
13841 })
13842 .unwrap()
13843 .await
13844 .unwrap();
13845 assert_eq!(
13846 follower
13847 .update(cx, |follower, _, cx| follower.scroll_position(cx))
13848 .unwrap(),
13849 gpui::Point::new(1.5, 3.5)
13850 );
13851 assert!(*is_still_following.borrow());
13852 assert_eq!(*follower_edit_event_count.borrow(), 0);
13853
13854 // Update the selections and scroll position. The follower's scroll position is updated
13855 // via autoscroll, not via the leader's exact scroll position.
13856 _ = leader.update(cx, |leader, window, cx| {
13857 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
13858 leader.request_autoscroll(Autoscroll::newest(), cx);
13859 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
13860 });
13861 follower
13862 .update(cx, |follower, window, cx| {
13863 follower.apply_update_proto(
13864 &project,
13865 pending_update.borrow_mut().take().unwrap(),
13866 window,
13867 cx,
13868 )
13869 })
13870 .unwrap()
13871 .await
13872 .unwrap();
13873 _ = follower.update(cx, |follower, _, cx| {
13874 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
13875 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
13876 });
13877 assert!(*is_still_following.borrow());
13878
13879 // Creating a pending selection that precedes another selection
13880 _ = leader.update(cx, |leader, window, cx| {
13881 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
13882 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
13883 });
13884 follower
13885 .update(cx, |follower, window, cx| {
13886 follower.apply_update_proto(
13887 &project,
13888 pending_update.borrow_mut().take().unwrap(),
13889 window,
13890 cx,
13891 )
13892 })
13893 .unwrap()
13894 .await
13895 .unwrap();
13896 _ = follower.update(cx, |follower, _, cx| {
13897 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
13898 });
13899 assert!(*is_still_following.borrow());
13900
13901 // Extend the pending selection so that it surrounds another selection
13902 _ = leader.update(cx, |leader, window, cx| {
13903 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
13904 });
13905 follower
13906 .update(cx, |follower, window, cx| {
13907 follower.apply_update_proto(
13908 &project,
13909 pending_update.borrow_mut().take().unwrap(),
13910 window,
13911 cx,
13912 )
13913 })
13914 .unwrap()
13915 .await
13916 .unwrap();
13917 _ = follower.update(cx, |follower, _, cx| {
13918 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
13919 });
13920
13921 // Scrolling locally breaks the follow
13922 _ = follower.update(cx, |follower, window, cx| {
13923 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
13924 follower.set_scroll_anchor(
13925 ScrollAnchor {
13926 anchor: top_anchor,
13927 offset: gpui::Point::new(0.0, 0.5),
13928 },
13929 window,
13930 cx,
13931 );
13932 });
13933 assert!(!(*is_still_following.borrow()));
13934}
13935
13936#[gpui::test]
13937async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
13938 init_test(cx, |_| {});
13939
13940 let fs = FakeFs::new(cx.executor());
13941 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
13942 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13943 let pane = workspace
13944 .update(cx, |workspace, _, _| workspace.active_pane().clone())
13945 .unwrap();
13946
13947 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
13948
13949 let leader = pane.update_in(cx, |_, window, cx| {
13950 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
13951 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
13952 });
13953
13954 // Start following the editor when it has no excerpts.
13955 let mut state_message =
13956 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
13957 let workspace_entity = workspace.root(cx).unwrap();
13958 let follower_1 = cx
13959 .update_window(*workspace.deref(), |_, window, cx| {
13960 Editor::from_state_proto(
13961 workspace_entity,
13962 ViewId {
13963 creator: CollaboratorId::PeerId(PeerId::default()),
13964 id: 0,
13965 },
13966 &mut state_message,
13967 window,
13968 cx,
13969 )
13970 })
13971 .unwrap()
13972 .unwrap()
13973 .await
13974 .unwrap();
13975
13976 let update_message = Rc::new(RefCell::new(None));
13977 follower_1.update_in(cx, {
13978 let update = update_message.clone();
13979 |_, window, cx| {
13980 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
13981 leader.read(cx).add_event_to_update_proto(
13982 event,
13983 &mut update.borrow_mut(),
13984 window,
13985 cx,
13986 );
13987 })
13988 .detach();
13989 }
13990 });
13991
13992 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
13993 (
13994 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
13995 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
13996 )
13997 });
13998
13999 // Insert some excerpts.
14000 leader.update(cx, |leader, cx| {
14001 leader.buffer.update(cx, |multibuffer, cx| {
14002 multibuffer.set_excerpts_for_path(
14003 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
14004 buffer_1.clone(),
14005 vec![
14006 Point::row_range(0..3),
14007 Point::row_range(1..6),
14008 Point::row_range(12..15),
14009 ],
14010 0,
14011 cx,
14012 );
14013 multibuffer.set_excerpts_for_path(
14014 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
14015 buffer_2.clone(),
14016 vec![Point::row_range(0..6), Point::row_range(8..12)],
14017 0,
14018 cx,
14019 );
14020 });
14021 });
14022
14023 // Apply the update of adding the excerpts.
14024 follower_1
14025 .update_in(cx, |follower, window, cx| {
14026 follower.apply_update_proto(
14027 &project,
14028 update_message.borrow().clone().unwrap(),
14029 window,
14030 cx,
14031 )
14032 })
14033 .await
14034 .unwrap();
14035 assert_eq!(
14036 follower_1.update(cx, |editor, cx| editor.text(cx)),
14037 leader.update(cx, |editor, cx| editor.text(cx))
14038 );
14039 update_message.borrow_mut().take();
14040
14041 // Start following separately after it already has excerpts.
14042 let mut state_message =
14043 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14044 let workspace_entity = workspace.root(cx).unwrap();
14045 let follower_2 = cx
14046 .update_window(*workspace.deref(), |_, window, cx| {
14047 Editor::from_state_proto(
14048 workspace_entity,
14049 ViewId {
14050 creator: CollaboratorId::PeerId(PeerId::default()),
14051 id: 0,
14052 },
14053 &mut state_message,
14054 window,
14055 cx,
14056 )
14057 })
14058 .unwrap()
14059 .unwrap()
14060 .await
14061 .unwrap();
14062 assert_eq!(
14063 follower_2.update(cx, |editor, cx| editor.text(cx)),
14064 leader.update(cx, |editor, cx| editor.text(cx))
14065 );
14066
14067 // Remove some excerpts.
14068 leader.update(cx, |leader, cx| {
14069 leader.buffer.update(cx, |multibuffer, cx| {
14070 let excerpt_ids = multibuffer.excerpt_ids();
14071 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
14072 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
14073 });
14074 });
14075
14076 // Apply the update of removing the excerpts.
14077 follower_1
14078 .update_in(cx, |follower, window, cx| {
14079 follower.apply_update_proto(
14080 &project,
14081 update_message.borrow().clone().unwrap(),
14082 window,
14083 cx,
14084 )
14085 })
14086 .await
14087 .unwrap();
14088 follower_2
14089 .update_in(cx, |follower, window, cx| {
14090 follower.apply_update_proto(
14091 &project,
14092 update_message.borrow().clone().unwrap(),
14093 window,
14094 cx,
14095 )
14096 })
14097 .await
14098 .unwrap();
14099 update_message.borrow_mut().take();
14100 assert_eq!(
14101 follower_1.update(cx, |editor, cx| editor.text(cx)),
14102 leader.update(cx, |editor, cx| editor.text(cx))
14103 );
14104}
14105
14106#[gpui::test]
14107async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14108 init_test(cx, |_| {});
14109
14110 let mut cx = EditorTestContext::new(cx).await;
14111 let lsp_store =
14112 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
14113
14114 cx.set_state(indoc! {"
14115 ˇfn func(abc def: i32) -> u32 {
14116 }
14117 "});
14118
14119 cx.update(|_, cx| {
14120 lsp_store.update(cx, |lsp_store, cx| {
14121 lsp_store
14122 .update_diagnostics(
14123 LanguageServerId(0),
14124 lsp::PublishDiagnosticsParams {
14125 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
14126 version: None,
14127 diagnostics: vec![
14128 lsp::Diagnostic {
14129 range: lsp::Range::new(
14130 lsp::Position::new(0, 11),
14131 lsp::Position::new(0, 12),
14132 ),
14133 severity: Some(lsp::DiagnosticSeverity::ERROR),
14134 ..Default::default()
14135 },
14136 lsp::Diagnostic {
14137 range: lsp::Range::new(
14138 lsp::Position::new(0, 12),
14139 lsp::Position::new(0, 15),
14140 ),
14141 severity: Some(lsp::DiagnosticSeverity::ERROR),
14142 ..Default::default()
14143 },
14144 lsp::Diagnostic {
14145 range: lsp::Range::new(
14146 lsp::Position::new(0, 25),
14147 lsp::Position::new(0, 28),
14148 ),
14149 severity: Some(lsp::DiagnosticSeverity::ERROR),
14150 ..Default::default()
14151 },
14152 ],
14153 },
14154 None,
14155 DiagnosticSourceKind::Pushed,
14156 &[],
14157 cx,
14158 )
14159 .unwrap()
14160 });
14161 });
14162
14163 executor.run_until_parked();
14164
14165 cx.update_editor(|editor, window, cx| {
14166 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14167 });
14168
14169 cx.assert_editor_state(indoc! {"
14170 fn func(abc def: i32) -> ˇu32 {
14171 }
14172 "});
14173
14174 cx.update_editor(|editor, window, cx| {
14175 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14176 });
14177
14178 cx.assert_editor_state(indoc! {"
14179 fn func(abc ˇdef: i32) -> u32 {
14180 }
14181 "});
14182
14183 cx.update_editor(|editor, window, cx| {
14184 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14185 });
14186
14187 cx.assert_editor_state(indoc! {"
14188 fn func(abcˇ def: i32) -> u32 {
14189 }
14190 "});
14191
14192 cx.update_editor(|editor, window, cx| {
14193 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14194 });
14195
14196 cx.assert_editor_state(indoc! {"
14197 fn func(abc def: i32) -> ˇu32 {
14198 }
14199 "});
14200}
14201
14202#[gpui::test]
14203async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14204 init_test(cx, |_| {});
14205
14206 let mut cx = EditorTestContext::new(cx).await;
14207
14208 let diff_base = r#"
14209 use some::mod;
14210
14211 const A: u32 = 42;
14212
14213 fn main() {
14214 println!("hello");
14215
14216 println!("world");
14217 }
14218 "#
14219 .unindent();
14220
14221 // Edits are modified, removed, modified, added
14222 cx.set_state(
14223 &r#"
14224 use some::modified;
14225
14226 ˇ
14227 fn main() {
14228 println!("hello there");
14229
14230 println!("around the");
14231 println!("world");
14232 }
14233 "#
14234 .unindent(),
14235 );
14236
14237 cx.set_head_text(&diff_base);
14238 executor.run_until_parked();
14239
14240 cx.update_editor(|editor, window, cx| {
14241 //Wrap around the bottom of the buffer
14242 for _ in 0..3 {
14243 editor.go_to_next_hunk(&GoToHunk, window, cx);
14244 }
14245 });
14246
14247 cx.assert_editor_state(
14248 &r#"
14249 ˇuse some::modified;
14250
14251
14252 fn main() {
14253 println!("hello there");
14254
14255 println!("around the");
14256 println!("world");
14257 }
14258 "#
14259 .unindent(),
14260 );
14261
14262 cx.update_editor(|editor, window, cx| {
14263 //Wrap around the top of the buffer
14264 for _ in 0..2 {
14265 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14266 }
14267 });
14268
14269 cx.assert_editor_state(
14270 &r#"
14271 use some::modified;
14272
14273
14274 fn main() {
14275 ˇ println!("hello there");
14276
14277 println!("around the");
14278 println!("world");
14279 }
14280 "#
14281 .unindent(),
14282 );
14283
14284 cx.update_editor(|editor, window, cx| {
14285 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14286 });
14287
14288 cx.assert_editor_state(
14289 &r#"
14290 use some::modified;
14291
14292 ˇ
14293 fn main() {
14294 println!("hello there");
14295
14296 println!("around the");
14297 println!("world");
14298 }
14299 "#
14300 .unindent(),
14301 );
14302
14303 cx.update_editor(|editor, window, cx| {
14304 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14305 });
14306
14307 cx.assert_editor_state(
14308 &r#"
14309 ˇuse some::modified;
14310
14311
14312 fn main() {
14313 println!("hello there");
14314
14315 println!("around the");
14316 println!("world");
14317 }
14318 "#
14319 .unindent(),
14320 );
14321
14322 cx.update_editor(|editor, window, cx| {
14323 for _ in 0..2 {
14324 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14325 }
14326 });
14327
14328 cx.assert_editor_state(
14329 &r#"
14330 use some::modified;
14331
14332
14333 fn main() {
14334 ˇ println!("hello there");
14335
14336 println!("around the");
14337 println!("world");
14338 }
14339 "#
14340 .unindent(),
14341 );
14342
14343 cx.update_editor(|editor, window, cx| {
14344 editor.fold(&Fold, window, cx);
14345 });
14346
14347 cx.update_editor(|editor, window, cx| {
14348 editor.go_to_next_hunk(&GoToHunk, window, cx);
14349 });
14350
14351 cx.assert_editor_state(
14352 &r#"
14353 ˇuse some::modified;
14354
14355
14356 fn main() {
14357 println!("hello there");
14358
14359 println!("around the");
14360 println!("world");
14361 }
14362 "#
14363 .unindent(),
14364 );
14365}
14366
14367#[test]
14368fn test_split_words() {
14369 fn split(text: &str) -> Vec<&str> {
14370 split_words(text).collect()
14371 }
14372
14373 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
14374 assert_eq!(split("hello_world"), &["hello_", "world"]);
14375 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
14376 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
14377 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
14378 assert_eq!(split("helloworld"), &["helloworld"]);
14379
14380 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
14381}
14382
14383#[gpui::test]
14384async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
14385 init_test(cx, |_| {});
14386
14387 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
14388 let mut assert = |before, after| {
14389 let _state_context = cx.set_state(before);
14390 cx.run_until_parked();
14391 cx.update_editor(|editor, window, cx| {
14392 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
14393 });
14394 cx.run_until_parked();
14395 cx.assert_editor_state(after);
14396 };
14397
14398 // Outside bracket jumps to outside of matching bracket
14399 assert("console.logˇ(var);", "console.log(var)ˇ;");
14400 assert("console.log(var)ˇ;", "console.logˇ(var);");
14401
14402 // Inside bracket jumps to inside of matching bracket
14403 assert("console.log(ˇvar);", "console.log(varˇ);");
14404 assert("console.log(varˇ);", "console.log(ˇvar);");
14405
14406 // When outside a bracket and inside, favor jumping to the inside bracket
14407 assert(
14408 "console.log('foo', [1, 2, 3]ˇ);",
14409 "console.log(ˇ'foo', [1, 2, 3]);",
14410 );
14411 assert(
14412 "console.log(ˇ'foo', [1, 2, 3]);",
14413 "console.log('foo', [1, 2, 3]ˇ);",
14414 );
14415
14416 // Bias forward if two options are equally likely
14417 assert(
14418 "let result = curried_fun()ˇ();",
14419 "let result = curried_fun()()ˇ;",
14420 );
14421
14422 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
14423 assert(
14424 indoc! {"
14425 function test() {
14426 console.log('test')ˇ
14427 }"},
14428 indoc! {"
14429 function test() {
14430 console.logˇ('test')
14431 }"},
14432 );
14433}
14434
14435#[gpui::test]
14436async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
14437 init_test(cx, |_| {});
14438
14439 let fs = FakeFs::new(cx.executor());
14440 fs.insert_tree(
14441 path!("/a"),
14442 json!({
14443 "main.rs": "fn main() { let a = 5; }",
14444 "other.rs": "// Test file",
14445 }),
14446 )
14447 .await;
14448 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14449
14450 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14451 language_registry.add(Arc::new(Language::new(
14452 LanguageConfig {
14453 name: "Rust".into(),
14454 matcher: LanguageMatcher {
14455 path_suffixes: vec!["rs".to_string()],
14456 ..Default::default()
14457 },
14458 brackets: BracketPairConfig {
14459 pairs: vec![BracketPair {
14460 start: "{".to_string(),
14461 end: "}".to_string(),
14462 close: true,
14463 surround: true,
14464 newline: true,
14465 }],
14466 disabled_scopes_by_bracket_ix: Vec::new(),
14467 },
14468 ..Default::default()
14469 },
14470 Some(tree_sitter_rust::LANGUAGE.into()),
14471 )));
14472 let mut fake_servers = language_registry.register_fake_lsp(
14473 "Rust",
14474 FakeLspAdapter {
14475 capabilities: lsp::ServerCapabilities {
14476 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
14477 first_trigger_character: "{".to_string(),
14478 more_trigger_character: None,
14479 }),
14480 ..Default::default()
14481 },
14482 ..Default::default()
14483 },
14484 );
14485
14486 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14487
14488 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14489
14490 let worktree_id = workspace
14491 .update(cx, |workspace, _, cx| {
14492 workspace.project().update(cx, |project, cx| {
14493 project.worktrees(cx).next().unwrap().read(cx).id()
14494 })
14495 })
14496 .unwrap();
14497
14498 let buffer = project
14499 .update(cx, |project, cx| {
14500 project.open_local_buffer(path!("/a/main.rs"), cx)
14501 })
14502 .await
14503 .unwrap();
14504 let editor_handle = workspace
14505 .update(cx, |workspace, window, cx| {
14506 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
14507 })
14508 .unwrap()
14509 .await
14510 .unwrap()
14511 .downcast::<Editor>()
14512 .unwrap();
14513
14514 cx.executor().start_waiting();
14515 let fake_server = fake_servers.next().await.unwrap();
14516
14517 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
14518 |params, _| async move {
14519 assert_eq!(
14520 params.text_document_position.text_document.uri,
14521 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
14522 );
14523 assert_eq!(
14524 params.text_document_position.position,
14525 lsp::Position::new(0, 21),
14526 );
14527
14528 Ok(Some(vec![lsp::TextEdit {
14529 new_text: "]".to_string(),
14530 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14531 }]))
14532 },
14533 );
14534
14535 editor_handle.update_in(cx, |editor, window, cx| {
14536 window.focus(&editor.focus_handle(cx));
14537 editor.change_selections(None, window, cx, |s| {
14538 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
14539 });
14540 editor.handle_input("{", window, cx);
14541 });
14542
14543 cx.executor().run_until_parked();
14544
14545 buffer.update(cx, |buffer, _| {
14546 assert_eq!(
14547 buffer.text(),
14548 "fn main() { let a = {5}; }",
14549 "No extra braces from on type formatting should appear in the buffer"
14550 )
14551 });
14552}
14553
14554#[gpui::test]
14555async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
14556 init_test(cx, |_| {});
14557
14558 let fs = FakeFs::new(cx.executor());
14559 fs.insert_tree(
14560 path!("/a"),
14561 json!({
14562 "main.rs": "fn main() { let a = 5; }",
14563 "other.rs": "// Test file",
14564 }),
14565 )
14566 .await;
14567
14568 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14569
14570 let server_restarts = Arc::new(AtomicUsize::new(0));
14571 let closure_restarts = Arc::clone(&server_restarts);
14572 let language_server_name = "test language server";
14573 let language_name: LanguageName = "Rust".into();
14574
14575 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14576 language_registry.add(Arc::new(Language::new(
14577 LanguageConfig {
14578 name: language_name.clone(),
14579 matcher: LanguageMatcher {
14580 path_suffixes: vec!["rs".to_string()],
14581 ..Default::default()
14582 },
14583 ..Default::default()
14584 },
14585 Some(tree_sitter_rust::LANGUAGE.into()),
14586 )));
14587 let mut fake_servers = language_registry.register_fake_lsp(
14588 "Rust",
14589 FakeLspAdapter {
14590 name: language_server_name,
14591 initialization_options: Some(json!({
14592 "testOptionValue": true
14593 })),
14594 initializer: Some(Box::new(move |fake_server| {
14595 let task_restarts = Arc::clone(&closure_restarts);
14596 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
14597 task_restarts.fetch_add(1, atomic::Ordering::Release);
14598 futures::future::ready(Ok(()))
14599 });
14600 })),
14601 ..Default::default()
14602 },
14603 );
14604
14605 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14606 let _buffer = project
14607 .update(cx, |project, cx| {
14608 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
14609 })
14610 .await
14611 .unwrap();
14612 let _fake_server = fake_servers.next().await.unwrap();
14613 update_test_language_settings(cx, |language_settings| {
14614 language_settings.languages.insert(
14615 language_name.clone(),
14616 LanguageSettingsContent {
14617 tab_size: NonZeroU32::new(8),
14618 ..Default::default()
14619 },
14620 );
14621 });
14622 cx.executor().run_until_parked();
14623 assert_eq!(
14624 server_restarts.load(atomic::Ordering::Acquire),
14625 0,
14626 "Should not restart LSP server on an unrelated change"
14627 );
14628
14629 update_test_project_settings(cx, |project_settings| {
14630 project_settings.lsp.insert(
14631 "Some other server name".into(),
14632 LspSettings {
14633 binary: None,
14634 settings: None,
14635 initialization_options: Some(json!({
14636 "some other init value": false
14637 })),
14638 enable_lsp_tasks: false,
14639 },
14640 );
14641 });
14642 cx.executor().run_until_parked();
14643 assert_eq!(
14644 server_restarts.load(atomic::Ordering::Acquire),
14645 0,
14646 "Should not restart LSP server on an unrelated LSP settings change"
14647 );
14648
14649 update_test_project_settings(cx, |project_settings| {
14650 project_settings.lsp.insert(
14651 language_server_name.into(),
14652 LspSettings {
14653 binary: None,
14654 settings: None,
14655 initialization_options: Some(json!({
14656 "anotherInitValue": false
14657 })),
14658 enable_lsp_tasks: false,
14659 },
14660 );
14661 });
14662 cx.executor().run_until_parked();
14663 assert_eq!(
14664 server_restarts.load(atomic::Ordering::Acquire),
14665 1,
14666 "Should restart LSP server on a related LSP settings change"
14667 );
14668
14669 update_test_project_settings(cx, |project_settings| {
14670 project_settings.lsp.insert(
14671 language_server_name.into(),
14672 LspSettings {
14673 binary: None,
14674 settings: None,
14675 initialization_options: Some(json!({
14676 "anotherInitValue": false
14677 })),
14678 enable_lsp_tasks: false,
14679 },
14680 );
14681 });
14682 cx.executor().run_until_parked();
14683 assert_eq!(
14684 server_restarts.load(atomic::Ordering::Acquire),
14685 1,
14686 "Should not restart LSP server on a related LSP settings change that is the same"
14687 );
14688
14689 update_test_project_settings(cx, |project_settings| {
14690 project_settings.lsp.insert(
14691 language_server_name.into(),
14692 LspSettings {
14693 binary: None,
14694 settings: None,
14695 initialization_options: None,
14696 enable_lsp_tasks: false,
14697 },
14698 );
14699 });
14700 cx.executor().run_until_parked();
14701 assert_eq!(
14702 server_restarts.load(atomic::Ordering::Acquire),
14703 2,
14704 "Should restart LSP server on another related LSP settings change"
14705 );
14706}
14707
14708#[gpui::test]
14709async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
14710 init_test(cx, |_| {});
14711
14712 let mut cx = EditorLspTestContext::new_rust(
14713 lsp::ServerCapabilities {
14714 completion_provider: Some(lsp::CompletionOptions {
14715 trigger_characters: Some(vec![".".to_string()]),
14716 resolve_provider: Some(true),
14717 ..Default::default()
14718 }),
14719 ..Default::default()
14720 },
14721 cx,
14722 )
14723 .await;
14724
14725 cx.set_state("fn main() { let a = 2ˇ; }");
14726 cx.simulate_keystroke(".");
14727 let completion_item = lsp::CompletionItem {
14728 label: "some".into(),
14729 kind: Some(lsp::CompletionItemKind::SNIPPET),
14730 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
14731 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
14732 kind: lsp::MarkupKind::Markdown,
14733 value: "```rust\nSome(2)\n```".to_string(),
14734 })),
14735 deprecated: Some(false),
14736 sort_text: Some("fffffff2".to_string()),
14737 filter_text: Some("some".to_string()),
14738 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
14739 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14740 range: lsp::Range {
14741 start: lsp::Position {
14742 line: 0,
14743 character: 22,
14744 },
14745 end: lsp::Position {
14746 line: 0,
14747 character: 22,
14748 },
14749 },
14750 new_text: "Some(2)".to_string(),
14751 })),
14752 additional_text_edits: Some(vec![lsp::TextEdit {
14753 range: lsp::Range {
14754 start: lsp::Position {
14755 line: 0,
14756 character: 20,
14757 },
14758 end: lsp::Position {
14759 line: 0,
14760 character: 22,
14761 },
14762 },
14763 new_text: "".to_string(),
14764 }]),
14765 ..Default::default()
14766 };
14767
14768 let closure_completion_item = completion_item.clone();
14769 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14770 let task_completion_item = closure_completion_item.clone();
14771 async move {
14772 Ok(Some(lsp::CompletionResponse::Array(vec![
14773 task_completion_item,
14774 ])))
14775 }
14776 });
14777
14778 request.next().await;
14779
14780 cx.condition(|editor, _| editor.context_menu_visible())
14781 .await;
14782 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14783 editor
14784 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14785 .unwrap()
14786 });
14787 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
14788
14789 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
14790 let task_completion_item = completion_item.clone();
14791 async move { Ok(task_completion_item) }
14792 })
14793 .next()
14794 .await
14795 .unwrap();
14796 apply_additional_edits.await.unwrap();
14797 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
14798}
14799
14800#[gpui::test]
14801async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
14802 init_test(cx, |_| {});
14803
14804 let mut cx = EditorLspTestContext::new_rust(
14805 lsp::ServerCapabilities {
14806 completion_provider: Some(lsp::CompletionOptions {
14807 trigger_characters: Some(vec![".".to_string()]),
14808 resolve_provider: Some(true),
14809 ..Default::default()
14810 }),
14811 ..Default::default()
14812 },
14813 cx,
14814 )
14815 .await;
14816
14817 cx.set_state("fn main() { let a = 2ˇ; }");
14818 cx.simulate_keystroke(".");
14819
14820 let item1 = lsp::CompletionItem {
14821 label: "method id()".to_string(),
14822 filter_text: Some("id".to_string()),
14823 detail: None,
14824 documentation: None,
14825 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14826 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14827 new_text: ".id".to_string(),
14828 })),
14829 ..lsp::CompletionItem::default()
14830 };
14831
14832 let item2 = lsp::CompletionItem {
14833 label: "other".to_string(),
14834 filter_text: Some("other".to_string()),
14835 detail: None,
14836 documentation: None,
14837 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14838 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14839 new_text: ".other".to_string(),
14840 })),
14841 ..lsp::CompletionItem::default()
14842 };
14843
14844 let item1 = item1.clone();
14845 cx.set_request_handler::<lsp::request::Completion, _, _>({
14846 let item1 = item1.clone();
14847 move |_, _, _| {
14848 let item1 = item1.clone();
14849 let item2 = item2.clone();
14850 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
14851 }
14852 })
14853 .next()
14854 .await;
14855
14856 cx.condition(|editor, _| editor.context_menu_visible())
14857 .await;
14858 cx.update_editor(|editor, _, _| {
14859 let context_menu = editor.context_menu.borrow_mut();
14860 let context_menu = context_menu
14861 .as_ref()
14862 .expect("Should have the context menu deployed");
14863 match context_menu {
14864 CodeContextMenu::Completions(completions_menu) => {
14865 let completions = completions_menu.completions.borrow_mut();
14866 assert_eq!(
14867 completions
14868 .iter()
14869 .map(|completion| &completion.label.text)
14870 .collect::<Vec<_>>(),
14871 vec!["method id()", "other"]
14872 )
14873 }
14874 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14875 }
14876 });
14877
14878 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
14879 let item1 = item1.clone();
14880 move |_, item_to_resolve, _| {
14881 let item1 = item1.clone();
14882 async move {
14883 if item1 == item_to_resolve {
14884 Ok(lsp::CompletionItem {
14885 label: "method id()".to_string(),
14886 filter_text: Some("id".to_string()),
14887 detail: Some("Now resolved!".to_string()),
14888 documentation: Some(lsp::Documentation::String("Docs".to_string())),
14889 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14890 range: lsp::Range::new(
14891 lsp::Position::new(0, 22),
14892 lsp::Position::new(0, 22),
14893 ),
14894 new_text: ".id".to_string(),
14895 })),
14896 ..lsp::CompletionItem::default()
14897 })
14898 } else {
14899 Ok(item_to_resolve)
14900 }
14901 }
14902 }
14903 })
14904 .next()
14905 .await
14906 .unwrap();
14907 cx.run_until_parked();
14908
14909 cx.update_editor(|editor, window, cx| {
14910 editor.context_menu_next(&Default::default(), window, cx);
14911 });
14912
14913 cx.update_editor(|editor, _, _| {
14914 let context_menu = editor.context_menu.borrow_mut();
14915 let context_menu = context_menu
14916 .as_ref()
14917 .expect("Should have the context menu deployed");
14918 match context_menu {
14919 CodeContextMenu::Completions(completions_menu) => {
14920 let completions = completions_menu.completions.borrow_mut();
14921 assert_eq!(
14922 completions
14923 .iter()
14924 .map(|completion| &completion.label.text)
14925 .collect::<Vec<_>>(),
14926 vec!["method id() Now resolved!", "other"],
14927 "Should update first completion label, but not second as the filter text did not match."
14928 );
14929 }
14930 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14931 }
14932 });
14933}
14934
14935#[gpui::test]
14936async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
14937 init_test(cx, |_| {});
14938 let mut cx = EditorLspTestContext::new_rust(
14939 lsp::ServerCapabilities {
14940 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
14941 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
14942 completion_provider: Some(lsp::CompletionOptions {
14943 resolve_provider: Some(true),
14944 ..Default::default()
14945 }),
14946 ..Default::default()
14947 },
14948 cx,
14949 )
14950 .await;
14951 cx.set_state(indoc! {"
14952 struct TestStruct {
14953 field: i32
14954 }
14955
14956 fn mainˇ() {
14957 let unused_var = 42;
14958 let test_struct = TestStruct { field: 42 };
14959 }
14960 "});
14961 let symbol_range = cx.lsp_range(indoc! {"
14962 struct TestStruct {
14963 field: i32
14964 }
14965
14966 «fn main»() {
14967 let unused_var = 42;
14968 let test_struct = TestStruct { field: 42 };
14969 }
14970 "});
14971 let mut hover_requests =
14972 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
14973 Ok(Some(lsp::Hover {
14974 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
14975 kind: lsp::MarkupKind::Markdown,
14976 value: "Function documentation".to_string(),
14977 }),
14978 range: Some(symbol_range),
14979 }))
14980 });
14981
14982 // Case 1: Test that code action menu hide hover popover
14983 cx.dispatch_action(Hover);
14984 hover_requests.next().await;
14985 cx.condition(|editor, _| editor.hover_state.visible()).await;
14986 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
14987 move |_, _, _| async move {
14988 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
14989 lsp::CodeAction {
14990 title: "Remove unused variable".to_string(),
14991 kind: Some(CodeActionKind::QUICKFIX),
14992 edit: Some(lsp::WorkspaceEdit {
14993 changes: Some(
14994 [(
14995 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
14996 vec![lsp::TextEdit {
14997 range: lsp::Range::new(
14998 lsp::Position::new(5, 4),
14999 lsp::Position::new(5, 27),
15000 ),
15001 new_text: "".to_string(),
15002 }],
15003 )]
15004 .into_iter()
15005 .collect(),
15006 ),
15007 ..Default::default()
15008 }),
15009 ..Default::default()
15010 },
15011 )]))
15012 },
15013 );
15014 cx.update_editor(|editor, window, cx| {
15015 editor.toggle_code_actions(
15016 &ToggleCodeActions {
15017 deployed_from: None,
15018 quick_launch: false,
15019 },
15020 window,
15021 cx,
15022 );
15023 });
15024 code_action_requests.next().await;
15025 cx.run_until_parked();
15026 cx.condition(|editor, _| editor.context_menu_visible())
15027 .await;
15028 cx.update_editor(|editor, _, _| {
15029 assert!(
15030 !editor.hover_state.visible(),
15031 "Hover popover should be hidden when code action menu is shown"
15032 );
15033 // Hide code actions
15034 editor.context_menu.take();
15035 });
15036
15037 // Case 2: Test that code completions hide hover popover
15038 cx.dispatch_action(Hover);
15039 hover_requests.next().await;
15040 cx.condition(|editor, _| editor.hover_state.visible()).await;
15041 let counter = Arc::new(AtomicUsize::new(0));
15042 let mut completion_requests =
15043 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15044 let counter = counter.clone();
15045 async move {
15046 counter.fetch_add(1, atomic::Ordering::Release);
15047 Ok(Some(lsp::CompletionResponse::Array(vec![
15048 lsp::CompletionItem {
15049 label: "main".into(),
15050 kind: Some(lsp::CompletionItemKind::FUNCTION),
15051 detail: Some("() -> ()".to_string()),
15052 ..Default::default()
15053 },
15054 lsp::CompletionItem {
15055 label: "TestStruct".into(),
15056 kind: Some(lsp::CompletionItemKind::STRUCT),
15057 detail: Some("struct TestStruct".to_string()),
15058 ..Default::default()
15059 },
15060 ])))
15061 }
15062 });
15063 cx.update_editor(|editor, window, cx| {
15064 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15065 });
15066 completion_requests.next().await;
15067 cx.condition(|editor, _| editor.context_menu_visible())
15068 .await;
15069 cx.update_editor(|editor, _, _| {
15070 assert!(
15071 !editor.hover_state.visible(),
15072 "Hover popover should be hidden when completion menu is shown"
15073 );
15074 });
15075}
15076
15077#[gpui::test]
15078async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
15079 init_test(cx, |_| {});
15080
15081 let mut cx = EditorLspTestContext::new_rust(
15082 lsp::ServerCapabilities {
15083 completion_provider: Some(lsp::CompletionOptions {
15084 trigger_characters: Some(vec![".".to_string()]),
15085 resolve_provider: Some(true),
15086 ..Default::default()
15087 }),
15088 ..Default::default()
15089 },
15090 cx,
15091 )
15092 .await;
15093
15094 cx.set_state("fn main() { let a = 2ˇ; }");
15095 cx.simulate_keystroke(".");
15096
15097 let unresolved_item_1 = lsp::CompletionItem {
15098 label: "id".to_string(),
15099 filter_text: Some("id".to_string()),
15100 detail: None,
15101 documentation: None,
15102 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15103 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15104 new_text: ".id".to_string(),
15105 })),
15106 ..lsp::CompletionItem::default()
15107 };
15108 let resolved_item_1 = lsp::CompletionItem {
15109 additional_text_edits: Some(vec![lsp::TextEdit {
15110 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15111 new_text: "!!".to_string(),
15112 }]),
15113 ..unresolved_item_1.clone()
15114 };
15115 let unresolved_item_2 = lsp::CompletionItem {
15116 label: "other".to_string(),
15117 filter_text: Some("other".to_string()),
15118 detail: None,
15119 documentation: None,
15120 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15121 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15122 new_text: ".other".to_string(),
15123 })),
15124 ..lsp::CompletionItem::default()
15125 };
15126 let resolved_item_2 = lsp::CompletionItem {
15127 additional_text_edits: Some(vec![lsp::TextEdit {
15128 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15129 new_text: "??".to_string(),
15130 }]),
15131 ..unresolved_item_2.clone()
15132 };
15133
15134 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
15135 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
15136 cx.lsp
15137 .server
15138 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
15139 let unresolved_item_1 = unresolved_item_1.clone();
15140 let resolved_item_1 = resolved_item_1.clone();
15141 let unresolved_item_2 = unresolved_item_2.clone();
15142 let resolved_item_2 = resolved_item_2.clone();
15143 let resolve_requests_1 = resolve_requests_1.clone();
15144 let resolve_requests_2 = resolve_requests_2.clone();
15145 move |unresolved_request, _| {
15146 let unresolved_item_1 = unresolved_item_1.clone();
15147 let resolved_item_1 = resolved_item_1.clone();
15148 let unresolved_item_2 = unresolved_item_2.clone();
15149 let resolved_item_2 = resolved_item_2.clone();
15150 let resolve_requests_1 = resolve_requests_1.clone();
15151 let resolve_requests_2 = resolve_requests_2.clone();
15152 async move {
15153 if unresolved_request == unresolved_item_1 {
15154 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
15155 Ok(resolved_item_1.clone())
15156 } else if unresolved_request == unresolved_item_2 {
15157 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
15158 Ok(resolved_item_2.clone())
15159 } else {
15160 panic!("Unexpected completion item {unresolved_request:?}")
15161 }
15162 }
15163 }
15164 })
15165 .detach();
15166
15167 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15168 let unresolved_item_1 = unresolved_item_1.clone();
15169 let unresolved_item_2 = unresolved_item_2.clone();
15170 async move {
15171 Ok(Some(lsp::CompletionResponse::Array(vec![
15172 unresolved_item_1,
15173 unresolved_item_2,
15174 ])))
15175 }
15176 })
15177 .next()
15178 .await;
15179
15180 cx.condition(|editor, _| editor.context_menu_visible())
15181 .await;
15182 cx.update_editor(|editor, _, _| {
15183 let context_menu = editor.context_menu.borrow_mut();
15184 let context_menu = context_menu
15185 .as_ref()
15186 .expect("Should have the context menu deployed");
15187 match context_menu {
15188 CodeContextMenu::Completions(completions_menu) => {
15189 let completions = completions_menu.completions.borrow_mut();
15190 assert_eq!(
15191 completions
15192 .iter()
15193 .map(|completion| &completion.label.text)
15194 .collect::<Vec<_>>(),
15195 vec!["id", "other"]
15196 )
15197 }
15198 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15199 }
15200 });
15201 cx.run_until_parked();
15202
15203 cx.update_editor(|editor, window, cx| {
15204 editor.context_menu_next(&ContextMenuNext, window, cx);
15205 });
15206 cx.run_until_parked();
15207 cx.update_editor(|editor, window, cx| {
15208 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
15209 });
15210 cx.run_until_parked();
15211 cx.update_editor(|editor, window, cx| {
15212 editor.context_menu_next(&ContextMenuNext, window, cx);
15213 });
15214 cx.run_until_parked();
15215 cx.update_editor(|editor, window, cx| {
15216 editor
15217 .compose_completion(&ComposeCompletion::default(), window, cx)
15218 .expect("No task returned")
15219 })
15220 .await
15221 .expect("Completion failed");
15222 cx.run_until_parked();
15223
15224 cx.update_editor(|editor, _, cx| {
15225 assert_eq!(
15226 resolve_requests_1.load(atomic::Ordering::Acquire),
15227 1,
15228 "Should always resolve once despite multiple selections"
15229 );
15230 assert_eq!(
15231 resolve_requests_2.load(atomic::Ordering::Acquire),
15232 1,
15233 "Should always resolve once after multiple selections and applying the completion"
15234 );
15235 assert_eq!(
15236 editor.text(cx),
15237 "fn main() { let a = ??.other; }",
15238 "Should use resolved data when applying the completion"
15239 );
15240 });
15241}
15242
15243#[gpui::test]
15244async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
15245 init_test(cx, |_| {});
15246
15247 let item_0 = lsp::CompletionItem {
15248 label: "abs".into(),
15249 insert_text: Some("abs".into()),
15250 data: Some(json!({ "very": "special"})),
15251 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
15252 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
15253 lsp::InsertReplaceEdit {
15254 new_text: "abs".to_string(),
15255 insert: lsp::Range::default(),
15256 replace: lsp::Range::default(),
15257 },
15258 )),
15259 ..lsp::CompletionItem::default()
15260 };
15261 let items = iter::once(item_0.clone())
15262 .chain((11..51).map(|i| lsp::CompletionItem {
15263 label: format!("item_{}", i),
15264 insert_text: Some(format!("item_{}", i)),
15265 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15266 ..lsp::CompletionItem::default()
15267 }))
15268 .collect::<Vec<_>>();
15269
15270 let default_commit_characters = vec!["?".to_string()];
15271 let default_data = json!({ "default": "data"});
15272 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
15273 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
15274 let default_edit_range = lsp::Range {
15275 start: lsp::Position {
15276 line: 0,
15277 character: 5,
15278 },
15279 end: lsp::Position {
15280 line: 0,
15281 character: 5,
15282 },
15283 };
15284
15285 let mut cx = EditorLspTestContext::new_rust(
15286 lsp::ServerCapabilities {
15287 completion_provider: Some(lsp::CompletionOptions {
15288 trigger_characters: Some(vec![".".to_string()]),
15289 resolve_provider: Some(true),
15290 ..Default::default()
15291 }),
15292 ..Default::default()
15293 },
15294 cx,
15295 )
15296 .await;
15297
15298 cx.set_state("fn main() { let a = 2ˇ; }");
15299 cx.simulate_keystroke(".");
15300
15301 let completion_data = default_data.clone();
15302 let completion_characters = default_commit_characters.clone();
15303 let completion_items = items.clone();
15304 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15305 let default_data = completion_data.clone();
15306 let default_commit_characters = completion_characters.clone();
15307 let items = completion_items.clone();
15308 async move {
15309 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15310 items,
15311 item_defaults: Some(lsp::CompletionListItemDefaults {
15312 data: Some(default_data.clone()),
15313 commit_characters: Some(default_commit_characters.clone()),
15314 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
15315 default_edit_range,
15316 )),
15317 insert_text_format: Some(default_insert_text_format),
15318 insert_text_mode: Some(default_insert_text_mode),
15319 }),
15320 ..lsp::CompletionList::default()
15321 })))
15322 }
15323 })
15324 .next()
15325 .await;
15326
15327 let resolved_items = Arc::new(Mutex::new(Vec::new()));
15328 cx.lsp
15329 .server
15330 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
15331 let closure_resolved_items = resolved_items.clone();
15332 move |item_to_resolve, _| {
15333 let closure_resolved_items = closure_resolved_items.clone();
15334 async move {
15335 closure_resolved_items.lock().push(item_to_resolve.clone());
15336 Ok(item_to_resolve)
15337 }
15338 }
15339 })
15340 .detach();
15341
15342 cx.condition(|editor, _| editor.context_menu_visible())
15343 .await;
15344 cx.run_until_parked();
15345 cx.update_editor(|editor, _, _| {
15346 let menu = editor.context_menu.borrow_mut();
15347 match menu.as_ref().expect("should have the completions menu") {
15348 CodeContextMenu::Completions(completions_menu) => {
15349 assert_eq!(
15350 completions_menu
15351 .entries
15352 .borrow()
15353 .iter()
15354 .map(|mat| mat.string.clone())
15355 .collect::<Vec<String>>(),
15356 items
15357 .iter()
15358 .map(|completion| completion.label.clone())
15359 .collect::<Vec<String>>()
15360 );
15361 }
15362 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
15363 }
15364 });
15365 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
15366 // with 4 from the end.
15367 assert_eq!(
15368 *resolved_items.lock(),
15369 [&items[0..16], &items[items.len() - 4..items.len()]]
15370 .concat()
15371 .iter()
15372 .cloned()
15373 .map(|mut item| {
15374 if item.data.is_none() {
15375 item.data = Some(default_data.clone());
15376 }
15377 item
15378 })
15379 .collect::<Vec<lsp::CompletionItem>>(),
15380 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
15381 );
15382 resolved_items.lock().clear();
15383
15384 cx.update_editor(|editor, window, cx| {
15385 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
15386 });
15387 cx.run_until_parked();
15388 // Completions that have already been resolved are skipped.
15389 assert_eq!(
15390 *resolved_items.lock(),
15391 items[items.len() - 16..items.len() - 4]
15392 .iter()
15393 .cloned()
15394 .map(|mut item| {
15395 if item.data.is_none() {
15396 item.data = Some(default_data.clone());
15397 }
15398 item
15399 })
15400 .collect::<Vec<lsp::CompletionItem>>()
15401 );
15402 resolved_items.lock().clear();
15403}
15404
15405#[gpui::test]
15406async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
15407 init_test(cx, |_| {});
15408
15409 let mut cx = EditorLspTestContext::new(
15410 Language::new(
15411 LanguageConfig {
15412 matcher: LanguageMatcher {
15413 path_suffixes: vec!["jsx".into()],
15414 ..Default::default()
15415 },
15416 overrides: [(
15417 "element".into(),
15418 LanguageConfigOverride {
15419 completion_query_characters: Override::Set(['-'].into_iter().collect()),
15420 ..Default::default()
15421 },
15422 )]
15423 .into_iter()
15424 .collect(),
15425 ..Default::default()
15426 },
15427 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15428 )
15429 .with_override_query("(jsx_self_closing_element) @element")
15430 .unwrap(),
15431 lsp::ServerCapabilities {
15432 completion_provider: Some(lsp::CompletionOptions {
15433 trigger_characters: Some(vec![":".to_string()]),
15434 ..Default::default()
15435 }),
15436 ..Default::default()
15437 },
15438 cx,
15439 )
15440 .await;
15441
15442 cx.lsp
15443 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15444 Ok(Some(lsp::CompletionResponse::Array(vec![
15445 lsp::CompletionItem {
15446 label: "bg-blue".into(),
15447 ..Default::default()
15448 },
15449 lsp::CompletionItem {
15450 label: "bg-red".into(),
15451 ..Default::default()
15452 },
15453 lsp::CompletionItem {
15454 label: "bg-yellow".into(),
15455 ..Default::default()
15456 },
15457 ])))
15458 });
15459
15460 cx.set_state(r#"<p class="bgˇ" />"#);
15461
15462 // Trigger completion when typing a dash, because the dash is an extra
15463 // word character in the 'element' scope, which contains the cursor.
15464 cx.simulate_keystroke("-");
15465 cx.executor().run_until_parked();
15466 cx.update_editor(|editor, _, _| {
15467 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15468 {
15469 assert_eq!(
15470 completion_menu_entries(&menu),
15471 &["bg-red", "bg-blue", "bg-yellow"]
15472 );
15473 } else {
15474 panic!("expected completion menu to be open");
15475 }
15476 });
15477
15478 cx.simulate_keystroke("l");
15479 cx.executor().run_until_parked();
15480 cx.update_editor(|editor, _, _| {
15481 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15482 {
15483 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
15484 } else {
15485 panic!("expected completion menu to be open");
15486 }
15487 });
15488
15489 // When filtering completions, consider the character after the '-' to
15490 // be the start of a subword.
15491 cx.set_state(r#"<p class="yelˇ" />"#);
15492 cx.simulate_keystroke("l");
15493 cx.executor().run_until_parked();
15494 cx.update_editor(|editor, _, _| {
15495 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15496 {
15497 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
15498 } else {
15499 panic!("expected completion menu to be open");
15500 }
15501 });
15502}
15503
15504fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
15505 let entries = menu.entries.borrow();
15506 entries.iter().map(|mat| mat.string.clone()).collect()
15507}
15508
15509#[gpui::test]
15510async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
15511 init_test(cx, |settings| {
15512 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
15513 FormatterList(vec![Formatter::Prettier].into()),
15514 ))
15515 });
15516
15517 let fs = FakeFs::new(cx.executor());
15518 fs.insert_file(path!("/file.ts"), Default::default()).await;
15519
15520 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
15521 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15522
15523 language_registry.add(Arc::new(Language::new(
15524 LanguageConfig {
15525 name: "TypeScript".into(),
15526 matcher: LanguageMatcher {
15527 path_suffixes: vec!["ts".to_string()],
15528 ..Default::default()
15529 },
15530 ..Default::default()
15531 },
15532 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
15533 )));
15534 update_test_language_settings(cx, |settings| {
15535 settings.defaults.prettier = Some(PrettierSettings {
15536 allowed: true,
15537 ..PrettierSettings::default()
15538 });
15539 });
15540
15541 let test_plugin = "test_plugin";
15542 let _ = language_registry.register_fake_lsp(
15543 "TypeScript",
15544 FakeLspAdapter {
15545 prettier_plugins: vec![test_plugin],
15546 ..Default::default()
15547 },
15548 );
15549
15550 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
15551 let buffer = project
15552 .update(cx, |project, cx| {
15553 project.open_local_buffer(path!("/file.ts"), cx)
15554 })
15555 .await
15556 .unwrap();
15557
15558 let buffer_text = "one\ntwo\nthree\n";
15559 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
15560 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
15561 editor.update_in(cx, |editor, window, cx| {
15562 editor.set_text(buffer_text, window, cx)
15563 });
15564
15565 editor
15566 .update_in(cx, |editor, window, cx| {
15567 editor.perform_format(
15568 project.clone(),
15569 FormatTrigger::Manual,
15570 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
15571 window,
15572 cx,
15573 )
15574 })
15575 .unwrap()
15576 .await;
15577 assert_eq!(
15578 editor.update(cx, |editor, cx| editor.text(cx)),
15579 buffer_text.to_string() + prettier_format_suffix,
15580 "Test prettier formatting was not applied to the original buffer text",
15581 );
15582
15583 update_test_language_settings(cx, |settings| {
15584 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
15585 });
15586 let format = editor.update_in(cx, |editor, window, cx| {
15587 editor.perform_format(
15588 project.clone(),
15589 FormatTrigger::Manual,
15590 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
15591 window,
15592 cx,
15593 )
15594 });
15595 format.await.unwrap();
15596 assert_eq!(
15597 editor.update(cx, |editor, cx| editor.text(cx)),
15598 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
15599 "Autoformatting (via test prettier) was not applied to the original buffer text",
15600 );
15601}
15602
15603#[gpui::test]
15604async fn test_addition_reverts(cx: &mut TestAppContext) {
15605 init_test(cx, |_| {});
15606 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15607 let base_text = indoc! {r#"
15608 struct Row;
15609 struct Row1;
15610 struct Row2;
15611
15612 struct Row4;
15613 struct Row5;
15614 struct Row6;
15615
15616 struct Row8;
15617 struct Row9;
15618 struct Row10;"#};
15619
15620 // When addition hunks are not adjacent to carets, no hunk revert is performed
15621 assert_hunk_revert(
15622 indoc! {r#"struct Row;
15623 struct Row1;
15624 struct Row1.1;
15625 struct Row1.2;
15626 struct Row2;ˇ
15627
15628 struct Row4;
15629 struct Row5;
15630 struct Row6;
15631
15632 struct Row8;
15633 ˇstruct Row9;
15634 struct Row9.1;
15635 struct Row9.2;
15636 struct Row9.3;
15637 struct Row10;"#},
15638 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
15639 indoc! {r#"struct Row;
15640 struct Row1;
15641 struct Row1.1;
15642 struct Row1.2;
15643 struct Row2;ˇ
15644
15645 struct Row4;
15646 struct Row5;
15647 struct Row6;
15648
15649 struct Row8;
15650 ˇstruct Row9;
15651 struct Row9.1;
15652 struct Row9.2;
15653 struct Row9.3;
15654 struct Row10;"#},
15655 base_text,
15656 &mut cx,
15657 );
15658 // Same for selections
15659 assert_hunk_revert(
15660 indoc! {r#"struct Row;
15661 struct Row1;
15662 struct Row2;
15663 struct Row2.1;
15664 struct Row2.2;
15665 «ˇ
15666 struct Row4;
15667 struct» Row5;
15668 «struct Row6;
15669 ˇ»
15670 struct Row9.1;
15671 struct Row9.2;
15672 struct Row9.3;
15673 struct Row8;
15674 struct Row9;
15675 struct Row10;"#},
15676 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
15677 indoc! {r#"struct Row;
15678 struct Row1;
15679 struct Row2;
15680 struct Row2.1;
15681 struct Row2.2;
15682 «ˇ
15683 struct Row4;
15684 struct» Row5;
15685 «struct Row6;
15686 ˇ»
15687 struct Row9.1;
15688 struct Row9.2;
15689 struct Row9.3;
15690 struct Row8;
15691 struct Row9;
15692 struct Row10;"#},
15693 base_text,
15694 &mut cx,
15695 );
15696
15697 // When carets and selections intersect the addition hunks, those are reverted.
15698 // Adjacent carets got merged.
15699 assert_hunk_revert(
15700 indoc! {r#"struct Row;
15701 ˇ// something on the top
15702 struct Row1;
15703 struct Row2;
15704 struct Roˇw3.1;
15705 struct Row2.2;
15706 struct Row2.3;ˇ
15707
15708 struct Row4;
15709 struct ˇRow5.1;
15710 struct Row5.2;
15711 struct «Rowˇ»5.3;
15712 struct Row5;
15713 struct Row6;
15714 ˇ
15715 struct Row9.1;
15716 struct «Rowˇ»9.2;
15717 struct «ˇRow»9.3;
15718 struct Row8;
15719 struct Row9;
15720 «ˇ// something on bottom»
15721 struct Row10;"#},
15722 vec![
15723 DiffHunkStatusKind::Added,
15724 DiffHunkStatusKind::Added,
15725 DiffHunkStatusKind::Added,
15726 DiffHunkStatusKind::Added,
15727 DiffHunkStatusKind::Added,
15728 ],
15729 indoc! {r#"struct Row;
15730 ˇstruct Row1;
15731 struct Row2;
15732 ˇ
15733 struct Row4;
15734 ˇstruct Row5;
15735 struct Row6;
15736 ˇ
15737 ˇstruct Row8;
15738 struct Row9;
15739 ˇstruct Row10;"#},
15740 base_text,
15741 &mut cx,
15742 );
15743}
15744
15745#[gpui::test]
15746async fn test_modification_reverts(cx: &mut TestAppContext) {
15747 init_test(cx, |_| {});
15748 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15749 let base_text = indoc! {r#"
15750 struct Row;
15751 struct Row1;
15752 struct Row2;
15753
15754 struct Row4;
15755 struct Row5;
15756 struct Row6;
15757
15758 struct Row8;
15759 struct Row9;
15760 struct Row10;"#};
15761
15762 // Modification hunks behave the same as the addition ones.
15763 assert_hunk_revert(
15764 indoc! {r#"struct Row;
15765 struct Row1;
15766 struct Row33;
15767 ˇ
15768 struct Row4;
15769 struct Row5;
15770 struct Row6;
15771 ˇ
15772 struct Row99;
15773 struct Row9;
15774 struct Row10;"#},
15775 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
15776 indoc! {r#"struct Row;
15777 struct Row1;
15778 struct Row33;
15779 ˇ
15780 struct Row4;
15781 struct Row5;
15782 struct Row6;
15783 ˇ
15784 struct Row99;
15785 struct Row9;
15786 struct Row10;"#},
15787 base_text,
15788 &mut cx,
15789 );
15790 assert_hunk_revert(
15791 indoc! {r#"struct Row;
15792 struct Row1;
15793 struct Row33;
15794 «ˇ
15795 struct Row4;
15796 struct» Row5;
15797 «struct Row6;
15798 ˇ»
15799 struct Row99;
15800 struct Row9;
15801 struct Row10;"#},
15802 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
15803 indoc! {r#"struct Row;
15804 struct Row1;
15805 struct Row33;
15806 «ˇ
15807 struct Row4;
15808 struct» Row5;
15809 «struct Row6;
15810 ˇ»
15811 struct Row99;
15812 struct Row9;
15813 struct Row10;"#},
15814 base_text,
15815 &mut cx,
15816 );
15817
15818 assert_hunk_revert(
15819 indoc! {r#"ˇstruct Row1.1;
15820 struct Row1;
15821 «ˇstr»uct Row22;
15822
15823 struct ˇRow44;
15824 struct Row5;
15825 struct «Rˇ»ow66;ˇ
15826
15827 «struˇ»ct Row88;
15828 struct Row9;
15829 struct Row1011;ˇ"#},
15830 vec![
15831 DiffHunkStatusKind::Modified,
15832 DiffHunkStatusKind::Modified,
15833 DiffHunkStatusKind::Modified,
15834 DiffHunkStatusKind::Modified,
15835 DiffHunkStatusKind::Modified,
15836 DiffHunkStatusKind::Modified,
15837 ],
15838 indoc! {r#"struct Row;
15839 ˇstruct Row1;
15840 struct Row2;
15841 ˇ
15842 struct Row4;
15843 ˇstruct Row5;
15844 struct Row6;
15845 ˇ
15846 struct Row8;
15847 ˇstruct Row9;
15848 struct Row10;ˇ"#},
15849 base_text,
15850 &mut cx,
15851 );
15852}
15853
15854#[gpui::test]
15855async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
15856 init_test(cx, |_| {});
15857 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15858 let base_text = indoc! {r#"
15859 one
15860
15861 two
15862 three
15863 "#};
15864
15865 cx.set_head_text(base_text);
15866 cx.set_state("\nˇ\n");
15867 cx.executor().run_until_parked();
15868 cx.update_editor(|editor, _window, cx| {
15869 editor.expand_selected_diff_hunks(cx);
15870 });
15871 cx.executor().run_until_parked();
15872 cx.update_editor(|editor, window, cx| {
15873 editor.backspace(&Default::default(), window, cx);
15874 });
15875 cx.run_until_parked();
15876 cx.assert_state_with_diff(
15877 indoc! {r#"
15878
15879 - two
15880 - threeˇ
15881 +
15882 "#}
15883 .to_string(),
15884 );
15885}
15886
15887#[gpui::test]
15888async fn test_deletion_reverts(cx: &mut TestAppContext) {
15889 init_test(cx, |_| {});
15890 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15891 let base_text = indoc! {r#"struct Row;
15892struct Row1;
15893struct Row2;
15894
15895struct Row4;
15896struct Row5;
15897struct Row6;
15898
15899struct Row8;
15900struct Row9;
15901struct Row10;"#};
15902
15903 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
15904 assert_hunk_revert(
15905 indoc! {r#"struct Row;
15906 struct Row2;
15907
15908 ˇstruct Row4;
15909 struct Row5;
15910 struct Row6;
15911 ˇ
15912 struct Row8;
15913 struct Row10;"#},
15914 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15915 indoc! {r#"struct Row;
15916 struct Row2;
15917
15918 ˇstruct Row4;
15919 struct Row5;
15920 struct Row6;
15921 ˇ
15922 struct Row8;
15923 struct Row10;"#},
15924 base_text,
15925 &mut cx,
15926 );
15927 assert_hunk_revert(
15928 indoc! {r#"struct Row;
15929 struct Row2;
15930
15931 «ˇstruct Row4;
15932 struct» Row5;
15933 «struct Row6;
15934 ˇ»
15935 struct Row8;
15936 struct Row10;"#},
15937 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15938 indoc! {r#"struct Row;
15939 struct Row2;
15940
15941 «ˇstruct Row4;
15942 struct» Row5;
15943 «struct Row6;
15944 ˇ»
15945 struct Row8;
15946 struct Row10;"#},
15947 base_text,
15948 &mut cx,
15949 );
15950
15951 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
15952 assert_hunk_revert(
15953 indoc! {r#"struct Row;
15954 ˇstruct Row2;
15955
15956 struct Row4;
15957 struct Row5;
15958 struct Row6;
15959
15960 struct Row8;ˇ
15961 struct Row10;"#},
15962 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15963 indoc! {r#"struct Row;
15964 struct Row1;
15965 ˇstruct Row2;
15966
15967 struct Row4;
15968 struct Row5;
15969 struct Row6;
15970
15971 struct Row8;ˇ
15972 struct Row9;
15973 struct Row10;"#},
15974 base_text,
15975 &mut cx,
15976 );
15977 assert_hunk_revert(
15978 indoc! {r#"struct Row;
15979 struct Row2«ˇ;
15980 struct Row4;
15981 struct» Row5;
15982 «struct Row6;
15983
15984 struct Row8;ˇ»
15985 struct Row10;"#},
15986 vec![
15987 DiffHunkStatusKind::Deleted,
15988 DiffHunkStatusKind::Deleted,
15989 DiffHunkStatusKind::Deleted,
15990 ],
15991 indoc! {r#"struct Row;
15992 struct Row1;
15993 struct Row2«ˇ;
15994
15995 struct Row4;
15996 struct» Row5;
15997 «struct Row6;
15998
15999 struct Row8;ˇ»
16000 struct Row9;
16001 struct Row10;"#},
16002 base_text,
16003 &mut cx,
16004 );
16005}
16006
16007#[gpui::test]
16008async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
16009 init_test(cx, |_| {});
16010
16011 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
16012 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
16013 let base_text_3 =
16014 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
16015
16016 let text_1 = edit_first_char_of_every_line(base_text_1);
16017 let text_2 = edit_first_char_of_every_line(base_text_2);
16018 let text_3 = edit_first_char_of_every_line(base_text_3);
16019
16020 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
16021 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
16022 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
16023
16024 let multibuffer = cx.new(|cx| {
16025 let mut multibuffer = MultiBuffer::new(ReadWrite);
16026 multibuffer.push_excerpts(
16027 buffer_1.clone(),
16028 [
16029 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16030 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16031 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16032 ],
16033 cx,
16034 );
16035 multibuffer.push_excerpts(
16036 buffer_2.clone(),
16037 [
16038 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16039 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16040 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16041 ],
16042 cx,
16043 );
16044 multibuffer.push_excerpts(
16045 buffer_3.clone(),
16046 [
16047 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16048 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16049 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16050 ],
16051 cx,
16052 );
16053 multibuffer
16054 });
16055
16056 let fs = FakeFs::new(cx.executor());
16057 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
16058 let (editor, cx) = cx
16059 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
16060 editor.update_in(cx, |editor, _window, cx| {
16061 for (buffer, diff_base) in [
16062 (buffer_1.clone(), base_text_1),
16063 (buffer_2.clone(), base_text_2),
16064 (buffer_3.clone(), base_text_3),
16065 ] {
16066 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
16067 editor
16068 .buffer
16069 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
16070 }
16071 });
16072 cx.executor().run_until_parked();
16073
16074 editor.update_in(cx, |editor, window, cx| {
16075 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}");
16076 editor.select_all(&SelectAll, window, cx);
16077 editor.git_restore(&Default::default(), window, cx);
16078 });
16079 cx.executor().run_until_parked();
16080
16081 // When all ranges are selected, all buffer hunks are reverted.
16082 editor.update(cx, |editor, cx| {
16083 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");
16084 });
16085 buffer_1.update(cx, |buffer, _| {
16086 assert_eq!(buffer.text(), base_text_1);
16087 });
16088 buffer_2.update(cx, |buffer, _| {
16089 assert_eq!(buffer.text(), base_text_2);
16090 });
16091 buffer_3.update(cx, |buffer, _| {
16092 assert_eq!(buffer.text(), base_text_3);
16093 });
16094
16095 editor.update_in(cx, |editor, window, cx| {
16096 editor.undo(&Default::default(), window, cx);
16097 });
16098
16099 editor.update_in(cx, |editor, window, cx| {
16100 editor.change_selections(None, window, cx, |s| {
16101 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
16102 });
16103 editor.git_restore(&Default::default(), window, cx);
16104 });
16105
16106 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
16107 // but not affect buffer_2 and its related excerpts.
16108 editor.update(cx, |editor, cx| {
16109 assert_eq!(
16110 editor.text(cx),
16111 "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}"
16112 );
16113 });
16114 buffer_1.update(cx, |buffer, _| {
16115 assert_eq!(buffer.text(), base_text_1);
16116 });
16117 buffer_2.update(cx, |buffer, _| {
16118 assert_eq!(
16119 buffer.text(),
16120 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
16121 );
16122 });
16123 buffer_3.update(cx, |buffer, _| {
16124 assert_eq!(
16125 buffer.text(),
16126 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
16127 );
16128 });
16129
16130 fn edit_first_char_of_every_line(text: &str) -> String {
16131 text.split('\n')
16132 .map(|line| format!("X{}", &line[1..]))
16133 .collect::<Vec<_>>()
16134 .join("\n")
16135 }
16136}
16137
16138#[gpui::test]
16139async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
16140 init_test(cx, |_| {});
16141
16142 let cols = 4;
16143 let rows = 10;
16144 let sample_text_1 = sample_text(rows, cols, 'a');
16145 assert_eq!(
16146 sample_text_1,
16147 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
16148 );
16149 let sample_text_2 = sample_text(rows, cols, 'l');
16150 assert_eq!(
16151 sample_text_2,
16152 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
16153 );
16154 let sample_text_3 = sample_text(rows, cols, 'v');
16155 assert_eq!(
16156 sample_text_3,
16157 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
16158 );
16159
16160 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
16161 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
16162 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
16163
16164 let multi_buffer = cx.new(|cx| {
16165 let mut multibuffer = MultiBuffer::new(ReadWrite);
16166 multibuffer.push_excerpts(
16167 buffer_1.clone(),
16168 [
16169 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16170 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16171 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16172 ],
16173 cx,
16174 );
16175 multibuffer.push_excerpts(
16176 buffer_2.clone(),
16177 [
16178 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16179 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16180 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16181 ],
16182 cx,
16183 );
16184 multibuffer.push_excerpts(
16185 buffer_3.clone(),
16186 [
16187 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16188 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16189 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16190 ],
16191 cx,
16192 );
16193 multibuffer
16194 });
16195
16196 let fs = FakeFs::new(cx.executor());
16197 fs.insert_tree(
16198 "/a",
16199 json!({
16200 "main.rs": sample_text_1,
16201 "other.rs": sample_text_2,
16202 "lib.rs": sample_text_3,
16203 }),
16204 )
16205 .await;
16206 let project = Project::test(fs, ["/a".as_ref()], cx).await;
16207 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16208 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16209 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16210 Editor::new(
16211 EditorMode::full(),
16212 multi_buffer,
16213 Some(project.clone()),
16214 window,
16215 cx,
16216 )
16217 });
16218 let multibuffer_item_id = workspace
16219 .update(cx, |workspace, window, cx| {
16220 assert!(
16221 workspace.active_item(cx).is_none(),
16222 "active item should be None before the first item is added"
16223 );
16224 workspace.add_item_to_active_pane(
16225 Box::new(multi_buffer_editor.clone()),
16226 None,
16227 true,
16228 window,
16229 cx,
16230 );
16231 let active_item = workspace
16232 .active_item(cx)
16233 .expect("should have an active item after adding the multi buffer");
16234 assert!(
16235 !active_item.is_singleton(cx),
16236 "A multi buffer was expected to active after adding"
16237 );
16238 active_item.item_id()
16239 })
16240 .unwrap();
16241 cx.executor().run_until_parked();
16242
16243 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16244 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
16245 s.select_ranges(Some(1..2))
16246 });
16247 editor.open_excerpts(&OpenExcerpts, window, cx);
16248 });
16249 cx.executor().run_until_parked();
16250 let first_item_id = workspace
16251 .update(cx, |workspace, window, cx| {
16252 let active_item = workspace
16253 .active_item(cx)
16254 .expect("should have an active item after navigating into the 1st buffer");
16255 let first_item_id = active_item.item_id();
16256 assert_ne!(
16257 first_item_id, multibuffer_item_id,
16258 "Should navigate into the 1st buffer and activate it"
16259 );
16260 assert!(
16261 active_item.is_singleton(cx),
16262 "New active item should be a singleton buffer"
16263 );
16264 assert_eq!(
16265 active_item
16266 .act_as::<Editor>(cx)
16267 .expect("should have navigated into an editor for the 1st buffer")
16268 .read(cx)
16269 .text(cx),
16270 sample_text_1
16271 );
16272
16273 workspace
16274 .go_back(workspace.active_pane().downgrade(), window, cx)
16275 .detach_and_log_err(cx);
16276
16277 first_item_id
16278 })
16279 .unwrap();
16280 cx.executor().run_until_parked();
16281 workspace
16282 .update(cx, |workspace, _, cx| {
16283 let active_item = workspace
16284 .active_item(cx)
16285 .expect("should have an active item after navigating back");
16286 assert_eq!(
16287 active_item.item_id(),
16288 multibuffer_item_id,
16289 "Should navigate back to the multi buffer"
16290 );
16291 assert!(!active_item.is_singleton(cx));
16292 })
16293 .unwrap();
16294
16295 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16296 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
16297 s.select_ranges(Some(39..40))
16298 });
16299 editor.open_excerpts(&OpenExcerpts, window, cx);
16300 });
16301 cx.executor().run_until_parked();
16302 let second_item_id = workspace
16303 .update(cx, |workspace, window, cx| {
16304 let active_item = workspace
16305 .active_item(cx)
16306 .expect("should have an active item after navigating into the 2nd buffer");
16307 let second_item_id = active_item.item_id();
16308 assert_ne!(
16309 second_item_id, multibuffer_item_id,
16310 "Should navigate away from the multibuffer"
16311 );
16312 assert_ne!(
16313 second_item_id, first_item_id,
16314 "Should navigate into the 2nd buffer and activate it"
16315 );
16316 assert!(
16317 active_item.is_singleton(cx),
16318 "New active item should be a singleton buffer"
16319 );
16320 assert_eq!(
16321 active_item
16322 .act_as::<Editor>(cx)
16323 .expect("should have navigated into an editor")
16324 .read(cx)
16325 .text(cx),
16326 sample_text_2
16327 );
16328
16329 workspace
16330 .go_back(workspace.active_pane().downgrade(), window, cx)
16331 .detach_and_log_err(cx);
16332
16333 second_item_id
16334 })
16335 .unwrap();
16336 cx.executor().run_until_parked();
16337 workspace
16338 .update(cx, |workspace, _, cx| {
16339 let active_item = workspace
16340 .active_item(cx)
16341 .expect("should have an active item after navigating back from the 2nd buffer");
16342 assert_eq!(
16343 active_item.item_id(),
16344 multibuffer_item_id,
16345 "Should navigate back from the 2nd buffer to the multi buffer"
16346 );
16347 assert!(!active_item.is_singleton(cx));
16348 })
16349 .unwrap();
16350
16351 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16352 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
16353 s.select_ranges(Some(70..70))
16354 });
16355 editor.open_excerpts(&OpenExcerpts, window, cx);
16356 });
16357 cx.executor().run_until_parked();
16358 workspace
16359 .update(cx, |workspace, window, cx| {
16360 let active_item = workspace
16361 .active_item(cx)
16362 .expect("should have an active item after navigating into the 3rd buffer");
16363 let third_item_id = active_item.item_id();
16364 assert_ne!(
16365 third_item_id, multibuffer_item_id,
16366 "Should navigate into the 3rd buffer and activate it"
16367 );
16368 assert_ne!(third_item_id, first_item_id);
16369 assert_ne!(third_item_id, second_item_id);
16370 assert!(
16371 active_item.is_singleton(cx),
16372 "New active item should be a singleton buffer"
16373 );
16374 assert_eq!(
16375 active_item
16376 .act_as::<Editor>(cx)
16377 .expect("should have navigated into an editor")
16378 .read(cx)
16379 .text(cx),
16380 sample_text_3
16381 );
16382
16383 workspace
16384 .go_back(workspace.active_pane().downgrade(), window, cx)
16385 .detach_and_log_err(cx);
16386 })
16387 .unwrap();
16388 cx.executor().run_until_parked();
16389 workspace
16390 .update(cx, |workspace, _, cx| {
16391 let active_item = workspace
16392 .active_item(cx)
16393 .expect("should have an active item after navigating back from the 3rd buffer");
16394 assert_eq!(
16395 active_item.item_id(),
16396 multibuffer_item_id,
16397 "Should navigate back from the 3rd buffer to the multi buffer"
16398 );
16399 assert!(!active_item.is_singleton(cx));
16400 })
16401 .unwrap();
16402}
16403
16404#[gpui::test]
16405async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16406 init_test(cx, |_| {});
16407
16408 let mut cx = EditorTestContext::new(cx).await;
16409
16410 let diff_base = r#"
16411 use some::mod;
16412
16413 const A: u32 = 42;
16414
16415 fn main() {
16416 println!("hello");
16417
16418 println!("world");
16419 }
16420 "#
16421 .unindent();
16422
16423 cx.set_state(
16424 &r#"
16425 use some::modified;
16426
16427 ˇ
16428 fn main() {
16429 println!("hello there");
16430
16431 println!("around the");
16432 println!("world");
16433 }
16434 "#
16435 .unindent(),
16436 );
16437
16438 cx.set_head_text(&diff_base);
16439 executor.run_until_parked();
16440
16441 cx.update_editor(|editor, window, cx| {
16442 editor.go_to_next_hunk(&GoToHunk, window, cx);
16443 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16444 });
16445 executor.run_until_parked();
16446 cx.assert_state_with_diff(
16447 r#"
16448 use some::modified;
16449
16450
16451 fn main() {
16452 - println!("hello");
16453 + ˇ println!("hello there");
16454
16455 println!("around the");
16456 println!("world");
16457 }
16458 "#
16459 .unindent(),
16460 );
16461
16462 cx.update_editor(|editor, window, cx| {
16463 for _ in 0..2 {
16464 editor.go_to_next_hunk(&GoToHunk, window, cx);
16465 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16466 }
16467 });
16468 executor.run_until_parked();
16469 cx.assert_state_with_diff(
16470 r#"
16471 - use some::mod;
16472 + ˇuse some::modified;
16473
16474
16475 fn main() {
16476 - println!("hello");
16477 + println!("hello there");
16478
16479 + println!("around the");
16480 println!("world");
16481 }
16482 "#
16483 .unindent(),
16484 );
16485
16486 cx.update_editor(|editor, window, cx| {
16487 editor.go_to_next_hunk(&GoToHunk, window, cx);
16488 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16489 });
16490 executor.run_until_parked();
16491 cx.assert_state_with_diff(
16492 r#"
16493 - use some::mod;
16494 + use some::modified;
16495
16496 - const A: u32 = 42;
16497 ˇ
16498 fn main() {
16499 - println!("hello");
16500 + println!("hello there");
16501
16502 + println!("around the");
16503 println!("world");
16504 }
16505 "#
16506 .unindent(),
16507 );
16508
16509 cx.update_editor(|editor, window, cx| {
16510 editor.cancel(&Cancel, window, cx);
16511 });
16512
16513 cx.assert_state_with_diff(
16514 r#"
16515 use some::modified;
16516
16517 ˇ
16518 fn main() {
16519 println!("hello there");
16520
16521 println!("around the");
16522 println!("world");
16523 }
16524 "#
16525 .unindent(),
16526 );
16527}
16528
16529#[gpui::test]
16530async fn test_diff_base_change_with_expanded_diff_hunks(
16531 executor: BackgroundExecutor,
16532 cx: &mut TestAppContext,
16533) {
16534 init_test(cx, |_| {});
16535
16536 let mut cx = EditorTestContext::new(cx).await;
16537
16538 let diff_base = r#"
16539 use some::mod1;
16540 use some::mod2;
16541
16542 const A: u32 = 42;
16543 const B: u32 = 42;
16544 const C: u32 = 42;
16545
16546 fn main() {
16547 println!("hello");
16548
16549 println!("world");
16550 }
16551 "#
16552 .unindent();
16553
16554 cx.set_state(
16555 &r#"
16556 use some::mod2;
16557
16558 const A: u32 = 42;
16559 const C: u32 = 42;
16560
16561 fn main(ˇ) {
16562 //println!("hello");
16563
16564 println!("world");
16565 //
16566 //
16567 }
16568 "#
16569 .unindent(),
16570 );
16571
16572 cx.set_head_text(&diff_base);
16573 executor.run_until_parked();
16574
16575 cx.update_editor(|editor, window, cx| {
16576 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16577 });
16578 executor.run_until_parked();
16579 cx.assert_state_with_diff(
16580 r#"
16581 - use some::mod1;
16582 use some::mod2;
16583
16584 const A: u32 = 42;
16585 - const B: u32 = 42;
16586 const C: u32 = 42;
16587
16588 fn main(ˇ) {
16589 - println!("hello");
16590 + //println!("hello");
16591
16592 println!("world");
16593 + //
16594 + //
16595 }
16596 "#
16597 .unindent(),
16598 );
16599
16600 cx.set_head_text("new diff base!");
16601 executor.run_until_parked();
16602 cx.assert_state_with_diff(
16603 r#"
16604 - new diff base!
16605 + use some::mod2;
16606 +
16607 + const A: u32 = 42;
16608 + const C: u32 = 42;
16609 +
16610 + fn main(ˇ) {
16611 + //println!("hello");
16612 +
16613 + println!("world");
16614 + //
16615 + //
16616 + }
16617 "#
16618 .unindent(),
16619 );
16620}
16621
16622#[gpui::test]
16623async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
16624 init_test(cx, |_| {});
16625
16626 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
16627 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
16628 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
16629 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
16630 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
16631 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
16632
16633 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
16634 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
16635 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
16636
16637 let multi_buffer = cx.new(|cx| {
16638 let mut multibuffer = MultiBuffer::new(ReadWrite);
16639 multibuffer.push_excerpts(
16640 buffer_1.clone(),
16641 [
16642 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16643 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16644 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16645 ],
16646 cx,
16647 );
16648 multibuffer.push_excerpts(
16649 buffer_2.clone(),
16650 [
16651 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16652 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16653 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16654 ],
16655 cx,
16656 );
16657 multibuffer.push_excerpts(
16658 buffer_3.clone(),
16659 [
16660 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16661 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16662 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16663 ],
16664 cx,
16665 );
16666 multibuffer
16667 });
16668
16669 let editor =
16670 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
16671 editor
16672 .update(cx, |editor, _window, cx| {
16673 for (buffer, diff_base) in [
16674 (buffer_1.clone(), file_1_old),
16675 (buffer_2.clone(), file_2_old),
16676 (buffer_3.clone(), file_3_old),
16677 ] {
16678 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
16679 editor
16680 .buffer
16681 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
16682 }
16683 })
16684 .unwrap();
16685
16686 let mut cx = EditorTestContext::for_editor(editor, cx).await;
16687 cx.run_until_parked();
16688
16689 cx.assert_editor_state(
16690 &"
16691 ˇaaa
16692 ccc
16693 ddd
16694
16695 ggg
16696 hhh
16697
16698
16699 lll
16700 mmm
16701 NNN
16702
16703 qqq
16704 rrr
16705
16706 uuu
16707 111
16708 222
16709 333
16710
16711 666
16712 777
16713
16714 000
16715 !!!"
16716 .unindent(),
16717 );
16718
16719 cx.update_editor(|editor, window, cx| {
16720 editor.select_all(&SelectAll, window, cx);
16721 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16722 });
16723 cx.executor().run_until_parked();
16724
16725 cx.assert_state_with_diff(
16726 "
16727 «aaa
16728 - bbb
16729 ccc
16730 ddd
16731
16732 ggg
16733 hhh
16734
16735
16736 lll
16737 mmm
16738 - nnn
16739 + NNN
16740
16741 qqq
16742 rrr
16743
16744 uuu
16745 111
16746 222
16747 333
16748
16749 + 666
16750 777
16751
16752 000
16753 !!!ˇ»"
16754 .unindent(),
16755 );
16756}
16757
16758#[gpui::test]
16759async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
16760 init_test(cx, |_| {});
16761
16762 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
16763 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
16764
16765 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
16766 let multi_buffer = cx.new(|cx| {
16767 let mut multibuffer = MultiBuffer::new(ReadWrite);
16768 multibuffer.push_excerpts(
16769 buffer.clone(),
16770 [
16771 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
16772 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
16773 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
16774 ],
16775 cx,
16776 );
16777 multibuffer
16778 });
16779
16780 let editor =
16781 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
16782 editor
16783 .update(cx, |editor, _window, cx| {
16784 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
16785 editor
16786 .buffer
16787 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
16788 })
16789 .unwrap();
16790
16791 let mut cx = EditorTestContext::for_editor(editor, cx).await;
16792 cx.run_until_parked();
16793
16794 cx.update_editor(|editor, window, cx| {
16795 editor.expand_all_diff_hunks(&Default::default(), window, cx)
16796 });
16797 cx.executor().run_until_parked();
16798
16799 // When the start of a hunk coincides with the start of its excerpt,
16800 // the hunk is expanded. When the start of a a hunk is earlier than
16801 // the start of its excerpt, the hunk is not expanded.
16802 cx.assert_state_with_diff(
16803 "
16804 ˇaaa
16805 - bbb
16806 + BBB
16807
16808 - ddd
16809 - eee
16810 + DDD
16811 + EEE
16812 fff
16813
16814 iii
16815 "
16816 .unindent(),
16817 );
16818}
16819
16820#[gpui::test]
16821async fn test_edits_around_expanded_insertion_hunks(
16822 executor: BackgroundExecutor,
16823 cx: &mut TestAppContext,
16824) {
16825 init_test(cx, |_| {});
16826
16827 let mut cx = EditorTestContext::new(cx).await;
16828
16829 let diff_base = r#"
16830 use some::mod1;
16831 use some::mod2;
16832
16833 const A: u32 = 42;
16834
16835 fn main() {
16836 println!("hello");
16837
16838 println!("world");
16839 }
16840 "#
16841 .unindent();
16842 executor.run_until_parked();
16843 cx.set_state(
16844 &r#"
16845 use some::mod1;
16846 use some::mod2;
16847
16848 const A: u32 = 42;
16849 const B: u32 = 42;
16850 const C: u32 = 42;
16851 ˇ
16852
16853 fn main() {
16854 println!("hello");
16855
16856 println!("world");
16857 }
16858 "#
16859 .unindent(),
16860 );
16861
16862 cx.set_head_text(&diff_base);
16863 executor.run_until_parked();
16864
16865 cx.update_editor(|editor, window, cx| {
16866 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16867 });
16868 executor.run_until_parked();
16869
16870 cx.assert_state_with_diff(
16871 r#"
16872 use some::mod1;
16873 use some::mod2;
16874
16875 const A: u32 = 42;
16876 + const B: u32 = 42;
16877 + const C: u32 = 42;
16878 + ˇ
16879
16880 fn main() {
16881 println!("hello");
16882
16883 println!("world");
16884 }
16885 "#
16886 .unindent(),
16887 );
16888
16889 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
16890 executor.run_until_parked();
16891
16892 cx.assert_state_with_diff(
16893 r#"
16894 use some::mod1;
16895 use some::mod2;
16896
16897 const A: u32 = 42;
16898 + const B: u32 = 42;
16899 + const C: u32 = 42;
16900 + const D: u32 = 42;
16901 + ˇ
16902
16903 fn main() {
16904 println!("hello");
16905
16906 println!("world");
16907 }
16908 "#
16909 .unindent(),
16910 );
16911
16912 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
16913 executor.run_until_parked();
16914
16915 cx.assert_state_with_diff(
16916 r#"
16917 use some::mod1;
16918 use some::mod2;
16919
16920 const A: u32 = 42;
16921 + const B: u32 = 42;
16922 + const C: u32 = 42;
16923 + const D: u32 = 42;
16924 + const E: u32 = 42;
16925 + ˇ
16926
16927 fn main() {
16928 println!("hello");
16929
16930 println!("world");
16931 }
16932 "#
16933 .unindent(),
16934 );
16935
16936 cx.update_editor(|editor, window, cx| {
16937 editor.delete_line(&DeleteLine, window, cx);
16938 });
16939 executor.run_until_parked();
16940
16941 cx.assert_state_with_diff(
16942 r#"
16943 use some::mod1;
16944 use some::mod2;
16945
16946 const A: u32 = 42;
16947 + const B: u32 = 42;
16948 + const C: u32 = 42;
16949 + const D: u32 = 42;
16950 + const E: u32 = 42;
16951 ˇ
16952 fn main() {
16953 println!("hello");
16954
16955 println!("world");
16956 }
16957 "#
16958 .unindent(),
16959 );
16960
16961 cx.update_editor(|editor, window, cx| {
16962 editor.move_up(&MoveUp, window, cx);
16963 editor.delete_line(&DeleteLine, window, cx);
16964 editor.move_up(&MoveUp, window, cx);
16965 editor.delete_line(&DeleteLine, window, cx);
16966 editor.move_up(&MoveUp, window, cx);
16967 editor.delete_line(&DeleteLine, window, cx);
16968 });
16969 executor.run_until_parked();
16970 cx.assert_state_with_diff(
16971 r#"
16972 use some::mod1;
16973 use some::mod2;
16974
16975 const A: u32 = 42;
16976 + const B: u32 = 42;
16977 ˇ
16978 fn main() {
16979 println!("hello");
16980
16981 println!("world");
16982 }
16983 "#
16984 .unindent(),
16985 );
16986
16987 cx.update_editor(|editor, window, cx| {
16988 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
16989 editor.delete_line(&DeleteLine, window, cx);
16990 });
16991 executor.run_until_parked();
16992 cx.assert_state_with_diff(
16993 r#"
16994 ˇ
16995 fn main() {
16996 println!("hello");
16997
16998 println!("world");
16999 }
17000 "#
17001 .unindent(),
17002 );
17003}
17004
17005#[gpui::test]
17006async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
17007 init_test(cx, |_| {});
17008
17009 let mut cx = EditorTestContext::new(cx).await;
17010 cx.set_head_text(indoc! { "
17011 one
17012 two
17013 three
17014 four
17015 five
17016 "
17017 });
17018 cx.set_state(indoc! { "
17019 one
17020 ˇthree
17021 five
17022 "});
17023 cx.run_until_parked();
17024 cx.update_editor(|editor, window, cx| {
17025 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17026 });
17027 cx.assert_state_with_diff(
17028 indoc! { "
17029 one
17030 - two
17031 ˇthree
17032 - four
17033 five
17034 "}
17035 .to_string(),
17036 );
17037 cx.update_editor(|editor, window, cx| {
17038 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17039 });
17040
17041 cx.assert_state_with_diff(
17042 indoc! { "
17043 one
17044 ˇthree
17045 five
17046 "}
17047 .to_string(),
17048 );
17049
17050 cx.set_state(indoc! { "
17051 one
17052 ˇTWO
17053 three
17054 four
17055 five
17056 "});
17057 cx.run_until_parked();
17058 cx.update_editor(|editor, window, cx| {
17059 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17060 });
17061
17062 cx.assert_state_with_diff(
17063 indoc! { "
17064 one
17065 - two
17066 + ˇTWO
17067 three
17068 four
17069 five
17070 "}
17071 .to_string(),
17072 );
17073 cx.update_editor(|editor, window, cx| {
17074 editor.move_up(&Default::default(), window, cx);
17075 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17076 });
17077 cx.assert_state_with_diff(
17078 indoc! { "
17079 one
17080 ˇTWO
17081 three
17082 four
17083 five
17084 "}
17085 .to_string(),
17086 );
17087}
17088
17089#[gpui::test]
17090async fn test_edits_around_expanded_deletion_hunks(
17091 executor: BackgroundExecutor,
17092 cx: &mut TestAppContext,
17093) {
17094 init_test(cx, |_| {});
17095
17096 let mut cx = EditorTestContext::new(cx).await;
17097
17098 let diff_base = r#"
17099 use some::mod1;
17100 use some::mod2;
17101
17102 const A: u32 = 42;
17103 const B: u32 = 42;
17104 const C: u32 = 42;
17105
17106
17107 fn main() {
17108 println!("hello");
17109
17110 println!("world");
17111 }
17112 "#
17113 .unindent();
17114 executor.run_until_parked();
17115 cx.set_state(
17116 &r#"
17117 use some::mod1;
17118 use some::mod2;
17119
17120 ˇconst B: u32 = 42;
17121 const C: u32 = 42;
17122
17123
17124 fn main() {
17125 println!("hello");
17126
17127 println!("world");
17128 }
17129 "#
17130 .unindent(),
17131 );
17132
17133 cx.set_head_text(&diff_base);
17134 executor.run_until_parked();
17135
17136 cx.update_editor(|editor, window, cx| {
17137 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17138 });
17139 executor.run_until_parked();
17140
17141 cx.assert_state_with_diff(
17142 r#"
17143 use some::mod1;
17144 use some::mod2;
17145
17146 - const A: u32 = 42;
17147 ˇconst B: u32 = 42;
17148 const C: u32 = 42;
17149
17150
17151 fn main() {
17152 println!("hello");
17153
17154 println!("world");
17155 }
17156 "#
17157 .unindent(),
17158 );
17159
17160 cx.update_editor(|editor, window, cx| {
17161 editor.delete_line(&DeleteLine, window, cx);
17162 });
17163 executor.run_until_parked();
17164 cx.assert_state_with_diff(
17165 r#"
17166 use some::mod1;
17167 use some::mod2;
17168
17169 - const A: u32 = 42;
17170 - const B: u32 = 42;
17171 ˇconst C: u32 = 42;
17172
17173
17174 fn main() {
17175 println!("hello");
17176
17177 println!("world");
17178 }
17179 "#
17180 .unindent(),
17181 );
17182
17183 cx.update_editor(|editor, window, cx| {
17184 editor.delete_line(&DeleteLine, window, cx);
17185 });
17186 executor.run_until_parked();
17187 cx.assert_state_with_diff(
17188 r#"
17189 use some::mod1;
17190 use some::mod2;
17191
17192 - const A: u32 = 42;
17193 - const B: u32 = 42;
17194 - const C: u32 = 42;
17195 ˇ
17196
17197 fn main() {
17198 println!("hello");
17199
17200 println!("world");
17201 }
17202 "#
17203 .unindent(),
17204 );
17205
17206 cx.update_editor(|editor, window, cx| {
17207 editor.handle_input("replacement", window, cx);
17208 });
17209 executor.run_until_parked();
17210 cx.assert_state_with_diff(
17211 r#"
17212 use some::mod1;
17213 use some::mod2;
17214
17215 - const A: u32 = 42;
17216 - const B: u32 = 42;
17217 - const C: u32 = 42;
17218 -
17219 + replacementˇ
17220
17221 fn main() {
17222 println!("hello");
17223
17224 println!("world");
17225 }
17226 "#
17227 .unindent(),
17228 );
17229}
17230
17231#[gpui::test]
17232async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17233 init_test(cx, |_| {});
17234
17235 let mut cx = EditorTestContext::new(cx).await;
17236
17237 let base_text = r#"
17238 one
17239 two
17240 three
17241 four
17242 five
17243 "#
17244 .unindent();
17245 executor.run_until_parked();
17246 cx.set_state(
17247 &r#"
17248 one
17249 two
17250 fˇour
17251 five
17252 "#
17253 .unindent(),
17254 );
17255
17256 cx.set_head_text(&base_text);
17257 executor.run_until_parked();
17258
17259 cx.update_editor(|editor, window, cx| {
17260 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17261 });
17262 executor.run_until_parked();
17263
17264 cx.assert_state_with_diff(
17265 r#"
17266 one
17267 two
17268 - three
17269 fˇour
17270 five
17271 "#
17272 .unindent(),
17273 );
17274
17275 cx.update_editor(|editor, window, cx| {
17276 editor.backspace(&Backspace, window, cx);
17277 editor.backspace(&Backspace, window, cx);
17278 });
17279 executor.run_until_parked();
17280 cx.assert_state_with_diff(
17281 r#"
17282 one
17283 two
17284 - threeˇ
17285 - four
17286 + our
17287 five
17288 "#
17289 .unindent(),
17290 );
17291}
17292
17293#[gpui::test]
17294async fn test_edit_after_expanded_modification_hunk(
17295 executor: BackgroundExecutor,
17296 cx: &mut TestAppContext,
17297) {
17298 init_test(cx, |_| {});
17299
17300 let mut cx = EditorTestContext::new(cx).await;
17301
17302 let diff_base = r#"
17303 use some::mod1;
17304 use some::mod2;
17305
17306 const A: u32 = 42;
17307 const B: u32 = 42;
17308 const C: u32 = 42;
17309 const D: u32 = 42;
17310
17311
17312 fn main() {
17313 println!("hello");
17314
17315 println!("world");
17316 }"#
17317 .unindent();
17318
17319 cx.set_state(
17320 &r#"
17321 use some::mod1;
17322 use some::mod2;
17323
17324 const A: u32 = 42;
17325 const B: u32 = 42;
17326 const C: u32 = 43ˇ
17327 const D: u32 = 42;
17328
17329
17330 fn main() {
17331 println!("hello");
17332
17333 println!("world");
17334 }"#
17335 .unindent(),
17336 );
17337
17338 cx.set_head_text(&diff_base);
17339 executor.run_until_parked();
17340 cx.update_editor(|editor, window, cx| {
17341 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17342 });
17343 executor.run_until_parked();
17344
17345 cx.assert_state_with_diff(
17346 r#"
17347 use some::mod1;
17348 use some::mod2;
17349
17350 const A: u32 = 42;
17351 const B: u32 = 42;
17352 - const C: u32 = 42;
17353 + const C: u32 = 43ˇ
17354 const D: u32 = 42;
17355
17356
17357 fn main() {
17358 println!("hello");
17359
17360 println!("world");
17361 }"#
17362 .unindent(),
17363 );
17364
17365 cx.update_editor(|editor, window, cx| {
17366 editor.handle_input("\nnew_line\n", window, cx);
17367 });
17368 executor.run_until_parked();
17369
17370 cx.assert_state_with_diff(
17371 r#"
17372 use some::mod1;
17373 use some::mod2;
17374
17375 const A: u32 = 42;
17376 const B: u32 = 42;
17377 - const C: u32 = 42;
17378 + const C: u32 = 43
17379 + new_line
17380 + ˇ
17381 const D: u32 = 42;
17382
17383
17384 fn main() {
17385 println!("hello");
17386
17387 println!("world");
17388 }"#
17389 .unindent(),
17390 );
17391}
17392
17393#[gpui::test]
17394async fn test_stage_and_unstage_added_file_hunk(
17395 executor: BackgroundExecutor,
17396 cx: &mut TestAppContext,
17397) {
17398 init_test(cx, |_| {});
17399
17400 let mut cx = EditorTestContext::new(cx).await;
17401 cx.update_editor(|editor, _, cx| {
17402 editor.set_expand_all_diff_hunks(cx);
17403 });
17404
17405 let working_copy = r#"
17406 ˇfn main() {
17407 println!("hello, world!");
17408 }
17409 "#
17410 .unindent();
17411
17412 cx.set_state(&working_copy);
17413 executor.run_until_parked();
17414
17415 cx.assert_state_with_diff(
17416 r#"
17417 + ˇfn main() {
17418 + println!("hello, world!");
17419 + }
17420 "#
17421 .unindent(),
17422 );
17423 cx.assert_index_text(None);
17424
17425 cx.update_editor(|editor, window, cx| {
17426 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17427 });
17428 executor.run_until_parked();
17429 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
17430 cx.assert_state_with_diff(
17431 r#"
17432 + ˇfn main() {
17433 + println!("hello, world!");
17434 + }
17435 "#
17436 .unindent(),
17437 );
17438
17439 cx.update_editor(|editor, window, cx| {
17440 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17441 });
17442 executor.run_until_parked();
17443 cx.assert_index_text(None);
17444}
17445
17446async fn setup_indent_guides_editor(
17447 text: &str,
17448 cx: &mut TestAppContext,
17449) -> (BufferId, EditorTestContext) {
17450 init_test(cx, |_| {});
17451
17452 let mut cx = EditorTestContext::new(cx).await;
17453
17454 let buffer_id = cx.update_editor(|editor, window, cx| {
17455 editor.set_text(text, window, cx);
17456 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
17457
17458 buffer_ids[0]
17459 });
17460
17461 (buffer_id, cx)
17462}
17463
17464fn assert_indent_guides(
17465 range: Range<u32>,
17466 expected: Vec<IndentGuide>,
17467 active_indices: Option<Vec<usize>>,
17468 cx: &mut EditorTestContext,
17469) {
17470 let indent_guides = cx.update_editor(|editor, window, cx| {
17471 let snapshot = editor.snapshot(window, cx).display_snapshot;
17472 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
17473 editor,
17474 MultiBufferRow(range.start)..MultiBufferRow(range.end),
17475 true,
17476 &snapshot,
17477 cx,
17478 );
17479
17480 indent_guides.sort_by(|a, b| {
17481 a.depth.cmp(&b.depth).then(
17482 a.start_row
17483 .cmp(&b.start_row)
17484 .then(a.end_row.cmp(&b.end_row)),
17485 )
17486 });
17487 indent_guides
17488 });
17489
17490 if let Some(expected) = active_indices {
17491 let active_indices = cx.update_editor(|editor, window, cx| {
17492 let snapshot = editor.snapshot(window, cx).display_snapshot;
17493 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
17494 });
17495
17496 assert_eq!(
17497 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
17498 expected,
17499 "Active indent guide indices do not match"
17500 );
17501 }
17502
17503 assert_eq!(indent_guides, expected, "Indent guides do not match");
17504}
17505
17506fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
17507 IndentGuide {
17508 buffer_id,
17509 start_row: MultiBufferRow(start_row),
17510 end_row: MultiBufferRow(end_row),
17511 depth,
17512 tab_size: 4,
17513 settings: IndentGuideSettings {
17514 enabled: true,
17515 line_width: 1,
17516 active_line_width: 1,
17517 ..Default::default()
17518 },
17519 }
17520}
17521
17522#[gpui::test]
17523async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
17524 let (buffer_id, mut cx) = setup_indent_guides_editor(
17525 &"
17526 fn main() {
17527 let a = 1;
17528 }"
17529 .unindent(),
17530 cx,
17531 )
17532 .await;
17533
17534 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
17535}
17536
17537#[gpui::test]
17538async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
17539 let (buffer_id, mut cx) = setup_indent_guides_editor(
17540 &"
17541 fn main() {
17542 let a = 1;
17543 let b = 2;
17544 }"
17545 .unindent(),
17546 cx,
17547 )
17548 .await;
17549
17550 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
17551}
17552
17553#[gpui::test]
17554async fn test_indent_guide_nested(cx: &mut TestAppContext) {
17555 let (buffer_id, mut cx) = setup_indent_guides_editor(
17556 &"
17557 fn main() {
17558 let a = 1;
17559 if a == 3 {
17560 let b = 2;
17561 } else {
17562 let c = 3;
17563 }
17564 }"
17565 .unindent(),
17566 cx,
17567 )
17568 .await;
17569
17570 assert_indent_guides(
17571 0..8,
17572 vec![
17573 indent_guide(buffer_id, 1, 6, 0),
17574 indent_guide(buffer_id, 3, 3, 1),
17575 indent_guide(buffer_id, 5, 5, 1),
17576 ],
17577 None,
17578 &mut cx,
17579 );
17580}
17581
17582#[gpui::test]
17583async fn test_indent_guide_tab(cx: &mut TestAppContext) {
17584 let (buffer_id, mut cx) = setup_indent_guides_editor(
17585 &"
17586 fn main() {
17587 let a = 1;
17588 let b = 2;
17589 let c = 3;
17590 }"
17591 .unindent(),
17592 cx,
17593 )
17594 .await;
17595
17596 assert_indent_guides(
17597 0..5,
17598 vec![
17599 indent_guide(buffer_id, 1, 3, 0),
17600 indent_guide(buffer_id, 2, 2, 1),
17601 ],
17602 None,
17603 &mut cx,
17604 );
17605}
17606
17607#[gpui::test]
17608async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
17609 let (buffer_id, mut cx) = setup_indent_guides_editor(
17610 &"
17611 fn main() {
17612 let a = 1;
17613
17614 let c = 3;
17615 }"
17616 .unindent(),
17617 cx,
17618 )
17619 .await;
17620
17621 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
17622}
17623
17624#[gpui::test]
17625async fn test_indent_guide_complex(cx: &mut TestAppContext) {
17626 let (buffer_id, mut cx) = setup_indent_guides_editor(
17627 &"
17628 fn main() {
17629 let a = 1;
17630
17631 let c = 3;
17632
17633 if a == 3 {
17634 let b = 2;
17635 } else {
17636 let c = 3;
17637 }
17638 }"
17639 .unindent(),
17640 cx,
17641 )
17642 .await;
17643
17644 assert_indent_guides(
17645 0..11,
17646 vec![
17647 indent_guide(buffer_id, 1, 9, 0),
17648 indent_guide(buffer_id, 6, 6, 1),
17649 indent_guide(buffer_id, 8, 8, 1),
17650 ],
17651 None,
17652 &mut cx,
17653 );
17654}
17655
17656#[gpui::test]
17657async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
17658 let (buffer_id, mut cx) = setup_indent_guides_editor(
17659 &"
17660 fn main() {
17661 let a = 1;
17662
17663 let c = 3;
17664
17665 if a == 3 {
17666 let b = 2;
17667 } else {
17668 let c = 3;
17669 }
17670 }"
17671 .unindent(),
17672 cx,
17673 )
17674 .await;
17675
17676 assert_indent_guides(
17677 1..11,
17678 vec![
17679 indent_guide(buffer_id, 1, 9, 0),
17680 indent_guide(buffer_id, 6, 6, 1),
17681 indent_guide(buffer_id, 8, 8, 1),
17682 ],
17683 None,
17684 &mut cx,
17685 );
17686}
17687
17688#[gpui::test]
17689async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
17690 let (buffer_id, mut cx) = setup_indent_guides_editor(
17691 &"
17692 fn main() {
17693 let a = 1;
17694
17695 let c = 3;
17696
17697 if a == 3 {
17698 let b = 2;
17699 } else {
17700 let c = 3;
17701 }
17702 }"
17703 .unindent(),
17704 cx,
17705 )
17706 .await;
17707
17708 assert_indent_guides(
17709 1..10,
17710 vec![
17711 indent_guide(buffer_id, 1, 9, 0),
17712 indent_guide(buffer_id, 6, 6, 1),
17713 indent_guide(buffer_id, 8, 8, 1),
17714 ],
17715 None,
17716 &mut cx,
17717 );
17718}
17719
17720#[gpui::test]
17721async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
17722 let (buffer_id, mut cx) = setup_indent_guides_editor(
17723 &"
17724 fn main() {
17725 if a {
17726 b(
17727 c,
17728 d,
17729 )
17730 } else {
17731 e(
17732 f
17733 )
17734 }
17735 }"
17736 .unindent(),
17737 cx,
17738 )
17739 .await;
17740
17741 assert_indent_guides(
17742 0..11,
17743 vec![
17744 indent_guide(buffer_id, 1, 10, 0),
17745 indent_guide(buffer_id, 2, 5, 1),
17746 indent_guide(buffer_id, 7, 9, 1),
17747 indent_guide(buffer_id, 3, 4, 2),
17748 indent_guide(buffer_id, 8, 8, 2),
17749 ],
17750 None,
17751 &mut cx,
17752 );
17753
17754 cx.update_editor(|editor, window, cx| {
17755 editor.fold_at(MultiBufferRow(2), window, cx);
17756 assert_eq!(
17757 editor.display_text(cx),
17758 "
17759 fn main() {
17760 if a {
17761 b(⋯
17762 )
17763 } else {
17764 e(
17765 f
17766 )
17767 }
17768 }"
17769 .unindent()
17770 );
17771 });
17772
17773 assert_indent_guides(
17774 0..11,
17775 vec![
17776 indent_guide(buffer_id, 1, 10, 0),
17777 indent_guide(buffer_id, 2, 5, 1),
17778 indent_guide(buffer_id, 7, 9, 1),
17779 indent_guide(buffer_id, 8, 8, 2),
17780 ],
17781 None,
17782 &mut cx,
17783 );
17784}
17785
17786#[gpui::test]
17787async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
17788 let (buffer_id, mut cx) = setup_indent_guides_editor(
17789 &"
17790 block1
17791 block2
17792 block3
17793 block4
17794 block2
17795 block1
17796 block1"
17797 .unindent(),
17798 cx,
17799 )
17800 .await;
17801
17802 assert_indent_guides(
17803 1..10,
17804 vec![
17805 indent_guide(buffer_id, 1, 4, 0),
17806 indent_guide(buffer_id, 2, 3, 1),
17807 indent_guide(buffer_id, 3, 3, 2),
17808 ],
17809 None,
17810 &mut cx,
17811 );
17812}
17813
17814#[gpui::test]
17815async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
17816 let (buffer_id, mut cx) = setup_indent_guides_editor(
17817 &"
17818 block1
17819 block2
17820 block3
17821
17822 block1
17823 block1"
17824 .unindent(),
17825 cx,
17826 )
17827 .await;
17828
17829 assert_indent_guides(
17830 0..6,
17831 vec![
17832 indent_guide(buffer_id, 1, 2, 0),
17833 indent_guide(buffer_id, 2, 2, 1),
17834 ],
17835 None,
17836 &mut cx,
17837 );
17838}
17839
17840#[gpui::test]
17841async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
17842 let (buffer_id, mut cx) = setup_indent_guides_editor(
17843 &"
17844 function component() {
17845 \treturn (
17846 \t\t\t
17847 \t\t<div>
17848 \t\t\t<abc></abc>
17849 \t\t</div>
17850 \t)
17851 }"
17852 .unindent(),
17853 cx,
17854 )
17855 .await;
17856
17857 assert_indent_guides(
17858 0..8,
17859 vec![
17860 indent_guide(buffer_id, 1, 6, 0),
17861 indent_guide(buffer_id, 2, 5, 1),
17862 indent_guide(buffer_id, 4, 4, 2),
17863 ],
17864 None,
17865 &mut cx,
17866 );
17867}
17868
17869#[gpui::test]
17870async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
17871 let (buffer_id, mut cx) = setup_indent_guides_editor(
17872 &"
17873 function component() {
17874 \treturn (
17875 \t
17876 \t\t<div>
17877 \t\t\t<abc></abc>
17878 \t\t</div>
17879 \t)
17880 }"
17881 .unindent(),
17882 cx,
17883 )
17884 .await;
17885
17886 assert_indent_guides(
17887 0..8,
17888 vec![
17889 indent_guide(buffer_id, 1, 6, 0),
17890 indent_guide(buffer_id, 2, 5, 1),
17891 indent_guide(buffer_id, 4, 4, 2),
17892 ],
17893 None,
17894 &mut cx,
17895 );
17896}
17897
17898#[gpui::test]
17899async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
17900 let (buffer_id, mut cx) = setup_indent_guides_editor(
17901 &"
17902 block1
17903
17904
17905
17906 block2
17907 "
17908 .unindent(),
17909 cx,
17910 )
17911 .await;
17912
17913 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
17914}
17915
17916#[gpui::test]
17917async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
17918 let (buffer_id, mut cx) = setup_indent_guides_editor(
17919 &"
17920 def a:
17921 \tb = 3
17922 \tif True:
17923 \t\tc = 4
17924 \t\td = 5
17925 \tprint(b)
17926 "
17927 .unindent(),
17928 cx,
17929 )
17930 .await;
17931
17932 assert_indent_guides(
17933 0..6,
17934 vec![
17935 indent_guide(buffer_id, 1, 5, 0),
17936 indent_guide(buffer_id, 3, 4, 1),
17937 ],
17938 None,
17939 &mut cx,
17940 );
17941}
17942
17943#[gpui::test]
17944async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
17945 let (buffer_id, mut cx) = setup_indent_guides_editor(
17946 &"
17947 fn main() {
17948 let a = 1;
17949 }"
17950 .unindent(),
17951 cx,
17952 )
17953 .await;
17954
17955 cx.update_editor(|editor, window, cx| {
17956 editor.change_selections(None, window, cx, |s| {
17957 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17958 });
17959 });
17960
17961 assert_indent_guides(
17962 0..3,
17963 vec![indent_guide(buffer_id, 1, 1, 0)],
17964 Some(vec![0]),
17965 &mut cx,
17966 );
17967}
17968
17969#[gpui::test]
17970async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
17971 let (buffer_id, mut cx) = setup_indent_guides_editor(
17972 &"
17973 fn main() {
17974 if 1 == 2 {
17975 let a = 1;
17976 }
17977 }"
17978 .unindent(),
17979 cx,
17980 )
17981 .await;
17982
17983 cx.update_editor(|editor, window, cx| {
17984 editor.change_selections(None, window, cx, |s| {
17985 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17986 });
17987 });
17988
17989 assert_indent_guides(
17990 0..4,
17991 vec![
17992 indent_guide(buffer_id, 1, 3, 0),
17993 indent_guide(buffer_id, 2, 2, 1),
17994 ],
17995 Some(vec![1]),
17996 &mut cx,
17997 );
17998
17999 cx.update_editor(|editor, window, cx| {
18000 editor.change_selections(None, window, cx, |s| {
18001 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18002 });
18003 });
18004
18005 assert_indent_guides(
18006 0..4,
18007 vec![
18008 indent_guide(buffer_id, 1, 3, 0),
18009 indent_guide(buffer_id, 2, 2, 1),
18010 ],
18011 Some(vec![1]),
18012 &mut cx,
18013 );
18014
18015 cx.update_editor(|editor, window, cx| {
18016 editor.change_selections(None, window, cx, |s| {
18017 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
18018 });
18019 });
18020
18021 assert_indent_guides(
18022 0..4,
18023 vec![
18024 indent_guide(buffer_id, 1, 3, 0),
18025 indent_guide(buffer_id, 2, 2, 1),
18026 ],
18027 Some(vec![0]),
18028 &mut cx,
18029 );
18030}
18031
18032#[gpui::test]
18033async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
18034 let (buffer_id, mut cx) = setup_indent_guides_editor(
18035 &"
18036 fn main() {
18037 let a = 1;
18038
18039 let b = 2;
18040 }"
18041 .unindent(),
18042 cx,
18043 )
18044 .await;
18045
18046 cx.update_editor(|editor, window, cx| {
18047 editor.change_selections(None, window, cx, |s| {
18048 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18049 });
18050 });
18051
18052 assert_indent_guides(
18053 0..5,
18054 vec![indent_guide(buffer_id, 1, 3, 0)],
18055 Some(vec![0]),
18056 &mut cx,
18057 );
18058}
18059
18060#[gpui::test]
18061async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
18062 let (buffer_id, mut cx) = setup_indent_guides_editor(
18063 &"
18064 def m:
18065 a = 1
18066 pass"
18067 .unindent(),
18068 cx,
18069 )
18070 .await;
18071
18072 cx.update_editor(|editor, window, cx| {
18073 editor.change_selections(None, window, cx, |s| {
18074 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18075 });
18076 });
18077
18078 assert_indent_guides(
18079 0..3,
18080 vec![indent_guide(buffer_id, 1, 2, 0)],
18081 Some(vec![0]),
18082 &mut cx,
18083 );
18084}
18085
18086#[gpui::test]
18087async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
18088 init_test(cx, |_| {});
18089 let mut cx = EditorTestContext::new(cx).await;
18090 let text = indoc! {
18091 "
18092 impl A {
18093 fn b() {
18094 0;
18095 3;
18096 5;
18097 6;
18098 7;
18099 }
18100 }
18101 "
18102 };
18103 let base_text = indoc! {
18104 "
18105 impl A {
18106 fn b() {
18107 0;
18108 1;
18109 2;
18110 3;
18111 4;
18112 }
18113 fn c() {
18114 5;
18115 6;
18116 7;
18117 }
18118 }
18119 "
18120 };
18121
18122 cx.update_editor(|editor, window, cx| {
18123 editor.set_text(text, window, cx);
18124
18125 editor.buffer().update(cx, |multibuffer, cx| {
18126 let buffer = multibuffer.as_singleton().unwrap();
18127 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
18128
18129 multibuffer.set_all_diff_hunks_expanded(cx);
18130 multibuffer.add_diff(diff, cx);
18131
18132 buffer.read(cx).remote_id()
18133 })
18134 });
18135 cx.run_until_parked();
18136
18137 cx.assert_state_with_diff(
18138 indoc! { "
18139 impl A {
18140 fn b() {
18141 0;
18142 - 1;
18143 - 2;
18144 3;
18145 - 4;
18146 - }
18147 - fn c() {
18148 5;
18149 6;
18150 7;
18151 }
18152 }
18153 ˇ"
18154 }
18155 .to_string(),
18156 );
18157
18158 let mut actual_guides = cx.update_editor(|editor, window, cx| {
18159 editor
18160 .snapshot(window, cx)
18161 .buffer_snapshot
18162 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
18163 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
18164 .collect::<Vec<_>>()
18165 });
18166 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
18167 assert_eq!(
18168 actual_guides,
18169 vec![
18170 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
18171 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
18172 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
18173 ]
18174 );
18175}
18176
18177#[gpui::test]
18178async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18179 init_test(cx, |_| {});
18180 let mut cx = EditorTestContext::new(cx).await;
18181
18182 let diff_base = r#"
18183 a
18184 b
18185 c
18186 "#
18187 .unindent();
18188
18189 cx.set_state(
18190 &r#"
18191 ˇA
18192 b
18193 C
18194 "#
18195 .unindent(),
18196 );
18197 cx.set_head_text(&diff_base);
18198 cx.update_editor(|editor, window, cx| {
18199 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18200 });
18201 executor.run_until_parked();
18202
18203 let both_hunks_expanded = r#"
18204 - a
18205 + ˇA
18206 b
18207 - c
18208 + C
18209 "#
18210 .unindent();
18211
18212 cx.assert_state_with_diff(both_hunks_expanded.clone());
18213
18214 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18215 let snapshot = editor.snapshot(window, cx);
18216 let hunks = editor
18217 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18218 .collect::<Vec<_>>();
18219 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18220 let buffer_id = hunks[0].buffer_id;
18221 hunks
18222 .into_iter()
18223 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18224 .collect::<Vec<_>>()
18225 });
18226 assert_eq!(hunk_ranges.len(), 2);
18227
18228 cx.update_editor(|editor, _, cx| {
18229 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18230 });
18231 executor.run_until_parked();
18232
18233 let second_hunk_expanded = r#"
18234 ˇA
18235 b
18236 - c
18237 + C
18238 "#
18239 .unindent();
18240
18241 cx.assert_state_with_diff(second_hunk_expanded);
18242
18243 cx.update_editor(|editor, _, cx| {
18244 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18245 });
18246 executor.run_until_parked();
18247
18248 cx.assert_state_with_diff(both_hunks_expanded.clone());
18249
18250 cx.update_editor(|editor, _, cx| {
18251 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18252 });
18253 executor.run_until_parked();
18254
18255 let first_hunk_expanded = r#"
18256 - a
18257 + ˇA
18258 b
18259 C
18260 "#
18261 .unindent();
18262
18263 cx.assert_state_with_diff(first_hunk_expanded);
18264
18265 cx.update_editor(|editor, _, cx| {
18266 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18267 });
18268 executor.run_until_parked();
18269
18270 cx.assert_state_with_diff(both_hunks_expanded);
18271
18272 cx.set_state(
18273 &r#"
18274 ˇA
18275 b
18276 "#
18277 .unindent(),
18278 );
18279 cx.run_until_parked();
18280
18281 // TODO this cursor position seems bad
18282 cx.assert_state_with_diff(
18283 r#"
18284 - ˇa
18285 + A
18286 b
18287 "#
18288 .unindent(),
18289 );
18290
18291 cx.update_editor(|editor, window, cx| {
18292 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18293 });
18294
18295 cx.assert_state_with_diff(
18296 r#"
18297 - ˇa
18298 + A
18299 b
18300 - c
18301 "#
18302 .unindent(),
18303 );
18304
18305 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18306 let snapshot = editor.snapshot(window, cx);
18307 let hunks = editor
18308 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18309 .collect::<Vec<_>>();
18310 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18311 let buffer_id = hunks[0].buffer_id;
18312 hunks
18313 .into_iter()
18314 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18315 .collect::<Vec<_>>()
18316 });
18317 assert_eq!(hunk_ranges.len(), 2);
18318
18319 cx.update_editor(|editor, _, cx| {
18320 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18321 });
18322 executor.run_until_parked();
18323
18324 cx.assert_state_with_diff(
18325 r#"
18326 - ˇa
18327 + A
18328 b
18329 "#
18330 .unindent(),
18331 );
18332}
18333
18334#[gpui::test]
18335async fn test_toggle_deletion_hunk_at_start_of_file(
18336 executor: BackgroundExecutor,
18337 cx: &mut TestAppContext,
18338) {
18339 init_test(cx, |_| {});
18340 let mut cx = EditorTestContext::new(cx).await;
18341
18342 let diff_base = r#"
18343 a
18344 b
18345 c
18346 "#
18347 .unindent();
18348
18349 cx.set_state(
18350 &r#"
18351 ˇb
18352 c
18353 "#
18354 .unindent(),
18355 );
18356 cx.set_head_text(&diff_base);
18357 cx.update_editor(|editor, window, cx| {
18358 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18359 });
18360 executor.run_until_parked();
18361
18362 let hunk_expanded = r#"
18363 - a
18364 ˇb
18365 c
18366 "#
18367 .unindent();
18368
18369 cx.assert_state_with_diff(hunk_expanded.clone());
18370
18371 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18372 let snapshot = editor.snapshot(window, cx);
18373 let hunks = editor
18374 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18375 .collect::<Vec<_>>();
18376 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18377 let buffer_id = hunks[0].buffer_id;
18378 hunks
18379 .into_iter()
18380 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18381 .collect::<Vec<_>>()
18382 });
18383 assert_eq!(hunk_ranges.len(), 1);
18384
18385 cx.update_editor(|editor, _, cx| {
18386 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18387 });
18388 executor.run_until_parked();
18389
18390 let hunk_collapsed = r#"
18391 ˇb
18392 c
18393 "#
18394 .unindent();
18395
18396 cx.assert_state_with_diff(hunk_collapsed);
18397
18398 cx.update_editor(|editor, _, cx| {
18399 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18400 });
18401 executor.run_until_parked();
18402
18403 cx.assert_state_with_diff(hunk_expanded.clone());
18404}
18405
18406#[gpui::test]
18407async fn test_display_diff_hunks(cx: &mut TestAppContext) {
18408 init_test(cx, |_| {});
18409
18410 let fs = FakeFs::new(cx.executor());
18411 fs.insert_tree(
18412 path!("/test"),
18413 json!({
18414 ".git": {},
18415 "file-1": "ONE\n",
18416 "file-2": "TWO\n",
18417 "file-3": "THREE\n",
18418 }),
18419 )
18420 .await;
18421
18422 fs.set_head_for_repo(
18423 path!("/test/.git").as_ref(),
18424 &[
18425 ("file-1".into(), "one\n".into()),
18426 ("file-2".into(), "two\n".into()),
18427 ("file-3".into(), "three\n".into()),
18428 ],
18429 "deadbeef",
18430 );
18431
18432 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
18433 let mut buffers = vec![];
18434 for i in 1..=3 {
18435 let buffer = project
18436 .update(cx, |project, cx| {
18437 let path = format!(path!("/test/file-{}"), i);
18438 project.open_local_buffer(path, cx)
18439 })
18440 .await
18441 .unwrap();
18442 buffers.push(buffer);
18443 }
18444
18445 let multibuffer = cx.new(|cx| {
18446 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
18447 multibuffer.set_all_diff_hunks_expanded(cx);
18448 for buffer in &buffers {
18449 let snapshot = buffer.read(cx).snapshot();
18450 multibuffer.set_excerpts_for_path(
18451 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
18452 buffer.clone(),
18453 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
18454 DEFAULT_MULTIBUFFER_CONTEXT,
18455 cx,
18456 );
18457 }
18458 multibuffer
18459 });
18460
18461 let editor = cx.add_window(|window, cx| {
18462 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
18463 });
18464 cx.run_until_parked();
18465
18466 let snapshot = editor
18467 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18468 .unwrap();
18469 let hunks = snapshot
18470 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
18471 .map(|hunk| match hunk {
18472 DisplayDiffHunk::Unfolded {
18473 display_row_range, ..
18474 } => display_row_range,
18475 DisplayDiffHunk::Folded { .. } => unreachable!(),
18476 })
18477 .collect::<Vec<_>>();
18478 assert_eq!(
18479 hunks,
18480 [
18481 DisplayRow(2)..DisplayRow(4),
18482 DisplayRow(7)..DisplayRow(9),
18483 DisplayRow(12)..DisplayRow(14),
18484 ]
18485 );
18486}
18487
18488#[gpui::test]
18489async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
18490 init_test(cx, |_| {});
18491
18492 let mut cx = EditorTestContext::new(cx).await;
18493 cx.set_head_text(indoc! { "
18494 one
18495 two
18496 three
18497 four
18498 five
18499 "
18500 });
18501 cx.set_index_text(indoc! { "
18502 one
18503 two
18504 three
18505 four
18506 five
18507 "
18508 });
18509 cx.set_state(indoc! {"
18510 one
18511 TWO
18512 ˇTHREE
18513 FOUR
18514 five
18515 "});
18516 cx.run_until_parked();
18517 cx.update_editor(|editor, window, cx| {
18518 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18519 });
18520 cx.run_until_parked();
18521 cx.assert_index_text(Some(indoc! {"
18522 one
18523 TWO
18524 THREE
18525 FOUR
18526 five
18527 "}));
18528 cx.set_state(indoc! { "
18529 one
18530 TWO
18531 ˇTHREE-HUNDRED
18532 FOUR
18533 five
18534 "});
18535 cx.run_until_parked();
18536 cx.update_editor(|editor, window, cx| {
18537 let snapshot = editor.snapshot(window, cx);
18538 let hunks = editor
18539 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18540 .collect::<Vec<_>>();
18541 assert_eq!(hunks.len(), 1);
18542 assert_eq!(
18543 hunks[0].status(),
18544 DiffHunkStatus {
18545 kind: DiffHunkStatusKind::Modified,
18546 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
18547 }
18548 );
18549
18550 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18551 });
18552 cx.run_until_parked();
18553 cx.assert_index_text(Some(indoc! {"
18554 one
18555 TWO
18556 THREE-HUNDRED
18557 FOUR
18558 five
18559 "}));
18560}
18561
18562#[gpui::test]
18563fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
18564 init_test(cx, |_| {});
18565
18566 let editor = cx.add_window(|window, cx| {
18567 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
18568 build_editor(buffer, window, cx)
18569 });
18570
18571 let render_args = Arc::new(Mutex::new(None));
18572 let snapshot = editor
18573 .update(cx, |editor, window, cx| {
18574 let snapshot = editor.buffer().read(cx).snapshot(cx);
18575 let range =
18576 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
18577
18578 struct RenderArgs {
18579 row: MultiBufferRow,
18580 folded: bool,
18581 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
18582 }
18583
18584 let crease = Crease::inline(
18585 range,
18586 FoldPlaceholder::test(),
18587 {
18588 let toggle_callback = render_args.clone();
18589 move |row, folded, callback, _window, _cx| {
18590 *toggle_callback.lock() = Some(RenderArgs {
18591 row,
18592 folded,
18593 callback,
18594 });
18595 div()
18596 }
18597 },
18598 |_row, _folded, _window, _cx| div(),
18599 );
18600
18601 editor.insert_creases(Some(crease), cx);
18602 let snapshot = editor.snapshot(window, cx);
18603 let _div = snapshot.render_crease_toggle(
18604 MultiBufferRow(1),
18605 false,
18606 cx.entity().clone(),
18607 window,
18608 cx,
18609 );
18610 snapshot
18611 })
18612 .unwrap();
18613
18614 let render_args = render_args.lock().take().unwrap();
18615 assert_eq!(render_args.row, MultiBufferRow(1));
18616 assert!(!render_args.folded);
18617 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
18618
18619 cx.update_window(*editor, |_, window, cx| {
18620 (render_args.callback)(true, window, cx)
18621 })
18622 .unwrap();
18623 let snapshot = editor
18624 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18625 .unwrap();
18626 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
18627
18628 cx.update_window(*editor, |_, window, cx| {
18629 (render_args.callback)(false, window, cx)
18630 })
18631 .unwrap();
18632 let snapshot = editor
18633 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18634 .unwrap();
18635 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
18636}
18637
18638#[gpui::test]
18639async fn test_input_text(cx: &mut TestAppContext) {
18640 init_test(cx, |_| {});
18641 let mut cx = EditorTestContext::new(cx).await;
18642
18643 cx.set_state(
18644 &r#"ˇone
18645 two
18646
18647 three
18648 fourˇ
18649 five
18650
18651 siˇx"#
18652 .unindent(),
18653 );
18654
18655 cx.dispatch_action(HandleInput(String::new()));
18656 cx.assert_editor_state(
18657 &r#"ˇone
18658 two
18659
18660 three
18661 fourˇ
18662 five
18663
18664 siˇx"#
18665 .unindent(),
18666 );
18667
18668 cx.dispatch_action(HandleInput("AAAA".to_string()));
18669 cx.assert_editor_state(
18670 &r#"AAAAˇone
18671 two
18672
18673 three
18674 fourAAAAˇ
18675 five
18676
18677 siAAAAˇx"#
18678 .unindent(),
18679 );
18680}
18681
18682#[gpui::test]
18683async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
18684 init_test(cx, |_| {});
18685
18686 let mut cx = EditorTestContext::new(cx).await;
18687 cx.set_state(
18688 r#"let foo = 1;
18689let foo = 2;
18690let foo = 3;
18691let fooˇ = 4;
18692let foo = 5;
18693let foo = 6;
18694let foo = 7;
18695let foo = 8;
18696let foo = 9;
18697let foo = 10;
18698let foo = 11;
18699let foo = 12;
18700let foo = 13;
18701let foo = 14;
18702let foo = 15;"#,
18703 );
18704
18705 cx.update_editor(|e, window, cx| {
18706 assert_eq!(
18707 e.next_scroll_position,
18708 NextScrollCursorCenterTopBottom::Center,
18709 "Default next scroll direction is center",
18710 );
18711
18712 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18713 assert_eq!(
18714 e.next_scroll_position,
18715 NextScrollCursorCenterTopBottom::Top,
18716 "After center, next scroll direction should be top",
18717 );
18718
18719 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18720 assert_eq!(
18721 e.next_scroll_position,
18722 NextScrollCursorCenterTopBottom::Bottom,
18723 "After top, next scroll direction should be bottom",
18724 );
18725
18726 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18727 assert_eq!(
18728 e.next_scroll_position,
18729 NextScrollCursorCenterTopBottom::Center,
18730 "After bottom, scrolling should start over",
18731 );
18732
18733 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18734 assert_eq!(
18735 e.next_scroll_position,
18736 NextScrollCursorCenterTopBottom::Top,
18737 "Scrolling continues if retriggered fast enough"
18738 );
18739 });
18740
18741 cx.executor()
18742 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
18743 cx.executor().run_until_parked();
18744 cx.update_editor(|e, _, _| {
18745 assert_eq!(
18746 e.next_scroll_position,
18747 NextScrollCursorCenterTopBottom::Center,
18748 "If scrolling is not triggered fast enough, it should reset"
18749 );
18750 });
18751}
18752
18753#[gpui::test]
18754async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
18755 init_test(cx, |_| {});
18756 let mut cx = EditorLspTestContext::new_rust(
18757 lsp::ServerCapabilities {
18758 definition_provider: Some(lsp::OneOf::Left(true)),
18759 references_provider: Some(lsp::OneOf::Left(true)),
18760 ..lsp::ServerCapabilities::default()
18761 },
18762 cx,
18763 )
18764 .await;
18765
18766 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
18767 let go_to_definition = cx
18768 .lsp
18769 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
18770 move |params, _| async move {
18771 if empty_go_to_definition {
18772 Ok(None)
18773 } else {
18774 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
18775 uri: params.text_document_position_params.text_document.uri,
18776 range: lsp::Range::new(
18777 lsp::Position::new(4, 3),
18778 lsp::Position::new(4, 6),
18779 ),
18780 })))
18781 }
18782 },
18783 );
18784 let references = cx
18785 .lsp
18786 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
18787 Ok(Some(vec![lsp::Location {
18788 uri: params.text_document_position.text_document.uri,
18789 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
18790 }]))
18791 });
18792 (go_to_definition, references)
18793 };
18794
18795 cx.set_state(
18796 &r#"fn one() {
18797 let mut a = ˇtwo();
18798 }
18799
18800 fn two() {}"#
18801 .unindent(),
18802 );
18803 set_up_lsp_handlers(false, &mut cx);
18804 let navigated = cx
18805 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
18806 .await
18807 .expect("Failed to navigate to definition");
18808 assert_eq!(
18809 navigated,
18810 Navigated::Yes,
18811 "Should have navigated to definition from the GetDefinition response"
18812 );
18813 cx.assert_editor_state(
18814 &r#"fn one() {
18815 let mut a = two();
18816 }
18817
18818 fn «twoˇ»() {}"#
18819 .unindent(),
18820 );
18821
18822 let editors = cx.update_workspace(|workspace, _, cx| {
18823 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
18824 });
18825 cx.update_editor(|_, _, test_editor_cx| {
18826 assert_eq!(
18827 editors.len(),
18828 1,
18829 "Initially, only one, test, editor should be open in the workspace"
18830 );
18831 assert_eq!(
18832 test_editor_cx.entity(),
18833 editors.last().expect("Asserted len is 1").clone()
18834 );
18835 });
18836
18837 set_up_lsp_handlers(true, &mut cx);
18838 let navigated = cx
18839 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
18840 .await
18841 .expect("Failed to navigate to lookup references");
18842 assert_eq!(
18843 navigated,
18844 Navigated::Yes,
18845 "Should have navigated to references as a fallback after empty GoToDefinition response"
18846 );
18847 // We should not change the selections in the existing file,
18848 // if opening another milti buffer with the references
18849 cx.assert_editor_state(
18850 &r#"fn one() {
18851 let mut a = two();
18852 }
18853
18854 fn «twoˇ»() {}"#
18855 .unindent(),
18856 );
18857 let editors = cx.update_workspace(|workspace, _, cx| {
18858 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
18859 });
18860 cx.update_editor(|_, _, test_editor_cx| {
18861 assert_eq!(
18862 editors.len(),
18863 2,
18864 "After falling back to references search, we open a new editor with the results"
18865 );
18866 let references_fallback_text = editors
18867 .into_iter()
18868 .find(|new_editor| *new_editor != test_editor_cx.entity())
18869 .expect("Should have one non-test editor now")
18870 .read(test_editor_cx)
18871 .text(test_editor_cx);
18872 assert_eq!(
18873 references_fallback_text, "fn one() {\n let mut a = two();\n}",
18874 "Should use the range from the references response and not the GoToDefinition one"
18875 );
18876 });
18877}
18878
18879#[gpui::test]
18880async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
18881 init_test(cx, |_| {});
18882 cx.update(|cx| {
18883 let mut editor_settings = EditorSettings::get_global(cx).clone();
18884 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
18885 EditorSettings::override_global(editor_settings, cx);
18886 });
18887 let mut cx = EditorLspTestContext::new_rust(
18888 lsp::ServerCapabilities {
18889 definition_provider: Some(lsp::OneOf::Left(true)),
18890 references_provider: Some(lsp::OneOf::Left(true)),
18891 ..lsp::ServerCapabilities::default()
18892 },
18893 cx,
18894 )
18895 .await;
18896 let original_state = r#"fn one() {
18897 let mut a = ˇtwo();
18898 }
18899
18900 fn two() {}"#
18901 .unindent();
18902 cx.set_state(&original_state);
18903
18904 let mut go_to_definition = cx
18905 .lsp
18906 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
18907 move |_, _| async move { Ok(None) },
18908 );
18909 let _references = cx
18910 .lsp
18911 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
18912 panic!("Should not call for references with no go to definition fallback")
18913 });
18914
18915 let navigated = cx
18916 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
18917 .await
18918 .expect("Failed to navigate to lookup references");
18919 go_to_definition
18920 .next()
18921 .await
18922 .expect("Should have called the go_to_definition handler");
18923
18924 assert_eq!(
18925 navigated,
18926 Navigated::No,
18927 "Should have navigated to references as a fallback after empty GoToDefinition response"
18928 );
18929 cx.assert_editor_state(&original_state);
18930 let editors = cx.update_workspace(|workspace, _, cx| {
18931 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
18932 });
18933 cx.update_editor(|_, _, _| {
18934 assert_eq!(
18935 editors.len(),
18936 1,
18937 "After unsuccessful fallback, no other editor should have been opened"
18938 );
18939 });
18940}
18941
18942#[gpui::test]
18943async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
18944 init_test(cx, |_| {});
18945
18946 let language = Arc::new(Language::new(
18947 LanguageConfig::default(),
18948 Some(tree_sitter_rust::LANGUAGE.into()),
18949 ));
18950
18951 let text = r#"
18952 #[cfg(test)]
18953 mod tests() {
18954 #[test]
18955 fn runnable_1() {
18956 let a = 1;
18957 }
18958
18959 #[test]
18960 fn runnable_2() {
18961 let a = 1;
18962 let b = 2;
18963 }
18964 }
18965 "#
18966 .unindent();
18967
18968 let fs = FakeFs::new(cx.executor());
18969 fs.insert_file("/file.rs", Default::default()).await;
18970
18971 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18972 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18973 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18974 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
18975 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
18976
18977 let editor = cx.new_window_entity(|window, cx| {
18978 Editor::new(
18979 EditorMode::full(),
18980 multi_buffer,
18981 Some(project.clone()),
18982 window,
18983 cx,
18984 )
18985 });
18986
18987 editor.update_in(cx, |editor, window, cx| {
18988 let snapshot = editor.buffer().read(cx).snapshot(cx);
18989 editor.tasks.insert(
18990 (buffer.read(cx).remote_id(), 3),
18991 RunnableTasks {
18992 templates: vec![],
18993 offset: snapshot.anchor_before(43),
18994 column: 0,
18995 extra_variables: HashMap::default(),
18996 context_range: BufferOffset(43)..BufferOffset(85),
18997 },
18998 );
18999 editor.tasks.insert(
19000 (buffer.read(cx).remote_id(), 8),
19001 RunnableTasks {
19002 templates: vec![],
19003 offset: snapshot.anchor_before(86),
19004 column: 0,
19005 extra_variables: HashMap::default(),
19006 context_range: BufferOffset(86)..BufferOffset(191),
19007 },
19008 );
19009
19010 // Test finding task when cursor is inside function body
19011 editor.change_selections(None, window, cx, |s| {
19012 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
19013 });
19014 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19015 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
19016
19017 // Test finding task when cursor is on function name
19018 editor.change_selections(None, window, cx, |s| {
19019 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
19020 });
19021 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19022 assert_eq!(row, 8, "Should find task when cursor is on function name");
19023 });
19024}
19025
19026#[gpui::test]
19027async fn test_folding_buffers(cx: &mut TestAppContext) {
19028 init_test(cx, |_| {});
19029
19030 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
19031 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
19032 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
19033
19034 let fs = FakeFs::new(cx.executor());
19035 fs.insert_tree(
19036 path!("/a"),
19037 json!({
19038 "first.rs": sample_text_1,
19039 "second.rs": sample_text_2,
19040 "third.rs": sample_text_3,
19041 }),
19042 )
19043 .await;
19044 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19045 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19046 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19047 let worktree = project.update(cx, |project, cx| {
19048 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19049 assert_eq!(worktrees.len(), 1);
19050 worktrees.pop().unwrap()
19051 });
19052 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19053
19054 let buffer_1 = project
19055 .update(cx, |project, cx| {
19056 project.open_buffer((worktree_id, "first.rs"), cx)
19057 })
19058 .await
19059 .unwrap();
19060 let buffer_2 = project
19061 .update(cx, |project, cx| {
19062 project.open_buffer((worktree_id, "second.rs"), cx)
19063 })
19064 .await
19065 .unwrap();
19066 let buffer_3 = project
19067 .update(cx, |project, cx| {
19068 project.open_buffer((worktree_id, "third.rs"), cx)
19069 })
19070 .await
19071 .unwrap();
19072
19073 let multi_buffer = cx.new(|cx| {
19074 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19075 multi_buffer.push_excerpts(
19076 buffer_1.clone(),
19077 [
19078 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19079 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19080 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19081 ],
19082 cx,
19083 );
19084 multi_buffer.push_excerpts(
19085 buffer_2.clone(),
19086 [
19087 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19088 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19089 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19090 ],
19091 cx,
19092 );
19093 multi_buffer.push_excerpts(
19094 buffer_3.clone(),
19095 [
19096 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19097 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19098 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19099 ],
19100 cx,
19101 );
19102 multi_buffer
19103 });
19104 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19105 Editor::new(
19106 EditorMode::full(),
19107 multi_buffer.clone(),
19108 Some(project.clone()),
19109 window,
19110 cx,
19111 )
19112 });
19113
19114 assert_eq!(
19115 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19116 "\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",
19117 );
19118
19119 multi_buffer_editor.update(cx, |editor, cx| {
19120 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
19121 });
19122 assert_eq!(
19123 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19124 "\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",
19125 "After folding the first buffer, its text should not be displayed"
19126 );
19127
19128 multi_buffer_editor.update(cx, |editor, cx| {
19129 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
19130 });
19131 assert_eq!(
19132 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19133 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
19134 "After folding the second buffer, its text should not be displayed"
19135 );
19136
19137 multi_buffer_editor.update(cx, |editor, cx| {
19138 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
19139 });
19140 assert_eq!(
19141 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19142 "\n\n\n\n\n",
19143 "After folding the third buffer, its text should not be displayed"
19144 );
19145
19146 // Emulate selection inside the fold logic, that should work
19147 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19148 editor
19149 .snapshot(window, cx)
19150 .next_line_boundary(Point::new(0, 4));
19151 });
19152
19153 multi_buffer_editor.update(cx, |editor, cx| {
19154 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
19155 });
19156 assert_eq!(
19157 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19158 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19159 "After unfolding the second buffer, its text should be displayed"
19160 );
19161
19162 // Typing inside of buffer 1 causes that buffer to be unfolded.
19163 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19164 assert_eq!(
19165 multi_buffer
19166 .read(cx)
19167 .snapshot(cx)
19168 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
19169 .collect::<String>(),
19170 "bbbb"
19171 );
19172 editor.change_selections(None, window, cx, |selections| {
19173 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
19174 });
19175 editor.handle_input("B", window, cx);
19176 });
19177
19178 assert_eq!(
19179 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19180 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19181 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
19182 );
19183
19184 multi_buffer_editor.update(cx, |editor, cx| {
19185 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
19186 });
19187 assert_eq!(
19188 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19189 "\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",
19190 "After unfolding the all buffers, all original text should be displayed"
19191 );
19192}
19193
19194#[gpui::test]
19195async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
19196 init_test(cx, |_| {});
19197
19198 let sample_text_1 = "1111\n2222\n3333".to_string();
19199 let sample_text_2 = "4444\n5555\n6666".to_string();
19200 let sample_text_3 = "7777\n8888\n9999".to_string();
19201
19202 let fs = FakeFs::new(cx.executor());
19203 fs.insert_tree(
19204 path!("/a"),
19205 json!({
19206 "first.rs": sample_text_1,
19207 "second.rs": sample_text_2,
19208 "third.rs": sample_text_3,
19209 }),
19210 )
19211 .await;
19212 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19213 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19214 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19215 let worktree = project.update(cx, |project, cx| {
19216 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19217 assert_eq!(worktrees.len(), 1);
19218 worktrees.pop().unwrap()
19219 });
19220 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19221
19222 let buffer_1 = project
19223 .update(cx, |project, cx| {
19224 project.open_buffer((worktree_id, "first.rs"), cx)
19225 })
19226 .await
19227 .unwrap();
19228 let buffer_2 = project
19229 .update(cx, |project, cx| {
19230 project.open_buffer((worktree_id, "second.rs"), cx)
19231 })
19232 .await
19233 .unwrap();
19234 let buffer_3 = project
19235 .update(cx, |project, cx| {
19236 project.open_buffer((worktree_id, "third.rs"), cx)
19237 })
19238 .await
19239 .unwrap();
19240
19241 let multi_buffer = cx.new(|cx| {
19242 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19243 multi_buffer.push_excerpts(
19244 buffer_1.clone(),
19245 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19246 cx,
19247 );
19248 multi_buffer.push_excerpts(
19249 buffer_2.clone(),
19250 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19251 cx,
19252 );
19253 multi_buffer.push_excerpts(
19254 buffer_3.clone(),
19255 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19256 cx,
19257 );
19258 multi_buffer
19259 });
19260
19261 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19262 Editor::new(
19263 EditorMode::full(),
19264 multi_buffer,
19265 Some(project.clone()),
19266 window,
19267 cx,
19268 )
19269 });
19270
19271 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
19272 assert_eq!(
19273 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19274 full_text,
19275 );
19276
19277 multi_buffer_editor.update(cx, |editor, cx| {
19278 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
19279 });
19280 assert_eq!(
19281 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19282 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
19283 "After folding the first buffer, its text should not be displayed"
19284 );
19285
19286 multi_buffer_editor.update(cx, |editor, cx| {
19287 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
19288 });
19289
19290 assert_eq!(
19291 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19292 "\n\n\n\n\n\n7777\n8888\n9999",
19293 "After folding the second buffer, its text should not be displayed"
19294 );
19295
19296 multi_buffer_editor.update(cx, |editor, cx| {
19297 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
19298 });
19299 assert_eq!(
19300 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19301 "\n\n\n\n\n",
19302 "After folding the third buffer, its text should not be displayed"
19303 );
19304
19305 multi_buffer_editor.update(cx, |editor, cx| {
19306 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
19307 });
19308 assert_eq!(
19309 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19310 "\n\n\n\n4444\n5555\n6666\n\n",
19311 "After unfolding the second buffer, its text should be displayed"
19312 );
19313
19314 multi_buffer_editor.update(cx, |editor, cx| {
19315 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
19316 });
19317 assert_eq!(
19318 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19319 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
19320 "After unfolding the first buffer, its text should be displayed"
19321 );
19322
19323 multi_buffer_editor.update(cx, |editor, cx| {
19324 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
19325 });
19326 assert_eq!(
19327 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19328 full_text,
19329 "After unfolding all buffers, all original text should be displayed"
19330 );
19331}
19332
19333#[gpui::test]
19334async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
19335 init_test(cx, |_| {});
19336
19337 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
19338
19339 let fs = FakeFs::new(cx.executor());
19340 fs.insert_tree(
19341 path!("/a"),
19342 json!({
19343 "main.rs": sample_text,
19344 }),
19345 )
19346 .await;
19347 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19348 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19349 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19350 let worktree = project.update(cx, |project, cx| {
19351 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19352 assert_eq!(worktrees.len(), 1);
19353 worktrees.pop().unwrap()
19354 });
19355 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19356
19357 let buffer_1 = project
19358 .update(cx, |project, cx| {
19359 project.open_buffer((worktree_id, "main.rs"), cx)
19360 })
19361 .await
19362 .unwrap();
19363
19364 let multi_buffer = cx.new(|cx| {
19365 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19366 multi_buffer.push_excerpts(
19367 buffer_1.clone(),
19368 [ExcerptRange::new(
19369 Point::new(0, 0)
19370 ..Point::new(
19371 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
19372 0,
19373 ),
19374 )],
19375 cx,
19376 );
19377 multi_buffer
19378 });
19379 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19380 Editor::new(
19381 EditorMode::full(),
19382 multi_buffer,
19383 Some(project.clone()),
19384 window,
19385 cx,
19386 )
19387 });
19388
19389 let selection_range = Point::new(1, 0)..Point::new(2, 0);
19390 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19391 enum TestHighlight {}
19392 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
19393 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
19394 editor.highlight_text::<TestHighlight>(
19395 vec![(
19396 highlight_range.clone(),
19397 HighlightStyle::color(Hsla::green()),
19398 )],
19399 cx,
19400 );
19401 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
19402 });
19403
19404 let full_text = format!("\n\n{sample_text}");
19405 assert_eq!(
19406 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19407 full_text,
19408 );
19409}
19410
19411#[gpui::test]
19412async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
19413 init_test(cx, |_| {});
19414 cx.update(|cx| {
19415 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
19416 "keymaps/default-linux.json",
19417 cx,
19418 )
19419 .unwrap();
19420 cx.bind_keys(default_key_bindings);
19421 });
19422
19423 let (editor, cx) = cx.add_window_view(|window, cx| {
19424 let multi_buffer = MultiBuffer::build_multi(
19425 [
19426 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
19427 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
19428 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
19429 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
19430 ],
19431 cx,
19432 );
19433 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
19434
19435 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
19436 // fold all but the second buffer, so that we test navigating between two
19437 // adjacent folded buffers, as well as folded buffers at the start and
19438 // end the multibuffer
19439 editor.fold_buffer(buffer_ids[0], cx);
19440 editor.fold_buffer(buffer_ids[2], cx);
19441 editor.fold_buffer(buffer_ids[3], cx);
19442
19443 editor
19444 });
19445 cx.simulate_resize(size(px(1000.), px(1000.)));
19446
19447 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
19448 cx.assert_excerpts_with_selections(indoc! {"
19449 [EXCERPT]
19450 ˇ[FOLDED]
19451 [EXCERPT]
19452 a1
19453 b1
19454 [EXCERPT]
19455 [FOLDED]
19456 [EXCERPT]
19457 [FOLDED]
19458 "
19459 });
19460 cx.simulate_keystroke("down");
19461 cx.assert_excerpts_with_selections(indoc! {"
19462 [EXCERPT]
19463 [FOLDED]
19464 [EXCERPT]
19465 ˇa1
19466 b1
19467 [EXCERPT]
19468 [FOLDED]
19469 [EXCERPT]
19470 [FOLDED]
19471 "
19472 });
19473 cx.simulate_keystroke("down");
19474 cx.assert_excerpts_with_selections(indoc! {"
19475 [EXCERPT]
19476 [FOLDED]
19477 [EXCERPT]
19478 a1
19479 ˇb1
19480 [EXCERPT]
19481 [FOLDED]
19482 [EXCERPT]
19483 [FOLDED]
19484 "
19485 });
19486 cx.simulate_keystroke("down");
19487 cx.assert_excerpts_with_selections(indoc! {"
19488 [EXCERPT]
19489 [FOLDED]
19490 [EXCERPT]
19491 a1
19492 b1
19493 ˇ[EXCERPT]
19494 [FOLDED]
19495 [EXCERPT]
19496 [FOLDED]
19497 "
19498 });
19499 cx.simulate_keystroke("down");
19500 cx.assert_excerpts_with_selections(indoc! {"
19501 [EXCERPT]
19502 [FOLDED]
19503 [EXCERPT]
19504 a1
19505 b1
19506 [EXCERPT]
19507 ˇ[FOLDED]
19508 [EXCERPT]
19509 [FOLDED]
19510 "
19511 });
19512 for _ in 0..5 {
19513 cx.simulate_keystroke("down");
19514 cx.assert_excerpts_with_selections(indoc! {"
19515 [EXCERPT]
19516 [FOLDED]
19517 [EXCERPT]
19518 a1
19519 b1
19520 [EXCERPT]
19521 [FOLDED]
19522 [EXCERPT]
19523 ˇ[FOLDED]
19524 "
19525 });
19526 }
19527
19528 cx.simulate_keystroke("up");
19529 cx.assert_excerpts_with_selections(indoc! {"
19530 [EXCERPT]
19531 [FOLDED]
19532 [EXCERPT]
19533 a1
19534 b1
19535 [EXCERPT]
19536 ˇ[FOLDED]
19537 [EXCERPT]
19538 [FOLDED]
19539 "
19540 });
19541 cx.simulate_keystroke("up");
19542 cx.assert_excerpts_with_selections(indoc! {"
19543 [EXCERPT]
19544 [FOLDED]
19545 [EXCERPT]
19546 a1
19547 b1
19548 ˇ[EXCERPT]
19549 [FOLDED]
19550 [EXCERPT]
19551 [FOLDED]
19552 "
19553 });
19554 cx.simulate_keystroke("up");
19555 cx.assert_excerpts_with_selections(indoc! {"
19556 [EXCERPT]
19557 [FOLDED]
19558 [EXCERPT]
19559 a1
19560 ˇb1
19561 [EXCERPT]
19562 [FOLDED]
19563 [EXCERPT]
19564 [FOLDED]
19565 "
19566 });
19567 cx.simulate_keystroke("up");
19568 cx.assert_excerpts_with_selections(indoc! {"
19569 [EXCERPT]
19570 [FOLDED]
19571 [EXCERPT]
19572 ˇa1
19573 b1
19574 [EXCERPT]
19575 [FOLDED]
19576 [EXCERPT]
19577 [FOLDED]
19578 "
19579 });
19580 for _ in 0..5 {
19581 cx.simulate_keystroke("up");
19582 cx.assert_excerpts_with_selections(indoc! {"
19583 [EXCERPT]
19584 ˇ[FOLDED]
19585 [EXCERPT]
19586 a1
19587 b1
19588 [EXCERPT]
19589 [FOLDED]
19590 [EXCERPT]
19591 [FOLDED]
19592 "
19593 });
19594 }
19595}
19596
19597#[gpui::test]
19598async fn test_inline_completion_text(cx: &mut TestAppContext) {
19599 init_test(cx, |_| {});
19600
19601 // Simple insertion
19602 assert_highlighted_edits(
19603 "Hello, world!",
19604 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
19605 true,
19606 cx,
19607 |highlighted_edits, cx| {
19608 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
19609 assert_eq!(highlighted_edits.highlights.len(), 1);
19610 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
19611 assert_eq!(
19612 highlighted_edits.highlights[0].1.background_color,
19613 Some(cx.theme().status().created_background)
19614 );
19615 },
19616 )
19617 .await;
19618
19619 // Replacement
19620 assert_highlighted_edits(
19621 "This is a test.",
19622 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
19623 false,
19624 cx,
19625 |highlighted_edits, cx| {
19626 assert_eq!(highlighted_edits.text, "That is a test.");
19627 assert_eq!(highlighted_edits.highlights.len(), 1);
19628 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
19629 assert_eq!(
19630 highlighted_edits.highlights[0].1.background_color,
19631 Some(cx.theme().status().created_background)
19632 );
19633 },
19634 )
19635 .await;
19636
19637 // Multiple edits
19638 assert_highlighted_edits(
19639 "Hello, world!",
19640 vec![
19641 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
19642 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
19643 ],
19644 false,
19645 cx,
19646 |highlighted_edits, cx| {
19647 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
19648 assert_eq!(highlighted_edits.highlights.len(), 2);
19649 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
19650 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
19651 assert_eq!(
19652 highlighted_edits.highlights[0].1.background_color,
19653 Some(cx.theme().status().created_background)
19654 );
19655 assert_eq!(
19656 highlighted_edits.highlights[1].1.background_color,
19657 Some(cx.theme().status().created_background)
19658 );
19659 },
19660 )
19661 .await;
19662
19663 // Multiple lines with edits
19664 assert_highlighted_edits(
19665 "First line\nSecond line\nThird line\nFourth line",
19666 vec![
19667 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
19668 (
19669 Point::new(2, 0)..Point::new(2, 10),
19670 "New third line".to_string(),
19671 ),
19672 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
19673 ],
19674 false,
19675 cx,
19676 |highlighted_edits, cx| {
19677 assert_eq!(
19678 highlighted_edits.text,
19679 "Second modified\nNew third line\nFourth updated line"
19680 );
19681 assert_eq!(highlighted_edits.highlights.len(), 3);
19682 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
19683 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
19684 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
19685 for highlight in &highlighted_edits.highlights {
19686 assert_eq!(
19687 highlight.1.background_color,
19688 Some(cx.theme().status().created_background)
19689 );
19690 }
19691 },
19692 )
19693 .await;
19694}
19695
19696#[gpui::test]
19697async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
19698 init_test(cx, |_| {});
19699
19700 // Deletion
19701 assert_highlighted_edits(
19702 "Hello, world!",
19703 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
19704 true,
19705 cx,
19706 |highlighted_edits, cx| {
19707 assert_eq!(highlighted_edits.text, "Hello, world!");
19708 assert_eq!(highlighted_edits.highlights.len(), 1);
19709 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
19710 assert_eq!(
19711 highlighted_edits.highlights[0].1.background_color,
19712 Some(cx.theme().status().deleted_background)
19713 );
19714 },
19715 )
19716 .await;
19717
19718 // Insertion
19719 assert_highlighted_edits(
19720 "Hello, world!",
19721 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
19722 true,
19723 cx,
19724 |highlighted_edits, cx| {
19725 assert_eq!(highlighted_edits.highlights.len(), 1);
19726 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
19727 assert_eq!(
19728 highlighted_edits.highlights[0].1.background_color,
19729 Some(cx.theme().status().created_background)
19730 );
19731 },
19732 )
19733 .await;
19734}
19735
19736async fn assert_highlighted_edits(
19737 text: &str,
19738 edits: Vec<(Range<Point>, String)>,
19739 include_deletions: bool,
19740 cx: &mut TestAppContext,
19741 assertion_fn: impl Fn(HighlightedText, &App),
19742) {
19743 let window = cx.add_window(|window, cx| {
19744 let buffer = MultiBuffer::build_simple(text, cx);
19745 Editor::new(EditorMode::full(), buffer, None, window, cx)
19746 });
19747 let cx = &mut VisualTestContext::from_window(*window, cx);
19748
19749 let (buffer, snapshot) = window
19750 .update(cx, |editor, _window, cx| {
19751 (
19752 editor.buffer().clone(),
19753 editor.buffer().read(cx).snapshot(cx),
19754 )
19755 })
19756 .unwrap();
19757
19758 let edits = edits
19759 .into_iter()
19760 .map(|(range, edit)| {
19761 (
19762 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
19763 edit,
19764 )
19765 })
19766 .collect::<Vec<_>>();
19767
19768 let text_anchor_edits = edits
19769 .clone()
19770 .into_iter()
19771 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
19772 .collect::<Vec<_>>();
19773
19774 let edit_preview = window
19775 .update(cx, |_, _window, cx| {
19776 buffer
19777 .read(cx)
19778 .as_singleton()
19779 .unwrap()
19780 .read(cx)
19781 .preview_edits(text_anchor_edits.into(), cx)
19782 })
19783 .unwrap()
19784 .await;
19785
19786 cx.update(|_window, cx| {
19787 let highlighted_edits = inline_completion_edit_text(
19788 &snapshot.as_singleton().unwrap().2,
19789 &edits,
19790 &edit_preview,
19791 include_deletions,
19792 cx,
19793 );
19794 assertion_fn(highlighted_edits, cx)
19795 });
19796}
19797
19798#[track_caller]
19799fn assert_breakpoint(
19800 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
19801 path: &Arc<Path>,
19802 expected: Vec<(u32, Breakpoint)>,
19803) {
19804 if expected.len() == 0usize {
19805 assert!(!breakpoints.contains_key(path), "{}", path.display());
19806 } else {
19807 let mut breakpoint = breakpoints
19808 .get(path)
19809 .unwrap()
19810 .into_iter()
19811 .map(|breakpoint| {
19812 (
19813 breakpoint.row,
19814 Breakpoint {
19815 message: breakpoint.message.clone(),
19816 state: breakpoint.state,
19817 condition: breakpoint.condition.clone(),
19818 hit_condition: breakpoint.hit_condition.clone(),
19819 },
19820 )
19821 })
19822 .collect::<Vec<_>>();
19823
19824 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
19825
19826 assert_eq!(expected, breakpoint);
19827 }
19828}
19829
19830fn add_log_breakpoint_at_cursor(
19831 editor: &mut Editor,
19832 log_message: &str,
19833 window: &mut Window,
19834 cx: &mut Context<Editor>,
19835) {
19836 let (anchor, bp) = editor
19837 .breakpoints_at_cursors(window, cx)
19838 .first()
19839 .and_then(|(anchor, bp)| {
19840 if let Some(bp) = bp {
19841 Some((*anchor, bp.clone()))
19842 } else {
19843 None
19844 }
19845 })
19846 .unwrap_or_else(|| {
19847 let cursor_position: Point = editor.selections.newest(cx).head();
19848
19849 let breakpoint_position = editor
19850 .snapshot(window, cx)
19851 .display_snapshot
19852 .buffer_snapshot
19853 .anchor_before(Point::new(cursor_position.row, 0));
19854
19855 (breakpoint_position, Breakpoint::new_log(&log_message))
19856 });
19857
19858 editor.edit_breakpoint_at_anchor(
19859 anchor,
19860 bp,
19861 BreakpointEditAction::EditLogMessage(log_message.into()),
19862 cx,
19863 );
19864}
19865
19866#[gpui::test]
19867async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
19868 init_test(cx, |_| {});
19869
19870 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19871 let fs = FakeFs::new(cx.executor());
19872 fs.insert_tree(
19873 path!("/a"),
19874 json!({
19875 "main.rs": sample_text,
19876 }),
19877 )
19878 .await;
19879 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19880 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19881 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19882
19883 let fs = FakeFs::new(cx.executor());
19884 fs.insert_tree(
19885 path!("/a"),
19886 json!({
19887 "main.rs": sample_text,
19888 }),
19889 )
19890 .await;
19891 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19892 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19893 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19894 let worktree_id = workspace
19895 .update(cx, |workspace, _window, cx| {
19896 workspace.project().update(cx, |project, cx| {
19897 project.worktrees(cx).next().unwrap().read(cx).id()
19898 })
19899 })
19900 .unwrap();
19901
19902 let buffer = project
19903 .update(cx, |project, cx| {
19904 project.open_buffer((worktree_id, "main.rs"), cx)
19905 })
19906 .await
19907 .unwrap();
19908
19909 let (editor, cx) = cx.add_window_view(|window, cx| {
19910 Editor::new(
19911 EditorMode::full(),
19912 MultiBuffer::build_from_buffer(buffer, cx),
19913 Some(project.clone()),
19914 window,
19915 cx,
19916 )
19917 });
19918
19919 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19920 let abs_path = project.read_with(cx, |project, cx| {
19921 project
19922 .absolute_path(&project_path, cx)
19923 .map(|path_buf| Arc::from(path_buf.to_owned()))
19924 .unwrap()
19925 });
19926
19927 // assert we can add breakpoint on the first line
19928 editor.update_in(cx, |editor, window, cx| {
19929 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19930 editor.move_to_end(&MoveToEnd, window, cx);
19931 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19932 });
19933
19934 let breakpoints = editor.update(cx, |editor, cx| {
19935 editor
19936 .breakpoint_store()
19937 .as_ref()
19938 .unwrap()
19939 .read(cx)
19940 .all_source_breakpoints(cx)
19941 .clone()
19942 });
19943
19944 assert_eq!(1, breakpoints.len());
19945 assert_breakpoint(
19946 &breakpoints,
19947 &abs_path,
19948 vec![
19949 (0, Breakpoint::new_standard()),
19950 (3, Breakpoint::new_standard()),
19951 ],
19952 );
19953
19954 editor.update_in(cx, |editor, window, cx| {
19955 editor.move_to_beginning(&MoveToBeginning, window, cx);
19956 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19957 });
19958
19959 let breakpoints = editor.update(cx, |editor, cx| {
19960 editor
19961 .breakpoint_store()
19962 .as_ref()
19963 .unwrap()
19964 .read(cx)
19965 .all_source_breakpoints(cx)
19966 .clone()
19967 });
19968
19969 assert_eq!(1, breakpoints.len());
19970 assert_breakpoint(
19971 &breakpoints,
19972 &abs_path,
19973 vec![(3, Breakpoint::new_standard())],
19974 );
19975
19976 editor.update_in(cx, |editor, window, cx| {
19977 editor.move_to_end(&MoveToEnd, window, cx);
19978 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19979 });
19980
19981 let breakpoints = editor.update(cx, |editor, cx| {
19982 editor
19983 .breakpoint_store()
19984 .as_ref()
19985 .unwrap()
19986 .read(cx)
19987 .all_source_breakpoints(cx)
19988 .clone()
19989 });
19990
19991 assert_eq!(0, breakpoints.len());
19992 assert_breakpoint(&breakpoints, &abs_path, vec![]);
19993}
19994
19995#[gpui::test]
19996async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
19997 init_test(cx, |_| {});
19998
19999 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20000
20001 let fs = FakeFs::new(cx.executor());
20002 fs.insert_tree(
20003 path!("/a"),
20004 json!({
20005 "main.rs": sample_text,
20006 }),
20007 )
20008 .await;
20009 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20010 let (workspace, cx) =
20011 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20012
20013 let worktree_id = workspace.update(cx, |workspace, cx| {
20014 workspace.project().update(cx, |project, cx| {
20015 project.worktrees(cx).next().unwrap().read(cx).id()
20016 })
20017 });
20018
20019 let buffer = project
20020 .update(cx, |project, cx| {
20021 project.open_buffer((worktree_id, "main.rs"), cx)
20022 })
20023 .await
20024 .unwrap();
20025
20026 let (editor, cx) = cx.add_window_view(|window, cx| {
20027 Editor::new(
20028 EditorMode::full(),
20029 MultiBuffer::build_from_buffer(buffer, cx),
20030 Some(project.clone()),
20031 window,
20032 cx,
20033 )
20034 });
20035
20036 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20037 let abs_path = project.read_with(cx, |project, cx| {
20038 project
20039 .absolute_path(&project_path, cx)
20040 .map(|path_buf| Arc::from(path_buf.to_owned()))
20041 .unwrap()
20042 });
20043
20044 editor.update_in(cx, |editor, window, cx| {
20045 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20046 });
20047
20048 let breakpoints = editor.update(cx, |editor, cx| {
20049 editor
20050 .breakpoint_store()
20051 .as_ref()
20052 .unwrap()
20053 .read(cx)
20054 .all_source_breakpoints(cx)
20055 .clone()
20056 });
20057
20058 assert_breakpoint(
20059 &breakpoints,
20060 &abs_path,
20061 vec![(0, Breakpoint::new_log("hello world"))],
20062 );
20063
20064 // Removing a log message from a log breakpoint should remove it
20065 editor.update_in(cx, |editor, window, cx| {
20066 add_log_breakpoint_at_cursor(editor, "", window, cx);
20067 });
20068
20069 let breakpoints = editor.update(cx, |editor, cx| {
20070 editor
20071 .breakpoint_store()
20072 .as_ref()
20073 .unwrap()
20074 .read(cx)
20075 .all_source_breakpoints(cx)
20076 .clone()
20077 });
20078
20079 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20080
20081 editor.update_in(cx, |editor, window, cx| {
20082 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20083 editor.move_to_end(&MoveToEnd, window, cx);
20084 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20085 // Not adding a log message to a standard breakpoint shouldn't remove it
20086 add_log_breakpoint_at_cursor(editor, "", window, cx);
20087 });
20088
20089 let breakpoints = editor.update(cx, |editor, cx| {
20090 editor
20091 .breakpoint_store()
20092 .as_ref()
20093 .unwrap()
20094 .read(cx)
20095 .all_source_breakpoints(cx)
20096 .clone()
20097 });
20098
20099 assert_breakpoint(
20100 &breakpoints,
20101 &abs_path,
20102 vec![
20103 (0, Breakpoint::new_standard()),
20104 (3, Breakpoint::new_standard()),
20105 ],
20106 );
20107
20108 editor.update_in(cx, |editor, window, cx| {
20109 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20110 });
20111
20112 let breakpoints = editor.update(cx, |editor, cx| {
20113 editor
20114 .breakpoint_store()
20115 .as_ref()
20116 .unwrap()
20117 .read(cx)
20118 .all_source_breakpoints(cx)
20119 .clone()
20120 });
20121
20122 assert_breakpoint(
20123 &breakpoints,
20124 &abs_path,
20125 vec![
20126 (0, Breakpoint::new_standard()),
20127 (3, Breakpoint::new_log("hello world")),
20128 ],
20129 );
20130
20131 editor.update_in(cx, |editor, window, cx| {
20132 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
20133 });
20134
20135 let breakpoints = editor.update(cx, |editor, cx| {
20136 editor
20137 .breakpoint_store()
20138 .as_ref()
20139 .unwrap()
20140 .read(cx)
20141 .all_source_breakpoints(cx)
20142 .clone()
20143 });
20144
20145 assert_breakpoint(
20146 &breakpoints,
20147 &abs_path,
20148 vec![
20149 (0, Breakpoint::new_standard()),
20150 (3, Breakpoint::new_log("hello Earth!!")),
20151 ],
20152 );
20153}
20154
20155/// This also tests that Editor::breakpoint_at_cursor_head is working properly
20156/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
20157/// or when breakpoints were placed out of order. This tests for a regression too
20158#[gpui::test]
20159async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
20160 init_test(cx, |_| {});
20161
20162 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20163 let fs = FakeFs::new(cx.executor());
20164 fs.insert_tree(
20165 path!("/a"),
20166 json!({
20167 "main.rs": sample_text,
20168 }),
20169 )
20170 .await;
20171 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20172 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20173 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20174
20175 let fs = FakeFs::new(cx.executor());
20176 fs.insert_tree(
20177 path!("/a"),
20178 json!({
20179 "main.rs": sample_text,
20180 }),
20181 )
20182 .await;
20183 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20184 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20185 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20186 let worktree_id = workspace
20187 .update(cx, |workspace, _window, cx| {
20188 workspace.project().update(cx, |project, cx| {
20189 project.worktrees(cx).next().unwrap().read(cx).id()
20190 })
20191 })
20192 .unwrap();
20193
20194 let buffer = project
20195 .update(cx, |project, cx| {
20196 project.open_buffer((worktree_id, "main.rs"), cx)
20197 })
20198 .await
20199 .unwrap();
20200
20201 let (editor, cx) = cx.add_window_view(|window, cx| {
20202 Editor::new(
20203 EditorMode::full(),
20204 MultiBuffer::build_from_buffer(buffer, cx),
20205 Some(project.clone()),
20206 window,
20207 cx,
20208 )
20209 });
20210
20211 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20212 let abs_path = project.read_with(cx, |project, cx| {
20213 project
20214 .absolute_path(&project_path, cx)
20215 .map(|path_buf| Arc::from(path_buf.to_owned()))
20216 .unwrap()
20217 });
20218
20219 // assert we can add breakpoint on the first line
20220 editor.update_in(cx, |editor, window, cx| {
20221 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20222 editor.move_to_end(&MoveToEnd, window, cx);
20223 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20224 editor.move_up(&MoveUp, window, cx);
20225 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20226 });
20227
20228 let breakpoints = editor.update(cx, |editor, cx| {
20229 editor
20230 .breakpoint_store()
20231 .as_ref()
20232 .unwrap()
20233 .read(cx)
20234 .all_source_breakpoints(cx)
20235 .clone()
20236 });
20237
20238 assert_eq!(1, breakpoints.len());
20239 assert_breakpoint(
20240 &breakpoints,
20241 &abs_path,
20242 vec![
20243 (0, Breakpoint::new_standard()),
20244 (2, Breakpoint::new_standard()),
20245 (3, Breakpoint::new_standard()),
20246 ],
20247 );
20248
20249 editor.update_in(cx, |editor, window, cx| {
20250 editor.move_to_beginning(&MoveToBeginning, window, cx);
20251 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20252 editor.move_to_end(&MoveToEnd, window, cx);
20253 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20254 // Disabling a breakpoint that doesn't exist should do nothing
20255 editor.move_up(&MoveUp, window, cx);
20256 editor.move_up(&MoveUp, window, cx);
20257 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20258 });
20259
20260 let breakpoints = editor.update(cx, |editor, cx| {
20261 editor
20262 .breakpoint_store()
20263 .as_ref()
20264 .unwrap()
20265 .read(cx)
20266 .all_source_breakpoints(cx)
20267 .clone()
20268 });
20269
20270 let disable_breakpoint = {
20271 let mut bp = Breakpoint::new_standard();
20272 bp.state = BreakpointState::Disabled;
20273 bp
20274 };
20275
20276 assert_eq!(1, breakpoints.len());
20277 assert_breakpoint(
20278 &breakpoints,
20279 &abs_path,
20280 vec![
20281 (0, disable_breakpoint.clone()),
20282 (2, Breakpoint::new_standard()),
20283 (3, disable_breakpoint.clone()),
20284 ],
20285 );
20286
20287 editor.update_in(cx, |editor, window, cx| {
20288 editor.move_to_beginning(&MoveToBeginning, window, cx);
20289 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
20290 editor.move_to_end(&MoveToEnd, window, cx);
20291 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
20292 editor.move_up(&MoveUp, window, cx);
20293 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20294 });
20295
20296 let breakpoints = editor.update(cx, |editor, cx| {
20297 editor
20298 .breakpoint_store()
20299 .as_ref()
20300 .unwrap()
20301 .read(cx)
20302 .all_source_breakpoints(cx)
20303 .clone()
20304 });
20305
20306 assert_eq!(1, breakpoints.len());
20307 assert_breakpoint(
20308 &breakpoints,
20309 &abs_path,
20310 vec![
20311 (0, Breakpoint::new_standard()),
20312 (2, disable_breakpoint),
20313 (3, Breakpoint::new_standard()),
20314 ],
20315 );
20316}
20317
20318#[gpui::test]
20319async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
20320 init_test(cx, |_| {});
20321 let capabilities = lsp::ServerCapabilities {
20322 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
20323 prepare_provider: Some(true),
20324 work_done_progress_options: Default::default(),
20325 })),
20326 ..Default::default()
20327 };
20328 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
20329
20330 cx.set_state(indoc! {"
20331 struct Fˇoo {}
20332 "});
20333
20334 cx.update_editor(|editor, _, cx| {
20335 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
20336 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
20337 editor.highlight_background::<DocumentHighlightRead>(
20338 &[highlight_range],
20339 |c| c.colors().editor_document_highlight_read_background,
20340 cx,
20341 );
20342 });
20343
20344 let mut prepare_rename_handler = cx
20345 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
20346 move |_, _, _| async move {
20347 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
20348 start: lsp::Position {
20349 line: 0,
20350 character: 7,
20351 },
20352 end: lsp::Position {
20353 line: 0,
20354 character: 10,
20355 },
20356 })))
20357 },
20358 );
20359 let prepare_rename_task = cx
20360 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
20361 .expect("Prepare rename was not started");
20362 prepare_rename_handler.next().await.unwrap();
20363 prepare_rename_task.await.expect("Prepare rename failed");
20364
20365 let mut rename_handler =
20366 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
20367 let edit = lsp::TextEdit {
20368 range: lsp::Range {
20369 start: lsp::Position {
20370 line: 0,
20371 character: 7,
20372 },
20373 end: lsp::Position {
20374 line: 0,
20375 character: 10,
20376 },
20377 },
20378 new_text: "FooRenamed".to_string(),
20379 };
20380 Ok(Some(lsp::WorkspaceEdit::new(
20381 // Specify the same edit twice
20382 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
20383 )))
20384 });
20385 let rename_task = cx
20386 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
20387 .expect("Confirm rename was not started");
20388 rename_handler.next().await.unwrap();
20389 rename_task.await.expect("Confirm rename failed");
20390 cx.run_until_parked();
20391
20392 // Despite two edits, only one is actually applied as those are identical
20393 cx.assert_editor_state(indoc! {"
20394 struct FooRenamedˇ {}
20395 "});
20396}
20397
20398#[gpui::test]
20399async fn test_rename_without_prepare(cx: &mut TestAppContext) {
20400 init_test(cx, |_| {});
20401 // These capabilities indicate that the server does not support prepare rename.
20402 let capabilities = lsp::ServerCapabilities {
20403 rename_provider: Some(lsp::OneOf::Left(true)),
20404 ..Default::default()
20405 };
20406 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
20407
20408 cx.set_state(indoc! {"
20409 struct Fˇoo {}
20410 "});
20411
20412 cx.update_editor(|editor, _window, cx| {
20413 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
20414 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
20415 editor.highlight_background::<DocumentHighlightRead>(
20416 &[highlight_range],
20417 |c| c.colors().editor_document_highlight_read_background,
20418 cx,
20419 );
20420 });
20421
20422 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
20423 .expect("Prepare rename was not started")
20424 .await
20425 .expect("Prepare rename failed");
20426
20427 let mut rename_handler =
20428 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
20429 let edit = lsp::TextEdit {
20430 range: lsp::Range {
20431 start: lsp::Position {
20432 line: 0,
20433 character: 7,
20434 },
20435 end: lsp::Position {
20436 line: 0,
20437 character: 10,
20438 },
20439 },
20440 new_text: "FooRenamed".to_string(),
20441 };
20442 Ok(Some(lsp::WorkspaceEdit::new(
20443 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
20444 )))
20445 });
20446 let rename_task = cx
20447 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
20448 .expect("Confirm rename was not started");
20449 rename_handler.next().await.unwrap();
20450 rename_task.await.expect("Confirm rename failed");
20451 cx.run_until_parked();
20452
20453 // Correct range is renamed, as `surrounding_word` is used to find it.
20454 cx.assert_editor_state(indoc! {"
20455 struct FooRenamedˇ {}
20456 "});
20457}
20458
20459#[gpui::test]
20460async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
20461 init_test(cx, |_| {});
20462 let mut cx = EditorTestContext::new(cx).await;
20463
20464 let language = Arc::new(
20465 Language::new(
20466 LanguageConfig::default(),
20467 Some(tree_sitter_html::LANGUAGE.into()),
20468 )
20469 .with_brackets_query(
20470 r#"
20471 ("<" @open "/>" @close)
20472 ("</" @open ">" @close)
20473 ("<" @open ">" @close)
20474 ("\"" @open "\"" @close)
20475 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
20476 "#,
20477 )
20478 .unwrap(),
20479 );
20480 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20481
20482 cx.set_state(indoc! {"
20483 <span>ˇ</span>
20484 "});
20485 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
20486 cx.assert_editor_state(indoc! {"
20487 <span>
20488 ˇ
20489 </span>
20490 "});
20491
20492 cx.set_state(indoc! {"
20493 <span><span></span>ˇ</span>
20494 "});
20495 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
20496 cx.assert_editor_state(indoc! {"
20497 <span><span></span>
20498 ˇ</span>
20499 "});
20500
20501 cx.set_state(indoc! {"
20502 <span>ˇ
20503 </span>
20504 "});
20505 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
20506 cx.assert_editor_state(indoc! {"
20507 <span>
20508 ˇ
20509 </span>
20510 "});
20511}
20512
20513#[gpui::test(iterations = 10)]
20514async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
20515 init_test(cx, |_| {});
20516
20517 let fs = FakeFs::new(cx.executor());
20518 fs.insert_tree(
20519 path!("/dir"),
20520 json!({
20521 "a.ts": "a",
20522 }),
20523 )
20524 .await;
20525
20526 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
20527 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20528 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20529
20530 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20531 language_registry.add(Arc::new(Language::new(
20532 LanguageConfig {
20533 name: "TypeScript".into(),
20534 matcher: LanguageMatcher {
20535 path_suffixes: vec!["ts".to_string()],
20536 ..Default::default()
20537 },
20538 ..Default::default()
20539 },
20540 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
20541 )));
20542 let mut fake_language_servers = language_registry.register_fake_lsp(
20543 "TypeScript",
20544 FakeLspAdapter {
20545 capabilities: lsp::ServerCapabilities {
20546 code_lens_provider: Some(lsp::CodeLensOptions {
20547 resolve_provider: Some(true),
20548 }),
20549 execute_command_provider: Some(lsp::ExecuteCommandOptions {
20550 commands: vec!["_the/command".to_string()],
20551 ..lsp::ExecuteCommandOptions::default()
20552 }),
20553 ..lsp::ServerCapabilities::default()
20554 },
20555 ..FakeLspAdapter::default()
20556 },
20557 );
20558
20559 let (buffer, _handle) = project
20560 .update(cx, |p, cx| {
20561 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
20562 })
20563 .await
20564 .unwrap();
20565 cx.executor().run_until_parked();
20566
20567 let fake_server = fake_language_servers.next().await.unwrap();
20568
20569 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
20570 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
20571 drop(buffer_snapshot);
20572 let actions = cx
20573 .update_window(*workspace, |_, window, cx| {
20574 project.code_actions(&buffer, anchor..anchor, window, cx)
20575 })
20576 .unwrap();
20577
20578 fake_server
20579 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
20580 Ok(Some(vec![
20581 lsp::CodeLens {
20582 range: lsp::Range::default(),
20583 command: Some(lsp::Command {
20584 title: "Code lens command".to_owned(),
20585 command: "_the/command".to_owned(),
20586 arguments: None,
20587 }),
20588 data: None,
20589 },
20590 lsp::CodeLens {
20591 range: lsp::Range::default(),
20592 command: Some(lsp::Command {
20593 title: "Command not in capabilities".to_owned(),
20594 command: "not in capabilities".to_owned(),
20595 arguments: None,
20596 }),
20597 data: None,
20598 },
20599 lsp::CodeLens {
20600 range: lsp::Range {
20601 start: lsp::Position {
20602 line: 1,
20603 character: 1,
20604 },
20605 end: lsp::Position {
20606 line: 1,
20607 character: 1,
20608 },
20609 },
20610 command: Some(lsp::Command {
20611 title: "Command not in range".to_owned(),
20612 command: "_the/command".to_owned(),
20613 arguments: None,
20614 }),
20615 data: None,
20616 },
20617 ]))
20618 })
20619 .next()
20620 .await;
20621
20622 let actions = actions.await.unwrap();
20623 assert_eq!(
20624 actions.len(),
20625 1,
20626 "Should have only one valid action for the 0..0 range"
20627 );
20628 let action = actions[0].clone();
20629 let apply = project.update(cx, |project, cx| {
20630 project.apply_code_action(buffer.clone(), action, true, cx)
20631 });
20632
20633 // Resolving the code action does not populate its edits. In absence of
20634 // edits, we must execute the given command.
20635 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
20636 |mut lens, _| async move {
20637 let lens_command = lens.command.as_mut().expect("should have a command");
20638 assert_eq!(lens_command.title, "Code lens command");
20639 lens_command.arguments = Some(vec![json!("the-argument")]);
20640 Ok(lens)
20641 },
20642 );
20643
20644 // While executing the command, the language server sends the editor
20645 // a `workspaceEdit` request.
20646 fake_server
20647 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
20648 let fake = fake_server.clone();
20649 move |params, _| {
20650 assert_eq!(params.command, "_the/command");
20651 let fake = fake.clone();
20652 async move {
20653 fake.server
20654 .request::<lsp::request::ApplyWorkspaceEdit>(
20655 lsp::ApplyWorkspaceEditParams {
20656 label: None,
20657 edit: lsp::WorkspaceEdit {
20658 changes: Some(
20659 [(
20660 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
20661 vec![lsp::TextEdit {
20662 range: lsp::Range::new(
20663 lsp::Position::new(0, 0),
20664 lsp::Position::new(0, 0),
20665 ),
20666 new_text: "X".into(),
20667 }],
20668 )]
20669 .into_iter()
20670 .collect(),
20671 ),
20672 ..Default::default()
20673 },
20674 },
20675 )
20676 .await
20677 .into_response()
20678 .unwrap();
20679 Ok(Some(json!(null)))
20680 }
20681 }
20682 })
20683 .next()
20684 .await;
20685
20686 // Applying the code lens command returns a project transaction containing the edits
20687 // sent by the language server in its `workspaceEdit` request.
20688 let transaction = apply.await.unwrap();
20689 assert!(transaction.0.contains_key(&buffer));
20690 buffer.update(cx, |buffer, cx| {
20691 assert_eq!(buffer.text(), "Xa");
20692 buffer.undo(cx);
20693 assert_eq!(buffer.text(), "a");
20694 });
20695}
20696
20697#[gpui::test]
20698async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
20699 init_test(cx, |_| {});
20700
20701 let fs = FakeFs::new(cx.executor());
20702 let main_text = r#"fn main() {
20703println!("1");
20704println!("2");
20705println!("3");
20706println!("4");
20707println!("5");
20708}"#;
20709 let lib_text = "mod foo {}";
20710 fs.insert_tree(
20711 path!("/a"),
20712 json!({
20713 "lib.rs": lib_text,
20714 "main.rs": main_text,
20715 }),
20716 )
20717 .await;
20718
20719 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20720 let (workspace, cx) =
20721 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20722 let worktree_id = workspace.update(cx, |workspace, cx| {
20723 workspace.project().update(cx, |project, cx| {
20724 project.worktrees(cx).next().unwrap().read(cx).id()
20725 })
20726 });
20727
20728 let expected_ranges = vec![
20729 Point::new(0, 0)..Point::new(0, 0),
20730 Point::new(1, 0)..Point::new(1, 1),
20731 Point::new(2, 0)..Point::new(2, 2),
20732 Point::new(3, 0)..Point::new(3, 3),
20733 ];
20734
20735 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20736 let editor_1 = workspace
20737 .update_in(cx, |workspace, window, cx| {
20738 workspace.open_path(
20739 (worktree_id, "main.rs"),
20740 Some(pane_1.downgrade()),
20741 true,
20742 window,
20743 cx,
20744 )
20745 })
20746 .unwrap()
20747 .await
20748 .downcast::<Editor>()
20749 .unwrap();
20750 pane_1.update(cx, |pane, cx| {
20751 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20752 open_editor.update(cx, |editor, cx| {
20753 assert_eq!(
20754 editor.display_text(cx),
20755 main_text,
20756 "Original main.rs text on initial open",
20757 );
20758 assert_eq!(
20759 editor
20760 .selections
20761 .all::<Point>(cx)
20762 .into_iter()
20763 .map(|s| s.range())
20764 .collect::<Vec<_>>(),
20765 vec![Point::zero()..Point::zero()],
20766 "Default selections on initial open",
20767 );
20768 })
20769 });
20770 editor_1.update_in(cx, |editor, window, cx| {
20771 editor.change_selections(None, window, cx, |s| {
20772 s.select_ranges(expected_ranges.clone());
20773 });
20774 });
20775
20776 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
20777 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
20778 });
20779 let editor_2 = workspace
20780 .update_in(cx, |workspace, window, cx| {
20781 workspace.open_path(
20782 (worktree_id, "main.rs"),
20783 Some(pane_2.downgrade()),
20784 true,
20785 window,
20786 cx,
20787 )
20788 })
20789 .unwrap()
20790 .await
20791 .downcast::<Editor>()
20792 .unwrap();
20793 pane_2.update(cx, |pane, cx| {
20794 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20795 open_editor.update(cx, |editor, cx| {
20796 assert_eq!(
20797 editor.display_text(cx),
20798 main_text,
20799 "Original main.rs text on initial open in another panel",
20800 );
20801 assert_eq!(
20802 editor
20803 .selections
20804 .all::<Point>(cx)
20805 .into_iter()
20806 .map(|s| s.range())
20807 .collect::<Vec<_>>(),
20808 vec![Point::zero()..Point::zero()],
20809 "Default selections on initial open in another panel",
20810 );
20811 })
20812 });
20813
20814 editor_2.update_in(cx, |editor, window, cx| {
20815 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
20816 });
20817
20818 let _other_editor_1 = workspace
20819 .update_in(cx, |workspace, window, cx| {
20820 workspace.open_path(
20821 (worktree_id, "lib.rs"),
20822 Some(pane_1.downgrade()),
20823 true,
20824 window,
20825 cx,
20826 )
20827 })
20828 .unwrap()
20829 .await
20830 .downcast::<Editor>()
20831 .unwrap();
20832 pane_1
20833 .update_in(cx, |pane, window, cx| {
20834 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
20835 })
20836 .await
20837 .unwrap();
20838 drop(editor_1);
20839 pane_1.update(cx, |pane, cx| {
20840 pane.active_item()
20841 .unwrap()
20842 .downcast::<Editor>()
20843 .unwrap()
20844 .update(cx, |editor, cx| {
20845 assert_eq!(
20846 editor.display_text(cx),
20847 lib_text,
20848 "Other file should be open and active",
20849 );
20850 });
20851 assert_eq!(pane.items().count(), 1, "No other editors should be open");
20852 });
20853
20854 let _other_editor_2 = workspace
20855 .update_in(cx, |workspace, window, cx| {
20856 workspace.open_path(
20857 (worktree_id, "lib.rs"),
20858 Some(pane_2.downgrade()),
20859 true,
20860 window,
20861 cx,
20862 )
20863 })
20864 .unwrap()
20865 .await
20866 .downcast::<Editor>()
20867 .unwrap();
20868 pane_2
20869 .update_in(cx, |pane, window, cx| {
20870 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
20871 })
20872 .await
20873 .unwrap();
20874 drop(editor_2);
20875 pane_2.update(cx, |pane, cx| {
20876 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20877 open_editor.update(cx, |editor, cx| {
20878 assert_eq!(
20879 editor.display_text(cx),
20880 lib_text,
20881 "Other file should be open and active in another panel too",
20882 );
20883 });
20884 assert_eq!(
20885 pane.items().count(),
20886 1,
20887 "No other editors should be open in another pane",
20888 );
20889 });
20890
20891 let _editor_1_reopened = workspace
20892 .update_in(cx, |workspace, window, cx| {
20893 workspace.open_path(
20894 (worktree_id, "main.rs"),
20895 Some(pane_1.downgrade()),
20896 true,
20897 window,
20898 cx,
20899 )
20900 })
20901 .unwrap()
20902 .await
20903 .downcast::<Editor>()
20904 .unwrap();
20905 let _editor_2_reopened = workspace
20906 .update_in(cx, |workspace, window, cx| {
20907 workspace.open_path(
20908 (worktree_id, "main.rs"),
20909 Some(pane_2.downgrade()),
20910 true,
20911 window,
20912 cx,
20913 )
20914 })
20915 .unwrap()
20916 .await
20917 .downcast::<Editor>()
20918 .unwrap();
20919 pane_1.update(cx, |pane, cx| {
20920 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20921 open_editor.update(cx, |editor, cx| {
20922 assert_eq!(
20923 editor.display_text(cx),
20924 main_text,
20925 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
20926 );
20927 assert_eq!(
20928 editor
20929 .selections
20930 .all::<Point>(cx)
20931 .into_iter()
20932 .map(|s| s.range())
20933 .collect::<Vec<_>>(),
20934 expected_ranges,
20935 "Previous editor in the 1st panel had selections and should get them restored on reopen",
20936 );
20937 })
20938 });
20939 pane_2.update(cx, |pane, cx| {
20940 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20941 open_editor.update(cx, |editor, cx| {
20942 assert_eq!(
20943 editor.display_text(cx),
20944 r#"fn main() {
20945⋯rintln!("1");
20946⋯intln!("2");
20947⋯ntln!("3");
20948println!("4");
20949println!("5");
20950}"#,
20951 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
20952 );
20953 assert_eq!(
20954 editor
20955 .selections
20956 .all::<Point>(cx)
20957 .into_iter()
20958 .map(|s| s.range())
20959 .collect::<Vec<_>>(),
20960 vec![Point::zero()..Point::zero()],
20961 "Previous editor in the 2nd pane had no selections changed hence should restore none",
20962 );
20963 })
20964 });
20965}
20966
20967#[gpui::test]
20968async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
20969 init_test(cx, |_| {});
20970
20971 let fs = FakeFs::new(cx.executor());
20972 let main_text = r#"fn main() {
20973println!("1");
20974println!("2");
20975println!("3");
20976println!("4");
20977println!("5");
20978}"#;
20979 let lib_text = "mod foo {}";
20980 fs.insert_tree(
20981 path!("/a"),
20982 json!({
20983 "lib.rs": lib_text,
20984 "main.rs": main_text,
20985 }),
20986 )
20987 .await;
20988
20989 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20990 let (workspace, cx) =
20991 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20992 let worktree_id = workspace.update(cx, |workspace, cx| {
20993 workspace.project().update(cx, |project, cx| {
20994 project.worktrees(cx).next().unwrap().read(cx).id()
20995 })
20996 });
20997
20998 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20999 let editor = workspace
21000 .update_in(cx, |workspace, window, cx| {
21001 workspace.open_path(
21002 (worktree_id, "main.rs"),
21003 Some(pane.downgrade()),
21004 true,
21005 window,
21006 cx,
21007 )
21008 })
21009 .unwrap()
21010 .await
21011 .downcast::<Editor>()
21012 .unwrap();
21013 pane.update(cx, |pane, cx| {
21014 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21015 open_editor.update(cx, |editor, cx| {
21016 assert_eq!(
21017 editor.display_text(cx),
21018 main_text,
21019 "Original main.rs text on initial open",
21020 );
21021 })
21022 });
21023 editor.update_in(cx, |editor, window, cx| {
21024 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
21025 });
21026
21027 cx.update_global(|store: &mut SettingsStore, cx| {
21028 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21029 s.restore_on_file_reopen = Some(false);
21030 });
21031 });
21032 editor.update_in(cx, |editor, window, cx| {
21033 editor.fold_ranges(
21034 vec![
21035 Point::new(1, 0)..Point::new(1, 1),
21036 Point::new(2, 0)..Point::new(2, 2),
21037 Point::new(3, 0)..Point::new(3, 3),
21038 ],
21039 false,
21040 window,
21041 cx,
21042 );
21043 });
21044 pane.update_in(cx, |pane, window, cx| {
21045 pane.close_all_items(&CloseAllItems::default(), window, cx)
21046 })
21047 .await
21048 .unwrap();
21049 pane.update(cx, |pane, _| {
21050 assert!(pane.active_item().is_none());
21051 });
21052 cx.update_global(|store: &mut SettingsStore, cx| {
21053 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21054 s.restore_on_file_reopen = Some(true);
21055 });
21056 });
21057
21058 let _editor_reopened = workspace
21059 .update_in(cx, |workspace, window, cx| {
21060 workspace.open_path(
21061 (worktree_id, "main.rs"),
21062 Some(pane.downgrade()),
21063 true,
21064 window,
21065 cx,
21066 )
21067 })
21068 .unwrap()
21069 .await
21070 .downcast::<Editor>()
21071 .unwrap();
21072 pane.update(cx, |pane, cx| {
21073 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21074 open_editor.update(cx, |editor, cx| {
21075 assert_eq!(
21076 editor.display_text(cx),
21077 main_text,
21078 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
21079 );
21080 })
21081 });
21082}
21083
21084#[gpui::test]
21085async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
21086 struct EmptyModalView {
21087 focus_handle: gpui::FocusHandle,
21088 }
21089 impl EventEmitter<DismissEvent> for EmptyModalView {}
21090 impl Render for EmptyModalView {
21091 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
21092 div()
21093 }
21094 }
21095 impl Focusable for EmptyModalView {
21096 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
21097 self.focus_handle.clone()
21098 }
21099 }
21100 impl workspace::ModalView for EmptyModalView {}
21101 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
21102 EmptyModalView {
21103 focus_handle: cx.focus_handle(),
21104 }
21105 }
21106
21107 init_test(cx, |_| {});
21108
21109 let fs = FakeFs::new(cx.executor());
21110 let project = Project::test(fs, [], cx).await;
21111 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21112 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
21113 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21114 let editor = cx.new_window_entity(|window, cx| {
21115 Editor::new(
21116 EditorMode::full(),
21117 buffer,
21118 Some(project.clone()),
21119 window,
21120 cx,
21121 )
21122 });
21123 workspace
21124 .update(cx, |workspace, window, cx| {
21125 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
21126 })
21127 .unwrap();
21128 editor.update_in(cx, |editor, window, cx| {
21129 editor.open_context_menu(&OpenContextMenu, window, cx);
21130 assert!(editor.mouse_context_menu.is_some());
21131 });
21132 workspace
21133 .update(cx, |workspace, window, cx| {
21134 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
21135 })
21136 .unwrap();
21137 cx.read(|cx| {
21138 assert!(editor.read(cx).mouse_context_menu.is_none());
21139 });
21140}
21141
21142#[gpui::test]
21143async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
21144 init_test(cx, |_| {});
21145
21146 let fs = FakeFs::new(cx.executor());
21147 fs.insert_file(path!("/file.html"), Default::default())
21148 .await;
21149
21150 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
21151
21152 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21153 let html_language = Arc::new(Language::new(
21154 LanguageConfig {
21155 name: "HTML".into(),
21156 matcher: LanguageMatcher {
21157 path_suffixes: vec!["html".to_string()],
21158 ..LanguageMatcher::default()
21159 },
21160 brackets: BracketPairConfig {
21161 pairs: vec![BracketPair {
21162 start: "<".into(),
21163 end: ">".into(),
21164 close: true,
21165 ..Default::default()
21166 }],
21167 ..Default::default()
21168 },
21169 ..Default::default()
21170 },
21171 Some(tree_sitter_html::LANGUAGE.into()),
21172 ));
21173 language_registry.add(html_language);
21174 let mut fake_servers = language_registry.register_fake_lsp(
21175 "HTML",
21176 FakeLspAdapter {
21177 capabilities: lsp::ServerCapabilities {
21178 completion_provider: Some(lsp::CompletionOptions {
21179 resolve_provider: Some(true),
21180 ..Default::default()
21181 }),
21182 ..Default::default()
21183 },
21184 ..Default::default()
21185 },
21186 );
21187
21188 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21189 let cx = &mut VisualTestContext::from_window(*workspace, cx);
21190
21191 let worktree_id = workspace
21192 .update(cx, |workspace, _window, cx| {
21193 workspace.project().update(cx, |project, cx| {
21194 project.worktrees(cx).next().unwrap().read(cx).id()
21195 })
21196 })
21197 .unwrap();
21198 project
21199 .update(cx, |project, cx| {
21200 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
21201 })
21202 .await
21203 .unwrap();
21204 let editor = workspace
21205 .update(cx, |workspace, window, cx| {
21206 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
21207 })
21208 .unwrap()
21209 .await
21210 .unwrap()
21211 .downcast::<Editor>()
21212 .unwrap();
21213
21214 let fake_server = fake_servers.next().await.unwrap();
21215 editor.update_in(cx, |editor, window, cx| {
21216 editor.set_text("<ad></ad>", window, cx);
21217 editor.change_selections(None, window, cx, |selections| {
21218 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
21219 });
21220 let Some((buffer, _)) = editor
21221 .buffer
21222 .read(cx)
21223 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
21224 else {
21225 panic!("Failed to get buffer for selection position");
21226 };
21227 let buffer = buffer.read(cx);
21228 let buffer_id = buffer.remote_id();
21229 let opening_range =
21230 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
21231 let closing_range =
21232 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
21233 let mut linked_ranges = HashMap::default();
21234 linked_ranges.insert(
21235 buffer_id,
21236 vec![(opening_range.clone(), vec![closing_range.clone()])],
21237 );
21238 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
21239 });
21240 let mut completion_handle =
21241 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
21242 Ok(Some(lsp::CompletionResponse::Array(vec![
21243 lsp::CompletionItem {
21244 label: "head".to_string(),
21245 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
21246 lsp::InsertReplaceEdit {
21247 new_text: "head".to_string(),
21248 insert: lsp::Range::new(
21249 lsp::Position::new(0, 1),
21250 lsp::Position::new(0, 3),
21251 ),
21252 replace: lsp::Range::new(
21253 lsp::Position::new(0, 1),
21254 lsp::Position::new(0, 3),
21255 ),
21256 },
21257 )),
21258 ..Default::default()
21259 },
21260 ])))
21261 });
21262 editor.update_in(cx, |editor, window, cx| {
21263 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
21264 });
21265 cx.run_until_parked();
21266 completion_handle.next().await.unwrap();
21267 editor.update(cx, |editor, _| {
21268 assert!(
21269 editor.context_menu_visible(),
21270 "Completion menu should be visible"
21271 );
21272 });
21273 editor.update_in(cx, |editor, window, cx| {
21274 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
21275 });
21276 cx.executor().run_until_parked();
21277 editor.update(cx, |editor, cx| {
21278 assert_eq!(editor.text(cx), "<head></head>");
21279 });
21280}
21281
21282#[gpui::test]
21283async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
21284 init_test(cx, |_| {});
21285
21286 let fs = FakeFs::new(cx.executor());
21287 fs.insert_tree(
21288 path!("/root"),
21289 json!({
21290 "a": {
21291 "main.rs": "fn main() {}",
21292 },
21293 "foo": {
21294 "bar": {
21295 "external_file.rs": "pub mod external {}",
21296 }
21297 }
21298 }),
21299 )
21300 .await;
21301
21302 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
21303 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21304 language_registry.add(rust_lang());
21305 let _fake_servers = language_registry.register_fake_lsp(
21306 "Rust",
21307 FakeLspAdapter {
21308 ..FakeLspAdapter::default()
21309 },
21310 );
21311 let (workspace, cx) =
21312 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21313 let worktree_id = workspace.update(cx, |workspace, cx| {
21314 workspace.project().update(cx, |project, cx| {
21315 project.worktrees(cx).next().unwrap().read(cx).id()
21316 })
21317 });
21318
21319 let assert_language_servers_count =
21320 |expected: usize, context: &str, cx: &mut VisualTestContext| {
21321 project.update(cx, |project, cx| {
21322 let current = project
21323 .lsp_store()
21324 .read(cx)
21325 .as_local()
21326 .unwrap()
21327 .language_servers
21328 .len();
21329 assert_eq!(expected, current, "{context}");
21330 });
21331 };
21332
21333 assert_language_servers_count(
21334 0,
21335 "No servers should be running before any file is open",
21336 cx,
21337 );
21338 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21339 let main_editor = workspace
21340 .update_in(cx, |workspace, window, cx| {
21341 workspace.open_path(
21342 (worktree_id, "main.rs"),
21343 Some(pane.downgrade()),
21344 true,
21345 window,
21346 cx,
21347 )
21348 })
21349 .unwrap()
21350 .await
21351 .downcast::<Editor>()
21352 .unwrap();
21353 pane.update(cx, |pane, cx| {
21354 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21355 open_editor.update(cx, |editor, cx| {
21356 assert_eq!(
21357 editor.display_text(cx),
21358 "fn main() {}",
21359 "Original main.rs text on initial open",
21360 );
21361 });
21362 assert_eq!(open_editor, main_editor);
21363 });
21364 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
21365
21366 let external_editor = workspace
21367 .update_in(cx, |workspace, window, cx| {
21368 workspace.open_abs_path(
21369 PathBuf::from("/root/foo/bar/external_file.rs"),
21370 OpenOptions::default(),
21371 window,
21372 cx,
21373 )
21374 })
21375 .await
21376 .expect("opening external file")
21377 .downcast::<Editor>()
21378 .expect("downcasted external file's open element to editor");
21379 pane.update(cx, |pane, cx| {
21380 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21381 open_editor.update(cx, |editor, cx| {
21382 assert_eq!(
21383 editor.display_text(cx),
21384 "pub mod external {}",
21385 "External file is open now",
21386 );
21387 });
21388 assert_eq!(open_editor, external_editor);
21389 });
21390 assert_language_servers_count(
21391 1,
21392 "Second, external, *.rs file should join the existing server",
21393 cx,
21394 );
21395
21396 pane.update_in(cx, |pane, window, cx| {
21397 pane.close_active_item(&CloseActiveItem::default(), window, cx)
21398 })
21399 .await
21400 .unwrap();
21401 pane.update_in(cx, |pane, window, cx| {
21402 pane.navigate_backward(window, cx);
21403 });
21404 cx.run_until_parked();
21405 pane.update(cx, |pane, cx| {
21406 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21407 open_editor.update(cx, |editor, cx| {
21408 assert_eq!(
21409 editor.display_text(cx),
21410 "pub mod external {}",
21411 "External file is open now",
21412 );
21413 });
21414 });
21415 assert_language_servers_count(
21416 1,
21417 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
21418 cx,
21419 );
21420
21421 cx.update(|_, cx| {
21422 workspace::reload(&workspace::Reload::default(), cx);
21423 });
21424 assert_language_servers_count(
21425 1,
21426 "After reloading the worktree with local and external files opened, only one project should be started",
21427 cx,
21428 );
21429}
21430
21431#[gpui::test]
21432async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
21433 init_test(cx, |_| {});
21434
21435 let mut cx = EditorTestContext::new(cx).await;
21436 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
21437 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21438
21439 // test cursor move to start of each line on tab
21440 // for `if`, `elif`, `else`, `while`, `with` and `for`
21441 cx.set_state(indoc! {"
21442 def main():
21443 ˇ for item in items:
21444 ˇ while item.active:
21445 ˇ if item.value > 10:
21446 ˇ continue
21447 ˇ elif item.value < 0:
21448 ˇ break
21449 ˇ else:
21450 ˇ with item.context() as ctx:
21451 ˇ yield count
21452 ˇ else:
21453 ˇ log('while else')
21454 ˇ else:
21455 ˇ log('for else')
21456 "});
21457 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21458 cx.assert_editor_state(indoc! {"
21459 def main():
21460 ˇfor item in items:
21461 ˇwhile item.active:
21462 ˇif item.value > 10:
21463 ˇcontinue
21464 ˇelif item.value < 0:
21465 ˇbreak
21466 ˇelse:
21467 ˇwith item.context() as ctx:
21468 ˇyield count
21469 ˇelse:
21470 ˇlog('while else')
21471 ˇelse:
21472 ˇlog('for else')
21473 "});
21474 // test relative indent is preserved when tab
21475 // for `if`, `elif`, `else`, `while`, `with` and `for`
21476 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21477 cx.assert_editor_state(indoc! {"
21478 def main():
21479 ˇfor item in items:
21480 ˇwhile item.active:
21481 ˇif item.value > 10:
21482 ˇcontinue
21483 ˇelif item.value < 0:
21484 ˇbreak
21485 ˇelse:
21486 ˇwith item.context() as ctx:
21487 ˇyield count
21488 ˇelse:
21489 ˇlog('while else')
21490 ˇelse:
21491 ˇlog('for else')
21492 "});
21493
21494 // test cursor move to start of each line on tab
21495 // for `try`, `except`, `else`, `finally`, `match` and `def`
21496 cx.set_state(indoc! {"
21497 def main():
21498 ˇ try:
21499 ˇ fetch()
21500 ˇ except ValueError:
21501 ˇ handle_error()
21502 ˇ else:
21503 ˇ match value:
21504 ˇ case _:
21505 ˇ finally:
21506 ˇ def status():
21507 ˇ return 0
21508 "});
21509 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21510 cx.assert_editor_state(indoc! {"
21511 def main():
21512 ˇtry:
21513 ˇfetch()
21514 ˇexcept ValueError:
21515 ˇhandle_error()
21516 ˇelse:
21517 ˇmatch value:
21518 ˇcase _:
21519 ˇfinally:
21520 ˇdef status():
21521 ˇreturn 0
21522 "});
21523 // test relative indent is preserved when tab
21524 // for `try`, `except`, `else`, `finally`, `match` and `def`
21525 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21526 cx.assert_editor_state(indoc! {"
21527 def main():
21528 ˇtry:
21529 ˇfetch()
21530 ˇexcept ValueError:
21531 ˇhandle_error()
21532 ˇelse:
21533 ˇmatch value:
21534 ˇcase _:
21535 ˇfinally:
21536 ˇdef status():
21537 ˇreturn 0
21538 "});
21539}
21540
21541#[gpui::test]
21542async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
21543 init_test(cx, |_| {});
21544
21545 let mut cx = EditorTestContext::new(cx).await;
21546 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
21547 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21548
21549 // test `else` auto outdents when typed inside `if` block
21550 cx.set_state(indoc! {"
21551 def main():
21552 if i == 2:
21553 return
21554 ˇ
21555 "});
21556 cx.update_editor(|editor, window, cx| {
21557 editor.handle_input("else:", window, cx);
21558 });
21559 cx.assert_editor_state(indoc! {"
21560 def main():
21561 if i == 2:
21562 return
21563 else:ˇ
21564 "});
21565
21566 // test `except` auto outdents when typed inside `try` block
21567 cx.set_state(indoc! {"
21568 def main():
21569 try:
21570 i = 2
21571 ˇ
21572 "});
21573 cx.update_editor(|editor, window, cx| {
21574 editor.handle_input("except:", window, cx);
21575 });
21576 cx.assert_editor_state(indoc! {"
21577 def main():
21578 try:
21579 i = 2
21580 except:ˇ
21581 "});
21582
21583 // test `else` auto outdents when typed inside `except` block
21584 cx.set_state(indoc! {"
21585 def main():
21586 try:
21587 i = 2
21588 except:
21589 j = 2
21590 ˇ
21591 "});
21592 cx.update_editor(|editor, window, cx| {
21593 editor.handle_input("else:", window, cx);
21594 });
21595 cx.assert_editor_state(indoc! {"
21596 def main():
21597 try:
21598 i = 2
21599 except:
21600 j = 2
21601 else:ˇ
21602 "});
21603
21604 // test `finally` auto outdents when typed inside `else` block
21605 cx.set_state(indoc! {"
21606 def main():
21607 try:
21608 i = 2
21609 except:
21610 j = 2
21611 else:
21612 k = 2
21613 ˇ
21614 "});
21615 cx.update_editor(|editor, window, cx| {
21616 editor.handle_input("finally:", window, cx);
21617 });
21618 cx.assert_editor_state(indoc! {"
21619 def main():
21620 try:
21621 i = 2
21622 except:
21623 j = 2
21624 else:
21625 k = 2
21626 finally:ˇ
21627 "});
21628
21629 // TODO: test `except` auto outdents when typed inside `try` block right after for block
21630 // cx.set_state(indoc! {"
21631 // def main():
21632 // try:
21633 // for i in range(n):
21634 // pass
21635 // ˇ
21636 // "});
21637 // cx.update_editor(|editor, window, cx| {
21638 // editor.handle_input("except:", window, cx);
21639 // });
21640 // cx.assert_editor_state(indoc! {"
21641 // def main():
21642 // try:
21643 // for i in range(n):
21644 // pass
21645 // except:ˇ
21646 // "});
21647
21648 // TODO: test `else` auto outdents when typed inside `except` block right after for block
21649 // cx.set_state(indoc! {"
21650 // def main():
21651 // try:
21652 // i = 2
21653 // except:
21654 // for i in range(n):
21655 // pass
21656 // ˇ
21657 // "});
21658 // cx.update_editor(|editor, window, cx| {
21659 // editor.handle_input("else:", window, cx);
21660 // });
21661 // cx.assert_editor_state(indoc! {"
21662 // def main():
21663 // try:
21664 // i = 2
21665 // except:
21666 // for i in range(n):
21667 // pass
21668 // else:ˇ
21669 // "});
21670
21671 // TODO: test `finally` auto outdents when typed inside `else` block right after for block
21672 // cx.set_state(indoc! {"
21673 // def main():
21674 // try:
21675 // i = 2
21676 // except:
21677 // j = 2
21678 // else:
21679 // for i in range(n):
21680 // pass
21681 // ˇ
21682 // "});
21683 // cx.update_editor(|editor, window, cx| {
21684 // editor.handle_input("finally:", window, cx);
21685 // });
21686 // cx.assert_editor_state(indoc! {"
21687 // def main():
21688 // try:
21689 // i = 2
21690 // except:
21691 // j = 2
21692 // else:
21693 // for i in range(n):
21694 // pass
21695 // finally:ˇ
21696 // "});
21697
21698 // test `else` stays at correct indent when typed after `for` block
21699 cx.set_state(indoc! {"
21700 def main():
21701 for i in range(10):
21702 if i == 3:
21703 break
21704 ˇ
21705 "});
21706 cx.update_editor(|editor, window, cx| {
21707 editor.handle_input("else:", window, cx);
21708 });
21709 cx.assert_editor_state(indoc! {"
21710 def main():
21711 for i in range(10):
21712 if i == 3:
21713 break
21714 else:ˇ
21715 "});
21716
21717 // test does not outdent on typing after line with square brackets
21718 cx.set_state(indoc! {"
21719 def f() -> list[str]:
21720 ˇ
21721 "});
21722 cx.update_editor(|editor, window, cx| {
21723 editor.handle_input("a", window, cx);
21724 });
21725 cx.assert_editor_state(indoc! {"
21726 def f() -> list[str]:
21727 aˇ
21728 "});
21729}
21730
21731#[gpui::test]
21732async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
21733 init_test(cx, |_| {});
21734 update_test_language_settings(cx, |settings| {
21735 settings.defaults.extend_comment_on_newline = Some(false);
21736 });
21737 let mut cx = EditorTestContext::new(cx).await;
21738 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
21739 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21740
21741 // test correct indent after newline on comment
21742 cx.set_state(indoc! {"
21743 # COMMENT:ˇ
21744 "});
21745 cx.update_editor(|editor, window, cx| {
21746 editor.newline(&Newline, window, cx);
21747 });
21748 cx.assert_editor_state(indoc! {"
21749 # COMMENT:
21750 ˇ
21751 "});
21752
21753 // test correct indent after newline in brackets
21754 cx.set_state(indoc! {"
21755 {ˇ}
21756 "});
21757 cx.update_editor(|editor, window, cx| {
21758 editor.newline(&Newline, window, cx);
21759 });
21760 cx.run_until_parked();
21761 cx.assert_editor_state(indoc! {"
21762 {
21763 ˇ
21764 }
21765 "});
21766
21767 cx.set_state(indoc! {"
21768 (ˇ)
21769 "});
21770 cx.update_editor(|editor, window, cx| {
21771 editor.newline(&Newline, window, cx);
21772 });
21773 cx.run_until_parked();
21774 cx.assert_editor_state(indoc! {"
21775 (
21776 ˇ
21777 )
21778 "});
21779
21780 // do not indent after empty lists or dictionaries
21781 cx.set_state(indoc! {"
21782 a = []ˇ
21783 "});
21784 cx.update_editor(|editor, window, cx| {
21785 editor.newline(&Newline, window, cx);
21786 });
21787 cx.run_until_parked();
21788 cx.assert_editor_state(indoc! {"
21789 a = []
21790 ˇ
21791 "});
21792}
21793
21794fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
21795 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
21796 point..point
21797}
21798
21799#[track_caller]
21800fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
21801 let (text, ranges) = marked_text_ranges(marked_text, true);
21802 assert_eq!(editor.text(cx), text);
21803 assert_eq!(
21804 editor.selections.ranges(cx),
21805 ranges,
21806 "Assert selections are {}",
21807 marked_text
21808 );
21809}
21810
21811pub fn handle_signature_help_request(
21812 cx: &mut EditorLspTestContext,
21813 mocked_response: lsp::SignatureHelp,
21814) -> impl Future<Output = ()> + use<> {
21815 let mut request =
21816 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
21817 let mocked_response = mocked_response.clone();
21818 async move { Ok(Some(mocked_response)) }
21819 });
21820
21821 async move {
21822 request.next().await;
21823 }
21824}
21825
21826#[track_caller]
21827pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
21828 cx.update_editor(|editor, _, _| {
21829 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
21830 let entries = menu.entries.borrow();
21831 let entries = entries
21832 .iter()
21833 .map(|entry| entry.string.as_str())
21834 .collect::<Vec<_>>();
21835 assert_eq!(entries, expected);
21836 } else {
21837 panic!("Expected completions menu");
21838 }
21839 });
21840}
21841
21842/// Handle completion request passing a marked string specifying where the completion
21843/// should be triggered from using '|' character, what range should be replaced, and what completions
21844/// should be returned using '<' and '>' to delimit the range.
21845///
21846/// Also see `handle_completion_request_with_insert_and_replace`.
21847#[track_caller]
21848pub fn handle_completion_request(
21849 marked_string: &str,
21850 completions: Vec<&'static str>,
21851 is_incomplete: bool,
21852 counter: Arc<AtomicUsize>,
21853 cx: &mut EditorLspTestContext,
21854) -> impl Future<Output = ()> {
21855 let complete_from_marker: TextRangeMarker = '|'.into();
21856 let replace_range_marker: TextRangeMarker = ('<', '>').into();
21857 let (_, mut marked_ranges) = marked_text_ranges_by(
21858 marked_string,
21859 vec![complete_from_marker.clone(), replace_range_marker.clone()],
21860 );
21861
21862 let complete_from_position =
21863 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
21864 let replace_range =
21865 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
21866
21867 let mut request =
21868 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
21869 let completions = completions.clone();
21870 counter.fetch_add(1, atomic::Ordering::Release);
21871 async move {
21872 assert_eq!(params.text_document_position.text_document.uri, url.clone());
21873 assert_eq!(
21874 params.text_document_position.position,
21875 complete_from_position
21876 );
21877 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
21878 is_incomplete: is_incomplete,
21879 item_defaults: None,
21880 items: completions
21881 .iter()
21882 .map(|completion_text| lsp::CompletionItem {
21883 label: completion_text.to_string(),
21884 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
21885 range: replace_range,
21886 new_text: completion_text.to_string(),
21887 })),
21888 ..Default::default()
21889 })
21890 .collect(),
21891 })))
21892 }
21893 });
21894
21895 async move {
21896 request.next().await;
21897 }
21898}
21899
21900/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
21901/// given instead, which also contains an `insert` range.
21902///
21903/// This function uses markers to define ranges:
21904/// - `|` marks the cursor position
21905/// - `<>` marks the replace range
21906/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
21907pub fn handle_completion_request_with_insert_and_replace(
21908 cx: &mut EditorLspTestContext,
21909 marked_string: &str,
21910 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
21911 counter: Arc<AtomicUsize>,
21912) -> impl Future<Output = ()> {
21913 let complete_from_marker: TextRangeMarker = '|'.into();
21914 let replace_range_marker: TextRangeMarker = ('<', '>').into();
21915 let insert_range_marker: TextRangeMarker = ('{', '}').into();
21916
21917 let (_, mut marked_ranges) = marked_text_ranges_by(
21918 marked_string,
21919 vec![
21920 complete_from_marker.clone(),
21921 replace_range_marker.clone(),
21922 insert_range_marker.clone(),
21923 ],
21924 );
21925
21926 let complete_from_position =
21927 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
21928 let replace_range =
21929 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
21930
21931 let insert_range = match marked_ranges.remove(&insert_range_marker) {
21932 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
21933 _ => lsp::Range {
21934 start: replace_range.start,
21935 end: complete_from_position,
21936 },
21937 };
21938
21939 let mut request =
21940 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
21941 let completions = completions.clone();
21942 counter.fetch_add(1, atomic::Ordering::Release);
21943 async move {
21944 assert_eq!(params.text_document_position.text_document.uri, url.clone());
21945 assert_eq!(
21946 params.text_document_position.position, complete_from_position,
21947 "marker `|` position doesn't match",
21948 );
21949 Ok(Some(lsp::CompletionResponse::Array(
21950 completions
21951 .iter()
21952 .map(|(label, new_text)| lsp::CompletionItem {
21953 label: label.to_string(),
21954 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
21955 lsp::InsertReplaceEdit {
21956 insert: insert_range,
21957 replace: replace_range,
21958 new_text: new_text.to_string(),
21959 },
21960 )),
21961 ..Default::default()
21962 })
21963 .collect(),
21964 )))
21965 }
21966 });
21967
21968 async move {
21969 request.next().await;
21970 }
21971}
21972
21973fn handle_resolve_completion_request(
21974 cx: &mut EditorLspTestContext,
21975 edits: Option<Vec<(&'static str, &'static str)>>,
21976) -> impl Future<Output = ()> {
21977 let edits = edits.map(|edits| {
21978 edits
21979 .iter()
21980 .map(|(marked_string, new_text)| {
21981 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
21982 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
21983 lsp::TextEdit::new(replace_range, new_text.to_string())
21984 })
21985 .collect::<Vec<_>>()
21986 });
21987
21988 let mut request =
21989 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
21990 let edits = edits.clone();
21991 async move {
21992 Ok(lsp::CompletionItem {
21993 additional_text_edits: edits,
21994 ..Default::default()
21995 })
21996 }
21997 });
21998
21999 async move {
22000 request.next().await;
22001 }
22002}
22003
22004pub(crate) fn update_test_language_settings(
22005 cx: &mut TestAppContext,
22006 f: impl Fn(&mut AllLanguageSettingsContent),
22007) {
22008 cx.update(|cx| {
22009 SettingsStore::update_global(cx, |store, cx| {
22010 store.update_user_settings::<AllLanguageSettings>(cx, f);
22011 });
22012 });
22013}
22014
22015pub(crate) fn update_test_project_settings(
22016 cx: &mut TestAppContext,
22017 f: impl Fn(&mut ProjectSettings),
22018) {
22019 cx.update(|cx| {
22020 SettingsStore::update_global(cx, |store, cx| {
22021 store.update_user_settings::<ProjectSettings>(cx, f);
22022 });
22023 });
22024}
22025
22026pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
22027 cx.update(|cx| {
22028 assets::Assets.load_test_fonts(cx);
22029 let store = SettingsStore::test(cx);
22030 cx.set_global(store);
22031 theme::init(theme::LoadThemes::JustBase, cx);
22032 release_channel::init(SemanticVersion::default(), cx);
22033 client::init_settings(cx);
22034 language::init(cx);
22035 Project::init_settings(cx);
22036 workspace::init_settings(cx);
22037 crate::init(cx);
22038 });
22039
22040 update_test_language_settings(cx, f);
22041}
22042
22043#[track_caller]
22044fn assert_hunk_revert(
22045 not_reverted_text_with_selections: &str,
22046 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
22047 expected_reverted_text_with_selections: &str,
22048 base_text: &str,
22049 cx: &mut EditorLspTestContext,
22050) {
22051 cx.set_state(not_reverted_text_with_selections);
22052 cx.set_head_text(base_text);
22053 cx.executor().run_until_parked();
22054
22055 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
22056 let snapshot = editor.snapshot(window, cx);
22057 let reverted_hunk_statuses = snapshot
22058 .buffer_snapshot
22059 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
22060 .map(|hunk| hunk.status().kind)
22061 .collect::<Vec<_>>();
22062
22063 editor.git_restore(&Default::default(), window, cx);
22064 reverted_hunk_statuses
22065 });
22066 cx.executor().run_until_parked();
22067 cx.assert_editor_state(expected_reverted_text_with_selections);
22068 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
22069}
22070
22071#[gpui::test(iterations = 10)]
22072async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
22073 init_test(cx, |_| {});
22074
22075 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
22076 let counter = diagnostic_requests.clone();
22077
22078 let fs = FakeFs::new(cx.executor());
22079 fs.insert_tree(
22080 path!("/a"),
22081 json!({
22082 "first.rs": "fn main() { let a = 5; }",
22083 "second.rs": "// Test file",
22084 }),
22085 )
22086 .await;
22087
22088 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22089 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22090 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22091
22092 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22093 language_registry.add(rust_lang());
22094 let mut fake_servers = language_registry.register_fake_lsp(
22095 "Rust",
22096 FakeLspAdapter {
22097 capabilities: lsp::ServerCapabilities {
22098 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
22099 lsp::DiagnosticOptions {
22100 identifier: None,
22101 inter_file_dependencies: true,
22102 workspace_diagnostics: true,
22103 work_done_progress_options: Default::default(),
22104 },
22105 )),
22106 ..Default::default()
22107 },
22108 ..Default::default()
22109 },
22110 );
22111
22112 let editor = workspace
22113 .update(cx, |workspace, window, cx| {
22114 workspace.open_abs_path(
22115 PathBuf::from(path!("/a/first.rs")),
22116 OpenOptions::default(),
22117 window,
22118 cx,
22119 )
22120 })
22121 .unwrap()
22122 .await
22123 .unwrap()
22124 .downcast::<Editor>()
22125 .unwrap();
22126 let fake_server = fake_servers.next().await.unwrap();
22127 let server_id = fake_server.server.server_id();
22128 let mut first_request = fake_server
22129 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
22130 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
22131 let result_id = Some(new_result_id.to_string());
22132 assert_eq!(
22133 params.text_document.uri,
22134 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
22135 );
22136 async move {
22137 Ok(lsp::DocumentDiagnosticReportResult::Report(
22138 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
22139 related_documents: None,
22140 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
22141 items: Vec::new(),
22142 result_id,
22143 },
22144 }),
22145 ))
22146 }
22147 });
22148
22149 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
22150 project.update(cx, |project, cx| {
22151 let buffer_id = editor
22152 .read(cx)
22153 .buffer()
22154 .read(cx)
22155 .as_singleton()
22156 .expect("created a singleton buffer")
22157 .read(cx)
22158 .remote_id();
22159 let buffer_result_id = project
22160 .lsp_store()
22161 .read(cx)
22162 .result_id(server_id, buffer_id, cx);
22163 assert_eq!(expected, buffer_result_id);
22164 });
22165 };
22166
22167 ensure_result_id(None, cx);
22168 cx.executor().advance_clock(Duration::from_millis(60));
22169 cx.executor().run_until_parked();
22170 assert_eq!(
22171 diagnostic_requests.load(atomic::Ordering::Acquire),
22172 1,
22173 "Opening file should trigger diagnostic request"
22174 );
22175 first_request
22176 .next()
22177 .await
22178 .expect("should have sent the first diagnostics pull request");
22179 ensure_result_id(Some("1".to_string()), cx);
22180
22181 // Editing should trigger diagnostics
22182 editor.update_in(cx, |editor, window, cx| {
22183 editor.handle_input("2", window, cx)
22184 });
22185 cx.executor().advance_clock(Duration::from_millis(60));
22186 cx.executor().run_until_parked();
22187 assert_eq!(
22188 diagnostic_requests.load(atomic::Ordering::Acquire),
22189 2,
22190 "Editing should trigger diagnostic request"
22191 );
22192 ensure_result_id(Some("2".to_string()), cx);
22193
22194 // Moving cursor should not trigger diagnostic request
22195 editor.update_in(cx, |editor, window, cx| {
22196 editor.change_selections(None, window, cx, |s| {
22197 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
22198 });
22199 });
22200 cx.executor().advance_clock(Duration::from_millis(60));
22201 cx.executor().run_until_parked();
22202 assert_eq!(
22203 diagnostic_requests.load(atomic::Ordering::Acquire),
22204 2,
22205 "Cursor movement should not trigger diagnostic request"
22206 );
22207 ensure_result_id(Some("2".to_string()), cx);
22208 // Multiple rapid edits should be debounced
22209 for _ in 0..5 {
22210 editor.update_in(cx, |editor, window, cx| {
22211 editor.handle_input("x", window, cx)
22212 });
22213 }
22214 cx.executor().advance_clock(Duration::from_millis(60));
22215 cx.executor().run_until_parked();
22216
22217 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
22218 assert!(
22219 final_requests <= 4,
22220 "Multiple rapid edits should be debounced (got {final_requests} requests)",
22221 );
22222 ensure_result_id(Some(final_requests.to_string()), cx);
22223}
22224
22225#[gpui::test]
22226async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
22227 // Regression test for issue #11671
22228 // Previously, adding a cursor after moving multiple cursors would reset
22229 // the cursor count instead of adding to the existing cursors.
22230 init_test(cx, |_| {});
22231 let mut cx = EditorTestContext::new(cx).await;
22232
22233 // Create a simple buffer with cursor at start
22234 cx.set_state(indoc! {"
22235 ˇaaaa
22236 bbbb
22237 cccc
22238 dddd
22239 eeee
22240 ffff
22241 gggg
22242 hhhh"});
22243
22244 // Add 2 cursors below (so we have 3 total)
22245 cx.update_editor(|editor, window, cx| {
22246 editor.add_selection_below(&Default::default(), window, cx);
22247 editor.add_selection_below(&Default::default(), window, cx);
22248 });
22249
22250 // Verify we have 3 cursors
22251 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
22252 assert_eq!(
22253 initial_count, 3,
22254 "Should have 3 cursors after adding 2 below"
22255 );
22256
22257 // Move down one line
22258 cx.update_editor(|editor, window, cx| {
22259 editor.move_down(&MoveDown, window, cx);
22260 });
22261
22262 // Add another cursor below
22263 cx.update_editor(|editor, window, cx| {
22264 editor.add_selection_below(&Default::default(), window, cx);
22265 });
22266
22267 // Should now have 4 cursors (3 original + 1 new)
22268 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
22269 assert_eq!(
22270 final_count, 4,
22271 "Should have 4 cursors after moving and adding another"
22272 );
22273}