1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 inline_completion_tests::FakeInlineCompletionProvider,
6 linked_editing_ranges::LinkedEditingRanges,
7 scroll::scroll_amount::ScrollAmount,
8 test::{
9 assert_text_with_selections, build_editor,
10 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
11 editor_test_context::EditorTestContext,
12 select_ranges,
13 },
14};
15use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
16use futures::StreamExt;
17use gpui::{
18 BackgroundExecutor, DismissEvent, SemanticVersion, TestAppContext, UpdateGlobal,
19 VisualTestContext, WindowBounds, WindowOptions, div,
20};
21use indoc::indoc;
22use language::{
23 BracketPairConfig,
24 Capability::ReadWrite,
25 FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName,
26 Override, Point,
27 language_settings::{
28 AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
29 LanguageSettingsContent, LspInsertMode, PrettierSettings,
30 },
31 tree_sitter_python,
32};
33use language_settings::{Formatter, FormatterList, IndentGuideSettings};
34use lsp::CompletionParams;
35use multi_buffer::{IndentGuide, PathKey};
36use parking_lot::Mutex;
37use pretty_assertions::{assert_eq, assert_ne};
38use project::{
39 FakeFs,
40 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
41 project_settings::{LspSettings, ProjectSettings},
42};
43use serde_json::{self, json};
44use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
45use std::{
46 iter,
47 sync::atomic::{self, AtomicUsize},
48};
49use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
50use text::ToPoint as _;
51use unindent::Unindent;
52use util::{
53 assert_set_eq, path,
54 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
55 uri,
56};
57use workspace::{
58 CloseActiveItem, CloseAllItems, CloseInactiveItems, NavigationEntry, OpenOptions, ViewId,
59 item::{FollowEvent, FollowableItem, Item, ItemHandle},
60};
61
62#[gpui::test]
63fn test_edit_events(cx: &mut TestAppContext) {
64 init_test(cx, |_| {});
65
66 let buffer = cx.new(|cx| {
67 let mut buffer = language::Buffer::local("123456", cx);
68 buffer.set_group_interval(Duration::from_secs(1));
69 buffer
70 });
71
72 let events = Rc::new(RefCell::new(Vec::new()));
73 let editor1 = cx.add_window({
74 let events = events.clone();
75 |window, cx| {
76 let entity = cx.entity().clone();
77 cx.subscribe_in(
78 &entity,
79 window,
80 move |_, _, event: &EditorEvent, _, _| match event {
81 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
82 EditorEvent::BufferEdited => {
83 events.borrow_mut().push(("editor1", "buffer edited"))
84 }
85 _ => {}
86 },
87 )
88 .detach();
89 Editor::for_buffer(buffer.clone(), None, window, cx)
90 }
91 });
92
93 let editor2 = cx.add_window({
94 let events = events.clone();
95 |window, cx| {
96 cx.subscribe_in(
97 &cx.entity().clone(),
98 window,
99 move |_, _, event: &EditorEvent, _, _| match event {
100 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
101 EditorEvent::BufferEdited => {
102 events.borrow_mut().push(("editor2", "buffer edited"))
103 }
104 _ => {}
105 },
106 )
107 .detach();
108 Editor::for_buffer(buffer.clone(), None, window, cx)
109 }
110 });
111
112 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
113
114 // Mutating editor 1 will emit an `Edited` event only for that editor.
115 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
116 assert_eq!(
117 mem::take(&mut *events.borrow_mut()),
118 [
119 ("editor1", "edited"),
120 ("editor1", "buffer edited"),
121 ("editor2", "buffer edited"),
122 ]
123 );
124
125 // Mutating editor 2 will emit an `Edited` event only for that editor.
126 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
127 assert_eq!(
128 mem::take(&mut *events.borrow_mut()),
129 [
130 ("editor2", "edited"),
131 ("editor1", "buffer edited"),
132 ("editor2", "buffer edited"),
133 ]
134 );
135
136 // Undoing on editor 1 will emit an `Edited` event only for that editor.
137 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
138 assert_eq!(
139 mem::take(&mut *events.borrow_mut()),
140 [
141 ("editor1", "edited"),
142 ("editor1", "buffer edited"),
143 ("editor2", "buffer edited"),
144 ]
145 );
146
147 // Redoing on editor 1 will emit an `Edited` event only for that editor.
148 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
149 assert_eq!(
150 mem::take(&mut *events.borrow_mut()),
151 [
152 ("editor1", "edited"),
153 ("editor1", "buffer edited"),
154 ("editor2", "buffer edited"),
155 ]
156 );
157
158 // Undoing on editor 2 will emit an `Edited` event only for that editor.
159 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
160 assert_eq!(
161 mem::take(&mut *events.borrow_mut()),
162 [
163 ("editor2", "edited"),
164 ("editor1", "buffer edited"),
165 ("editor2", "buffer edited"),
166 ]
167 );
168
169 // Redoing on editor 2 will emit an `Edited` event only for that editor.
170 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
171 assert_eq!(
172 mem::take(&mut *events.borrow_mut()),
173 [
174 ("editor2", "edited"),
175 ("editor1", "buffer edited"),
176 ("editor2", "buffer edited"),
177 ]
178 );
179
180 // No event is emitted when the mutation is a no-op.
181 _ = editor2.update(cx, |editor, window, cx| {
182 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
183
184 editor.backspace(&Backspace, window, cx);
185 });
186 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
187}
188
189#[gpui::test]
190fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
191 init_test(cx, |_| {});
192
193 let mut now = Instant::now();
194 let group_interval = Duration::from_millis(1);
195 let buffer = cx.new(|cx| {
196 let mut buf = language::Buffer::local("123456", cx);
197 buf.set_group_interval(group_interval);
198 buf
199 });
200 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
201 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
202
203 _ = editor.update(cx, |editor, window, cx| {
204 editor.start_transaction_at(now, window, cx);
205 editor.change_selections(None, window, cx, |s| s.select_ranges([2..4]));
206
207 editor.insert("cd", window, cx);
208 editor.end_transaction_at(now, cx);
209 assert_eq!(editor.text(cx), "12cd56");
210 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
211
212 editor.start_transaction_at(now, window, cx);
213 editor.change_selections(None, window, cx, |s| s.select_ranges([4..5]));
214 editor.insert("e", window, cx);
215 editor.end_transaction_at(now, cx);
216 assert_eq!(editor.text(cx), "12cde6");
217 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
218
219 now += group_interval + Duration::from_millis(1);
220 editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]));
221
222 // Simulate an edit in another editor
223 buffer.update(cx, |buffer, cx| {
224 buffer.start_transaction_at(now, cx);
225 buffer.edit([(0..1, "a")], None, cx);
226 buffer.edit([(1..1, "b")], None, cx);
227 buffer.end_transaction_at(now, cx);
228 });
229
230 assert_eq!(editor.text(cx), "ab2cde6");
231 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
232
233 // Last transaction happened past the group interval in a different editor.
234 // Undo it individually and don't restore selections.
235 editor.undo(&Undo, window, cx);
236 assert_eq!(editor.text(cx), "12cde6");
237 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
238
239 // First two transactions happened within the group interval in this editor.
240 // Undo them together and restore selections.
241 editor.undo(&Undo, window, cx);
242 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
243 assert_eq!(editor.text(cx), "123456");
244 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
245
246 // Redo the first two transactions together.
247 editor.redo(&Redo, window, cx);
248 assert_eq!(editor.text(cx), "12cde6");
249 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
250
251 // Redo the last transaction on its own.
252 editor.redo(&Redo, window, cx);
253 assert_eq!(editor.text(cx), "ab2cde6");
254 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
255
256 // Test empty transactions.
257 editor.start_transaction_at(now, window, cx);
258 editor.end_transaction_at(now, cx);
259 editor.undo(&Undo, window, cx);
260 assert_eq!(editor.text(cx), "12cde6");
261 });
262}
263
264#[gpui::test]
265fn test_ime_composition(cx: &mut TestAppContext) {
266 init_test(cx, |_| {});
267
268 let buffer = cx.new(|cx| {
269 let mut buffer = language::Buffer::local("abcde", cx);
270 // Ensure automatic grouping doesn't occur.
271 buffer.set_group_interval(Duration::ZERO);
272 buffer
273 });
274
275 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
276 cx.add_window(|window, cx| {
277 let mut editor = build_editor(buffer.clone(), window, cx);
278
279 // Start a new IME composition.
280 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
281 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
282 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
283 assert_eq!(editor.text(cx), "äbcde");
284 assert_eq!(
285 editor.marked_text_ranges(cx),
286 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
287 );
288
289 // Finalize IME composition.
290 editor.replace_text_in_range(None, "ā", window, cx);
291 assert_eq!(editor.text(cx), "ābcde");
292 assert_eq!(editor.marked_text_ranges(cx), None);
293
294 // IME composition edits are grouped and are undone/redone at once.
295 editor.undo(&Default::default(), window, cx);
296 assert_eq!(editor.text(cx), "abcde");
297 assert_eq!(editor.marked_text_ranges(cx), None);
298 editor.redo(&Default::default(), window, cx);
299 assert_eq!(editor.text(cx), "ābcde");
300 assert_eq!(editor.marked_text_ranges(cx), None);
301
302 // Start a new IME composition.
303 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
304 assert_eq!(
305 editor.marked_text_ranges(cx),
306 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
307 );
308
309 // Undoing during an IME composition cancels it.
310 editor.undo(&Default::default(), window, cx);
311 assert_eq!(editor.text(cx), "ābcde");
312 assert_eq!(editor.marked_text_ranges(cx), None);
313
314 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
315 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
316 assert_eq!(editor.text(cx), "ābcdè");
317 assert_eq!(
318 editor.marked_text_ranges(cx),
319 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
320 );
321
322 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
323 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
324 assert_eq!(editor.text(cx), "ābcdę");
325 assert_eq!(editor.marked_text_ranges(cx), None);
326
327 // Start a new IME composition with multiple cursors.
328 editor.change_selections(None, window, cx, |s| {
329 s.select_ranges([
330 OffsetUtf16(1)..OffsetUtf16(1),
331 OffsetUtf16(3)..OffsetUtf16(3),
332 OffsetUtf16(5)..OffsetUtf16(5),
333 ])
334 });
335 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
336 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
337 assert_eq!(
338 editor.marked_text_ranges(cx),
339 Some(vec![
340 OffsetUtf16(0)..OffsetUtf16(3),
341 OffsetUtf16(4)..OffsetUtf16(7),
342 OffsetUtf16(8)..OffsetUtf16(11)
343 ])
344 );
345
346 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
347 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
348 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
349 assert_eq!(
350 editor.marked_text_ranges(cx),
351 Some(vec![
352 OffsetUtf16(1)..OffsetUtf16(2),
353 OffsetUtf16(5)..OffsetUtf16(6),
354 OffsetUtf16(9)..OffsetUtf16(10)
355 ])
356 );
357
358 // Finalize IME composition with multiple cursors.
359 editor.replace_text_in_range(Some(9..10), "2", window, cx);
360 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
361 assert_eq!(editor.marked_text_ranges(cx), None);
362
363 editor
364 });
365}
366
367#[gpui::test]
368fn test_selection_with_mouse(cx: &mut TestAppContext) {
369 init_test(cx, |_| {});
370
371 let editor = cx.add_window(|window, cx| {
372 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
373 build_editor(buffer, window, cx)
374 });
375
376 _ = editor.update(cx, |editor, window, cx| {
377 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
378 });
379 assert_eq!(
380 editor
381 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
382 .unwrap(),
383 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
384 );
385
386 _ = editor.update(cx, |editor, window, cx| {
387 editor.update_selection(
388 DisplayPoint::new(DisplayRow(3), 3),
389 0,
390 gpui::Point::<f32>::default(),
391 window,
392 cx,
393 );
394 });
395
396 assert_eq!(
397 editor
398 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
399 .unwrap(),
400 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
401 );
402
403 _ = editor.update(cx, |editor, window, cx| {
404 editor.update_selection(
405 DisplayPoint::new(DisplayRow(1), 1),
406 0,
407 gpui::Point::<f32>::default(),
408 window,
409 cx,
410 );
411 });
412
413 assert_eq!(
414 editor
415 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
416 .unwrap(),
417 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
418 );
419
420 _ = editor.update(cx, |editor, window, cx| {
421 editor.end_selection(window, cx);
422 editor.update_selection(
423 DisplayPoint::new(DisplayRow(3), 3),
424 0,
425 gpui::Point::<f32>::default(),
426 window,
427 cx,
428 );
429 });
430
431 assert_eq!(
432 editor
433 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
434 .unwrap(),
435 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
436 );
437
438 _ = editor.update(cx, |editor, window, cx| {
439 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
440 editor.update_selection(
441 DisplayPoint::new(DisplayRow(0), 0),
442 0,
443 gpui::Point::<f32>::default(),
444 window,
445 cx,
446 );
447 });
448
449 assert_eq!(
450 editor
451 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
452 .unwrap(),
453 [
454 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
455 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
456 ]
457 );
458
459 _ = editor.update(cx, |editor, window, cx| {
460 editor.end_selection(window, cx);
461 });
462
463 assert_eq!(
464 editor
465 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
466 .unwrap(),
467 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
468 );
469}
470
471#[gpui::test]
472fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
473 init_test(cx, |_| {});
474
475 let editor = cx.add_window(|window, cx| {
476 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
477 build_editor(buffer, window, cx)
478 });
479
480 _ = editor.update(cx, |editor, window, cx| {
481 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
482 });
483
484 _ = editor.update(cx, |editor, window, cx| {
485 editor.end_selection(window, cx);
486 });
487
488 _ = editor.update(cx, |editor, window, cx| {
489 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
490 });
491
492 _ = editor.update(cx, |editor, window, cx| {
493 editor.end_selection(window, cx);
494 });
495
496 assert_eq!(
497 editor
498 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
499 .unwrap(),
500 [
501 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
502 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
503 ]
504 );
505
506 _ = editor.update(cx, |editor, window, cx| {
507 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
508 });
509
510 _ = editor.update(cx, |editor, window, cx| {
511 editor.end_selection(window, cx);
512 });
513
514 assert_eq!(
515 editor
516 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
517 .unwrap(),
518 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
519 );
520}
521
522#[gpui::test]
523fn test_canceling_pending_selection(cx: &mut TestAppContext) {
524 init_test(cx, |_| {});
525
526 let editor = cx.add_window(|window, cx| {
527 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
528 build_editor(buffer, window, cx)
529 });
530
531 _ = editor.update(cx, |editor, window, cx| {
532 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
533 assert_eq!(
534 editor.selections.display_ranges(cx),
535 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
536 );
537 });
538
539 _ = editor.update(cx, |editor, window, cx| {
540 editor.update_selection(
541 DisplayPoint::new(DisplayRow(3), 3),
542 0,
543 gpui::Point::<f32>::default(),
544 window,
545 cx,
546 );
547 assert_eq!(
548 editor.selections.display_ranges(cx),
549 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
550 );
551 });
552
553 _ = editor.update(cx, |editor, window, cx| {
554 editor.cancel(&Cancel, window, cx);
555 editor.update_selection(
556 DisplayPoint::new(DisplayRow(1), 1),
557 0,
558 gpui::Point::<f32>::default(),
559 window,
560 cx,
561 );
562 assert_eq!(
563 editor.selections.display_ranges(cx),
564 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
565 );
566 });
567}
568
569#[gpui::test]
570fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
571 init_test(cx, |_| {});
572
573 let editor = cx.add_window(|window, cx| {
574 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
575 build_editor(buffer, window, cx)
576 });
577
578 _ = editor.update(cx, |editor, window, cx| {
579 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
580 assert_eq!(
581 editor.selections.display_ranges(cx),
582 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
583 );
584
585 editor.move_down(&Default::default(), window, cx);
586 assert_eq!(
587 editor.selections.display_ranges(cx),
588 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
589 );
590
591 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
592 assert_eq!(
593 editor.selections.display_ranges(cx),
594 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
595 );
596
597 editor.move_up(&Default::default(), window, cx);
598 assert_eq!(
599 editor.selections.display_ranges(cx),
600 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
601 );
602 });
603}
604
605#[gpui::test]
606fn test_clone(cx: &mut TestAppContext) {
607 init_test(cx, |_| {});
608
609 let (text, selection_ranges) = marked_text_ranges(
610 indoc! {"
611 one
612 two
613 threeˇ
614 four
615 fiveˇ
616 "},
617 true,
618 );
619
620 let editor = cx.add_window(|window, cx| {
621 let buffer = MultiBuffer::build_simple(&text, cx);
622 build_editor(buffer, window, cx)
623 });
624
625 _ = editor.update(cx, |editor, window, cx| {
626 editor.change_selections(None, window, cx, |s| {
627 s.select_ranges(selection_ranges.clone())
628 });
629 editor.fold_creases(
630 vec![
631 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
632 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
633 ],
634 true,
635 window,
636 cx,
637 );
638 });
639
640 let cloned_editor = editor
641 .update(cx, |editor, _, cx| {
642 cx.open_window(Default::default(), |window, cx| {
643 cx.new(|cx| editor.clone(window, cx))
644 })
645 })
646 .unwrap()
647 .unwrap();
648
649 let snapshot = editor
650 .update(cx, |e, window, cx| e.snapshot(window, cx))
651 .unwrap();
652 let cloned_snapshot = cloned_editor
653 .update(cx, |e, window, cx| e.snapshot(window, cx))
654 .unwrap();
655
656 assert_eq!(
657 cloned_editor
658 .update(cx, |e, _, cx| e.display_text(cx))
659 .unwrap(),
660 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
661 );
662 assert_eq!(
663 cloned_snapshot
664 .folds_in_range(0..text.len())
665 .collect::<Vec<_>>(),
666 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
667 );
668 assert_set_eq!(
669 cloned_editor
670 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
671 .unwrap(),
672 editor
673 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
674 .unwrap()
675 );
676 assert_set_eq!(
677 cloned_editor
678 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
679 .unwrap(),
680 editor
681 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
682 .unwrap()
683 );
684}
685
686#[gpui::test]
687async fn test_navigation_history(cx: &mut TestAppContext) {
688 init_test(cx, |_| {});
689
690 use workspace::item::Item;
691
692 let fs = FakeFs::new(cx.executor());
693 let project = Project::test(fs, [], cx).await;
694 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
695 let pane = workspace
696 .update(cx, |workspace, _, _| workspace.active_pane().clone())
697 .unwrap();
698
699 _ = workspace.update(cx, |_v, window, cx| {
700 cx.new(|cx| {
701 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
702 let mut editor = build_editor(buffer.clone(), window, cx);
703 let handle = cx.entity();
704 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
705
706 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
707 editor.nav_history.as_mut().unwrap().pop_backward(cx)
708 }
709
710 // Move the cursor a small distance.
711 // Nothing is added to the navigation history.
712 editor.change_selections(None, window, cx, |s| {
713 s.select_display_ranges([
714 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
715 ])
716 });
717 editor.change_selections(None, window, cx, |s| {
718 s.select_display_ranges([
719 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
720 ])
721 });
722 assert!(pop_history(&mut editor, cx).is_none());
723
724 // Move the cursor a large distance.
725 // The history can jump back to the previous position.
726 editor.change_selections(None, window, cx, |s| {
727 s.select_display_ranges([
728 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
729 ])
730 });
731 let nav_entry = pop_history(&mut editor, cx).unwrap();
732 editor.navigate(nav_entry.data.unwrap(), window, cx);
733 assert_eq!(nav_entry.item.id(), cx.entity_id());
734 assert_eq!(
735 editor.selections.display_ranges(cx),
736 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
737 );
738 assert!(pop_history(&mut editor, cx).is_none());
739
740 // Move the cursor a small distance via the mouse.
741 // Nothing is added to the navigation history.
742 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
743 editor.end_selection(window, cx);
744 assert_eq!(
745 editor.selections.display_ranges(cx),
746 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
747 );
748 assert!(pop_history(&mut editor, cx).is_none());
749
750 // Move the cursor a large distance via the mouse.
751 // The history can jump back to the previous position.
752 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
753 editor.end_selection(window, cx);
754 assert_eq!(
755 editor.selections.display_ranges(cx),
756 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
757 );
758 let nav_entry = pop_history(&mut editor, cx).unwrap();
759 editor.navigate(nav_entry.data.unwrap(), window, cx);
760 assert_eq!(nav_entry.item.id(), cx.entity_id());
761 assert_eq!(
762 editor.selections.display_ranges(cx),
763 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
764 );
765 assert!(pop_history(&mut editor, cx).is_none());
766
767 // Set scroll position to check later
768 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
769 let original_scroll_position = editor.scroll_manager.anchor();
770
771 // Jump to the end of the document and adjust scroll
772 editor.move_to_end(&MoveToEnd, window, cx);
773 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
774 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
775
776 let nav_entry = pop_history(&mut editor, cx).unwrap();
777 editor.navigate(nav_entry.data.unwrap(), window, cx);
778 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
779
780 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
781 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
782 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
783 let invalid_point = Point::new(9999, 0);
784 editor.navigate(
785 Box::new(NavigationData {
786 cursor_anchor: invalid_anchor,
787 cursor_position: invalid_point,
788 scroll_anchor: ScrollAnchor {
789 anchor: invalid_anchor,
790 offset: Default::default(),
791 },
792 scroll_top_row: invalid_point.row,
793 }),
794 window,
795 cx,
796 );
797 assert_eq!(
798 editor.selections.display_ranges(cx),
799 &[editor.max_point(cx)..editor.max_point(cx)]
800 );
801 assert_eq!(
802 editor.scroll_position(cx),
803 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
804 );
805
806 editor
807 })
808 });
809}
810
811#[gpui::test]
812fn test_cancel(cx: &mut TestAppContext) {
813 init_test(cx, |_| {});
814
815 let editor = cx.add_window(|window, cx| {
816 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
817 build_editor(buffer, window, cx)
818 });
819
820 _ = editor.update(cx, |editor, window, cx| {
821 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
822 editor.update_selection(
823 DisplayPoint::new(DisplayRow(1), 1),
824 0,
825 gpui::Point::<f32>::default(),
826 window,
827 cx,
828 );
829 editor.end_selection(window, cx);
830
831 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
832 editor.update_selection(
833 DisplayPoint::new(DisplayRow(0), 3),
834 0,
835 gpui::Point::<f32>::default(),
836 window,
837 cx,
838 );
839 editor.end_selection(window, cx);
840 assert_eq!(
841 editor.selections.display_ranges(cx),
842 [
843 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
844 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
845 ]
846 );
847 });
848
849 _ = editor.update(cx, |editor, window, cx| {
850 editor.cancel(&Cancel, window, cx);
851 assert_eq!(
852 editor.selections.display_ranges(cx),
853 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
854 );
855 });
856
857 _ = editor.update(cx, |editor, window, cx| {
858 editor.cancel(&Cancel, window, cx);
859 assert_eq!(
860 editor.selections.display_ranges(cx),
861 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
862 );
863 });
864}
865
866#[gpui::test]
867fn test_fold_action(cx: &mut TestAppContext) {
868 init_test(cx, |_| {});
869
870 let editor = cx.add_window(|window, cx| {
871 let buffer = MultiBuffer::build_simple(
872 &"
873 impl Foo {
874 // Hello!
875
876 fn a() {
877 1
878 }
879
880 fn b() {
881 2
882 }
883
884 fn c() {
885 3
886 }
887 }
888 "
889 .unindent(),
890 cx,
891 );
892 build_editor(buffer.clone(), window, cx)
893 });
894
895 _ = editor.update(cx, |editor, window, cx| {
896 editor.change_selections(None, window, cx, |s| {
897 s.select_display_ranges([
898 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
899 ]);
900 });
901 editor.fold(&Fold, window, cx);
902 assert_eq!(
903 editor.display_text(cx),
904 "
905 impl Foo {
906 // Hello!
907
908 fn a() {
909 1
910 }
911
912 fn b() {⋯
913 }
914
915 fn c() {⋯
916 }
917 }
918 "
919 .unindent(),
920 );
921
922 editor.fold(&Fold, window, cx);
923 assert_eq!(
924 editor.display_text(cx),
925 "
926 impl Foo {⋯
927 }
928 "
929 .unindent(),
930 );
931
932 editor.unfold_lines(&UnfoldLines, window, cx);
933 assert_eq!(
934 editor.display_text(cx),
935 "
936 impl Foo {
937 // Hello!
938
939 fn a() {
940 1
941 }
942
943 fn b() {⋯
944 }
945
946 fn c() {⋯
947 }
948 }
949 "
950 .unindent(),
951 );
952
953 editor.unfold_lines(&UnfoldLines, window, cx);
954 assert_eq!(
955 editor.display_text(cx),
956 editor.buffer.read(cx).read(cx).text()
957 );
958 });
959}
960
961#[gpui::test]
962fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
963 init_test(cx, |_| {});
964
965 let editor = cx.add_window(|window, cx| {
966 let buffer = MultiBuffer::build_simple(
967 &"
968 class Foo:
969 # Hello!
970
971 def a():
972 print(1)
973
974 def b():
975 print(2)
976
977 def c():
978 print(3)
979 "
980 .unindent(),
981 cx,
982 );
983 build_editor(buffer.clone(), window, cx)
984 });
985
986 _ = editor.update(cx, |editor, window, cx| {
987 editor.change_selections(None, window, cx, |s| {
988 s.select_display_ranges([
989 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
990 ]);
991 });
992 editor.fold(&Fold, window, cx);
993 assert_eq!(
994 editor.display_text(cx),
995 "
996 class Foo:
997 # Hello!
998
999 def a():
1000 print(1)
1001
1002 def b():⋯
1003
1004 def c():⋯
1005 "
1006 .unindent(),
1007 );
1008
1009 editor.fold(&Fold, window, cx);
1010 assert_eq!(
1011 editor.display_text(cx),
1012 "
1013 class Foo:⋯
1014 "
1015 .unindent(),
1016 );
1017
1018 editor.unfold_lines(&UnfoldLines, window, cx);
1019 assert_eq!(
1020 editor.display_text(cx),
1021 "
1022 class Foo:
1023 # Hello!
1024
1025 def a():
1026 print(1)
1027
1028 def b():⋯
1029
1030 def c():⋯
1031 "
1032 .unindent(),
1033 );
1034
1035 editor.unfold_lines(&UnfoldLines, window, cx);
1036 assert_eq!(
1037 editor.display_text(cx),
1038 editor.buffer.read(cx).read(cx).text()
1039 );
1040 });
1041}
1042
1043#[gpui::test]
1044fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1045 init_test(cx, |_| {});
1046
1047 let editor = cx.add_window(|window, cx| {
1048 let buffer = MultiBuffer::build_simple(
1049 &"
1050 class Foo:
1051 # Hello!
1052
1053 def a():
1054 print(1)
1055
1056 def b():
1057 print(2)
1058
1059
1060 def c():
1061 print(3)
1062
1063
1064 "
1065 .unindent(),
1066 cx,
1067 );
1068 build_editor(buffer.clone(), window, cx)
1069 });
1070
1071 _ = editor.update(cx, |editor, window, cx| {
1072 editor.change_selections(None, window, cx, |s| {
1073 s.select_display_ranges([
1074 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1075 ]);
1076 });
1077 editor.fold(&Fold, window, cx);
1078 assert_eq!(
1079 editor.display_text(cx),
1080 "
1081 class Foo:
1082 # Hello!
1083
1084 def a():
1085 print(1)
1086
1087 def b():⋯
1088
1089
1090 def c():⋯
1091
1092
1093 "
1094 .unindent(),
1095 );
1096
1097 editor.fold(&Fold, window, cx);
1098 assert_eq!(
1099 editor.display_text(cx),
1100 "
1101 class Foo:⋯
1102
1103
1104 "
1105 .unindent(),
1106 );
1107
1108 editor.unfold_lines(&UnfoldLines, window, cx);
1109 assert_eq!(
1110 editor.display_text(cx),
1111 "
1112 class Foo:
1113 # Hello!
1114
1115 def a():
1116 print(1)
1117
1118 def b():⋯
1119
1120
1121 def c():⋯
1122
1123
1124 "
1125 .unindent(),
1126 );
1127
1128 editor.unfold_lines(&UnfoldLines, window, cx);
1129 assert_eq!(
1130 editor.display_text(cx),
1131 editor.buffer.read(cx).read(cx).text()
1132 );
1133 });
1134}
1135
1136#[gpui::test]
1137fn test_fold_at_level(cx: &mut TestAppContext) {
1138 init_test(cx, |_| {});
1139
1140 let editor = cx.add_window(|window, cx| {
1141 let buffer = MultiBuffer::build_simple(
1142 &"
1143 class Foo:
1144 # Hello!
1145
1146 def a():
1147 print(1)
1148
1149 def b():
1150 print(2)
1151
1152
1153 class Bar:
1154 # World!
1155
1156 def a():
1157 print(1)
1158
1159 def b():
1160 print(2)
1161
1162
1163 "
1164 .unindent(),
1165 cx,
1166 );
1167 build_editor(buffer.clone(), window, cx)
1168 });
1169
1170 _ = editor.update(cx, |editor, window, cx| {
1171 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1172 assert_eq!(
1173 editor.display_text(cx),
1174 "
1175 class Foo:
1176 # Hello!
1177
1178 def a():⋯
1179
1180 def b():⋯
1181
1182
1183 class Bar:
1184 # World!
1185
1186 def a():⋯
1187
1188 def b():⋯
1189
1190
1191 "
1192 .unindent(),
1193 );
1194
1195 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1196 assert_eq!(
1197 editor.display_text(cx),
1198 "
1199 class Foo:⋯
1200
1201
1202 class Bar:⋯
1203
1204
1205 "
1206 .unindent(),
1207 );
1208
1209 editor.unfold_all(&UnfoldAll, window, cx);
1210 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1211 assert_eq!(
1212 editor.display_text(cx),
1213 "
1214 class Foo:
1215 # Hello!
1216
1217 def a():
1218 print(1)
1219
1220 def b():
1221 print(2)
1222
1223
1224 class Bar:
1225 # World!
1226
1227 def a():
1228 print(1)
1229
1230 def b():
1231 print(2)
1232
1233
1234 "
1235 .unindent(),
1236 );
1237
1238 assert_eq!(
1239 editor.display_text(cx),
1240 editor.buffer.read(cx).read(cx).text()
1241 );
1242 });
1243}
1244
1245#[gpui::test]
1246fn test_move_cursor(cx: &mut TestAppContext) {
1247 init_test(cx, |_| {});
1248
1249 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1250 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1251
1252 buffer.update(cx, |buffer, cx| {
1253 buffer.edit(
1254 vec![
1255 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1256 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1257 ],
1258 None,
1259 cx,
1260 );
1261 });
1262 _ = editor.update(cx, |editor, window, cx| {
1263 assert_eq!(
1264 editor.selections.display_ranges(cx),
1265 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1266 );
1267
1268 editor.move_down(&MoveDown, window, cx);
1269 assert_eq!(
1270 editor.selections.display_ranges(cx),
1271 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1272 );
1273
1274 editor.move_right(&MoveRight, window, cx);
1275 assert_eq!(
1276 editor.selections.display_ranges(cx),
1277 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1278 );
1279
1280 editor.move_left(&MoveLeft, window, cx);
1281 assert_eq!(
1282 editor.selections.display_ranges(cx),
1283 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1284 );
1285
1286 editor.move_up(&MoveUp, window, cx);
1287 assert_eq!(
1288 editor.selections.display_ranges(cx),
1289 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1290 );
1291
1292 editor.move_to_end(&MoveToEnd, window, cx);
1293 assert_eq!(
1294 editor.selections.display_ranges(cx),
1295 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1296 );
1297
1298 editor.move_to_beginning(&MoveToBeginning, window, cx);
1299 assert_eq!(
1300 editor.selections.display_ranges(cx),
1301 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1302 );
1303
1304 editor.change_selections(None, window, cx, |s| {
1305 s.select_display_ranges([
1306 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1307 ]);
1308 });
1309 editor.select_to_beginning(&SelectToBeginning, window, cx);
1310 assert_eq!(
1311 editor.selections.display_ranges(cx),
1312 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1313 );
1314
1315 editor.select_to_end(&SelectToEnd, window, cx);
1316 assert_eq!(
1317 editor.selections.display_ranges(cx),
1318 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1319 );
1320 });
1321}
1322
1323#[gpui::test]
1324fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1325 init_test(cx, |_| {});
1326
1327 let editor = cx.add_window(|window, cx| {
1328 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1329 build_editor(buffer.clone(), window, cx)
1330 });
1331
1332 assert_eq!('🟥'.len_utf8(), 4);
1333 assert_eq!('α'.len_utf8(), 2);
1334
1335 _ = editor.update(cx, |editor, window, cx| {
1336 editor.fold_creases(
1337 vec![
1338 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1339 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1340 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1341 ],
1342 true,
1343 window,
1344 cx,
1345 );
1346 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1347
1348 editor.move_right(&MoveRight, window, cx);
1349 assert_eq!(
1350 editor.selections.display_ranges(cx),
1351 &[empty_range(0, "🟥".len())]
1352 );
1353 editor.move_right(&MoveRight, window, cx);
1354 assert_eq!(
1355 editor.selections.display_ranges(cx),
1356 &[empty_range(0, "🟥🟧".len())]
1357 );
1358 editor.move_right(&MoveRight, window, cx);
1359 assert_eq!(
1360 editor.selections.display_ranges(cx),
1361 &[empty_range(0, "🟥🟧⋯".len())]
1362 );
1363
1364 editor.move_down(&MoveDown, window, cx);
1365 assert_eq!(
1366 editor.selections.display_ranges(cx),
1367 &[empty_range(1, "ab⋯e".len())]
1368 );
1369 editor.move_left(&MoveLeft, window, cx);
1370 assert_eq!(
1371 editor.selections.display_ranges(cx),
1372 &[empty_range(1, "ab⋯".len())]
1373 );
1374 editor.move_left(&MoveLeft, window, cx);
1375 assert_eq!(
1376 editor.selections.display_ranges(cx),
1377 &[empty_range(1, "ab".len())]
1378 );
1379 editor.move_left(&MoveLeft, window, cx);
1380 assert_eq!(
1381 editor.selections.display_ranges(cx),
1382 &[empty_range(1, "a".len())]
1383 );
1384
1385 editor.move_down(&MoveDown, window, cx);
1386 assert_eq!(
1387 editor.selections.display_ranges(cx),
1388 &[empty_range(2, "α".len())]
1389 );
1390 editor.move_right(&MoveRight, window, cx);
1391 assert_eq!(
1392 editor.selections.display_ranges(cx),
1393 &[empty_range(2, "αβ".len())]
1394 );
1395 editor.move_right(&MoveRight, window, cx);
1396 assert_eq!(
1397 editor.selections.display_ranges(cx),
1398 &[empty_range(2, "αβ⋯".len())]
1399 );
1400 editor.move_right(&MoveRight, window, cx);
1401 assert_eq!(
1402 editor.selections.display_ranges(cx),
1403 &[empty_range(2, "αβ⋯ε".len())]
1404 );
1405
1406 editor.move_up(&MoveUp, window, cx);
1407 assert_eq!(
1408 editor.selections.display_ranges(cx),
1409 &[empty_range(1, "ab⋯e".len())]
1410 );
1411 editor.move_down(&MoveDown, window, cx);
1412 assert_eq!(
1413 editor.selections.display_ranges(cx),
1414 &[empty_range(2, "αβ⋯ε".len())]
1415 );
1416 editor.move_up(&MoveUp, window, cx);
1417 assert_eq!(
1418 editor.selections.display_ranges(cx),
1419 &[empty_range(1, "ab⋯e".len())]
1420 );
1421
1422 editor.move_up(&MoveUp, window, cx);
1423 assert_eq!(
1424 editor.selections.display_ranges(cx),
1425 &[empty_range(0, "🟥🟧".len())]
1426 );
1427 editor.move_left(&MoveLeft, window, cx);
1428 assert_eq!(
1429 editor.selections.display_ranges(cx),
1430 &[empty_range(0, "🟥".len())]
1431 );
1432 editor.move_left(&MoveLeft, window, cx);
1433 assert_eq!(
1434 editor.selections.display_ranges(cx),
1435 &[empty_range(0, "".len())]
1436 );
1437 });
1438}
1439
1440#[gpui::test]
1441fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1442 init_test(cx, |_| {});
1443
1444 let editor = cx.add_window(|window, cx| {
1445 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1446 build_editor(buffer.clone(), window, cx)
1447 });
1448 _ = editor.update(cx, |editor, window, cx| {
1449 editor.change_selections(None, window, cx, |s| {
1450 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1451 });
1452
1453 // moving above start of document should move selection to start of document,
1454 // but the next move down should still be at the original goal_x
1455 editor.move_up(&MoveUp, window, cx);
1456 assert_eq!(
1457 editor.selections.display_ranges(cx),
1458 &[empty_range(0, "".len())]
1459 );
1460
1461 editor.move_down(&MoveDown, window, cx);
1462 assert_eq!(
1463 editor.selections.display_ranges(cx),
1464 &[empty_range(1, "abcd".len())]
1465 );
1466
1467 editor.move_down(&MoveDown, window, cx);
1468 assert_eq!(
1469 editor.selections.display_ranges(cx),
1470 &[empty_range(2, "αβγ".len())]
1471 );
1472
1473 editor.move_down(&MoveDown, window, cx);
1474 assert_eq!(
1475 editor.selections.display_ranges(cx),
1476 &[empty_range(3, "abcd".len())]
1477 );
1478
1479 editor.move_down(&MoveDown, window, cx);
1480 assert_eq!(
1481 editor.selections.display_ranges(cx),
1482 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1483 );
1484
1485 // moving past end of document should not change goal_x
1486 editor.move_down(&MoveDown, window, cx);
1487 assert_eq!(
1488 editor.selections.display_ranges(cx),
1489 &[empty_range(5, "".len())]
1490 );
1491
1492 editor.move_down(&MoveDown, window, cx);
1493 assert_eq!(
1494 editor.selections.display_ranges(cx),
1495 &[empty_range(5, "".len())]
1496 );
1497
1498 editor.move_up(&MoveUp, window, cx);
1499 assert_eq!(
1500 editor.selections.display_ranges(cx),
1501 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1502 );
1503
1504 editor.move_up(&MoveUp, window, cx);
1505 assert_eq!(
1506 editor.selections.display_ranges(cx),
1507 &[empty_range(3, "abcd".len())]
1508 );
1509
1510 editor.move_up(&MoveUp, window, cx);
1511 assert_eq!(
1512 editor.selections.display_ranges(cx),
1513 &[empty_range(2, "αβγ".len())]
1514 );
1515 });
1516}
1517
1518#[gpui::test]
1519fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1520 init_test(cx, |_| {});
1521 let move_to_beg = MoveToBeginningOfLine {
1522 stop_at_soft_wraps: true,
1523 stop_at_indent: true,
1524 };
1525
1526 let delete_to_beg = DeleteToBeginningOfLine {
1527 stop_at_indent: false,
1528 };
1529
1530 let move_to_end = MoveToEndOfLine {
1531 stop_at_soft_wraps: true,
1532 };
1533
1534 let editor = cx.add_window(|window, cx| {
1535 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1536 build_editor(buffer, window, cx)
1537 });
1538 _ = editor.update(cx, |editor, window, cx| {
1539 editor.change_selections(None, window, cx, |s| {
1540 s.select_display_ranges([
1541 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1542 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1543 ]);
1544 });
1545 });
1546
1547 _ = editor.update(cx, |editor, window, cx| {
1548 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1549 assert_eq!(
1550 editor.selections.display_ranges(cx),
1551 &[
1552 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1553 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1554 ]
1555 );
1556 });
1557
1558 _ = editor.update(cx, |editor, window, cx| {
1559 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1560 assert_eq!(
1561 editor.selections.display_ranges(cx),
1562 &[
1563 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1564 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1565 ]
1566 );
1567 });
1568
1569 _ = editor.update(cx, |editor, window, cx| {
1570 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1571 assert_eq!(
1572 editor.selections.display_ranges(cx),
1573 &[
1574 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1575 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1576 ]
1577 );
1578 });
1579
1580 _ = editor.update(cx, |editor, window, cx| {
1581 editor.move_to_end_of_line(&move_to_end, window, cx);
1582 assert_eq!(
1583 editor.selections.display_ranges(cx),
1584 &[
1585 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1586 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1587 ]
1588 );
1589 });
1590
1591 // Moving to the end of line again is a no-op.
1592 _ = editor.update(cx, |editor, window, cx| {
1593 editor.move_to_end_of_line(&move_to_end, window, cx);
1594 assert_eq!(
1595 editor.selections.display_ranges(cx),
1596 &[
1597 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1598 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1599 ]
1600 );
1601 });
1602
1603 _ = editor.update(cx, |editor, window, cx| {
1604 editor.move_left(&MoveLeft, window, cx);
1605 editor.select_to_beginning_of_line(
1606 &SelectToBeginningOfLine {
1607 stop_at_soft_wraps: true,
1608 stop_at_indent: true,
1609 },
1610 window,
1611 cx,
1612 );
1613 assert_eq!(
1614 editor.selections.display_ranges(cx),
1615 &[
1616 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1617 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1618 ]
1619 );
1620 });
1621
1622 _ = editor.update(cx, |editor, window, cx| {
1623 editor.select_to_beginning_of_line(
1624 &SelectToBeginningOfLine {
1625 stop_at_soft_wraps: true,
1626 stop_at_indent: true,
1627 },
1628 window,
1629 cx,
1630 );
1631 assert_eq!(
1632 editor.selections.display_ranges(cx),
1633 &[
1634 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1635 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1636 ]
1637 );
1638 });
1639
1640 _ = editor.update(cx, |editor, window, cx| {
1641 editor.select_to_beginning_of_line(
1642 &SelectToBeginningOfLine {
1643 stop_at_soft_wraps: true,
1644 stop_at_indent: true,
1645 },
1646 window,
1647 cx,
1648 );
1649 assert_eq!(
1650 editor.selections.display_ranges(cx),
1651 &[
1652 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1653 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1654 ]
1655 );
1656 });
1657
1658 _ = editor.update(cx, |editor, window, cx| {
1659 editor.select_to_end_of_line(
1660 &SelectToEndOfLine {
1661 stop_at_soft_wraps: true,
1662 },
1663 window,
1664 cx,
1665 );
1666 assert_eq!(
1667 editor.selections.display_ranges(cx),
1668 &[
1669 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1670 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1671 ]
1672 );
1673 });
1674
1675 _ = editor.update(cx, |editor, window, cx| {
1676 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1677 assert_eq!(editor.display_text(cx), "ab\n de");
1678 assert_eq!(
1679 editor.selections.display_ranges(cx),
1680 &[
1681 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1682 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1683 ]
1684 );
1685 });
1686
1687 _ = editor.update(cx, |editor, window, cx| {
1688 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1689 assert_eq!(editor.display_text(cx), "\n");
1690 assert_eq!(
1691 editor.selections.display_ranges(cx),
1692 &[
1693 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1694 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1695 ]
1696 );
1697 });
1698}
1699
1700#[gpui::test]
1701fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1702 init_test(cx, |_| {});
1703 let move_to_beg = MoveToBeginningOfLine {
1704 stop_at_soft_wraps: false,
1705 stop_at_indent: false,
1706 };
1707
1708 let move_to_end = MoveToEndOfLine {
1709 stop_at_soft_wraps: false,
1710 };
1711
1712 let editor = cx.add_window(|window, cx| {
1713 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1714 build_editor(buffer, window, cx)
1715 });
1716
1717 _ = editor.update(cx, |editor, window, cx| {
1718 editor.set_wrap_width(Some(140.0.into()), cx);
1719
1720 // We expect the following lines after wrapping
1721 // ```
1722 // thequickbrownfox
1723 // jumpedoverthelazydo
1724 // gs
1725 // ```
1726 // The final `gs` was soft-wrapped onto a new line.
1727 assert_eq!(
1728 "thequickbrownfox\njumpedoverthelaz\nydogs",
1729 editor.display_text(cx),
1730 );
1731
1732 // First, let's assert behavior on the first line, that was not soft-wrapped.
1733 // Start the cursor at the `k` on the first line
1734 editor.change_selections(None, window, cx, |s| {
1735 s.select_display_ranges([
1736 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1737 ]);
1738 });
1739
1740 // Moving to the beginning of the line should put us at the beginning of the line.
1741 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1742 assert_eq!(
1743 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1744 editor.selections.display_ranges(cx)
1745 );
1746
1747 // Moving to the end of the line should put us at the end of the line.
1748 editor.move_to_end_of_line(&move_to_end, window, cx);
1749 assert_eq!(
1750 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1751 editor.selections.display_ranges(cx)
1752 );
1753
1754 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1755 // Start the cursor at the last line (`y` that was wrapped to a new line)
1756 editor.change_selections(None, window, cx, |s| {
1757 s.select_display_ranges([
1758 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1759 ]);
1760 });
1761
1762 // Moving to the beginning of the line should put us at the start of the second line of
1763 // display text, i.e., the `j`.
1764 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1765 assert_eq!(
1766 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1767 editor.selections.display_ranges(cx)
1768 );
1769
1770 // Moving to the beginning of the line again should be a no-op.
1771 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1772 assert_eq!(
1773 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1774 editor.selections.display_ranges(cx)
1775 );
1776
1777 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1778 // next display line.
1779 editor.move_to_end_of_line(&move_to_end, window, cx);
1780 assert_eq!(
1781 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1782 editor.selections.display_ranges(cx)
1783 );
1784
1785 // Moving to the end of the line again should be a no-op.
1786 editor.move_to_end_of_line(&move_to_end, window, cx);
1787 assert_eq!(
1788 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1789 editor.selections.display_ranges(cx)
1790 );
1791 });
1792}
1793
1794#[gpui::test]
1795fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1796 init_test(cx, |_| {});
1797
1798 let move_to_beg = MoveToBeginningOfLine {
1799 stop_at_soft_wraps: true,
1800 stop_at_indent: true,
1801 };
1802
1803 let select_to_beg = SelectToBeginningOfLine {
1804 stop_at_soft_wraps: true,
1805 stop_at_indent: true,
1806 };
1807
1808 let delete_to_beg = DeleteToBeginningOfLine {
1809 stop_at_indent: true,
1810 };
1811
1812 let move_to_end = MoveToEndOfLine {
1813 stop_at_soft_wraps: false,
1814 };
1815
1816 let editor = cx.add_window(|window, cx| {
1817 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1818 build_editor(buffer, window, cx)
1819 });
1820
1821 _ = editor.update(cx, |editor, window, cx| {
1822 editor.change_selections(None, window, cx, |s| {
1823 s.select_display_ranges([
1824 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1825 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1826 ]);
1827 });
1828
1829 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1830 // and the second cursor at the first non-whitespace character in the line.
1831 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1832 assert_eq!(
1833 editor.selections.display_ranges(cx),
1834 &[
1835 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1836 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1837 ]
1838 );
1839
1840 // Moving to the beginning of the line again should be a no-op for the first cursor,
1841 // and should move the second cursor to the beginning of the line.
1842 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1843 assert_eq!(
1844 editor.selections.display_ranges(cx),
1845 &[
1846 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1847 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1848 ]
1849 );
1850
1851 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1852 // and should move the second cursor back to the first non-whitespace character in the line.
1853 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1854 assert_eq!(
1855 editor.selections.display_ranges(cx),
1856 &[
1857 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1858 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1859 ]
1860 );
1861
1862 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1863 // and to the first non-whitespace character in the line for the second cursor.
1864 editor.move_to_end_of_line(&move_to_end, window, cx);
1865 editor.move_left(&MoveLeft, window, cx);
1866 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1867 assert_eq!(
1868 editor.selections.display_ranges(cx),
1869 &[
1870 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1871 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1872 ]
1873 );
1874
1875 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1876 // and should select to the beginning of the line for the second cursor.
1877 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1878 assert_eq!(
1879 editor.selections.display_ranges(cx),
1880 &[
1881 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1882 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1883 ]
1884 );
1885
1886 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1887 // and should delete to the first non-whitespace character in the line for the second cursor.
1888 editor.move_to_end_of_line(&move_to_end, window, cx);
1889 editor.move_left(&MoveLeft, window, cx);
1890 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1891 assert_eq!(editor.text(cx), "c\n f");
1892 });
1893}
1894
1895#[gpui::test]
1896fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1897 init_test(cx, |_| {});
1898
1899 let editor = cx.add_window(|window, cx| {
1900 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1901 build_editor(buffer, window, cx)
1902 });
1903 _ = editor.update(cx, |editor, window, cx| {
1904 editor.change_selections(None, window, cx, |s| {
1905 s.select_display_ranges([
1906 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1907 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1908 ])
1909 });
1910
1911 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1912 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1913
1914 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1915 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1916
1917 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1918 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1919
1920 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1921 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1922
1923 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1924 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
1925
1926 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1927 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1928
1929 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1930 assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1931
1932 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1933 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1934
1935 editor.move_right(&MoveRight, window, cx);
1936 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1937 assert_selection_ranges(
1938 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1939 editor,
1940 cx,
1941 );
1942
1943 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1944 assert_selection_ranges(
1945 "use std«ˇ::s»tr::{foo, bar}\n\n«ˇ {b»az.qux()}",
1946 editor,
1947 cx,
1948 );
1949
1950 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1951 assert_selection_ranges(
1952 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1953 editor,
1954 cx,
1955 );
1956 });
1957}
1958
1959#[gpui::test]
1960fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1961 init_test(cx, |_| {});
1962
1963 let editor = cx.add_window(|window, cx| {
1964 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1965 build_editor(buffer, window, cx)
1966 });
1967
1968 _ = editor.update(cx, |editor, window, cx| {
1969 editor.set_wrap_width(Some(140.0.into()), cx);
1970 assert_eq!(
1971 editor.display_text(cx),
1972 "use one::{\n two::three::\n four::five\n};"
1973 );
1974
1975 editor.change_selections(None, window, cx, |s| {
1976 s.select_display_ranges([
1977 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1978 ]);
1979 });
1980
1981 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1982 assert_eq!(
1983 editor.selections.display_ranges(cx),
1984 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1985 );
1986
1987 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1988 assert_eq!(
1989 editor.selections.display_ranges(cx),
1990 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1991 );
1992
1993 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1994 assert_eq!(
1995 editor.selections.display_ranges(cx),
1996 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1997 );
1998
1999 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2000 assert_eq!(
2001 editor.selections.display_ranges(cx),
2002 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2003 );
2004
2005 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2006 assert_eq!(
2007 editor.selections.display_ranges(cx),
2008 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2009 );
2010
2011 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2012 assert_eq!(
2013 editor.selections.display_ranges(cx),
2014 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2015 );
2016 });
2017}
2018
2019#[gpui::test]
2020async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2021 init_test(cx, |_| {});
2022 let mut cx = EditorTestContext::new(cx).await;
2023
2024 let line_height = cx.editor(|editor, window, _| {
2025 editor
2026 .style()
2027 .unwrap()
2028 .text
2029 .line_height_in_pixels(window.rem_size())
2030 });
2031 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2032
2033 cx.set_state(
2034 &r#"ˇone
2035 two
2036
2037 three
2038 fourˇ
2039 five
2040
2041 six"#
2042 .unindent(),
2043 );
2044
2045 cx.update_editor(|editor, window, cx| {
2046 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2047 });
2048 cx.assert_editor_state(
2049 &r#"one
2050 two
2051 ˇ
2052 three
2053 four
2054 five
2055 ˇ
2056 six"#
2057 .unindent(),
2058 );
2059
2060 cx.update_editor(|editor, window, cx| {
2061 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2062 });
2063 cx.assert_editor_state(
2064 &r#"one
2065 two
2066
2067 three
2068 four
2069 five
2070 ˇ
2071 sixˇ"#
2072 .unindent(),
2073 );
2074
2075 cx.update_editor(|editor, window, cx| {
2076 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2077 });
2078 cx.assert_editor_state(
2079 &r#"one
2080 two
2081
2082 three
2083 four
2084 five
2085
2086 sixˇ"#
2087 .unindent(),
2088 );
2089
2090 cx.update_editor(|editor, window, cx| {
2091 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2092 });
2093 cx.assert_editor_state(
2094 &r#"one
2095 two
2096
2097 three
2098 four
2099 five
2100 ˇ
2101 six"#
2102 .unindent(),
2103 );
2104
2105 cx.update_editor(|editor, window, cx| {
2106 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2107 });
2108 cx.assert_editor_state(
2109 &r#"one
2110 two
2111 ˇ
2112 three
2113 four
2114 five
2115
2116 six"#
2117 .unindent(),
2118 );
2119
2120 cx.update_editor(|editor, window, cx| {
2121 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2122 });
2123 cx.assert_editor_state(
2124 &r#"ˇone
2125 two
2126
2127 three
2128 four
2129 five
2130
2131 six"#
2132 .unindent(),
2133 );
2134}
2135
2136#[gpui::test]
2137async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2138 init_test(cx, |_| {});
2139 let mut cx = EditorTestContext::new(cx).await;
2140 let line_height = cx.editor(|editor, window, _| {
2141 editor
2142 .style()
2143 .unwrap()
2144 .text
2145 .line_height_in_pixels(window.rem_size())
2146 });
2147 let window = cx.window;
2148 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2149
2150 cx.set_state(
2151 r#"ˇone
2152 two
2153 three
2154 four
2155 five
2156 six
2157 seven
2158 eight
2159 nine
2160 ten
2161 "#,
2162 );
2163
2164 cx.update_editor(|editor, window, cx| {
2165 assert_eq!(
2166 editor.snapshot(window, cx).scroll_position(),
2167 gpui::Point::new(0., 0.)
2168 );
2169 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2170 assert_eq!(
2171 editor.snapshot(window, cx).scroll_position(),
2172 gpui::Point::new(0., 3.)
2173 );
2174 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2175 assert_eq!(
2176 editor.snapshot(window, cx).scroll_position(),
2177 gpui::Point::new(0., 6.)
2178 );
2179 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2180 assert_eq!(
2181 editor.snapshot(window, cx).scroll_position(),
2182 gpui::Point::new(0., 3.)
2183 );
2184
2185 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2186 assert_eq!(
2187 editor.snapshot(window, cx).scroll_position(),
2188 gpui::Point::new(0., 1.)
2189 );
2190 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2191 assert_eq!(
2192 editor.snapshot(window, cx).scroll_position(),
2193 gpui::Point::new(0., 3.)
2194 );
2195 });
2196}
2197
2198#[gpui::test]
2199async fn test_autoscroll(cx: &mut TestAppContext) {
2200 init_test(cx, |_| {});
2201 let mut cx = EditorTestContext::new(cx).await;
2202
2203 let line_height = cx.update_editor(|editor, window, cx| {
2204 editor.set_vertical_scroll_margin(2, cx);
2205 editor
2206 .style()
2207 .unwrap()
2208 .text
2209 .line_height_in_pixels(window.rem_size())
2210 });
2211 let window = cx.window;
2212 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2213
2214 cx.set_state(
2215 r#"ˇone
2216 two
2217 three
2218 four
2219 five
2220 six
2221 seven
2222 eight
2223 nine
2224 ten
2225 "#,
2226 );
2227 cx.update_editor(|editor, window, cx| {
2228 assert_eq!(
2229 editor.snapshot(window, cx).scroll_position(),
2230 gpui::Point::new(0., 0.0)
2231 );
2232 });
2233
2234 // Add a cursor below the visible area. Since both cursors cannot fit
2235 // on screen, the editor autoscrolls to reveal the newest cursor, and
2236 // allows the vertical scroll margin below that cursor.
2237 cx.update_editor(|editor, window, cx| {
2238 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2239 selections.select_ranges([
2240 Point::new(0, 0)..Point::new(0, 0),
2241 Point::new(6, 0)..Point::new(6, 0),
2242 ]);
2243 })
2244 });
2245 cx.update_editor(|editor, window, cx| {
2246 assert_eq!(
2247 editor.snapshot(window, cx).scroll_position(),
2248 gpui::Point::new(0., 3.0)
2249 );
2250 });
2251
2252 // Move down. The editor cursor scrolls down to track the newest cursor.
2253 cx.update_editor(|editor, window, cx| {
2254 editor.move_down(&Default::default(), window, cx);
2255 });
2256 cx.update_editor(|editor, window, cx| {
2257 assert_eq!(
2258 editor.snapshot(window, cx).scroll_position(),
2259 gpui::Point::new(0., 4.0)
2260 );
2261 });
2262
2263 // Add a cursor above the visible area. Since both cursors fit on screen,
2264 // the editor scrolls to show both.
2265 cx.update_editor(|editor, window, cx| {
2266 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2267 selections.select_ranges([
2268 Point::new(1, 0)..Point::new(1, 0),
2269 Point::new(6, 0)..Point::new(6, 0),
2270 ]);
2271 })
2272 });
2273 cx.update_editor(|editor, window, cx| {
2274 assert_eq!(
2275 editor.snapshot(window, cx).scroll_position(),
2276 gpui::Point::new(0., 1.0)
2277 );
2278 });
2279}
2280
2281#[gpui::test]
2282async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2283 init_test(cx, |_| {});
2284 let mut cx = EditorTestContext::new(cx).await;
2285
2286 let line_height = cx.editor(|editor, window, _cx| {
2287 editor
2288 .style()
2289 .unwrap()
2290 .text
2291 .line_height_in_pixels(window.rem_size())
2292 });
2293 let window = cx.window;
2294 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2295 cx.set_state(
2296 &r#"
2297 ˇone
2298 two
2299 threeˇ
2300 four
2301 five
2302 six
2303 seven
2304 eight
2305 nine
2306 ten
2307 "#
2308 .unindent(),
2309 );
2310
2311 cx.update_editor(|editor, window, cx| {
2312 editor.move_page_down(&MovePageDown::default(), window, cx)
2313 });
2314 cx.assert_editor_state(
2315 &r#"
2316 one
2317 two
2318 three
2319 ˇfour
2320 five
2321 sixˇ
2322 seven
2323 eight
2324 nine
2325 ten
2326 "#
2327 .unindent(),
2328 );
2329
2330 cx.update_editor(|editor, window, cx| {
2331 editor.move_page_down(&MovePageDown::default(), window, cx)
2332 });
2333 cx.assert_editor_state(
2334 &r#"
2335 one
2336 two
2337 three
2338 four
2339 five
2340 six
2341 ˇseven
2342 eight
2343 nineˇ
2344 ten
2345 "#
2346 .unindent(),
2347 );
2348
2349 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2350 cx.assert_editor_state(
2351 &r#"
2352 one
2353 two
2354 three
2355 ˇfour
2356 five
2357 sixˇ
2358 seven
2359 eight
2360 nine
2361 ten
2362 "#
2363 .unindent(),
2364 );
2365
2366 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2367 cx.assert_editor_state(
2368 &r#"
2369 ˇone
2370 two
2371 threeˇ
2372 four
2373 five
2374 six
2375 seven
2376 eight
2377 nine
2378 ten
2379 "#
2380 .unindent(),
2381 );
2382
2383 // Test select collapsing
2384 cx.update_editor(|editor, window, cx| {
2385 editor.move_page_down(&MovePageDown::default(), window, cx);
2386 editor.move_page_down(&MovePageDown::default(), window, cx);
2387 editor.move_page_down(&MovePageDown::default(), window, cx);
2388 });
2389 cx.assert_editor_state(
2390 &r#"
2391 one
2392 two
2393 three
2394 four
2395 five
2396 six
2397 seven
2398 eight
2399 nine
2400 ˇten
2401 ˇ"#
2402 .unindent(),
2403 );
2404}
2405
2406#[gpui::test]
2407async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2408 init_test(cx, |_| {});
2409 let mut cx = EditorTestContext::new(cx).await;
2410 cx.set_state("one «two threeˇ» four");
2411 cx.update_editor(|editor, window, cx| {
2412 editor.delete_to_beginning_of_line(
2413 &DeleteToBeginningOfLine {
2414 stop_at_indent: false,
2415 },
2416 window,
2417 cx,
2418 );
2419 assert_eq!(editor.text(cx), " four");
2420 });
2421}
2422
2423#[gpui::test]
2424fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2425 init_test(cx, |_| {});
2426
2427 let editor = cx.add_window(|window, cx| {
2428 let buffer = MultiBuffer::build_simple("one two three four", cx);
2429 build_editor(buffer.clone(), window, cx)
2430 });
2431
2432 _ = editor.update(cx, |editor, window, cx| {
2433 editor.change_selections(None, window, cx, |s| {
2434 s.select_display_ranges([
2435 // an empty selection - the preceding word fragment is deleted
2436 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2437 // characters selected - they are deleted
2438 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2439 ])
2440 });
2441 editor.delete_to_previous_word_start(
2442 &DeleteToPreviousWordStart {
2443 ignore_newlines: false,
2444 },
2445 window,
2446 cx,
2447 );
2448 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2449 });
2450
2451 _ = editor.update(cx, |editor, window, cx| {
2452 editor.change_selections(None, window, cx, |s| {
2453 s.select_display_ranges([
2454 // an empty selection - the following word fragment is deleted
2455 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2456 // characters selected - they are deleted
2457 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2458 ])
2459 });
2460 editor.delete_to_next_word_end(
2461 &DeleteToNextWordEnd {
2462 ignore_newlines: false,
2463 },
2464 window,
2465 cx,
2466 );
2467 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2468 });
2469}
2470
2471#[gpui::test]
2472fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2473 init_test(cx, |_| {});
2474
2475 let editor = cx.add_window(|window, cx| {
2476 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2477 build_editor(buffer.clone(), window, cx)
2478 });
2479 let del_to_prev_word_start = DeleteToPreviousWordStart {
2480 ignore_newlines: false,
2481 };
2482 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2483 ignore_newlines: true,
2484 };
2485
2486 _ = editor.update(cx, |editor, window, cx| {
2487 editor.change_selections(None, window, cx, |s| {
2488 s.select_display_ranges([
2489 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2490 ])
2491 });
2492 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2493 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2494 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2495 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2496 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2497 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2498 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2499 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2500 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2501 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2502 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2503 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2504 });
2505}
2506
2507#[gpui::test]
2508fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2509 init_test(cx, |_| {});
2510
2511 let editor = cx.add_window(|window, cx| {
2512 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2513 build_editor(buffer.clone(), window, cx)
2514 });
2515 let del_to_next_word_end = DeleteToNextWordEnd {
2516 ignore_newlines: false,
2517 };
2518 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2519 ignore_newlines: true,
2520 };
2521
2522 _ = editor.update(cx, |editor, window, cx| {
2523 editor.change_selections(None, window, cx, |s| {
2524 s.select_display_ranges([
2525 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2526 ])
2527 });
2528 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2529 assert_eq!(
2530 editor.buffer.read(cx).read(cx).text(),
2531 "one\n two\nthree\n four"
2532 );
2533 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2534 assert_eq!(
2535 editor.buffer.read(cx).read(cx).text(),
2536 "\n two\nthree\n four"
2537 );
2538 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2539 assert_eq!(
2540 editor.buffer.read(cx).read(cx).text(),
2541 "two\nthree\n four"
2542 );
2543 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2544 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2545 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2546 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2547 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2548 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2549 });
2550}
2551
2552#[gpui::test]
2553fn test_newline(cx: &mut TestAppContext) {
2554 init_test(cx, |_| {});
2555
2556 let editor = cx.add_window(|window, cx| {
2557 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2558 build_editor(buffer.clone(), window, cx)
2559 });
2560
2561 _ = editor.update(cx, |editor, window, cx| {
2562 editor.change_selections(None, window, cx, |s| {
2563 s.select_display_ranges([
2564 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2565 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2566 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2567 ])
2568 });
2569
2570 editor.newline(&Newline, window, cx);
2571 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2572 });
2573}
2574
2575#[gpui::test]
2576fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2577 init_test(cx, |_| {});
2578
2579 let editor = cx.add_window(|window, cx| {
2580 let buffer = MultiBuffer::build_simple(
2581 "
2582 a
2583 b(
2584 X
2585 )
2586 c(
2587 X
2588 )
2589 "
2590 .unindent()
2591 .as_str(),
2592 cx,
2593 );
2594 let mut editor = build_editor(buffer.clone(), window, cx);
2595 editor.change_selections(None, window, cx, |s| {
2596 s.select_ranges([
2597 Point::new(2, 4)..Point::new(2, 5),
2598 Point::new(5, 4)..Point::new(5, 5),
2599 ])
2600 });
2601 editor
2602 });
2603
2604 _ = editor.update(cx, |editor, window, cx| {
2605 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2606 editor.buffer.update(cx, |buffer, cx| {
2607 buffer.edit(
2608 [
2609 (Point::new(1, 2)..Point::new(3, 0), ""),
2610 (Point::new(4, 2)..Point::new(6, 0), ""),
2611 ],
2612 None,
2613 cx,
2614 );
2615 assert_eq!(
2616 buffer.read(cx).text(),
2617 "
2618 a
2619 b()
2620 c()
2621 "
2622 .unindent()
2623 );
2624 });
2625 assert_eq!(
2626 editor.selections.ranges(cx),
2627 &[
2628 Point::new(1, 2)..Point::new(1, 2),
2629 Point::new(2, 2)..Point::new(2, 2),
2630 ],
2631 );
2632
2633 editor.newline(&Newline, window, cx);
2634 assert_eq!(
2635 editor.text(cx),
2636 "
2637 a
2638 b(
2639 )
2640 c(
2641 )
2642 "
2643 .unindent()
2644 );
2645
2646 // The selections are moved after the inserted newlines
2647 assert_eq!(
2648 editor.selections.ranges(cx),
2649 &[
2650 Point::new(2, 0)..Point::new(2, 0),
2651 Point::new(4, 0)..Point::new(4, 0),
2652 ],
2653 );
2654 });
2655}
2656
2657#[gpui::test]
2658async fn test_newline_above(cx: &mut TestAppContext) {
2659 init_test(cx, |settings| {
2660 settings.defaults.tab_size = NonZeroU32::new(4)
2661 });
2662
2663 let language = Arc::new(
2664 Language::new(
2665 LanguageConfig::default(),
2666 Some(tree_sitter_rust::LANGUAGE.into()),
2667 )
2668 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2669 .unwrap(),
2670 );
2671
2672 let mut cx = EditorTestContext::new(cx).await;
2673 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2674 cx.set_state(indoc! {"
2675 const a: ˇA = (
2676 (ˇ
2677 «const_functionˇ»(ˇ),
2678 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2679 )ˇ
2680 ˇ);ˇ
2681 "});
2682
2683 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2684 cx.assert_editor_state(indoc! {"
2685 ˇ
2686 const a: A = (
2687 ˇ
2688 (
2689 ˇ
2690 ˇ
2691 const_function(),
2692 ˇ
2693 ˇ
2694 ˇ
2695 ˇ
2696 something_else,
2697 ˇ
2698 )
2699 ˇ
2700 ˇ
2701 );
2702 "});
2703}
2704
2705#[gpui::test]
2706async fn test_newline_below(cx: &mut TestAppContext) {
2707 init_test(cx, |settings| {
2708 settings.defaults.tab_size = NonZeroU32::new(4)
2709 });
2710
2711 let language = Arc::new(
2712 Language::new(
2713 LanguageConfig::default(),
2714 Some(tree_sitter_rust::LANGUAGE.into()),
2715 )
2716 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2717 .unwrap(),
2718 );
2719
2720 let mut cx = EditorTestContext::new(cx).await;
2721 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2722 cx.set_state(indoc! {"
2723 const a: ˇA = (
2724 (ˇ
2725 «const_functionˇ»(ˇ),
2726 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2727 )ˇ
2728 ˇ);ˇ
2729 "});
2730
2731 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2732 cx.assert_editor_state(indoc! {"
2733 const a: A = (
2734 ˇ
2735 (
2736 ˇ
2737 const_function(),
2738 ˇ
2739 ˇ
2740 something_else,
2741 ˇ
2742 ˇ
2743 ˇ
2744 ˇ
2745 )
2746 ˇ
2747 );
2748 ˇ
2749 ˇ
2750 "});
2751}
2752
2753#[gpui::test]
2754async fn test_newline_comments(cx: &mut TestAppContext) {
2755 init_test(cx, |settings| {
2756 settings.defaults.tab_size = NonZeroU32::new(4)
2757 });
2758
2759 let language = Arc::new(Language::new(
2760 LanguageConfig {
2761 line_comments: vec!["// ".into()],
2762 ..LanguageConfig::default()
2763 },
2764 None,
2765 ));
2766 {
2767 let mut cx = EditorTestContext::new(cx).await;
2768 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2769 cx.set_state(indoc! {"
2770 // Fooˇ
2771 "});
2772
2773 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2774 cx.assert_editor_state(indoc! {"
2775 // Foo
2776 // ˇ
2777 "});
2778 // Ensure that we add comment prefix when existing line contains space
2779 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2780 cx.assert_editor_state(
2781 indoc! {"
2782 // Foo
2783 //s
2784 // ˇ
2785 "}
2786 .replace("s", " ") // s is used as space placeholder to prevent format on save
2787 .as_str(),
2788 );
2789 // Ensure that we add comment prefix when existing line does not contain space
2790 cx.set_state(indoc! {"
2791 // Foo
2792 //ˇ
2793 "});
2794 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2795 cx.assert_editor_state(indoc! {"
2796 // Foo
2797 //
2798 // ˇ
2799 "});
2800 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2801 cx.set_state(indoc! {"
2802 ˇ// Foo
2803 "});
2804 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2805 cx.assert_editor_state(indoc! {"
2806
2807 ˇ// Foo
2808 "});
2809 }
2810 // Ensure that comment continuations can be disabled.
2811 update_test_language_settings(cx, |settings| {
2812 settings.defaults.extend_comment_on_newline = Some(false);
2813 });
2814 let mut cx = EditorTestContext::new(cx).await;
2815 cx.set_state(indoc! {"
2816 // Fooˇ
2817 "});
2818 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2819 cx.assert_editor_state(indoc! {"
2820 // Foo
2821 ˇ
2822 "});
2823}
2824
2825#[gpui::test]
2826async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
2827 init_test(cx, |settings| {
2828 settings.defaults.tab_size = NonZeroU32::new(4)
2829 });
2830
2831 let language = Arc::new(Language::new(
2832 LanguageConfig {
2833 line_comments: vec!["// ".into(), "/// ".into()],
2834 ..LanguageConfig::default()
2835 },
2836 None,
2837 ));
2838 {
2839 let mut cx = EditorTestContext::new(cx).await;
2840 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2841 cx.set_state(indoc! {"
2842 //ˇ
2843 "});
2844 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2845 cx.assert_editor_state(indoc! {"
2846 //
2847 // ˇ
2848 "});
2849
2850 cx.set_state(indoc! {"
2851 ///ˇ
2852 "});
2853 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2854 cx.assert_editor_state(indoc! {"
2855 ///
2856 /// ˇ
2857 "});
2858 }
2859}
2860
2861#[gpui::test]
2862async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
2863 init_test(cx, |settings| {
2864 settings.defaults.tab_size = NonZeroU32::new(4)
2865 });
2866
2867 let language = Arc::new(
2868 Language::new(
2869 LanguageConfig {
2870 documentation: Some(language::DocumentationConfig {
2871 start: "/**".into(),
2872 end: "*/".into(),
2873 prefix: "* ".into(),
2874 tab_size: NonZeroU32::new(1).unwrap(),
2875 }),
2876
2877 ..LanguageConfig::default()
2878 },
2879 Some(tree_sitter_rust::LANGUAGE.into()),
2880 )
2881 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
2882 .unwrap(),
2883 );
2884
2885 {
2886 let mut cx = EditorTestContext::new(cx).await;
2887 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2888 cx.set_state(indoc! {"
2889 /**ˇ
2890 "});
2891
2892 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2893 cx.assert_editor_state(indoc! {"
2894 /**
2895 * ˇ
2896 "});
2897 // Ensure that if cursor is before the comment start,
2898 // we do not actually insert a comment prefix.
2899 cx.set_state(indoc! {"
2900 ˇ/**
2901 "});
2902 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2903 cx.assert_editor_state(indoc! {"
2904
2905 ˇ/**
2906 "});
2907 // Ensure that if cursor is between it doesn't add comment prefix.
2908 cx.set_state(indoc! {"
2909 /*ˇ*
2910 "});
2911 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2912 cx.assert_editor_state(indoc! {"
2913 /*
2914 ˇ*
2915 "});
2916 // Ensure that if suffix exists on same line after cursor it adds new line.
2917 cx.set_state(indoc! {"
2918 /**ˇ*/
2919 "});
2920 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2921 cx.assert_editor_state(indoc! {"
2922 /**
2923 * ˇ
2924 */
2925 "});
2926 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2927 cx.set_state(indoc! {"
2928 /**ˇ */
2929 "});
2930 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2931 cx.assert_editor_state(indoc! {"
2932 /**
2933 * ˇ
2934 */
2935 "});
2936 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2937 cx.set_state(indoc! {"
2938 /** ˇ*/
2939 "});
2940 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2941 cx.assert_editor_state(
2942 indoc! {"
2943 /**s
2944 * ˇ
2945 */
2946 "}
2947 .replace("s", " ") // s is used as space placeholder to prevent format on save
2948 .as_str(),
2949 );
2950 // Ensure that delimiter space is preserved when newline on already
2951 // spaced delimiter.
2952 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2953 cx.assert_editor_state(
2954 indoc! {"
2955 /**s
2956 *s
2957 * ˇ
2958 */
2959 "}
2960 .replace("s", " ") // s is used as space placeholder to prevent format on save
2961 .as_str(),
2962 );
2963 // Ensure that delimiter space is preserved when space is not
2964 // on existing delimiter.
2965 cx.set_state(indoc! {"
2966 /**
2967 *ˇ
2968 */
2969 "});
2970 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2971 cx.assert_editor_state(indoc! {"
2972 /**
2973 *
2974 * ˇ
2975 */
2976 "});
2977 // Ensure that if suffix exists on same line after cursor it
2978 // doesn't add extra new line if prefix is not on same line.
2979 cx.set_state(indoc! {"
2980 /**
2981 ˇ*/
2982 "});
2983 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2984 cx.assert_editor_state(indoc! {"
2985 /**
2986
2987 ˇ*/
2988 "});
2989 // Ensure that it detects suffix after existing prefix.
2990 cx.set_state(indoc! {"
2991 /**ˇ/
2992 "});
2993 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2994 cx.assert_editor_state(indoc! {"
2995 /**
2996 ˇ/
2997 "});
2998 // Ensure that if suffix exists on same line before
2999 // cursor it does not add comment prefix.
3000 cx.set_state(indoc! {"
3001 /** */ˇ
3002 "});
3003 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3004 cx.assert_editor_state(indoc! {"
3005 /** */
3006 ˇ
3007 "});
3008 // Ensure that if suffix exists on same line before
3009 // cursor it does not add comment prefix.
3010 cx.set_state(indoc! {"
3011 /**
3012 *
3013 */ˇ
3014 "});
3015 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3016 cx.assert_editor_state(indoc! {"
3017 /**
3018 *
3019 */
3020 ˇ
3021 "});
3022
3023 // Ensure that inline comment followed by code
3024 // doesn't add comment prefix on newline
3025 cx.set_state(indoc! {"
3026 /** */ textˇ
3027 "});
3028 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3029 cx.assert_editor_state(indoc! {"
3030 /** */ text
3031 ˇ
3032 "});
3033
3034 // Ensure that text after comment end tag
3035 // doesn't add comment prefix on newline
3036 cx.set_state(indoc! {"
3037 /**
3038 *
3039 */ˇtext
3040 "});
3041 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3042 cx.assert_editor_state(indoc! {"
3043 /**
3044 *
3045 */
3046 ˇtext
3047 "});
3048
3049 // Ensure if not comment block it doesn't
3050 // add comment prefix on newline
3051 cx.set_state(indoc! {"
3052 * textˇ
3053 "});
3054 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3055 cx.assert_editor_state(indoc! {"
3056 * text
3057 ˇ
3058 "});
3059 }
3060 // Ensure that comment continuations can be disabled.
3061 update_test_language_settings(cx, |settings| {
3062 settings.defaults.extend_comment_on_newline = Some(false);
3063 });
3064 let mut cx = EditorTestContext::new(cx).await;
3065 cx.set_state(indoc! {"
3066 /**ˇ
3067 "});
3068 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3069 cx.assert_editor_state(indoc! {"
3070 /**
3071 ˇ
3072 "});
3073}
3074
3075#[gpui::test]
3076fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3077 init_test(cx, |_| {});
3078
3079 let editor = cx.add_window(|window, cx| {
3080 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3081 let mut editor = build_editor(buffer.clone(), window, cx);
3082 editor.change_selections(None, window, cx, |s| {
3083 s.select_ranges([3..4, 11..12, 19..20])
3084 });
3085 editor
3086 });
3087
3088 _ = editor.update(cx, |editor, window, cx| {
3089 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3090 editor.buffer.update(cx, |buffer, cx| {
3091 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3092 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3093 });
3094 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3095
3096 editor.insert("Z", window, cx);
3097 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3098
3099 // The selections are moved after the inserted characters
3100 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3101 });
3102}
3103
3104#[gpui::test]
3105async fn test_tab(cx: &mut TestAppContext) {
3106 init_test(cx, |settings| {
3107 settings.defaults.tab_size = NonZeroU32::new(3)
3108 });
3109
3110 let mut cx = EditorTestContext::new(cx).await;
3111 cx.set_state(indoc! {"
3112 ˇabˇc
3113 ˇ🏀ˇ🏀ˇefg
3114 dˇ
3115 "});
3116 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3117 cx.assert_editor_state(indoc! {"
3118 ˇab ˇc
3119 ˇ🏀 ˇ🏀 ˇefg
3120 d ˇ
3121 "});
3122
3123 cx.set_state(indoc! {"
3124 a
3125 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3126 "});
3127 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3128 cx.assert_editor_state(indoc! {"
3129 a
3130 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3131 "});
3132}
3133
3134#[gpui::test]
3135async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3136 init_test(cx, |_| {});
3137
3138 let mut cx = EditorTestContext::new(cx).await;
3139 let language = Arc::new(
3140 Language::new(
3141 LanguageConfig::default(),
3142 Some(tree_sitter_rust::LANGUAGE.into()),
3143 )
3144 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3145 .unwrap(),
3146 );
3147 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3148
3149 // test when all cursors are not at suggested indent
3150 // then simply move to their suggested indent location
3151 cx.set_state(indoc! {"
3152 const a: B = (
3153 c(
3154 ˇ
3155 ˇ )
3156 );
3157 "});
3158 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3159 cx.assert_editor_state(indoc! {"
3160 const a: B = (
3161 c(
3162 ˇ
3163 ˇ)
3164 );
3165 "});
3166
3167 // test cursor already at suggested indent not moving when
3168 // other cursors are yet to reach their suggested indents
3169 cx.set_state(indoc! {"
3170 ˇ
3171 const a: B = (
3172 c(
3173 d(
3174 ˇ
3175 )
3176 ˇ
3177 ˇ )
3178 );
3179 "});
3180 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3181 cx.assert_editor_state(indoc! {"
3182 ˇ
3183 const a: B = (
3184 c(
3185 d(
3186 ˇ
3187 )
3188 ˇ
3189 ˇ)
3190 );
3191 "});
3192 // test when all cursors are at suggested indent then tab is inserted
3193 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3194 cx.assert_editor_state(indoc! {"
3195 ˇ
3196 const a: B = (
3197 c(
3198 d(
3199 ˇ
3200 )
3201 ˇ
3202 ˇ)
3203 );
3204 "});
3205
3206 // test when current indent is less than suggested indent,
3207 // we adjust line to match suggested indent and move cursor to it
3208 //
3209 // when no other cursor is at word boundary, all of them should move
3210 cx.set_state(indoc! {"
3211 const a: B = (
3212 c(
3213 d(
3214 ˇ
3215 ˇ )
3216 ˇ )
3217 );
3218 "});
3219 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3220 cx.assert_editor_state(indoc! {"
3221 const a: B = (
3222 c(
3223 d(
3224 ˇ
3225 ˇ)
3226 ˇ)
3227 );
3228 "});
3229
3230 // test when current indent is less than suggested indent,
3231 // we adjust line to match suggested indent and move cursor to it
3232 //
3233 // when some other cursor is at word boundary, it should not move
3234 cx.set_state(indoc! {"
3235 const a: B = (
3236 c(
3237 d(
3238 ˇ
3239 ˇ )
3240 ˇ)
3241 );
3242 "});
3243 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3244 cx.assert_editor_state(indoc! {"
3245 const a: B = (
3246 c(
3247 d(
3248 ˇ
3249 ˇ)
3250 ˇ)
3251 );
3252 "});
3253
3254 // test when current indent is more than suggested indent,
3255 // we just move cursor to current indent instead of suggested indent
3256 //
3257 // when no other cursor is at word boundary, all of them should move
3258 cx.set_state(indoc! {"
3259 const a: B = (
3260 c(
3261 d(
3262 ˇ
3263 ˇ )
3264 ˇ )
3265 );
3266 "});
3267 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3268 cx.assert_editor_state(indoc! {"
3269 const a: B = (
3270 c(
3271 d(
3272 ˇ
3273 ˇ)
3274 ˇ)
3275 );
3276 "});
3277 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3278 cx.assert_editor_state(indoc! {"
3279 const a: B = (
3280 c(
3281 d(
3282 ˇ
3283 ˇ)
3284 ˇ)
3285 );
3286 "});
3287
3288 // test when current indent is more than suggested indent,
3289 // we just move cursor to current indent instead of suggested indent
3290 //
3291 // when some other cursor is at word boundary, it doesn't move
3292 cx.set_state(indoc! {"
3293 const a: B = (
3294 c(
3295 d(
3296 ˇ
3297 ˇ )
3298 ˇ)
3299 );
3300 "});
3301 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3302 cx.assert_editor_state(indoc! {"
3303 const a: B = (
3304 c(
3305 d(
3306 ˇ
3307 ˇ)
3308 ˇ)
3309 );
3310 "});
3311
3312 // handle auto-indent when there are multiple cursors on the same line
3313 cx.set_state(indoc! {"
3314 const a: B = (
3315 c(
3316 ˇ ˇ
3317 ˇ )
3318 );
3319 "});
3320 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3321 cx.assert_editor_state(indoc! {"
3322 const a: B = (
3323 c(
3324 ˇ
3325 ˇ)
3326 );
3327 "});
3328}
3329
3330#[gpui::test]
3331async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3332 init_test(cx, |settings| {
3333 settings.defaults.tab_size = NonZeroU32::new(3)
3334 });
3335
3336 let mut cx = EditorTestContext::new(cx).await;
3337 cx.set_state(indoc! {"
3338 ˇ
3339 \t ˇ
3340 \t ˇ
3341 \t ˇ
3342 \t \t\t \t \t\t \t\t \t \t ˇ
3343 "});
3344
3345 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3346 cx.assert_editor_state(indoc! {"
3347 ˇ
3348 \t ˇ
3349 \t ˇ
3350 \t ˇ
3351 \t \t\t \t \t\t \t\t \t \t ˇ
3352 "});
3353}
3354
3355#[gpui::test]
3356async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3357 init_test(cx, |settings| {
3358 settings.defaults.tab_size = NonZeroU32::new(4)
3359 });
3360
3361 let language = Arc::new(
3362 Language::new(
3363 LanguageConfig::default(),
3364 Some(tree_sitter_rust::LANGUAGE.into()),
3365 )
3366 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3367 .unwrap(),
3368 );
3369
3370 let mut cx = EditorTestContext::new(cx).await;
3371 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3372 cx.set_state(indoc! {"
3373 fn a() {
3374 if b {
3375 \t ˇc
3376 }
3377 }
3378 "});
3379
3380 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3381 cx.assert_editor_state(indoc! {"
3382 fn a() {
3383 if b {
3384 ˇc
3385 }
3386 }
3387 "});
3388}
3389
3390#[gpui::test]
3391async fn test_indent_outdent(cx: &mut TestAppContext) {
3392 init_test(cx, |settings| {
3393 settings.defaults.tab_size = NonZeroU32::new(4);
3394 });
3395
3396 let mut cx = EditorTestContext::new(cx).await;
3397
3398 cx.set_state(indoc! {"
3399 «oneˇ» «twoˇ»
3400 three
3401 four
3402 "});
3403 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3404 cx.assert_editor_state(indoc! {"
3405 «oneˇ» «twoˇ»
3406 three
3407 four
3408 "});
3409
3410 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3411 cx.assert_editor_state(indoc! {"
3412 «oneˇ» «twoˇ»
3413 three
3414 four
3415 "});
3416
3417 // select across line ending
3418 cx.set_state(indoc! {"
3419 one two
3420 t«hree
3421 ˇ» four
3422 "});
3423 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3424 cx.assert_editor_state(indoc! {"
3425 one two
3426 t«hree
3427 ˇ» four
3428 "});
3429
3430 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3431 cx.assert_editor_state(indoc! {"
3432 one two
3433 t«hree
3434 ˇ» four
3435 "});
3436
3437 // Ensure that indenting/outdenting works when the cursor is at column 0.
3438 cx.set_state(indoc! {"
3439 one two
3440 ˇthree
3441 four
3442 "});
3443 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3444 cx.assert_editor_state(indoc! {"
3445 one two
3446 ˇthree
3447 four
3448 "});
3449
3450 cx.set_state(indoc! {"
3451 one two
3452 ˇ three
3453 four
3454 "});
3455 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3456 cx.assert_editor_state(indoc! {"
3457 one two
3458 ˇthree
3459 four
3460 "});
3461}
3462
3463#[gpui::test]
3464async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3465 init_test(cx, |settings| {
3466 settings.defaults.hard_tabs = Some(true);
3467 });
3468
3469 let mut cx = EditorTestContext::new(cx).await;
3470
3471 // select two ranges on one line
3472 cx.set_state(indoc! {"
3473 «oneˇ» «twoˇ»
3474 three
3475 four
3476 "});
3477 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3478 cx.assert_editor_state(indoc! {"
3479 \t«oneˇ» «twoˇ»
3480 three
3481 four
3482 "});
3483 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3484 cx.assert_editor_state(indoc! {"
3485 \t\t«oneˇ» «twoˇ»
3486 three
3487 four
3488 "});
3489 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3490 cx.assert_editor_state(indoc! {"
3491 \t«oneˇ» «twoˇ»
3492 three
3493 four
3494 "});
3495 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3496 cx.assert_editor_state(indoc! {"
3497 «oneˇ» «twoˇ»
3498 three
3499 four
3500 "});
3501
3502 // select across a line ending
3503 cx.set_state(indoc! {"
3504 one two
3505 t«hree
3506 ˇ»four
3507 "});
3508 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3509 cx.assert_editor_state(indoc! {"
3510 one two
3511 \tt«hree
3512 ˇ»four
3513 "});
3514 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3515 cx.assert_editor_state(indoc! {"
3516 one two
3517 \t\tt«hree
3518 ˇ»four
3519 "});
3520 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3521 cx.assert_editor_state(indoc! {"
3522 one two
3523 \tt«hree
3524 ˇ»four
3525 "});
3526 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3527 cx.assert_editor_state(indoc! {"
3528 one two
3529 t«hree
3530 ˇ»four
3531 "});
3532
3533 // Ensure that indenting/outdenting works when the cursor is at column 0.
3534 cx.set_state(indoc! {"
3535 one two
3536 ˇthree
3537 four
3538 "});
3539 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3540 cx.assert_editor_state(indoc! {"
3541 one two
3542 ˇthree
3543 four
3544 "});
3545 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3546 cx.assert_editor_state(indoc! {"
3547 one two
3548 \tˇthree
3549 four
3550 "});
3551 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3552 cx.assert_editor_state(indoc! {"
3553 one two
3554 ˇthree
3555 four
3556 "});
3557}
3558
3559#[gpui::test]
3560fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3561 init_test(cx, |settings| {
3562 settings.languages.extend([
3563 (
3564 "TOML".into(),
3565 LanguageSettingsContent {
3566 tab_size: NonZeroU32::new(2),
3567 ..Default::default()
3568 },
3569 ),
3570 (
3571 "Rust".into(),
3572 LanguageSettingsContent {
3573 tab_size: NonZeroU32::new(4),
3574 ..Default::default()
3575 },
3576 ),
3577 ]);
3578 });
3579
3580 let toml_language = Arc::new(Language::new(
3581 LanguageConfig {
3582 name: "TOML".into(),
3583 ..Default::default()
3584 },
3585 None,
3586 ));
3587 let rust_language = Arc::new(Language::new(
3588 LanguageConfig {
3589 name: "Rust".into(),
3590 ..Default::default()
3591 },
3592 None,
3593 ));
3594
3595 let toml_buffer =
3596 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3597 let rust_buffer =
3598 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3599 let multibuffer = cx.new(|cx| {
3600 let mut multibuffer = MultiBuffer::new(ReadWrite);
3601 multibuffer.push_excerpts(
3602 toml_buffer.clone(),
3603 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3604 cx,
3605 );
3606 multibuffer.push_excerpts(
3607 rust_buffer.clone(),
3608 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3609 cx,
3610 );
3611 multibuffer
3612 });
3613
3614 cx.add_window(|window, cx| {
3615 let mut editor = build_editor(multibuffer, window, cx);
3616
3617 assert_eq!(
3618 editor.text(cx),
3619 indoc! {"
3620 a = 1
3621 b = 2
3622
3623 const c: usize = 3;
3624 "}
3625 );
3626
3627 select_ranges(
3628 &mut editor,
3629 indoc! {"
3630 «aˇ» = 1
3631 b = 2
3632
3633 «const c:ˇ» usize = 3;
3634 "},
3635 window,
3636 cx,
3637 );
3638
3639 editor.tab(&Tab, window, cx);
3640 assert_text_with_selections(
3641 &mut editor,
3642 indoc! {"
3643 «aˇ» = 1
3644 b = 2
3645
3646 «const c:ˇ» usize = 3;
3647 "},
3648 cx,
3649 );
3650 editor.backtab(&Backtab, window, cx);
3651 assert_text_with_selections(
3652 &mut editor,
3653 indoc! {"
3654 «aˇ» = 1
3655 b = 2
3656
3657 «const c:ˇ» usize = 3;
3658 "},
3659 cx,
3660 );
3661
3662 editor
3663 });
3664}
3665
3666#[gpui::test]
3667async fn test_backspace(cx: &mut TestAppContext) {
3668 init_test(cx, |_| {});
3669
3670 let mut cx = EditorTestContext::new(cx).await;
3671
3672 // Basic backspace
3673 cx.set_state(indoc! {"
3674 onˇe two three
3675 fou«rˇ» five six
3676 seven «ˇeight nine
3677 »ten
3678 "});
3679 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3680 cx.assert_editor_state(indoc! {"
3681 oˇe two three
3682 fouˇ five six
3683 seven ˇten
3684 "});
3685
3686 // Test backspace inside and around indents
3687 cx.set_state(indoc! {"
3688 zero
3689 ˇone
3690 ˇtwo
3691 ˇ ˇ ˇ three
3692 ˇ ˇ four
3693 "});
3694 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3695 cx.assert_editor_state(indoc! {"
3696 zero
3697 ˇone
3698 ˇtwo
3699 ˇ threeˇ four
3700 "});
3701}
3702
3703#[gpui::test]
3704async fn test_delete(cx: &mut TestAppContext) {
3705 init_test(cx, |_| {});
3706
3707 let mut cx = EditorTestContext::new(cx).await;
3708 cx.set_state(indoc! {"
3709 onˇe two three
3710 fou«rˇ» five six
3711 seven «ˇeight nine
3712 »ten
3713 "});
3714 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3715 cx.assert_editor_state(indoc! {"
3716 onˇ two three
3717 fouˇ five six
3718 seven ˇten
3719 "});
3720}
3721
3722#[gpui::test]
3723fn test_delete_line(cx: &mut TestAppContext) {
3724 init_test(cx, |_| {});
3725
3726 let editor = cx.add_window(|window, cx| {
3727 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3728 build_editor(buffer, window, cx)
3729 });
3730 _ = editor.update(cx, |editor, window, cx| {
3731 editor.change_selections(None, window, cx, |s| {
3732 s.select_display_ranges([
3733 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3734 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3735 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3736 ])
3737 });
3738 editor.delete_line(&DeleteLine, window, cx);
3739 assert_eq!(editor.display_text(cx), "ghi");
3740 assert_eq!(
3741 editor.selections.display_ranges(cx),
3742 vec![
3743 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3744 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3745 ]
3746 );
3747 });
3748
3749 let editor = cx.add_window(|window, cx| {
3750 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3751 build_editor(buffer, window, cx)
3752 });
3753 _ = editor.update(cx, |editor, window, cx| {
3754 editor.change_selections(None, window, cx, |s| {
3755 s.select_display_ranges([
3756 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3757 ])
3758 });
3759 editor.delete_line(&DeleteLine, window, cx);
3760 assert_eq!(editor.display_text(cx), "ghi\n");
3761 assert_eq!(
3762 editor.selections.display_ranges(cx),
3763 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3764 );
3765 });
3766}
3767
3768#[gpui::test]
3769fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3770 init_test(cx, |_| {});
3771
3772 cx.add_window(|window, cx| {
3773 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3774 let mut editor = build_editor(buffer.clone(), window, cx);
3775 let buffer = buffer.read(cx).as_singleton().unwrap();
3776
3777 assert_eq!(
3778 editor.selections.ranges::<Point>(cx),
3779 &[Point::new(0, 0)..Point::new(0, 0)]
3780 );
3781
3782 // When on single line, replace newline at end by space
3783 editor.join_lines(&JoinLines, window, cx);
3784 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3785 assert_eq!(
3786 editor.selections.ranges::<Point>(cx),
3787 &[Point::new(0, 3)..Point::new(0, 3)]
3788 );
3789
3790 // When multiple lines are selected, remove newlines that are spanned by the selection
3791 editor.change_selections(None, window, cx, |s| {
3792 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3793 });
3794 editor.join_lines(&JoinLines, window, cx);
3795 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3796 assert_eq!(
3797 editor.selections.ranges::<Point>(cx),
3798 &[Point::new(0, 11)..Point::new(0, 11)]
3799 );
3800
3801 // Undo should be transactional
3802 editor.undo(&Undo, window, cx);
3803 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3804 assert_eq!(
3805 editor.selections.ranges::<Point>(cx),
3806 &[Point::new(0, 5)..Point::new(2, 2)]
3807 );
3808
3809 // When joining an empty line don't insert a space
3810 editor.change_selections(None, window, cx, |s| {
3811 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3812 });
3813 editor.join_lines(&JoinLines, window, cx);
3814 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3815 assert_eq!(
3816 editor.selections.ranges::<Point>(cx),
3817 [Point::new(2, 3)..Point::new(2, 3)]
3818 );
3819
3820 // We can remove trailing newlines
3821 editor.join_lines(&JoinLines, window, cx);
3822 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3823 assert_eq!(
3824 editor.selections.ranges::<Point>(cx),
3825 [Point::new(2, 3)..Point::new(2, 3)]
3826 );
3827
3828 // We don't blow up on the last line
3829 editor.join_lines(&JoinLines, window, cx);
3830 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3831 assert_eq!(
3832 editor.selections.ranges::<Point>(cx),
3833 [Point::new(2, 3)..Point::new(2, 3)]
3834 );
3835
3836 // reset to test indentation
3837 editor.buffer.update(cx, |buffer, cx| {
3838 buffer.edit(
3839 [
3840 (Point::new(1, 0)..Point::new(1, 2), " "),
3841 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3842 ],
3843 None,
3844 cx,
3845 )
3846 });
3847
3848 // We remove any leading spaces
3849 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3850 editor.change_selections(None, window, cx, |s| {
3851 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3852 });
3853 editor.join_lines(&JoinLines, window, cx);
3854 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3855
3856 // We don't insert a space for a line containing only spaces
3857 editor.join_lines(&JoinLines, window, cx);
3858 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3859
3860 // We ignore any leading tabs
3861 editor.join_lines(&JoinLines, window, cx);
3862 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3863
3864 editor
3865 });
3866}
3867
3868#[gpui::test]
3869fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3870 init_test(cx, |_| {});
3871
3872 cx.add_window(|window, cx| {
3873 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3874 let mut editor = build_editor(buffer.clone(), window, cx);
3875 let buffer = buffer.read(cx).as_singleton().unwrap();
3876
3877 editor.change_selections(None, window, cx, |s| {
3878 s.select_ranges([
3879 Point::new(0, 2)..Point::new(1, 1),
3880 Point::new(1, 2)..Point::new(1, 2),
3881 Point::new(3, 1)..Point::new(3, 2),
3882 ])
3883 });
3884
3885 editor.join_lines(&JoinLines, window, cx);
3886 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3887
3888 assert_eq!(
3889 editor.selections.ranges::<Point>(cx),
3890 [
3891 Point::new(0, 7)..Point::new(0, 7),
3892 Point::new(1, 3)..Point::new(1, 3)
3893 ]
3894 );
3895 editor
3896 });
3897}
3898
3899#[gpui::test]
3900async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3901 init_test(cx, |_| {});
3902
3903 let mut cx = EditorTestContext::new(cx).await;
3904
3905 let diff_base = r#"
3906 Line 0
3907 Line 1
3908 Line 2
3909 Line 3
3910 "#
3911 .unindent();
3912
3913 cx.set_state(
3914 &r#"
3915 ˇLine 0
3916 Line 1
3917 Line 2
3918 Line 3
3919 "#
3920 .unindent(),
3921 );
3922
3923 cx.set_head_text(&diff_base);
3924 executor.run_until_parked();
3925
3926 // Join lines
3927 cx.update_editor(|editor, window, cx| {
3928 editor.join_lines(&JoinLines, window, cx);
3929 });
3930 executor.run_until_parked();
3931
3932 cx.assert_editor_state(
3933 &r#"
3934 Line 0ˇ Line 1
3935 Line 2
3936 Line 3
3937 "#
3938 .unindent(),
3939 );
3940 // Join again
3941 cx.update_editor(|editor, window, cx| {
3942 editor.join_lines(&JoinLines, window, cx);
3943 });
3944 executor.run_until_parked();
3945
3946 cx.assert_editor_state(
3947 &r#"
3948 Line 0 Line 1ˇ Line 2
3949 Line 3
3950 "#
3951 .unindent(),
3952 );
3953}
3954
3955#[gpui::test]
3956async fn test_custom_newlines_cause_no_false_positive_diffs(
3957 executor: BackgroundExecutor,
3958 cx: &mut TestAppContext,
3959) {
3960 init_test(cx, |_| {});
3961 let mut cx = EditorTestContext::new(cx).await;
3962 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3963 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3964 executor.run_until_parked();
3965
3966 cx.update_editor(|editor, window, cx| {
3967 let snapshot = editor.snapshot(window, cx);
3968 assert_eq!(
3969 snapshot
3970 .buffer_snapshot
3971 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3972 .collect::<Vec<_>>(),
3973 Vec::new(),
3974 "Should not have any diffs for files with custom newlines"
3975 );
3976 });
3977}
3978
3979#[gpui::test]
3980async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3981 init_test(cx, |_| {});
3982
3983 let mut cx = EditorTestContext::new(cx).await;
3984
3985 // Test sort_lines_case_insensitive()
3986 cx.set_state(indoc! {"
3987 «z
3988 y
3989 x
3990 Z
3991 Y
3992 Xˇ»
3993 "});
3994 cx.update_editor(|e, window, cx| {
3995 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3996 });
3997 cx.assert_editor_state(indoc! {"
3998 «x
3999 X
4000 y
4001 Y
4002 z
4003 Zˇ»
4004 "});
4005
4006 // Test reverse_lines()
4007 cx.set_state(indoc! {"
4008 «5
4009 4
4010 3
4011 2
4012 1ˇ»
4013 "});
4014 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4015 cx.assert_editor_state(indoc! {"
4016 «1
4017 2
4018 3
4019 4
4020 5ˇ»
4021 "});
4022
4023 // Skip testing shuffle_line()
4024
4025 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
4026 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
4027
4028 // Don't manipulate when cursor is on single line, but expand the selection
4029 cx.set_state(indoc! {"
4030 ddˇdd
4031 ccc
4032 bb
4033 a
4034 "});
4035 cx.update_editor(|e, window, cx| {
4036 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4037 });
4038 cx.assert_editor_state(indoc! {"
4039 «ddddˇ»
4040 ccc
4041 bb
4042 a
4043 "});
4044
4045 // Basic manipulate case
4046 // Start selection moves to column 0
4047 // End of selection shrinks to fit shorter line
4048 cx.set_state(indoc! {"
4049 dd«d
4050 ccc
4051 bb
4052 aaaaaˇ»
4053 "});
4054 cx.update_editor(|e, window, cx| {
4055 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4056 });
4057 cx.assert_editor_state(indoc! {"
4058 «aaaaa
4059 bb
4060 ccc
4061 dddˇ»
4062 "});
4063
4064 // Manipulate case with newlines
4065 cx.set_state(indoc! {"
4066 dd«d
4067 ccc
4068
4069 bb
4070 aaaaa
4071
4072 ˇ»
4073 "});
4074 cx.update_editor(|e, window, cx| {
4075 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4076 });
4077 cx.assert_editor_state(indoc! {"
4078 «
4079
4080 aaaaa
4081 bb
4082 ccc
4083 dddˇ»
4084
4085 "});
4086
4087 // Adding new line
4088 cx.set_state(indoc! {"
4089 aa«a
4090 bbˇ»b
4091 "});
4092 cx.update_editor(|e, window, cx| {
4093 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
4094 });
4095 cx.assert_editor_state(indoc! {"
4096 «aaa
4097 bbb
4098 added_lineˇ»
4099 "});
4100
4101 // Removing line
4102 cx.set_state(indoc! {"
4103 aa«a
4104 bbbˇ»
4105 "});
4106 cx.update_editor(|e, window, cx| {
4107 e.manipulate_lines(window, cx, |lines| {
4108 lines.pop();
4109 })
4110 });
4111 cx.assert_editor_state(indoc! {"
4112 «aaaˇ»
4113 "});
4114
4115 // Removing all lines
4116 cx.set_state(indoc! {"
4117 aa«a
4118 bbbˇ»
4119 "});
4120 cx.update_editor(|e, window, cx| {
4121 e.manipulate_lines(window, cx, |lines| {
4122 lines.drain(..);
4123 })
4124 });
4125 cx.assert_editor_state(indoc! {"
4126 ˇ
4127 "});
4128}
4129
4130#[gpui::test]
4131async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4132 init_test(cx, |_| {});
4133
4134 let mut cx = EditorTestContext::new(cx).await;
4135
4136 // Consider continuous selection as single selection
4137 cx.set_state(indoc! {"
4138 Aaa«aa
4139 cˇ»c«c
4140 bb
4141 aaaˇ»aa
4142 "});
4143 cx.update_editor(|e, window, cx| {
4144 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4145 });
4146 cx.assert_editor_state(indoc! {"
4147 «Aaaaa
4148 ccc
4149 bb
4150 aaaaaˇ»
4151 "});
4152
4153 cx.set_state(indoc! {"
4154 Aaa«aa
4155 cˇ»c«c
4156 bb
4157 aaaˇ»aa
4158 "});
4159 cx.update_editor(|e, window, cx| {
4160 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4161 });
4162 cx.assert_editor_state(indoc! {"
4163 «Aaaaa
4164 ccc
4165 bbˇ»
4166 "});
4167
4168 // Consider non continuous selection as distinct dedup operations
4169 cx.set_state(indoc! {"
4170 «aaaaa
4171 bb
4172 aaaaa
4173 aaaaaˇ»
4174
4175 aaa«aaˇ»
4176 "});
4177 cx.update_editor(|e, window, cx| {
4178 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4179 });
4180 cx.assert_editor_state(indoc! {"
4181 «aaaaa
4182 bbˇ»
4183
4184 «aaaaaˇ»
4185 "});
4186}
4187
4188#[gpui::test]
4189async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4190 init_test(cx, |_| {});
4191
4192 let mut cx = EditorTestContext::new(cx).await;
4193
4194 cx.set_state(indoc! {"
4195 «Aaa
4196 aAa
4197 Aaaˇ»
4198 "});
4199 cx.update_editor(|e, window, cx| {
4200 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4201 });
4202 cx.assert_editor_state(indoc! {"
4203 «Aaa
4204 aAaˇ»
4205 "});
4206
4207 cx.set_state(indoc! {"
4208 «Aaa
4209 aAa
4210 aaAˇ»
4211 "});
4212 cx.update_editor(|e, window, cx| {
4213 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4214 });
4215 cx.assert_editor_state(indoc! {"
4216 «Aaaˇ»
4217 "});
4218}
4219
4220#[gpui::test]
4221async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
4222 init_test(cx, |_| {});
4223
4224 let mut cx = EditorTestContext::new(cx).await;
4225
4226 // Manipulate with multiple selections on a single line
4227 cx.set_state(indoc! {"
4228 dd«dd
4229 cˇ»c«c
4230 bb
4231 aaaˇ»aa
4232 "});
4233 cx.update_editor(|e, window, cx| {
4234 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4235 });
4236 cx.assert_editor_state(indoc! {"
4237 «aaaaa
4238 bb
4239 ccc
4240 ddddˇ»
4241 "});
4242
4243 // Manipulate with multiple disjoin selections
4244 cx.set_state(indoc! {"
4245 5«
4246 4
4247 3
4248 2
4249 1ˇ»
4250
4251 dd«dd
4252 ccc
4253 bb
4254 aaaˇ»aa
4255 "});
4256 cx.update_editor(|e, window, cx| {
4257 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4258 });
4259 cx.assert_editor_state(indoc! {"
4260 «1
4261 2
4262 3
4263 4
4264 5ˇ»
4265
4266 «aaaaa
4267 bb
4268 ccc
4269 ddddˇ»
4270 "});
4271
4272 // Adding lines on each selection
4273 cx.set_state(indoc! {"
4274 2«
4275 1ˇ»
4276
4277 bb«bb
4278 aaaˇ»aa
4279 "});
4280 cx.update_editor(|e, window, cx| {
4281 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
4282 });
4283 cx.assert_editor_state(indoc! {"
4284 «2
4285 1
4286 added lineˇ»
4287
4288 «bbbb
4289 aaaaa
4290 added lineˇ»
4291 "});
4292
4293 // Removing lines on each selection
4294 cx.set_state(indoc! {"
4295 2«
4296 1ˇ»
4297
4298 bb«bb
4299 aaaˇ»aa
4300 "});
4301 cx.update_editor(|e, window, cx| {
4302 e.manipulate_lines(window, cx, |lines| {
4303 lines.pop();
4304 })
4305 });
4306 cx.assert_editor_state(indoc! {"
4307 «2ˇ»
4308
4309 «bbbbˇ»
4310 "});
4311}
4312
4313#[gpui::test]
4314async fn test_toggle_case(cx: &mut TestAppContext) {
4315 init_test(cx, |_| {});
4316
4317 let mut cx = EditorTestContext::new(cx).await;
4318
4319 // If all lower case -> upper case
4320 cx.set_state(indoc! {"
4321 «hello worldˇ»
4322 "});
4323 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4324 cx.assert_editor_state(indoc! {"
4325 «HELLO WORLDˇ»
4326 "});
4327
4328 // If all upper case -> lower case
4329 cx.set_state(indoc! {"
4330 «HELLO WORLDˇ»
4331 "});
4332 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4333 cx.assert_editor_state(indoc! {"
4334 «hello worldˇ»
4335 "});
4336
4337 // If any upper case characters are identified -> lower case
4338 // This matches JetBrains IDEs
4339 cx.set_state(indoc! {"
4340 «hEllo worldˇ»
4341 "});
4342 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4343 cx.assert_editor_state(indoc! {"
4344 «hello worldˇ»
4345 "});
4346}
4347
4348#[gpui::test]
4349async fn test_manipulate_text(cx: &mut TestAppContext) {
4350 init_test(cx, |_| {});
4351
4352 let mut cx = EditorTestContext::new(cx).await;
4353
4354 // Test convert_to_upper_case()
4355 cx.set_state(indoc! {"
4356 «hello worldˇ»
4357 "});
4358 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4359 cx.assert_editor_state(indoc! {"
4360 «HELLO WORLDˇ»
4361 "});
4362
4363 // Test convert_to_lower_case()
4364 cx.set_state(indoc! {"
4365 «HELLO WORLDˇ»
4366 "});
4367 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4368 cx.assert_editor_state(indoc! {"
4369 «hello worldˇ»
4370 "});
4371
4372 // Test multiple line, single selection case
4373 cx.set_state(indoc! {"
4374 «The quick brown
4375 fox jumps over
4376 the lazy dogˇ»
4377 "});
4378 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4379 cx.assert_editor_state(indoc! {"
4380 «The Quick Brown
4381 Fox Jumps Over
4382 The Lazy Dogˇ»
4383 "});
4384
4385 // Test multiple line, single selection case
4386 cx.set_state(indoc! {"
4387 «The quick brown
4388 fox jumps over
4389 the lazy dogˇ»
4390 "});
4391 cx.update_editor(|e, window, cx| {
4392 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4393 });
4394 cx.assert_editor_state(indoc! {"
4395 «TheQuickBrown
4396 FoxJumpsOver
4397 TheLazyDogˇ»
4398 "});
4399
4400 // From here on out, test more complex cases of manipulate_text()
4401
4402 // Test no selection case - should affect words cursors are in
4403 // Cursor at beginning, middle, and end of word
4404 cx.set_state(indoc! {"
4405 ˇhello big beauˇtiful worldˇ
4406 "});
4407 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4408 cx.assert_editor_state(indoc! {"
4409 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4410 "});
4411
4412 // Test multiple selections on a single line and across multiple lines
4413 cx.set_state(indoc! {"
4414 «Theˇ» quick «brown
4415 foxˇ» jumps «overˇ»
4416 the «lazyˇ» dog
4417 "});
4418 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4419 cx.assert_editor_state(indoc! {"
4420 «THEˇ» quick «BROWN
4421 FOXˇ» jumps «OVERˇ»
4422 the «LAZYˇ» dog
4423 "});
4424
4425 // Test case where text length grows
4426 cx.set_state(indoc! {"
4427 «tschüߡ»
4428 "});
4429 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4430 cx.assert_editor_state(indoc! {"
4431 «TSCHÜSSˇ»
4432 "});
4433
4434 // Test to make sure we don't crash when text shrinks
4435 cx.set_state(indoc! {"
4436 aaa_bbbˇ
4437 "});
4438 cx.update_editor(|e, window, cx| {
4439 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4440 });
4441 cx.assert_editor_state(indoc! {"
4442 «aaaBbbˇ»
4443 "});
4444
4445 // Test to make sure we all aware of the fact that each word can grow and shrink
4446 // Final selections should be aware of this fact
4447 cx.set_state(indoc! {"
4448 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4449 "});
4450 cx.update_editor(|e, window, cx| {
4451 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4452 });
4453 cx.assert_editor_state(indoc! {"
4454 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4455 "});
4456
4457 cx.set_state(indoc! {"
4458 «hElLo, WoRld!ˇ»
4459 "});
4460 cx.update_editor(|e, window, cx| {
4461 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4462 });
4463 cx.assert_editor_state(indoc! {"
4464 «HeLlO, wOrLD!ˇ»
4465 "});
4466}
4467
4468#[gpui::test]
4469fn test_duplicate_line(cx: &mut TestAppContext) {
4470 init_test(cx, |_| {});
4471
4472 let editor = cx.add_window(|window, cx| {
4473 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4474 build_editor(buffer, window, cx)
4475 });
4476 _ = editor.update(cx, |editor, window, cx| {
4477 editor.change_selections(None, window, cx, |s| {
4478 s.select_display_ranges([
4479 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4480 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4481 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4482 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4483 ])
4484 });
4485 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4486 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4487 assert_eq!(
4488 editor.selections.display_ranges(cx),
4489 vec![
4490 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4491 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4492 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4493 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4494 ]
4495 );
4496 });
4497
4498 let editor = cx.add_window(|window, cx| {
4499 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4500 build_editor(buffer, window, cx)
4501 });
4502 _ = editor.update(cx, |editor, window, cx| {
4503 editor.change_selections(None, window, cx, |s| {
4504 s.select_display_ranges([
4505 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4506 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4507 ])
4508 });
4509 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4510 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4511 assert_eq!(
4512 editor.selections.display_ranges(cx),
4513 vec![
4514 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4515 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4516 ]
4517 );
4518 });
4519
4520 // With `move_upwards` the selections stay in place, except for
4521 // the lines inserted above them
4522 let editor = cx.add_window(|window, cx| {
4523 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4524 build_editor(buffer, window, cx)
4525 });
4526 _ = editor.update(cx, |editor, window, cx| {
4527 editor.change_selections(None, window, cx, |s| {
4528 s.select_display_ranges([
4529 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4530 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4531 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4532 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4533 ])
4534 });
4535 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4536 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4537 assert_eq!(
4538 editor.selections.display_ranges(cx),
4539 vec![
4540 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4541 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4542 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4543 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4544 ]
4545 );
4546 });
4547
4548 let editor = cx.add_window(|window, cx| {
4549 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4550 build_editor(buffer, window, cx)
4551 });
4552 _ = editor.update(cx, |editor, window, cx| {
4553 editor.change_selections(None, window, cx, |s| {
4554 s.select_display_ranges([
4555 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4556 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4557 ])
4558 });
4559 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4560 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4561 assert_eq!(
4562 editor.selections.display_ranges(cx),
4563 vec![
4564 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4565 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4566 ]
4567 );
4568 });
4569
4570 let editor = cx.add_window(|window, cx| {
4571 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4572 build_editor(buffer, window, cx)
4573 });
4574 _ = editor.update(cx, |editor, window, cx| {
4575 editor.change_selections(None, window, cx, |s| {
4576 s.select_display_ranges([
4577 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4578 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4579 ])
4580 });
4581 editor.duplicate_selection(&DuplicateSelection, window, cx);
4582 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4583 assert_eq!(
4584 editor.selections.display_ranges(cx),
4585 vec![
4586 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4587 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4588 ]
4589 );
4590 });
4591}
4592
4593#[gpui::test]
4594fn test_move_line_up_down(cx: &mut TestAppContext) {
4595 init_test(cx, |_| {});
4596
4597 let editor = cx.add_window(|window, cx| {
4598 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4599 build_editor(buffer, window, cx)
4600 });
4601 _ = editor.update(cx, |editor, window, cx| {
4602 editor.fold_creases(
4603 vec![
4604 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4605 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4606 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4607 ],
4608 true,
4609 window,
4610 cx,
4611 );
4612 editor.change_selections(None, window, cx, |s| {
4613 s.select_display_ranges([
4614 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4615 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4616 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4617 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4618 ])
4619 });
4620 assert_eq!(
4621 editor.display_text(cx),
4622 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4623 );
4624
4625 editor.move_line_up(&MoveLineUp, window, cx);
4626 assert_eq!(
4627 editor.display_text(cx),
4628 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4629 );
4630 assert_eq!(
4631 editor.selections.display_ranges(cx),
4632 vec![
4633 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4634 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4635 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4636 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4637 ]
4638 );
4639 });
4640
4641 _ = editor.update(cx, |editor, window, cx| {
4642 editor.move_line_down(&MoveLineDown, window, cx);
4643 assert_eq!(
4644 editor.display_text(cx),
4645 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4646 );
4647 assert_eq!(
4648 editor.selections.display_ranges(cx),
4649 vec![
4650 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4651 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4652 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4653 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4654 ]
4655 );
4656 });
4657
4658 _ = editor.update(cx, |editor, window, cx| {
4659 editor.move_line_down(&MoveLineDown, window, cx);
4660 assert_eq!(
4661 editor.display_text(cx),
4662 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4663 );
4664 assert_eq!(
4665 editor.selections.display_ranges(cx),
4666 vec![
4667 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4668 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4669 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4670 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4671 ]
4672 );
4673 });
4674
4675 _ = editor.update(cx, |editor, window, cx| {
4676 editor.move_line_up(&MoveLineUp, window, cx);
4677 assert_eq!(
4678 editor.display_text(cx),
4679 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4680 );
4681 assert_eq!(
4682 editor.selections.display_ranges(cx),
4683 vec![
4684 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4685 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4686 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4687 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4688 ]
4689 );
4690 });
4691}
4692
4693#[gpui::test]
4694fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4695 init_test(cx, |_| {});
4696
4697 let editor = cx.add_window(|window, cx| {
4698 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4699 build_editor(buffer, window, cx)
4700 });
4701 _ = editor.update(cx, |editor, window, cx| {
4702 let snapshot = editor.buffer.read(cx).snapshot(cx);
4703 editor.insert_blocks(
4704 [BlockProperties {
4705 style: BlockStyle::Fixed,
4706 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4707 height: Some(1),
4708 render: Arc::new(|_| div().into_any()),
4709 priority: 0,
4710 render_in_minimap: true,
4711 }],
4712 Some(Autoscroll::fit()),
4713 cx,
4714 );
4715 editor.change_selections(None, window, cx, |s| {
4716 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4717 });
4718 editor.move_line_down(&MoveLineDown, window, cx);
4719 });
4720}
4721
4722#[gpui::test]
4723async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4724 init_test(cx, |_| {});
4725
4726 let mut cx = EditorTestContext::new(cx).await;
4727 cx.set_state(
4728 &"
4729 ˇzero
4730 one
4731 two
4732 three
4733 four
4734 five
4735 "
4736 .unindent(),
4737 );
4738
4739 // Create a four-line block that replaces three lines of text.
4740 cx.update_editor(|editor, window, cx| {
4741 let snapshot = editor.snapshot(window, cx);
4742 let snapshot = &snapshot.buffer_snapshot;
4743 let placement = BlockPlacement::Replace(
4744 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4745 );
4746 editor.insert_blocks(
4747 [BlockProperties {
4748 placement,
4749 height: Some(4),
4750 style: BlockStyle::Sticky,
4751 render: Arc::new(|_| gpui::div().into_any_element()),
4752 priority: 0,
4753 render_in_minimap: true,
4754 }],
4755 None,
4756 cx,
4757 );
4758 });
4759
4760 // Move down so that the cursor touches the block.
4761 cx.update_editor(|editor, window, cx| {
4762 editor.move_down(&Default::default(), window, cx);
4763 });
4764 cx.assert_editor_state(
4765 &"
4766 zero
4767 «one
4768 two
4769 threeˇ»
4770 four
4771 five
4772 "
4773 .unindent(),
4774 );
4775
4776 // Move down past the block.
4777 cx.update_editor(|editor, window, cx| {
4778 editor.move_down(&Default::default(), window, cx);
4779 });
4780 cx.assert_editor_state(
4781 &"
4782 zero
4783 one
4784 two
4785 three
4786 ˇfour
4787 five
4788 "
4789 .unindent(),
4790 );
4791}
4792
4793#[gpui::test]
4794fn test_transpose(cx: &mut TestAppContext) {
4795 init_test(cx, |_| {});
4796
4797 _ = cx.add_window(|window, cx| {
4798 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4799 editor.set_style(EditorStyle::default(), window, cx);
4800 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4801 editor.transpose(&Default::default(), window, cx);
4802 assert_eq!(editor.text(cx), "bac");
4803 assert_eq!(editor.selections.ranges(cx), [2..2]);
4804
4805 editor.transpose(&Default::default(), window, cx);
4806 assert_eq!(editor.text(cx), "bca");
4807 assert_eq!(editor.selections.ranges(cx), [3..3]);
4808
4809 editor.transpose(&Default::default(), window, cx);
4810 assert_eq!(editor.text(cx), "bac");
4811 assert_eq!(editor.selections.ranges(cx), [3..3]);
4812
4813 editor
4814 });
4815
4816 _ = cx.add_window(|window, cx| {
4817 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4818 editor.set_style(EditorStyle::default(), window, cx);
4819 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4820 editor.transpose(&Default::default(), window, cx);
4821 assert_eq!(editor.text(cx), "acb\nde");
4822 assert_eq!(editor.selections.ranges(cx), [3..3]);
4823
4824 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4825 editor.transpose(&Default::default(), window, cx);
4826 assert_eq!(editor.text(cx), "acbd\ne");
4827 assert_eq!(editor.selections.ranges(cx), [5..5]);
4828
4829 editor.transpose(&Default::default(), window, cx);
4830 assert_eq!(editor.text(cx), "acbde\n");
4831 assert_eq!(editor.selections.ranges(cx), [6..6]);
4832
4833 editor.transpose(&Default::default(), window, cx);
4834 assert_eq!(editor.text(cx), "acbd\ne");
4835 assert_eq!(editor.selections.ranges(cx), [6..6]);
4836
4837 editor
4838 });
4839
4840 _ = cx.add_window(|window, cx| {
4841 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4842 editor.set_style(EditorStyle::default(), window, cx);
4843 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4844 editor.transpose(&Default::default(), window, cx);
4845 assert_eq!(editor.text(cx), "bacd\ne");
4846 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4847
4848 editor.transpose(&Default::default(), window, cx);
4849 assert_eq!(editor.text(cx), "bcade\n");
4850 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4851
4852 editor.transpose(&Default::default(), window, cx);
4853 assert_eq!(editor.text(cx), "bcda\ne");
4854 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4855
4856 editor.transpose(&Default::default(), window, cx);
4857 assert_eq!(editor.text(cx), "bcade\n");
4858 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4859
4860 editor.transpose(&Default::default(), window, cx);
4861 assert_eq!(editor.text(cx), "bcaed\n");
4862 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4863
4864 editor
4865 });
4866
4867 _ = cx.add_window(|window, cx| {
4868 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4869 editor.set_style(EditorStyle::default(), window, cx);
4870 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4871 editor.transpose(&Default::default(), window, cx);
4872 assert_eq!(editor.text(cx), "🏀🍐✋");
4873 assert_eq!(editor.selections.ranges(cx), [8..8]);
4874
4875 editor.transpose(&Default::default(), window, cx);
4876 assert_eq!(editor.text(cx), "🏀✋🍐");
4877 assert_eq!(editor.selections.ranges(cx), [11..11]);
4878
4879 editor.transpose(&Default::default(), window, cx);
4880 assert_eq!(editor.text(cx), "🏀🍐✋");
4881 assert_eq!(editor.selections.ranges(cx), [11..11]);
4882
4883 editor
4884 });
4885}
4886
4887#[gpui::test]
4888async fn test_rewrap(cx: &mut TestAppContext) {
4889 init_test(cx, |settings| {
4890 settings.languages.extend([
4891 (
4892 "Markdown".into(),
4893 LanguageSettingsContent {
4894 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4895 ..Default::default()
4896 },
4897 ),
4898 (
4899 "Plain Text".into(),
4900 LanguageSettingsContent {
4901 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4902 ..Default::default()
4903 },
4904 ),
4905 ])
4906 });
4907
4908 let mut cx = EditorTestContext::new(cx).await;
4909
4910 let language_with_c_comments = Arc::new(Language::new(
4911 LanguageConfig {
4912 line_comments: vec!["// ".into()],
4913 ..LanguageConfig::default()
4914 },
4915 None,
4916 ));
4917 let language_with_pound_comments = Arc::new(Language::new(
4918 LanguageConfig {
4919 line_comments: vec!["# ".into()],
4920 ..LanguageConfig::default()
4921 },
4922 None,
4923 ));
4924 let markdown_language = Arc::new(Language::new(
4925 LanguageConfig {
4926 name: "Markdown".into(),
4927 ..LanguageConfig::default()
4928 },
4929 None,
4930 ));
4931 let language_with_doc_comments = Arc::new(Language::new(
4932 LanguageConfig {
4933 line_comments: vec!["// ".into(), "/// ".into()],
4934 ..LanguageConfig::default()
4935 },
4936 Some(tree_sitter_rust::LANGUAGE.into()),
4937 ));
4938
4939 let plaintext_language = Arc::new(Language::new(
4940 LanguageConfig {
4941 name: "Plain Text".into(),
4942 ..LanguageConfig::default()
4943 },
4944 None,
4945 ));
4946
4947 assert_rewrap(
4948 indoc! {"
4949 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4950 "},
4951 indoc! {"
4952 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4953 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4954 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4955 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4956 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4957 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4958 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4959 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4960 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4961 // porttitor id. Aliquam id accumsan eros.
4962 "},
4963 language_with_c_comments.clone(),
4964 &mut cx,
4965 );
4966
4967 // Test that rewrapping works inside of a selection
4968 assert_rewrap(
4969 indoc! {"
4970 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.ˇ»
4971 "},
4972 indoc! {"
4973 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4974 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4975 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4976 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4977 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4978 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4979 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4980 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4981 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4982 // porttitor id. Aliquam id accumsan eros.ˇ»
4983 "},
4984 language_with_c_comments.clone(),
4985 &mut cx,
4986 );
4987
4988 // Test that cursors that expand to the same region are collapsed.
4989 assert_rewrap(
4990 indoc! {"
4991 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4992 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4993 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4994 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4995 "},
4996 indoc! {"
4997 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4998 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4999 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
5000 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
5001 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
5002 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
5003 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
5004 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
5005 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
5006 // porttitor id. Aliquam id accumsan eros.
5007 "},
5008 language_with_c_comments.clone(),
5009 &mut cx,
5010 );
5011
5012 // Test that non-contiguous selections are treated separately.
5013 assert_rewrap(
5014 indoc! {"
5015 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
5016 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
5017 //
5018 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
5019 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
5020 "},
5021 indoc! {"
5022 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
5023 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
5024 // auctor, eu lacinia sapien scelerisque.
5025 //
5026 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
5027 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
5028 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
5029 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
5030 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
5031 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
5032 // vulputate turpis porttitor id. Aliquam id accumsan eros.
5033 "},
5034 language_with_c_comments.clone(),
5035 &mut cx,
5036 );
5037
5038 // Test that different comment prefixes are supported.
5039 assert_rewrap(
5040 indoc! {"
5041 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
5042 "},
5043 indoc! {"
5044 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
5045 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5046 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5047 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5048 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
5049 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
5050 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
5051 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
5052 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
5053 # accumsan eros.
5054 "},
5055 language_with_pound_comments.clone(),
5056 &mut cx,
5057 );
5058
5059 // Test that rewrapping is ignored outside of comments in most languages.
5060 assert_rewrap(
5061 indoc! {"
5062 /// Adds two numbers.
5063 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
5064 fn add(a: u32, b: u32) -> u32 {
5065 a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + bˇ
5066 }
5067 "},
5068 indoc! {"
5069 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
5070 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
5071 fn add(a: u32, b: u32) -> u32 {
5072 a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + bˇ
5073 }
5074 "},
5075 language_with_doc_comments.clone(),
5076 &mut cx,
5077 );
5078
5079 // Test that rewrapping works in Markdown and Plain Text languages.
5080 assert_rewrap(
5081 indoc! {"
5082 # Hello
5083
5084 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi.
5085 "},
5086 indoc! {"
5087 # Hello
5088
5089 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
5090 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5091 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5092 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5093 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
5094 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
5095 Integer sit amet scelerisque nisi.
5096 "},
5097 markdown_language,
5098 &mut cx,
5099 );
5100
5101 assert_rewrap(
5102 indoc! {"
5103 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi.
5104 "},
5105 indoc! {"
5106 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
5107 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5108 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5109 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5110 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
5111 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
5112 Integer sit amet scelerisque nisi.
5113 "},
5114 plaintext_language.clone(),
5115 &mut cx,
5116 );
5117
5118 // Test rewrapping unaligned comments in a selection.
5119 assert_rewrap(
5120 indoc! {"
5121 fn foo() {
5122 if true {
5123 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
5124 // Praesent semper egestas tellus id dignissim.ˇ»
5125 do_something();
5126 } else {
5127 //
5128 }
5129 }
5130 "},
5131 indoc! {"
5132 fn foo() {
5133 if true {
5134 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
5135 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
5136 // egestas tellus id dignissim.ˇ»
5137 do_something();
5138 } else {
5139 //
5140 }
5141 }
5142 "},
5143 language_with_doc_comments.clone(),
5144 &mut cx,
5145 );
5146
5147 assert_rewrap(
5148 indoc! {"
5149 fn foo() {
5150 if true {
5151 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
5152 // Praesent semper egestas tellus id dignissim.»
5153 do_something();
5154 } else {
5155 //
5156 }
5157
5158 }
5159 "},
5160 indoc! {"
5161 fn foo() {
5162 if true {
5163 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
5164 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
5165 // egestas tellus id dignissim.»
5166 do_something();
5167 } else {
5168 //
5169 }
5170
5171 }
5172 "},
5173 language_with_doc_comments.clone(),
5174 &mut cx,
5175 );
5176
5177 assert_rewrap(
5178 indoc! {"
5179 «ˇone one one one one one one one one one one one one one one one one one one one one one one one one
5180
5181 two»
5182
5183 three
5184
5185 «ˇ\t
5186
5187 four four four four four four four four four four four four four four four four four four four four»
5188
5189 «ˇfive five five five five five five five five five five five five five five five five five five five
5190 \t»
5191 six six six six six six six six six six six six six six six six six six six six six six six six six
5192 "},
5193 indoc! {"
5194 «ˇone one one one one one one one one one one one one one one one one one one one
5195 one one one one one
5196
5197 two»
5198
5199 three
5200
5201 «ˇ\t
5202
5203 four four four four four four four four four four four four four four four four
5204 four four four four»
5205
5206 «ˇfive five five five five five five five five five five five five five five five
5207 five five five five
5208 \t»
5209 six six six six six six six six six six six six six six six six six six six six six six six six six
5210 "},
5211 plaintext_language.clone(),
5212 &mut cx,
5213 );
5214
5215 assert_rewrap(
5216 indoc! {"
5217 //ˇ long long long long long long long long long long long long long long long long long long long long long long long long long long long long
5218 //ˇ
5219 //ˇ long long long long long long long long long long long long long long long long long long long long long long long long long long long long
5220 //ˇ short short short
5221 int main(void) {
5222 return 17;
5223 }
5224 "},
5225 indoc! {"
5226 //ˇ long long long long long long long long long long long long long long long
5227 // long long long long long long long long long long long long long
5228 //ˇ
5229 //ˇ long long long long long long long long long long long long long long long
5230 //ˇ long long long long long long long long long long long long long short short
5231 // short
5232 int main(void) {
5233 return 17;
5234 }
5235 "},
5236 language_with_c_comments,
5237 &mut cx,
5238 );
5239
5240 #[track_caller]
5241 fn assert_rewrap(
5242 unwrapped_text: &str,
5243 wrapped_text: &str,
5244 language: Arc<Language>,
5245 cx: &mut EditorTestContext,
5246 ) {
5247 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5248 cx.set_state(unwrapped_text);
5249 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5250 cx.assert_editor_state(wrapped_text);
5251 }
5252}
5253
5254#[gpui::test]
5255async fn test_hard_wrap(cx: &mut TestAppContext) {
5256 init_test(cx, |_| {});
5257 let mut cx = EditorTestContext::new(cx).await;
5258
5259 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5260 cx.update_editor(|editor, _, cx| {
5261 editor.set_hard_wrap(Some(14), cx);
5262 });
5263
5264 cx.set_state(indoc!(
5265 "
5266 one two three ˇ
5267 "
5268 ));
5269 cx.simulate_input("four");
5270 cx.run_until_parked();
5271
5272 cx.assert_editor_state(indoc!(
5273 "
5274 one two three
5275 fourˇ
5276 "
5277 ));
5278
5279 cx.update_editor(|editor, window, cx| {
5280 editor.newline(&Default::default(), window, cx);
5281 });
5282 cx.run_until_parked();
5283 cx.assert_editor_state(indoc!(
5284 "
5285 one two three
5286 four
5287 ˇ
5288 "
5289 ));
5290
5291 cx.simulate_input("five");
5292 cx.run_until_parked();
5293 cx.assert_editor_state(indoc!(
5294 "
5295 one two three
5296 four
5297 fiveˇ
5298 "
5299 ));
5300
5301 cx.update_editor(|editor, window, cx| {
5302 editor.newline(&Default::default(), window, cx);
5303 });
5304 cx.run_until_parked();
5305 cx.simulate_input("# ");
5306 cx.run_until_parked();
5307 cx.assert_editor_state(indoc!(
5308 "
5309 one two three
5310 four
5311 five
5312 # ˇ
5313 "
5314 ));
5315
5316 cx.update_editor(|editor, window, cx| {
5317 editor.newline(&Default::default(), window, cx);
5318 });
5319 cx.run_until_parked();
5320 cx.assert_editor_state(indoc!(
5321 "
5322 one two three
5323 four
5324 five
5325 #\x20
5326 #ˇ
5327 "
5328 ));
5329
5330 cx.simulate_input(" 6");
5331 cx.run_until_parked();
5332 cx.assert_editor_state(indoc!(
5333 "
5334 one two three
5335 four
5336 five
5337 #
5338 # 6ˇ
5339 "
5340 ));
5341}
5342
5343#[gpui::test]
5344async fn test_clipboard(cx: &mut TestAppContext) {
5345 init_test(cx, |_| {});
5346
5347 let mut cx = EditorTestContext::new(cx).await;
5348
5349 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5350 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5351 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5352
5353 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5354 cx.set_state("two ˇfour ˇsix ˇ");
5355 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5356 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5357
5358 // Paste again but with only two cursors. Since the number of cursors doesn't
5359 // match the number of slices in the clipboard, the entire clipboard text
5360 // is pasted at each cursor.
5361 cx.set_state("ˇtwo one✅ four three six five ˇ");
5362 cx.update_editor(|e, window, cx| {
5363 e.handle_input("( ", window, cx);
5364 e.paste(&Paste, window, cx);
5365 e.handle_input(") ", window, cx);
5366 });
5367 cx.assert_editor_state(
5368 &([
5369 "( one✅ ",
5370 "three ",
5371 "five ) ˇtwo one✅ four three six five ( one✅ ",
5372 "three ",
5373 "five ) ˇ",
5374 ]
5375 .join("\n")),
5376 );
5377
5378 // Cut with three selections, one of which is full-line.
5379 cx.set_state(indoc! {"
5380 1«2ˇ»3
5381 4ˇ567
5382 «8ˇ»9"});
5383 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5384 cx.assert_editor_state(indoc! {"
5385 1ˇ3
5386 ˇ9"});
5387
5388 // Paste with three selections, noticing how the copied selection that was full-line
5389 // gets inserted before the second cursor.
5390 cx.set_state(indoc! {"
5391 1ˇ3
5392 9ˇ
5393 «oˇ»ne"});
5394 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5395 cx.assert_editor_state(indoc! {"
5396 12ˇ3
5397 4567
5398 9ˇ
5399 8ˇne"});
5400
5401 // Copy with a single cursor only, which writes the whole line into the clipboard.
5402 cx.set_state(indoc! {"
5403 The quick brown
5404 fox juˇmps over
5405 the lazy dog"});
5406 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5407 assert_eq!(
5408 cx.read_from_clipboard()
5409 .and_then(|item| item.text().as_deref().map(str::to_string)),
5410 Some("fox jumps over\n".to_string())
5411 );
5412
5413 // Paste with three selections, noticing how the copied full-line selection is inserted
5414 // before the empty selections but replaces the selection that is non-empty.
5415 cx.set_state(indoc! {"
5416 Tˇhe quick brown
5417 «foˇ»x jumps over
5418 tˇhe lazy dog"});
5419 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5420 cx.assert_editor_state(indoc! {"
5421 fox jumps over
5422 Tˇhe quick brown
5423 fox jumps over
5424 ˇx jumps over
5425 fox jumps over
5426 tˇhe lazy dog"});
5427}
5428
5429#[gpui::test]
5430async fn test_copy_trim(cx: &mut TestAppContext) {
5431 init_test(cx, |_| {});
5432
5433 let mut cx = EditorTestContext::new(cx).await;
5434 cx.set_state(
5435 r#" «for selection in selections.iter() {
5436 let mut start = selection.start;
5437 let mut end = selection.end;
5438 let is_entire_line = selection.is_empty();
5439 if is_entire_line {
5440 start = Point::new(start.row, 0);ˇ»
5441 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5442 }
5443 "#,
5444 );
5445 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5446 assert_eq!(
5447 cx.read_from_clipboard()
5448 .and_then(|item| item.text().as_deref().map(str::to_string)),
5449 Some(
5450 "for selection in selections.iter() {
5451 let mut start = selection.start;
5452 let mut end = selection.end;
5453 let is_entire_line = selection.is_empty();
5454 if is_entire_line {
5455 start = Point::new(start.row, 0);"
5456 .to_string()
5457 ),
5458 "Regular copying preserves all indentation selected",
5459 );
5460 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5461 assert_eq!(
5462 cx.read_from_clipboard()
5463 .and_then(|item| item.text().as_deref().map(str::to_string)),
5464 Some(
5465 "for selection in selections.iter() {
5466let mut start = selection.start;
5467let mut end = selection.end;
5468let is_entire_line = selection.is_empty();
5469if is_entire_line {
5470 start = Point::new(start.row, 0);"
5471 .to_string()
5472 ),
5473 "Copying with stripping should strip all leading whitespaces"
5474 );
5475
5476 cx.set_state(
5477 r#" « for selection in selections.iter() {
5478 let mut start = selection.start;
5479 let mut end = selection.end;
5480 let is_entire_line = selection.is_empty();
5481 if is_entire_line {
5482 start = Point::new(start.row, 0);ˇ»
5483 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5484 }
5485 "#,
5486 );
5487 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5488 assert_eq!(
5489 cx.read_from_clipboard()
5490 .and_then(|item| item.text().as_deref().map(str::to_string)),
5491 Some(
5492 " for selection in selections.iter() {
5493 let mut start = selection.start;
5494 let mut end = selection.end;
5495 let is_entire_line = selection.is_empty();
5496 if is_entire_line {
5497 start = Point::new(start.row, 0);"
5498 .to_string()
5499 ),
5500 "Regular copying preserves all indentation selected",
5501 );
5502 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5503 assert_eq!(
5504 cx.read_from_clipboard()
5505 .and_then(|item| item.text().as_deref().map(str::to_string)),
5506 Some(
5507 "for selection in selections.iter() {
5508let mut start = selection.start;
5509let mut end = selection.end;
5510let is_entire_line = selection.is_empty();
5511if is_entire_line {
5512 start = Point::new(start.row, 0);"
5513 .to_string()
5514 ),
5515 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5516 );
5517
5518 cx.set_state(
5519 r#" «ˇ for selection in selections.iter() {
5520 let mut start = selection.start;
5521 let mut end = selection.end;
5522 let is_entire_line = selection.is_empty();
5523 if is_entire_line {
5524 start = Point::new(start.row, 0);»
5525 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5526 }
5527 "#,
5528 );
5529 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5530 assert_eq!(
5531 cx.read_from_clipboard()
5532 .and_then(|item| item.text().as_deref().map(str::to_string)),
5533 Some(
5534 " for selection in selections.iter() {
5535 let mut start = selection.start;
5536 let mut end = selection.end;
5537 let is_entire_line = selection.is_empty();
5538 if is_entire_line {
5539 start = Point::new(start.row, 0);"
5540 .to_string()
5541 ),
5542 "Regular copying for reverse selection works the same",
5543 );
5544 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5545 assert_eq!(
5546 cx.read_from_clipboard()
5547 .and_then(|item| item.text().as_deref().map(str::to_string)),
5548 Some(
5549 "for selection in selections.iter() {
5550let mut start = selection.start;
5551let mut end = selection.end;
5552let is_entire_line = selection.is_empty();
5553if is_entire_line {
5554 start = Point::new(start.row, 0);"
5555 .to_string()
5556 ),
5557 "Copying with stripping for reverse selection works the same"
5558 );
5559
5560 cx.set_state(
5561 r#" for selection «in selections.iter() {
5562 let mut start = selection.start;
5563 let mut end = selection.end;
5564 let is_entire_line = selection.is_empty();
5565 if is_entire_line {
5566 start = Point::new(start.row, 0);ˇ»
5567 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5568 }
5569 "#,
5570 );
5571 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5572 assert_eq!(
5573 cx.read_from_clipboard()
5574 .and_then(|item| item.text().as_deref().map(str::to_string)),
5575 Some(
5576 "in selections.iter() {
5577 let mut start = selection.start;
5578 let mut end = selection.end;
5579 let is_entire_line = selection.is_empty();
5580 if is_entire_line {
5581 start = Point::new(start.row, 0);"
5582 .to_string()
5583 ),
5584 "When selecting past the indent, the copying works as usual",
5585 );
5586 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5587 assert_eq!(
5588 cx.read_from_clipboard()
5589 .and_then(|item| item.text().as_deref().map(str::to_string)),
5590 Some(
5591 "in selections.iter() {
5592 let mut start = selection.start;
5593 let mut end = selection.end;
5594 let is_entire_line = selection.is_empty();
5595 if is_entire_line {
5596 start = Point::new(start.row, 0);"
5597 .to_string()
5598 ),
5599 "When selecting past the indent, nothing is trimmed"
5600 );
5601
5602 cx.set_state(
5603 r#" «for selection in selections.iter() {
5604 let mut start = selection.start;
5605
5606 let mut end = selection.end;
5607 let is_entire_line = selection.is_empty();
5608 if is_entire_line {
5609 start = Point::new(start.row, 0);
5610ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
5611 }
5612 "#,
5613 );
5614 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5615 assert_eq!(
5616 cx.read_from_clipboard()
5617 .and_then(|item| item.text().as_deref().map(str::to_string)),
5618 Some(
5619 "for selection in selections.iter() {
5620let mut start = selection.start;
5621
5622let mut end = selection.end;
5623let is_entire_line = selection.is_empty();
5624if is_entire_line {
5625 start = Point::new(start.row, 0);
5626"
5627 .to_string()
5628 ),
5629 "Copying with stripping should ignore empty lines"
5630 );
5631}
5632
5633#[gpui::test]
5634async fn test_paste_multiline(cx: &mut TestAppContext) {
5635 init_test(cx, |_| {});
5636
5637 let mut cx = EditorTestContext::new(cx).await;
5638 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5639
5640 // Cut an indented block, without the leading whitespace.
5641 cx.set_state(indoc! {"
5642 const a: B = (
5643 c(),
5644 «d(
5645 e,
5646 f
5647 )ˇ»
5648 );
5649 "});
5650 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5651 cx.assert_editor_state(indoc! {"
5652 const a: B = (
5653 c(),
5654 ˇ
5655 );
5656 "});
5657
5658 // Paste it at the same position.
5659 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5660 cx.assert_editor_state(indoc! {"
5661 const a: B = (
5662 c(),
5663 d(
5664 e,
5665 f
5666 )ˇ
5667 );
5668 "});
5669
5670 // Paste it at a line with a lower indent level.
5671 cx.set_state(indoc! {"
5672 ˇ
5673 const a: B = (
5674 c(),
5675 );
5676 "});
5677 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5678 cx.assert_editor_state(indoc! {"
5679 d(
5680 e,
5681 f
5682 )ˇ
5683 const a: B = (
5684 c(),
5685 );
5686 "});
5687
5688 // Cut an indented block, with the leading whitespace.
5689 cx.set_state(indoc! {"
5690 const a: B = (
5691 c(),
5692 « d(
5693 e,
5694 f
5695 )
5696 ˇ»);
5697 "});
5698 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5699 cx.assert_editor_state(indoc! {"
5700 const a: B = (
5701 c(),
5702 ˇ);
5703 "});
5704
5705 // Paste it at the same position.
5706 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5707 cx.assert_editor_state(indoc! {"
5708 const a: B = (
5709 c(),
5710 d(
5711 e,
5712 f
5713 )
5714 ˇ);
5715 "});
5716
5717 // Paste it at a line with a higher indent level.
5718 cx.set_state(indoc! {"
5719 const a: B = (
5720 c(),
5721 d(
5722 e,
5723 fˇ
5724 )
5725 );
5726 "});
5727 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5728 cx.assert_editor_state(indoc! {"
5729 const a: B = (
5730 c(),
5731 d(
5732 e,
5733 f d(
5734 e,
5735 f
5736 )
5737 ˇ
5738 )
5739 );
5740 "});
5741
5742 // Copy an indented block, starting mid-line
5743 cx.set_state(indoc! {"
5744 const a: B = (
5745 c(),
5746 somethin«g(
5747 e,
5748 f
5749 )ˇ»
5750 );
5751 "});
5752 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5753
5754 // Paste it on a line with a lower indent level
5755 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
5756 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5757 cx.assert_editor_state(indoc! {"
5758 const a: B = (
5759 c(),
5760 something(
5761 e,
5762 f
5763 )
5764 );
5765 g(
5766 e,
5767 f
5768 )ˇ"});
5769}
5770
5771#[gpui::test]
5772async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
5773 init_test(cx, |_| {});
5774
5775 cx.write_to_clipboard(ClipboardItem::new_string(
5776 " d(\n e\n );\n".into(),
5777 ));
5778
5779 let mut cx = EditorTestContext::new(cx).await;
5780 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5781
5782 cx.set_state(indoc! {"
5783 fn a() {
5784 b();
5785 if c() {
5786 ˇ
5787 }
5788 }
5789 "});
5790
5791 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5792 cx.assert_editor_state(indoc! {"
5793 fn a() {
5794 b();
5795 if c() {
5796 d(
5797 e
5798 );
5799 ˇ
5800 }
5801 }
5802 "});
5803
5804 cx.set_state(indoc! {"
5805 fn a() {
5806 b();
5807 ˇ
5808 }
5809 "});
5810
5811 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5812 cx.assert_editor_state(indoc! {"
5813 fn a() {
5814 b();
5815 d(
5816 e
5817 );
5818 ˇ
5819 }
5820 "});
5821}
5822
5823#[gpui::test]
5824fn test_select_all(cx: &mut TestAppContext) {
5825 init_test(cx, |_| {});
5826
5827 let editor = cx.add_window(|window, cx| {
5828 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
5829 build_editor(buffer, window, cx)
5830 });
5831 _ = editor.update(cx, |editor, window, cx| {
5832 editor.select_all(&SelectAll, window, cx);
5833 assert_eq!(
5834 editor.selections.display_ranges(cx),
5835 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
5836 );
5837 });
5838}
5839
5840#[gpui::test]
5841fn test_select_line(cx: &mut TestAppContext) {
5842 init_test(cx, |_| {});
5843
5844 let editor = cx.add_window(|window, cx| {
5845 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
5846 build_editor(buffer, window, cx)
5847 });
5848 _ = editor.update(cx, |editor, window, cx| {
5849 editor.change_selections(None, window, cx, |s| {
5850 s.select_display_ranges([
5851 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5852 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5853 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5854 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
5855 ])
5856 });
5857 editor.select_line(&SelectLine, window, cx);
5858 assert_eq!(
5859 editor.selections.display_ranges(cx),
5860 vec![
5861 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
5862 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
5863 ]
5864 );
5865 });
5866
5867 _ = editor.update(cx, |editor, window, cx| {
5868 editor.select_line(&SelectLine, window, cx);
5869 assert_eq!(
5870 editor.selections.display_ranges(cx),
5871 vec![
5872 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
5873 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
5874 ]
5875 );
5876 });
5877
5878 _ = editor.update(cx, |editor, window, cx| {
5879 editor.select_line(&SelectLine, window, cx);
5880 assert_eq!(
5881 editor.selections.display_ranges(cx),
5882 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
5883 );
5884 });
5885}
5886
5887#[gpui::test]
5888async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
5889 init_test(cx, |_| {});
5890 let mut cx = EditorTestContext::new(cx).await;
5891
5892 #[track_caller]
5893 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
5894 cx.set_state(initial_state);
5895 cx.update_editor(|e, window, cx| {
5896 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
5897 });
5898 cx.assert_editor_state(expected_state);
5899 }
5900
5901 // Selection starts and ends at the middle of lines, left-to-right
5902 test(
5903 &mut cx,
5904 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
5905 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5906 );
5907 // Same thing, right-to-left
5908 test(
5909 &mut cx,
5910 "aa\nb«b\ncc\ndd\neˇ»e\nff",
5911 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5912 );
5913
5914 // Whole buffer, left-to-right, last line *doesn't* end with newline
5915 test(
5916 &mut cx,
5917 "«ˇaa\nbb\ncc\ndd\nee\nff»",
5918 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5919 );
5920 // Same thing, right-to-left
5921 test(
5922 &mut cx,
5923 "«aa\nbb\ncc\ndd\nee\nffˇ»",
5924 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5925 );
5926
5927 // Whole buffer, left-to-right, last line ends with newline
5928 test(
5929 &mut cx,
5930 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
5931 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5932 );
5933 // Same thing, right-to-left
5934 test(
5935 &mut cx,
5936 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
5937 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5938 );
5939
5940 // Starts at the end of a line, ends at the start of another
5941 test(
5942 &mut cx,
5943 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
5944 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
5945 );
5946}
5947
5948#[gpui::test]
5949async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
5950 init_test(cx, |_| {});
5951
5952 let editor = cx.add_window(|window, cx| {
5953 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
5954 build_editor(buffer, window, cx)
5955 });
5956
5957 // setup
5958 _ = editor.update(cx, |editor, window, cx| {
5959 editor.fold_creases(
5960 vec![
5961 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5962 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5963 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5964 ],
5965 true,
5966 window,
5967 cx,
5968 );
5969 assert_eq!(
5970 editor.display_text(cx),
5971 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5972 );
5973 });
5974
5975 _ = editor.update(cx, |editor, window, cx| {
5976 editor.change_selections(None, window, cx, |s| {
5977 s.select_display_ranges([
5978 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5979 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5980 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5981 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
5982 ])
5983 });
5984 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5985 assert_eq!(
5986 editor.display_text(cx),
5987 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5988 );
5989 });
5990 EditorTestContext::for_editor(editor, cx)
5991 .await
5992 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
5993
5994 _ = editor.update(cx, |editor, window, cx| {
5995 editor.change_selections(None, window, cx, |s| {
5996 s.select_display_ranges([
5997 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
5998 ])
5999 });
6000 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6001 assert_eq!(
6002 editor.display_text(cx),
6003 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
6004 );
6005 assert_eq!(
6006 editor.selections.display_ranges(cx),
6007 [
6008 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
6009 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
6010 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
6011 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
6012 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
6013 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
6014 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
6015 ]
6016 );
6017 });
6018 EditorTestContext::for_editor(editor, cx)
6019 .await
6020 .assert_editor_state(
6021 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
6022 );
6023}
6024
6025#[gpui::test]
6026async fn test_add_selection_above_below(cx: &mut TestAppContext) {
6027 init_test(cx, |_| {});
6028
6029 let mut cx = EditorTestContext::new(cx).await;
6030
6031 cx.set_state(indoc!(
6032 r#"abc
6033 defˇghi
6034
6035 jk
6036 nlmo
6037 "#
6038 ));
6039
6040 cx.update_editor(|editor, window, cx| {
6041 editor.add_selection_above(&Default::default(), window, cx);
6042 });
6043
6044 cx.assert_editor_state(indoc!(
6045 r#"abcˇ
6046 defˇghi
6047
6048 jk
6049 nlmo
6050 "#
6051 ));
6052
6053 cx.update_editor(|editor, window, cx| {
6054 editor.add_selection_above(&Default::default(), window, cx);
6055 });
6056
6057 cx.assert_editor_state(indoc!(
6058 r#"abcˇ
6059 defˇghi
6060
6061 jk
6062 nlmo
6063 "#
6064 ));
6065
6066 cx.update_editor(|editor, window, cx| {
6067 editor.add_selection_below(&Default::default(), window, cx);
6068 });
6069
6070 cx.assert_editor_state(indoc!(
6071 r#"abc
6072 defˇghi
6073
6074 jk
6075 nlmo
6076 "#
6077 ));
6078
6079 cx.update_editor(|editor, window, cx| {
6080 editor.undo_selection(&Default::default(), window, cx);
6081 });
6082
6083 cx.assert_editor_state(indoc!(
6084 r#"abcˇ
6085 defˇghi
6086
6087 jk
6088 nlmo
6089 "#
6090 ));
6091
6092 cx.update_editor(|editor, window, cx| {
6093 editor.redo_selection(&Default::default(), window, cx);
6094 });
6095
6096 cx.assert_editor_state(indoc!(
6097 r#"abc
6098 defˇghi
6099
6100 jk
6101 nlmo
6102 "#
6103 ));
6104
6105 cx.update_editor(|editor, window, cx| {
6106 editor.add_selection_below(&Default::default(), window, cx);
6107 });
6108
6109 cx.assert_editor_state(indoc!(
6110 r#"abc
6111 defˇghi
6112 ˇ
6113 jk
6114 nlmo
6115 "#
6116 ));
6117
6118 cx.update_editor(|editor, window, cx| {
6119 editor.add_selection_below(&Default::default(), window, cx);
6120 });
6121
6122 cx.assert_editor_state(indoc!(
6123 r#"abc
6124 defˇghi
6125 ˇ
6126 jkˇ
6127 nlmo
6128 "#
6129 ));
6130
6131 cx.update_editor(|editor, window, cx| {
6132 editor.add_selection_below(&Default::default(), window, cx);
6133 });
6134
6135 cx.assert_editor_state(indoc!(
6136 r#"abc
6137 defˇghi
6138 ˇ
6139 jkˇ
6140 nlmˇo
6141 "#
6142 ));
6143
6144 cx.update_editor(|editor, window, cx| {
6145 editor.add_selection_below(&Default::default(), window, cx);
6146 });
6147
6148 cx.assert_editor_state(indoc!(
6149 r#"abc
6150 defˇghi
6151 ˇ
6152 jkˇ
6153 nlmˇo
6154 ˇ"#
6155 ));
6156
6157 // change selections
6158 cx.set_state(indoc!(
6159 r#"abc
6160 def«ˇg»hi
6161
6162 jk
6163 nlmo
6164 "#
6165 ));
6166
6167 cx.update_editor(|editor, window, cx| {
6168 editor.add_selection_below(&Default::default(), window, cx);
6169 });
6170
6171 cx.assert_editor_state(indoc!(
6172 r#"abc
6173 def«ˇg»hi
6174
6175 jk
6176 nlm«ˇo»
6177 "#
6178 ));
6179
6180 cx.update_editor(|editor, window, cx| {
6181 editor.add_selection_below(&Default::default(), window, cx);
6182 });
6183
6184 cx.assert_editor_state(indoc!(
6185 r#"abc
6186 def«ˇg»hi
6187
6188 jk
6189 nlm«ˇo»
6190 "#
6191 ));
6192
6193 cx.update_editor(|editor, window, cx| {
6194 editor.add_selection_above(&Default::default(), window, cx);
6195 });
6196
6197 cx.assert_editor_state(indoc!(
6198 r#"abc
6199 def«ˇg»hi
6200
6201 jk
6202 nlmo
6203 "#
6204 ));
6205
6206 cx.update_editor(|editor, window, cx| {
6207 editor.add_selection_above(&Default::default(), window, cx);
6208 });
6209
6210 cx.assert_editor_state(indoc!(
6211 r#"abc
6212 def«ˇg»hi
6213
6214 jk
6215 nlmo
6216 "#
6217 ));
6218
6219 // Change selections again
6220 cx.set_state(indoc!(
6221 r#"a«bc
6222 defgˇ»hi
6223
6224 jk
6225 nlmo
6226 "#
6227 ));
6228
6229 cx.update_editor(|editor, window, cx| {
6230 editor.add_selection_below(&Default::default(), window, cx);
6231 });
6232
6233 cx.assert_editor_state(indoc!(
6234 r#"a«bcˇ»
6235 d«efgˇ»hi
6236
6237 j«kˇ»
6238 nlmo
6239 "#
6240 ));
6241
6242 cx.update_editor(|editor, window, cx| {
6243 editor.add_selection_below(&Default::default(), window, cx);
6244 });
6245 cx.assert_editor_state(indoc!(
6246 r#"a«bcˇ»
6247 d«efgˇ»hi
6248
6249 j«kˇ»
6250 n«lmoˇ»
6251 "#
6252 ));
6253 cx.update_editor(|editor, window, cx| {
6254 editor.add_selection_above(&Default::default(), window, cx);
6255 });
6256
6257 cx.assert_editor_state(indoc!(
6258 r#"a«bcˇ»
6259 d«efgˇ»hi
6260
6261 j«kˇ»
6262 nlmo
6263 "#
6264 ));
6265
6266 // Change selections again
6267 cx.set_state(indoc!(
6268 r#"abc
6269 d«ˇefghi
6270
6271 jk
6272 nlm»o
6273 "#
6274 ));
6275
6276 cx.update_editor(|editor, window, cx| {
6277 editor.add_selection_above(&Default::default(), window, cx);
6278 });
6279
6280 cx.assert_editor_state(indoc!(
6281 r#"a«ˇbc»
6282 d«ˇef»ghi
6283
6284 j«ˇk»
6285 n«ˇlm»o
6286 "#
6287 ));
6288
6289 cx.update_editor(|editor, window, cx| {
6290 editor.add_selection_below(&Default::default(), window, cx);
6291 });
6292
6293 cx.assert_editor_state(indoc!(
6294 r#"abc
6295 d«ˇef»ghi
6296
6297 j«ˇk»
6298 n«ˇlm»o
6299 "#
6300 ));
6301}
6302
6303#[gpui::test]
6304async fn test_select_next(cx: &mut TestAppContext) {
6305 init_test(cx, |_| {});
6306
6307 let mut cx = EditorTestContext::new(cx).await;
6308 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6309
6310 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6311 .unwrap();
6312 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6313
6314 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6315 .unwrap();
6316 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6317
6318 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6319 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6320
6321 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6322 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6323
6324 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6325 .unwrap();
6326 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6327
6328 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6329 .unwrap();
6330 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6331
6332 // Test selection direction should be preserved
6333 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6334
6335 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6336 .unwrap();
6337 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
6338}
6339
6340#[gpui::test]
6341async fn test_select_all_matches(cx: &mut TestAppContext) {
6342 init_test(cx, |_| {});
6343
6344 let mut cx = EditorTestContext::new(cx).await;
6345
6346 // Test caret-only selections
6347 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6348 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6349 .unwrap();
6350 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6351
6352 // Test left-to-right selections
6353 cx.set_state("abc\n«abcˇ»\nabc");
6354 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6355 .unwrap();
6356 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
6357
6358 // Test right-to-left selections
6359 cx.set_state("abc\n«ˇabc»\nabc");
6360 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6361 .unwrap();
6362 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
6363
6364 // Test selecting whitespace with caret selection
6365 cx.set_state("abc\nˇ abc\nabc");
6366 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6367 .unwrap();
6368 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6369
6370 // Test selecting whitespace with left-to-right selection
6371 cx.set_state("abc\n«ˇ »abc\nabc");
6372 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6373 .unwrap();
6374 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
6375
6376 // Test no matches with right-to-left selection
6377 cx.set_state("abc\n« ˇ»abc\nabc");
6378 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6379 .unwrap();
6380 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6381}
6382
6383#[gpui::test]
6384async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
6385 init_test(cx, |_| {});
6386
6387 let mut cx = EditorTestContext::new(cx).await;
6388
6389 let large_body_1 = "\nd".repeat(200);
6390 let large_body_2 = "\ne".repeat(200);
6391
6392 cx.set_state(&format!(
6393 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
6394 ));
6395 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
6396 let scroll_position = editor.scroll_position(cx);
6397 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
6398 scroll_position
6399 });
6400
6401 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6402 .unwrap();
6403 cx.assert_editor_state(&format!(
6404 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
6405 ));
6406 let scroll_position_after_selection =
6407 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
6408 assert_eq!(
6409 initial_scroll_position, scroll_position_after_selection,
6410 "Scroll position should not change after selecting all matches"
6411 );
6412}
6413
6414#[gpui::test]
6415async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
6416 init_test(cx, |_| {});
6417
6418 let mut cx = EditorLspTestContext::new_rust(
6419 lsp::ServerCapabilities {
6420 document_formatting_provider: Some(lsp::OneOf::Left(true)),
6421 ..Default::default()
6422 },
6423 cx,
6424 )
6425 .await;
6426
6427 cx.set_state(indoc! {"
6428 line 1
6429 line 2
6430 linˇe 3
6431 line 4
6432 line 5
6433 "});
6434
6435 // Make an edit
6436 cx.update_editor(|editor, window, cx| {
6437 editor.handle_input("X", window, cx);
6438 });
6439
6440 // Move cursor to a different position
6441 cx.update_editor(|editor, window, cx| {
6442 editor.change_selections(None, window, cx, |s| {
6443 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
6444 });
6445 });
6446
6447 cx.assert_editor_state(indoc! {"
6448 line 1
6449 line 2
6450 linXe 3
6451 line 4
6452 liˇne 5
6453 "});
6454
6455 cx.lsp
6456 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
6457 Ok(Some(vec![lsp::TextEdit::new(
6458 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
6459 "PREFIX ".to_string(),
6460 )]))
6461 });
6462
6463 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
6464 .unwrap()
6465 .await
6466 .unwrap();
6467
6468 cx.assert_editor_state(indoc! {"
6469 PREFIX line 1
6470 line 2
6471 linXe 3
6472 line 4
6473 liˇne 5
6474 "});
6475
6476 // Undo formatting
6477 cx.update_editor(|editor, window, cx| {
6478 editor.undo(&Default::default(), window, cx);
6479 });
6480
6481 // Verify cursor moved back to position after edit
6482 cx.assert_editor_state(indoc! {"
6483 line 1
6484 line 2
6485 linXˇe 3
6486 line 4
6487 line 5
6488 "});
6489}
6490
6491#[gpui::test]
6492async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext) {
6493 init_test(cx, |_| {});
6494
6495 let mut cx = EditorTestContext::new(cx).await;
6496
6497 let provider = cx.new(|_| FakeInlineCompletionProvider::default());
6498 cx.update_editor(|editor, window, cx| {
6499 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
6500 });
6501
6502 cx.set_state(indoc! {"
6503 line 1
6504 line 2
6505 linˇe 3
6506 line 4
6507 line 5
6508 line 6
6509 line 7
6510 line 8
6511 line 9
6512 line 10
6513 "});
6514
6515 let snapshot = cx.buffer_snapshot();
6516 let edit_position = snapshot.anchor_after(Point::new(2, 4));
6517
6518 cx.update(|_, cx| {
6519 provider.update(cx, |provider, _| {
6520 provider.set_inline_completion(Some(inline_completion::InlineCompletion {
6521 id: None,
6522 edits: vec![(edit_position..edit_position, "X".into())],
6523 edit_preview: None,
6524 }))
6525 })
6526 });
6527
6528 cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx));
6529 cx.update_editor(|editor, window, cx| {
6530 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
6531 });
6532
6533 cx.assert_editor_state(indoc! {"
6534 line 1
6535 line 2
6536 lineXˇ 3
6537 line 4
6538 line 5
6539 line 6
6540 line 7
6541 line 8
6542 line 9
6543 line 10
6544 "});
6545
6546 cx.update_editor(|editor, window, cx| {
6547 editor.change_selections(None, window, cx, |s| {
6548 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
6549 });
6550 });
6551
6552 cx.assert_editor_state(indoc! {"
6553 line 1
6554 line 2
6555 lineX 3
6556 line 4
6557 line 5
6558 line 6
6559 line 7
6560 line 8
6561 line 9
6562 liˇne 10
6563 "});
6564
6565 cx.update_editor(|editor, window, cx| {
6566 editor.undo(&Default::default(), window, cx);
6567 });
6568
6569 cx.assert_editor_state(indoc! {"
6570 line 1
6571 line 2
6572 lineˇ 3
6573 line 4
6574 line 5
6575 line 6
6576 line 7
6577 line 8
6578 line 9
6579 line 10
6580 "});
6581}
6582
6583#[gpui::test]
6584async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
6585 init_test(cx, |_| {});
6586
6587 let mut cx = EditorTestContext::new(cx).await;
6588 cx.set_state(
6589 r#"let foo = 2;
6590lˇet foo = 2;
6591let fooˇ = 2;
6592let foo = 2;
6593let foo = ˇ2;"#,
6594 );
6595
6596 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6597 .unwrap();
6598 cx.assert_editor_state(
6599 r#"let foo = 2;
6600«letˇ» foo = 2;
6601let «fooˇ» = 2;
6602let foo = 2;
6603let foo = «2ˇ»;"#,
6604 );
6605
6606 // noop for multiple selections with different contents
6607 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6608 .unwrap();
6609 cx.assert_editor_state(
6610 r#"let foo = 2;
6611«letˇ» foo = 2;
6612let «fooˇ» = 2;
6613let foo = 2;
6614let foo = «2ˇ»;"#,
6615 );
6616
6617 // Test last selection direction should be preserved
6618 cx.set_state(
6619 r#"let foo = 2;
6620let foo = 2;
6621let «fooˇ» = 2;
6622let «ˇfoo» = 2;
6623let foo = 2;"#,
6624 );
6625
6626 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6627 .unwrap();
6628 cx.assert_editor_state(
6629 r#"let foo = 2;
6630let foo = 2;
6631let «fooˇ» = 2;
6632let «ˇfoo» = 2;
6633let «ˇfoo» = 2;"#,
6634 );
6635}
6636
6637#[gpui::test]
6638async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
6639 init_test(cx, |_| {});
6640
6641 let mut cx =
6642 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
6643
6644 cx.assert_editor_state(indoc! {"
6645 ˇbbb
6646 ccc
6647
6648 bbb
6649 ccc
6650 "});
6651 cx.dispatch_action(SelectPrevious::default());
6652 cx.assert_editor_state(indoc! {"
6653 «bbbˇ»
6654 ccc
6655
6656 bbb
6657 ccc
6658 "});
6659 cx.dispatch_action(SelectPrevious::default());
6660 cx.assert_editor_state(indoc! {"
6661 «bbbˇ»
6662 ccc
6663
6664 «bbbˇ»
6665 ccc
6666 "});
6667}
6668
6669#[gpui::test]
6670async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
6671 init_test(cx, |_| {});
6672
6673 let mut cx = EditorTestContext::new(cx).await;
6674 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6675
6676 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6677 .unwrap();
6678 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6679
6680 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6681 .unwrap();
6682 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6683
6684 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6685 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6686
6687 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6688 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6689
6690 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6691 .unwrap();
6692 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
6693
6694 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6695 .unwrap();
6696 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6697}
6698
6699#[gpui::test]
6700async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
6701 init_test(cx, |_| {});
6702
6703 let mut cx = EditorTestContext::new(cx).await;
6704 cx.set_state("aˇ");
6705
6706 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6707 .unwrap();
6708 cx.assert_editor_state("«aˇ»");
6709 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6710 .unwrap();
6711 cx.assert_editor_state("«aˇ»");
6712}
6713
6714#[gpui::test]
6715async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
6716 init_test(cx, |_| {});
6717
6718 let mut cx = EditorTestContext::new(cx).await;
6719 cx.set_state(
6720 r#"let foo = 2;
6721lˇet foo = 2;
6722let fooˇ = 2;
6723let foo = 2;
6724let foo = ˇ2;"#,
6725 );
6726
6727 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6728 .unwrap();
6729 cx.assert_editor_state(
6730 r#"let foo = 2;
6731«letˇ» foo = 2;
6732let «fooˇ» = 2;
6733let foo = 2;
6734let foo = «2ˇ»;"#,
6735 );
6736
6737 // noop for multiple selections with different contents
6738 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6739 .unwrap();
6740 cx.assert_editor_state(
6741 r#"let foo = 2;
6742«letˇ» foo = 2;
6743let «fooˇ» = 2;
6744let foo = 2;
6745let foo = «2ˇ»;"#,
6746 );
6747}
6748
6749#[gpui::test]
6750async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
6751 init_test(cx, |_| {});
6752
6753 let mut cx = EditorTestContext::new(cx).await;
6754 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6755
6756 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6757 .unwrap();
6758 // selection direction is preserved
6759 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
6760
6761 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6762 .unwrap();
6763 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
6764
6765 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6766 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
6767
6768 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6769 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
6770
6771 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6772 .unwrap();
6773 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
6774
6775 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6776 .unwrap();
6777 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
6778}
6779
6780#[gpui::test]
6781async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
6782 init_test(cx, |_| {});
6783
6784 let language = Arc::new(Language::new(
6785 LanguageConfig::default(),
6786 Some(tree_sitter_rust::LANGUAGE.into()),
6787 ));
6788
6789 let text = r#"
6790 use mod1::mod2::{mod3, mod4};
6791
6792 fn fn_1(param1: bool, param2: &str) {
6793 let var1 = "text";
6794 }
6795 "#
6796 .unindent();
6797
6798 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6799 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6800 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6801
6802 editor
6803 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6804 .await;
6805
6806 editor.update_in(cx, |editor, window, cx| {
6807 editor.change_selections(None, window, cx, |s| {
6808 s.select_display_ranges([
6809 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
6810 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
6811 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
6812 ]);
6813 });
6814 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6815 });
6816 editor.update(cx, |editor, cx| {
6817 assert_text_with_selections(
6818 editor,
6819 indoc! {r#"
6820 use mod1::mod2::{mod3, «mod4ˇ»};
6821
6822 fn fn_1«ˇ(param1: bool, param2: &str)» {
6823 let var1 = "«ˇtext»";
6824 }
6825 "#},
6826 cx,
6827 );
6828 });
6829
6830 editor.update_in(cx, |editor, window, cx| {
6831 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6832 });
6833 editor.update(cx, |editor, cx| {
6834 assert_text_with_selections(
6835 editor,
6836 indoc! {r#"
6837 use mod1::mod2::«{mod3, mod4}ˇ»;
6838
6839 «ˇfn fn_1(param1: bool, param2: &str) {
6840 let var1 = "text";
6841 }»
6842 "#},
6843 cx,
6844 );
6845 });
6846
6847 editor.update_in(cx, |editor, window, cx| {
6848 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6849 });
6850 assert_eq!(
6851 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6852 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6853 );
6854
6855 // Trying to expand the selected syntax node one more time has no effect.
6856 editor.update_in(cx, |editor, window, cx| {
6857 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6858 });
6859 assert_eq!(
6860 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6861 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6862 );
6863
6864 editor.update_in(cx, |editor, window, cx| {
6865 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6866 });
6867 editor.update(cx, |editor, cx| {
6868 assert_text_with_selections(
6869 editor,
6870 indoc! {r#"
6871 use mod1::mod2::«{mod3, mod4}ˇ»;
6872
6873 «ˇfn fn_1(param1: bool, param2: &str) {
6874 let var1 = "text";
6875 }»
6876 "#},
6877 cx,
6878 );
6879 });
6880
6881 editor.update_in(cx, |editor, window, cx| {
6882 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6883 });
6884 editor.update(cx, |editor, cx| {
6885 assert_text_with_selections(
6886 editor,
6887 indoc! {r#"
6888 use mod1::mod2::{mod3, «mod4ˇ»};
6889
6890 fn fn_1«ˇ(param1: bool, param2: &str)» {
6891 let var1 = "«ˇtext»";
6892 }
6893 "#},
6894 cx,
6895 );
6896 });
6897
6898 editor.update_in(cx, |editor, window, cx| {
6899 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6900 });
6901 editor.update(cx, |editor, cx| {
6902 assert_text_with_selections(
6903 editor,
6904 indoc! {r#"
6905 use mod1::mod2::{mod3, mo«ˇ»d4};
6906
6907 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6908 let var1 = "te«ˇ»xt";
6909 }
6910 "#},
6911 cx,
6912 );
6913 });
6914
6915 // Trying to shrink the selected syntax node one more time has no effect.
6916 editor.update_in(cx, |editor, window, cx| {
6917 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6918 });
6919 editor.update_in(cx, |editor, _, cx| {
6920 assert_text_with_selections(
6921 editor,
6922 indoc! {r#"
6923 use mod1::mod2::{mod3, mo«ˇ»d4};
6924
6925 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6926 let var1 = "te«ˇ»xt";
6927 }
6928 "#},
6929 cx,
6930 );
6931 });
6932
6933 // Ensure that we keep expanding the selection if the larger selection starts or ends within
6934 // a fold.
6935 editor.update_in(cx, |editor, window, cx| {
6936 editor.fold_creases(
6937 vec![
6938 Crease::simple(
6939 Point::new(0, 21)..Point::new(0, 24),
6940 FoldPlaceholder::test(),
6941 ),
6942 Crease::simple(
6943 Point::new(3, 20)..Point::new(3, 22),
6944 FoldPlaceholder::test(),
6945 ),
6946 ],
6947 true,
6948 window,
6949 cx,
6950 );
6951 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6952 });
6953 editor.update(cx, |editor, cx| {
6954 assert_text_with_selections(
6955 editor,
6956 indoc! {r#"
6957 use mod1::mod2::«{mod3, mod4}ˇ»;
6958
6959 fn fn_1«ˇ(param1: bool, param2: &str)» {
6960 let var1 = "«ˇtext»";
6961 }
6962 "#},
6963 cx,
6964 );
6965 });
6966}
6967
6968#[gpui::test]
6969async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
6970 init_test(cx, |_| {});
6971
6972 let language = Arc::new(Language::new(
6973 LanguageConfig::default(),
6974 Some(tree_sitter_rust::LANGUAGE.into()),
6975 ));
6976
6977 let text = "let a = 2;";
6978
6979 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6980 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6981 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6982
6983 editor
6984 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6985 .await;
6986
6987 // Test case 1: Cursor at end of word
6988 editor.update_in(cx, |editor, window, cx| {
6989 editor.change_selections(None, window, cx, |s| {
6990 s.select_display_ranges([
6991 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
6992 ]);
6993 });
6994 });
6995 editor.update(cx, |editor, cx| {
6996 assert_text_with_selections(editor, "let aˇ = 2;", cx);
6997 });
6998 editor.update_in(cx, |editor, window, cx| {
6999 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7000 });
7001 editor.update(cx, |editor, cx| {
7002 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
7003 });
7004 editor.update_in(cx, |editor, window, cx| {
7005 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7006 });
7007 editor.update(cx, |editor, cx| {
7008 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7009 });
7010
7011 // Test case 2: Cursor at end of statement
7012 editor.update_in(cx, |editor, window, cx| {
7013 editor.change_selections(None, window, cx, |s| {
7014 s.select_display_ranges([
7015 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
7016 ]);
7017 });
7018 });
7019 editor.update(cx, |editor, cx| {
7020 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
7021 });
7022 editor.update_in(cx, |editor, window, cx| {
7023 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7024 });
7025 editor.update(cx, |editor, cx| {
7026 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7027 });
7028}
7029
7030#[gpui::test]
7031async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
7032 init_test(cx, |_| {});
7033
7034 let language = Arc::new(Language::new(
7035 LanguageConfig::default(),
7036 Some(tree_sitter_rust::LANGUAGE.into()),
7037 ));
7038
7039 let text = r#"
7040 use mod1::mod2::{mod3, mod4};
7041
7042 fn fn_1(param1: bool, param2: &str) {
7043 let var1 = "hello world";
7044 }
7045 "#
7046 .unindent();
7047
7048 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7049 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7050 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7051
7052 editor
7053 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7054 .await;
7055
7056 // Test 1: Cursor on a letter of a string word
7057 editor.update_in(cx, |editor, window, cx| {
7058 editor.change_selections(None, window, cx, |s| {
7059 s.select_display_ranges([
7060 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
7061 ]);
7062 });
7063 });
7064 editor.update_in(cx, |editor, window, cx| {
7065 assert_text_with_selections(
7066 editor,
7067 indoc! {r#"
7068 use mod1::mod2::{mod3, mod4};
7069
7070 fn fn_1(param1: bool, param2: &str) {
7071 let var1 = "hˇello world";
7072 }
7073 "#},
7074 cx,
7075 );
7076 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7077 assert_text_with_selections(
7078 editor,
7079 indoc! {r#"
7080 use mod1::mod2::{mod3, mod4};
7081
7082 fn fn_1(param1: bool, param2: &str) {
7083 let var1 = "«ˇhello» world";
7084 }
7085 "#},
7086 cx,
7087 );
7088 });
7089
7090 // Test 2: Partial selection within a word
7091 editor.update_in(cx, |editor, window, cx| {
7092 editor.change_selections(None, window, cx, |s| {
7093 s.select_display_ranges([
7094 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
7095 ]);
7096 });
7097 });
7098 editor.update_in(cx, |editor, window, cx| {
7099 assert_text_with_selections(
7100 editor,
7101 indoc! {r#"
7102 use mod1::mod2::{mod3, mod4};
7103
7104 fn fn_1(param1: bool, param2: &str) {
7105 let var1 = "h«elˇ»lo world";
7106 }
7107 "#},
7108 cx,
7109 );
7110 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7111 assert_text_with_selections(
7112 editor,
7113 indoc! {r#"
7114 use mod1::mod2::{mod3, mod4};
7115
7116 fn fn_1(param1: bool, param2: &str) {
7117 let var1 = "«ˇhello» world";
7118 }
7119 "#},
7120 cx,
7121 );
7122 });
7123
7124 // Test 3: Complete word already selected
7125 editor.update_in(cx, |editor, window, cx| {
7126 editor.change_selections(None, window, cx, |s| {
7127 s.select_display_ranges([
7128 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
7129 ]);
7130 });
7131 });
7132 editor.update_in(cx, |editor, window, cx| {
7133 assert_text_with_selections(
7134 editor,
7135 indoc! {r#"
7136 use mod1::mod2::{mod3, mod4};
7137
7138 fn fn_1(param1: bool, param2: &str) {
7139 let var1 = "«helloˇ» world";
7140 }
7141 "#},
7142 cx,
7143 );
7144 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7145 assert_text_with_selections(
7146 editor,
7147 indoc! {r#"
7148 use mod1::mod2::{mod3, mod4};
7149
7150 fn fn_1(param1: bool, param2: &str) {
7151 let var1 = "«hello worldˇ»";
7152 }
7153 "#},
7154 cx,
7155 );
7156 });
7157
7158 // Test 4: Selection spanning across words
7159 editor.update_in(cx, |editor, window, cx| {
7160 editor.change_selections(None, window, cx, |s| {
7161 s.select_display_ranges([
7162 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
7163 ]);
7164 });
7165 });
7166 editor.update_in(cx, |editor, window, cx| {
7167 assert_text_with_selections(
7168 editor,
7169 indoc! {r#"
7170 use mod1::mod2::{mod3, mod4};
7171
7172 fn fn_1(param1: bool, param2: &str) {
7173 let var1 = "hel«lo woˇ»rld";
7174 }
7175 "#},
7176 cx,
7177 );
7178 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7179 assert_text_with_selections(
7180 editor,
7181 indoc! {r#"
7182 use mod1::mod2::{mod3, mod4};
7183
7184 fn fn_1(param1: bool, param2: &str) {
7185 let var1 = "«ˇhello world»";
7186 }
7187 "#},
7188 cx,
7189 );
7190 });
7191
7192 // Test 5: Expansion beyond string
7193 editor.update_in(cx, |editor, window, cx| {
7194 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7195 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7196 assert_text_with_selections(
7197 editor,
7198 indoc! {r#"
7199 use mod1::mod2::{mod3, mod4};
7200
7201 fn fn_1(param1: bool, param2: &str) {
7202 «ˇlet var1 = "hello world";»
7203 }
7204 "#},
7205 cx,
7206 );
7207 });
7208}
7209
7210#[gpui::test]
7211async fn test_fold_function_bodies(cx: &mut TestAppContext) {
7212 init_test(cx, |_| {});
7213
7214 let base_text = r#"
7215 impl A {
7216 // this is an uncommitted comment
7217
7218 fn b() {
7219 c();
7220 }
7221
7222 // this is another uncommitted comment
7223
7224 fn d() {
7225 // e
7226 // f
7227 }
7228 }
7229
7230 fn g() {
7231 // h
7232 }
7233 "#
7234 .unindent();
7235
7236 let text = r#"
7237 ˇimpl A {
7238
7239 fn b() {
7240 c();
7241 }
7242
7243 fn d() {
7244 // e
7245 // f
7246 }
7247 }
7248
7249 fn g() {
7250 // h
7251 }
7252 "#
7253 .unindent();
7254
7255 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7256 cx.set_state(&text);
7257 cx.set_head_text(&base_text);
7258 cx.update_editor(|editor, window, cx| {
7259 editor.expand_all_diff_hunks(&Default::default(), window, cx);
7260 });
7261
7262 cx.assert_state_with_diff(
7263 "
7264 ˇimpl A {
7265 - // this is an uncommitted comment
7266
7267 fn b() {
7268 c();
7269 }
7270
7271 - // this is another uncommitted comment
7272 -
7273 fn d() {
7274 // e
7275 // f
7276 }
7277 }
7278
7279 fn g() {
7280 // h
7281 }
7282 "
7283 .unindent(),
7284 );
7285
7286 let expected_display_text = "
7287 impl A {
7288 // this is an uncommitted comment
7289
7290 fn b() {
7291 ⋯
7292 }
7293
7294 // this is another uncommitted comment
7295
7296 fn d() {
7297 ⋯
7298 }
7299 }
7300
7301 fn g() {
7302 ⋯
7303 }
7304 "
7305 .unindent();
7306
7307 cx.update_editor(|editor, window, cx| {
7308 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
7309 assert_eq!(editor.display_text(cx), expected_display_text);
7310 });
7311}
7312
7313#[gpui::test]
7314async fn test_autoindent(cx: &mut TestAppContext) {
7315 init_test(cx, |_| {});
7316
7317 let language = Arc::new(
7318 Language::new(
7319 LanguageConfig {
7320 brackets: BracketPairConfig {
7321 pairs: vec![
7322 BracketPair {
7323 start: "{".to_string(),
7324 end: "}".to_string(),
7325 close: false,
7326 surround: false,
7327 newline: true,
7328 },
7329 BracketPair {
7330 start: "(".to_string(),
7331 end: ")".to_string(),
7332 close: false,
7333 surround: false,
7334 newline: true,
7335 },
7336 ],
7337 ..Default::default()
7338 },
7339 ..Default::default()
7340 },
7341 Some(tree_sitter_rust::LANGUAGE.into()),
7342 )
7343 .with_indents_query(
7344 r#"
7345 (_ "(" ")" @end) @indent
7346 (_ "{" "}" @end) @indent
7347 "#,
7348 )
7349 .unwrap(),
7350 );
7351
7352 let text = "fn a() {}";
7353
7354 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7355 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7356 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7357 editor
7358 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7359 .await;
7360
7361 editor.update_in(cx, |editor, window, cx| {
7362 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
7363 editor.newline(&Newline, window, cx);
7364 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
7365 assert_eq!(
7366 editor.selections.ranges(cx),
7367 &[
7368 Point::new(1, 4)..Point::new(1, 4),
7369 Point::new(3, 4)..Point::new(3, 4),
7370 Point::new(5, 0)..Point::new(5, 0)
7371 ]
7372 );
7373 });
7374}
7375
7376#[gpui::test]
7377async fn test_autoindent_selections(cx: &mut TestAppContext) {
7378 init_test(cx, |_| {});
7379
7380 {
7381 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7382 cx.set_state(indoc! {"
7383 impl A {
7384
7385 fn b() {}
7386
7387 «fn c() {
7388
7389 }ˇ»
7390 }
7391 "});
7392
7393 cx.update_editor(|editor, window, cx| {
7394 editor.autoindent(&Default::default(), window, cx);
7395 });
7396
7397 cx.assert_editor_state(indoc! {"
7398 impl A {
7399
7400 fn b() {}
7401
7402 «fn c() {
7403
7404 }ˇ»
7405 }
7406 "});
7407 }
7408
7409 {
7410 let mut cx = EditorTestContext::new_multibuffer(
7411 cx,
7412 [indoc! { "
7413 impl A {
7414 «
7415 // a
7416 fn b(){}
7417 »
7418 «
7419 }
7420 fn c(){}
7421 »
7422 "}],
7423 );
7424
7425 let buffer = cx.update_editor(|editor, _, cx| {
7426 let buffer = editor.buffer().update(cx, |buffer, _| {
7427 buffer.all_buffers().iter().next().unwrap().clone()
7428 });
7429 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7430 buffer
7431 });
7432
7433 cx.run_until_parked();
7434 cx.update_editor(|editor, window, cx| {
7435 editor.select_all(&Default::default(), window, cx);
7436 editor.autoindent(&Default::default(), window, cx)
7437 });
7438 cx.run_until_parked();
7439
7440 cx.update(|_, cx| {
7441 assert_eq!(
7442 buffer.read(cx).text(),
7443 indoc! { "
7444 impl A {
7445
7446 // a
7447 fn b(){}
7448
7449
7450 }
7451 fn c(){}
7452
7453 " }
7454 )
7455 });
7456 }
7457}
7458
7459#[gpui::test]
7460async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
7461 init_test(cx, |_| {});
7462
7463 let mut cx = EditorTestContext::new(cx).await;
7464
7465 let language = Arc::new(Language::new(
7466 LanguageConfig {
7467 brackets: BracketPairConfig {
7468 pairs: vec![
7469 BracketPair {
7470 start: "{".to_string(),
7471 end: "}".to_string(),
7472 close: true,
7473 surround: true,
7474 newline: true,
7475 },
7476 BracketPair {
7477 start: "(".to_string(),
7478 end: ")".to_string(),
7479 close: true,
7480 surround: true,
7481 newline: true,
7482 },
7483 BracketPair {
7484 start: "/*".to_string(),
7485 end: " */".to_string(),
7486 close: true,
7487 surround: true,
7488 newline: true,
7489 },
7490 BracketPair {
7491 start: "[".to_string(),
7492 end: "]".to_string(),
7493 close: false,
7494 surround: false,
7495 newline: true,
7496 },
7497 BracketPair {
7498 start: "\"".to_string(),
7499 end: "\"".to_string(),
7500 close: true,
7501 surround: true,
7502 newline: false,
7503 },
7504 BracketPair {
7505 start: "<".to_string(),
7506 end: ">".to_string(),
7507 close: false,
7508 surround: true,
7509 newline: true,
7510 },
7511 ],
7512 ..Default::default()
7513 },
7514 autoclose_before: "})]".to_string(),
7515 ..Default::default()
7516 },
7517 Some(tree_sitter_rust::LANGUAGE.into()),
7518 ));
7519
7520 cx.language_registry().add(language.clone());
7521 cx.update_buffer(|buffer, cx| {
7522 buffer.set_language(Some(language), cx);
7523 });
7524
7525 cx.set_state(
7526 &r#"
7527 🏀ˇ
7528 εˇ
7529 ❤️ˇ
7530 "#
7531 .unindent(),
7532 );
7533
7534 // autoclose multiple nested brackets at multiple cursors
7535 cx.update_editor(|editor, window, cx| {
7536 editor.handle_input("{", window, cx);
7537 editor.handle_input("{", window, cx);
7538 editor.handle_input("{", window, cx);
7539 });
7540 cx.assert_editor_state(
7541 &"
7542 🏀{{{ˇ}}}
7543 ε{{{ˇ}}}
7544 ❤️{{{ˇ}}}
7545 "
7546 .unindent(),
7547 );
7548
7549 // insert a different closing bracket
7550 cx.update_editor(|editor, window, cx| {
7551 editor.handle_input(")", window, cx);
7552 });
7553 cx.assert_editor_state(
7554 &"
7555 🏀{{{)ˇ}}}
7556 ε{{{)ˇ}}}
7557 ❤️{{{)ˇ}}}
7558 "
7559 .unindent(),
7560 );
7561
7562 // skip over the auto-closed brackets when typing a closing bracket
7563 cx.update_editor(|editor, window, cx| {
7564 editor.move_right(&MoveRight, window, cx);
7565 editor.handle_input("}", window, cx);
7566 editor.handle_input("}", window, cx);
7567 editor.handle_input("}", window, cx);
7568 });
7569 cx.assert_editor_state(
7570 &"
7571 🏀{{{)}}}}ˇ
7572 ε{{{)}}}}ˇ
7573 ❤️{{{)}}}}ˇ
7574 "
7575 .unindent(),
7576 );
7577
7578 // autoclose multi-character pairs
7579 cx.set_state(
7580 &"
7581 ˇ
7582 ˇ
7583 "
7584 .unindent(),
7585 );
7586 cx.update_editor(|editor, window, cx| {
7587 editor.handle_input("/", window, cx);
7588 editor.handle_input("*", window, cx);
7589 });
7590 cx.assert_editor_state(
7591 &"
7592 /*ˇ */
7593 /*ˇ */
7594 "
7595 .unindent(),
7596 );
7597
7598 // one cursor autocloses a multi-character pair, one cursor
7599 // does not autoclose.
7600 cx.set_state(
7601 &"
7602 /ˇ
7603 ˇ
7604 "
7605 .unindent(),
7606 );
7607 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
7608 cx.assert_editor_state(
7609 &"
7610 /*ˇ */
7611 *ˇ
7612 "
7613 .unindent(),
7614 );
7615
7616 // Don't autoclose if the next character isn't whitespace and isn't
7617 // listed in the language's "autoclose_before" section.
7618 cx.set_state("ˇa b");
7619 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7620 cx.assert_editor_state("{ˇa b");
7621
7622 // Don't autoclose if `close` is false for the bracket pair
7623 cx.set_state("ˇ");
7624 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
7625 cx.assert_editor_state("[ˇ");
7626
7627 // Surround with brackets if text is selected
7628 cx.set_state("«aˇ» b");
7629 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7630 cx.assert_editor_state("{«aˇ»} b");
7631
7632 // Autoclose when not immediately after a word character
7633 cx.set_state("a ˇ");
7634 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7635 cx.assert_editor_state("a \"ˇ\"");
7636
7637 // Autoclose pair where the start and end characters are the same
7638 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7639 cx.assert_editor_state("a \"\"ˇ");
7640
7641 // Don't autoclose when immediately after a word character
7642 cx.set_state("aˇ");
7643 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7644 cx.assert_editor_state("a\"ˇ");
7645
7646 // Do autoclose when after a non-word character
7647 cx.set_state("{ˇ");
7648 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7649 cx.assert_editor_state("{\"ˇ\"");
7650
7651 // Non identical pairs autoclose regardless of preceding character
7652 cx.set_state("aˇ");
7653 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7654 cx.assert_editor_state("a{ˇ}");
7655
7656 // Don't autoclose pair if autoclose is disabled
7657 cx.set_state("ˇ");
7658 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7659 cx.assert_editor_state("<ˇ");
7660
7661 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
7662 cx.set_state("«aˇ» b");
7663 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7664 cx.assert_editor_state("<«aˇ»> b");
7665}
7666
7667#[gpui::test]
7668async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
7669 init_test(cx, |settings| {
7670 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7671 });
7672
7673 let mut cx = EditorTestContext::new(cx).await;
7674
7675 let language = Arc::new(Language::new(
7676 LanguageConfig {
7677 brackets: BracketPairConfig {
7678 pairs: vec![
7679 BracketPair {
7680 start: "{".to_string(),
7681 end: "}".to_string(),
7682 close: true,
7683 surround: true,
7684 newline: true,
7685 },
7686 BracketPair {
7687 start: "(".to_string(),
7688 end: ")".to_string(),
7689 close: true,
7690 surround: true,
7691 newline: true,
7692 },
7693 BracketPair {
7694 start: "[".to_string(),
7695 end: "]".to_string(),
7696 close: false,
7697 surround: false,
7698 newline: true,
7699 },
7700 ],
7701 ..Default::default()
7702 },
7703 autoclose_before: "})]".to_string(),
7704 ..Default::default()
7705 },
7706 Some(tree_sitter_rust::LANGUAGE.into()),
7707 ));
7708
7709 cx.language_registry().add(language.clone());
7710 cx.update_buffer(|buffer, cx| {
7711 buffer.set_language(Some(language), cx);
7712 });
7713
7714 cx.set_state(
7715 &"
7716 ˇ
7717 ˇ
7718 ˇ
7719 "
7720 .unindent(),
7721 );
7722
7723 // ensure only matching closing brackets are skipped over
7724 cx.update_editor(|editor, window, cx| {
7725 editor.handle_input("}", window, cx);
7726 editor.move_left(&MoveLeft, window, cx);
7727 editor.handle_input(")", window, cx);
7728 editor.move_left(&MoveLeft, window, cx);
7729 });
7730 cx.assert_editor_state(
7731 &"
7732 ˇ)}
7733 ˇ)}
7734 ˇ)}
7735 "
7736 .unindent(),
7737 );
7738
7739 // skip-over closing brackets at multiple cursors
7740 cx.update_editor(|editor, window, cx| {
7741 editor.handle_input(")", window, cx);
7742 editor.handle_input("}", window, cx);
7743 });
7744 cx.assert_editor_state(
7745 &"
7746 )}ˇ
7747 )}ˇ
7748 )}ˇ
7749 "
7750 .unindent(),
7751 );
7752
7753 // ignore non-close brackets
7754 cx.update_editor(|editor, window, cx| {
7755 editor.handle_input("]", window, cx);
7756 editor.move_left(&MoveLeft, window, cx);
7757 editor.handle_input("]", window, cx);
7758 });
7759 cx.assert_editor_state(
7760 &"
7761 )}]ˇ]
7762 )}]ˇ]
7763 )}]ˇ]
7764 "
7765 .unindent(),
7766 );
7767}
7768
7769#[gpui::test]
7770async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
7771 init_test(cx, |_| {});
7772
7773 let mut cx = EditorTestContext::new(cx).await;
7774
7775 let html_language = Arc::new(
7776 Language::new(
7777 LanguageConfig {
7778 name: "HTML".into(),
7779 brackets: BracketPairConfig {
7780 pairs: vec![
7781 BracketPair {
7782 start: "<".into(),
7783 end: ">".into(),
7784 close: true,
7785 ..Default::default()
7786 },
7787 BracketPair {
7788 start: "{".into(),
7789 end: "}".into(),
7790 close: true,
7791 ..Default::default()
7792 },
7793 BracketPair {
7794 start: "(".into(),
7795 end: ")".into(),
7796 close: true,
7797 ..Default::default()
7798 },
7799 ],
7800 ..Default::default()
7801 },
7802 autoclose_before: "})]>".into(),
7803 ..Default::default()
7804 },
7805 Some(tree_sitter_html::LANGUAGE.into()),
7806 )
7807 .with_injection_query(
7808 r#"
7809 (script_element
7810 (raw_text) @injection.content
7811 (#set! injection.language "javascript"))
7812 "#,
7813 )
7814 .unwrap(),
7815 );
7816
7817 let javascript_language = Arc::new(Language::new(
7818 LanguageConfig {
7819 name: "JavaScript".into(),
7820 brackets: BracketPairConfig {
7821 pairs: vec![
7822 BracketPair {
7823 start: "/*".into(),
7824 end: " */".into(),
7825 close: true,
7826 ..Default::default()
7827 },
7828 BracketPair {
7829 start: "{".into(),
7830 end: "}".into(),
7831 close: true,
7832 ..Default::default()
7833 },
7834 BracketPair {
7835 start: "(".into(),
7836 end: ")".into(),
7837 close: true,
7838 ..Default::default()
7839 },
7840 ],
7841 ..Default::default()
7842 },
7843 autoclose_before: "})]>".into(),
7844 ..Default::default()
7845 },
7846 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
7847 ));
7848
7849 cx.language_registry().add(html_language.clone());
7850 cx.language_registry().add(javascript_language.clone());
7851
7852 cx.update_buffer(|buffer, cx| {
7853 buffer.set_language(Some(html_language), cx);
7854 });
7855
7856 cx.set_state(
7857 &r#"
7858 <body>ˇ
7859 <script>
7860 var x = 1;ˇ
7861 </script>
7862 </body>ˇ
7863 "#
7864 .unindent(),
7865 );
7866
7867 // Precondition: different languages are active at different locations.
7868 cx.update_editor(|editor, window, cx| {
7869 let snapshot = editor.snapshot(window, cx);
7870 let cursors = editor.selections.ranges::<usize>(cx);
7871 let languages = cursors
7872 .iter()
7873 .map(|c| snapshot.language_at(c.start).unwrap().name())
7874 .collect::<Vec<_>>();
7875 assert_eq!(
7876 languages,
7877 &["HTML".into(), "JavaScript".into(), "HTML".into()]
7878 );
7879 });
7880
7881 // Angle brackets autoclose in HTML, but not JavaScript.
7882 cx.update_editor(|editor, window, cx| {
7883 editor.handle_input("<", window, cx);
7884 editor.handle_input("a", window, cx);
7885 });
7886 cx.assert_editor_state(
7887 &r#"
7888 <body><aˇ>
7889 <script>
7890 var x = 1;<aˇ
7891 </script>
7892 </body><aˇ>
7893 "#
7894 .unindent(),
7895 );
7896
7897 // Curly braces and parens autoclose in both HTML and JavaScript.
7898 cx.update_editor(|editor, window, cx| {
7899 editor.handle_input(" b=", window, cx);
7900 editor.handle_input("{", window, cx);
7901 editor.handle_input("c", window, cx);
7902 editor.handle_input("(", window, cx);
7903 });
7904 cx.assert_editor_state(
7905 &r#"
7906 <body><a b={c(ˇ)}>
7907 <script>
7908 var x = 1;<a b={c(ˇ)}
7909 </script>
7910 </body><a b={c(ˇ)}>
7911 "#
7912 .unindent(),
7913 );
7914
7915 // Brackets that were already autoclosed are skipped.
7916 cx.update_editor(|editor, window, cx| {
7917 editor.handle_input(")", window, cx);
7918 editor.handle_input("d", window, cx);
7919 editor.handle_input("}", window, cx);
7920 });
7921 cx.assert_editor_state(
7922 &r#"
7923 <body><a b={c()d}ˇ>
7924 <script>
7925 var x = 1;<a b={c()d}ˇ
7926 </script>
7927 </body><a b={c()d}ˇ>
7928 "#
7929 .unindent(),
7930 );
7931 cx.update_editor(|editor, window, cx| {
7932 editor.handle_input(">", window, cx);
7933 });
7934 cx.assert_editor_state(
7935 &r#"
7936 <body><a b={c()d}>ˇ
7937 <script>
7938 var x = 1;<a b={c()d}>ˇ
7939 </script>
7940 </body><a b={c()d}>ˇ
7941 "#
7942 .unindent(),
7943 );
7944
7945 // Reset
7946 cx.set_state(
7947 &r#"
7948 <body>ˇ
7949 <script>
7950 var x = 1;ˇ
7951 </script>
7952 </body>ˇ
7953 "#
7954 .unindent(),
7955 );
7956
7957 cx.update_editor(|editor, window, cx| {
7958 editor.handle_input("<", window, cx);
7959 });
7960 cx.assert_editor_state(
7961 &r#"
7962 <body><ˇ>
7963 <script>
7964 var x = 1;<ˇ
7965 </script>
7966 </body><ˇ>
7967 "#
7968 .unindent(),
7969 );
7970
7971 // When backspacing, the closing angle brackets are removed.
7972 cx.update_editor(|editor, window, cx| {
7973 editor.backspace(&Backspace, window, cx);
7974 });
7975 cx.assert_editor_state(
7976 &r#"
7977 <body>ˇ
7978 <script>
7979 var x = 1;ˇ
7980 </script>
7981 </body>ˇ
7982 "#
7983 .unindent(),
7984 );
7985
7986 // Block comments autoclose in JavaScript, but not HTML.
7987 cx.update_editor(|editor, window, cx| {
7988 editor.handle_input("/", window, cx);
7989 editor.handle_input("*", window, cx);
7990 });
7991 cx.assert_editor_state(
7992 &r#"
7993 <body>/*ˇ
7994 <script>
7995 var x = 1;/*ˇ */
7996 </script>
7997 </body>/*ˇ
7998 "#
7999 .unindent(),
8000 );
8001}
8002
8003#[gpui::test]
8004async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
8005 init_test(cx, |_| {});
8006
8007 let mut cx = EditorTestContext::new(cx).await;
8008
8009 let rust_language = Arc::new(
8010 Language::new(
8011 LanguageConfig {
8012 name: "Rust".into(),
8013 brackets: serde_json::from_value(json!([
8014 { "start": "{", "end": "}", "close": true, "newline": true },
8015 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
8016 ]))
8017 .unwrap(),
8018 autoclose_before: "})]>".into(),
8019 ..Default::default()
8020 },
8021 Some(tree_sitter_rust::LANGUAGE.into()),
8022 )
8023 .with_override_query("(string_literal) @string")
8024 .unwrap(),
8025 );
8026
8027 cx.language_registry().add(rust_language.clone());
8028 cx.update_buffer(|buffer, cx| {
8029 buffer.set_language(Some(rust_language), cx);
8030 });
8031
8032 cx.set_state(
8033 &r#"
8034 let x = ˇ
8035 "#
8036 .unindent(),
8037 );
8038
8039 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
8040 cx.update_editor(|editor, window, cx| {
8041 editor.handle_input("\"", window, cx);
8042 });
8043 cx.assert_editor_state(
8044 &r#"
8045 let x = "ˇ"
8046 "#
8047 .unindent(),
8048 );
8049
8050 // Inserting another quotation mark. The cursor moves across the existing
8051 // automatically-inserted quotation mark.
8052 cx.update_editor(|editor, window, cx| {
8053 editor.handle_input("\"", window, cx);
8054 });
8055 cx.assert_editor_state(
8056 &r#"
8057 let x = ""ˇ
8058 "#
8059 .unindent(),
8060 );
8061
8062 // Reset
8063 cx.set_state(
8064 &r#"
8065 let x = ˇ
8066 "#
8067 .unindent(),
8068 );
8069
8070 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
8071 cx.update_editor(|editor, window, cx| {
8072 editor.handle_input("\"", window, cx);
8073 editor.handle_input(" ", window, cx);
8074 editor.move_left(&Default::default(), window, cx);
8075 editor.handle_input("\\", window, cx);
8076 editor.handle_input("\"", window, cx);
8077 });
8078 cx.assert_editor_state(
8079 &r#"
8080 let x = "\"ˇ "
8081 "#
8082 .unindent(),
8083 );
8084
8085 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
8086 // mark. Nothing is inserted.
8087 cx.update_editor(|editor, window, cx| {
8088 editor.move_right(&Default::default(), window, cx);
8089 editor.handle_input("\"", window, cx);
8090 });
8091 cx.assert_editor_state(
8092 &r#"
8093 let x = "\" "ˇ
8094 "#
8095 .unindent(),
8096 );
8097}
8098
8099#[gpui::test]
8100async fn test_surround_with_pair(cx: &mut TestAppContext) {
8101 init_test(cx, |_| {});
8102
8103 let language = Arc::new(Language::new(
8104 LanguageConfig {
8105 brackets: BracketPairConfig {
8106 pairs: vec![
8107 BracketPair {
8108 start: "{".to_string(),
8109 end: "}".to_string(),
8110 close: true,
8111 surround: true,
8112 newline: true,
8113 },
8114 BracketPair {
8115 start: "/* ".to_string(),
8116 end: "*/".to_string(),
8117 close: true,
8118 surround: true,
8119 ..Default::default()
8120 },
8121 ],
8122 ..Default::default()
8123 },
8124 ..Default::default()
8125 },
8126 Some(tree_sitter_rust::LANGUAGE.into()),
8127 ));
8128
8129 let text = r#"
8130 a
8131 b
8132 c
8133 "#
8134 .unindent();
8135
8136 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8137 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8138 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8139 editor
8140 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8141 .await;
8142
8143 editor.update_in(cx, |editor, window, cx| {
8144 editor.change_selections(None, window, cx, |s| {
8145 s.select_display_ranges([
8146 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8147 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8148 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
8149 ])
8150 });
8151
8152 editor.handle_input("{", window, cx);
8153 editor.handle_input("{", window, cx);
8154 editor.handle_input("{", window, cx);
8155 assert_eq!(
8156 editor.text(cx),
8157 "
8158 {{{a}}}
8159 {{{b}}}
8160 {{{c}}}
8161 "
8162 .unindent()
8163 );
8164 assert_eq!(
8165 editor.selections.display_ranges(cx),
8166 [
8167 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
8168 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
8169 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
8170 ]
8171 );
8172
8173 editor.undo(&Undo, window, cx);
8174 editor.undo(&Undo, window, cx);
8175 editor.undo(&Undo, window, cx);
8176 assert_eq!(
8177 editor.text(cx),
8178 "
8179 a
8180 b
8181 c
8182 "
8183 .unindent()
8184 );
8185 assert_eq!(
8186 editor.selections.display_ranges(cx),
8187 [
8188 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8189 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8190 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8191 ]
8192 );
8193
8194 // Ensure inserting the first character of a multi-byte bracket pair
8195 // doesn't surround the selections with the bracket.
8196 editor.handle_input("/", window, cx);
8197 assert_eq!(
8198 editor.text(cx),
8199 "
8200 /
8201 /
8202 /
8203 "
8204 .unindent()
8205 );
8206 assert_eq!(
8207 editor.selections.display_ranges(cx),
8208 [
8209 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8210 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8211 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8212 ]
8213 );
8214
8215 editor.undo(&Undo, window, cx);
8216 assert_eq!(
8217 editor.text(cx),
8218 "
8219 a
8220 b
8221 c
8222 "
8223 .unindent()
8224 );
8225 assert_eq!(
8226 editor.selections.display_ranges(cx),
8227 [
8228 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8229 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8230 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8231 ]
8232 );
8233
8234 // Ensure inserting the last character of a multi-byte bracket pair
8235 // doesn't surround the selections with the bracket.
8236 editor.handle_input("*", window, cx);
8237 assert_eq!(
8238 editor.text(cx),
8239 "
8240 *
8241 *
8242 *
8243 "
8244 .unindent()
8245 );
8246 assert_eq!(
8247 editor.selections.display_ranges(cx),
8248 [
8249 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8250 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8251 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8252 ]
8253 );
8254 });
8255}
8256
8257#[gpui::test]
8258async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
8259 init_test(cx, |_| {});
8260
8261 let language = Arc::new(Language::new(
8262 LanguageConfig {
8263 brackets: BracketPairConfig {
8264 pairs: vec![BracketPair {
8265 start: "{".to_string(),
8266 end: "}".to_string(),
8267 close: true,
8268 surround: true,
8269 newline: true,
8270 }],
8271 ..Default::default()
8272 },
8273 autoclose_before: "}".to_string(),
8274 ..Default::default()
8275 },
8276 Some(tree_sitter_rust::LANGUAGE.into()),
8277 ));
8278
8279 let text = r#"
8280 a
8281 b
8282 c
8283 "#
8284 .unindent();
8285
8286 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8287 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8288 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8289 editor
8290 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8291 .await;
8292
8293 editor.update_in(cx, |editor, window, cx| {
8294 editor.change_selections(None, window, cx, |s| {
8295 s.select_ranges([
8296 Point::new(0, 1)..Point::new(0, 1),
8297 Point::new(1, 1)..Point::new(1, 1),
8298 Point::new(2, 1)..Point::new(2, 1),
8299 ])
8300 });
8301
8302 editor.handle_input("{", window, cx);
8303 editor.handle_input("{", window, cx);
8304 editor.handle_input("_", window, cx);
8305 assert_eq!(
8306 editor.text(cx),
8307 "
8308 a{{_}}
8309 b{{_}}
8310 c{{_}}
8311 "
8312 .unindent()
8313 );
8314 assert_eq!(
8315 editor.selections.ranges::<Point>(cx),
8316 [
8317 Point::new(0, 4)..Point::new(0, 4),
8318 Point::new(1, 4)..Point::new(1, 4),
8319 Point::new(2, 4)..Point::new(2, 4)
8320 ]
8321 );
8322
8323 editor.backspace(&Default::default(), window, cx);
8324 editor.backspace(&Default::default(), window, cx);
8325 assert_eq!(
8326 editor.text(cx),
8327 "
8328 a{}
8329 b{}
8330 c{}
8331 "
8332 .unindent()
8333 );
8334 assert_eq!(
8335 editor.selections.ranges::<Point>(cx),
8336 [
8337 Point::new(0, 2)..Point::new(0, 2),
8338 Point::new(1, 2)..Point::new(1, 2),
8339 Point::new(2, 2)..Point::new(2, 2)
8340 ]
8341 );
8342
8343 editor.delete_to_previous_word_start(&Default::default(), window, cx);
8344 assert_eq!(
8345 editor.text(cx),
8346 "
8347 a
8348 b
8349 c
8350 "
8351 .unindent()
8352 );
8353 assert_eq!(
8354 editor.selections.ranges::<Point>(cx),
8355 [
8356 Point::new(0, 1)..Point::new(0, 1),
8357 Point::new(1, 1)..Point::new(1, 1),
8358 Point::new(2, 1)..Point::new(2, 1)
8359 ]
8360 );
8361 });
8362}
8363
8364#[gpui::test]
8365async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
8366 init_test(cx, |settings| {
8367 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8368 });
8369
8370 let mut cx = EditorTestContext::new(cx).await;
8371
8372 let language = Arc::new(Language::new(
8373 LanguageConfig {
8374 brackets: BracketPairConfig {
8375 pairs: vec![
8376 BracketPair {
8377 start: "{".to_string(),
8378 end: "}".to_string(),
8379 close: true,
8380 surround: true,
8381 newline: true,
8382 },
8383 BracketPair {
8384 start: "(".to_string(),
8385 end: ")".to_string(),
8386 close: true,
8387 surround: true,
8388 newline: true,
8389 },
8390 BracketPair {
8391 start: "[".to_string(),
8392 end: "]".to_string(),
8393 close: false,
8394 surround: true,
8395 newline: true,
8396 },
8397 ],
8398 ..Default::default()
8399 },
8400 autoclose_before: "})]".to_string(),
8401 ..Default::default()
8402 },
8403 Some(tree_sitter_rust::LANGUAGE.into()),
8404 ));
8405
8406 cx.language_registry().add(language.clone());
8407 cx.update_buffer(|buffer, cx| {
8408 buffer.set_language(Some(language), cx);
8409 });
8410
8411 cx.set_state(
8412 &"
8413 {(ˇ)}
8414 [[ˇ]]
8415 {(ˇ)}
8416 "
8417 .unindent(),
8418 );
8419
8420 cx.update_editor(|editor, window, cx| {
8421 editor.backspace(&Default::default(), window, cx);
8422 editor.backspace(&Default::default(), window, cx);
8423 });
8424
8425 cx.assert_editor_state(
8426 &"
8427 ˇ
8428 ˇ]]
8429 ˇ
8430 "
8431 .unindent(),
8432 );
8433
8434 cx.update_editor(|editor, window, cx| {
8435 editor.handle_input("{", window, cx);
8436 editor.handle_input("{", window, cx);
8437 editor.move_right(&MoveRight, window, cx);
8438 editor.move_right(&MoveRight, window, cx);
8439 editor.move_left(&MoveLeft, window, cx);
8440 editor.move_left(&MoveLeft, window, cx);
8441 editor.backspace(&Default::default(), window, cx);
8442 });
8443
8444 cx.assert_editor_state(
8445 &"
8446 {ˇ}
8447 {ˇ}]]
8448 {ˇ}
8449 "
8450 .unindent(),
8451 );
8452
8453 cx.update_editor(|editor, window, cx| {
8454 editor.backspace(&Default::default(), window, cx);
8455 });
8456
8457 cx.assert_editor_state(
8458 &"
8459 ˇ
8460 ˇ]]
8461 ˇ
8462 "
8463 .unindent(),
8464 );
8465}
8466
8467#[gpui::test]
8468async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
8469 init_test(cx, |_| {});
8470
8471 let language = Arc::new(Language::new(
8472 LanguageConfig::default(),
8473 Some(tree_sitter_rust::LANGUAGE.into()),
8474 ));
8475
8476 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
8477 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8478 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8479 editor
8480 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8481 .await;
8482
8483 editor.update_in(cx, |editor, window, cx| {
8484 editor.set_auto_replace_emoji_shortcode(true);
8485
8486 editor.handle_input("Hello ", window, cx);
8487 editor.handle_input(":wave", window, cx);
8488 assert_eq!(editor.text(cx), "Hello :wave".unindent());
8489
8490 editor.handle_input(":", window, cx);
8491 assert_eq!(editor.text(cx), "Hello 👋".unindent());
8492
8493 editor.handle_input(" :smile", window, cx);
8494 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
8495
8496 editor.handle_input(":", window, cx);
8497 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
8498
8499 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
8500 editor.handle_input(":wave", window, cx);
8501 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
8502
8503 editor.handle_input(":", window, cx);
8504 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
8505
8506 editor.handle_input(":1", window, cx);
8507 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
8508
8509 editor.handle_input(":", window, cx);
8510 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
8511
8512 // Ensure shortcode does not get replaced when it is part of a word
8513 editor.handle_input(" Test:wave", window, cx);
8514 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
8515
8516 editor.handle_input(":", window, cx);
8517 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
8518
8519 editor.set_auto_replace_emoji_shortcode(false);
8520
8521 // Ensure shortcode does not get replaced when auto replace is off
8522 editor.handle_input(" :wave", window, cx);
8523 assert_eq!(
8524 editor.text(cx),
8525 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
8526 );
8527
8528 editor.handle_input(":", window, cx);
8529 assert_eq!(
8530 editor.text(cx),
8531 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
8532 );
8533 });
8534}
8535
8536#[gpui::test]
8537async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
8538 init_test(cx, |_| {});
8539
8540 let (text, insertion_ranges) = marked_text_ranges(
8541 indoc! {"
8542 ˇ
8543 "},
8544 false,
8545 );
8546
8547 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
8548 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8549
8550 _ = editor.update_in(cx, |editor, window, cx| {
8551 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
8552
8553 editor
8554 .insert_snippet(&insertion_ranges, snippet, window, cx)
8555 .unwrap();
8556
8557 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
8558 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
8559 assert_eq!(editor.text(cx), expected_text);
8560 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
8561 }
8562
8563 assert(
8564 editor,
8565 cx,
8566 indoc! {"
8567 type «» =•
8568 "},
8569 );
8570
8571 assert!(editor.context_menu_visible(), "There should be a matches");
8572 });
8573}
8574
8575#[gpui::test]
8576async fn test_snippets(cx: &mut TestAppContext) {
8577 init_test(cx, |_| {});
8578
8579 let mut cx = EditorTestContext::new(cx).await;
8580
8581 cx.set_state(indoc! {"
8582 a.ˇ b
8583 a.ˇ b
8584 a.ˇ b
8585 "});
8586
8587 cx.update_editor(|editor, window, cx| {
8588 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
8589 let insertion_ranges = editor
8590 .selections
8591 .all(cx)
8592 .iter()
8593 .map(|s| s.range().clone())
8594 .collect::<Vec<_>>();
8595 editor
8596 .insert_snippet(&insertion_ranges, snippet, window, cx)
8597 .unwrap();
8598 });
8599
8600 cx.assert_editor_state(indoc! {"
8601 a.f(«oneˇ», two, «threeˇ») b
8602 a.f(«oneˇ», two, «threeˇ») b
8603 a.f(«oneˇ», two, «threeˇ») b
8604 "});
8605
8606 // Can't move earlier than the first tab stop
8607 cx.update_editor(|editor, window, cx| {
8608 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
8609 });
8610 cx.assert_editor_state(indoc! {"
8611 a.f(«oneˇ», two, «threeˇ») b
8612 a.f(«oneˇ», two, «threeˇ») b
8613 a.f(«oneˇ», two, «threeˇ») b
8614 "});
8615
8616 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
8617 cx.assert_editor_state(indoc! {"
8618 a.f(one, «twoˇ», three) b
8619 a.f(one, «twoˇ», three) b
8620 a.f(one, «twoˇ», three) b
8621 "});
8622
8623 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
8624 cx.assert_editor_state(indoc! {"
8625 a.f(«oneˇ», two, «threeˇ») b
8626 a.f(«oneˇ», two, «threeˇ») b
8627 a.f(«oneˇ», two, «threeˇ») b
8628 "});
8629
8630 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
8631 cx.assert_editor_state(indoc! {"
8632 a.f(one, «twoˇ», three) b
8633 a.f(one, «twoˇ», three) b
8634 a.f(one, «twoˇ», three) b
8635 "});
8636 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
8637 cx.assert_editor_state(indoc! {"
8638 a.f(one, two, three)ˇ b
8639 a.f(one, two, three)ˇ b
8640 a.f(one, two, three)ˇ b
8641 "});
8642
8643 // As soon as the last tab stop is reached, snippet state is gone
8644 cx.update_editor(|editor, window, cx| {
8645 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
8646 });
8647 cx.assert_editor_state(indoc! {"
8648 a.f(one, two, three)ˇ b
8649 a.f(one, two, three)ˇ b
8650 a.f(one, two, three)ˇ b
8651 "});
8652}
8653
8654#[gpui::test]
8655async fn test_snippet_indentation(cx: &mut TestAppContext) {
8656 init_test(cx, |_| {});
8657
8658 let mut cx = EditorTestContext::new(cx).await;
8659
8660 cx.update_editor(|editor, window, cx| {
8661 let snippet = Snippet::parse(indoc! {"
8662 /*
8663 * Multiline comment with leading indentation
8664 *
8665 * $1
8666 */
8667 $0"})
8668 .unwrap();
8669 let insertion_ranges = editor
8670 .selections
8671 .all(cx)
8672 .iter()
8673 .map(|s| s.range().clone())
8674 .collect::<Vec<_>>();
8675 editor
8676 .insert_snippet(&insertion_ranges, snippet, window, cx)
8677 .unwrap();
8678 });
8679
8680 cx.assert_editor_state(indoc! {"
8681 /*
8682 * Multiline comment with leading indentation
8683 *
8684 * ˇ
8685 */
8686 "});
8687
8688 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
8689 cx.assert_editor_state(indoc! {"
8690 /*
8691 * Multiline comment with leading indentation
8692 *
8693 *•
8694 */
8695 ˇ"});
8696}
8697
8698#[gpui::test]
8699async fn test_document_format_during_save(cx: &mut TestAppContext) {
8700 init_test(cx, |_| {});
8701
8702 let fs = FakeFs::new(cx.executor());
8703 fs.insert_file(path!("/file.rs"), Default::default()).await;
8704
8705 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
8706
8707 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8708 language_registry.add(rust_lang());
8709 let mut fake_servers = language_registry.register_fake_lsp(
8710 "Rust",
8711 FakeLspAdapter {
8712 capabilities: lsp::ServerCapabilities {
8713 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8714 ..Default::default()
8715 },
8716 ..Default::default()
8717 },
8718 );
8719
8720 let buffer = project
8721 .update(cx, |project, cx| {
8722 project.open_local_buffer(path!("/file.rs"), cx)
8723 })
8724 .await
8725 .unwrap();
8726
8727 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8728 let (editor, cx) = cx.add_window_view(|window, cx| {
8729 build_editor_with_project(project.clone(), buffer, window, cx)
8730 });
8731 editor.update_in(cx, |editor, window, cx| {
8732 editor.set_text("one\ntwo\nthree\n", window, cx)
8733 });
8734 assert!(cx.read(|cx| editor.is_dirty(cx)));
8735
8736 cx.executor().start_waiting();
8737 let fake_server = fake_servers.next().await.unwrap();
8738
8739 {
8740 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8741 move |params, _| async move {
8742 assert_eq!(
8743 params.text_document.uri,
8744 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8745 );
8746 assert_eq!(params.options.tab_size, 4);
8747 Ok(Some(vec![lsp::TextEdit::new(
8748 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8749 ", ".to_string(),
8750 )]))
8751 },
8752 );
8753 let save = editor
8754 .update_in(cx, |editor, window, cx| {
8755 editor.save(true, project.clone(), window, cx)
8756 })
8757 .unwrap();
8758 cx.executor().start_waiting();
8759 save.await;
8760
8761 assert_eq!(
8762 editor.update(cx, |editor, cx| editor.text(cx)),
8763 "one, two\nthree\n"
8764 );
8765 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8766 }
8767
8768 {
8769 editor.update_in(cx, |editor, window, cx| {
8770 editor.set_text("one\ntwo\nthree\n", window, cx)
8771 });
8772 assert!(cx.read(|cx| editor.is_dirty(cx)));
8773
8774 // Ensure we can still save even if formatting hangs.
8775 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8776 move |params, _| async move {
8777 assert_eq!(
8778 params.text_document.uri,
8779 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8780 );
8781 futures::future::pending::<()>().await;
8782 unreachable!()
8783 },
8784 );
8785 let save = editor
8786 .update_in(cx, |editor, window, cx| {
8787 editor.save(true, project.clone(), window, cx)
8788 })
8789 .unwrap();
8790 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8791 cx.executor().start_waiting();
8792 save.await;
8793 assert_eq!(
8794 editor.update(cx, |editor, cx| editor.text(cx)),
8795 "one\ntwo\nthree\n"
8796 );
8797 }
8798
8799 // For non-dirty buffer, no formatting request should be sent
8800 {
8801 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8802
8803 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8804 panic!("Should not be invoked on non-dirty buffer");
8805 });
8806 let save = editor
8807 .update_in(cx, |editor, window, cx| {
8808 editor.save(true, project.clone(), window, cx)
8809 })
8810 .unwrap();
8811 cx.executor().start_waiting();
8812 save.await;
8813 }
8814
8815 // Set rust language override and assert overridden tabsize is sent to language server
8816 update_test_language_settings(cx, |settings| {
8817 settings.languages.insert(
8818 "Rust".into(),
8819 LanguageSettingsContent {
8820 tab_size: NonZeroU32::new(8),
8821 ..Default::default()
8822 },
8823 );
8824 });
8825
8826 {
8827 editor.update_in(cx, |editor, window, cx| {
8828 editor.set_text("somehting_new\n", window, cx)
8829 });
8830 assert!(cx.read(|cx| editor.is_dirty(cx)));
8831 let _formatting_request_signal = fake_server
8832 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8833 assert_eq!(
8834 params.text_document.uri,
8835 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8836 );
8837 assert_eq!(params.options.tab_size, 8);
8838 Ok(Some(vec![]))
8839 });
8840 let save = editor
8841 .update_in(cx, |editor, window, cx| {
8842 editor.save(true, project.clone(), window, cx)
8843 })
8844 .unwrap();
8845 cx.executor().start_waiting();
8846 save.await;
8847 }
8848}
8849
8850#[gpui::test]
8851async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
8852 init_test(cx, |_| {});
8853
8854 let cols = 4;
8855 let rows = 10;
8856 let sample_text_1 = sample_text(rows, cols, 'a');
8857 assert_eq!(
8858 sample_text_1,
8859 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
8860 );
8861 let sample_text_2 = sample_text(rows, cols, 'l');
8862 assert_eq!(
8863 sample_text_2,
8864 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
8865 );
8866 let sample_text_3 = sample_text(rows, cols, 'v');
8867 assert_eq!(
8868 sample_text_3,
8869 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
8870 );
8871
8872 let fs = FakeFs::new(cx.executor());
8873 fs.insert_tree(
8874 path!("/a"),
8875 json!({
8876 "main.rs": sample_text_1,
8877 "other.rs": sample_text_2,
8878 "lib.rs": sample_text_3,
8879 }),
8880 )
8881 .await;
8882
8883 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
8884 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
8885 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
8886
8887 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8888 language_registry.add(rust_lang());
8889 let mut fake_servers = language_registry.register_fake_lsp(
8890 "Rust",
8891 FakeLspAdapter {
8892 capabilities: lsp::ServerCapabilities {
8893 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8894 ..Default::default()
8895 },
8896 ..Default::default()
8897 },
8898 );
8899
8900 let worktree = project.update(cx, |project, cx| {
8901 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
8902 assert_eq!(worktrees.len(), 1);
8903 worktrees.pop().unwrap()
8904 });
8905 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
8906
8907 let buffer_1 = project
8908 .update(cx, |project, cx| {
8909 project.open_buffer((worktree_id, "main.rs"), cx)
8910 })
8911 .await
8912 .unwrap();
8913 let buffer_2 = project
8914 .update(cx, |project, cx| {
8915 project.open_buffer((worktree_id, "other.rs"), cx)
8916 })
8917 .await
8918 .unwrap();
8919 let buffer_3 = project
8920 .update(cx, |project, cx| {
8921 project.open_buffer((worktree_id, "lib.rs"), cx)
8922 })
8923 .await
8924 .unwrap();
8925
8926 let multi_buffer = cx.new(|cx| {
8927 let mut multi_buffer = MultiBuffer::new(ReadWrite);
8928 multi_buffer.push_excerpts(
8929 buffer_1.clone(),
8930 [
8931 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8932 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8933 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8934 ],
8935 cx,
8936 );
8937 multi_buffer.push_excerpts(
8938 buffer_2.clone(),
8939 [
8940 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8941 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8942 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8943 ],
8944 cx,
8945 );
8946 multi_buffer.push_excerpts(
8947 buffer_3.clone(),
8948 [
8949 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8950 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8951 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8952 ],
8953 cx,
8954 );
8955 multi_buffer
8956 });
8957 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
8958 Editor::new(
8959 EditorMode::full(),
8960 multi_buffer,
8961 Some(project.clone()),
8962 window,
8963 cx,
8964 )
8965 });
8966
8967 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8968 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8969 s.select_ranges(Some(1..2))
8970 });
8971 editor.insert("|one|two|three|", window, cx);
8972 });
8973 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8974 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8975 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8976 s.select_ranges(Some(60..70))
8977 });
8978 editor.insert("|four|five|six|", window, cx);
8979 });
8980 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8981
8982 // First two buffers should be edited, but not the third one.
8983 assert_eq!(
8984 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8985 "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}",
8986 );
8987 buffer_1.update(cx, |buffer, _| {
8988 assert!(buffer.is_dirty());
8989 assert_eq!(
8990 buffer.text(),
8991 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
8992 )
8993 });
8994 buffer_2.update(cx, |buffer, _| {
8995 assert!(buffer.is_dirty());
8996 assert_eq!(
8997 buffer.text(),
8998 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
8999 )
9000 });
9001 buffer_3.update(cx, |buffer, _| {
9002 assert!(!buffer.is_dirty());
9003 assert_eq!(buffer.text(), sample_text_3,)
9004 });
9005 cx.executor().run_until_parked();
9006
9007 cx.executor().start_waiting();
9008 let save = multi_buffer_editor
9009 .update_in(cx, |editor, window, cx| {
9010 editor.save(true, project.clone(), window, cx)
9011 })
9012 .unwrap();
9013
9014 let fake_server = fake_servers.next().await.unwrap();
9015 fake_server
9016 .server
9017 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
9018 Ok(Some(vec![lsp::TextEdit::new(
9019 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9020 format!("[{} formatted]", params.text_document.uri),
9021 )]))
9022 })
9023 .detach();
9024 save.await;
9025
9026 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
9027 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
9028 assert_eq!(
9029 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9030 uri!(
9031 "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}"
9032 ),
9033 );
9034 buffer_1.update(cx, |buffer, _| {
9035 assert!(!buffer.is_dirty());
9036 assert_eq!(
9037 buffer.text(),
9038 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
9039 )
9040 });
9041 buffer_2.update(cx, |buffer, _| {
9042 assert!(!buffer.is_dirty());
9043 assert_eq!(
9044 buffer.text(),
9045 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
9046 )
9047 });
9048 buffer_3.update(cx, |buffer, _| {
9049 assert!(!buffer.is_dirty());
9050 assert_eq!(buffer.text(), sample_text_3,)
9051 });
9052}
9053
9054#[gpui::test]
9055async fn test_range_format_during_save(cx: &mut TestAppContext) {
9056 init_test(cx, |_| {});
9057
9058 let fs = FakeFs::new(cx.executor());
9059 fs.insert_file(path!("/file.rs"), Default::default()).await;
9060
9061 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9062
9063 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9064 language_registry.add(rust_lang());
9065 let mut fake_servers = language_registry.register_fake_lsp(
9066 "Rust",
9067 FakeLspAdapter {
9068 capabilities: lsp::ServerCapabilities {
9069 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
9070 ..Default::default()
9071 },
9072 ..Default::default()
9073 },
9074 );
9075
9076 let buffer = project
9077 .update(cx, |project, cx| {
9078 project.open_local_buffer(path!("/file.rs"), cx)
9079 })
9080 .await
9081 .unwrap();
9082
9083 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9084 let (editor, cx) = cx.add_window_view(|window, cx| {
9085 build_editor_with_project(project.clone(), buffer, window, cx)
9086 });
9087 editor.update_in(cx, |editor, window, cx| {
9088 editor.set_text("one\ntwo\nthree\n", window, cx)
9089 });
9090 assert!(cx.read(|cx| editor.is_dirty(cx)));
9091
9092 cx.executor().start_waiting();
9093 let fake_server = fake_servers.next().await.unwrap();
9094
9095 let save = editor
9096 .update_in(cx, |editor, window, cx| {
9097 editor.save(true, project.clone(), window, cx)
9098 })
9099 .unwrap();
9100 fake_server
9101 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
9102 assert_eq!(
9103 params.text_document.uri,
9104 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9105 );
9106 assert_eq!(params.options.tab_size, 4);
9107 Ok(Some(vec![lsp::TextEdit::new(
9108 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9109 ", ".to_string(),
9110 )]))
9111 })
9112 .next()
9113 .await;
9114 cx.executor().start_waiting();
9115 save.await;
9116 assert_eq!(
9117 editor.update(cx, |editor, cx| editor.text(cx)),
9118 "one, two\nthree\n"
9119 );
9120 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9121
9122 editor.update_in(cx, |editor, window, cx| {
9123 editor.set_text("one\ntwo\nthree\n", window, cx)
9124 });
9125 assert!(cx.read(|cx| editor.is_dirty(cx)));
9126
9127 // Ensure we can still save even if formatting hangs.
9128 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
9129 move |params, _| async move {
9130 assert_eq!(
9131 params.text_document.uri,
9132 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9133 );
9134 futures::future::pending::<()>().await;
9135 unreachable!()
9136 },
9137 );
9138 let save = editor
9139 .update_in(cx, |editor, window, cx| {
9140 editor.save(true, project.clone(), window, cx)
9141 })
9142 .unwrap();
9143 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9144 cx.executor().start_waiting();
9145 save.await;
9146 assert_eq!(
9147 editor.update(cx, |editor, cx| editor.text(cx)),
9148 "one\ntwo\nthree\n"
9149 );
9150 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9151
9152 // For non-dirty buffer, no formatting request should be sent
9153 let save = editor
9154 .update_in(cx, |editor, window, cx| {
9155 editor.save(true, project.clone(), window, cx)
9156 })
9157 .unwrap();
9158 let _pending_format_request = fake_server
9159 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
9160 panic!("Should not be invoked on non-dirty buffer");
9161 })
9162 .next();
9163 cx.executor().start_waiting();
9164 save.await;
9165
9166 // Set Rust language override and assert overridden tabsize is sent to language server
9167 update_test_language_settings(cx, |settings| {
9168 settings.languages.insert(
9169 "Rust".into(),
9170 LanguageSettingsContent {
9171 tab_size: NonZeroU32::new(8),
9172 ..Default::default()
9173 },
9174 );
9175 });
9176
9177 editor.update_in(cx, |editor, window, cx| {
9178 editor.set_text("somehting_new\n", window, cx)
9179 });
9180 assert!(cx.read(|cx| editor.is_dirty(cx)));
9181 let save = editor
9182 .update_in(cx, |editor, window, cx| {
9183 editor.save(true, project.clone(), window, cx)
9184 })
9185 .unwrap();
9186 fake_server
9187 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
9188 assert_eq!(
9189 params.text_document.uri,
9190 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9191 );
9192 assert_eq!(params.options.tab_size, 8);
9193 Ok(Some(Vec::new()))
9194 })
9195 .next()
9196 .await;
9197 save.await;
9198}
9199
9200#[gpui::test]
9201async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
9202 init_test(cx, |settings| {
9203 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
9204 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
9205 ))
9206 });
9207
9208 let fs = FakeFs::new(cx.executor());
9209 fs.insert_file(path!("/file.rs"), Default::default()).await;
9210
9211 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9212
9213 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9214 language_registry.add(Arc::new(Language::new(
9215 LanguageConfig {
9216 name: "Rust".into(),
9217 matcher: LanguageMatcher {
9218 path_suffixes: vec!["rs".to_string()],
9219 ..Default::default()
9220 },
9221 ..LanguageConfig::default()
9222 },
9223 Some(tree_sitter_rust::LANGUAGE.into()),
9224 )));
9225 update_test_language_settings(cx, |settings| {
9226 // Enable Prettier formatting for the same buffer, and ensure
9227 // LSP is called instead of Prettier.
9228 settings.defaults.prettier = Some(PrettierSettings {
9229 allowed: true,
9230 ..PrettierSettings::default()
9231 });
9232 });
9233 let mut fake_servers = language_registry.register_fake_lsp(
9234 "Rust",
9235 FakeLspAdapter {
9236 capabilities: lsp::ServerCapabilities {
9237 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9238 ..Default::default()
9239 },
9240 ..Default::default()
9241 },
9242 );
9243
9244 let buffer = project
9245 .update(cx, |project, cx| {
9246 project.open_local_buffer(path!("/file.rs"), cx)
9247 })
9248 .await
9249 .unwrap();
9250
9251 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9252 let (editor, cx) = cx.add_window_view(|window, cx| {
9253 build_editor_with_project(project.clone(), buffer, window, cx)
9254 });
9255 editor.update_in(cx, |editor, window, cx| {
9256 editor.set_text("one\ntwo\nthree\n", window, cx)
9257 });
9258
9259 cx.executor().start_waiting();
9260 let fake_server = fake_servers.next().await.unwrap();
9261
9262 let format = editor
9263 .update_in(cx, |editor, window, cx| {
9264 editor.perform_format(
9265 project.clone(),
9266 FormatTrigger::Manual,
9267 FormatTarget::Buffers,
9268 window,
9269 cx,
9270 )
9271 })
9272 .unwrap();
9273 fake_server
9274 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9275 assert_eq!(
9276 params.text_document.uri,
9277 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9278 );
9279 assert_eq!(params.options.tab_size, 4);
9280 Ok(Some(vec![lsp::TextEdit::new(
9281 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9282 ", ".to_string(),
9283 )]))
9284 })
9285 .next()
9286 .await;
9287 cx.executor().start_waiting();
9288 format.await;
9289 assert_eq!(
9290 editor.update(cx, |editor, cx| editor.text(cx)),
9291 "one, two\nthree\n"
9292 );
9293
9294 editor.update_in(cx, |editor, window, cx| {
9295 editor.set_text("one\ntwo\nthree\n", window, cx)
9296 });
9297 // Ensure we don't lock if formatting hangs.
9298 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9299 move |params, _| async move {
9300 assert_eq!(
9301 params.text_document.uri,
9302 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9303 );
9304 futures::future::pending::<()>().await;
9305 unreachable!()
9306 },
9307 );
9308 let format = editor
9309 .update_in(cx, |editor, window, cx| {
9310 editor.perform_format(
9311 project,
9312 FormatTrigger::Manual,
9313 FormatTarget::Buffers,
9314 window,
9315 cx,
9316 )
9317 })
9318 .unwrap();
9319 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9320 cx.executor().start_waiting();
9321 format.await;
9322 assert_eq!(
9323 editor.update(cx, |editor, cx| editor.text(cx)),
9324 "one\ntwo\nthree\n"
9325 );
9326}
9327
9328#[gpui::test]
9329async fn test_multiple_formatters(cx: &mut TestAppContext) {
9330 init_test(cx, |settings| {
9331 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
9332 settings.defaults.formatter =
9333 Some(language_settings::SelectedFormatter::List(FormatterList(
9334 vec![
9335 Formatter::LanguageServer { name: None },
9336 Formatter::CodeActions(
9337 [
9338 ("code-action-1".into(), true),
9339 ("code-action-2".into(), true),
9340 ]
9341 .into_iter()
9342 .collect(),
9343 ),
9344 ]
9345 .into(),
9346 )))
9347 });
9348
9349 let fs = FakeFs::new(cx.executor());
9350 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
9351 .await;
9352
9353 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9354 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9355 language_registry.add(rust_lang());
9356
9357 let mut fake_servers = language_registry.register_fake_lsp(
9358 "Rust",
9359 FakeLspAdapter {
9360 capabilities: lsp::ServerCapabilities {
9361 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9362 execute_command_provider: Some(lsp::ExecuteCommandOptions {
9363 commands: vec!["the-command-for-code-action-1".into()],
9364 ..Default::default()
9365 }),
9366 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9367 ..Default::default()
9368 },
9369 ..Default::default()
9370 },
9371 );
9372
9373 let buffer = project
9374 .update(cx, |project, cx| {
9375 project.open_local_buffer(path!("/file.rs"), cx)
9376 })
9377 .await
9378 .unwrap();
9379
9380 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9381 let (editor, cx) = cx.add_window_view(|window, cx| {
9382 build_editor_with_project(project.clone(), buffer, window, cx)
9383 });
9384
9385 cx.executor().start_waiting();
9386
9387 let fake_server = fake_servers.next().await.unwrap();
9388 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9389 move |_params, _| async move {
9390 Ok(Some(vec![lsp::TextEdit::new(
9391 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9392 "applied-formatting\n".to_string(),
9393 )]))
9394 },
9395 );
9396 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9397 move |params, _| async move {
9398 assert_eq!(
9399 params.context.only,
9400 Some(vec!["code-action-1".into(), "code-action-2".into()])
9401 );
9402 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
9403 Ok(Some(vec![
9404 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9405 kind: Some("code-action-1".into()),
9406 edit: Some(lsp::WorkspaceEdit::new(
9407 [(
9408 uri.clone(),
9409 vec![lsp::TextEdit::new(
9410 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9411 "applied-code-action-1-edit\n".to_string(),
9412 )],
9413 )]
9414 .into_iter()
9415 .collect(),
9416 )),
9417 command: Some(lsp::Command {
9418 command: "the-command-for-code-action-1".into(),
9419 ..Default::default()
9420 }),
9421 ..Default::default()
9422 }),
9423 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9424 kind: Some("code-action-2".into()),
9425 edit: Some(lsp::WorkspaceEdit::new(
9426 [(
9427 uri.clone(),
9428 vec![lsp::TextEdit::new(
9429 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9430 "applied-code-action-2-edit\n".to_string(),
9431 )],
9432 )]
9433 .into_iter()
9434 .collect(),
9435 )),
9436 ..Default::default()
9437 }),
9438 ]))
9439 },
9440 );
9441
9442 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
9443 move |params, _| async move { Ok(params) }
9444 });
9445
9446 let command_lock = Arc::new(futures::lock::Mutex::new(()));
9447 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
9448 let fake = fake_server.clone();
9449 let lock = command_lock.clone();
9450 move |params, _| {
9451 assert_eq!(params.command, "the-command-for-code-action-1");
9452 let fake = fake.clone();
9453 let lock = lock.clone();
9454 async move {
9455 lock.lock().await;
9456 fake.server
9457 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
9458 label: None,
9459 edit: lsp::WorkspaceEdit {
9460 changes: Some(
9461 [(
9462 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
9463 vec![lsp::TextEdit {
9464 range: lsp::Range::new(
9465 lsp::Position::new(0, 0),
9466 lsp::Position::new(0, 0),
9467 ),
9468 new_text: "applied-code-action-1-command\n".into(),
9469 }],
9470 )]
9471 .into_iter()
9472 .collect(),
9473 ),
9474 ..Default::default()
9475 },
9476 })
9477 .await
9478 .into_response()
9479 .unwrap();
9480 Ok(Some(json!(null)))
9481 }
9482 }
9483 });
9484
9485 cx.executor().start_waiting();
9486 editor
9487 .update_in(cx, |editor, window, cx| {
9488 editor.perform_format(
9489 project.clone(),
9490 FormatTrigger::Manual,
9491 FormatTarget::Buffers,
9492 window,
9493 cx,
9494 )
9495 })
9496 .unwrap()
9497 .await;
9498 editor.update(cx, |editor, cx| {
9499 assert_eq!(
9500 editor.text(cx),
9501 r#"
9502 applied-code-action-2-edit
9503 applied-code-action-1-command
9504 applied-code-action-1-edit
9505 applied-formatting
9506 one
9507 two
9508 three
9509 "#
9510 .unindent()
9511 );
9512 });
9513
9514 editor.update_in(cx, |editor, window, cx| {
9515 editor.undo(&Default::default(), window, cx);
9516 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9517 });
9518
9519 // Perform a manual edit while waiting for an LSP command
9520 // that's being run as part of a formatting code action.
9521 let lock_guard = command_lock.lock().await;
9522 let format = editor
9523 .update_in(cx, |editor, window, cx| {
9524 editor.perform_format(
9525 project.clone(),
9526 FormatTrigger::Manual,
9527 FormatTarget::Buffers,
9528 window,
9529 cx,
9530 )
9531 })
9532 .unwrap();
9533 cx.run_until_parked();
9534 editor.update(cx, |editor, cx| {
9535 assert_eq!(
9536 editor.text(cx),
9537 r#"
9538 applied-code-action-1-edit
9539 applied-formatting
9540 one
9541 two
9542 three
9543 "#
9544 .unindent()
9545 );
9546
9547 editor.buffer.update(cx, |buffer, cx| {
9548 let ix = buffer.len(cx);
9549 buffer.edit([(ix..ix, "edited\n")], None, cx);
9550 });
9551 });
9552
9553 // Allow the LSP command to proceed. Because the buffer was edited,
9554 // the second code action will not be run.
9555 drop(lock_guard);
9556 format.await;
9557 editor.update_in(cx, |editor, window, cx| {
9558 assert_eq!(
9559 editor.text(cx),
9560 r#"
9561 applied-code-action-1-command
9562 applied-code-action-1-edit
9563 applied-formatting
9564 one
9565 two
9566 three
9567 edited
9568 "#
9569 .unindent()
9570 );
9571
9572 // The manual edit is undone first, because it is the last thing the user did
9573 // (even though the command completed afterwards).
9574 editor.undo(&Default::default(), window, cx);
9575 assert_eq!(
9576 editor.text(cx),
9577 r#"
9578 applied-code-action-1-command
9579 applied-code-action-1-edit
9580 applied-formatting
9581 one
9582 two
9583 three
9584 "#
9585 .unindent()
9586 );
9587
9588 // All the formatting (including the command, which completed after the manual edit)
9589 // is undone together.
9590 editor.undo(&Default::default(), window, cx);
9591 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9592 });
9593}
9594
9595#[gpui::test]
9596async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
9597 init_test(cx, |settings| {
9598 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
9599 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
9600 ))
9601 });
9602
9603 let fs = FakeFs::new(cx.executor());
9604 fs.insert_file(path!("/file.ts"), Default::default()).await;
9605
9606 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9607
9608 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9609 language_registry.add(Arc::new(Language::new(
9610 LanguageConfig {
9611 name: "TypeScript".into(),
9612 matcher: LanguageMatcher {
9613 path_suffixes: vec!["ts".to_string()],
9614 ..Default::default()
9615 },
9616 ..LanguageConfig::default()
9617 },
9618 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
9619 )));
9620 update_test_language_settings(cx, |settings| {
9621 settings.defaults.prettier = Some(PrettierSettings {
9622 allowed: true,
9623 ..PrettierSettings::default()
9624 });
9625 });
9626 let mut fake_servers = language_registry.register_fake_lsp(
9627 "TypeScript",
9628 FakeLspAdapter {
9629 capabilities: lsp::ServerCapabilities {
9630 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9631 ..Default::default()
9632 },
9633 ..Default::default()
9634 },
9635 );
9636
9637 let buffer = project
9638 .update(cx, |project, cx| {
9639 project.open_local_buffer(path!("/file.ts"), cx)
9640 })
9641 .await
9642 .unwrap();
9643
9644 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9645 let (editor, cx) = cx.add_window_view(|window, cx| {
9646 build_editor_with_project(project.clone(), buffer, window, cx)
9647 });
9648 editor.update_in(cx, |editor, window, cx| {
9649 editor.set_text(
9650 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9651 window,
9652 cx,
9653 )
9654 });
9655
9656 cx.executor().start_waiting();
9657 let fake_server = fake_servers.next().await.unwrap();
9658
9659 let format = editor
9660 .update_in(cx, |editor, window, cx| {
9661 editor.perform_code_action_kind(
9662 project.clone(),
9663 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9664 window,
9665 cx,
9666 )
9667 })
9668 .unwrap();
9669 fake_server
9670 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
9671 assert_eq!(
9672 params.text_document.uri,
9673 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9674 );
9675 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
9676 lsp::CodeAction {
9677 title: "Organize Imports".to_string(),
9678 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
9679 edit: Some(lsp::WorkspaceEdit {
9680 changes: Some(
9681 [(
9682 params.text_document.uri.clone(),
9683 vec![lsp::TextEdit::new(
9684 lsp::Range::new(
9685 lsp::Position::new(1, 0),
9686 lsp::Position::new(2, 0),
9687 ),
9688 "".to_string(),
9689 )],
9690 )]
9691 .into_iter()
9692 .collect(),
9693 ),
9694 ..Default::default()
9695 }),
9696 ..Default::default()
9697 },
9698 )]))
9699 })
9700 .next()
9701 .await;
9702 cx.executor().start_waiting();
9703 format.await;
9704 assert_eq!(
9705 editor.update(cx, |editor, cx| editor.text(cx)),
9706 "import { a } from 'module';\n\nconst x = a;\n"
9707 );
9708
9709 editor.update_in(cx, |editor, window, cx| {
9710 editor.set_text(
9711 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9712 window,
9713 cx,
9714 )
9715 });
9716 // Ensure we don't lock if code action hangs.
9717 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9718 move |params, _| async move {
9719 assert_eq!(
9720 params.text_document.uri,
9721 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9722 );
9723 futures::future::pending::<()>().await;
9724 unreachable!()
9725 },
9726 );
9727 let format = editor
9728 .update_in(cx, |editor, window, cx| {
9729 editor.perform_code_action_kind(
9730 project,
9731 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9732 window,
9733 cx,
9734 )
9735 })
9736 .unwrap();
9737 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
9738 cx.executor().start_waiting();
9739 format.await;
9740 assert_eq!(
9741 editor.update(cx, |editor, cx| editor.text(cx)),
9742 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
9743 );
9744}
9745
9746#[gpui::test]
9747async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
9748 init_test(cx, |_| {});
9749
9750 let mut cx = EditorLspTestContext::new_rust(
9751 lsp::ServerCapabilities {
9752 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9753 ..Default::default()
9754 },
9755 cx,
9756 )
9757 .await;
9758
9759 cx.set_state(indoc! {"
9760 one.twoˇ
9761 "});
9762
9763 // The format request takes a long time. When it completes, it inserts
9764 // a newline and an indent before the `.`
9765 cx.lsp
9766 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
9767 let executor = cx.background_executor().clone();
9768 async move {
9769 executor.timer(Duration::from_millis(100)).await;
9770 Ok(Some(vec![lsp::TextEdit {
9771 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
9772 new_text: "\n ".into(),
9773 }]))
9774 }
9775 });
9776
9777 // Submit a format request.
9778 let format_1 = cx
9779 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9780 .unwrap();
9781 cx.executor().run_until_parked();
9782
9783 // Submit a second format request.
9784 let format_2 = cx
9785 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9786 .unwrap();
9787 cx.executor().run_until_parked();
9788
9789 // Wait for both format requests to complete
9790 cx.executor().advance_clock(Duration::from_millis(200));
9791 cx.executor().start_waiting();
9792 format_1.await.unwrap();
9793 cx.executor().start_waiting();
9794 format_2.await.unwrap();
9795
9796 // The formatting edits only happens once.
9797 cx.assert_editor_state(indoc! {"
9798 one
9799 .twoˇ
9800 "});
9801}
9802
9803#[gpui::test]
9804async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
9805 init_test(cx, |settings| {
9806 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
9807 });
9808
9809 let mut cx = EditorLspTestContext::new_rust(
9810 lsp::ServerCapabilities {
9811 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9812 ..Default::default()
9813 },
9814 cx,
9815 )
9816 .await;
9817
9818 // Set up a buffer white some trailing whitespace and no trailing newline.
9819 cx.set_state(
9820 &[
9821 "one ", //
9822 "twoˇ", //
9823 "three ", //
9824 "four", //
9825 ]
9826 .join("\n"),
9827 );
9828
9829 // Submit a format request.
9830 let format = cx
9831 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9832 .unwrap();
9833
9834 // Record which buffer changes have been sent to the language server
9835 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
9836 cx.lsp
9837 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
9838 let buffer_changes = buffer_changes.clone();
9839 move |params, _| {
9840 buffer_changes.lock().extend(
9841 params
9842 .content_changes
9843 .into_iter()
9844 .map(|e| (e.range.unwrap(), e.text)),
9845 );
9846 }
9847 });
9848
9849 // Handle formatting requests to the language server.
9850 cx.lsp
9851 .set_request_handler::<lsp::request::Formatting, _, _>({
9852 let buffer_changes = buffer_changes.clone();
9853 move |_, _| {
9854 // When formatting is requested, trailing whitespace has already been stripped,
9855 // and the trailing newline has already been added.
9856 assert_eq!(
9857 &buffer_changes.lock()[1..],
9858 &[
9859 (
9860 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
9861 "".into()
9862 ),
9863 (
9864 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
9865 "".into()
9866 ),
9867 (
9868 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
9869 "\n".into()
9870 ),
9871 ]
9872 );
9873
9874 // Insert blank lines between each line of the buffer.
9875 async move {
9876 Ok(Some(vec![
9877 lsp::TextEdit {
9878 range: lsp::Range::new(
9879 lsp::Position::new(1, 0),
9880 lsp::Position::new(1, 0),
9881 ),
9882 new_text: "\n".into(),
9883 },
9884 lsp::TextEdit {
9885 range: lsp::Range::new(
9886 lsp::Position::new(2, 0),
9887 lsp::Position::new(2, 0),
9888 ),
9889 new_text: "\n".into(),
9890 },
9891 ]))
9892 }
9893 }
9894 });
9895
9896 // After formatting the buffer, the trailing whitespace is stripped,
9897 // a newline is appended, and the edits provided by the language server
9898 // have been applied.
9899 format.await.unwrap();
9900 cx.assert_editor_state(
9901 &[
9902 "one", //
9903 "", //
9904 "twoˇ", //
9905 "", //
9906 "three", //
9907 "four", //
9908 "", //
9909 ]
9910 .join("\n"),
9911 );
9912
9913 // Undoing the formatting undoes the trailing whitespace removal, the
9914 // trailing newline, and the LSP edits.
9915 cx.update_buffer(|buffer, cx| buffer.undo(cx));
9916 cx.assert_editor_state(
9917 &[
9918 "one ", //
9919 "twoˇ", //
9920 "three ", //
9921 "four", //
9922 ]
9923 .join("\n"),
9924 );
9925}
9926
9927#[gpui::test]
9928async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
9929 cx: &mut TestAppContext,
9930) {
9931 init_test(cx, |_| {});
9932
9933 cx.update(|cx| {
9934 cx.update_global::<SettingsStore, _>(|settings, cx| {
9935 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9936 settings.auto_signature_help = Some(true);
9937 });
9938 });
9939 });
9940
9941 let mut cx = EditorLspTestContext::new_rust(
9942 lsp::ServerCapabilities {
9943 signature_help_provider: Some(lsp::SignatureHelpOptions {
9944 ..Default::default()
9945 }),
9946 ..Default::default()
9947 },
9948 cx,
9949 )
9950 .await;
9951
9952 let language = Language::new(
9953 LanguageConfig {
9954 name: "Rust".into(),
9955 brackets: BracketPairConfig {
9956 pairs: vec![
9957 BracketPair {
9958 start: "{".to_string(),
9959 end: "}".to_string(),
9960 close: true,
9961 surround: true,
9962 newline: true,
9963 },
9964 BracketPair {
9965 start: "(".to_string(),
9966 end: ")".to_string(),
9967 close: true,
9968 surround: true,
9969 newline: true,
9970 },
9971 BracketPair {
9972 start: "/*".to_string(),
9973 end: " */".to_string(),
9974 close: true,
9975 surround: true,
9976 newline: true,
9977 },
9978 BracketPair {
9979 start: "[".to_string(),
9980 end: "]".to_string(),
9981 close: false,
9982 surround: false,
9983 newline: true,
9984 },
9985 BracketPair {
9986 start: "\"".to_string(),
9987 end: "\"".to_string(),
9988 close: true,
9989 surround: true,
9990 newline: false,
9991 },
9992 BracketPair {
9993 start: "<".to_string(),
9994 end: ">".to_string(),
9995 close: false,
9996 surround: true,
9997 newline: true,
9998 },
9999 ],
10000 ..Default::default()
10001 },
10002 autoclose_before: "})]".to_string(),
10003 ..Default::default()
10004 },
10005 Some(tree_sitter_rust::LANGUAGE.into()),
10006 );
10007 let language = Arc::new(language);
10008
10009 cx.language_registry().add(language.clone());
10010 cx.update_buffer(|buffer, cx| {
10011 buffer.set_language(Some(language), cx);
10012 });
10013
10014 cx.set_state(
10015 &r#"
10016 fn main() {
10017 sampleˇ
10018 }
10019 "#
10020 .unindent(),
10021 );
10022
10023 cx.update_editor(|editor, window, cx| {
10024 editor.handle_input("(", window, cx);
10025 });
10026 cx.assert_editor_state(
10027 &"
10028 fn main() {
10029 sample(ˇ)
10030 }
10031 "
10032 .unindent(),
10033 );
10034
10035 let mocked_response = lsp::SignatureHelp {
10036 signatures: vec![lsp::SignatureInformation {
10037 label: "fn sample(param1: u8, param2: u8)".to_string(),
10038 documentation: None,
10039 parameters: Some(vec![
10040 lsp::ParameterInformation {
10041 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10042 documentation: None,
10043 },
10044 lsp::ParameterInformation {
10045 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10046 documentation: None,
10047 },
10048 ]),
10049 active_parameter: None,
10050 }],
10051 active_signature: Some(0),
10052 active_parameter: Some(0),
10053 };
10054 handle_signature_help_request(&mut cx, mocked_response).await;
10055
10056 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10057 .await;
10058
10059 cx.editor(|editor, _, _| {
10060 let signature_help_state = editor.signature_help_state.popover().cloned();
10061 assert_eq!(
10062 signature_help_state.unwrap().label,
10063 "param1: u8, param2: u8"
10064 );
10065 });
10066}
10067
10068#[gpui::test]
10069async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
10070 init_test(cx, |_| {});
10071
10072 cx.update(|cx| {
10073 cx.update_global::<SettingsStore, _>(|settings, cx| {
10074 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10075 settings.auto_signature_help = Some(false);
10076 settings.show_signature_help_after_edits = Some(false);
10077 });
10078 });
10079 });
10080
10081 let mut cx = EditorLspTestContext::new_rust(
10082 lsp::ServerCapabilities {
10083 signature_help_provider: Some(lsp::SignatureHelpOptions {
10084 ..Default::default()
10085 }),
10086 ..Default::default()
10087 },
10088 cx,
10089 )
10090 .await;
10091
10092 let language = Language::new(
10093 LanguageConfig {
10094 name: "Rust".into(),
10095 brackets: BracketPairConfig {
10096 pairs: vec![
10097 BracketPair {
10098 start: "{".to_string(),
10099 end: "}".to_string(),
10100 close: true,
10101 surround: true,
10102 newline: true,
10103 },
10104 BracketPair {
10105 start: "(".to_string(),
10106 end: ")".to_string(),
10107 close: true,
10108 surround: true,
10109 newline: true,
10110 },
10111 BracketPair {
10112 start: "/*".to_string(),
10113 end: " */".to_string(),
10114 close: true,
10115 surround: true,
10116 newline: true,
10117 },
10118 BracketPair {
10119 start: "[".to_string(),
10120 end: "]".to_string(),
10121 close: false,
10122 surround: false,
10123 newline: true,
10124 },
10125 BracketPair {
10126 start: "\"".to_string(),
10127 end: "\"".to_string(),
10128 close: true,
10129 surround: true,
10130 newline: false,
10131 },
10132 BracketPair {
10133 start: "<".to_string(),
10134 end: ">".to_string(),
10135 close: false,
10136 surround: true,
10137 newline: true,
10138 },
10139 ],
10140 ..Default::default()
10141 },
10142 autoclose_before: "})]".to_string(),
10143 ..Default::default()
10144 },
10145 Some(tree_sitter_rust::LANGUAGE.into()),
10146 );
10147 let language = Arc::new(language);
10148
10149 cx.language_registry().add(language.clone());
10150 cx.update_buffer(|buffer, cx| {
10151 buffer.set_language(Some(language), cx);
10152 });
10153
10154 // Ensure that signature_help is not called when no signature help is enabled.
10155 cx.set_state(
10156 &r#"
10157 fn main() {
10158 sampleˇ
10159 }
10160 "#
10161 .unindent(),
10162 );
10163 cx.update_editor(|editor, window, cx| {
10164 editor.handle_input("(", window, cx);
10165 });
10166 cx.assert_editor_state(
10167 &"
10168 fn main() {
10169 sample(ˇ)
10170 }
10171 "
10172 .unindent(),
10173 );
10174 cx.editor(|editor, _, _| {
10175 assert!(editor.signature_help_state.task().is_none());
10176 });
10177
10178 let mocked_response = lsp::SignatureHelp {
10179 signatures: vec![lsp::SignatureInformation {
10180 label: "fn sample(param1: u8, param2: u8)".to_string(),
10181 documentation: None,
10182 parameters: Some(vec![
10183 lsp::ParameterInformation {
10184 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10185 documentation: None,
10186 },
10187 lsp::ParameterInformation {
10188 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10189 documentation: None,
10190 },
10191 ]),
10192 active_parameter: None,
10193 }],
10194 active_signature: Some(0),
10195 active_parameter: Some(0),
10196 };
10197
10198 // Ensure that signature_help is called when enabled afte edits
10199 cx.update(|_, cx| {
10200 cx.update_global::<SettingsStore, _>(|settings, cx| {
10201 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10202 settings.auto_signature_help = Some(false);
10203 settings.show_signature_help_after_edits = Some(true);
10204 });
10205 });
10206 });
10207 cx.set_state(
10208 &r#"
10209 fn main() {
10210 sampleˇ
10211 }
10212 "#
10213 .unindent(),
10214 );
10215 cx.update_editor(|editor, window, cx| {
10216 editor.handle_input("(", window, cx);
10217 });
10218 cx.assert_editor_state(
10219 &"
10220 fn main() {
10221 sample(ˇ)
10222 }
10223 "
10224 .unindent(),
10225 );
10226 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10227 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10228 .await;
10229 cx.update_editor(|editor, _, _| {
10230 let signature_help_state = editor.signature_help_state.popover().cloned();
10231 assert!(signature_help_state.is_some());
10232 assert_eq!(
10233 signature_help_state.unwrap().label,
10234 "param1: u8, param2: u8"
10235 );
10236 editor.signature_help_state = SignatureHelpState::default();
10237 });
10238
10239 // Ensure that signature_help is called when auto signature help override is enabled
10240 cx.update(|_, cx| {
10241 cx.update_global::<SettingsStore, _>(|settings, cx| {
10242 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10243 settings.auto_signature_help = Some(true);
10244 settings.show_signature_help_after_edits = Some(false);
10245 });
10246 });
10247 });
10248 cx.set_state(
10249 &r#"
10250 fn main() {
10251 sampleˇ
10252 }
10253 "#
10254 .unindent(),
10255 );
10256 cx.update_editor(|editor, window, cx| {
10257 editor.handle_input("(", window, cx);
10258 });
10259 cx.assert_editor_state(
10260 &"
10261 fn main() {
10262 sample(ˇ)
10263 }
10264 "
10265 .unindent(),
10266 );
10267 handle_signature_help_request(&mut cx, mocked_response).await;
10268 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10269 .await;
10270 cx.editor(|editor, _, _| {
10271 let signature_help_state = editor.signature_help_state.popover().cloned();
10272 assert!(signature_help_state.is_some());
10273 assert_eq!(
10274 signature_help_state.unwrap().label,
10275 "param1: u8, param2: u8"
10276 );
10277 });
10278}
10279
10280#[gpui::test]
10281async fn test_signature_help(cx: &mut TestAppContext) {
10282 init_test(cx, |_| {});
10283 cx.update(|cx| {
10284 cx.update_global::<SettingsStore, _>(|settings, cx| {
10285 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10286 settings.auto_signature_help = Some(true);
10287 });
10288 });
10289 });
10290
10291 let mut cx = EditorLspTestContext::new_rust(
10292 lsp::ServerCapabilities {
10293 signature_help_provider: Some(lsp::SignatureHelpOptions {
10294 ..Default::default()
10295 }),
10296 ..Default::default()
10297 },
10298 cx,
10299 )
10300 .await;
10301
10302 // A test that directly calls `show_signature_help`
10303 cx.update_editor(|editor, window, cx| {
10304 editor.show_signature_help(&ShowSignatureHelp, window, cx);
10305 });
10306
10307 let mocked_response = lsp::SignatureHelp {
10308 signatures: vec![lsp::SignatureInformation {
10309 label: "fn sample(param1: u8, param2: u8)".to_string(),
10310 documentation: None,
10311 parameters: Some(vec![
10312 lsp::ParameterInformation {
10313 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10314 documentation: None,
10315 },
10316 lsp::ParameterInformation {
10317 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10318 documentation: None,
10319 },
10320 ]),
10321 active_parameter: None,
10322 }],
10323 active_signature: Some(0),
10324 active_parameter: Some(0),
10325 };
10326 handle_signature_help_request(&mut cx, mocked_response).await;
10327
10328 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10329 .await;
10330
10331 cx.editor(|editor, _, _| {
10332 let signature_help_state = editor.signature_help_state.popover().cloned();
10333 assert!(signature_help_state.is_some());
10334 assert_eq!(
10335 signature_help_state.unwrap().label,
10336 "param1: u8, param2: u8"
10337 );
10338 });
10339
10340 // When exiting outside from inside the brackets, `signature_help` is closed.
10341 cx.set_state(indoc! {"
10342 fn main() {
10343 sample(ˇ);
10344 }
10345
10346 fn sample(param1: u8, param2: u8) {}
10347 "});
10348
10349 cx.update_editor(|editor, window, cx| {
10350 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
10351 });
10352
10353 let mocked_response = lsp::SignatureHelp {
10354 signatures: Vec::new(),
10355 active_signature: None,
10356 active_parameter: None,
10357 };
10358 handle_signature_help_request(&mut cx, mocked_response).await;
10359
10360 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10361 .await;
10362
10363 cx.editor(|editor, _, _| {
10364 assert!(!editor.signature_help_state.is_shown());
10365 });
10366
10367 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
10368 cx.set_state(indoc! {"
10369 fn main() {
10370 sample(ˇ);
10371 }
10372
10373 fn sample(param1: u8, param2: u8) {}
10374 "});
10375
10376 let mocked_response = lsp::SignatureHelp {
10377 signatures: vec![lsp::SignatureInformation {
10378 label: "fn sample(param1: u8, param2: u8)".to_string(),
10379 documentation: None,
10380 parameters: Some(vec![
10381 lsp::ParameterInformation {
10382 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10383 documentation: None,
10384 },
10385 lsp::ParameterInformation {
10386 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10387 documentation: None,
10388 },
10389 ]),
10390 active_parameter: None,
10391 }],
10392 active_signature: Some(0),
10393 active_parameter: Some(0),
10394 };
10395 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10396 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10397 .await;
10398 cx.editor(|editor, _, _| {
10399 assert!(editor.signature_help_state.is_shown());
10400 });
10401
10402 // Restore the popover with more parameter input
10403 cx.set_state(indoc! {"
10404 fn main() {
10405 sample(param1, param2ˇ);
10406 }
10407
10408 fn sample(param1: u8, param2: u8) {}
10409 "});
10410
10411 let mocked_response = lsp::SignatureHelp {
10412 signatures: vec![lsp::SignatureInformation {
10413 label: "fn sample(param1: u8, param2: u8)".to_string(),
10414 documentation: None,
10415 parameters: Some(vec![
10416 lsp::ParameterInformation {
10417 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10418 documentation: None,
10419 },
10420 lsp::ParameterInformation {
10421 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10422 documentation: None,
10423 },
10424 ]),
10425 active_parameter: None,
10426 }],
10427 active_signature: Some(0),
10428 active_parameter: Some(1),
10429 };
10430 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10431 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10432 .await;
10433
10434 // When selecting a range, the popover is gone.
10435 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
10436 cx.update_editor(|editor, window, cx| {
10437 editor.change_selections(None, window, cx, |s| {
10438 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10439 })
10440 });
10441 cx.assert_editor_state(indoc! {"
10442 fn main() {
10443 sample(param1, «ˇparam2»);
10444 }
10445
10446 fn sample(param1: u8, param2: u8) {}
10447 "});
10448 cx.editor(|editor, _, _| {
10449 assert!(!editor.signature_help_state.is_shown());
10450 });
10451
10452 // When unselecting again, the popover is back if within the brackets.
10453 cx.update_editor(|editor, window, cx| {
10454 editor.change_selections(None, window, cx, |s| {
10455 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10456 })
10457 });
10458 cx.assert_editor_state(indoc! {"
10459 fn main() {
10460 sample(param1, ˇparam2);
10461 }
10462
10463 fn sample(param1: u8, param2: u8) {}
10464 "});
10465 handle_signature_help_request(&mut cx, mocked_response).await;
10466 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10467 .await;
10468 cx.editor(|editor, _, _| {
10469 assert!(editor.signature_help_state.is_shown());
10470 });
10471
10472 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
10473 cx.update_editor(|editor, window, cx| {
10474 editor.change_selections(None, window, cx, |s| {
10475 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
10476 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10477 })
10478 });
10479 cx.assert_editor_state(indoc! {"
10480 fn main() {
10481 sample(param1, ˇparam2);
10482 }
10483
10484 fn sample(param1: u8, param2: u8) {}
10485 "});
10486
10487 let mocked_response = lsp::SignatureHelp {
10488 signatures: vec![lsp::SignatureInformation {
10489 label: "fn sample(param1: u8, param2: u8)".to_string(),
10490 documentation: None,
10491 parameters: Some(vec![
10492 lsp::ParameterInformation {
10493 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10494 documentation: None,
10495 },
10496 lsp::ParameterInformation {
10497 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10498 documentation: None,
10499 },
10500 ]),
10501 active_parameter: None,
10502 }],
10503 active_signature: Some(0),
10504 active_parameter: Some(1),
10505 };
10506 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10507 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10508 .await;
10509 cx.update_editor(|editor, _, cx| {
10510 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
10511 });
10512 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10513 .await;
10514 cx.update_editor(|editor, window, cx| {
10515 editor.change_selections(None, window, cx, |s| {
10516 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10517 })
10518 });
10519 cx.assert_editor_state(indoc! {"
10520 fn main() {
10521 sample(param1, «ˇparam2»);
10522 }
10523
10524 fn sample(param1: u8, param2: u8) {}
10525 "});
10526 cx.update_editor(|editor, window, cx| {
10527 editor.change_selections(None, window, cx, |s| {
10528 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10529 })
10530 });
10531 cx.assert_editor_state(indoc! {"
10532 fn main() {
10533 sample(param1, ˇparam2);
10534 }
10535
10536 fn sample(param1: u8, param2: u8) {}
10537 "});
10538 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
10539 .await;
10540}
10541
10542#[gpui::test]
10543async fn test_completion_mode(cx: &mut TestAppContext) {
10544 init_test(cx, |_| {});
10545 let mut cx = EditorLspTestContext::new_rust(
10546 lsp::ServerCapabilities {
10547 completion_provider: Some(lsp::CompletionOptions {
10548 resolve_provider: Some(true),
10549 ..Default::default()
10550 }),
10551 ..Default::default()
10552 },
10553 cx,
10554 )
10555 .await;
10556
10557 struct Run {
10558 run_description: &'static str,
10559 initial_state: String,
10560 buffer_marked_text: String,
10561 completion_label: &'static str,
10562 completion_text: &'static str,
10563 expected_with_insert_mode: String,
10564 expected_with_replace_mode: String,
10565 expected_with_replace_subsequence_mode: String,
10566 expected_with_replace_suffix_mode: String,
10567 }
10568
10569 let runs = [
10570 Run {
10571 run_description: "Start of word matches completion text",
10572 initial_state: "before ediˇ after".into(),
10573 buffer_marked_text: "before <edi|> after".into(),
10574 completion_label: "editor",
10575 completion_text: "editor",
10576 expected_with_insert_mode: "before editorˇ after".into(),
10577 expected_with_replace_mode: "before editorˇ after".into(),
10578 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10579 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10580 },
10581 Run {
10582 run_description: "Accept same text at the middle of the word",
10583 initial_state: "before ediˇtor after".into(),
10584 buffer_marked_text: "before <edi|tor> after".into(),
10585 completion_label: "editor",
10586 completion_text: "editor",
10587 expected_with_insert_mode: "before editorˇtor after".into(),
10588 expected_with_replace_mode: "before editorˇ after".into(),
10589 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10590 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10591 },
10592 Run {
10593 run_description: "End of word matches completion text -- cursor at end",
10594 initial_state: "before torˇ after".into(),
10595 buffer_marked_text: "before <tor|> after".into(),
10596 completion_label: "editor",
10597 completion_text: "editor",
10598 expected_with_insert_mode: "before editorˇ after".into(),
10599 expected_with_replace_mode: "before editorˇ after".into(),
10600 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10601 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10602 },
10603 Run {
10604 run_description: "End of word matches completion text -- cursor at start",
10605 initial_state: "before ˇtor after".into(),
10606 buffer_marked_text: "before <|tor> after".into(),
10607 completion_label: "editor",
10608 completion_text: "editor",
10609 expected_with_insert_mode: "before editorˇtor after".into(),
10610 expected_with_replace_mode: "before editorˇ after".into(),
10611 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10612 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10613 },
10614 Run {
10615 run_description: "Prepend text containing whitespace",
10616 initial_state: "pˇfield: bool".into(),
10617 buffer_marked_text: "<p|field>: bool".into(),
10618 completion_label: "pub ",
10619 completion_text: "pub ",
10620 expected_with_insert_mode: "pub ˇfield: bool".into(),
10621 expected_with_replace_mode: "pub ˇ: bool".into(),
10622 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
10623 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
10624 },
10625 Run {
10626 run_description: "Add element to start of list",
10627 initial_state: "[element_ˇelement_2]".into(),
10628 buffer_marked_text: "[<element_|element_2>]".into(),
10629 completion_label: "element_1",
10630 completion_text: "element_1",
10631 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
10632 expected_with_replace_mode: "[element_1ˇ]".into(),
10633 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
10634 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
10635 },
10636 Run {
10637 run_description: "Add element to start of list -- first and second elements are equal",
10638 initial_state: "[elˇelement]".into(),
10639 buffer_marked_text: "[<el|element>]".into(),
10640 completion_label: "element",
10641 completion_text: "element",
10642 expected_with_insert_mode: "[elementˇelement]".into(),
10643 expected_with_replace_mode: "[elementˇ]".into(),
10644 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
10645 expected_with_replace_suffix_mode: "[elementˇ]".into(),
10646 },
10647 Run {
10648 run_description: "Ends with matching suffix",
10649 initial_state: "SubˇError".into(),
10650 buffer_marked_text: "<Sub|Error>".into(),
10651 completion_label: "SubscriptionError",
10652 completion_text: "SubscriptionError",
10653 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
10654 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10655 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10656 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
10657 },
10658 Run {
10659 run_description: "Suffix is a subsequence -- contiguous",
10660 initial_state: "SubˇErr".into(),
10661 buffer_marked_text: "<Sub|Err>".into(),
10662 completion_label: "SubscriptionError",
10663 completion_text: "SubscriptionError",
10664 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
10665 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10666 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10667 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
10668 },
10669 Run {
10670 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
10671 initial_state: "Suˇscrirr".into(),
10672 buffer_marked_text: "<Su|scrirr>".into(),
10673 completion_label: "SubscriptionError",
10674 completion_text: "SubscriptionError",
10675 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
10676 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10677 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10678 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
10679 },
10680 Run {
10681 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
10682 initial_state: "foo(indˇix)".into(),
10683 buffer_marked_text: "foo(<ind|ix>)".into(),
10684 completion_label: "node_index",
10685 completion_text: "node_index",
10686 expected_with_insert_mode: "foo(node_indexˇix)".into(),
10687 expected_with_replace_mode: "foo(node_indexˇ)".into(),
10688 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
10689 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
10690 },
10691 Run {
10692 run_description: "Replace range ends before cursor - should extend to cursor",
10693 initial_state: "before editˇo after".into(),
10694 buffer_marked_text: "before <{ed}>it|o after".into(),
10695 completion_label: "editor",
10696 completion_text: "editor",
10697 expected_with_insert_mode: "before editorˇo after".into(),
10698 expected_with_replace_mode: "before editorˇo after".into(),
10699 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
10700 expected_with_replace_suffix_mode: "before editorˇo after".into(),
10701 },
10702 Run {
10703 run_description: "Uses label for suffix matching",
10704 initial_state: "before ediˇtor after".into(),
10705 buffer_marked_text: "before <edi|tor> after".into(),
10706 completion_label: "editor",
10707 completion_text: "editor()",
10708 expected_with_insert_mode: "before editor()ˇtor after".into(),
10709 expected_with_replace_mode: "before editor()ˇ after".into(),
10710 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
10711 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
10712 },
10713 Run {
10714 run_description: "Case insensitive subsequence and suffix matching",
10715 initial_state: "before EDiˇtoR after".into(),
10716 buffer_marked_text: "before <EDi|toR> after".into(),
10717 completion_label: "editor",
10718 completion_text: "editor",
10719 expected_with_insert_mode: "before editorˇtoR after".into(),
10720 expected_with_replace_mode: "before editorˇ after".into(),
10721 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10722 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10723 },
10724 ];
10725
10726 for run in runs {
10727 let run_variations = [
10728 (LspInsertMode::Insert, run.expected_with_insert_mode),
10729 (LspInsertMode::Replace, run.expected_with_replace_mode),
10730 (
10731 LspInsertMode::ReplaceSubsequence,
10732 run.expected_with_replace_subsequence_mode,
10733 ),
10734 (
10735 LspInsertMode::ReplaceSuffix,
10736 run.expected_with_replace_suffix_mode,
10737 ),
10738 ];
10739
10740 for (lsp_insert_mode, expected_text) in run_variations {
10741 eprintln!(
10742 "run = {:?}, mode = {lsp_insert_mode:.?}",
10743 run.run_description,
10744 );
10745
10746 update_test_language_settings(&mut cx, |settings| {
10747 settings.defaults.completions = Some(CompletionSettings {
10748 lsp_insert_mode,
10749 words: WordsCompletionMode::Disabled,
10750 lsp: true,
10751 lsp_fetch_timeout_ms: 0,
10752 });
10753 });
10754
10755 cx.set_state(&run.initial_state);
10756 cx.update_editor(|editor, window, cx| {
10757 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10758 });
10759
10760 let counter = Arc::new(AtomicUsize::new(0));
10761 handle_completion_request_with_insert_and_replace(
10762 &mut cx,
10763 &run.buffer_marked_text,
10764 vec![(run.completion_label, run.completion_text)],
10765 counter.clone(),
10766 )
10767 .await;
10768 cx.condition(|editor, _| editor.context_menu_visible())
10769 .await;
10770 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10771
10772 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10773 editor
10774 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10775 .unwrap()
10776 });
10777 cx.assert_editor_state(&expected_text);
10778 handle_resolve_completion_request(&mut cx, None).await;
10779 apply_additional_edits.await.unwrap();
10780 }
10781 }
10782}
10783
10784#[gpui::test]
10785async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
10786 init_test(cx, |_| {});
10787 let mut cx = EditorLspTestContext::new_rust(
10788 lsp::ServerCapabilities {
10789 completion_provider: Some(lsp::CompletionOptions {
10790 resolve_provider: Some(true),
10791 ..Default::default()
10792 }),
10793 ..Default::default()
10794 },
10795 cx,
10796 )
10797 .await;
10798
10799 let initial_state = "SubˇError";
10800 let buffer_marked_text = "<Sub|Error>";
10801 let completion_text = "SubscriptionError";
10802 let expected_with_insert_mode = "SubscriptionErrorˇError";
10803 let expected_with_replace_mode = "SubscriptionErrorˇ";
10804
10805 update_test_language_settings(&mut cx, |settings| {
10806 settings.defaults.completions = Some(CompletionSettings {
10807 words: WordsCompletionMode::Disabled,
10808 // set the opposite here to ensure that the action is overriding the default behavior
10809 lsp_insert_mode: LspInsertMode::Insert,
10810 lsp: true,
10811 lsp_fetch_timeout_ms: 0,
10812 });
10813 });
10814
10815 cx.set_state(initial_state);
10816 cx.update_editor(|editor, window, cx| {
10817 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10818 });
10819
10820 let counter = Arc::new(AtomicUsize::new(0));
10821 handle_completion_request_with_insert_and_replace(
10822 &mut cx,
10823 &buffer_marked_text,
10824 vec![(completion_text, completion_text)],
10825 counter.clone(),
10826 )
10827 .await;
10828 cx.condition(|editor, _| editor.context_menu_visible())
10829 .await;
10830 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10831
10832 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10833 editor
10834 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10835 .unwrap()
10836 });
10837 cx.assert_editor_state(&expected_with_replace_mode);
10838 handle_resolve_completion_request(&mut cx, None).await;
10839 apply_additional_edits.await.unwrap();
10840
10841 update_test_language_settings(&mut cx, |settings| {
10842 settings.defaults.completions = Some(CompletionSettings {
10843 words: WordsCompletionMode::Disabled,
10844 // set the opposite here to ensure that the action is overriding the default behavior
10845 lsp_insert_mode: LspInsertMode::Replace,
10846 lsp: true,
10847 lsp_fetch_timeout_ms: 0,
10848 });
10849 });
10850
10851 cx.set_state(initial_state);
10852 cx.update_editor(|editor, window, cx| {
10853 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10854 });
10855 handle_completion_request_with_insert_and_replace(
10856 &mut cx,
10857 &buffer_marked_text,
10858 vec![(completion_text, completion_text)],
10859 counter.clone(),
10860 )
10861 .await;
10862 cx.condition(|editor, _| editor.context_menu_visible())
10863 .await;
10864 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
10865
10866 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10867 editor
10868 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
10869 .unwrap()
10870 });
10871 cx.assert_editor_state(&expected_with_insert_mode);
10872 handle_resolve_completion_request(&mut cx, None).await;
10873 apply_additional_edits.await.unwrap();
10874}
10875
10876#[gpui::test]
10877async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
10878 init_test(cx, |_| {});
10879 let mut cx = EditorLspTestContext::new_rust(
10880 lsp::ServerCapabilities {
10881 completion_provider: Some(lsp::CompletionOptions {
10882 resolve_provider: Some(true),
10883 ..Default::default()
10884 }),
10885 ..Default::default()
10886 },
10887 cx,
10888 )
10889 .await;
10890
10891 // scenario: surrounding text matches completion text
10892 let completion_text = "to_offset";
10893 let initial_state = indoc! {"
10894 1. buf.to_offˇsuffix
10895 2. buf.to_offˇsuf
10896 3. buf.to_offˇfix
10897 4. buf.to_offˇ
10898 5. into_offˇensive
10899 6. ˇsuffix
10900 7. let ˇ //
10901 8. aaˇzz
10902 9. buf.to_off«zzzzzˇ»suffix
10903 10. buf.«ˇzzzzz»suffix
10904 11. to_off«ˇzzzzz»
10905
10906 buf.to_offˇsuffix // newest cursor
10907 "};
10908 let completion_marked_buffer = indoc! {"
10909 1. buf.to_offsuffix
10910 2. buf.to_offsuf
10911 3. buf.to_offfix
10912 4. buf.to_off
10913 5. into_offensive
10914 6. suffix
10915 7. let //
10916 8. aazz
10917 9. buf.to_offzzzzzsuffix
10918 10. buf.zzzzzsuffix
10919 11. to_offzzzzz
10920
10921 buf.<to_off|suffix> // newest cursor
10922 "};
10923 let expected = indoc! {"
10924 1. buf.to_offsetˇ
10925 2. buf.to_offsetˇsuf
10926 3. buf.to_offsetˇfix
10927 4. buf.to_offsetˇ
10928 5. into_offsetˇensive
10929 6. to_offsetˇsuffix
10930 7. let to_offsetˇ //
10931 8. aato_offsetˇzz
10932 9. buf.to_offsetˇ
10933 10. buf.to_offsetˇsuffix
10934 11. to_offsetˇ
10935
10936 buf.to_offsetˇ // newest cursor
10937 "};
10938 cx.set_state(initial_state);
10939 cx.update_editor(|editor, window, cx| {
10940 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10941 });
10942 handle_completion_request_with_insert_and_replace(
10943 &mut cx,
10944 completion_marked_buffer,
10945 vec![(completion_text, completion_text)],
10946 Arc::new(AtomicUsize::new(0)),
10947 )
10948 .await;
10949 cx.condition(|editor, _| editor.context_menu_visible())
10950 .await;
10951 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10952 editor
10953 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10954 .unwrap()
10955 });
10956 cx.assert_editor_state(expected);
10957 handle_resolve_completion_request(&mut cx, None).await;
10958 apply_additional_edits.await.unwrap();
10959
10960 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
10961 let completion_text = "foo_and_bar";
10962 let initial_state = indoc! {"
10963 1. ooanbˇ
10964 2. zooanbˇ
10965 3. ooanbˇz
10966 4. zooanbˇz
10967 5. ooanˇ
10968 6. oanbˇ
10969
10970 ooanbˇ
10971 "};
10972 let completion_marked_buffer = indoc! {"
10973 1. ooanb
10974 2. zooanb
10975 3. ooanbz
10976 4. zooanbz
10977 5. ooan
10978 6. oanb
10979
10980 <ooanb|>
10981 "};
10982 let expected = indoc! {"
10983 1. foo_and_barˇ
10984 2. zfoo_and_barˇ
10985 3. foo_and_barˇz
10986 4. zfoo_and_barˇz
10987 5. ooanfoo_and_barˇ
10988 6. oanbfoo_and_barˇ
10989
10990 foo_and_barˇ
10991 "};
10992 cx.set_state(initial_state);
10993 cx.update_editor(|editor, window, cx| {
10994 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10995 });
10996 handle_completion_request_with_insert_and_replace(
10997 &mut cx,
10998 completion_marked_buffer,
10999 vec![(completion_text, completion_text)],
11000 Arc::new(AtomicUsize::new(0)),
11001 )
11002 .await;
11003 cx.condition(|editor, _| editor.context_menu_visible())
11004 .await;
11005 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11006 editor
11007 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11008 .unwrap()
11009 });
11010 cx.assert_editor_state(expected);
11011 handle_resolve_completion_request(&mut cx, None).await;
11012 apply_additional_edits.await.unwrap();
11013
11014 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
11015 // (expects the same as if it was inserted at the end)
11016 let completion_text = "foo_and_bar";
11017 let initial_state = indoc! {"
11018 1. ooˇanb
11019 2. zooˇanb
11020 3. ooˇanbz
11021 4. zooˇanbz
11022
11023 ooˇanb
11024 "};
11025 let completion_marked_buffer = indoc! {"
11026 1. ooanb
11027 2. zooanb
11028 3. ooanbz
11029 4. zooanbz
11030
11031 <oo|anb>
11032 "};
11033 let expected = indoc! {"
11034 1. foo_and_barˇ
11035 2. zfoo_and_barˇ
11036 3. foo_and_barˇz
11037 4. zfoo_and_barˇz
11038
11039 foo_and_barˇ
11040 "};
11041 cx.set_state(initial_state);
11042 cx.update_editor(|editor, window, cx| {
11043 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11044 });
11045 handle_completion_request_with_insert_and_replace(
11046 &mut cx,
11047 completion_marked_buffer,
11048 vec![(completion_text, completion_text)],
11049 Arc::new(AtomicUsize::new(0)),
11050 )
11051 .await;
11052 cx.condition(|editor, _| editor.context_menu_visible())
11053 .await;
11054 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11055 editor
11056 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11057 .unwrap()
11058 });
11059 cx.assert_editor_state(expected);
11060 handle_resolve_completion_request(&mut cx, None).await;
11061 apply_additional_edits.await.unwrap();
11062}
11063
11064// This used to crash
11065#[gpui::test]
11066async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
11067 init_test(cx, |_| {});
11068
11069 let buffer_text = indoc! {"
11070 fn main() {
11071 10.satu;
11072
11073 //
11074 // separate cursors so they open in different excerpts (manually reproducible)
11075 //
11076
11077 10.satu20;
11078 }
11079 "};
11080 let multibuffer_text_with_selections = indoc! {"
11081 fn main() {
11082 10.satuˇ;
11083
11084 //
11085
11086 //
11087
11088 10.satuˇ20;
11089 }
11090 "};
11091 let expected_multibuffer = indoc! {"
11092 fn main() {
11093 10.saturating_sub()ˇ;
11094
11095 //
11096
11097 //
11098
11099 10.saturating_sub()ˇ;
11100 }
11101 "};
11102
11103 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
11104 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
11105
11106 let fs = FakeFs::new(cx.executor());
11107 fs.insert_tree(
11108 path!("/a"),
11109 json!({
11110 "main.rs": buffer_text,
11111 }),
11112 )
11113 .await;
11114
11115 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11116 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11117 language_registry.add(rust_lang());
11118 let mut fake_servers = language_registry.register_fake_lsp(
11119 "Rust",
11120 FakeLspAdapter {
11121 capabilities: lsp::ServerCapabilities {
11122 completion_provider: Some(lsp::CompletionOptions {
11123 resolve_provider: None,
11124 ..lsp::CompletionOptions::default()
11125 }),
11126 ..lsp::ServerCapabilities::default()
11127 },
11128 ..FakeLspAdapter::default()
11129 },
11130 );
11131 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11132 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11133 let buffer = project
11134 .update(cx, |project, cx| {
11135 project.open_local_buffer(path!("/a/main.rs"), cx)
11136 })
11137 .await
11138 .unwrap();
11139
11140 let multi_buffer = cx.new(|cx| {
11141 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
11142 multi_buffer.push_excerpts(
11143 buffer.clone(),
11144 [ExcerptRange::new(0..first_excerpt_end)],
11145 cx,
11146 );
11147 multi_buffer.push_excerpts(
11148 buffer.clone(),
11149 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
11150 cx,
11151 );
11152 multi_buffer
11153 });
11154
11155 let editor = workspace
11156 .update(cx, |_, window, cx| {
11157 cx.new(|cx| {
11158 Editor::new(
11159 EditorMode::Full {
11160 scale_ui_elements_with_buffer_font_size: false,
11161 show_active_line_background: false,
11162 sized_by_content: false,
11163 },
11164 multi_buffer.clone(),
11165 Some(project.clone()),
11166 window,
11167 cx,
11168 )
11169 })
11170 })
11171 .unwrap();
11172
11173 let pane = workspace
11174 .update(cx, |workspace, _, _| workspace.active_pane().clone())
11175 .unwrap();
11176 pane.update_in(cx, |pane, window, cx| {
11177 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
11178 });
11179
11180 let fake_server = fake_servers.next().await.unwrap();
11181
11182 editor.update_in(cx, |editor, window, cx| {
11183 editor.change_selections(None, window, cx, |s| {
11184 s.select_ranges([
11185 Point::new(1, 11)..Point::new(1, 11),
11186 Point::new(7, 11)..Point::new(7, 11),
11187 ])
11188 });
11189
11190 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
11191 });
11192
11193 editor.update_in(cx, |editor, window, cx| {
11194 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11195 });
11196
11197 fake_server
11198 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11199 let completion_item = lsp::CompletionItem {
11200 label: "saturating_sub()".into(),
11201 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
11202 lsp::InsertReplaceEdit {
11203 new_text: "saturating_sub()".to_owned(),
11204 insert: lsp::Range::new(
11205 lsp::Position::new(7, 7),
11206 lsp::Position::new(7, 11),
11207 ),
11208 replace: lsp::Range::new(
11209 lsp::Position::new(7, 7),
11210 lsp::Position::new(7, 13),
11211 ),
11212 },
11213 )),
11214 ..lsp::CompletionItem::default()
11215 };
11216
11217 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
11218 })
11219 .next()
11220 .await
11221 .unwrap();
11222
11223 cx.condition(&editor, |editor, _| editor.context_menu_visible())
11224 .await;
11225
11226 editor
11227 .update_in(cx, |editor, window, cx| {
11228 editor
11229 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11230 .unwrap()
11231 })
11232 .await
11233 .unwrap();
11234
11235 editor.update(cx, |editor, cx| {
11236 assert_text_with_selections(editor, expected_multibuffer, cx);
11237 })
11238}
11239
11240#[gpui::test]
11241async fn test_completion(cx: &mut TestAppContext) {
11242 init_test(cx, |_| {});
11243
11244 let mut cx = EditorLspTestContext::new_rust(
11245 lsp::ServerCapabilities {
11246 completion_provider: Some(lsp::CompletionOptions {
11247 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11248 resolve_provider: Some(true),
11249 ..Default::default()
11250 }),
11251 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11252 ..Default::default()
11253 },
11254 cx,
11255 )
11256 .await;
11257 let counter = Arc::new(AtomicUsize::new(0));
11258
11259 cx.set_state(indoc! {"
11260 oneˇ
11261 two
11262 three
11263 "});
11264 cx.simulate_keystroke(".");
11265 handle_completion_request(
11266 indoc! {"
11267 one.|<>
11268 two
11269 three
11270 "},
11271 vec!["first_completion", "second_completion"],
11272 true,
11273 counter.clone(),
11274 &mut cx,
11275 )
11276 .await;
11277 cx.condition(|editor, _| editor.context_menu_visible())
11278 .await;
11279 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11280
11281 let _handler = handle_signature_help_request(
11282 &mut cx,
11283 lsp::SignatureHelp {
11284 signatures: vec![lsp::SignatureInformation {
11285 label: "test signature".to_string(),
11286 documentation: None,
11287 parameters: Some(vec![lsp::ParameterInformation {
11288 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
11289 documentation: None,
11290 }]),
11291 active_parameter: None,
11292 }],
11293 active_signature: None,
11294 active_parameter: None,
11295 },
11296 );
11297 cx.update_editor(|editor, window, cx| {
11298 assert!(
11299 !editor.signature_help_state.is_shown(),
11300 "No signature help was called for"
11301 );
11302 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11303 });
11304 cx.run_until_parked();
11305 cx.update_editor(|editor, _, _| {
11306 assert!(
11307 !editor.signature_help_state.is_shown(),
11308 "No signature help should be shown when completions menu is open"
11309 );
11310 });
11311
11312 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11313 editor.context_menu_next(&Default::default(), window, cx);
11314 editor
11315 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11316 .unwrap()
11317 });
11318 cx.assert_editor_state(indoc! {"
11319 one.second_completionˇ
11320 two
11321 three
11322 "});
11323
11324 handle_resolve_completion_request(
11325 &mut cx,
11326 Some(vec![
11327 (
11328 //This overlaps with the primary completion edit which is
11329 //misbehavior from the LSP spec, test that we filter it out
11330 indoc! {"
11331 one.second_ˇcompletion
11332 two
11333 threeˇ
11334 "},
11335 "overlapping additional edit",
11336 ),
11337 (
11338 indoc! {"
11339 one.second_completion
11340 two
11341 threeˇ
11342 "},
11343 "\nadditional edit",
11344 ),
11345 ]),
11346 )
11347 .await;
11348 apply_additional_edits.await.unwrap();
11349 cx.assert_editor_state(indoc! {"
11350 one.second_completionˇ
11351 two
11352 three
11353 additional edit
11354 "});
11355
11356 cx.set_state(indoc! {"
11357 one.second_completion
11358 twoˇ
11359 threeˇ
11360 additional edit
11361 "});
11362 cx.simulate_keystroke(" ");
11363 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11364 cx.simulate_keystroke("s");
11365 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11366
11367 cx.assert_editor_state(indoc! {"
11368 one.second_completion
11369 two sˇ
11370 three sˇ
11371 additional edit
11372 "});
11373 handle_completion_request(
11374 indoc! {"
11375 one.second_completion
11376 two s
11377 three <s|>
11378 additional edit
11379 "},
11380 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11381 true,
11382 counter.clone(),
11383 &mut cx,
11384 )
11385 .await;
11386 cx.condition(|editor, _| editor.context_menu_visible())
11387 .await;
11388 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11389
11390 cx.simulate_keystroke("i");
11391
11392 handle_completion_request(
11393 indoc! {"
11394 one.second_completion
11395 two si
11396 three <si|>
11397 additional edit
11398 "},
11399 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11400 true,
11401 counter.clone(),
11402 &mut cx,
11403 )
11404 .await;
11405 cx.condition(|editor, _| editor.context_menu_visible())
11406 .await;
11407 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
11408
11409 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11410 editor
11411 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11412 .unwrap()
11413 });
11414 cx.assert_editor_state(indoc! {"
11415 one.second_completion
11416 two sixth_completionˇ
11417 three sixth_completionˇ
11418 additional edit
11419 "});
11420
11421 apply_additional_edits.await.unwrap();
11422
11423 update_test_language_settings(&mut cx, |settings| {
11424 settings.defaults.show_completions_on_input = Some(false);
11425 });
11426 cx.set_state("editorˇ");
11427 cx.simulate_keystroke(".");
11428 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11429 cx.simulate_keystrokes("c l o");
11430 cx.assert_editor_state("editor.cloˇ");
11431 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11432 cx.update_editor(|editor, window, cx| {
11433 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11434 });
11435 handle_completion_request(
11436 "editor.<clo|>",
11437 vec!["close", "clobber"],
11438 true,
11439 counter.clone(),
11440 &mut cx,
11441 )
11442 .await;
11443 cx.condition(|editor, _| editor.context_menu_visible())
11444 .await;
11445 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
11446
11447 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11448 editor
11449 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11450 .unwrap()
11451 });
11452 cx.assert_editor_state("editor.closeˇ");
11453 handle_resolve_completion_request(&mut cx, None).await;
11454 apply_additional_edits.await.unwrap();
11455}
11456
11457#[gpui::test]
11458async fn test_completion_reuse(cx: &mut TestAppContext) {
11459 init_test(cx, |_| {});
11460
11461 let mut cx = EditorLspTestContext::new_rust(
11462 lsp::ServerCapabilities {
11463 completion_provider: Some(lsp::CompletionOptions {
11464 trigger_characters: Some(vec![".".to_string()]),
11465 ..Default::default()
11466 }),
11467 ..Default::default()
11468 },
11469 cx,
11470 )
11471 .await;
11472
11473 let counter = Arc::new(AtomicUsize::new(0));
11474 cx.set_state("objˇ");
11475 cx.simulate_keystroke(".");
11476
11477 // Initial completion request returns complete results
11478 let is_incomplete = false;
11479 handle_completion_request(
11480 "obj.|<>",
11481 vec!["a", "ab", "abc"],
11482 is_incomplete,
11483 counter.clone(),
11484 &mut cx,
11485 )
11486 .await;
11487 cx.run_until_parked();
11488 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11489 cx.assert_editor_state("obj.ˇ");
11490 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
11491
11492 // Type "a" - filters existing completions
11493 cx.simulate_keystroke("a");
11494 cx.run_until_parked();
11495 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11496 cx.assert_editor_state("obj.aˇ");
11497 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
11498
11499 // Type "b" - filters existing completions
11500 cx.simulate_keystroke("b");
11501 cx.run_until_parked();
11502 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11503 cx.assert_editor_state("obj.abˇ");
11504 check_displayed_completions(vec!["ab", "abc"], &mut cx);
11505
11506 // Type "c" - filters existing completions
11507 cx.simulate_keystroke("c");
11508 cx.run_until_parked();
11509 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11510 cx.assert_editor_state("obj.abcˇ");
11511 check_displayed_completions(vec!["abc"], &mut cx);
11512
11513 // Backspace to delete "c" - filters existing completions
11514 cx.update_editor(|editor, window, cx| {
11515 editor.backspace(&Backspace, window, cx);
11516 });
11517 cx.run_until_parked();
11518 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11519 cx.assert_editor_state("obj.abˇ");
11520 check_displayed_completions(vec!["ab", "abc"], &mut cx);
11521
11522 // Moving cursor to the left dismisses menu.
11523 cx.update_editor(|editor, window, cx| {
11524 editor.move_left(&MoveLeft, window, cx);
11525 });
11526 cx.run_until_parked();
11527 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11528 cx.assert_editor_state("obj.aˇb");
11529 cx.update_editor(|editor, _, _| {
11530 assert_eq!(editor.context_menu_visible(), false);
11531 });
11532
11533 // Type "b" - new request
11534 cx.simulate_keystroke("b");
11535 let is_incomplete = false;
11536 handle_completion_request(
11537 "obj.<ab|>a",
11538 vec!["ab", "abc"],
11539 is_incomplete,
11540 counter.clone(),
11541 &mut cx,
11542 )
11543 .await;
11544 cx.run_until_parked();
11545 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11546 cx.assert_editor_state("obj.abˇb");
11547 check_displayed_completions(vec!["ab", "abc"], &mut cx);
11548
11549 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
11550 cx.update_editor(|editor, window, cx| {
11551 editor.backspace(&Backspace, window, cx);
11552 });
11553 let is_incomplete = false;
11554 handle_completion_request(
11555 "obj.<a|>b",
11556 vec!["a", "ab", "abc"],
11557 is_incomplete,
11558 counter.clone(),
11559 &mut cx,
11560 )
11561 .await;
11562 cx.run_until_parked();
11563 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
11564 cx.assert_editor_state("obj.aˇb");
11565 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
11566
11567 // Backspace to delete "a" - dismisses menu.
11568 cx.update_editor(|editor, window, cx| {
11569 editor.backspace(&Backspace, window, cx);
11570 });
11571 cx.run_until_parked();
11572 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
11573 cx.assert_editor_state("obj.ˇb");
11574 cx.update_editor(|editor, _, _| {
11575 assert_eq!(editor.context_menu_visible(), false);
11576 });
11577}
11578
11579#[gpui::test]
11580async fn test_word_completion(cx: &mut TestAppContext) {
11581 let lsp_fetch_timeout_ms = 10;
11582 init_test(cx, |language_settings| {
11583 language_settings.defaults.completions = Some(CompletionSettings {
11584 words: WordsCompletionMode::Fallback,
11585 lsp: true,
11586 lsp_fetch_timeout_ms: 10,
11587 lsp_insert_mode: LspInsertMode::Insert,
11588 });
11589 });
11590
11591 let mut cx = EditorLspTestContext::new_rust(
11592 lsp::ServerCapabilities {
11593 completion_provider: Some(lsp::CompletionOptions {
11594 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11595 ..lsp::CompletionOptions::default()
11596 }),
11597 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11598 ..lsp::ServerCapabilities::default()
11599 },
11600 cx,
11601 )
11602 .await;
11603
11604 let throttle_completions = Arc::new(AtomicBool::new(false));
11605
11606 let lsp_throttle_completions = throttle_completions.clone();
11607 let _completion_requests_handler =
11608 cx.lsp
11609 .server
11610 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
11611 let lsp_throttle_completions = lsp_throttle_completions.clone();
11612 let cx = cx.clone();
11613 async move {
11614 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
11615 cx.background_executor()
11616 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
11617 .await;
11618 }
11619 Ok(Some(lsp::CompletionResponse::Array(vec![
11620 lsp::CompletionItem {
11621 label: "first".into(),
11622 ..lsp::CompletionItem::default()
11623 },
11624 lsp::CompletionItem {
11625 label: "last".into(),
11626 ..lsp::CompletionItem::default()
11627 },
11628 ])))
11629 }
11630 });
11631
11632 cx.set_state(indoc! {"
11633 oneˇ
11634 two
11635 three
11636 "});
11637 cx.simulate_keystroke(".");
11638 cx.executor().run_until_parked();
11639 cx.condition(|editor, _| editor.context_menu_visible())
11640 .await;
11641 cx.update_editor(|editor, window, cx| {
11642 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11643 {
11644 assert_eq!(
11645 completion_menu_entries(&menu),
11646 &["first", "last"],
11647 "When LSP server is fast to reply, no fallback word completions are used"
11648 );
11649 } else {
11650 panic!("expected completion menu to be open");
11651 }
11652 editor.cancel(&Cancel, window, cx);
11653 });
11654 cx.executor().run_until_parked();
11655 cx.condition(|editor, _| !editor.context_menu_visible())
11656 .await;
11657
11658 throttle_completions.store(true, atomic::Ordering::Release);
11659 cx.simulate_keystroke(".");
11660 cx.executor()
11661 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
11662 cx.executor().run_until_parked();
11663 cx.condition(|editor, _| editor.context_menu_visible())
11664 .await;
11665 cx.update_editor(|editor, _, _| {
11666 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11667 {
11668 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
11669 "When LSP server is slow, document words can be shown instead, if configured accordingly");
11670 } else {
11671 panic!("expected completion menu to be open");
11672 }
11673 });
11674}
11675
11676#[gpui::test]
11677async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
11678 init_test(cx, |language_settings| {
11679 language_settings.defaults.completions = Some(CompletionSettings {
11680 words: WordsCompletionMode::Enabled,
11681 lsp: true,
11682 lsp_fetch_timeout_ms: 0,
11683 lsp_insert_mode: LspInsertMode::Insert,
11684 });
11685 });
11686
11687 let mut cx = EditorLspTestContext::new_rust(
11688 lsp::ServerCapabilities {
11689 completion_provider: Some(lsp::CompletionOptions {
11690 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11691 ..lsp::CompletionOptions::default()
11692 }),
11693 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11694 ..lsp::ServerCapabilities::default()
11695 },
11696 cx,
11697 )
11698 .await;
11699
11700 let _completion_requests_handler =
11701 cx.lsp
11702 .server
11703 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11704 Ok(Some(lsp::CompletionResponse::Array(vec![
11705 lsp::CompletionItem {
11706 label: "first".into(),
11707 ..lsp::CompletionItem::default()
11708 },
11709 lsp::CompletionItem {
11710 label: "last".into(),
11711 ..lsp::CompletionItem::default()
11712 },
11713 ])))
11714 });
11715
11716 cx.set_state(indoc! {"ˇ
11717 first
11718 last
11719 second
11720 "});
11721 cx.simulate_keystroke(".");
11722 cx.executor().run_until_parked();
11723 cx.condition(|editor, _| editor.context_menu_visible())
11724 .await;
11725 cx.update_editor(|editor, _, _| {
11726 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11727 {
11728 assert_eq!(
11729 completion_menu_entries(&menu),
11730 &["first", "last", "second"],
11731 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
11732 );
11733 } else {
11734 panic!("expected completion menu to be open");
11735 }
11736 });
11737}
11738
11739#[gpui::test]
11740async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
11741 init_test(cx, |language_settings| {
11742 language_settings.defaults.completions = Some(CompletionSettings {
11743 words: WordsCompletionMode::Disabled,
11744 lsp: true,
11745 lsp_fetch_timeout_ms: 0,
11746 lsp_insert_mode: LspInsertMode::Insert,
11747 });
11748 });
11749
11750 let mut cx = EditorLspTestContext::new_rust(
11751 lsp::ServerCapabilities {
11752 completion_provider: Some(lsp::CompletionOptions {
11753 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11754 ..lsp::CompletionOptions::default()
11755 }),
11756 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11757 ..lsp::ServerCapabilities::default()
11758 },
11759 cx,
11760 )
11761 .await;
11762
11763 let _completion_requests_handler =
11764 cx.lsp
11765 .server
11766 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11767 panic!("LSP completions should not be queried when dealing with word completions")
11768 });
11769
11770 cx.set_state(indoc! {"ˇ
11771 first
11772 last
11773 second
11774 "});
11775 cx.update_editor(|editor, window, cx| {
11776 editor.show_word_completions(&ShowWordCompletions, window, cx);
11777 });
11778 cx.executor().run_until_parked();
11779 cx.condition(|editor, _| editor.context_menu_visible())
11780 .await;
11781 cx.update_editor(|editor, _, _| {
11782 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11783 {
11784 assert_eq!(
11785 completion_menu_entries(&menu),
11786 &["first", "last", "second"],
11787 "`ShowWordCompletions` action should show word completions"
11788 );
11789 } else {
11790 panic!("expected completion menu to be open");
11791 }
11792 });
11793
11794 cx.simulate_keystroke("l");
11795 cx.executor().run_until_parked();
11796 cx.condition(|editor, _| editor.context_menu_visible())
11797 .await;
11798 cx.update_editor(|editor, _, _| {
11799 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11800 {
11801 assert_eq!(
11802 completion_menu_entries(&menu),
11803 &["last"],
11804 "After showing word completions, further editing should filter them and not query the LSP"
11805 );
11806 } else {
11807 panic!("expected completion menu to be open");
11808 }
11809 });
11810}
11811
11812#[gpui::test]
11813async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
11814 init_test(cx, |language_settings| {
11815 language_settings.defaults.completions = Some(CompletionSettings {
11816 words: WordsCompletionMode::Fallback,
11817 lsp: false,
11818 lsp_fetch_timeout_ms: 0,
11819 lsp_insert_mode: LspInsertMode::Insert,
11820 });
11821 });
11822
11823 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
11824
11825 cx.set_state(indoc! {"ˇ
11826 0_usize
11827 let
11828 33
11829 4.5f32
11830 "});
11831 cx.update_editor(|editor, window, cx| {
11832 editor.show_completions(&ShowCompletions::default(), window, cx);
11833 });
11834 cx.executor().run_until_parked();
11835 cx.condition(|editor, _| editor.context_menu_visible())
11836 .await;
11837 cx.update_editor(|editor, window, cx| {
11838 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11839 {
11840 assert_eq!(
11841 completion_menu_entries(&menu),
11842 &["let"],
11843 "With no digits in the completion query, no digits should be in the word completions"
11844 );
11845 } else {
11846 panic!("expected completion menu to be open");
11847 }
11848 editor.cancel(&Cancel, window, cx);
11849 });
11850
11851 cx.set_state(indoc! {"3ˇ
11852 0_usize
11853 let
11854 3
11855 33.35f32
11856 "});
11857 cx.update_editor(|editor, window, cx| {
11858 editor.show_completions(&ShowCompletions::default(), window, cx);
11859 });
11860 cx.executor().run_until_parked();
11861 cx.condition(|editor, _| editor.context_menu_visible())
11862 .await;
11863 cx.update_editor(|editor, _, _| {
11864 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11865 {
11866 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
11867 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
11868 } else {
11869 panic!("expected completion menu to be open");
11870 }
11871 });
11872}
11873
11874fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
11875 let position = || lsp::Position {
11876 line: params.text_document_position.position.line,
11877 character: params.text_document_position.position.character,
11878 };
11879 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11880 range: lsp::Range {
11881 start: position(),
11882 end: position(),
11883 },
11884 new_text: text.to_string(),
11885 }))
11886}
11887
11888#[gpui::test]
11889async fn test_multiline_completion(cx: &mut TestAppContext) {
11890 init_test(cx, |_| {});
11891
11892 let fs = FakeFs::new(cx.executor());
11893 fs.insert_tree(
11894 path!("/a"),
11895 json!({
11896 "main.ts": "a",
11897 }),
11898 )
11899 .await;
11900
11901 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11902 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11903 let typescript_language = Arc::new(Language::new(
11904 LanguageConfig {
11905 name: "TypeScript".into(),
11906 matcher: LanguageMatcher {
11907 path_suffixes: vec!["ts".to_string()],
11908 ..LanguageMatcher::default()
11909 },
11910 line_comments: vec!["// ".into()],
11911 ..LanguageConfig::default()
11912 },
11913 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
11914 ));
11915 language_registry.add(typescript_language.clone());
11916 let mut fake_servers = language_registry.register_fake_lsp(
11917 "TypeScript",
11918 FakeLspAdapter {
11919 capabilities: lsp::ServerCapabilities {
11920 completion_provider: Some(lsp::CompletionOptions {
11921 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11922 ..lsp::CompletionOptions::default()
11923 }),
11924 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11925 ..lsp::ServerCapabilities::default()
11926 },
11927 // Emulate vtsls label generation
11928 label_for_completion: Some(Box::new(|item, _| {
11929 let text = if let Some(description) = item
11930 .label_details
11931 .as_ref()
11932 .and_then(|label_details| label_details.description.as_ref())
11933 {
11934 format!("{} {}", item.label, description)
11935 } else if let Some(detail) = &item.detail {
11936 format!("{} {}", item.label, detail)
11937 } else {
11938 item.label.clone()
11939 };
11940 let len = text.len();
11941 Some(language::CodeLabel {
11942 text,
11943 runs: Vec::new(),
11944 filter_range: 0..len,
11945 })
11946 })),
11947 ..FakeLspAdapter::default()
11948 },
11949 );
11950 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11951 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11952 let worktree_id = workspace
11953 .update(cx, |workspace, _window, cx| {
11954 workspace.project().update(cx, |project, cx| {
11955 project.worktrees(cx).next().unwrap().read(cx).id()
11956 })
11957 })
11958 .unwrap();
11959 let _buffer = project
11960 .update(cx, |project, cx| {
11961 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
11962 })
11963 .await
11964 .unwrap();
11965 let editor = workspace
11966 .update(cx, |workspace, window, cx| {
11967 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
11968 })
11969 .unwrap()
11970 .await
11971 .unwrap()
11972 .downcast::<Editor>()
11973 .unwrap();
11974 let fake_server = fake_servers.next().await.unwrap();
11975
11976 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
11977 let multiline_label_2 = "a\nb\nc\n";
11978 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
11979 let multiline_description = "d\ne\nf\n";
11980 let multiline_detail_2 = "g\nh\ni\n";
11981
11982 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
11983 move |params, _| async move {
11984 Ok(Some(lsp::CompletionResponse::Array(vec![
11985 lsp::CompletionItem {
11986 label: multiline_label.to_string(),
11987 text_edit: gen_text_edit(¶ms, "new_text_1"),
11988 ..lsp::CompletionItem::default()
11989 },
11990 lsp::CompletionItem {
11991 label: "single line label 1".to_string(),
11992 detail: Some(multiline_detail.to_string()),
11993 text_edit: gen_text_edit(¶ms, "new_text_2"),
11994 ..lsp::CompletionItem::default()
11995 },
11996 lsp::CompletionItem {
11997 label: "single line label 2".to_string(),
11998 label_details: Some(lsp::CompletionItemLabelDetails {
11999 description: Some(multiline_description.to_string()),
12000 detail: None,
12001 }),
12002 text_edit: gen_text_edit(¶ms, "new_text_2"),
12003 ..lsp::CompletionItem::default()
12004 },
12005 lsp::CompletionItem {
12006 label: multiline_label_2.to_string(),
12007 detail: Some(multiline_detail_2.to_string()),
12008 text_edit: gen_text_edit(¶ms, "new_text_3"),
12009 ..lsp::CompletionItem::default()
12010 },
12011 lsp::CompletionItem {
12012 label: "Label with many spaces and \t but without newlines".to_string(),
12013 detail: Some(
12014 "Details with many spaces and \t but without newlines".to_string(),
12015 ),
12016 text_edit: gen_text_edit(¶ms, "new_text_4"),
12017 ..lsp::CompletionItem::default()
12018 },
12019 ])))
12020 },
12021 );
12022
12023 editor.update_in(cx, |editor, window, cx| {
12024 cx.focus_self(window);
12025 editor.move_to_end(&MoveToEnd, window, cx);
12026 editor.handle_input(".", window, cx);
12027 });
12028 cx.run_until_parked();
12029 completion_handle.next().await.unwrap();
12030
12031 editor.update(cx, |editor, _| {
12032 assert!(editor.context_menu_visible());
12033 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12034 {
12035 let completion_labels = menu
12036 .completions
12037 .borrow()
12038 .iter()
12039 .map(|c| c.label.text.clone())
12040 .collect::<Vec<_>>();
12041 assert_eq!(
12042 completion_labels,
12043 &[
12044 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
12045 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
12046 "single line label 2 d e f ",
12047 "a b c g h i ",
12048 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
12049 ],
12050 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
12051 );
12052
12053 for completion in menu
12054 .completions
12055 .borrow()
12056 .iter() {
12057 assert_eq!(
12058 completion.label.filter_range,
12059 0..completion.label.text.len(),
12060 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
12061 );
12062 }
12063 } else {
12064 panic!("expected completion menu to be open");
12065 }
12066 });
12067}
12068
12069#[gpui::test]
12070async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
12071 init_test(cx, |_| {});
12072 let mut cx = EditorLspTestContext::new_rust(
12073 lsp::ServerCapabilities {
12074 completion_provider: Some(lsp::CompletionOptions {
12075 trigger_characters: Some(vec![".".to_string()]),
12076 ..Default::default()
12077 }),
12078 ..Default::default()
12079 },
12080 cx,
12081 )
12082 .await;
12083 cx.lsp
12084 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12085 Ok(Some(lsp::CompletionResponse::Array(vec![
12086 lsp::CompletionItem {
12087 label: "first".into(),
12088 ..Default::default()
12089 },
12090 lsp::CompletionItem {
12091 label: "last".into(),
12092 ..Default::default()
12093 },
12094 ])))
12095 });
12096 cx.set_state("variableˇ");
12097 cx.simulate_keystroke(".");
12098 cx.executor().run_until_parked();
12099
12100 cx.update_editor(|editor, _, _| {
12101 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12102 {
12103 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
12104 } else {
12105 panic!("expected completion menu to be open");
12106 }
12107 });
12108
12109 cx.update_editor(|editor, window, cx| {
12110 editor.move_page_down(&MovePageDown::default(), window, cx);
12111 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12112 {
12113 assert!(
12114 menu.selected_item == 1,
12115 "expected PageDown to select the last item from the context menu"
12116 );
12117 } else {
12118 panic!("expected completion menu to stay open after PageDown");
12119 }
12120 });
12121
12122 cx.update_editor(|editor, window, cx| {
12123 editor.move_page_up(&MovePageUp::default(), window, cx);
12124 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12125 {
12126 assert!(
12127 menu.selected_item == 0,
12128 "expected PageUp to select the first item from the context menu"
12129 );
12130 } else {
12131 panic!("expected completion menu to stay open after PageUp");
12132 }
12133 });
12134}
12135
12136#[gpui::test]
12137async fn test_as_is_completions(cx: &mut TestAppContext) {
12138 init_test(cx, |_| {});
12139 let mut cx = EditorLspTestContext::new_rust(
12140 lsp::ServerCapabilities {
12141 completion_provider: Some(lsp::CompletionOptions {
12142 ..Default::default()
12143 }),
12144 ..Default::default()
12145 },
12146 cx,
12147 )
12148 .await;
12149 cx.lsp
12150 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12151 Ok(Some(lsp::CompletionResponse::Array(vec![
12152 lsp::CompletionItem {
12153 label: "unsafe".into(),
12154 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12155 range: lsp::Range {
12156 start: lsp::Position {
12157 line: 1,
12158 character: 2,
12159 },
12160 end: lsp::Position {
12161 line: 1,
12162 character: 3,
12163 },
12164 },
12165 new_text: "unsafe".to_string(),
12166 })),
12167 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
12168 ..Default::default()
12169 },
12170 ])))
12171 });
12172 cx.set_state("fn a() {}\n nˇ");
12173 cx.executor().run_until_parked();
12174 cx.update_editor(|editor, window, cx| {
12175 editor.show_completions(
12176 &ShowCompletions {
12177 trigger: Some("\n".into()),
12178 },
12179 window,
12180 cx,
12181 );
12182 });
12183 cx.executor().run_until_parked();
12184
12185 cx.update_editor(|editor, window, cx| {
12186 editor.confirm_completion(&Default::default(), window, cx)
12187 });
12188 cx.executor().run_until_parked();
12189 cx.assert_editor_state("fn a() {}\n unsafeˇ");
12190}
12191
12192#[gpui::test]
12193async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
12194 init_test(cx, |_| {});
12195
12196 let mut cx = EditorLspTestContext::new_rust(
12197 lsp::ServerCapabilities {
12198 completion_provider: Some(lsp::CompletionOptions {
12199 trigger_characters: Some(vec![".".to_string()]),
12200 resolve_provider: Some(true),
12201 ..Default::default()
12202 }),
12203 ..Default::default()
12204 },
12205 cx,
12206 )
12207 .await;
12208
12209 cx.set_state("fn main() { let a = 2ˇ; }");
12210 cx.simulate_keystroke(".");
12211 let completion_item = lsp::CompletionItem {
12212 label: "Some".into(),
12213 kind: Some(lsp::CompletionItemKind::SNIPPET),
12214 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
12215 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
12216 kind: lsp::MarkupKind::Markdown,
12217 value: "```rust\nSome(2)\n```".to_string(),
12218 })),
12219 deprecated: Some(false),
12220 sort_text: Some("Some".to_string()),
12221 filter_text: Some("Some".to_string()),
12222 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
12223 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12224 range: lsp::Range {
12225 start: lsp::Position {
12226 line: 0,
12227 character: 22,
12228 },
12229 end: lsp::Position {
12230 line: 0,
12231 character: 22,
12232 },
12233 },
12234 new_text: "Some(2)".to_string(),
12235 })),
12236 additional_text_edits: Some(vec![lsp::TextEdit {
12237 range: lsp::Range {
12238 start: lsp::Position {
12239 line: 0,
12240 character: 20,
12241 },
12242 end: lsp::Position {
12243 line: 0,
12244 character: 22,
12245 },
12246 },
12247 new_text: "".to_string(),
12248 }]),
12249 ..Default::default()
12250 };
12251
12252 let closure_completion_item = completion_item.clone();
12253 let counter = Arc::new(AtomicUsize::new(0));
12254 let counter_clone = counter.clone();
12255 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
12256 let task_completion_item = closure_completion_item.clone();
12257 counter_clone.fetch_add(1, atomic::Ordering::Release);
12258 async move {
12259 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
12260 is_incomplete: true,
12261 item_defaults: None,
12262 items: vec![task_completion_item],
12263 })))
12264 }
12265 });
12266
12267 cx.condition(|editor, _| editor.context_menu_visible())
12268 .await;
12269 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
12270 assert!(request.next().await.is_some());
12271 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12272
12273 cx.simulate_keystrokes("S o m");
12274 cx.condition(|editor, _| editor.context_menu_visible())
12275 .await;
12276 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
12277 assert!(request.next().await.is_some());
12278 assert!(request.next().await.is_some());
12279 assert!(request.next().await.is_some());
12280 request.close();
12281 assert!(request.next().await.is_none());
12282 assert_eq!(
12283 counter.load(atomic::Ordering::Acquire),
12284 4,
12285 "With the completions menu open, only one LSP request should happen per input"
12286 );
12287}
12288
12289#[gpui::test]
12290async fn test_toggle_comment(cx: &mut TestAppContext) {
12291 init_test(cx, |_| {});
12292 let mut cx = EditorTestContext::new(cx).await;
12293 let language = Arc::new(Language::new(
12294 LanguageConfig {
12295 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
12296 ..Default::default()
12297 },
12298 Some(tree_sitter_rust::LANGUAGE.into()),
12299 ));
12300 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
12301
12302 // If multiple selections intersect a line, the line is only toggled once.
12303 cx.set_state(indoc! {"
12304 fn a() {
12305 «//b();
12306 ˇ»// «c();
12307 //ˇ» d();
12308 }
12309 "});
12310
12311 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12312
12313 cx.assert_editor_state(indoc! {"
12314 fn a() {
12315 «b();
12316 c();
12317 ˇ» d();
12318 }
12319 "});
12320
12321 // The comment prefix is inserted at the same column for every line in a
12322 // selection.
12323 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12324
12325 cx.assert_editor_state(indoc! {"
12326 fn a() {
12327 // «b();
12328 // c();
12329 ˇ»// d();
12330 }
12331 "});
12332
12333 // If a selection ends at the beginning of a line, that line is not toggled.
12334 cx.set_selections_state(indoc! {"
12335 fn a() {
12336 // b();
12337 «// c();
12338 ˇ» // d();
12339 }
12340 "});
12341
12342 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12343
12344 cx.assert_editor_state(indoc! {"
12345 fn a() {
12346 // b();
12347 «c();
12348 ˇ» // d();
12349 }
12350 "});
12351
12352 // If a selection span a single line and is empty, the line is toggled.
12353 cx.set_state(indoc! {"
12354 fn a() {
12355 a();
12356 b();
12357 ˇ
12358 }
12359 "});
12360
12361 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12362
12363 cx.assert_editor_state(indoc! {"
12364 fn a() {
12365 a();
12366 b();
12367 //•ˇ
12368 }
12369 "});
12370
12371 // If a selection span multiple lines, empty lines are not toggled.
12372 cx.set_state(indoc! {"
12373 fn a() {
12374 «a();
12375
12376 c();ˇ»
12377 }
12378 "});
12379
12380 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12381
12382 cx.assert_editor_state(indoc! {"
12383 fn a() {
12384 // «a();
12385
12386 // c();ˇ»
12387 }
12388 "});
12389
12390 // If a selection includes multiple comment prefixes, all lines are uncommented.
12391 cx.set_state(indoc! {"
12392 fn a() {
12393 «// a();
12394 /// b();
12395 //! c();ˇ»
12396 }
12397 "});
12398
12399 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12400
12401 cx.assert_editor_state(indoc! {"
12402 fn a() {
12403 «a();
12404 b();
12405 c();ˇ»
12406 }
12407 "});
12408}
12409
12410#[gpui::test]
12411async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
12412 init_test(cx, |_| {});
12413 let mut cx = EditorTestContext::new(cx).await;
12414 let language = Arc::new(Language::new(
12415 LanguageConfig {
12416 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
12417 ..Default::default()
12418 },
12419 Some(tree_sitter_rust::LANGUAGE.into()),
12420 ));
12421 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
12422
12423 let toggle_comments = &ToggleComments {
12424 advance_downwards: false,
12425 ignore_indent: true,
12426 };
12427
12428 // If multiple selections intersect a line, the line is only toggled once.
12429 cx.set_state(indoc! {"
12430 fn a() {
12431 // «b();
12432 // c();
12433 // ˇ» d();
12434 }
12435 "});
12436
12437 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12438
12439 cx.assert_editor_state(indoc! {"
12440 fn a() {
12441 «b();
12442 c();
12443 ˇ» d();
12444 }
12445 "});
12446
12447 // The comment prefix is inserted at the beginning of each line
12448 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12449
12450 cx.assert_editor_state(indoc! {"
12451 fn a() {
12452 // «b();
12453 // c();
12454 // ˇ» d();
12455 }
12456 "});
12457
12458 // If a selection ends at the beginning of a line, that line is not toggled.
12459 cx.set_selections_state(indoc! {"
12460 fn a() {
12461 // b();
12462 // «c();
12463 ˇ»// d();
12464 }
12465 "});
12466
12467 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12468
12469 cx.assert_editor_state(indoc! {"
12470 fn a() {
12471 // b();
12472 «c();
12473 ˇ»// d();
12474 }
12475 "});
12476
12477 // If a selection span a single line and is empty, the line is toggled.
12478 cx.set_state(indoc! {"
12479 fn a() {
12480 a();
12481 b();
12482 ˇ
12483 }
12484 "});
12485
12486 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12487
12488 cx.assert_editor_state(indoc! {"
12489 fn a() {
12490 a();
12491 b();
12492 //ˇ
12493 }
12494 "});
12495
12496 // If a selection span multiple lines, empty lines are not toggled.
12497 cx.set_state(indoc! {"
12498 fn a() {
12499 «a();
12500
12501 c();ˇ»
12502 }
12503 "});
12504
12505 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12506
12507 cx.assert_editor_state(indoc! {"
12508 fn a() {
12509 // «a();
12510
12511 // c();ˇ»
12512 }
12513 "});
12514
12515 // If a selection includes multiple comment prefixes, all lines are uncommented.
12516 cx.set_state(indoc! {"
12517 fn a() {
12518 // «a();
12519 /// b();
12520 //! c();ˇ»
12521 }
12522 "});
12523
12524 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12525
12526 cx.assert_editor_state(indoc! {"
12527 fn a() {
12528 «a();
12529 b();
12530 c();ˇ»
12531 }
12532 "});
12533}
12534
12535#[gpui::test]
12536async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
12537 init_test(cx, |_| {});
12538
12539 let language = Arc::new(Language::new(
12540 LanguageConfig {
12541 line_comments: vec!["// ".into()],
12542 ..Default::default()
12543 },
12544 Some(tree_sitter_rust::LANGUAGE.into()),
12545 ));
12546
12547 let mut cx = EditorTestContext::new(cx).await;
12548
12549 cx.language_registry().add(language.clone());
12550 cx.update_buffer(|buffer, cx| {
12551 buffer.set_language(Some(language), cx);
12552 });
12553
12554 let toggle_comments = &ToggleComments {
12555 advance_downwards: true,
12556 ignore_indent: false,
12557 };
12558
12559 // Single cursor on one line -> advance
12560 // Cursor moves horizontally 3 characters as well on non-blank line
12561 cx.set_state(indoc!(
12562 "fn a() {
12563 ˇdog();
12564 cat();
12565 }"
12566 ));
12567 cx.update_editor(|editor, window, cx| {
12568 editor.toggle_comments(toggle_comments, window, cx);
12569 });
12570 cx.assert_editor_state(indoc!(
12571 "fn a() {
12572 // dog();
12573 catˇ();
12574 }"
12575 ));
12576
12577 // Single selection on one line -> don't advance
12578 cx.set_state(indoc!(
12579 "fn a() {
12580 «dog()ˇ»;
12581 cat();
12582 }"
12583 ));
12584 cx.update_editor(|editor, window, cx| {
12585 editor.toggle_comments(toggle_comments, window, cx);
12586 });
12587 cx.assert_editor_state(indoc!(
12588 "fn a() {
12589 // «dog()ˇ»;
12590 cat();
12591 }"
12592 ));
12593
12594 // Multiple cursors on one line -> advance
12595 cx.set_state(indoc!(
12596 "fn a() {
12597 ˇdˇog();
12598 cat();
12599 }"
12600 ));
12601 cx.update_editor(|editor, window, cx| {
12602 editor.toggle_comments(toggle_comments, window, cx);
12603 });
12604 cx.assert_editor_state(indoc!(
12605 "fn a() {
12606 // dog();
12607 catˇ(ˇ);
12608 }"
12609 ));
12610
12611 // Multiple cursors on one line, with selection -> don't advance
12612 cx.set_state(indoc!(
12613 "fn a() {
12614 ˇdˇog«()ˇ»;
12615 cat();
12616 }"
12617 ));
12618 cx.update_editor(|editor, window, cx| {
12619 editor.toggle_comments(toggle_comments, window, cx);
12620 });
12621 cx.assert_editor_state(indoc!(
12622 "fn a() {
12623 // ˇdˇog«()ˇ»;
12624 cat();
12625 }"
12626 ));
12627
12628 // Single cursor on one line -> advance
12629 // Cursor moves to column 0 on blank line
12630 cx.set_state(indoc!(
12631 "fn a() {
12632 ˇdog();
12633
12634 cat();
12635 }"
12636 ));
12637 cx.update_editor(|editor, window, cx| {
12638 editor.toggle_comments(toggle_comments, window, cx);
12639 });
12640 cx.assert_editor_state(indoc!(
12641 "fn a() {
12642 // dog();
12643 ˇ
12644 cat();
12645 }"
12646 ));
12647
12648 // Single cursor on one line -> advance
12649 // Cursor starts and ends at column 0
12650 cx.set_state(indoc!(
12651 "fn a() {
12652 ˇ dog();
12653 cat();
12654 }"
12655 ));
12656 cx.update_editor(|editor, window, cx| {
12657 editor.toggle_comments(toggle_comments, window, cx);
12658 });
12659 cx.assert_editor_state(indoc!(
12660 "fn a() {
12661 // dog();
12662 ˇ cat();
12663 }"
12664 ));
12665}
12666
12667#[gpui::test]
12668async fn test_toggle_block_comment(cx: &mut TestAppContext) {
12669 init_test(cx, |_| {});
12670
12671 let mut cx = EditorTestContext::new(cx).await;
12672
12673 let html_language = Arc::new(
12674 Language::new(
12675 LanguageConfig {
12676 name: "HTML".into(),
12677 block_comment: Some(("<!-- ".into(), " -->".into())),
12678 ..Default::default()
12679 },
12680 Some(tree_sitter_html::LANGUAGE.into()),
12681 )
12682 .with_injection_query(
12683 r#"
12684 (script_element
12685 (raw_text) @injection.content
12686 (#set! injection.language "javascript"))
12687 "#,
12688 )
12689 .unwrap(),
12690 );
12691
12692 let javascript_language = Arc::new(Language::new(
12693 LanguageConfig {
12694 name: "JavaScript".into(),
12695 line_comments: vec!["// ".into()],
12696 ..Default::default()
12697 },
12698 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
12699 ));
12700
12701 cx.language_registry().add(html_language.clone());
12702 cx.language_registry().add(javascript_language.clone());
12703 cx.update_buffer(|buffer, cx| {
12704 buffer.set_language(Some(html_language), cx);
12705 });
12706
12707 // Toggle comments for empty selections
12708 cx.set_state(
12709 &r#"
12710 <p>A</p>ˇ
12711 <p>B</p>ˇ
12712 <p>C</p>ˇ
12713 "#
12714 .unindent(),
12715 );
12716 cx.update_editor(|editor, window, cx| {
12717 editor.toggle_comments(&ToggleComments::default(), window, cx)
12718 });
12719 cx.assert_editor_state(
12720 &r#"
12721 <!-- <p>A</p>ˇ -->
12722 <!-- <p>B</p>ˇ -->
12723 <!-- <p>C</p>ˇ -->
12724 "#
12725 .unindent(),
12726 );
12727 cx.update_editor(|editor, window, cx| {
12728 editor.toggle_comments(&ToggleComments::default(), window, cx)
12729 });
12730 cx.assert_editor_state(
12731 &r#"
12732 <p>A</p>ˇ
12733 <p>B</p>ˇ
12734 <p>C</p>ˇ
12735 "#
12736 .unindent(),
12737 );
12738
12739 // Toggle comments for mixture of empty and non-empty selections, where
12740 // multiple selections occupy a given line.
12741 cx.set_state(
12742 &r#"
12743 <p>A«</p>
12744 <p>ˇ»B</p>ˇ
12745 <p>C«</p>
12746 <p>ˇ»D</p>ˇ
12747 "#
12748 .unindent(),
12749 );
12750
12751 cx.update_editor(|editor, window, cx| {
12752 editor.toggle_comments(&ToggleComments::default(), window, cx)
12753 });
12754 cx.assert_editor_state(
12755 &r#"
12756 <!-- <p>A«</p>
12757 <p>ˇ»B</p>ˇ -->
12758 <!-- <p>C«</p>
12759 <p>ˇ»D</p>ˇ -->
12760 "#
12761 .unindent(),
12762 );
12763 cx.update_editor(|editor, window, cx| {
12764 editor.toggle_comments(&ToggleComments::default(), window, cx)
12765 });
12766 cx.assert_editor_state(
12767 &r#"
12768 <p>A«</p>
12769 <p>ˇ»B</p>ˇ
12770 <p>C«</p>
12771 <p>ˇ»D</p>ˇ
12772 "#
12773 .unindent(),
12774 );
12775
12776 // Toggle comments when different languages are active for different
12777 // selections.
12778 cx.set_state(
12779 &r#"
12780 ˇ<script>
12781 ˇvar x = new Y();
12782 ˇ</script>
12783 "#
12784 .unindent(),
12785 );
12786 cx.executor().run_until_parked();
12787 cx.update_editor(|editor, window, cx| {
12788 editor.toggle_comments(&ToggleComments::default(), window, cx)
12789 });
12790 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
12791 // Uncommenting and commenting from this position brings in even more wrong artifacts.
12792 cx.assert_editor_state(
12793 &r#"
12794 <!-- ˇ<script> -->
12795 // ˇvar x = new Y();
12796 <!-- ˇ</script> -->
12797 "#
12798 .unindent(),
12799 );
12800}
12801
12802#[gpui::test]
12803fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
12804 init_test(cx, |_| {});
12805
12806 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12807 let multibuffer = cx.new(|cx| {
12808 let mut multibuffer = MultiBuffer::new(ReadWrite);
12809 multibuffer.push_excerpts(
12810 buffer.clone(),
12811 [
12812 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
12813 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
12814 ],
12815 cx,
12816 );
12817 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
12818 multibuffer
12819 });
12820
12821 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12822 editor.update_in(cx, |editor, window, cx| {
12823 assert_eq!(editor.text(cx), "aaaa\nbbbb");
12824 editor.change_selections(None, window, cx, |s| {
12825 s.select_ranges([
12826 Point::new(0, 0)..Point::new(0, 0),
12827 Point::new(1, 0)..Point::new(1, 0),
12828 ])
12829 });
12830
12831 editor.handle_input("X", window, cx);
12832 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
12833 assert_eq!(
12834 editor.selections.ranges(cx),
12835 [
12836 Point::new(0, 1)..Point::new(0, 1),
12837 Point::new(1, 1)..Point::new(1, 1),
12838 ]
12839 );
12840
12841 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
12842 editor.change_selections(None, window, cx, |s| {
12843 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
12844 });
12845 editor.backspace(&Default::default(), window, cx);
12846 assert_eq!(editor.text(cx), "Xa\nbbb");
12847 assert_eq!(
12848 editor.selections.ranges(cx),
12849 [Point::new(1, 0)..Point::new(1, 0)]
12850 );
12851
12852 editor.change_selections(None, window, cx, |s| {
12853 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
12854 });
12855 editor.backspace(&Default::default(), window, cx);
12856 assert_eq!(editor.text(cx), "X\nbb");
12857 assert_eq!(
12858 editor.selections.ranges(cx),
12859 [Point::new(0, 1)..Point::new(0, 1)]
12860 );
12861 });
12862}
12863
12864#[gpui::test]
12865fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
12866 init_test(cx, |_| {});
12867
12868 let markers = vec![('[', ']').into(), ('(', ')').into()];
12869 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
12870 indoc! {"
12871 [aaaa
12872 (bbbb]
12873 cccc)",
12874 },
12875 markers.clone(),
12876 );
12877 let excerpt_ranges = markers.into_iter().map(|marker| {
12878 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
12879 ExcerptRange::new(context.clone())
12880 });
12881 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
12882 let multibuffer = cx.new(|cx| {
12883 let mut multibuffer = MultiBuffer::new(ReadWrite);
12884 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
12885 multibuffer
12886 });
12887
12888 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12889 editor.update_in(cx, |editor, window, cx| {
12890 let (expected_text, selection_ranges) = marked_text_ranges(
12891 indoc! {"
12892 aaaa
12893 bˇbbb
12894 bˇbbˇb
12895 cccc"
12896 },
12897 true,
12898 );
12899 assert_eq!(editor.text(cx), expected_text);
12900 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
12901
12902 editor.handle_input("X", window, cx);
12903
12904 let (expected_text, expected_selections) = marked_text_ranges(
12905 indoc! {"
12906 aaaa
12907 bXˇbbXb
12908 bXˇbbXˇb
12909 cccc"
12910 },
12911 false,
12912 );
12913 assert_eq!(editor.text(cx), expected_text);
12914 assert_eq!(editor.selections.ranges(cx), expected_selections);
12915
12916 editor.newline(&Newline, window, cx);
12917 let (expected_text, expected_selections) = marked_text_ranges(
12918 indoc! {"
12919 aaaa
12920 bX
12921 ˇbbX
12922 b
12923 bX
12924 ˇbbX
12925 ˇb
12926 cccc"
12927 },
12928 false,
12929 );
12930 assert_eq!(editor.text(cx), expected_text);
12931 assert_eq!(editor.selections.ranges(cx), expected_selections);
12932 });
12933}
12934
12935#[gpui::test]
12936fn test_refresh_selections(cx: &mut TestAppContext) {
12937 init_test(cx, |_| {});
12938
12939 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12940 let mut excerpt1_id = None;
12941 let multibuffer = cx.new(|cx| {
12942 let mut multibuffer = MultiBuffer::new(ReadWrite);
12943 excerpt1_id = multibuffer
12944 .push_excerpts(
12945 buffer.clone(),
12946 [
12947 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12948 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12949 ],
12950 cx,
12951 )
12952 .into_iter()
12953 .next();
12954 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12955 multibuffer
12956 });
12957
12958 let editor = cx.add_window(|window, cx| {
12959 let mut editor = build_editor(multibuffer.clone(), window, cx);
12960 let snapshot = editor.snapshot(window, cx);
12961 editor.change_selections(None, window, cx, |s| {
12962 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
12963 });
12964 editor.begin_selection(
12965 Point::new(2, 1).to_display_point(&snapshot),
12966 true,
12967 1,
12968 window,
12969 cx,
12970 );
12971 assert_eq!(
12972 editor.selections.ranges(cx),
12973 [
12974 Point::new(1, 3)..Point::new(1, 3),
12975 Point::new(2, 1)..Point::new(2, 1),
12976 ]
12977 );
12978 editor
12979 });
12980
12981 // Refreshing selections is a no-op when excerpts haven't changed.
12982 _ = editor.update(cx, |editor, window, cx| {
12983 editor.change_selections(None, window, cx, |s| s.refresh());
12984 assert_eq!(
12985 editor.selections.ranges(cx),
12986 [
12987 Point::new(1, 3)..Point::new(1, 3),
12988 Point::new(2, 1)..Point::new(2, 1),
12989 ]
12990 );
12991 });
12992
12993 multibuffer.update(cx, |multibuffer, cx| {
12994 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12995 });
12996 _ = editor.update(cx, |editor, window, cx| {
12997 // Removing an excerpt causes the first selection to become degenerate.
12998 assert_eq!(
12999 editor.selections.ranges(cx),
13000 [
13001 Point::new(0, 0)..Point::new(0, 0),
13002 Point::new(0, 1)..Point::new(0, 1)
13003 ]
13004 );
13005
13006 // Refreshing selections will relocate the first selection to the original buffer
13007 // location.
13008 editor.change_selections(None, window, cx, |s| s.refresh());
13009 assert_eq!(
13010 editor.selections.ranges(cx),
13011 [
13012 Point::new(0, 1)..Point::new(0, 1),
13013 Point::new(0, 3)..Point::new(0, 3)
13014 ]
13015 );
13016 assert!(editor.selections.pending_anchor().is_some());
13017 });
13018}
13019
13020#[gpui::test]
13021fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
13022 init_test(cx, |_| {});
13023
13024 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13025 let mut excerpt1_id = None;
13026 let multibuffer = cx.new(|cx| {
13027 let mut multibuffer = MultiBuffer::new(ReadWrite);
13028 excerpt1_id = multibuffer
13029 .push_excerpts(
13030 buffer.clone(),
13031 [
13032 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
13033 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
13034 ],
13035 cx,
13036 )
13037 .into_iter()
13038 .next();
13039 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
13040 multibuffer
13041 });
13042
13043 let editor = cx.add_window(|window, cx| {
13044 let mut editor = build_editor(multibuffer.clone(), window, cx);
13045 let snapshot = editor.snapshot(window, cx);
13046 editor.begin_selection(
13047 Point::new(1, 3).to_display_point(&snapshot),
13048 false,
13049 1,
13050 window,
13051 cx,
13052 );
13053 assert_eq!(
13054 editor.selections.ranges(cx),
13055 [Point::new(1, 3)..Point::new(1, 3)]
13056 );
13057 editor
13058 });
13059
13060 multibuffer.update(cx, |multibuffer, cx| {
13061 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
13062 });
13063 _ = editor.update(cx, |editor, window, cx| {
13064 assert_eq!(
13065 editor.selections.ranges(cx),
13066 [Point::new(0, 0)..Point::new(0, 0)]
13067 );
13068
13069 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
13070 editor.change_selections(None, window, cx, |s| s.refresh());
13071 assert_eq!(
13072 editor.selections.ranges(cx),
13073 [Point::new(0, 3)..Point::new(0, 3)]
13074 );
13075 assert!(editor.selections.pending_anchor().is_some());
13076 });
13077}
13078
13079#[gpui::test]
13080async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
13081 init_test(cx, |_| {});
13082
13083 let language = Arc::new(
13084 Language::new(
13085 LanguageConfig {
13086 brackets: BracketPairConfig {
13087 pairs: vec![
13088 BracketPair {
13089 start: "{".to_string(),
13090 end: "}".to_string(),
13091 close: true,
13092 surround: true,
13093 newline: true,
13094 },
13095 BracketPair {
13096 start: "/* ".to_string(),
13097 end: " */".to_string(),
13098 close: true,
13099 surround: true,
13100 newline: true,
13101 },
13102 ],
13103 ..Default::default()
13104 },
13105 ..Default::default()
13106 },
13107 Some(tree_sitter_rust::LANGUAGE.into()),
13108 )
13109 .with_indents_query("")
13110 .unwrap(),
13111 );
13112
13113 let text = concat!(
13114 "{ }\n", //
13115 " x\n", //
13116 " /* */\n", //
13117 "x\n", //
13118 "{{} }\n", //
13119 );
13120
13121 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
13122 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13123 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
13124 editor
13125 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
13126 .await;
13127
13128 editor.update_in(cx, |editor, window, cx| {
13129 editor.change_selections(None, window, cx, |s| {
13130 s.select_display_ranges([
13131 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
13132 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
13133 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
13134 ])
13135 });
13136 editor.newline(&Newline, window, cx);
13137
13138 assert_eq!(
13139 editor.buffer().read(cx).read(cx).text(),
13140 concat!(
13141 "{ \n", // Suppress rustfmt
13142 "\n", //
13143 "}\n", //
13144 " x\n", //
13145 " /* \n", //
13146 " \n", //
13147 " */\n", //
13148 "x\n", //
13149 "{{} \n", //
13150 "}\n", //
13151 )
13152 );
13153 });
13154}
13155
13156#[gpui::test]
13157fn test_highlighted_ranges(cx: &mut TestAppContext) {
13158 init_test(cx, |_| {});
13159
13160 let editor = cx.add_window(|window, cx| {
13161 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
13162 build_editor(buffer.clone(), window, cx)
13163 });
13164
13165 _ = editor.update(cx, |editor, window, cx| {
13166 struct Type1;
13167 struct Type2;
13168
13169 let buffer = editor.buffer.read(cx).snapshot(cx);
13170
13171 let anchor_range =
13172 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
13173
13174 editor.highlight_background::<Type1>(
13175 &[
13176 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
13177 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
13178 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
13179 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
13180 ],
13181 |_| Hsla::red(),
13182 cx,
13183 );
13184 editor.highlight_background::<Type2>(
13185 &[
13186 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
13187 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
13188 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
13189 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
13190 ],
13191 |_| Hsla::green(),
13192 cx,
13193 );
13194
13195 let snapshot = editor.snapshot(window, cx);
13196 let mut highlighted_ranges = editor.background_highlights_in_range(
13197 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
13198 &snapshot,
13199 cx.theme().colors(),
13200 );
13201 // Enforce a consistent ordering based on color without relying on the ordering of the
13202 // highlight's `TypeId` which is non-executor.
13203 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
13204 assert_eq!(
13205 highlighted_ranges,
13206 &[
13207 (
13208 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
13209 Hsla::red(),
13210 ),
13211 (
13212 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
13213 Hsla::red(),
13214 ),
13215 (
13216 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
13217 Hsla::green(),
13218 ),
13219 (
13220 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
13221 Hsla::green(),
13222 ),
13223 ]
13224 );
13225 assert_eq!(
13226 editor.background_highlights_in_range(
13227 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
13228 &snapshot,
13229 cx.theme().colors(),
13230 ),
13231 &[(
13232 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
13233 Hsla::red(),
13234 )]
13235 );
13236 });
13237}
13238
13239#[gpui::test]
13240async fn test_following(cx: &mut TestAppContext) {
13241 init_test(cx, |_| {});
13242
13243 let fs = FakeFs::new(cx.executor());
13244 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
13245
13246 let buffer = project.update(cx, |project, cx| {
13247 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
13248 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
13249 });
13250 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
13251 let follower = cx.update(|cx| {
13252 cx.open_window(
13253 WindowOptions {
13254 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
13255 gpui::Point::new(px(0.), px(0.)),
13256 gpui::Point::new(px(10.), px(80.)),
13257 ))),
13258 ..Default::default()
13259 },
13260 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
13261 )
13262 .unwrap()
13263 });
13264
13265 let is_still_following = Rc::new(RefCell::new(true));
13266 let follower_edit_event_count = Rc::new(RefCell::new(0));
13267 let pending_update = Rc::new(RefCell::new(None));
13268 let leader_entity = leader.root(cx).unwrap();
13269 let follower_entity = follower.root(cx).unwrap();
13270 _ = follower.update(cx, {
13271 let update = pending_update.clone();
13272 let is_still_following = is_still_following.clone();
13273 let follower_edit_event_count = follower_edit_event_count.clone();
13274 |_, window, cx| {
13275 cx.subscribe_in(
13276 &leader_entity,
13277 window,
13278 move |_, leader, event, window, cx| {
13279 leader.read(cx).add_event_to_update_proto(
13280 event,
13281 &mut update.borrow_mut(),
13282 window,
13283 cx,
13284 );
13285 },
13286 )
13287 .detach();
13288
13289 cx.subscribe_in(
13290 &follower_entity,
13291 window,
13292 move |_, _, event: &EditorEvent, _window, _cx| {
13293 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
13294 *is_still_following.borrow_mut() = false;
13295 }
13296
13297 if let EditorEvent::BufferEdited = event {
13298 *follower_edit_event_count.borrow_mut() += 1;
13299 }
13300 },
13301 )
13302 .detach();
13303 }
13304 });
13305
13306 // Update the selections only
13307 _ = leader.update(cx, |leader, window, cx| {
13308 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
13309 });
13310 follower
13311 .update(cx, |follower, window, cx| {
13312 follower.apply_update_proto(
13313 &project,
13314 pending_update.borrow_mut().take().unwrap(),
13315 window,
13316 cx,
13317 )
13318 })
13319 .unwrap()
13320 .await
13321 .unwrap();
13322 _ = follower.update(cx, |follower, _, cx| {
13323 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
13324 });
13325 assert!(*is_still_following.borrow());
13326 assert_eq!(*follower_edit_event_count.borrow(), 0);
13327
13328 // Update the scroll position only
13329 _ = leader.update(cx, |leader, window, cx| {
13330 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
13331 });
13332 follower
13333 .update(cx, |follower, window, cx| {
13334 follower.apply_update_proto(
13335 &project,
13336 pending_update.borrow_mut().take().unwrap(),
13337 window,
13338 cx,
13339 )
13340 })
13341 .unwrap()
13342 .await
13343 .unwrap();
13344 assert_eq!(
13345 follower
13346 .update(cx, |follower, _, cx| follower.scroll_position(cx))
13347 .unwrap(),
13348 gpui::Point::new(1.5, 3.5)
13349 );
13350 assert!(*is_still_following.borrow());
13351 assert_eq!(*follower_edit_event_count.borrow(), 0);
13352
13353 // Update the selections and scroll position. The follower's scroll position is updated
13354 // via autoscroll, not via the leader's exact scroll position.
13355 _ = leader.update(cx, |leader, window, cx| {
13356 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
13357 leader.request_autoscroll(Autoscroll::newest(), cx);
13358 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
13359 });
13360 follower
13361 .update(cx, |follower, window, cx| {
13362 follower.apply_update_proto(
13363 &project,
13364 pending_update.borrow_mut().take().unwrap(),
13365 window,
13366 cx,
13367 )
13368 })
13369 .unwrap()
13370 .await
13371 .unwrap();
13372 _ = follower.update(cx, |follower, _, cx| {
13373 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
13374 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
13375 });
13376 assert!(*is_still_following.borrow());
13377
13378 // Creating a pending selection that precedes another selection
13379 _ = leader.update(cx, |leader, window, cx| {
13380 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
13381 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
13382 });
13383 follower
13384 .update(cx, |follower, window, cx| {
13385 follower.apply_update_proto(
13386 &project,
13387 pending_update.borrow_mut().take().unwrap(),
13388 window,
13389 cx,
13390 )
13391 })
13392 .unwrap()
13393 .await
13394 .unwrap();
13395 _ = follower.update(cx, |follower, _, cx| {
13396 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
13397 });
13398 assert!(*is_still_following.borrow());
13399
13400 // Extend the pending selection so that it surrounds another selection
13401 _ = leader.update(cx, |leader, window, cx| {
13402 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
13403 });
13404 follower
13405 .update(cx, |follower, window, cx| {
13406 follower.apply_update_proto(
13407 &project,
13408 pending_update.borrow_mut().take().unwrap(),
13409 window,
13410 cx,
13411 )
13412 })
13413 .unwrap()
13414 .await
13415 .unwrap();
13416 _ = follower.update(cx, |follower, _, cx| {
13417 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
13418 });
13419
13420 // Scrolling locally breaks the follow
13421 _ = follower.update(cx, |follower, window, cx| {
13422 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
13423 follower.set_scroll_anchor(
13424 ScrollAnchor {
13425 anchor: top_anchor,
13426 offset: gpui::Point::new(0.0, 0.5),
13427 },
13428 window,
13429 cx,
13430 );
13431 });
13432 assert!(!(*is_still_following.borrow()));
13433}
13434
13435#[gpui::test]
13436async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
13437 init_test(cx, |_| {});
13438
13439 let fs = FakeFs::new(cx.executor());
13440 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
13441 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13442 let pane = workspace
13443 .update(cx, |workspace, _, _| workspace.active_pane().clone())
13444 .unwrap();
13445
13446 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
13447
13448 let leader = pane.update_in(cx, |_, window, cx| {
13449 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
13450 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
13451 });
13452
13453 // Start following the editor when it has no excerpts.
13454 let mut state_message =
13455 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
13456 let workspace_entity = workspace.root(cx).unwrap();
13457 let follower_1 = cx
13458 .update_window(*workspace.deref(), |_, window, cx| {
13459 Editor::from_state_proto(
13460 workspace_entity,
13461 ViewId {
13462 creator: CollaboratorId::PeerId(PeerId::default()),
13463 id: 0,
13464 },
13465 &mut state_message,
13466 window,
13467 cx,
13468 )
13469 })
13470 .unwrap()
13471 .unwrap()
13472 .await
13473 .unwrap();
13474
13475 let update_message = Rc::new(RefCell::new(None));
13476 follower_1.update_in(cx, {
13477 let update = update_message.clone();
13478 |_, window, cx| {
13479 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
13480 leader.read(cx).add_event_to_update_proto(
13481 event,
13482 &mut update.borrow_mut(),
13483 window,
13484 cx,
13485 );
13486 })
13487 .detach();
13488 }
13489 });
13490
13491 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
13492 (
13493 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
13494 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
13495 )
13496 });
13497
13498 // Insert some excerpts.
13499 leader.update(cx, |leader, cx| {
13500 leader.buffer.update(cx, |multibuffer, cx| {
13501 multibuffer.set_excerpts_for_path(
13502 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
13503 buffer_1.clone(),
13504 vec![
13505 Point::row_range(0..3),
13506 Point::row_range(1..6),
13507 Point::row_range(12..15),
13508 ],
13509 0,
13510 cx,
13511 );
13512 multibuffer.set_excerpts_for_path(
13513 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
13514 buffer_2.clone(),
13515 vec![Point::row_range(0..6), Point::row_range(8..12)],
13516 0,
13517 cx,
13518 );
13519 });
13520 });
13521
13522 // Apply the update of adding the excerpts.
13523 follower_1
13524 .update_in(cx, |follower, window, cx| {
13525 follower.apply_update_proto(
13526 &project,
13527 update_message.borrow().clone().unwrap(),
13528 window,
13529 cx,
13530 )
13531 })
13532 .await
13533 .unwrap();
13534 assert_eq!(
13535 follower_1.update(cx, |editor, cx| editor.text(cx)),
13536 leader.update(cx, |editor, cx| editor.text(cx))
13537 );
13538 update_message.borrow_mut().take();
13539
13540 // Start following separately after it already has excerpts.
13541 let mut state_message =
13542 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
13543 let workspace_entity = workspace.root(cx).unwrap();
13544 let follower_2 = cx
13545 .update_window(*workspace.deref(), |_, window, cx| {
13546 Editor::from_state_proto(
13547 workspace_entity,
13548 ViewId {
13549 creator: CollaboratorId::PeerId(PeerId::default()),
13550 id: 0,
13551 },
13552 &mut state_message,
13553 window,
13554 cx,
13555 )
13556 })
13557 .unwrap()
13558 .unwrap()
13559 .await
13560 .unwrap();
13561 assert_eq!(
13562 follower_2.update(cx, |editor, cx| editor.text(cx)),
13563 leader.update(cx, |editor, cx| editor.text(cx))
13564 );
13565
13566 // Remove some excerpts.
13567 leader.update(cx, |leader, cx| {
13568 leader.buffer.update(cx, |multibuffer, cx| {
13569 let excerpt_ids = multibuffer.excerpt_ids();
13570 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
13571 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
13572 });
13573 });
13574
13575 // Apply the update of removing the excerpts.
13576 follower_1
13577 .update_in(cx, |follower, window, cx| {
13578 follower.apply_update_proto(
13579 &project,
13580 update_message.borrow().clone().unwrap(),
13581 window,
13582 cx,
13583 )
13584 })
13585 .await
13586 .unwrap();
13587 follower_2
13588 .update_in(cx, |follower, window, cx| {
13589 follower.apply_update_proto(
13590 &project,
13591 update_message.borrow().clone().unwrap(),
13592 window,
13593 cx,
13594 )
13595 })
13596 .await
13597 .unwrap();
13598 update_message.borrow_mut().take();
13599 assert_eq!(
13600 follower_1.update(cx, |editor, cx| editor.text(cx)),
13601 leader.update(cx, |editor, cx| editor.text(cx))
13602 );
13603}
13604
13605#[gpui::test]
13606async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13607 init_test(cx, |_| {});
13608
13609 let mut cx = EditorTestContext::new(cx).await;
13610 let lsp_store =
13611 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
13612
13613 cx.set_state(indoc! {"
13614 ˇfn func(abc def: i32) -> u32 {
13615 }
13616 "});
13617
13618 cx.update(|_, cx| {
13619 lsp_store.update(cx, |lsp_store, cx| {
13620 lsp_store
13621 .update_diagnostics(
13622 LanguageServerId(0),
13623 lsp::PublishDiagnosticsParams {
13624 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
13625 version: None,
13626 diagnostics: vec![
13627 lsp::Diagnostic {
13628 range: lsp::Range::new(
13629 lsp::Position::new(0, 11),
13630 lsp::Position::new(0, 12),
13631 ),
13632 severity: Some(lsp::DiagnosticSeverity::ERROR),
13633 ..Default::default()
13634 },
13635 lsp::Diagnostic {
13636 range: lsp::Range::new(
13637 lsp::Position::new(0, 12),
13638 lsp::Position::new(0, 15),
13639 ),
13640 severity: Some(lsp::DiagnosticSeverity::ERROR),
13641 ..Default::default()
13642 },
13643 lsp::Diagnostic {
13644 range: lsp::Range::new(
13645 lsp::Position::new(0, 25),
13646 lsp::Position::new(0, 28),
13647 ),
13648 severity: Some(lsp::DiagnosticSeverity::ERROR),
13649 ..Default::default()
13650 },
13651 ],
13652 },
13653 &[],
13654 cx,
13655 )
13656 .unwrap()
13657 });
13658 });
13659
13660 executor.run_until_parked();
13661
13662 cx.update_editor(|editor, window, cx| {
13663 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13664 });
13665
13666 cx.assert_editor_state(indoc! {"
13667 fn func(abc def: i32) -> ˇu32 {
13668 }
13669 "});
13670
13671 cx.update_editor(|editor, window, cx| {
13672 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13673 });
13674
13675 cx.assert_editor_state(indoc! {"
13676 fn func(abc ˇdef: i32) -> u32 {
13677 }
13678 "});
13679
13680 cx.update_editor(|editor, window, cx| {
13681 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13682 });
13683
13684 cx.assert_editor_state(indoc! {"
13685 fn func(abcˇ def: i32) -> u32 {
13686 }
13687 "});
13688
13689 cx.update_editor(|editor, window, cx| {
13690 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13691 });
13692
13693 cx.assert_editor_state(indoc! {"
13694 fn func(abc def: i32) -> ˇu32 {
13695 }
13696 "});
13697}
13698
13699#[gpui::test]
13700async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13701 init_test(cx, |_| {});
13702
13703 let mut cx = EditorTestContext::new(cx).await;
13704
13705 let diff_base = r#"
13706 use some::mod;
13707
13708 const A: u32 = 42;
13709
13710 fn main() {
13711 println!("hello");
13712
13713 println!("world");
13714 }
13715 "#
13716 .unindent();
13717
13718 // Edits are modified, removed, modified, added
13719 cx.set_state(
13720 &r#"
13721 use some::modified;
13722
13723 ˇ
13724 fn main() {
13725 println!("hello there");
13726
13727 println!("around the");
13728 println!("world");
13729 }
13730 "#
13731 .unindent(),
13732 );
13733
13734 cx.set_head_text(&diff_base);
13735 executor.run_until_parked();
13736
13737 cx.update_editor(|editor, window, cx| {
13738 //Wrap around the bottom of the buffer
13739 for _ in 0..3 {
13740 editor.go_to_next_hunk(&GoToHunk, window, cx);
13741 }
13742 });
13743
13744 cx.assert_editor_state(
13745 &r#"
13746 ˇuse some::modified;
13747
13748
13749 fn main() {
13750 println!("hello there");
13751
13752 println!("around the");
13753 println!("world");
13754 }
13755 "#
13756 .unindent(),
13757 );
13758
13759 cx.update_editor(|editor, window, cx| {
13760 //Wrap around the top of the buffer
13761 for _ in 0..2 {
13762 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13763 }
13764 });
13765
13766 cx.assert_editor_state(
13767 &r#"
13768 use some::modified;
13769
13770
13771 fn main() {
13772 ˇ println!("hello there");
13773
13774 println!("around the");
13775 println!("world");
13776 }
13777 "#
13778 .unindent(),
13779 );
13780
13781 cx.update_editor(|editor, window, cx| {
13782 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13783 });
13784
13785 cx.assert_editor_state(
13786 &r#"
13787 use some::modified;
13788
13789 ˇ
13790 fn main() {
13791 println!("hello there");
13792
13793 println!("around the");
13794 println!("world");
13795 }
13796 "#
13797 .unindent(),
13798 );
13799
13800 cx.update_editor(|editor, window, cx| {
13801 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13802 });
13803
13804 cx.assert_editor_state(
13805 &r#"
13806 ˇuse some::modified;
13807
13808
13809 fn main() {
13810 println!("hello there");
13811
13812 println!("around the");
13813 println!("world");
13814 }
13815 "#
13816 .unindent(),
13817 );
13818
13819 cx.update_editor(|editor, window, cx| {
13820 for _ in 0..2 {
13821 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13822 }
13823 });
13824
13825 cx.assert_editor_state(
13826 &r#"
13827 use some::modified;
13828
13829
13830 fn main() {
13831 ˇ println!("hello there");
13832
13833 println!("around the");
13834 println!("world");
13835 }
13836 "#
13837 .unindent(),
13838 );
13839
13840 cx.update_editor(|editor, window, cx| {
13841 editor.fold(&Fold, window, cx);
13842 });
13843
13844 cx.update_editor(|editor, window, cx| {
13845 editor.go_to_next_hunk(&GoToHunk, window, cx);
13846 });
13847
13848 cx.assert_editor_state(
13849 &r#"
13850 ˇuse some::modified;
13851
13852
13853 fn main() {
13854 println!("hello there");
13855
13856 println!("around the");
13857 println!("world");
13858 }
13859 "#
13860 .unindent(),
13861 );
13862}
13863
13864#[test]
13865fn test_split_words() {
13866 fn split(text: &str) -> Vec<&str> {
13867 split_words(text).collect()
13868 }
13869
13870 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
13871 assert_eq!(split("hello_world"), &["hello_", "world"]);
13872 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
13873 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
13874 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
13875 assert_eq!(split("helloworld"), &["helloworld"]);
13876
13877 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
13878}
13879
13880#[gpui::test]
13881async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
13882 init_test(cx, |_| {});
13883
13884 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
13885 let mut assert = |before, after| {
13886 let _state_context = cx.set_state(before);
13887 cx.run_until_parked();
13888 cx.update_editor(|editor, window, cx| {
13889 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
13890 });
13891 cx.run_until_parked();
13892 cx.assert_editor_state(after);
13893 };
13894
13895 // Outside bracket jumps to outside of matching bracket
13896 assert("console.logˇ(var);", "console.log(var)ˇ;");
13897 assert("console.log(var)ˇ;", "console.logˇ(var);");
13898
13899 // Inside bracket jumps to inside of matching bracket
13900 assert("console.log(ˇvar);", "console.log(varˇ);");
13901 assert("console.log(varˇ);", "console.log(ˇvar);");
13902
13903 // When outside a bracket and inside, favor jumping to the inside bracket
13904 assert(
13905 "console.log('foo', [1, 2, 3]ˇ);",
13906 "console.log(ˇ'foo', [1, 2, 3]);",
13907 );
13908 assert(
13909 "console.log(ˇ'foo', [1, 2, 3]);",
13910 "console.log('foo', [1, 2, 3]ˇ);",
13911 );
13912
13913 // Bias forward if two options are equally likely
13914 assert(
13915 "let result = curried_fun()ˇ();",
13916 "let result = curried_fun()()ˇ;",
13917 );
13918
13919 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
13920 assert(
13921 indoc! {"
13922 function test() {
13923 console.log('test')ˇ
13924 }"},
13925 indoc! {"
13926 function test() {
13927 console.logˇ('test')
13928 }"},
13929 );
13930}
13931
13932#[gpui::test]
13933async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
13934 init_test(cx, |_| {});
13935
13936 let fs = FakeFs::new(cx.executor());
13937 fs.insert_tree(
13938 path!("/a"),
13939 json!({
13940 "main.rs": "fn main() { let a = 5; }",
13941 "other.rs": "// Test file",
13942 }),
13943 )
13944 .await;
13945 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13946
13947 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13948 language_registry.add(Arc::new(Language::new(
13949 LanguageConfig {
13950 name: "Rust".into(),
13951 matcher: LanguageMatcher {
13952 path_suffixes: vec!["rs".to_string()],
13953 ..Default::default()
13954 },
13955 brackets: BracketPairConfig {
13956 pairs: vec![BracketPair {
13957 start: "{".to_string(),
13958 end: "}".to_string(),
13959 close: true,
13960 surround: true,
13961 newline: true,
13962 }],
13963 disabled_scopes_by_bracket_ix: Vec::new(),
13964 },
13965 ..Default::default()
13966 },
13967 Some(tree_sitter_rust::LANGUAGE.into()),
13968 )));
13969 let mut fake_servers = language_registry.register_fake_lsp(
13970 "Rust",
13971 FakeLspAdapter {
13972 capabilities: lsp::ServerCapabilities {
13973 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
13974 first_trigger_character: "{".to_string(),
13975 more_trigger_character: None,
13976 }),
13977 ..Default::default()
13978 },
13979 ..Default::default()
13980 },
13981 );
13982
13983 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13984
13985 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13986
13987 let worktree_id = workspace
13988 .update(cx, |workspace, _, cx| {
13989 workspace.project().update(cx, |project, cx| {
13990 project.worktrees(cx).next().unwrap().read(cx).id()
13991 })
13992 })
13993 .unwrap();
13994
13995 let buffer = project
13996 .update(cx, |project, cx| {
13997 project.open_local_buffer(path!("/a/main.rs"), cx)
13998 })
13999 .await
14000 .unwrap();
14001 let editor_handle = workspace
14002 .update(cx, |workspace, window, cx| {
14003 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
14004 })
14005 .unwrap()
14006 .await
14007 .unwrap()
14008 .downcast::<Editor>()
14009 .unwrap();
14010
14011 cx.executor().start_waiting();
14012 let fake_server = fake_servers.next().await.unwrap();
14013
14014 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
14015 |params, _| async move {
14016 assert_eq!(
14017 params.text_document_position.text_document.uri,
14018 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
14019 );
14020 assert_eq!(
14021 params.text_document_position.position,
14022 lsp::Position::new(0, 21),
14023 );
14024
14025 Ok(Some(vec![lsp::TextEdit {
14026 new_text: "]".to_string(),
14027 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14028 }]))
14029 },
14030 );
14031
14032 editor_handle.update_in(cx, |editor, window, cx| {
14033 window.focus(&editor.focus_handle(cx));
14034 editor.change_selections(None, window, cx, |s| {
14035 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
14036 });
14037 editor.handle_input("{", window, cx);
14038 });
14039
14040 cx.executor().run_until_parked();
14041
14042 buffer.update(cx, |buffer, _| {
14043 assert_eq!(
14044 buffer.text(),
14045 "fn main() { let a = {5}; }",
14046 "No extra braces from on type formatting should appear in the buffer"
14047 )
14048 });
14049}
14050
14051#[gpui::test]
14052async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
14053 init_test(cx, |_| {});
14054
14055 let fs = FakeFs::new(cx.executor());
14056 fs.insert_tree(
14057 path!("/a"),
14058 json!({
14059 "main.rs": "fn main() { let a = 5; }",
14060 "other.rs": "// Test file",
14061 }),
14062 )
14063 .await;
14064
14065 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14066
14067 let server_restarts = Arc::new(AtomicUsize::new(0));
14068 let closure_restarts = Arc::clone(&server_restarts);
14069 let language_server_name = "test language server";
14070 let language_name: LanguageName = "Rust".into();
14071
14072 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14073 language_registry.add(Arc::new(Language::new(
14074 LanguageConfig {
14075 name: language_name.clone(),
14076 matcher: LanguageMatcher {
14077 path_suffixes: vec!["rs".to_string()],
14078 ..Default::default()
14079 },
14080 ..Default::default()
14081 },
14082 Some(tree_sitter_rust::LANGUAGE.into()),
14083 )));
14084 let mut fake_servers = language_registry.register_fake_lsp(
14085 "Rust",
14086 FakeLspAdapter {
14087 name: language_server_name,
14088 initialization_options: Some(json!({
14089 "testOptionValue": true
14090 })),
14091 initializer: Some(Box::new(move |fake_server| {
14092 let task_restarts = Arc::clone(&closure_restarts);
14093 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
14094 task_restarts.fetch_add(1, atomic::Ordering::Release);
14095 futures::future::ready(Ok(()))
14096 });
14097 })),
14098 ..Default::default()
14099 },
14100 );
14101
14102 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14103 let _buffer = project
14104 .update(cx, |project, cx| {
14105 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
14106 })
14107 .await
14108 .unwrap();
14109 let _fake_server = fake_servers.next().await.unwrap();
14110 update_test_language_settings(cx, |language_settings| {
14111 language_settings.languages.insert(
14112 language_name.clone(),
14113 LanguageSettingsContent {
14114 tab_size: NonZeroU32::new(8),
14115 ..Default::default()
14116 },
14117 );
14118 });
14119 cx.executor().run_until_parked();
14120 assert_eq!(
14121 server_restarts.load(atomic::Ordering::Acquire),
14122 0,
14123 "Should not restart LSP server on an unrelated change"
14124 );
14125
14126 update_test_project_settings(cx, |project_settings| {
14127 project_settings.lsp.insert(
14128 "Some other server name".into(),
14129 LspSettings {
14130 binary: None,
14131 settings: None,
14132 initialization_options: Some(json!({
14133 "some other init value": false
14134 })),
14135 enable_lsp_tasks: false,
14136 },
14137 );
14138 });
14139 cx.executor().run_until_parked();
14140 assert_eq!(
14141 server_restarts.load(atomic::Ordering::Acquire),
14142 0,
14143 "Should not restart LSP server on an unrelated LSP settings change"
14144 );
14145
14146 update_test_project_settings(cx, |project_settings| {
14147 project_settings.lsp.insert(
14148 language_server_name.into(),
14149 LspSettings {
14150 binary: None,
14151 settings: None,
14152 initialization_options: Some(json!({
14153 "anotherInitValue": false
14154 })),
14155 enable_lsp_tasks: false,
14156 },
14157 );
14158 });
14159 cx.executor().run_until_parked();
14160 assert_eq!(
14161 server_restarts.load(atomic::Ordering::Acquire),
14162 1,
14163 "Should restart LSP server on a related LSP settings change"
14164 );
14165
14166 update_test_project_settings(cx, |project_settings| {
14167 project_settings.lsp.insert(
14168 language_server_name.into(),
14169 LspSettings {
14170 binary: None,
14171 settings: None,
14172 initialization_options: Some(json!({
14173 "anotherInitValue": false
14174 })),
14175 enable_lsp_tasks: false,
14176 },
14177 );
14178 });
14179 cx.executor().run_until_parked();
14180 assert_eq!(
14181 server_restarts.load(atomic::Ordering::Acquire),
14182 1,
14183 "Should not restart LSP server on a related LSP settings change that is the same"
14184 );
14185
14186 update_test_project_settings(cx, |project_settings| {
14187 project_settings.lsp.insert(
14188 language_server_name.into(),
14189 LspSettings {
14190 binary: None,
14191 settings: None,
14192 initialization_options: None,
14193 enable_lsp_tasks: false,
14194 },
14195 );
14196 });
14197 cx.executor().run_until_parked();
14198 assert_eq!(
14199 server_restarts.load(atomic::Ordering::Acquire),
14200 2,
14201 "Should restart LSP server on another related LSP settings change"
14202 );
14203}
14204
14205#[gpui::test]
14206async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
14207 init_test(cx, |_| {});
14208
14209 let mut cx = EditorLspTestContext::new_rust(
14210 lsp::ServerCapabilities {
14211 completion_provider: Some(lsp::CompletionOptions {
14212 trigger_characters: Some(vec![".".to_string()]),
14213 resolve_provider: Some(true),
14214 ..Default::default()
14215 }),
14216 ..Default::default()
14217 },
14218 cx,
14219 )
14220 .await;
14221
14222 cx.set_state("fn main() { let a = 2ˇ; }");
14223 cx.simulate_keystroke(".");
14224 let completion_item = lsp::CompletionItem {
14225 label: "some".into(),
14226 kind: Some(lsp::CompletionItemKind::SNIPPET),
14227 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
14228 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
14229 kind: lsp::MarkupKind::Markdown,
14230 value: "```rust\nSome(2)\n```".to_string(),
14231 })),
14232 deprecated: Some(false),
14233 sort_text: Some("fffffff2".to_string()),
14234 filter_text: Some("some".to_string()),
14235 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
14236 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14237 range: lsp::Range {
14238 start: lsp::Position {
14239 line: 0,
14240 character: 22,
14241 },
14242 end: lsp::Position {
14243 line: 0,
14244 character: 22,
14245 },
14246 },
14247 new_text: "Some(2)".to_string(),
14248 })),
14249 additional_text_edits: Some(vec![lsp::TextEdit {
14250 range: lsp::Range {
14251 start: lsp::Position {
14252 line: 0,
14253 character: 20,
14254 },
14255 end: lsp::Position {
14256 line: 0,
14257 character: 22,
14258 },
14259 },
14260 new_text: "".to_string(),
14261 }]),
14262 ..Default::default()
14263 };
14264
14265 let closure_completion_item = completion_item.clone();
14266 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14267 let task_completion_item = closure_completion_item.clone();
14268 async move {
14269 Ok(Some(lsp::CompletionResponse::Array(vec![
14270 task_completion_item,
14271 ])))
14272 }
14273 });
14274
14275 request.next().await;
14276
14277 cx.condition(|editor, _| editor.context_menu_visible())
14278 .await;
14279 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14280 editor
14281 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14282 .unwrap()
14283 });
14284 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
14285
14286 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
14287 let task_completion_item = completion_item.clone();
14288 async move { Ok(task_completion_item) }
14289 })
14290 .next()
14291 .await
14292 .unwrap();
14293 apply_additional_edits.await.unwrap();
14294 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
14295}
14296
14297#[gpui::test]
14298async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
14299 init_test(cx, |_| {});
14300
14301 let mut cx = EditorLspTestContext::new_rust(
14302 lsp::ServerCapabilities {
14303 completion_provider: Some(lsp::CompletionOptions {
14304 trigger_characters: Some(vec![".".to_string()]),
14305 resolve_provider: Some(true),
14306 ..Default::default()
14307 }),
14308 ..Default::default()
14309 },
14310 cx,
14311 )
14312 .await;
14313
14314 cx.set_state("fn main() { let a = 2ˇ; }");
14315 cx.simulate_keystroke(".");
14316
14317 let item1 = lsp::CompletionItem {
14318 label: "method id()".to_string(),
14319 filter_text: Some("id".to_string()),
14320 detail: None,
14321 documentation: None,
14322 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14323 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14324 new_text: ".id".to_string(),
14325 })),
14326 ..lsp::CompletionItem::default()
14327 };
14328
14329 let item2 = lsp::CompletionItem {
14330 label: "other".to_string(),
14331 filter_text: Some("other".to_string()),
14332 detail: None,
14333 documentation: None,
14334 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14335 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14336 new_text: ".other".to_string(),
14337 })),
14338 ..lsp::CompletionItem::default()
14339 };
14340
14341 let item1 = item1.clone();
14342 cx.set_request_handler::<lsp::request::Completion, _, _>({
14343 let item1 = item1.clone();
14344 move |_, _, _| {
14345 let item1 = item1.clone();
14346 let item2 = item2.clone();
14347 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
14348 }
14349 })
14350 .next()
14351 .await;
14352
14353 cx.condition(|editor, _| editor.context_menu_visible())
14354 .await;
14355 cx.update_editor(|editor, _, _| {
14356 let context_menu = editor.context_menu.borrow_mut();
14357 let context_menu = context_menu
14358 .as_ref()
14359 .expect("Should have the context menu deployed");
14360 match context_menu {
14361 CodeContextMenu::Completions(completions_menu) => {
14362 let completions = completions_menu.completions.borrow_mut();
14363 assert_eq!(
14364 completions
14365 .iter()
14366 .map(|completion| &completion.label.text)
14367 .collect::<Vec<_>>(),
14368 vec!["method id()", "other"]
14369 )
14370 }
14371 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14372 }
14373 });
14374
14375 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
14376 let item1 = item1.clone();
14377 move |_, item_to_resolve, _| {
14378 let item1 = item1.clone();
14379 async move {
14380 if item1 == item_to_resolve {
14381 Ok(lsp::CompletionItem {
14382 label: "method id()".to_string(),
14383 filter_text: Some("id".to_string()),
14384 detail: Some("Now resolved!".to_string()),
14385 documentation: Some(lsp::Documentation::String("Docs".to_string())),
14386 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14387 range: lsp::Range::new(
14388 lsp::Position::new(0, 22),
14389 lsp::Position::new(0, 22),
14390 ),
14391 new_text: ".id".to_string(),
14392 })),
14393 ..lsp::CompletionItem::default()
14394 })
14395 } else {
14396 Ok(item_to_resolve)
14397 }
14398 }
14399 }
14400 })
14401 .next()
14402 .await
14403 .unwrap();
14404 cx.run_until_parked();
14405
14406 cx.update_editor(|editor, window, cx| {
14407 editor.context_menu_next(&Default::default(), window, cx);
14408 });
14409
14410 cx.update_editor(|editor, _, _| {
14411 let context_menu = editor.context_menu.borrow_mut();
14412 let context_menu = context_menu
14413 .as_ref()
14414 .expect("Should have the context menu deployed");
14415 match context_menu {
14416 CodeContextMenu::Completions(completions_menu) => {
14417 let completions = completions_menu.completions.borrow_mut();
14418 assert_eq!(
14419 completions
14420 .iter()
14421 .map(|completion| &completion.label.text)
14422 .collect::<Vec<_>>(),
14423 vec!["method id() Now resolved!", "other"],
14424 "Should update first completion label, but not second as the filter text did not match."
14425 );
14426 }
14427 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14428 }
14429 });
14430}
14431
14432#[gpui::test]
14433async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
14434 init_test(cx, |_| {});
14435 let mut cx = EditorLspTestContext::new_rust(
14436 lsp::ServerCapabilities {
14437 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
14438 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
14439 completion_provider: Some(lsp::CompletionOptions {
14440 resolve_provider: Some(true),
14441 ..Default::default()
14442 }),
14443 ..Default::default()
14444 },
14445 cx,
14446 )
14447 .await;
14448 cx.set_state(indoc! {"
14449 struct TestStruct {
14450 field: i32
14451 }
14452
14453 fn mainˇ() {
14454 let unused_var = 42;
14455 let test_struct = TestStruct { field: 42 };
14456 }
14457 "});
14458 let symbol_range = cx.lsp_range(indoc! {"
14459 struct TestStruct {
14460 field: i32
14461 }
14462
14463 «fn main»() {
14464 let unused_var = 42;
14465 let test_struct = TestStruct { field: 42 };
14466 }
14467 "});
14468 let mut hover_requests =
14469 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
14470 Ok(Some(lsp::Hover {
14471 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
14472 kind: lsp::MarkupKind::Markdown,
14473 value: "Function documentation".to_string(),
14474 }),
14475 range: Some(symbol_range),
14476 }))
14477 });
14478
14479 // Case 1: Test that code action menu hide hover popover
14480 cx.dispatch_action(Hover);
14481 hover_requests.next().await;
14482 cx.condition(|editor, _| editor.hover_state.visible()).await;
14483 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
14484 move |_, _, _| async move {
14485 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
14486 lsp::CodeAction {
14487 title: "Remove unused variable".to_string(),
14488 kind: Some(CodeActionKind::QUICKFIX),
14489 edit: Some(lsp::WorkspaceEdit {
14490 changes: Some(
14491 [(
14492 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
14493 vec![lsp::TextEdit {
14494 range: lsp::Range::new(
14495 lsp::Position::new(5, 4),
14496 lsp::Position::new(5, 27),
14497 ),
14498 new_text: "".to_string(),
14499 }],
14500 )]
14501 .into_iter()
14502 .collect(),
14503 ),
14504 ..Default::default()
14505 }),
14506 ..Default::default()
14507 },
14508 )]))
14509 },
14510 );
14511 cx.update_editor(|editor, window, cx| {
14512 editor.toggle_code_actions(
14513 &ToggleCodeActions {
14514 deployed_from: None,
14515 quick_launch: false,
14516 },
14517 window,
14518 cx,
14519 );
14520 });
14521 code_action_requests.next().await;
14522 cx.run_until_parked();
14523 cx.condition(|editor, _| editor.context_menu_visible())
14524 .await;
14525 cx.update_editor(|editor, _, _| {
14526 assert!(
14527 !editor.hover_state.visible(),
14528 "Hover popover should be hidden when code action menu is shown"
14529 );
14530 // Hide code actions
14531 editor.context_menu.take();
14532 });
14533
14534 // Case 2: Test that code completions hide hover popover
14535 cx.dispatch_action(Hover);
14536 hover_requests.next().await;
14537 cx.condition(|editor, _| editor.hover_state.visible()).await;
14538 let counter = Arc::new(AtomicUsize::new(0));
14539 let mut completion_requests =
14540 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14541 let counter = counter.clone();
14542 async move {
14543 counter.fetch_add(1, atomic::Ordering::Release);
14544 Ok(Some(lsp::CompletionResponse::Array(vec![
14545 lsp::CompletionItem {
14546 label: "main".into(),
14547 kind: Some(lsp::CompletionItemKind::FUNCTION),
14548 detail: Some("() -> ()".to_string()),
14549 ..Default::default()
14550 },
14551 lsp::CompletionItem {
14552 label: "TestStruct".into(),
14553 kind: Some(lsp::CompletionItemKind::STRUCT),
14554 detail: Some("struct TestStruct".to_string()),
14555 ..Default::default()
14556 },
14557 ])))
14558 }
14559 });
14560 cx.update_editor(|editor, window, cx| {
14561 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14562 });
14563 completion_requests.next().await;
14564 cx.condition(|editor, _| editor.context_menu_visible())
14565 .await;
14566 cx.update_editor(|editor, _, _| {
14567 assert!(
14568 !editor.hover_state.visible(),
14569 "Hover popover should be hidden when completion menu is shown"
14570 );
14571 });
14572}
14573
14574#[gpui::test]
14575async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
14576 init_test(cx, |_| {});
14577
14578 let mut cx = EditorLspTestContext::new_rust(
14579 lsp::ServerCapabilities {
14580 completion_provider: Some(lsp::CompletionOptions {
14581 trigger_characters: Some(vec![".".to_string()]),
14582 resolve_provider: Some(true),
14583 ..Default::default()
14584 }),
14585 ..Default::default()
14586 },
14587 cx,
14588 )
14589 .await;
14590
14591 cx.set_state("fn main() { let a = 2ˇ; }");
14592 cx.simulate_keystroke(".");
14593
14594 let unresolved_item_1 = lsp::CompletionItem {
14595 label: "id".to_string(),
14596 filter_text: Some("id".to_string()),
14597 detail: None,
14598 documentation: None,
14599 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14600 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14601 new_text: ".id".to_string(),
14602 })),
14603 ..lsp::CompletionItem::default()
14604 };
14605 let resolved_item_1 = lsp::CompletionItem {
14606 additional_text_edits: Some(vec![lsp::TextEdit {
14607 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
14608 new_text: "!!".to_string(),
14609 }]),
14610 ..unresolved_item_1.clone()
14611 };
14612 let unresolved_item_2 = lsp::CompletionItem {
14613 label: "other".to_string(),
14614 filter_text: Some("other".to_string()),
14615 detail: None,
14616 documentation: None,
14617 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14618 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14619 new_text: ".other".to_string(),
14620 })),
14621 ..lsp::CompletionItem::default()
14622 };
14623 let resolved_item_2 = lsp::CompletionItem {
14624 additional_text_edits: Some(vec![lsp::TextEdit {
14625 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
14626 new_text: "??".to_string(),
14627 }]),
14628 ..unresolved_item_2.clone()
14629 };
14630
14631 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
14632 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
14633 cx.lsp
14634 .server
14635 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
14636 let unresolved_item_1 = unresolved_item_1.clone();
14637 let resolved_item_1 = resolved_item_1.clone();
14638 let unresolved_item_2 = unresolved_item_2.clone();
14639 let resolved_item_2 = resolved_item_2.clone();
14640 let resolve_requests_1 = resolve_requests_1.clone();
14641 let resolve_requests_2 = resolve_requests_2.clone();
14642 move |unresolved_request, _| {
14643 let unresolved_item_1 = unresolved_item_1.clone();
14644 let resolved_item_1 = resolved_item_1.clone();
14645 let unresolved_item_2 = unresolved_item_2.clone();
14646 let resolved_item_2 = resolved_item_2.clone();
14647 let resolve_requests_1 = resolve_requests_1.clone();
14648 let resolve_requests_2 = resolve_requests_2.clone();
14649 async move {
14650 if unresolved_request == unresolved_item_1 {
14651 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
14652 Ok(resolved_item_1.clone())
14653 } else if unresolved_request == unresolved_item_2 {
14654 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
14655 Ok(resolved_item_2.clone())
14656 } else {
14657 panic!("Unexpected completion item {unresolved_request:?}")
14658 }
14659 }
14660 }
14661 })
14662 .detach();
14663
14664 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14665 let unresolved_item_1 = unresolved_item_1.clone();
14666 let unresolved_item_2 = unresolved_item_2.clone();
14667 async move {
14668 Ok(Some(lsp::CompletionResponse::Array(vec![
14669 unresolved_item_1,
14670 unresolved_item_2,
14671 ])))
14672 }
14673 })
14674 .next()
14675 .await;
14676
14677 cx.condition(|editor, _| editor.context_menu_visible())
14678 .await;
14679 cx.update_editor(|editor, _, _| {
14680 let context_menu = editor.context_menu.borrow_mut();
14681 let context_menu = context_menu
14682 .as_ref()
14683 .expect("Should have the context menu deployed");
14684 match context_menu {
14685 CodeContextMenu::Completions(completions_menu) => {
14686 let completions = completions_menu.completions.borrow_mut();
14687 assert_eq!(
14688 completions
14689 .iter()
14690 .map(|completion| &completion.label.text)
14691 .collect::<Vec<_>>(),
14692 vec!["id", "other"]
14693 )
14694 }
14695 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14696 }
14697 });
14698 cx.run_until_parked();
14699
14700 cx.update_editor(|editor, window, cx| {
14701 editor.context_menu_next(&ContextMenuNext, window, cx);
14702 });
14703 cx.run_until_parked();
14704 cx.update_editor(|editor, window, cx| {
14705 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
14706 });
14707 cx.run_until_parked();
14708 cx.update_editor(|editor, window, cx| {
14709 editor.context_menu_next(&ContextMenuNext, window, cx);
14710 });
14711 cx.run_until_parked();
14712 cx.update_editor(|editor, window, cx| {
14713 editor
14714 .compose_completion(&ComposeCompletion::default(), window, cx)
14715 .expect("No task returned")
14716 })
14717 .await
14718 .expect("Completion failed");
14719 cx.run_until_parked();
14720
14721 cx.update_editor(|editor, _, cx| {
14722 assert_eq!(
14723 resolve_requests_1.load(atomic::Ordering::Acquire),
14724 1,
14725 "Should always resolve once despite multiple selections"
14726 );
14727 assert_eq!(
14728 resolve_requests_2.load(atomic::Ordering::Acquire),
14729 1,
14730 "Should always resolve once after multiple selections and applying the completion"
14731 );
14732 assert_eq!(
14733 editor.text(cx),
14734 "fn main() { let a = ??.other; }",
14735 "Should use resolved data when applying the completion"
14736 );
14737 });
14738}
14739
14740#[gpui::test]
14741async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
14742 init_test(cx, |_| {});
14743
14744 let item_0 = lsp::CompletionItem {
14745 label: "abs".into(),
14746 insert_text: Some("abs".into()),
14747 data: Some(json!({ "very": "special"})),
14748 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
14749 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14750 lsp::InsertReplaceEdit {
14751 new_text: "abs".to_string(),
14752 insert: lsp::Range::default(),
14753 replace: lsp::Range::default(),
14754 },
14755 )),
14756 ..lsp::CompletionItem::default()
14757 };
14758 let items = iter::once(item_0.clone())
14759 .chain((11..51).map(|i| lsp::CompletionItem {
14760 label: format!("item_{}", i),
14761 insert_text: Some(format!("item_{}", i)),
14762 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
14763 ..lsp::CompletionItem::default()
14764 }))
14765 .collect::<Vec<_>>();
14766
14767 let default_commit_characters = vec!["?".to_string()];
14768 let default_data = json!({ "default": "data"});
14769 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
14770 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
14771 let default_edit_range = lsp::Range {
14772 start: lsp::Position {
14773 line: 0,
14774 character: 5,
14775 },
14776 end: lsp::Position {
14777 line: 0,
14778 character: 5,
14779 },
14780 };
14781
14782 let mut cx = EditorLspTestContext::new_rust(
14783 lsp::ServerCapabilities {
14784 completion_provider: Some(lsp::CompletionOptions {
14785 trigger_characters: Some(vec![".".to_string()]),
14786 resolve_provider: Some(true),
14787 ..Default::default()
14788 }),
14789 ..Default::default()
14790 },
14791 cx,
14792 )
14793 .await;
14794
14795 cx.set_state("fn main() { let a = 2ˇ; }");
14796 cx.simulate_keystroke(".");
14797
14798 let completion_data = default_data.clone();
14799 let completion_characters = default_commit_characters.clone();
14800 let completion_items = items.clone();
14801 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14802 let default_data = completion_data.clone();
14803 let default_commit_characters = completion_characters.clone();
14804 let items = completion_items.clone();
14805 async move {
14806 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14807 items,
14808 item_defaults: Some(lsp::CompletionListItemDefaults {
14809 data: Some(default_data.clone()),
14810 commit_characters: Some(default_commit_characters.clone()),
14811 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
14812 default_edit_range,
14813 )),
14814 insert_text_format: Some(default_insert_text_format),
14815 insert_text_mode: Some(default_insert_text_mode),
14816 }),
14817 ..lsp::CompletionList::default()
14818 })))
14819 }
14820 })
14821 .next()
14822 .await;
14823
14824 let resolved_items = Arc::new(Mutex::new(Vec::new()));
14825 cx.lsp
14826 .server
14827 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
14828 let closure_resolved_items = resolved_items.clone();
14829 move |item_to_resolve, _| {
14830 let closure_resolved_items = closure_resolved_items.clone();
14831 async move {
14832 closure_resolved_items.lock().push(item_to_resolve.clone());
14833 Ok(item_to_resolve)
14834 }
14835 }
14836 })
14837 .detach();
14838
14839 cx.condition(|editor, _| editor.context_menu_visible())
14840 .await;
14841 cx.run_until_parked();
14842 cx.update_editor(|editor, _, _| {
14843 let menu = editor.context_menu.borrow_mut();
14844 match menu.as_ref().expect("should have the completions menu") {
14845 CodeContextMenu::Completions(completions_menu) => {
14846 assert_eq!(
14847 completions_menu
14848 .entries
14849 .borrow()
14850 .iter()
14851 .map(|mat| mat.string.clone())
14852 .collect::<Vec<String>>(),
14853 items
14854 .iter()
14855 .map(|completion| completion.label.clone())
14856 .collect::<Vec<String>>()
14857 );
14858 }
14859 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
14860 }
14861 });
14862 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
14863 // with 4 from the end.
14864 assert_eq!(
14865 *resolved_items.lock(),
14866 [&items[0..16], &items[items.len() - 4..items.len()]]
14867 .concat()
14868 .iter()
14869 .cloned()
14870 .map(|mut item| {
14871 if item.data.is_none() {
14872 item.data = Some(default_data.clone());
14873 }
14874 item
14875 })
14876 .collect::<Vec<lsp::CompletionItem>>(),
14877 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
14878 );
14879 resolved_items.lock().clear();
14880
14881 cx.update_editor(|editor, window, cx| {
14882 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
14883 });
14884 cx.run_until_parked();
14885 // Completions that have already been resolved are skipped.
14886 assert_eq!(
14887 *resolved_items.lock(),
14888 items[items.len() - 16..items.len() - 4]
14889 .iter()
14890 .cloned()
14891 .map(|mut item| {
14892 if item.data.is_none() {
14893 item.data = Some(default_data.clone());
14894 }
14895 item
14896 })
14897 .collect::<Vec<lsp::CompletionItem>>()
14898 );
14899 resolved_items.lock().clear();
14900}
14901
14902#[gpui::test]
14903async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
14904 init_test(cx, |_| {});
14905
14906 let mut cx = EditorLspTestContext::new(
14907 Language::new(
14908 LanguageConfig {
14909 matcher: LanguageMatcher {
14910 path_suffixes: vec!["jsx".into()],
14911 ..Default::default()
14912 },
14913 overrides: [(
14914 "element".into(),
14915 LanguageConfigOverride {
14916 completion_query_characters: Override::Set(['-'].into_iter().collect()),
14917 ..Default::default()
14918 },
14919 )]
14920 .into_iter()
14921 .collect(),
14922 ..Default::default()
14923 },
14924 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
14925 )
14926 .with_override_query("(jsx_self_closing_element) @element")
14927 .unwrap(),
14928 lsp::ServerCapabilities {
14929 completion_provider: Some(lsp::CompletionOptions {
14930 trigger_characters: Some(vec![":".to_string()]),
14931 ..Default::default()
14932 }),
14933 ..Default::default()
14934 },
14935 cx,
14936 )
14937 .await;
14938
14939 cx.lsp
14940 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14941 Ok(Some(lsp::CompletionResponse::Array(vec![
14942 lsp::CompletionItem {
14943 label: "bg-blue".into(),
14944 ..Default::default()
14945 },
14946 lsp::CompletionItem {
14947 label: "bg-red".into(),
14948 ..Default::default()
14949 },
14950 lsp::CompletionItem {
14951 label: "bg-yellow".into(),
14952 ..Default::default()
14953 },
14954 ])))
14955 });
14956
14957 cx.set_state(r#"<p class="bgˇ" />"#);
14958
14959 // Trigger completion when typing a dash, because the dash is an extra
14960 // word character in the 'element' scope, which contains the cursor.
14961 cx.simulate_keystroke("-");
14962 cx.executor().run_until_parked();
14963 cx.update_editor(|editor, _, _| {
14964 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14965 {
14966 assert_eq!(
14967 completion_menu_entries(&menu),
14968 &["bg-red", "bg-blue", "bg-yellow"]
14969 );
14970 } else {
14971 panic!("expected completion menu to be open");
14972 }
14973 });
14974
14975 cx.simulate_keystroke("l");
14976 cx.executor().run_until_parked();
14977 cx.update_editor(|editor, _, _| {
14978 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14979 {
14980 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
14981 } else {
14982 panic!("expected completion menu to be open");
14983 }
14984 });
14985
14986 // When filtering completions, consider the character after the '-' to
14987 // be the start of a subword.
14988 cx.set_state(r#"<p class="yelˇ" />"#);
14989 cx.simulate_keystroke("l");
14990 cx.executor().run_until_parked();
14991 cx.update_editor(|editor, _, _| {
14992 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14993 {
14994 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
14995 } else {
14996 panic!("expected completion menu to be open");
14997 }
14998 });
14999}
15000
15001fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
15002 let entries = menu.entries.borrow();
15003 entries.iter().map(|mat| mat.string.clone()).collect()
15004}
15005
15006#[gpui::test]
15007async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
15008 init_test(cx, |settings| {
15009 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
15010 FormatterList(vec![Formatter::Prettier].into()),
15011 ))
15012 });
15013
15014 let fs = FakeFs::new(cx.executor());
15015 fs.insert_file(path!("/file.ts"), Default::default()).await;
15016
15017 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
15018 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15019
15020 language_registry.add(Arc::new(Language::new(
15021 LanguageConfig {
15022 name: "TypeScript".into(),
15023 matcher: LanguageMatcher {
15024 path_suffixes: vec!["ts".to_string()],
15025 ..Default::default()
15026 },
15027 ..Default::default()
15028 },
15029 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
15030 )));
15031 update_test_language_settings(cx, |settings| {
15032 settings.defaults.prettier = Some(PrettierSettings {
15033 allowed: true,
15034 ..PrettierSettings::default()
15035 });
15036 });
15037
15038 let test_plugin = "test_plugin";
15039 let _ = language_registry.register_fake_lsp(
15040 "TypeScript",
15041 FakeLspAdapter {
15042 prettier_plugins: vec![test_plugin],
15043 ..Default::default()
15044 },
15045 );
15046
15047 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
15048 let buffer = project
15049 .update(cx, |project, cx| {
15050 project.open_local_buffer(path!("/file.ts"), cx)
15051 })
15052 .await
15053 .unwrap();
15054
15055 let buffer_text = "one\ntwo\nthree\n";
15056 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
15057 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
15058 editor.update_in(cx, |editor, window, cx| {
15059 editor.set_text(buffer_text, window, cx)
15060 });
15061
15062 editor
15063 .update_in(cx, |editor, window, cx| {
15064 editor.perform_format(
15065 project.clone(),
15066 FormatTrigger::Manual,
15067 FormatTarget::Buffers,
15068 window,
15069 cx,
15070 )
15071 })
15072 .unwrap()
15073 .await;
15074 assert_eq!(
15075 editor.update(cx, |editor, cx| editor.text(cx)),
15076 buffer_text.to_string() + prettier_format_suffix,
15077 "Test prettier formatting was not applied to the original buffer text",
15078 );
15079
15080 update_test_language_settings(cx, |settings| {
15081 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
15082 });
15083 let format = editor.update_in(cx, |editor, window, cx| {
15084 editor.perform_format(
15085 project.clone(),
15086 FormatTrigger::Manual,
15087 FormatTarget::Buffers,
15088 window,
15089 cx,
15090 )
15091 });
15092 format.await.unwrap();
15093 assert_eq!(
15094 editor.update(cx, |editor, cx| editor.text(cx)),
15095 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
15096 "Autoformatting (via test prettier) was not applied to the original buffer text",
15097 );
15098}
15099
15100#[gpui::test]
15101async fn test_addition_reverts(cx: &mut TestAppContext) {
15102 init_test(cx, |_| {});
15103 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15104 let base_text = indoc! {r#"
15105 struct Row;
15106 struct Row1;
15107 struct Row2;
15108
15109 struct Row4;
15110 struct Row5;
15111 struct Row6;
15112
15113 struct Row8;
15114 struct Row9;
15115 struct Row10;"#};
15116
15117 // When addition hunks are not adjacent to carets, no hunk revert is performed
15118 assert_hunk_revert(
15119 indoc! {r#"struct Row;
15120 struct Row1;
15121 struct Row1.1;
15122 struct Row1.2;
15123 struct Row2;ˇ
15124
15125 struct Row4;
15126 struct Row5;
15127 struct Row6;
15128
15129 struct Row8;
15130 ˇstruct Row9;
15131 struct Row9.1;
15132 struct Row9.2;
15133 struct Row9.3;
15134 struct Row10;"#},
15135 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
15136 indoc! {r#"struct Row;
15137 struct Row1;
15138 struct Row1.1;
15139 struct Row1.2;
15140 struct Row2;ˇ
15141
15142 struct Row4;
15143 struct Row5;
15144 struct Row6;
15145
15146 struct Row8;
15147 ˇstruct Row9;
15148 struct Row9.1;
15149 struct Row9.2;
15150 struct Row9.3;
15151 struct Row10;"#},
15152 base_text,
15153 &mut cx,
15154 );
15155 // Same for selections
15156 assert_hunk_revert(
15157 indoc! {r#"struct Row;
15158 struct Row1;
15159 struct Row2;
15160 struct Row2.1;
15161 struct Row2.2;
15162 «ˇ
15163 struct Row4;
15164 struct» Row5;
15165 «struct Row6;
15166 ˇ»
15167 struct Row9.1;
15168 struct Row9.2;
15169 struct Row9.3;
15170 struct Row8;
15171 struct Row9;
15172 struct Row10;"#},
15173 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
15174 indoc! {r#"struct Row;
15175 struct Row1;
15176 struct Row2;
15177 struct Row2.1;
15178 struct Row2.2;
15179 «ˇ
15180 struct Row4;
15181 struct» Row5;
15182 «struct Row6;
15183 ˇ»
15184 struct Row9.1;
15185 struct Row9.2;
15186 struct Row9.3;
15187 struct Row8;
15188 struct Row9;
15189 struct Row10;"#},
15190 base_text,
15191 &mut cx,
15192 );
15193
15194 // When carets and selections intersect the addition hunks, those are reverted.
15195 // Adjacent carets got merged.
15196 assert_hunk_revert(
15197 indoc! {r#"struct Row;
15198 ˇ// something on the top
15199 struct Row1;
15200 struct Row2;
15201 struct Roˇw3.1;
15202 struct Row2.2;
15203 struct Row2.3;ˇ
15204
15205 struct Row4;
15206 struct ˇRow5.1;
15207 struct Row5.2;
15208 struct «Rowˇ»5.3;
15209 struct Row5;
15210 struct Row6;
15211 ˇ
15212 struct Row9.1;
15213 struct «Rowˇ»9.2;
15214 struct «ˇRow»9.3;
15215 struct Row8;
15216 struct Row9;
15217 «ˇ// something on bottom»
15218 struct Row10;"#},
15219 vec![
15220 DiffHunkStatusKind::Added,
15221 DiffHunkStatusKind::Added,
15222 DiffHunkStatusKind::Added,
15223 DiffHunkStatusKind::Added,
15224 DiffHunkStatusKind::Added,
15225 ],
15226 indoc! {r#"struct Row;
15227 ˇstruct Row1;
15228 struct Row2;
15229 ˇ
15230 struct Row4;
15231 ˇstruct Row5;
15232 struct Row6;
15233 ˇ
15234 ˇstruct Row8;
15235 struct Row9;
15236 ˇstruct Row10;"#},
15237 base_text,
15238 &mut cx,
15239 );
15240}
15241
15242#[gpui::test]
15243async fn test_modification_reverts(cx: &mut TestAppContext) {
15244 init_test(cx, |_| {});
15245 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15246 let base_text = indoc! {r#"
15247 struct Row;
15248 struct Row1;
15249 struct Row2;
15250
15251 struct Row4;
15252 struct Row5;
15253 struct Row6;
15254
15255 struct Row8;
15256 struct Row9;
15257 struct Row10;"#};
15258
15259 // Modification hunks behave the same as the addition ones.
15260 assert_hunk_revert(
15261 indoc! {r#"struct Row;
15262 struct Row1;
15263 struct Row33;
15264 ˇ
15265 struct Row4;
15266 struct Row5;
15267 struct Row6;
15268 ˇ
15269 struct Row99;
15270 struct Row9;
15271 struct Row10;"#},
15272 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
15273 indoc! {r#"struct Row;
15274 struct Row1;
15275 struct Row33;
15276 ˇ
15277 struct Row4;
15278 struct Row5;
15279 struct Row6;
15280 ˇ
15281 struct Row99;
15282 struct Row9;
15283 struct Row10;"#},
15284 base_text,
15285 &mut cx,
15286 );
15287 assert_hunk_revert(
15288 indoc! {r#"struct Row;
15289 struct Row1;
15290 struct Row33;
15291 «ˇ
15292 struct Row4;
15293 struct» Row5;
15294 «struct Row6;
15295 ˇ»
15296 struct Row99;
15297 struct Row9;
15298 struct Row10;"#},
15299 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
15300 indoc! {r#"struct Row;
15301 struct Row1;
15302 struct Row33;
15303 «ˇ
15304 struct Row4;
15305 struct» Row5;
15306 «struct Row6;
15307 ˇ»
15308 struct Row99;
15309 struct Row9;
15310 struct Row10;"#},
15311 base_text,
15312 &mut cx,
15313 );
15314
15315 assert_hunk_revert(
15316 indoc! {r#"ˇstruct Row1.1;
15317 struct Row1;
15318 «ˇstr»uct Row22;
15319
15320 struct ˇRow44;
15321 struct Row5;
15322 struct «Rˇ»ow66;ˇ
15323
15324 «struˇ»ct Row88;
15325 struct Row9;
15326 struct Row1011;ˇ"#},
15327 vec![
15328 DiffHunkStatusKind::Modified,
15329 DiffHunkStatusKind::Modified,
15330 DiffHunkStatusKind::Modified,
15331 DiffHunkStatusKind::Modified,
15332 DiffHunkStatusKind::Modified,
15333 DiffHunkStatusKind::Modified,
15334 ],
15335 indoc! {r#"struct Row;
15336 ˇstruct Row1;
15337 struct Row2;
15338 ˇ
15339 struct Row4;
15340 ˇstruct Row5;
15341 struct Row6;
15342 ˇ
15343 struct Row8;
15344 ˇstruct Row9;
15345 struct Row10;ˇ"#},
15346 base_text,
15347 &mut cx,
15348 );
15349}
15350
15351#[gpui::test]
15352async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
15353 init_test(cx, |_| {});
15354 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15355 let base_text = indoc! {r#"
15356 one
15357
15358 two
15359 three
15360 "#};
15361
15362 cx.set_head_text(base_text);
15363 cx.set_state("\nˇ\n");
15364 cx.executor().run_until_parked();
15365 cx.update_editor(|editor, _window, cx| {
15366 editor.expand_selected_diff_hunks(cx);
15367 });
15368 cx.executor().run_until_parked();
15369 cx.update_editor(|editor, window, cx| {
15370 editor.backspace(&Default::default(), window, cx);
15371 });
15372 cx.run_until_parked();
15373 cx.assert_state_with_diff(
15374 indoc! {r#"
15375
15376 - two
15377 - threeˇ
15378 +
15379 "#}
15380 .to_string(),
15381 );
15382}
15383
15384#[gpui::test]
15385async fn test_deletion_reverts(cx: &mut TestAppContext) {
15386 init_test(cx, |_| {});
15387 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15388 let base_text = indoc! {r#"struct Row;
15389struct Row1;
15390struct Row2;
15391
15392struct Row4;
15393struct Row5;
15394struct Row6;
15395
15396struct Row8;
15397struct Row9;
15398struct Row10;"#};
15399
15400 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
15401 assert_hunk_revert(
15402 indoc! {r#"struct Row;
15403 struct Row2;
15404
15405 ˇstruct Row4;
15406 struct Row5;
15407 struct Row6;
15408 ˇ
15409 struct Row8;
15410 struct Row10;"#},
15411 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15412 indoc! {r#"struct Row;
15413 struct Row2;
15414
15415 ˇstruct Row4;
15416 struct Row5;
15417 struct Row6;
15418 ˇ
15419 struct Row8;
15420 struct Row10;"#},
15421 base_text,
15422 &mut cx,
15423 );
15424 assert_hunk_revert(
15425 indoc! {r#"struct Row;
15426 struct Row2;
15427
15428 «ˇstruct Row4;
15429 struct» Row5;
15430 «struct Row6;
15431 ˇ»
15432 struct Row8;
15433 struct Row10;"#},
15434 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15435 indoc! {r#"struct Row;
15436 struct Row2;
15437
15438 «ˇstruct Row4;
15439 struct» Row5;
15440 «struct Row6;
15441 ˇ»
15442 struct Row8;
15443 struct Row10;"#},
15444 base_text,
15445 &mut cx,
15446 );
15447
15448 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
15449 assert_hunk_revert(
15450 indoc! {r#"struct Row;
15451 ˇstruct Row2;
15452
15453 struct Row4;
15454 struct Row5;
15455 struct Row6;
15456
15457 struct Row8;ˇ
15458 struct Row10;"#},
15459 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15460 indoc! {r#"struct Row;
15461 struct Row1;
15462 ˇstruct Row2;
15463
15464 struct Row4;
15465 struct Row5;
15466 struct Row6;
15467
15468 struct Row8;ˇ
15469 struct Row9;
15470 struct Row10;"#},
15471 base_text,
15472 &mut cx,
15473 );
15474 assert_hunk_revert(
15475 indoc! {r#"struct Row;
15476 struct Row2«ˇ;
15477 struct Row4;
15478 struct» Row5;
15479 «struct Row6;
15480
15481 struct Row8;ˇ»
15482 struct Row10;"#},
15483 vec![
15484 DiffHunkStatusKind::Deleted,
15485 DiffHunkStatusKind::Deleted,
15486 DiffHunkStatusKind::Deleted,
15487 ],
15488 indoc! {r#"struct Row;
15489 struct Row1;
15490 struct Row2«ˇ;
15491
15492 struct Row4;
15493 struct» Row5;
15494 «struct Row6;
15495
15496 struct Row8;ˇ»
15497 struct Row9;
15498 struct Row10;"#},
15499 base_text,
15500 &mut cx,
15501 );
15502}
15503
15504#[gpui::test]
15505async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
15506 init_test(cx, |_| {});
15507
15508 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
15509 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
15510 let base_text_3 =
15511 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
15512
15513 let text_1 = edit_first_char_of_every_line(base_text_1);
15514 let text_2 = edit_first_char_of_every_line(base_text_2);
15515 let text_3 = edit_first_char_of_every_line(base_text_3);
15516
15517 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
15518 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
15519 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
15520
15521 let multibuffer = cx.new(|cx| {
15522 let mut multibuffer = MultiBuffer::new(ReadWrite);
15523 multibuffer.push_excerpts(
15524 buffer_1.clone(),
15525 [
15526 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15527 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15528 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15529 ],
15530 cx,
15531 );
15532 multibuffer.push_excerpts(
15533 buffer_2.clone(),
15534 [
15535 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15536 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15537 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15538 ],
15539 cx,
15540 );
15541 multibuffer.push_excerpts(
15542 buffer_3.clone(),
15543 [
15544 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15545 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15546 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15547 ],
15548 cx,
15549 );
15550 multibuffer
15551 });
15552
15553 let fs = FakeFs::new(cx.executor());
15554 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
15555 let (editor, cx) = cx
15556 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
15557 editor.update_in(cx, |editor, _window, cx| {
15558 for (buffer, diff_base) in [
15559 (buffer_1.clone(), base_text_1),
15560 (buffer_2.clone(), base_text_2),
15561 (buffer_3.clone(), base_text_3),
15562 ] {
15563 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
15564 editor
15565 .buffer
15566 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
15567 }
15568 });
15569 cx.executor().run_until_parked();
15570
15571 editor.update_in(cx, |editor, window, cx| {
15572 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}");
15573 editor.select_all(&SelectAll, window, cx);
15574 editor.git_restore(&Default::default(), window, cx);
15575 });
15576 cx.executor().run_until_parked();
15577
15578 // When all ranges are selected, all buffer hunks are reverted.
15579 editor.update(cx, |editor, cx| {
15580 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");
15581 });
15582 buffer_1.update(cx, |buffer, _| {
15583 assert_eq!(buffer.text(), base_text_1);
15584 });
15585 buffer_2.update(cx, |buffer, _| {
15586 assert_eq!(buffer.text(), base_text_2);
15587 });
15588 buffer_3.update(cx, |buffer, _| {
15589 assert_eq!(buffer.text(), base_text_3);
15590 });
15591
15592 editor.update_in(cx, |editor, window, cx| {
15593 editor.undo(&Default::default(), window, cx);
15594 });
15595
15596 editor.update_in(cx, |editor, window, cx| {
15597 editor.change_selections(None, window, cx, |s| {
15598 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
15599 });
15600 editor.git_restore(&Default::default(), window, cx);
15601 });
15602
15603 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
15604 // but not affect buffer_2 and its related excerpts.
15605 editor.update(cx, |editor, cx| {
15606 assert_eq!(
15607 editor.text(cx),
15608 "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}"
15609 );
15610 });
15611 buffer_1.update(cx, |buffer, _| {
15612 assert_eq!(buffer.text(), base_text_1);
15613 });
15614 buffer_2.update(cx, |buffer, _| {
15615 assert_eq!(
15616 buffer.text(),
15617 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
15618 );
15619 });
15620 buffer_3.update(cx, |buffer, _| {
15621 assert_eq!(
15622 buffer.text(),
15623 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
15624 );
15625 });
15626
15627 fn edit_first_char_of_every_line(text: &str) -> String {
15628 text.split('\n')
15629 .map(|line| format!("X{}", &line[1..]))
15630 .collect::<Vec<_>>()
15631 .join("\n")
15632 }
15633}
15634
15635#[gpui::test]
15636async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
15637 init_test(cx, |_| {});
15638
15639 let cols = 4;
15640 let rows = 10;
15641 let sample_text_1 = sample_text(rows, cols, 'a');
15642 assert_eq!(
15643 sample_text_1,
15644 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
15645 );
15646 let sample_text_2 = sample_text(rows, cols, 'l');
15647 assert_eq!(
15648 sample_text_2,
15649 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
15650 );
15651 let sample_text_3 = sample_text(rows, cols, 'v');
15652 assert_eq!(
15653 sample_text_3,
15654 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
15655 );
15656
15657 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
15658 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
15659 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
15660
15661 let multi_buffer = cx.new(|cx| {
15662 let mut multibuffer = MultiBuffer::new(ReadWrite);
15663 multibuffer.push_excerpts(
15664 buffer_1.clone(),
15665 [
15666 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15667 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15668 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15669 ],
15670 cx,
15671 );
15672 multibuffer.push_excerpts(
15673 buffer_2.clone(),
15674 [
15675 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15676 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15677 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15678 ],
15679 cx,
15680 );
15681 multibuffer.push_excerpts(
15682 buffer_3.clone(),
15683 [
15684 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15685 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15686 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15687 ],
15688 cx,
15689 );
15690 multibuffer
15691 });
15692
15693 let fs = FakeFs::new(cx.executor());
15694 fs.insert_tree(
15695 "/a",
15696 json!({
15697 "main.rs": sample_text_1,
15698 "other.rs": sample_text_2,
15699 "lib.rs": sample_text_3,
15700 }),
15701 )
15702 .await;
15703 let project = Project::test(fs, ["/a".as_ref()], cx).await;
15704 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15705 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15706 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15707 Editor::new(
15708 EditorMode::full(),
15709 multi_buffer,
15710 Some(project.clone()),
15711 window,
15712 cx,
15713 )
15714 });
15715 let multibuffer_item_id = workspace
15716 .update(cx, |workspace, window, cx| {
15717 assert!(
15718 workspace.active_item(cx).is_none(),
15719 "active item should be None before the first item is added"
15720 );
15721 workspace.add_item_to_active_pane(
15722 Box::new(multi_buffer_editor.clone()),
15723 None,
15724 true,
15725 window,
15726 cx,
15727 );
15728 let active_item = workspace
15729 .active_item(cx)
15730 .expect("should have an active item after adding the multi buffer");
15731 assert!(
15732 !active_item.is_singleton(cx),
15733 "A multi buffer was expected to active after adding"
15734 );
15735 active_item.item_id()
15736 })
15737 .unwrap();
15738 cx.executor().run_until_parked();
15739
15740 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15741 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15742 s.select_ranges(Some(1..2))
15743 });
15744 editor.open_excerpts(&OpenExcerpts, window, cx);
15745 });
15746 cx.executor().run_until_parked();
15747 let first_item_id = workspace
15748 .update(cx, |workspace, window, cx| {
15749 let active_item = workspace
15750 .active_item(cx)
15751 .expect("should have an active item after navigating into the 1st buffer");
15752 let first_item_id = active_item.item_id();
15753 assert_ne!(
15754 first_item_id, multibuffer_item_id,
15755 "Should navigate into the 1st buffer and activate it"
15756 );
15757 assert!(
15758 active_item.is_singleton(cx),
15759 "New active item should be a singleton buffer"
15760 );
15761 assert_eq!(
15762 active_item
15763 .act_as::<Editor>(cx)
15764 .expect("should have navigated into an editor for the 1st buffer")
15765 .read(cx)
15766 .text(cx),
15767 sample_text_1
15768 );
15769
15770 workspace
15771 .go_back(workspace.active_pane().downgrade(), window, cx)
15772 .detach_and_log_err(cx);
15773
15774 first_item_id
15775 })
15776 .unwrap();
15777 cx.executor().run_until_parked();
15778 workspace
15779 .update(cx, |workspace, _, cx| {
15780 let active_item = workspace
15781 .active_item(cx)
15782 .expect("should have an active item after navigating back");
15783 assert_eq!(
15784 active_item.item_id(),
15785 multibuffer_item_id,
15786 "Should navigate back to the multi buffer"
15787 );
15788 assert!(!active_item.is_singleton(cx));
15789 })
15790 .unwrap();
15791
15792 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15793 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15794 s.select_ranges(Some(39..40))
15795 });
15796 editor.open_excerpts(&OpenExcerpts, window, cx);
15797 });
15798 cx.executor().run_until_parked();
15799 let second_item_id = workspace
15800 .update(cx, |workspace, window, cx| {
15801 let active_item = workspace
15802 .active_item(cx)
15803 .expect("should have an active item after navigating into the 2nd buffer");
15804 let second_item_id = active_item.item_id();
15805 assert_ne!(
15806 second_item_id, multibuffer_item_id,
15807 "Should navigate away from the multibuffer"
15808 );
15809 assert_ne!(
15810 second_item_id, first_item_id,
15811 "Should navigate into the 2nd buffer and activate it"
15812 );
15813 assert!(
15814 active_item.is_singleton(cx),
15815 "New active item should be a singleton buffer"
15816 );
15817 assert_eq!(
15818 active_item
15819 .act_as::<Editor>(cx)
15820 .expect("should have navigated into an editor")
15821 .read(cx)
15822 .text(cx),
15823 sample_text_2
15824 );
15825
15826 workspace
15827 .go_back(workspace.active_pane().downgrade(), window, cx)
15828 .detach_and_log_err(cx);
15829
15830 second_item_id
15831 })
15832 .unwrap();
15833 cx.executor().run_until_parked();
15834 workspace
15835 .update(cx, |workspace, _, cx| {
15836 let active_item = workspace
15837 .active_item(cx)
15838 .expect("should have an active item after navigating back from the 2nd buffer");
15839 assert_eq!(
15840 active_item.item_id(),
15841 multibuffer_item_id,
15842 "Should navigate back from the 2nd buffer to the multi buffer"
15843 );
15844 assert!(!active_item.is_singleton(cx));
15845 })
15846 .unwrap();
15847
15848 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15849 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15850 s.select_ranges(Some(70..70))
15851 });
15852 editor.open_excerpts(&OpenExcerpts, window, cx);
15853 });
15854 cx.executor().run_until_parked();
15855 workspace
15856 .update(cx, |workspace, window, cx| {
15857 let active_item = workspace
15858 .active_item(cx)
15859 .expect("should have an active item after navigating into the 3rd buffer");
15860 let third_item_id = active_item.item_id();
15861 assert_ne!(
15862 third_item_id, multibuffer_item_id,
15863 "Should navigate into the 3rd buffer and activate it"
15864 );
15865 assert_ne!(third_item_id, first_item_id);
15866 assert_ne!(third_item_id, second_item_id);
15867 assert!(
15868 active_item.is_singleton(cx),
15869 "New active item should be a singleton buffer"
15870 );
15871 assert_eq!(
15872 active_item
15873 .act_as::<Editor>(cx)
15874 .expect("should have navigated into an editor")
15875 .read(cx)
15876 .text(cx),
15877 sample_text_3
15878 );
15879
15880 workspace
15881 .go_back(workspace.active_pane().downgrade(), window, cx)
15882 .detach_and_log_err(cx);
15883 })
15884 .unwrap();
15885 cx.executor().run_until_parked();
15886 workspace
15887 .update(cx, |workspace, _, cx| {
15888 let active_item = workspace
15889 .active_item(cx)
15890 .expect("should have an active item after navigating back from the 3rd buffer");
15891 assert_eq!(
15892 active_item.item_id(),
15893 multibuffer_item_id,
15894 "Should navigate back from the 3rd buffer to the multi buffer"
15895 );
15896 assert!(!active_item.is_singleton(cx));
15897 })
15898 .unwrap();
15899}
15900
15901#[gpui::test]
15902async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15903 init_test(cx, |_| {});
15904
15905 let mut cx = EditorTestContext::new(cx).await;
15906
15907 let diff_base = r#"
15908 use some::mod;
15909
15910 const A: u32 = 42;
15911
15912 fn main() {
15913 println!("hello");
15914
15915 println!("world");
15916 }
15917 "#
15918 .unindent();
15919
15920 cx.set_state(
15921 &r#"
15922 use some::modified;
15923
15924 ˇ
15925 fn main() {
15926 println!("hello there");
15927
15928 println!("around the");
15929 println!("world");
15930 }
15931 "#
15932 .unindent(),
15933 );
15934
15935 cx.set_head_text(&diff_base);
15936 executor.run_until_parked();
15937
15938 cx.update_editor(|editor, window, cx| {
15939 editor.go_to_next_hunk(&GoToHunk, window, cx);
15940 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15941 });
15942 executor.run_until_parked();
15943 cx.assert_state_with_diff(
15944 r#"
15945 use some::modified;
15946
15947
15948 fn main() {
15949 - println!("hello");
15950 + ˇ println!("hello there");
15951
15952 println!("around the");
15953 println!("world");
15954 }
15955 "#
15956 .unindent(),
15957 );
15958
15959 cx.update_editor(|editor, window, cx| {
15960 for _ in 0..2 {
15961 editor.go_to_next_hunk(&GoToHunk, window, cx);
15962 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15963 }
15964 });
15965 executor.run_until_parked();
15966 cx.assert_state_with_diff(
15967 r#"
15968 - use some::mod;
15969 + ˇuse some::modified;
15970
15971
15972 fn main() {
15973 - println!("hello");
15974 + println!("hello there");
15975
15976 + println!("around the");
15977 println!("world");
15978 }
15979 "#
15980 .unindent(),
15981 );
15982
15983 cx.update_editor(|editor, window, cx| {
15984 editor.go_to_next_hunk(&GoToHunk, window, cx);
15985 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15986 });
15987 executor.run_until_parked();
15988 cx.assert_state_with_diff(
15989 r#"
15990 - use some::mod;
15991 + use some::modified;
15992
15993 - const A: u32 = 42;
15994 ˇ
15995 fn main() {
15996 - println!("hello");
15997 + println!("hello there");
15998
15999 + println!("around the");
16000 println!("world");
16001 }
16002 "#
16003 .unindent(),
16004 );
16005
16006 cx.update_editor(|editor, window, cx| {
16007 editor.cancel(&Cancel, window, cx);
16008 });
16009
16010 cx.assert_state_with_diff(
16011 r#"
16012 use some::modified;
16013
16014 ˇ
16015 fn main() {
16016 println!("hello there");
16017
16018 println!("around the");
16019 println!("world");
16020 }
16021 "#
16022 .unindent(),
16023 );
16024}
16025
16026#[gpui::test]
16027async fn test_diff_base_change_with_expanded_diff_hunks(
16028 executor: BackgroundExecutor,
16029 cx: &mut TestAppContext,
16030) {
16031 init_test(cx, |_| {});
16032
16033 let mut cx = EditorTestContext::new(cx).await;
16034
16035 let diff_base = r#"
16036 use some::mod1;
16037 use some::mod2;
16038
16039 const A: u32 = 42;
16040 const B: u32 = 42;
16041 const C: u32 = 42;
16042
16043 fn main() {
16044 println!("hello");
16045
16046 println!("world");
16047 }
16048 "#
16049 .unindent();
16050
16051 cx.set_state(
16052 &r#"
16053 use some::mod2;
16054
16055 const A: u32 = 42;
16056 const C: u32 = 42;
16057
16058 fn main(ˇ) {
16059 //println!("hello");
16060
16061 println!("world");
16062 //
16063 //
16064 }
16065 "#
16066 .unindent(),
16067 );
16068
16069 cx.set_head_text(&diff_base);
16070 executor.run_until_parked();
16071
16072 cx.update_editor(|editor, window, cx| {
16073 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16074 });
16075 executor.run_until_parked();
16076 cx.assert_state_with_diff(
16077 r#"
16078 - use some::mod1;
16079 use some::mod2;
16080
16081 const A: u32 = 42;
16082 - const B: u32 = 42;
16083 const C: u32 = 42;
16084
16085 fn main(ˇ) {
16086 - println!("hello");
16087 + //println!("hello");
16088
16089 println!("world");
16090 + //
16091 + //
16092 }
16093 "#
16094 .unindent(),
16095 );
16096
16097 cx.set_head_text("new diff base!");
16098 executor.run_until_parked();
16099 cx.assert_state_with_diff(
16100 r#"
16101 - new diff base!
16102 + use some::mod2;
16103 +
16104 + const A: u32 = 42;
16105 + const C: u32 = 42;
16106 +
16107 + fn main(ˇ) {
16108 + //println!("hello");
16109 +
16110 + println!("world");
16111 + //
16112 + //
16113 + }
16114 "#
16115 .unindent(),
16116 );
16117}
16118
16119#[gpui::test]
16120async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
16121 init_test(cx, |_| {});
16122
16123 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
16124 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
16125 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
16126 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
16127 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
16128 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
16129
16130 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
16131 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
16132 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
16133
16134 let multi_buffer = cx.new(|cx| {
16135 let mut multibuffer = MultiBuffer::new(ReadWrite);
16136 multibuffer.push_excerpts(
16137 buffer_1.clone(),
16138 [
16139 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16140 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16141 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16142 ],
16143 cx,
16144 );
16145 multibuffer.push_excerpts(
16146 buffer_2.clone(),
16147 [
16148 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16149 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16150 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16151 ],
16152 cx,
16153 );
16154 multibuffer.push_excerpts(
16155 buffer_3.clone(),
16156 [
16157 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16158 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16159 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16160 ],
16161 cx,
16162 );
16163 multibuffer
16164 });
16165
16166 let editor =
16167 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
16168 editor
16169 .update(cx, |editor, _window, cx| {
16170 for (buffer, diff_base) in [
16171 (buffer_1.clone(), file_1_old),
16172 (buffer_2.clone(), file_2_old),
16173 (buffer_3.clone(), file_3_old),
16174 ] {
16175 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
16176 editor
16177 .buffer
16178 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
16179 }
16180 })
16181 .unwrap();
16182
16183 let mut cx = EditorTestContext::for_editor(editor, cx).await;
16184 cx.run_until_parked();
16185
16186 cx.assert_editor_state(
16187 &"
16188 ˇaaa
16189 ccc
16190 ddd
16191
16192 ggg
16193 hhh
16194
16195
16196 lll
16197 mmm
16198 NNN
16199
16200 qqq
16201 rrr
16202
16203 uuu
16204 111
16205 222
16206 333
16207
16208 666
16209 777
16210
16211 000
16212 !!!"
16213 .unindent(),
16214 );
16215
16216 cx.update_editor(|editor, window, cx| {
16217 editor.select_all(&SelectAll, window, cx);
16218 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16219 });
16220 cx.executor().run_until_parked();
16221
16222 cx.assert_state_with_diff(
16223 "
16224 «aaa
16225 - bbb
16226 ccc
16227 ddd
16228
16229 ggg
16230 hhh
16231
16232
16233 lll
16234 mmm
16235 - nnn
16236 + NNN
16237
16238 qqq
16239 rrr
16240
16241 uuu
16242 111
16243 222
16244 333
16245
16246 + 666
16247 777
16248
16249 000
16250 !!!ˇ»"
16251 .unindent(),
16252 );
16253}
16254
16255#[gpui::test]
16256async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
16257 init_test(cx, |_| {});
16258
16259 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
16260 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
16261
16262 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
16263 let multi_buffer = cx.new(|cx| {
16264 let mut multibuffer = MultiBuffer::new(ReadWrite);
16265 multibuffer.push_excerpts(
16266 buffer.clone(),
16267 [
16268 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
16269 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
16270 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
16271 ],
16272 cx,
16273 );
16274 multibuffer
16275 });
16276
16277 let editor =
16278 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
16279 editor
16280 .update(cx, |editor, _window, cx| {
16281 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
16282 editor
16283 .buffer
16284 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
16285 })
16286 .unwrap();
16287
16288 let mut cx = EditorTestContext::for_editor(editor, cx).await;
16289 cx.run_until_parked();
16290
16291 cx.update_editor(|editor, window, cx| {
16292 editor.expand_all_diff_hunks(&Default::default(), window, cx)
16293 });
16294 cx.executor().run_until_parked();
16295
16296 // When the start of a hunk coincides with the start of its excerpt,
16297 // the hunk is expanded. When the start of a a hunk is earlier than
16298 // the start of its excerpt, the hunk is not expanded.
16299 cx.assert_state_with_diff(
16300 "
16301 ˇaaa
16302 - bbb
16303 + BBB
16304
16305 - ddd
16306 - eee
16307 + DDD
16308 + EEE
16309 fff
16310
16311 iii
16312 "
16313 .unindent(),
16314 );
16315}
16316
16317#[gpui::test]
16318async fn test_edits_around_expanded_insertion_hunks(
16319 executor: BackgroundExecutor,
16320 cx: &mut TestAppContext,
16321) {
16322 init_test(cx, |_| {});
16323
16324 let mut cx = EditorTestContext::new(cx).await;
16325
16326 let diff_base = r#"
16327 use some::mod1;
16328 use some::mod2;
16329
16330 const A: u32 = 42;
16331
16332 fn main() {
16333 println!("hello");
16334
16335 println!("world");
16336 }
16337 "#
16338 .unindent();
16339 executor.run_until_parked();
16340 cx.set_state(
16341 &r#"
16342 use some::mod1;
16343 use some::mod2;
16344
16345 const A: u32 = 42;
16346 const B: u32 = 42;
16347 const C: u32 = 42;
16348 ˇ
16349
16350 fn main() {
16351 println!("hello");
16352
16353 println!("world");
16354 }
16355 "#
16356 .unindent(),
16357 );
16358
16359 cx.set_head_text(&diff_base);
16360 executor.run_until_parked();
16361
16362 cx.update_editor(|editor, window, cx| {
16363 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16364 });
16365 executor.run_until_parked();
16366
16367 cx.assert_state_with_diff(
16368 r#"
16369 use some::mod1;
16370 use some::mod2;
16371
16372 const A: u32 = 42;
16373 + const B: u32 = 42;
16374 + const C: u32 = 42;
16375 + ˇ
16376
16377 fn main() {
16378 println!("hello");
16379
16380 println!("world");
16381 }
16382 "#
16383 .unindent(),
16384 );
16385
16386 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
16387 executor.run_until_parked();
16388
16389 cx.assert_state_with_diff(
16390 r#"
16391 use some::mod1;
16392 use some::mod2;
16393
16394 const A: u32 = 42;
16395 + const B: u32 = 42;
16396 + const C: u32 = 42;
16397 + const D: u32 = 42;
16398 + ˇ
16399
16400 fn main() {
16401 println!("hello");
16402
16403 println!("world");
16404 }
16405 "#
16406 .unindent(),
16407 );
16408
16409 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
16410 executor.run_until_parked();
16411
16412 cx.assert_state_with_diff(
16413 r#"
16414 use some::mod1;
16415 use some::mod2;
16416
16417 const A: u32 = 42;
16418 + const B: u32 = 42;
16419 + const C: u32 = 42;
16420 + const D: u32 = 42;
16421 + const E: u32 = 42;
16422 + ˇ
16423
16424 fn main() {
16425 println!("hello");
16426
16427 println!("world");
16428 }
16429 "#
16430 .unindent(),
16431 );
16432
16433 cx.update_editor(|editor, window, cx| {
16434 editor.delete_line(&DeleteLine, window, cx);
16435 });
16436 executor.run_until_parked();
16437
16438 cx.assert_state_with_diff(
16439 r#"
16440 use some::mod1;
16441 use some::mod2;
16442
16443 const A: u32 = 42;
16444 + const B: u32 = 42;
16445 + const C: u32 = 42;
16446 + const D: u32 = 42;
16447 + const E: u32 = 42;
16448 ˇ
16449 fn main() {
16450 println!("hello");
16451
16452 println!("world");
16453 }
16454 "#
16455 .unindent(),
16456 );
16457
16458 cx.update_editor(|editor, window, cx| {
16459 editor.move_up(&MoveUp, window, cx);
16460 editor.delete_line(&DeleteLine, window, cx);
16461 editor.move_up(&MoveUp, window, cx);
16462 editor.delete_line(&DeleteLine, window, cx);
16463 editor.move_up(&MoveUp, window, cx);
16464 editor.delete_line(&DeleteLine, window, cx);
16465 });
16466 executor.run_until_parked();
16467 cx.assert_state_with_diff(
16468 r#"
16469 use some::mod1;
16470 use some::mod2;
16471
16472 const A: u32 = 42;
16473 + const B: u32 = 42;
16474 ˇ
16475 fn main() {
16476 println!("hello");
16477
16478 println!("world");
16479 }
16480 "#
16481 .unindent(),
16482 );
16483
16484 cx.update_editor(|editor, window, cx| {
16485 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
16486 editor.delete_line(&DeleteLine, window, cx);
16487 });
16488 executor.run_until_parked();
16489 cx.assert_state_with_diff(
16490 r#"
16491 ˇ
16492 fn main() {
16493 println!("hello");
16494
16495 println!("world");
16496 }
16497 "#
16498 .unindent(),
16499 );
16500}
16501
16502#[gpui::test]
16503async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
16504 init_test(cx, |_| {});
16505
16506 let mut cx = EditorTestContext::new(cx).await;
16507 cx.set_head_text(indoc! { "
16508 one
16509 two
16510 three
16511 four
16512 five
16513 "
16514 });
16515 cx.set_state(indoc! { "
16516 one
16517 ˇthree
16518 five
16519 "});
16520 cx.run_until_parked();
16521 cx.update_editor(|editor, window, cx| {
16522 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16523 });
16524 cx.assert_state_with_diff(
16525 indoc! { "
16526 one
16527 - two
16528 ˇthree
16529 - four
16530 five
16531 "}
16532 .to_string(),
16533 );
16534 cx.update_editor(|editor, window, cx| {
16535 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16536 });
16537
16538 cx.assert_state_with_diff(
16539 indoc! { "
16540 one
16541 ˇthree
16542 five
16543 "}
16544 .to_string(),
16545 );
16546
16547 cx.set_state(indoc! { "
16548 one
16549 ˇTWO
16550 three
16551 four
16552 five
16553 "});
16554 cx.run_until_parked();
16555 cx.update_editor(|editor, window, cx| {
16556 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16557 });
16558
16559 cx.assert_state_with_diff(
16560 indoc! { "
16561 one
16562 - two
16563 + ˇTWO
16564 three
16565 four
16566 five
16567 "}
16568 .to_string(),
16569 );
16570 cx.update_editor(|editor, window, cx| {
16571 editor.move_up(&Default::default(), window, cx);
16572 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16573 });
16574 cx.assert_state_with_diff(
16575 indoc! { "
16576 one
16577 ˇTWO
16578 three
16579 four
16580 five
16581 "}
16582 .to_string(),
16583 );
16584}
16585
16586#[gpui::test]
16587async fn test_edits_around_expanded_deletion_hunks(
16588 executor: BackgroundExecutor,
16589 cx: &mut TestAppContext,
16590) {
16591 init_test(cx, |_| {});
16592
16593 let mut cx = EditorTestContext::new(cx).await;
16594
16595 let diff_base = r#"
16596 use some::mod1;
16597 use some::mod2;
16598
16599 const A: u32 = 42;
16600 const B: u32 = 42;
16601 const C: u32 = 42;
16602
16603
16604 fn main() {
16605 println!("hello");
16606
16607 println!("world");
16608 }
16609 "#
16610 .unindent();
16611 executor.run_until_parked();
16612 cx.set_state(
16613 &r#"
16614 use some::mod1;
16615 use some::mod2;
16616
16617 ˇconst B: u32 = 42;
16618 const C: u32 = 42;
16619
16620
16621 fn main() {
16622 println!("hello");
16623
16624 println!("world");
16625 }
16626 "#
16627 .unindent(),
16628 );
16629
16630 cx.set_head_text(&diff_base);
16631 executor.run_until_parked();
16632
16633 cx.update_editor(|editor, window, cx| {
16634 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16635 });
16636 executor.run_until_parked();
16637
16638 cx.assert_state_with_diff(
16639 r#"
16640 use some::mod1;
16641 use some::mod2;
16642
16643 - const A: u32 = 42;
16644 ˇconst B: u32 = 42;
16645 const C: u32 = 42;
16646
16647
16648 fn main() {
16649 println!("hello");
16650
16651 println!("world");
16652 }
16653 "#
16654 .unindent(),
16655 );
16656
16657 cx.update_editor(|editor, window, cx| {
16658 editor.delete_line(&DeleteLine, window, cx);
16659 });
16660 executor.run_until_parked();
16661 cx.assert_state_with_diff(
16662 r#"
16663 use some::mod1;
16664 use some::mod2;
16665
16666 - const A: u32 = 42;
16667 - const B: u32 = 42;
16668 ˇconst C: u32 = 42;
16669
16670
16671 fn main() {
16672 println!("hello");
16673
16674 println!("world");
16675 }
16676 "#
16677 .unindent(),
16678 );
16679
16680 cx.update_editor(|editor, window, cx| {
16681 editor.delete_line(&DeleteLine, window, cx);
16682 });
16683 executor.run_until_parked();
16684 cx.assert_state_with_diff(
16685 r#"
16686 use some::mod1;
16687 use some::mod2;
16688
16689 - const A: u32 = 42;
16690 - const B: u32 = 42;
16691 - const C: u32 = 42;
16692 ˇ
16693
16694 fn main() {
16695 println!("hello");
16696
16697 println!("world");
16698 }
16699 "#
16700 .unindent(),
16701 );
16702
16703 cx.update_editor(|editor, window, cx| {
16704 editor.handle_input("replacement", window, cx);
16705 });
16706 executor.run_until_parked();
16707 cx.assert_state_with_diff(
16708 r#"
16709 use some::mod1;
16710 use some::mod2;
16711
16712 - const A: u32 = 42;
16713 - const B: u32 = 42;
16714 - const C: u32 = 42;
16715 -
16716 + replacementˇ
16717
16718 fn main() {
16719 println!("hello");
16720
16721 println!("world");
16722 }
16723 "#
16724 .unindent(),
16725 );
16726}
16727
16728#[gpui::test]
16729async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16730 init_test(cx, |_| {});
16731
16732 let mut cx = EditorTestContext::new(cx).await;
16733
16734 let base_text = r#"
16735 one
16736 two
16737 three
16738 four
16739 five
16740 "#
16741 .unindent();
16742 executor.run_until_parked();
16743 cx.set_state(
16744 &r#"
16745 one
16746 two
16747 fˇour
16748 five
16749 "#
16750 .unindent(),
16751 );
16752
16753 cx.set_head_text(&base_text);
16754 executor.run_until_parked();
16755
16756 cx.update_editor(|editor, window, cx| {
16757 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16758 });
16759 executor.run_until_parked();
16760
16761 cx.assert_state_with_diff(
16762 r#"
16763 one
16764 two
16765 - three
16766 fˇour
16767 five
16768 "#
16769 .unindent(),
16770 );
16771
16772 cx.update_editor(|editor, window, cx| {
16773 editor.backspace(&Backspace, window, cx);
16774 editor.backspace(&Backspace, window, cx);
16775 });
16776 executor.run_until_parked();
16777 cx.assert_state_with_diff(
16778 r#"
16779 one
16780 two
16781 - threeˇ
16782 - four
16783 + our
16784 five
16785 "#
16786 .unindent(),
16787 );
16788}
16789
16790#[gpui::test]
16791async fn test_edit_after_expanded_modification_hunk(
16792 executor: BackgroundExecutor,
16793 cx: &mut TestAppContext,
16794) {
16795 init_test(cx, |_| {});
16796
16797 let mut cx = EditorTestContext::new(cx).await;
16798
16799 let diff_base = r#"
16800 use some::mod1;
16801 use some::mod2;
16802
16803 const A: u32 = 42;
16804 const B: u32 = 42;
16805 const C: u32 = 42;
16806 const D: u32 = 42;
16807
16808
16809 fn main() {
16810 println!("hello");
16811
16812 println!("world");
16813 }"#
16814 .unindent();
16815
16816 cx.set_state(
16817 &r#"
16818 use some::mod1;
16819 use some::mod2;
16820
16821 const A: u32 = 42;
16822 const B: u32 = 42;
16823 const C: u32 = 43ˇ
16824 const D: u32 = 42;
16825
16826
16827 fn main() {
16828 println!("hello");
16829
16830 println!("world");
16831 }"#
16832 .unindent(),
16833 );
16834
16835 cx.set_head_text(&diff_base);
16836 executor.run_until_parked();
16837 cx.update_editor(|editor, window, cx| {
16838 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16839 });
16840 executor.run_until_parked();
16841
16842 cx.assert_state_with_diff(
16843 r#"
16844 use some::mod1;
16845 use some::mod2;
16846
16847 const A: u32 = 42;
16848 const B: u32 = 42;
16849 - const C: u32 = 42;
16850 + const C: u32 = 43ˇ
16851 const D: u32 = 42;
16852
16853
16854 fn main() {
16855 println!("hello");
16856
16857 println!("world");
16858 }"#
16859 .unindent(),
16860 );
16861
16862 cx.update_editor(|editor, window, cx| {
16863 editor.handle_input("\nnew_line\n", window, cx);
16864 });
16865 executor.run_until_parked();
16866
16867 cx.assert_state_with_diff(
16868 r#"
16869 use some::mod1;
16870 use some::mod2;
16871
16872 const A: u32 = 42;
16873 const B: u32 = 42;
16874 - const C: u32 = 42;
16875 + const C: u32 = 43
16876 + new_line
16877 + ˇ
16878 const D: u32 = 42;
16879
16880
16881 fn main() {
16882 println!("hello");
16883
16884 println!("world");
16885 }"#
16886 .unindent(),
16887 );
16888}
16889
16890#[gpui::test]
16891async fn test_stage_and_unstage_added_file_hunk(
16892 executor: BackgroundExecutor,
16893 cx: &mut TestAppContext,
16894) {
16895 init_test(cx, |_| {});
16896
16897 let mut cx = EditorTestContext::new(cx).await;
16898 cx.update_editor(|editor, _, cx| {
16899 editor.set_expand_all_diff_hunks(cx);
16900 });
16901
16902 let working_copy = r#"
16903 ˇfn main() {
16904 println!("hello, world!");
16905 }
16906 "#
16907 .unindent();
16908
16909 cx.set_state(&working_copy);
16910 executor.run_until_parked();
16911
16912 cx.assert_state_with_diff(
16913 r#"
16914 + ˇfn main() {
16915 + println!("hello, world!");
16916 + }
16917 "#
16918 .unindent(),
16919 );
16920 cx.assert_index_text(None);
16921
16922 cx.update_editor(|editor, window, cx| {
16923 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16924 });
16925 executor.run_until_parked();
16926 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
16927 cx.assert_state_with_diff(
16928 r#"
16929 + ˇfn main() {
16930 + println!("hello, world!");
16931 + }
16932 "#
16933 .unindent(),
16934 );
16935
16936 cx.update_editor(|editor, window, cx| {
16937 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16938 });
16939 executor.run_until_parked();
16940 cx.assert_index_text(None);
16941}
16942
16943async fn setup_indent_guides_editor(
16944 text: &str,
16945 cx: &mut TestAppContext,
16946) -> (BufferId, EditorTestContext) {
16947 init_test(cx, |_| {});
16948
16949 let mut cx = EditorTestContext::new(cx).await;
16950
16951 let buffer_id = cx.update_editor(|editor, window, cx| {
16952 editor.set_text(text, window, cx);
16953 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
16954
16955 buffer_ids[0]
16956 });
16957
16958 (buffer_id, cx)
16959}
16960
16961fn assert_indent_guides(
16962 range: Range<u32>,
16963 expected: Vec<IndentGuide>,
16964 active_indices: Option<Vec<usize>>,
16965 cx: &mut EditorTestContext,
16966) {
16967 let indent_guides = cx.update_editor(|editor, window, cx| {
16968 let snapshot = editor.snapshot(window, cx).display_snapshot;
16969 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
16970 editor,
16971 MultiBufferRow(range.start)..MultiBufferRow(range.end),
16972 true,
16973 &snapshot,
16974 cx,
16975 );
16976
16977 indent_guides.sort_by(|a, b| {
16978 a.depth.cmp(&b.depth).then(
16979 a.start_row
16980 .cmp(&b.start_row)
16981 .then(a.end_row.cmp(&b.end_row)),
16982 )
16983 });
16984 indent_guides
16985 });
16986
16987 if let Some(expected) = active_indices {
16988 let active_indices = cx.update_editor(|editor, window, cx| {
16989 let snapshot = editor.snapshot(window, cx).display_snapshot;
16990 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
16991 });
16992
16993 assert_eq!(
16994 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
16995 expected,
16996 "Active indent guide indices do not match"
16997 );
16998 }
16999
17000 assert_eq!(indent_guides, expected, "Indent guides do not match");
17001}
17002
17003fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
17004 IndentGuide {
17005 buffer_id,
17006 start_row: MultiBufferRow(start_row),
17007 end_row: MultiBufferRow(end_row),
17008 depth,
17009 tab_size: 4,
17010 settings: IndentGuideSettings {
17011 enabled: true,
17012 line_width: 1,
17013 active_line_width: 1,
17014 ..Default::default()
17015 },
17016 }
17017}
17018
17019#[gpui::test]
17020async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
17021 let (buffer_id, mut cx) = setup_indent_guides_editor(
17022 &"
17023 fn main() {
17024 let a = 1;
17025 }"
17026 .unindent(),
17027 cx,
17028 )
17029 .await;
17030
17031 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
17032}
17033
17034#[gpui::test]
17035async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
17036 let (buffer_id, mut cx) = setup_indent_guides_editor(
17037 &"
17038 fn main() {
17039 let a = 1;
17040 let b = 2;
17041 }"
17042 .unindent(),
17043 cx,
17044 )
17045 .await;
17046
17047 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
17048}
17049
17050#[gpui::test]
17051async fn test_indent_guide_nested(cx: &mut TestAppContext) {
17052 let (buffer_id, mut cx) = setup_indent_guides_editor(
17053 &"
17054 fn main() {
17055 let a = 1;
17056 if a == 3 {
17057 let b = 2;
17058 } else {
17059 let c = 3;
17060 }
17061 }"
17062 .unindent(),
17063 cx,
17064 )
17065 .await;
17066
17067 assert_indent_guides(
17068 0..8,
17069 vec![
17070 indent_guide(buffer_id, 1, 6, 0),
17071 indent_guide(buffer_id, 3, 3, 1),
17072 indent_guide(buffer_id, 5, 5, 1),
17073 ],
17074 None,
17075 &mut cx,
17076 );
17077}
17078
17079#[gpui::test]
17080async fn test_indent_guide_tab(cx: &mut TestAppContext) {
17081 let (buffer_id, mut cx) = setup_indent_guides_editor(
17082 &"
17083 fn main() {
17084 let a = 1;
17085 let b = 2;
17086 let c = 3;
17087 }"
17088 .unindent(),
17089 cx,
17090 )
17091 .await;
17092
17093 assert_indent_guides(
17094 0..5,
17095 vec![
17096 indent_guide(buffer_id, 1, 3, 0),
17097 indent_guide(buffer_id, 2, 2, 1),
17098 ],
17099 None,
17100 &mut cx,
17101 );
17102}
17103
17104#[gpui::test]
17105async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
17106 let (buffer_id, mut cx) = setup_indent_guides_editor(
17107 &"
17108 fn main() {
17109 let a = 1;
17110
17111 let c = 3;
17112 }"
17113 .unindent(),
17114 cx,
17115 )
17116 .await;
17117
17118 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
17119}
17120
17121#[gpui::test]
17122async fn test_indent_guide_complex(cx: &mut TestAppContext) {
17123 let (buffer_id, mut cx) = setup_indent_guides_editor(
17124 &"
17125 fn main() {
17126 let a = 1;
17127
17128 let c = 3;
17129
17130 if a == 3 {
17131 let b = 2;
17132 } else {
17133 let c = 3;
17134 }
17135 }"
17136 .unindent(),
17137 cx,
17138 )
17139 .await;
17140
17141 assert_indent_guides(
17142 0..11,
17143 vec![
17144 indent_guide(buffer_id, 1, 9, 0),
17145 indent_guide(buffer_id, 6, 6, 1),
17146 indent_guide(buffer_id, 8, 8, 1),
17147 ],
17148 None,
17149 &mut cx,
17150 );
17151}
17152
17153#[gpui::test]
17154async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
17155 let (buffer_id, mut cx) = setup_indent_guides_editor(
17156 &"
17157 fn main() {
17158 let a = 1;
17159
17160 let c = 3;
17161
17162 if a == 3 {
17163 let b = 2;
17164 } else {
17165 let c = 3;
17166 }
17167 }"
17168 .unindent(),
17169 cx,
17170 )
17171 .await;
17172
17173 assert_indent_guides(
17174 1..11,
17175 vec![
17176 indent_guide(buffer_id, 1, 9, 0),
17177 indent_guide(buffer_id, 6, 6, 1),
17178 indent_guide(buffer_id, 8, 8, 1),
17179 ],
17180 None,
17181 &mut cx,
17182 );
17183}
17184
17185#[gpui::test]
17186async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
17187 let (buffer_id, mut cx) = setup_indent_guides_editor(
17188 &"
17189 fn main() {
17190 let a = 1;
17191
17192 let c = 3;
17193
17194 if a == 3 {
17195 let b = 2;
17196 } else {
17197 let c = 3;
17198 }
17199 }"
17200 .unindent(),
17201 cx,
17202 )
17203 .await;
17204
17205 assert_indent_guides(
17206 1..10,
17207 vec![
17208 indent_guide(buffer_id, 1, 9, 0),
17209 indent_guide(buffer_id, 6, 6, 1),
17210 indent_guide(buffer_id, 8, 8, 1),
17211 ],
17212 None,
17213 &mut cx,
17214 );
17215}
17216
17217#[gpui::test]
17218async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
17219 let (buffer_id, mut cx) = setup_indent_guides_editor(
17220 &"
17221 fn main() {
17222 if a {
17223 b(
17224 c,
17225 d,
17226 )
17227 } else {
17228 e(
17229 f
17230 )
17231 }
17232 }"
17233 .unindent(),
17234 cx,
17235 )
17236 .await;
17237
17238 assert_indent_guides(
17239 0..11,
17240 vec![
17241 indent_guide(buffer_id, 1, 10, 0),
17242 indent_guide(buffer_id, 2, 5, 1),
17243 indent_guide(buffer_id, 7, 9, 1),
17244 indent_guide(buffer_id, 3, 4, 2),
17245 indent_guide(buffer_id, 8, 8, 2),
17246 ],
17247 None,
17248 &mut cx,
17249 );
17250
17251 cx.update_editor(|editor, window, cx| {
17252 editor.fold_at(MultiBufferRow(2), window, cx);
17253 assert_eq!(
17254 editor.display_text(cx),
17255 "
17256 fn main() {
17257 if a {
17258 b(⋯
17259 )
17260 } else {
17261 e(
17262 f
17263 )
17264 }
17265 }"
17266 .unindent()
17267 );
17268 });
17269
17270 assert_indent_guides(
17271 0..11,
17272 vec![
17273 indent_guide(buffer_id, 1, 10, 0),
17274 indent_guide(buffer_id, 2, 5, 1),
17275 indent_guide(buffer_id, 7, 9, 1),
17276 indent_guide(buffer_id, 8, 8, 2),
17277 ],
17278 None,
17279 &mut cx,
17280 );
17281}
17282
17283#[gpui::test]
17284async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
17285 let (buffer_id, mut cx) = setup_indent_guides_editor(
17286 &"
17287 block1
17288 block2
17289 block3
17290 block4
17291 block2
17292 block1
17293 block1"
17294 .unindent(),
17295 cx,
17296 )
17297 .await;
17298
17299 assert_indent_guides(
17300 1..10,
17301 vec![
17302 indent_guide(buffer_id, 1, 4, 0),
17303 indent_guide(buffer_id, 2, 3, 1),
17304 indent_guide(buffer_id, 3, 3, 2),
17305 ],
17306 None,
17307 &mut cx,
17308 );
17309}
17310
17311#[gpui::test]
17312async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
17313 let (buffer_id, mut cx) = setup_indent_guides_editor(
17314 &"
17315 block1
17316 block2
17317 block3
17318
17319 block1
17320 block1"
17321 .unindent(),
17322 cx,
17323 )
17324 .await;
17325
17326 assert_indent_guides(
17327 0..6,
17328 vec![
17329 indent_guide(buffer_id, 1, 2, 0),
17330 indent_guide(buffer_id, 2, 2, 1),
17331 ],
17332 None,
17333 &mut cx,
17334 );
17335}
17336
17337#[gpui::test]
17338async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
17339 let (buffer_id, mut cx) = setup_indent_guides_editor(
17340 &"
17341 function component() {
17342 \treturn (
17343 \t\t\t
17344 \t\t<div>
17345 \t\t\t<abc></abc>
17346 \t\t</div>
17347 \t)
17348 }"
17349 .unindent(),
17350 cx,
17351 )
17352 .await;
17353
17354 assert_indent_guides(
17355 0..8,
17356 vec![
17357 indent_guide(buffer_id, 1, 6, 0),
17358 indent_guide(buffer_id, 2, 5, 1),
17359 indent_guide(buffer_id, 4, 4, 2),
17360 ],
17361 None,
17362 &mut cx,
17363 );
17364}
17365
17366#[gpui::test]
17367async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
17368 let (buffer_id, mut cx) = setup_indent_guides_editor(
17369 &"
17370 function component() {
17371 \treturn (
17372 \t
17373 \t\t<div>
17374 \t\t\t<abc></abc>
17375 \t\t</div>
17376 \t)
17377 }"
17378 .unindent(),
17379 cx,
17380 )
17381 .await;
17382
17383 assert_indent_guides(
17384 0..8,
17385 vec![
17386 indent_guide(buffer_id, 1, 6, 0),
17387 indent_guide(buffer_id, 2, 5, 1),
17388 indent_guide(buffer_id, 4, 4, 2),
17389 ],
17390 None,
17391 &mut cx,
17392 );
17393}
17394
17395#[gpui::test]
17396async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
17397 let (buffer_id, mut cx) = setup_indent_guides_editor(
17398 &"
17399 block1
17400
17401
17402
17403 block2
17404 "
17405 .unindent(),
17406 cx,
17407 )
17408 .await;
17409
17410 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
17411}
17412
17413#[gpui::test]
17414async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
17415 let (buffer_id, mut cx) = setup_indent_guides_editor(
17416 &"
17417 def a:
17418 \tb = 3
17419 \tif True:
17420 \t\tc = 4
17421 \t\td = 5
17422 \tprint(b)
17423 "
17424 .unindent(),
17425 cx,
17426 )
17427 .await;
17428
17429 assert_indent_guides(
17430 0..6,
17431 vec![
17432 indent_guide(buffer_id, 1, 5, 0),
17433 indent_guide(buffer_id, 3, 4, 1),
17434 ],
17435 None,
17436 &mut cx,
17437 );
17438}
17439
17440#[gpui::test]
17441async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
17442 let (buffer_id, mut cx) = setup_indent_guides_editor(
17443 &"
17444 fn main() {
17445 let a = 1;
17446 }"
17447 .unindent(),
17448 cx,
17449 )
17450 .await;
17451
17452 cx.update_editor(|editor, window, cx| {
17453 editor.change_selections(None, window, cx, |s| {
17454 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17455 });
17456 });
17457
17458 assert_indent_guides(
17459 0..3,
17460 vec![indent_guide(buffer_id, 1, 1, 0)],
17461 Some(vec![0]),
17462 &mut cx,
17463 );
17464}
17465
17466#[gpui::test]
17467async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
17468 let (buffer_id, mut cx) = setup_indent_guides_editor(
17469 &"
17470 fn main() {
17471 if 1 == 2 {
17472 let a = 1;
17473 }
17474 }"
17475 .unindent(),
17476 cx,
17477 )
17478 .await;
17479
17480 cx.update_editor(|editor, window, cx| {
17481 editor.change_selections(None, window, cx, |s| {
17482 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17483 });
17484 });
17485
17486 assert_indent_guides(
17487 0..4,
17488 vec![
17489 indent_guide(buffer_id, 1, 3, 0),
17490 indent_guide(buffer_id, 2, 2, 1),
17491 ],
17492 Some(vec![1]),
17493 &mut cx,
17494 );
17495
17496 cx.update_editor(|editor, window, cx| {
17497 editor.change_selections(None, window, cx, |s| {
17498 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
17499 });
17500 });
17501
17502 assert_indent_guides(
17503 0..4,
17504 vec![
17505 indent_guide(buffer_id, 1, 3, 0),
17506 indent_guide(buffer_id, 2, 2, 1),
17507 ],
17508 Some(vec![1]),
17509 &mut cx,
17510 );
17511
17512 cx.update_editor(|editor, window, cx| {
17513 editor.change_selections(None, window, cx, |s| {
17514 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
17515 });
17516 });
17517
17518 assert_indent_guides(
17519 0..4,
17520 vec![
17521 indent_guide(buffer_id, 1, 3, 0),
17522 indent_guide(buffer_id, 2, 2, 1),
17523 ],
17524 Some(vec![0]),
17525 &mut cx,
17526 );
17527}
17528
17529#[gpui::test]
17530async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
17531 let (buffer_id, mut cx) = setup_indent_guides_editor(
17532 &"
17533 fn main() {
17534 let a = 1;
17535
17536 let b = 2;
17537 }"
17538 .unindent(),
17539 cx,
17540 )
17541 .await;
17542
17543 cx.update_editor(|editor, window, cx| {
17544 editor.change_selections(None, window, cx, |s| {
17545 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
17546 });
17547 });
17548
17549 assert_indent_guides(
17550 0..5,
17551 vec![indent_guide(buffer_id, 1, 3, 0)],
17552 Some(vec![0]),
17553 &mut cx,
17554 );
17555}
17556
17557#[gpui::test]
17558async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
17559 let (buffer_id, mut cx) = setup_indent_guides_editor(
17560 &"
17561 def m:
17562 a = 1
17563 pass"
17564 .unindent(),
17565 cx,
17566 )
17567 .await;
17568
17569 cx.update_editor(|editor, window, cx| {
17570 editor.change_selections(None, window, cx, |s| {
17571 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17572 });
17573 });
17574
17575 assert_indent_guides(
17576 0..3,
17577 vec![indent_guide(buffer_id, 1, 2, 0)],
17578 Some(vec![0]),
17579 &mut cx,
17580 );
17581}
17582
17583#[gpui::test]
17584async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
17585 init_test(cx, |_| {});
17586 let mut cx = EditorTestContext::new(cx).await;
17587 let text = indoc! {
17588 "
17589 impl A {
17590 fn b() {
17591 0;
17592 3;
17593 5;
17594 6;
17595 7;
17596 }
17597 }
17598 "
17599 };
17600 let base_text = indoc! {
17601 "
17602 impl A {
17603 fn b() {
17604 0;
17605 1;
17606 2;
17607 3;
17608 4;
17609 }
17610 fn c() {
17611 5;
17612 6;
17613 7;
17614 }
17615 }
17616 "
17617 };
17618
17619 cx.update_editor(|editor, window, cx| {
17620 editor.set_text(text, window, cx);
17621
17622 editor.buffer().update(cx, |multibuffer, cx| {
17623 let buffer = multibuffer.as_singleton().unwrap();
17624 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
17625
17626 multibuffer.set_all_diff_hunks_expanded(cx);
17627 multibuffer.add_diff(diff, cx);
17628
17629 buffer.read(cx).remote_id()
17630 })
17631 });
17632 cx.run_until_parked();
17633
17634 cx.assert_state_with_diff(
17635 indoc! { "
17636 impl A {
17637 fn b() {
17638 0;
17639 - 1;
17640 - 2;
17641 3;
17642 - 4;
17643 - }
17644 - fn c() {
17645 5;
17646 6;
17647 7;
17648 }
17649 }
17650 ˇ"
17651 }
17652 .to_string(),
17653 );
17654
17655 let mut actual_guides = cx.update_editor(|editor, window, cx| {
17656 editor
17657 .snapshot(window, cx)
17658 .buffer_snapshot
17659 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
17660 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
17661 .collect::<Vec<_>>()
17662 });
17663 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
17664 assert_eq!(
17665 actual_guides,
17666 vec![
17667 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
17668 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
17669 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
17670 ]
17671 );
17672}
17673
17674#[gpui::test]
17675async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17676 init_test(cx, |_| {});
17677 let mut cx = EditorTestContext::new(cx).await;
17678
17679 let diff_base = r#"
17680 a
17681 b
17682 c
17683 "#
17684 .unindent();
17685
17686 cx.set_state(
17687 &r#"
17688 ˇA
17689 b
17690 C
17691 "#
17692 .unindent(),
17693 );
17694 cx.set_head_text(&diff_base);
17695 cx.update_editor(|editor, window, cx| {
17696 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17697 });
17698 executor.run_until_parked();
17699
17700 let both_hunks_expanded = r#"
17701 - a
17702 + ˇA
17703 b
17704 - c
17705 + C
17706 "#
17707 .unindent();
17708
17709 cx.assert_state_with_diff(both_hunks_expanded.clone());
17710
17711 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17712 let snapshot = editor.snapshot(window, cx);
17713 let hunks = editor
17714 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17715 .collect::<Vec<_>>();
17716 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17717 let buffer_id = hunks[0].buffer_id;
17718 hunks
17719 .into_iter()
17720 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17721 .collect::<Vec<_>>()
17722 });
17723 assert_eq!(hunk_ranges.len(), 2);
17724
17725 cx.update_editor(|editor, _, cx| {
17726 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17727 });
17728 executor.run_until_parked();
17729
17730 let second_hunk_expanded = r#"
17731 ˇA
17732 b
17733 - c
17734 + C
17735 "#
17736 .unindent();
17737
17738 cx.assert_state_with_diff(second_hunk_expanded);
17739
17740 cx.update_editor(|editor, _, cx| {
17741 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17742 });
17743 executor.run_until_parked();
17744
17745 cx.assert_state_with_diff(both_hunks_expanded.clone());
17746
17747 cx.update_editor(|editor, _, cx| {
17748 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17749 });
17750 executor.run_until_parked();
17751
17752 let first_hunk_expanded = r#"
17753 - a
17754 + ˇA
17755 b
17756 C
17757 "#
17758 .unindent();
17759
17760 cx.assert_state_with_diff(first_hunk_expanded);
17761
17762 cx.update_editor(|editor, _, cx| {
17763 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17764 });
17765 executor.run_until_parked();
17766
17767 cx.assert_state_with_diff(both_hunks_expanded);
17768
17769 cx.set_state(
17770 &r#"
17771 ˇA
17772 b
17773 "#
17774 .unindent(),
17775 );
17776 cx.run_until_parked();
17777
17778 // TODO this cursor position seems bad
17779 cx.assert_state_with_diff(
17780 r#"
17781 - ˇa
17782 + A
17783 b
17784 "#
17785 .unindent(),
17786 );
17787
17788 cx.update_editor(|editor, window, cx| {
17789 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17790 });
17791
17792 cx.assert_state_with_diff(
17793 r#"
17794 - ˇa
17795 + A
17796 b
17797 - c
17798 "#
17799 .unindent(),
17800 );
17801
17802 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17803 let snapshot = editor.snapshot(window, cx);
17804 let hunks = editor
17805 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17806 .collect::<Vec<_>>();
17807 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17808 let buffer_id = hunks[0].buffer_id;
17809 hunks
17810 .into_iter()
17811 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17812 .collect::<Vec<_>>()
17813 });
17814 assert_eq!(hunk_ranges.len(), 2);
17815
17816 cx.update_editor(|editor, _, cx| {
17817 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17818 });
17819 executor.run_until_parked();
17820
17821 cx.assert_state_with_diff(
17822 r#"
17823 - ˇa
17824 + A
17825 b
17826 "#
17827 .unindent(),
17828 );
17829}
17830
17831#[gpui::test]
17832async fn test_toggle_deletion_hunk_at_start_of_file(
17833 executor: BackgroundExecutor,
17834 cx: &mut TestAppContext,
17835) {
17836 init_test(cx, |_| {});
17837 let mut cx = EditorTestContext::new(cx).await;
17838
17839 let diff_base = r#"
17840 a
17841 b
17842 c
17843 "#
17844 .unindent();
17845
17846 cx.set_state(
17847 &r#"
17848 ˇb
17849 c
17850 "#
17851 .unindent(),
17852 );
17853 cx.set_head_text(&diff_base);
17854 cx.update_editor(|editor, window, cx| {
17855 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17856 });
17857 executor.run_until_parked();
17858
17859 let hunk_expanded = r#"
17860 - a
17861 ˇb
17862 c
17863 "#
17864 .unindent();
17865
17866 cx.assert_state_with_diff(hunk_expanded.clone());
17867
17868 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17869 let snapshot = editor.snapshot(window, cx);
17870 let hunks = editor
17871 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17872 .collect::<Vec<_>>();
17873 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17874 let buffer_id = hunks[0].buffer_id;
17875 hunks
17876 .into_iter()
17877 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17878 .collect::<Vec<_>>()
17879 });
17880 assert_eq!(hunk_ranges.len(), 1);
17881
17882 cx.update_editor(|editor, _, cx| {
17883 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17884 });
17885 executor.run_until_parked();
17886
17887 let hunk_collapsed = r#"
17888 ˇb
17889 c
17890 "#
17891 .unindent();
17892
17893 cx.assert_state_with_diff(hunk_collapsed);
17894
17895 cx.update_editor(|editor, _, cx| {
17896 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17897 });
17898 executor.run_until_parked();
17899
17900 cx.assert_state_with_diff(hunk_expanded.clone());
17901}
17902
17903#[gpui::test]
17904async fn test_display_diff_hunks(cx: &mut TestAppContext) {
17905 init_test(cx, |_| {});
17906
17907 let fs = FakeFs::new(cx.executor());
17908 fs.insert_tree(
17909 path!("/test"),
17910 json!({
17911 ".git": {},
17912 "file-1": "ONE\n",
17913 "file-2": "TWO\n",
17914 "file-3": "THREE\n",
17915 }),
17916 )
17917 .await;
17918
17919 fs.set_head_for_repo(
17920 path!("/test/.git").as_ref(),
17921 &[
17922 ("file-1".into(), "one\n".into()),
17923 ("file-2".into(), "two\n".into()),
17924 ("file-3".into(), "three\n".into()),
17925 ],
17926 "deadbeef",
17927 );
17928
17929 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
17930 let mut buffers = vec![];
17931 for i in 1..=3 {
17932 let buffer = project
17933 .update(cx, |project, cx| {
17934 let path = format!(path!("/test/file-{}"), i);
17935 project.open_local_buffer(path, cx)
17936 })
17937 .await
17938 .unwrap();
17939 buffers.push(buffer);
17940 }
17941
17942 let multibuffer = cx.new(|cx| {
17943 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
17944 multibuffer.set_all_diff_hunks_expanded(cx);
17945 for buffer in &buffers {
17946 let snapshot = buffer.read(cx).snapshot();
17947 multibuffer.set_excerpts_for_path(
17948 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
17949 buffer.clone(),
17950 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
17951 DEFAULT_MULTIBUFFER_CONTEXT,
17952 cx,
17953 );
17954 }
17955 multibuffer
17956 });
17957
17958 let editor = cx.add_window(|window, cx| {
17959 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
17960 });
17961 cx.run_until_parked();
17962
17963 let snapshot = editor
17964 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17965 .unwrap();
17966 let hunks = snapshot
17967 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
17968 .map(|hunk| match hunk {
17969 DisplayDiffHunk::Unfolded {
17970 display_row_range, ..
17971 } => display_row_range,
17972 DisplayDiffHunk::Folded { .. } => unreachable!(),
17973 })
17974 .collect::<Vec<_>>();
17975 assert_eq!(
17976 hunks,
17977 [
17978 DisplayRow(2)..DisplayRow(4),
17979 DisplayRow(7)..DisplayRow(9),
17980 DisplayRow(12)..DisplayRow(14),
17981 ]
17982 );
17983}
17984
17985#[gpui::test]
17986async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
17987 init_test(cx, |_| {});
17988
17989 let mut cx = EditorTestContext::new(cx).await;
17990 cx.set_head_text(indoc! { "
17991 one
17992 two
17993 three
17994 four
17995 five
17996 "
17997 });
17998 cx.set_index_text(indoc! { "
17999 one
18000 two
18001 three
18002 four
18003 five
18004 "
18005 });
18006 cx.set_state(indoc! {"
18007 one
18008 TWO
18009 ˇTHREE
18010 FOUR
18011 five
18012 "});
18013 cx.run_until_parked();
18014 cx.update_editor(|editor, window, cx| {
18015 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18016 });
18017 cx.run_until_parked();
18018 cx.assert_index_text(Some(indoc! {"
18019 one
18020 TWO
18021 THREE
18022 FOUR
18023 five
18024 "}));
18025 cx.set_state(indoc! { "
18026 one
18027 TWO
18028 ˇTHREE-HUNDRED
18029 FOUR
18030 five
18031 "});
18032 cx.run_until_parked();
18033 cx.update_editor(|editor, window, cx| {
18034 let snapshot = editor.snapshot(window, cx);
18035 let hunks = editor
18036 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18037 .collect::<Vec<_>>();
18038 assert_eq!(hunks.len(), 1);
18039 assert_eq!(
18040 hunks[0].status(),
18041 DiffHunkStatus {
18042 kind: DiffHunkStatusKind::Modified,
18043 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
18044 }
18045 );
18046
18047 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18048 });
18049 cx.run_until_parked();
18050 cx.assert_index_text(Some(indoc! {"
18051 one
18052 TWO
18053 THREE-HUNDRED
18054 FOUR
18055 five
18056 "}));
18057}
18058
18059#[gpui::test]
18060fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
18061 init_test(cx, |_| {});
18062
18063 let editor = cx.add_window(|window, cx| {
18064 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
18065 build_editor(buffer, window, cx)
18066 });
18067
18068 let render_args = Arc::new(Mutex::new(None));
18069 let snapshot = editor
18070 .update(cx, |editor, window, cx| {
18071 let snapshot = editor.buffer().read(cx).snapshot(cx);
18072 let range =
18073 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
18074
18075 struct RenderArgs {
18076 row: MultiBufferRow,
18077 folded: bool,
18078 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
18079 }
18080
18081 let crease = Crease::inline(
18082 range,
18083 FoldPlaceholder::test(),
18084 {
18085 let toggle_callback = render_args.clone();
18086 move |row, folded, callback, _window, _cx| {
18087 *toggle_callback.lock() = Some(RenderArgs {
18088 row,
18089 folded,
18090 callback,
18091 });
18092 div()
18093 }
18094 },
18095 |_row, _folded, _window, _cx| div(),
18096 );
18097
18098 editor.insert_creases(Some(crease), cx);
18099 let snapshot = editor.snapshot(window, cx);
18100 let _div = snapshot.render_crease_toggle(
18101 MultiBufferRow(1),
18102 false,
18103 cx.entity().clone(),
18104 window,
18105 cx,
18106 );
18107 snapshot
18108 })
18109 .unwrap();
18110
18111 let render_args = render_args.lock().take().unwrap();
18112 assert_eq!(render_args.row, MultiBufferRow(1));
18113 assert!(!render_args.folded);
18114 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
18115
18116 cx.update_window(*editor, |_, window, cx| {
18117 (render_args.callback)(true, window, cx)
18118 })
18119 .unwrap();
18120 let snapshot = editor
18121 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18122 .unwrap();
18123 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
18124
18125 cx.update_window(*editor, |_, window, cx| {
18126 (render_args.callback)(false, window, cx)
18127 })
18128 .unwrap();
18129 let snapshot = editor
18130 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18131 .unwrap();
18132 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
18133}
18134
18135#[gpui::test]
18136async fn test_input_text(cx: &mut TestAppContext) {
18137 init_test(cx, |_| {});
18138 let mut cx = EditorTestContext::new(cx).await;
18139
18140 cx.set_state(
18141 &r#"ˇone
18142 two
18143
18144 three
18145 fourˇ
18146 five
18147
18148 siˇx"#
18149 .unindent(),
18150 );
18151
18152 cx.dispatch_action(HandleInput(String::new()));
18153 cx.assert_editor_state(
18154 &r#"ˇone
18155 two
18156
18157 three
18158 fourˇ
18159 five
18160
18161 siˇx"#
18162 .unindent(),
18163 );
18164
18165 cx.dispatch_action(HandleInput("AAAA".to_string()));
18166 cx.assert_editor_state(
18167 &r#"AAAAˇone
18168 two
18169
18170 three
18171 fourAAAAˇ
18172 five
18173
18174 siAAAAˇx"#
18175 .unindent(),
18176 );
18177}
18178
18179#[gpui::test]
18180async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
18181 init_test(cx, |_| {});
18182
18183 let mut cx = EditorTestContext::new(cx).await;
18184 cx.set_state(
18185 r#"let foo = 1;
18186let foo = 2;
18187let foo = 3;
18188let fooˇ = 4;
18189let foo = 5;
18190let foo = 6;
18191let foo = 7;
18192let foo = 8;
18193let foo = 9;
18194let foo = 10;
18195let foo = 11;
18196let foo = 12;
18197let foo = 13;
18198let foo = 14;
18199let foo = 15;"#,
18200 );
18201
18202 cx.update_editor(|e, window, cx| {
18203 assert_eq!(
18204 e.next_scroll_position,
18205 NextScrollCursorCenterTopBottom::Center,
18206 "Default next scroll direction is center",
18207 );
18208
18209 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18210 assert_eq!(
18211 e.next_scroll_position,
18212 NextScrollCursorCenterTopBottom::Top,
18213 "After center, next scroll direction should be top",
18214 );
18215
18216 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18217 assert_eq!(
18218 e.next_scroll_position,
18219 NextScrollCursorCenterTopBottom::Bottom,
18220 "After top, next scroll direction should be bottom",
18221 );
18222
18223 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18224 assert_eq!(
18225 e.next_scroll_position,
18226 NextScrollCursorCenterTopBottom::Center,
18227 "After bottom, scrolling should start over",
18228 );
18229
18230 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18231 assert_eq!(
18232 e.next_scroll_position,
18233 NextScrollCursorCenterTopBottom::Top,
18234 "Scrolling continues if retriggered fast enough"
18235 );
18236 });
18237
18238 cx.executor()
18239 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
18240 cx.executor().run_until_parked();
18241 cx.update_editor(|e, _, _| {
18242 assert_eq!(
18243 e.next_scroll_position,
18244 NextScrollCursorCenterTopBottom::Center,
18245 "If scrolling is not triggered fast enough, it should reset"
18246 );
18247 });
18248}
18249
18250#[gpui::test]
18251async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
18252 init_test(cx, |_| {});
18253 let mut cx = EditorLspTestContext::new_rust(
18254 lsp::ServerCapabilities {
18255 definition_provider: Some(lsp::OneOf::Left(true)),
18256 references_provider: Some(lsp::OneOf::Left(true)),
18257 ..lsp::ServerCapabilities::default()
18258 },
18259 cx,
18260 )
18261 .await;
18262
18263 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
18264 let go_to_definition = cx
18265 .lsp
18266 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
18267 move |params, _| async move {
18268 if empty_go_to_definition {
18269 Ok(None)
18270 } else {
18271 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
18272 uri: params.text_document_position_params.text_document.uri,
18273 range: lsp::Range::new(
18274 lsp::Position::new(4, 3),
18275 lsp::Position::new(4, 6),
18276 ),
18277 })))
18278 }
18279 },
18280 );
18281 let references = cx
18282 .lsp
18283 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
18284 Ok(Some(vec![lsp::Location {
18285 uri: params.text_document_position.text_document.uri,
18286 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
18287 }]))
18288 });
18289 (go_to_definition, references)
18290 };
18291
18292 cx.set_state(
18293 &r#"fn one() {
18294 let mut a = ˇtwo();
18295 }
18296
18297 fn two() {}"#
18298 .unindent(),
18299 );
18300 set_up_lsp_handlers(false, &mut cx);
18301 let navigated = cx
18302 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
18303 .await
18304 .expect("Failed to navigate to definition");
18305 assert_eq!(
18306 navigated,
18307 Navigated::Yes,
18308 "Should have navigated to definition from the GetDefinition response"
18309 );
18310 cx.assert_editor_state(
18311 &r#"fn one() {
18312 let mut a = two();
18313 }
18314
18315 fn «twoˇ»() {}"#
18316 .unindent(),
18317 );
18318
18319 let editors = cx.update_workspace(|workspace, _, cx| {
18320 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
18321 });
18322 cx.update_editor(|_, _, test_editor_cx| {
18323 assert_eq!(
18324 editors.len(),
18325 1,
18326 "Initially, only one, test, editor should be open in the workspace"
18327 );
18328 assert_eq!(
18329 test_editor_cx.entity(),
18330 editors.last().expect("Asserted len is 1").clone()
18331 );
18332 });
18333
18334 set_up_lsp_handlers(true, &mut cx);
18335 let navigated = cx
18336 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
18337 .await
18338 .expect("Failed to navigate to lookup references");
18339 assert_eq!(
18340 navigated,
18341 Navigated::Yes,
18342 "Should have navigated to references as a fallback after empty GoToDefinition response"
18343 );
18344 // We should not change the selections in the existing file,
18345 // if opening another milti buffer with the references
18346 cx.assert_editor_state(
18347 &r#"fn one() {
18348 let mut a = two();
18349 }
18350
18351 fn «twoˇ»() {}"#
18352 .unindent(),
18353 );
18354 let editors = cx.update_workspace(|workspace, _, cx| {
18355 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
18356 });
18357 cx.update_editor(|_, _, test_editor_cx| {
18358 assert_eq!(
18359 editors.len(),
18360 2,
18361 "After falling back to references search, we open a new editor with the results"
18362 );
18363 let references_fallback_text = editors
18364 .into_iter()
18365 .find(|new_editor| *new_editor != test_editor_cx.entity())
18366 .expect("Should have one non-test editor now")
18367 .read(test_editor_cx)
18368 .text(test_editor_cx);
18369 assert_eq!(
18370 references_fallback_text, "fn one() {\n let mut a = two();\n}",
18371 "Should use the range from the references response and not the GoToDefinition one"
18372 );
18373 });
18374}
18375
18376#[gpui::test]
18377async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
18378 init_test(cx, |_| {});
18379 cx.update(|cx| {
18380 let mut editor_settings = EditorSettings::get_global(cx).clone();
18381 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
18382 EditorSettings::override_global(editor_settings, cx);
18383 });
18384 let mut cx = EditorLspTestContext::new_rust(
18385 lsp::ServerCapabilities {
18386 definition_provider: Some(lsp::OneOf::Left(true)),
18387 references_provider: Some(lsp::OneOf::Left(true)),
18388 ..lsp::ServerCapabilities::default()
18389 },
18390 cx,
18391 )
18392 .await;
18393 let original_state = r#"fn one() {
18394 let mut a = ˇtwo();
18395 }
18396
18397 fn two() {}"#
18398 .unindent();
18399 cx.set_state(&original_state);
18400
18401 let mut go_to_definition = cx
18402 .lsp
18403 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
18404 move |_, _| async move { Ok(None) },
18405 );
18406 let _references = cx
18407 .lsp
18408 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
18409 panic!("Should not call for references with no go to definition fallback")
18410 });
18411
18412 let navigated = cx
18413 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
18414 .await
18415 .expect("Failed to navigate to lookup references");
18416 go_to_definition
18417 .next()
18418 .await
18419 .expect("Should have called the go_to_definition handler");
18420
18421 assert_eq!(
18422 navigated,
18423 Navigated::No,
18424 "Should have navigated to references as a fallback after empty GoToDefinition response"
18425 );
18426 cx.assert_editor_state(&original_state);
18427 let editors = cx.update_workspace(|workspace, _, cx| {
18428 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
18429 });
18430 cx.update_editor(|_, _, _| {
18431 assert_eq!(
18432 editors.len(),
18433 1,
18434 "After unsuccessful fallback, no other editor should have been opened"
18435 );
18436 });
18437}
18438
18439#[gpui::test]
18440async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
18441 init_test(cx, |_| {});
18442
18443 let language = Arc::new(Language::new(
18444 LanguageConfig::default(),
18445 Some(tree_sitter_rust::LANGUAGE.into()),
18446 ));
18447
18448 let text = r#"
18449 #[cfg(test)]
18450 mod tests() {
18451 #[test]
18452 fn runnable_1() {
18453 let a = 1;
18454 }
18455
18456 #[test]
18457 fn runnable_2() {
18458 let a = 1;
18459 let b = 2;
18460 }
18461 }
18462 "#
18463 .unindent();
18464
18465 let fs = FakeFs::new(cx.executor());
18466 fs.insert_file("/file.rs", Default::default()).await;
18467
18468 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18469 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18470 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18471 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
18472 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
18473
18474 let editor = cx.new_window_entity(|window, cx| {
18475 Editor::new(
18476 EditorMode::full(),
18477 multi_buffer,
18478 Some(project.clone()),
18479 window,
18480 cx,
18481 )
18482 });
18483
18484 editor.update_in(cx, |editor, window, cx| {
18485 let snapshot = editor.buffer().read(cx).snapshot(cx);
18486 editor.tasks.insert(
18487 (buffer.read(cx).remote_id(), 3),
18488 RunnableTasks {
18489 templates: vec![],
18490 offset: snapshot.anchor_before(43),
18491 column: 0,
18492 extra_variables: HashMap::default(),
18493 context_range: BufferOffset(43)..BufferOffset(85),
18494 },
18495 );
18496 editor.tasks.insert(
18497 (buffer.read(cx).remote_id(), 8),
18498 RunnableTasks {
18499 templates: vec![],
18500 offset: snapshot.anchor_before(86),
18501 column: 0,
18502 extra_variables: HashMap::default(),
18503 context_range: BufferOffset(86)..BufferOffset(191),
18504 },
18505 );
18506
18507 // Test finding task when cursor is inside function body
18508 editor.change_selections(None, window, cx, |s| {
18509 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
18510 });
18511 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
18512 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
18513
18514 // Test finding task when cursor is on function name
18515 editor.change_selections(None, window, cx, |s| {
18516 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
18517 });
18518 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
18519 assert_eq!(row, 8, "Should find task when cursor is on function name");
18520 });
18521}
18522
18523#[gpui::test]
18524async fn test_folding_buffers(cx: &mut TestAppContext) {
18525 init_test(cx, |_| {});
18526
18527 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
18528 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
18529 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
18530
18531 let fs = FakeFs::new(cx.executor());
18532 fs.insert_tree(
18533 path!("/a"),
18534 json!({
18535 "first.rs": sample_text_1,
18536 "second.rs": sample_text_2,
18537 "third.rs": sample_text_3,
18538 }),
18539 )
18540 .await;
18541 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18542 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18543 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18544 let worktree = project.update(cx, |project, cx| {
18545 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18546 assert_eq!(worktrees.len(), 1);
18547 worktrees.pop().unwrap()
18548 });
18549 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18550
18551 let buffer_1 = project
18552 .update(cx, |project, cx| {
18553 project.open_buffer((worktree_id, "first.rs"), cx)
18554 })
18555 .await
18556 .unwrap();
18557 let buffer_2 = project
18558 .update(cx, |project, cx| {
18559 project.open_buffer((worktree_id, "second.rs"), cx)
18560 })
18561 .await
18562 .unwrap();
18563 let buffer_3 = project
18564 .update(cx, |project, cx| {
18565 project.open_buffer((worktree_id, "third.rs"), cx)
18566 })
18567 .await
18568 .unwrap();
18569
18570 let multi_buffer = cx.new(|cx| {
18571 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18572 multi_buffer.push_excerpts(
18573 buffer_1.clone(),
18574 [
18575 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18576 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18577 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18578 ],
18579 cx,
18580 );
18581 multi_buffer.push_excerpts(
18582 buffer_2.clone(),
18583 [
18584 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18585 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18586 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18587 ],
18588 cx,
18589 );
18590 multi_buffer.push_excerpts(
18591 buffer_3.clone(),
18592 [
18593 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18594 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18595 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18596 ],
18597 cx,
18598 );
18599 multi_buffer
18600 });
18601 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18602 Editor::new(
18603 EditorMode::full(),
18604 multi_buffer.clone(),
18605 Some(project.clone()),
18606 window,
18607 cx,
18608 )
18609 });
18610
18611 assert_eq!(
18612 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18613 "\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",
18614 );
18615
18616 multi_buffer_editor.update(cx, |editor, cx| {
18617 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
18618 });
18619 assert_eq!(
18620 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18621 "\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",
18622 "After folding the first buffer, its text should not be displayed"
18623 );
18624
18625 multi_buffer_editor.update(cx, |editor, cx| {
18626 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
18627 });
18628 assert_eq!(
18629 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18630 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
18631 "After folding the second buffer, its text should not be displayed"
18632 );
18633
18634 multi_buffer_editor.update(cx, |editor, cx| {
18635 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
18636 });
18637 assert_eq!(
18638 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18639 "\n\n\n\n\n",
18640 "After folding the third buffer, its text should not be displayed"
18641 );
18642
18643 // Emulate selection inside the fold logic, that should work
18644 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18645 editor
18646 .snapshot(window, cx)
18647 .next_line_boundary(Point::new(0, 4));
18648 });
18649
18650 multi_buffer_editor.update(cx, |editor, cx| {
18651 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
18652 });
18653 assert_eq!(
18654 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18655 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
18656 "After unfolding the second buffer, its text should be displayed"
18657 );
18658
18659 // Typing inside of buffer 1 causes that buffer to be unfolded.
18660 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18661 assert_eq!(
18662 multi_buffer
18663 .read(cx)
18664 .snapshot(cx)
18665 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
18666 .collect::<String>(),
18667 "bbbb"
18668 );
18669 editor.change_selections(None, window, cx, |selections| {
18670 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
18671 });
18672 editor.handle_input("B", window, cx);
18673 });
18674
18675 assert_eq!(
18676 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18677 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
18678 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
18679 );
18680
18681 multi_buffer_editor.update(cx, |editor, cx| {
18682 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
18683 });
18684 assert_eq!(
18685 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18686 "\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",
18687 "After unfolding the all buffers, all original text should be displayed"
18688 );
18689}
18690
18691#[gpui::test]
18692async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
18693 init_test(cx, |_| {});
18694
18695 let sample_text_1 = "1111\n2222\n3333".to_string();
18696 let sample_text_2 = "4444\n5555\n6666".to_string();
18697 let sample_text_3 = "7777\n8888\n9999".to_string();
18698
18699 let fs = FakeFs::new(cx.executor());
18700 fs.insert_tree(
18701 path!("/a"),
18702 json!({
18703 "first.rs": sample_text_1,
18704 "second.rs": sample_text_2,
18705 "third.rs": sample_text_3,
18706 }),
18707 )
18708 .await;
18709 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18710 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18711 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18712 let worktree = project.update(cx, |project, cx| {
18713 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18714 assert_eq!(worktrees.len(), 1);
18715 worktrees.pop().unwrap()
18716 });
18717 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18718
18719 let buffer_1 = project
18720 .update(cx, |project, cx| {
18721 project.open_buffer((worktree_id, "first.rs"), cx)
18722 })
18723 .await
18724 .unwrap();
18725 let buffer_2 = project
18726 .update(cx, |project, cx| {
18727 project.open_buffer((worktree_id, "second.rs"), cx)
18728 })
18729 .await
18730 .unwrap();
18731 let buffer_3 = project
18732 .update(cx, |project, cx| {
18733 project.open_buffer((worktree_id, "third.rs"), cx)
18734 })
18735 .await
18736 .unwrap();
18737
18738 let multi_buffer = cx.new(|cx| {
18739 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18740 multi_buffer.push_excerpts(
18741 buffer_1.clone(),
18742 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18743 cx,
18744 );
18745 multi_buffer.push_excerpts(
18746 buffer_2.clone(),
18747 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18748 cx,
18749 );
18750 multi_buffer.push_excerpts(
18751 buffer_3.clone(),
18752 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18753 cx,
18754 );
18755 multi_buffer
18756 });
18757
18758 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18759 Editor::new(
18760 EditorMode::full(),
18761 multi_buffer,
18762 Some(project.clone()),
18763 window,
18764 cx,
18765 )
18766 });
18767
18768 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
18769 assert_eq!(
18770 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18771 full_text,
18772 );
18773
18774 multi_buffer_editor.update(cx, |editor, cx| {
18775 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
18776 });
18777 assert_eq!(
18778 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18779 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
18780 "After folding the first buffer, its text should not be displayed"
18781 );
18782
18783 multi_buffer_editor.update(cx, |editor, cx| {
18784 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
18785 });
18786
18787 assert_eq!(
18788 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18789 "\n\n\n\n\n\n7777\n8888\n9999",
18790 "After folding the second buffer, its text should not be displayed"
18791 );
18792
18793 multi_buffer_editor.update(cx, |editor, cx| {
18794 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
18795 });
18796 assert_eq!(
18797 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18798 "\n\n\n\n\n",
18799 "After folding the third buffer, its text should not be displayed"
18800 );
18801
18802 multi_buffer_editor.update(cx, |editor, cx| {
18803 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
18804 });
18805 assert_eq!(
18806 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18807 "\n\n\n\n4444\n5555\n6666\n\n",
18808 "After unfolding the second buffer, its text should be displayed"
18809 );
18810
18811 multi_buffer_editor.update(cx, |editor, cx| {
18812 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
18813 });
18814 assert_eq!(
18815 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18816 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
18817 "After unfolding the first buffer, its text should be displayed"
18818 );
18819
18820 multi_buffer_editor.update(cx, |editor, cx| {
18821 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
18822 });
18823 assert_eq!(
18824 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18825 full_text,
18826 "After unfolding all buffers, all original text should be displayed"
18827 );
18828}
18829
18830#[gpui::test]
18831async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
18832 init_test(cx, |_| {});
18833
18834 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
18835
18836 let fs = FakeFs::new(cx.executor());
18837 fs.insert_tree(
18838 path!("/a"),
18839 json!({
18840 "main.rs": sample_text,
18841 }),
18842 )
18843 .await;
18844 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18845 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18846 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18847 let worktree = project.update(cx, |project, cx| {
18848 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18849 assert_eq!(worktrees.len(), 1);
18850 worktrees.pop().unwrap()
18851 });
18852 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18853
18854 let buffer_1 = project
18855 .update(cx, |project, cx| {
18856 project.open_buffer((worktree_id, "main.rs"), cx)
18857 })
18858 .await
18859 .unwrap();
18860
18861 let multi_buffer = cx.new(|cx| {
18862 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18863 multi_buffer.push_excerpts(
18864 buffer_1.clone(),
18865 [ExcerptRange::new(
18866 Point::new(0, 0)
18867 ..Point::new(
18868 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
18869 0,
18870 ),
18871 )],
18872 cx,
18873 );
18874 multi_buffer
18875 });
18876 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18877 Editor::new(
18878 EditorMode::full(),
18879 multi_buffer,
18880 Some(project.clone()),
18881 window,
18882 cx,
18883 )
18884 });
18885
18886 let selection_range = Point::new(1, 0)..Point::new(2, 0);
18887 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18888 enum TestHighlight {}
18889 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
18890 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
18891 editor.highlight_text::<TestHighlight>(
18892 vec![highlight_range.clone()],
18893 HighlightStyle::color(Hsla::green()),
18894 cx,
18895 );
18896 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
18897 });
18898
18899 let full_text = format!("\n\n{sample_text}");
18900 assert_eq!(
18901 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18902 full_text,
18903 );
18904}
18905
18906#[gpui::test]
18907async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
18908 init_test(cx, |_| {});
18909 cx.update(|cx| {
18910 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
18911 "keymaps/default-linux.json",
18912 cx,
18913 )
18914 .unwrap();
18915 cx.bind_keys(default_key_bindings);
18916 });
18917
18918 let (editor, cx) = cx.add_window_view(|window, cx| {
18919 let multi_buffer = MultiBuffer::build_multi(
18920 [
18921 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
18922 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
18923 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
18924 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
18925 ],
18926 cx,
18927 );
18928 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
18929
18930 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
18931 // fold all but the second buffer, so that we test navigating between two
18932 // adjacent folded buffers, as well as folded buffers at the start and
18933 // end the multibuffer
18934 editor.fold_buffer(buffer_ids[0], cx);
18935 editor.fold_buffer(buffer_ids[2], cx);
18936 editor.fold_buffer(buffer_ids[3], cx);
18937
18938 editor
18939 });
18940 cx.simulate_resize(size(px(1000.), px(1000.)));
18941
18942 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
18943 cx.assert_excerpts_with_selections(indoc! {"
18944 [EXCERPT]
18945 ˇ[FOLDED]
18946 [EXCERPT]
18947 a1
18948 b1
18949 [EXCERPT]
18950 [FOLDED]
18951 [EXCERPT]
18952 [FOLDED]
18953 "
18954 });
18955 cx.simulate_keystroke("down");
18956 cx.assert_excerpts_with_selections(indoc! {"
18957 [EXCERPT]
18958 [FOLDED]
18959 [EXCERPT]
18960 ˇa1
18961 b1
18962 [EXCERPT]
18963 [FOLDED]
18964 [EXCERPT]
18965 [FOLDED]
18966 "
18967 });
18968 cx.simulate_keystroke("down");
18969 cx.assert_excerpts_with_selections(indoc! {"
18970 [EXCERPT]
18971 [FOLDED]
18972 [EXCERPT]
18973 a1
18974 ˇb1
18975 [EXCERPT]
18976 [FOLDED]
18977 [EXCERPT]
18978 [FOLDED]
18979 "
18980 });
18981 cx.simulate_keystroke("down");
18982 cx.assert_excerpts_with_selections(indoc! {"
18983 [EXCERPT]
18984 [FOLDED]
18985 [EXCERPT]
18986 a1
18987 b1
18988 ˇ[EXCERPT]
18989 [FOLDED]
18990 [EXCERPT]
18991 [FOLDED]
18992 "
18993 });
18994 cx.simulate_keystroke("down");
18995 cx.assert_excerpts_with_selections(indoc! {"
18996 [EXCERPT]
18997 [FOLDED]
18998 [EXCERPT]
18999 a1
19000 b1
19001 [EXCERPT]
19002 ˇ[FOLDED]
19003 [EXCERPT]
19004 [FOLDED]
19005 "
19006 });
19007 for _ in 0..5 {
19008 cx.simulate_keystroke("down");
19009 cx.assert_excerpts_with_selections(indoc! {"
19010 [EXCERPT]
19011 [FOLDED]
19012 [EXCERPT]
19013 a1
19014 b1
19015 [EXCERPT]
19016 [FOLDED]
19017 [EXCERPT]
19018 ˇ[FOLDED]
19019 "
19020 });
19021 }
19022
19023 cx.simulate_keystroke("up");
19024 cx.assert_excerpts_with_selections(indoc! {"
19025 [EXCERPT]
19026 [FOLDED]
19027 [EXCERPT]
19028 a1
19029 b1
19030 [EXCERPT]
19031 ˇ[FOLDED]
19032 [EXCERPT]
19033 [FOLDED]
19034 "
19035 });
19036 cx.simulate_keystroke("up");
19037 cx.assert_excerpts_with_selections(indoc! {"
19038 [EXCERPT]
19039 [FOLDED]
19040 [EXCERPT]
19041 a1
19042 b1
19043 ˇ[EXCERPT]
19044 [FOLDED]
19045 [EXCERPT]
19046 [FOLDED]
19047 "
19048 });
19049 cx.simulate_keystroke("up");
19050 cx.assert_excerpts_with_selections(indoc! {"
19051 [EXCERPT]
19052 [FOLDED]
19053 [EXCERPT]
19054 a1
19055 ˇb1
19056 [EXCERPT]
19057 [FOLDED]
19058 [EXCERPT]
19059 [FOLDED]
19060 "
19061 });
19062 cx.simulate_keystroke("up");
19063 cx.assert_excerpts_with_selections(indoc! {"
19064 [EXCERPT]
19065 [FOLDED]
19066 [EXCERPT]
19067 ˇa1
19068 b1
19069 [EXCERPT]
19070 [FOLDED]
19071 [EXCERPT]
19072 [FOLDED]
19073 "
19074 });
19075 for _ in 0..5 {
19076 cx.simulate_keystroke("up");
19077 cx.assert_excerpts_with_selections(indoc! {"
19078 [EXCERPT]
19079 ˇ[FOLDED]
19080 [EXCERPT]
19081 a1
19082 b1
19083 [EXCERPT]
19084 [FOLDED]
19085 [EXCERPT]
19086 [FOLDED]
19087 "
19088 });
19089 }
19090}
19091
19092#[gpui::test]
19093async fn test_inline_completion_text(cx: &mut TestAppContext) {
19094 init_test(cx, |_| {});
19095
19096 // Simple insertion
19097 assert_highlighted_edits(
19098 "Hello, world!",
19099 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
19100 true,
19101 cx,
19102 |highlighted_edits, cx| {
19103 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
19104 assert_eq!(highlighted_edits.highlights.len(), 1);
19105 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
19106 assert_eq!(
19107 highlighted_edits.highlights[0].1.background_color,
19108 Some(cx.theme().status().created_background)
19109 );
19110 },
19111 )
19112 .await;
19113
19114 // Replacement
19115 assert_highlighted_edits(
19116 "This is a test.",
19117 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
19118 false,
19119 cx,
19120 |highlighted_edits, cx| {
19121 assert_eq!(highlighted_edits.text, "That is a test.");
19122 assert_eq!(highlighted_edits.highlights.len(), 1);
19123 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
19124 assert_eq!(
19125 highlighted_edits.highlights[0].1.background_color,
19126 Some(cx.theme().status().created_background)
19127 );
19128 },
19129 )
19130 .await;
19131
19132 // Multiple edits
19133 assert_highlighted_edits(
19134 "Hello, world!",
19135 vec![
19136 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
19137 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
19138 ],
19139 false,
19140 cx,
19141 |highlighted_edits, cx| {
19142 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
19143 assert_eq!(highlighted_edits.highlights.len(), 2);
19144 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
19145 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
19146 assert_eq!(
19147 highlighted_edits.highlights[0].1.background_color,
19148 Some(cx.theme().status().created_background)
19149 );
19150 assert_eq!(
19151 highlighted_edits.highlights[1].1.background_color,
19152 Some(cx.theme().status().created_background)
19153 );
19154 },
19155 )
19156 .await;
19157
19158 // Multiple lines with edits
19159 assert_highlighted_edits(
19160 "First line\nSecond line\nThird line\nFourth line",
19161 vec![
19162 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
19163 (
19164 Point::new(2, 0)..Point::new(2, 10),
19165 "New third line".to_string(),
19166 ),
19167 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
19168 ],
19169 false,
19170 cx,
19171 |highlighted_edits, cx| {
19172 assert_eq!(
19173 highlighted_edits.text,
19174 "Second modified\nNew third line\nFourth updated line"
19175 );
19176 assert_eq!(highlighted_edits.highlights.len(), 3);
19177 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
19178 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
19179 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
19180 for highlight in &highlighted_edits.highlights {
19181 assert_eq!(
19182 highlight.1.background_color,
19183 Some(cx.theme().status().created_background)
19184 );
19185 }
19186 },
19187 )
19188 .await;
19189}
19190
19191#[gpui::test]
19192async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
19193 init_test(cx, |_| {});
19194
19195 // Deletion
19196 assert_highlighted_edits(
19197 "Hello, world!",
19198 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
19199 true,
19200 cx,
19201 |highlighted_edits, cx| {
19202 assert_eq!(highlighted_edits.text, "Hello, world!");
19203 assert_eq!(highlighted_edits.highlights.len(), 1);
19204 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
19205 assert_eq!(
19206 highlighted_edits.highlights[0].1.background_color,
19207 Some(cx.theme().status().deleted_background)
19208 );
19209 },
19210 )
19211 .await;
19212
19213 // Insertion
19214 assert_highlighted_edits(
19215 "Hello, world!",
19216 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
19217 true,
19218 cx,
19219 |highlighted_edits, cx| {
19220 assert_eq!(highlighted_edits.highlights.len(), 1);
19221 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
19222 assert_eq!(
19223 highlighted_edits.highlights[0].1.background_color,
19224 Some(cx.theme().status().created_background)
19225 );
19226 },
19227 )
19228 .await;
19229}
19230
19231async fn assert_highlighted_edits(
19232 text: &str,
19233 edits: Vec<(Range<Point>, String)>,
19234 include_deletions: bool,
19235 cx: &mut TestAppContext,
19236 assertion_fn: impl Fn(HighlightedText, &App),
19237) {
19238 let window = cx.add_window(|window, cx| {
19239 let buffer = MultiBuffer::build_simple(text, cx);
19240 Editor::new(EditorMode::full(), buffer, None, window, cx)
19241 });
19242 let cx = &mut VisualTestContext::from_window(*window, cx);
19243
19244 let (buffer, snapshot) = window
19245 .update(cx, |editor, _window, cx| {
19246 (
19247 editor.buffer().clone(),
19248 editor.buffer().read(cx).snapshot(cx),
19249 )
19250 })
19251 .unwrap();
19252
19253 let edits = edits
19254 .into_iter()
19255 .map(|(range, edit)| {
19256 (
19257 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
19258 edit,
19259 )
19260 })
19261 .collect::<Vec<_>>();
19262
19263 let text_anchor_edits = edits
19264 .clone()
19265 .into_iter()
19266 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
19267 .collect::<Vec<_>>();
19268
19269 let edit_preview = window
19270 .update(cx, |_, _window, cx| {
19271 buffer
19272 .read(cx)
19273 .as_singleton()
19274 .unwrap()
19275 .read(cx)
19276 .preview_edits(text_anchor_edits.into(), cx)
19277 })
19278 .unwrap()
19279 .await;
19280
19281 cx.update(|_window, cx| {
19282 let highlighted_edits = inline_completion_edit_text(
19283 &snapshot.as_singleton().unwrap().2,
19284 &edits,
19285 &edit_preview,
19286 include_deletions,
19287 cx,
19288 );
19289 assertion_fn(highlighted_edits, cx)
19290 });
19291}
19292
19293#[track_caller]
19294fn assert_breakpoint(
19295 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
19296 path: &Arc<Path>,
19297 expected: Vec<(u32, Breakpoint)>,
19298) {
19299 if expected.len() == 0usize {
19300 assert!(!breakpoints.contains_key(path), "{}", path.display());
19301 } else {
19302 let mut breakpoint = breakpoints
19303 .get(path)
19304 .unwrap()
19305 .into_iter()
19306 .map(|breakpoint| {
19307 (
19308 breakpoint.row,
19309 Breakpoint {
19310 message: breakpoint.message.clone(),
19311 state: breakpoint.state,
19312 condition: breakpoint.condition.clone(),
19313 hit_condition: breakpoint.hit_condition.clone(),
19314 },
19315 )
19316 })
19317 .collect::<Vec<_>>();
19318
19319 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
19320
19321 assert_eq!(expected, breakpoint);
19322 }
19323}
19324
19325fn add_log_breakpoint_at_cursor(
19326 editor: &mut Editor,
19327 log_message: &str,
19328 window: &mut Window,
19329 cx: &mut Context<Editor>,
19330) {
19331 let (anchor, bp) = editor
19332 .breakpoints_at_cursors(window, cx)
19333 .first()
19334 .and_then(|(anchor, bp)| {
19335 if let Some(bp) = bp {
19336 Some((*anchor, bp.clone()))
19337 } else {
19338 None
19339 }
19340 })
19341 .unwrap_or_else(|| {
19342 let cursor_position: Point = editor.selections.newest(cx).head();
19343
19344 let breakpoint_position = editor
19345 .snapshot(window, cx)
19346 .display_snapshot
19347 .buffer_snapshot
19348 .anchor_before(Point::new(cursor_position.row, 0));
19349
19350 (breakpoint_position, Breakpoint::new_log(&log_message))
19351 });
19352
19353 editor.edit_breakpoint_at_anchor(
19354 anchor,
19355 bp,
19356 BreakpointEditAction::EditLogMessage(log_message.into()),
19357 cx,
19358 );
19359}
19360
19361#[gpui::test]
19362async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
19363 init_test(cx, |_| {});
19364
19365 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19366 let fs = FakeFs::new(cx.executor());
19367 fs.insert_tree(
19368 path!("/a"),
19369 json!({
19370 "main.rs": sample_text,
19371 }),
19372 )
19373 .await;
19374 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19375 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19376 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19377
19378 let fs = FakeFs::new(cx.executor());
19379 fs.insert_tree(
19380 path!("/a"),
19381 json!({
19382 "main.rs": sample_text,
19383 }),
19384 )
19385 .await;
19386 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19387 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19388 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19389 let worktree_id = workspace
19390 .update(cx, |workspace, _window, cx| {
19391 workspace.project().update(cx, |project, cx| {
19392 project.worktrees(cx).next().unwrap().read(cx).id()
19393 })
19394 })
19395 .unwrap();
19396
19397 let buffer = project
19398 .update(cx, |project, cx| {
19399 project.open_buffer((worktree_id, "main.rs"), cx)
19400 })
19401 .await
19402 .unwrap();
19403
19404 let (editor, cx) = cx.add_window_view(|window, cx| {
19405 Editor::new(
19406 EditorMode::full(),
19407 MultiBuffer::build_from_buffer(buffer, cx),
19408 Some(project.clone()),
19409 window,
19410 cx,
19411 )
19412 });
19413
19414 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19415 let abs_path = project.read_with(cx, |project, cx| {
19416 project
19417 .absolute_path(&project_path, cx)
19418 .map(|path_buf| Arc::from(path_buf.to_owned()))
19419 .unwrap()
19420 });
19421
19422 // assert we can add breakpoint on the first line
19423 editor.update_in(cx, |editor, window, cx| {
19424 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19425 editor.move_to_end(&MoveToEnd, window, cx);
19426 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19427 });
19428
19429 let breakpoints = editor.update(cx, |editor, cx| {
19430 editor
19431 .breakpoint_store()
19432 .as_ref()
19433 .unwrap()
19434 .read(cx)
19435 .all_source_breakpoints(cx)
19436 .clone()
19437 });
19438
19439 assert_eq!(1, breakpoints.len());
19440 assert_breakpoint(
19441 &breakpoints,
19442 &abs_path,
19443 vec![
19444 (0, Breakpoint::new_standard()),
19445 (3, Breakpoint::new_standard()),
19446 ],
19447 );
19448
19449 editor.update_in(cx, |editor, window, cx| {
19450 editor.move_to_beginning(&MoveToBeginning, window, cx);
19451 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19452 });
19453
19454 let breakpoints = editor.update(cx, |editor, cx| {
19455 editor
19456 .breakpoint_store()
19457 .as_ref()
19458 .unwrap()
19459 .read(cx)
19460 .all_source_breakpoints(cx)
19461 .clone()
19462 });
19463
19464 assert_eq!(1, breakpoints.len());
19465 assert_breakpoint(
19466 &breakpoints,
19467 &abs_path,
19468 vec![(3, Breakpoint::new_standard())],
19469 );
19470
19471 editor.update_in(cx, |editor, window, cx| {
19472 editor.move_to_end(&MoveToEnd, window, cx);
19473 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19474 });
19475
19476 let breakpoints = editor.update(cx, |editor, cx| {
19477 editor
19478 .breakpoint_store()
19479 .as_ref()
19480 .unwrap()
19481 .read(cx)
19482 .all_source_breakpoints(cx)
19483 .clone()
19484 });
19485
19486 assert_eq!(0, breakpoints.len());
19487 assert_breakpoint(&breakpoints, &abs_path, vec![]);
19488}
19489
19490#[gpui::test]
19491async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
19492 init_test(cx, |_| {});
19493
19494 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19495
19496 let fs = FakeFs::new(cx.executor());
19497 fs.insert_tree(
19498 path!("/a"),
19499 json!({
19500 "main.rs": sample_text,
19501 }),
19502 )
19503 .await;
19504 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19505 let (workspace, cx) =
19506 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19507
19508 let worktree_id = workspace.update(cx, |workspace, cx| {
19509 workspace.project().update(cx, |project, cx| {
19510 project.worktrees(cx).next().unwrap().read(cx).id()
19511 })
19512 });
19513
19514 let buffer = project
19515 .update(cx, |project, cx| {
19516 project.open_buffer((worktree_id, "main.rs"), cx)
19517 })
19518 .await
19519 .unwrap();
19520
19521 let (editor, cx) = cx.add_window_view(|window, cx| {
19522 Editor::new(
19523 EditorMode::full(),
19524 MultiBuffer::build_from_buffer(buffer, cx),
19525 Some(project.clone()),
19526 window,
19527 cx,
19528 )
19529 });
19530
19531 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19532 let abs_path = project.read_with(cx, |project, cx| {
19533 project
19534 .absolute_path(&project_path, cx)
19535 .map(|path_buf| Arc::from(path_buf.to_owned()))
19536 .unwrap()
19537 });
19538
19539 editor.update_in(cx, |editor, window, cx| {
19540 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
19541 });
19542
19543 let breakpoints = editor.update(cx, |editor, cx| {
19544 editor
19545 .breakpoint_store()
19546 .as_ref()
19547 .unwrap()
19548 .read(cx)
19549 .all_source_breakpoints(cx)
19550 .clone()
19551 });
19552
19553 assert_breakpoint(
19554 &breakpoints,
19555 &abs_path,
19556 vec![(0, Breakpoint::new_log("hello world"))],
19557 );
19558
19559 // Removing a log message from a log breakpoint should remove it
19560 editor.update_in(cx, |editor, window, cx| {
19561 add_log_breakpoint_at_cursor(editor, "", window, cx);
19562 });
19563
19564 let breakpoints = editor.update(cx, |editor, cx| {
19565 editor
19566 .breakpoint_store()
19567 .as_ref()
19568 .unwrap()
19569 .read(cx)
19570 .all_source_breakpoints(cx)
19571 .clone()
19572 });
19573
19574 assert_breakpoint(&breakpoints, &abs_path, vec![]);
19575
19576 editor.update_in(cx, |editor, window, cx| {
19577 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19578 editor.move_to_end(&MoveToEnd, window, cx);
19579 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19580 // Not adding a log message to a standard breakpoint shouldn't remove it
19581 add_log_breakpoint_at_cursor(editor, "", window, cx);
19582 });
19583
19584 let breakpoints = editor.update(cx, |editor, cx| {
19585 editor
19586 .breakpoint_store()
19587 .as_ref()
19588 .unwrap()
19589 .read(cx)
19590 .all_source_breakpoints(cx)
19591 .clone()
19592 });
19593
19594 assert_breakpoint(
19595 &breakpoints,
19596 &abs_path,
19597 vec![
19598 (0, Breakpoint::new_standard()),
19599 (3, Breakpoint::new_standard()),
19600 ],
19601 );
19602
19603 editor.update_in(cx, |editor, window, cx| {
19604 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
19605 });
19606
19607 let breakpoints = editor.update(cx, |editor, cx| {
19608 editor
19609 .breakpoint_store()
19610 .as_ref()
19611 .unwrap()
19612 .read(cx)
19613 .all_source_breakpoints(cx)
19614 .clone()
19615 });
19616
19617 assert_breakpoint(
19618 &breakpoints,
19619 &abs_path,
19620 vec![
19621 (0, Breakpoint::new_standard()),
19622 (3, Breakpoint::new_log("hello world")),
19623 ],
19624 );
19625
19626 editor.update_in(cx, |editor, window, cx| {
19627 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
19628 });
19629
19630 let breakpoints = editor.update(cx, |editor, cx| {
19631 editor
19632 .breakpoint_store()
19633 .as_ref()
19634 .unwrap()
19635 .read(cx)
19636 .all_source_breakpoints(cx)
19637 .clone()
19638 });
19639
19640 assert_breakpoint(
19641 &breakpoints,
19642 &abs_path,
19643 vec![
19644 (0, Breakpoint::new_standard()),
19645 (3, Breakpoint::new_log("hello Earth!!")),
19646 ],
19647 );
19648}
19649
19650/// This also tests that Editor::breakpoint_at_cursor_head is working properly
19651/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
19652/// or when breakpoints were placed out of order. This tests for a regression too
19653#[gpui::test]
19654async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
19655 init_test(cx, |_| {});
19656
19657 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19658 let fs = FakeFs::new(cx.executor());
19659 fs.insert_tree(
19660 path!("/a"),
19661 json!({
19662 "main.rs": sample_text,
19663 }),
19664 )
19665 .await;
19666 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19667 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19668 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19669
19670 let fs = FakeFs::new(cx.executor());
19671 fs.insert_tree(
19672 path!("/a"),
19673 json!({
19674 "main.rs": sample_text,
19675 }),
19676 )
19677 .await;
19678 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19679 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19680 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19681 let worktree_id = workspace
19682 .update(cx, |workspace, _window, cx| {
19683 workspace.project().update(cx, |project, cx| {
19684 project.worktrees(cx).next().unwrap().read(cx).id()
19685 })
19686 })
19687 .unwrap();
19688
19689 let buffer = project
19690 .update(cx, |project, cx| {
19691 project.open_buffer((worktree_id, "main.rs"), cx)
19692 })
19693 .await
19694 .unwrap();
19695
19696 let (editor, cx) = cx.add_window_view(|window, cx| {
19697 Editor::new(
19698 EditorMode::full(),
19699 MultiBuffer::build_from_buffer(buffer, cx),
19700 Some(project.clone()),
19701 window,
19702 cx,
19703 )
19704 });
19705
19706 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19707 let abs_path = project.read_with(cx, |project, cx| {
19708 project
19709 .absolute_path(&project_path, cx)
19710 .map(|path_buf| Arc::from(path_buf.to_owned()))
19711 .unwrap()
19712 });
19713
19714 // assert we can add breakpoint on the first line
19715 editor.update_in(cx, |editor, window, cx| {
19716 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19717 editor.move_to_end(&MoveToEnd, window, cx);
19718 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19719 editor.move_up(&MoveUp, window, cx);
19720 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19721 });
19722
19723 let breakpoints = editor.update(cx, |editor, cx| {
19724 editor
19725 .breakpoint_store()
19726 .as_ref()
19727 .unwrap()
19728 .read(cx)
19729 .all_source_breakpoints(cx)
19730 .clone()
19731 });
19732
19733 assert_eq!(1, breakpoints.len());
19734 assert_breakpoint(
19735 &breakpoints,
19736 &abs_path,
19737 vec![
19738 (0, Breakpoint::new_standard()),
19739 (2, Breakpoint::new_standard()),
19740 (3, Breakpoint::new_standard()),
19741 ],
19742 );
19743
19744 editor.update_in(cx, |editor, window, cx| {
19745 editor.move_to_beginning(&MoveToBeginning, window, cx);
19746 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19747 editor.move_to_end(&MoveToEnd, window, cx);
19748 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19749 // Disabling a breakpoint that doesn't exist should do nothing
19750 editor.move_up(&MoveUp, window, cx);
19751 editor.move_up(&MoveUp, window, cx);
19752 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19753 });
19754
19755 let breakpoints = editor.update(cx, |editor, cx| {
19756 editor
19757 .breakpoint_store()
19758 .as_ref()
19759 .unwrap()
19760 .read(cx)
19761 .all_source_breakpoints(cx)
19762 .clone()
19763 });
19764
19765 let disable_breakpoint = {
19766 let mut bp = Breakpoint::new_standard();
19767 bp.state = BreakpointState::Disabled;
19768 bp
19769 };
19770
19771 assert_eq!(1, breakpoints.len());
19772 assert_breakpoint(
19773 &breakpoints,
19774 &abs_path,
19775 vec![
19776 (0, disable_breakpoint.clone()),
19777 (2, Breakpoint::new_standard()),
19778 (3, disable_breakpoint.clone()),
19779 ],
19780 );
19781
19782 editor.update_in(cx, |editor, window, cx| {
19783 editor.move_to_beginning(&MoveToBeginning, window, cx);
19784 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
19785 editor.move_to_end(&MoveToEnd, window, cx);
19786 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
19787 editor.move_up(&MoveUp, window, cx);
19788 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19789 });
19790
19791 let breakpoints = editor.update(cx, |editor, cx| {
19792 editor
19793 .breakpoint_store()
19794 .as_ref()
19795 .unwrap()
19796 .read(cx)
19797 .all_source_breakpoints(cx)
19798 .clone()
19799 });
19800
19801 assert_eq!(1, breakpoints.len());
19802 assert_breakpoint(
19803 &breakpoints,
19804 &abs_path,
19805 vec![
19806 (0, Breakpoint::new_standard()),
19807 (2, disable_breakpoint),
19808 (3, Breakpoint::new_standard()),
19809 ],
19810 );
19811}
19812
19813#[gpui::test]
19814async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
19815 init_test(cx, |_| {});
19816 let capabilities = lsp::ServerCapabilities {
19817 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
19818 prepare_provider: Some(true),
19819 work_done_progress_options: Default::default(),
19820 })),
19821 ..Default::default()
19822 };
19823 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
19824
19825 cx.set_state(indoc! {"
19826 struct Fˇoo {}
19827 "});
19828
19829 cx.update_editor(|editor, _, cx| {
19830 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
19831 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
19832 editor.highlight_background::<DocumentHighlightRead>(
19833 &[highlight_range],
19834 |c| c.editor_document_highlight_read_background,
19835 cx,
19836 );
19837 });
19838
19839 let mut prepare_rename_handler = cx
19840 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
19841 move |_, _, _| async move {
19842 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
19843 start: lsp::Position {
19844 line: 0,
19845 character: 7,
19846 },
19847 end: lsp::Position {
19848 line: 0,
19849 character: 10,
19850 },
19851 })))
19852 },
19853 );
19854 let prepare_rename_task = cx
19855 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
19856 .expect("Prepare rename was not started");
19857 prepare_rename_handler.next().await.unwrap();
19858 prepare_rename_task.await.expect("Prepare rename failed");
19859
19860 let mut rename_handler =
19861 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
19862 let edit = lsp::TextEdit {
19863 range: lsp::Range {
19864 start: lsp::Position {
19865 line: 0,
19866 character: 7,
19867 },
19868 end: lsp::Position {
19869 line: 0,
19870 character: 10,
19871 },
19872 },
19873 new_text: "FooRenamed".to_string(),
19874 };
19875 Ok(Some(lsp::WorkspaceEdit::new(
19876 // Specify the same edit twice
19877 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
19878 )))
19879 });
19880 let rename_task = cx
19881 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
19882 .expect("Confirm rename was not started");
19883 rename_handler.next().await.unwrap();
19884 rename_task.await.expect("Confirm rename failed");
19885 cx.run_until_parked();
19886
19887 // Despite two edits, only one is actually applied as those are identical
19888 cx.assert_editor_state(indoc! {"
19889 struct FooRenamedˇ {}
19890 "});
19891}
19892
19893#[gpui::test]
19894async fn test_rename_without_prepare(cx: &mut TestAppContext) {
19895 init_test(cx, |_| {});
19896 // These capabilities indicate that the server does not support prepare rename.
19897 let capabilities = lsp::ServerCapabilities {
19898 rename_provider: Some(lsp::OneOf::Left(true)),
19899 ..Default::default()
19900 };
19901 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
19902
19903 cx.set_state(indoc! {"
19904 struct Fˇoo {}
19905 "});
19906
19907 cx.update_editor(|editor, _window, cx| {
19908 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
19909 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
19910 editor.highlight_background::<DocumentHighlightRead>(
19911 &[highlight_range],
19912 |c| c.editor_document_highlight_read_background,
19913 cx,
19914 );
19915 });
19916
19917 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
19918 .expect("Prepare rename was not started")
19919 .await
19920 .expect("Prepare rename failed");
19921
19922 let mut rename_handler =
19923 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
19924 let edit = lsp::TextEdit {
19925 range: lsp::Range {
19926 start: lsp::Position {
19927 line: 0,
19928 character: 7,
19929 },
19930 end: lsp::Position {
19931 line: 0,
19932 character: 10,
19933 },
19934 },
19935 new_text: "FooRenamed".to_string(),
19936 };
19937 Ok(Some(lsp::WorkspaceEdit::new(
19938 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
19939 )))
19940 });
19941 let rename_task = cx
19942 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
19943 .expect("Confirm rename was not started");
19944 rename_handler.next().await.unwrap();
19945 rename_task.await.expect("Confirm rename failed");
19946 cx.run_until_parked();
19947
19948 // Correct range is renamed, as `surrounding_word` is used to find it.
19949 cx.assert_editor_state(indoc! {"
19950 struct FooRenamedˇ {}
19951 "});
19952}
19953
19954#[gpui::test]
19955async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
19956 init_test(cx, |_| {});
19957 let mut cx = EditorTestContext::new(cx).await;
19958
19959 let language = Arc::new(
19960 Language::new(
19961 LanguageConfig::default(),
19962 Some(tree_sitter_html::LANGUAGE.into()),
19963 )
19964 .with_brackets_query(
19965 r#"
19966 ("<" @open "/>" @close)
19967 ("</" @open ">" @close)
19968 ("<" @open ">" @close)
19969 ("\"" @open "\"" @close)
19970 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
19971 "#,
19972 )
19973 .unwrap(),
19974 );
19975 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
19976
19977 cx.set_state(indoc! {"
19978 <span>ˇ</span>
19979 "});
19980 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19981 cx.assert_editor_state(indoc! {"
19982 <span>
19983 ˇ
19984 </span>
19985 "});
19986
19987 cx.set_state(indoc! {"
19988 <span><span></span>ˇ</span>
19989 "});
19990 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19991 cx.assert_editor_state(indoc! {"
19992 <span><span></span>
19993 ˇ</span>
19994 "});
19995
19996 cx.set_state(indoc! {"
19997 <span>ˇ
19998 </span>
19999 "});
20000 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
20001 cx.assert_editor_state(indoc! {"
20002 <span>
20003 ˇ
20004 </span>
20005 "});
20006}
20007
20008#[gpui::test(iterations = 10)]
20009async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
20010 init_test(cx, |_| {});
20011
20012 let fs = FakeFs::new(cx.executor());
20013 fs.insert_tree(
20014 path!("/dir"),
20015 json!({
20016 "a.ts": "a",
20017 }),
20018 )
20019 .await;
20020
20021 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
20022 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20023 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20024
20025 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20026 language_registry.add(Arc::new(Language::new(
20027 LanguageConfig {
20028 name: "TypeScript".into(),
20029 matcher: LanguageMatcher {
20030 path_suffixes: vec!["ts".to_string()],
20031 ..Default::default()
20032 },
20033 ..Default::default()
20034 },
20035 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
20036 )));
20037 let mut fake_language_servers = language_registry.register_fake_lsp(
20038 "TypeScript",
20039 FakeLspAdapter {
20040 capabilities: lsp::ServerCapabilities {
20041 code_lens_provider: Some(lsp::CodeLensOptions {
20042 resolve_provider: Some(true),
20043 }),
20044 execute_command_provider: Some(lsp::ExecuteCommandOptions {
20045 commands: vec!["_the/command".to_string()],
20046 ..lsp::ExecuteCommandOptions::default()
20047 }),
20048 ..lsp::ServerCapabilities::default()
20049 },
20050 ..FakeLspAdapter::default()
20051 },
20052 );
20053
20054 let (buffer, _handle) = project
20055 .update(cx, |p, cx| {
20056 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
20057 })
20058 .await
20059 .unwrap();
20060 cx.executor().run_until_parked();
20061
20062 let fake_server = fake_language_servers.next().await.unwrap();
20063
20064 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
20065 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
20066 drop(buffer_snapshot);
20067 let actions = cx
20068 .update_window(*workspace, |_, window, cx| {
20069 project.code_actions(&buffer, anchor..anchor, window, cx)
20070 })
20071 .unwrap();
20072
20073 fake_server
20074 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
20075 Ok(Some(vec![
20076 lsp::CodeLens {
20077 range: lsp::Range::default(),
20078 command: Some(lsp::Command {
20079 title: "Code lens command".to_owned(),
20080 command: "_the/command".to_owned(),
20081 arguments: None,
20082 }),
20083 data: None,
20084 },
20085 lsp::CodeLens {
20086 range: lsp::Range::default(),
20087 command: Some(lsp::Command {
20088 title: "Command not in capabilities".to_owned(),
20089 command: "not in capabilities".to_owned(),
20090 arguments: None,
20091 }),
20092 data: None,
20093 },
20094 lsp::CodeLens {
20095 range: lsp::Range {
20096 start: lsp::Position {
20097 line: 1,
20098 character: 1,
20099 },
20100 end: lsp::Position {
20101 line: 1,
20102 character: 1,
20103 },
20104 },
20105 command: Some(lsp::Command {
20106 title: "Command not in range".to_owned(),
20107 command: "_the/command".to_owned(),
20108 arguments: None,
20109 }),
20110 data: None,
20111 },
20112 ]))
20113 })
20114 .next()
20115 .await;
20116
20117 let actions = actions.await.unwrap();
20118 assert_eq!(
20119 actions.len(),
20120 1,
20121 "Should have only one valid action for the 0..0 range"
20122 );
20123 let action = actions[0].clone();
20124 let apply = project.update(cx, |project, cx| {
20125 project.apply_code_action(buffer.clone(), action, true, cx)
20126 });
20127
20128 // Resolving the code action does not populate its edits. In absence of
20129 // edits, we must execute the given command.
20130 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
20131 |mut lens, _| async move {
20132 let lens_command = lens.command.as_mut().expect("should have a command");
20133 assert_eq!(lens_command.title, "Code lens command");
20134 lens_command.arguments = Some(vec![json!("the-argument")]);
20135 Ok(lens)
20136 },
20137 );
20138
20139 // While executing the command, the language server sends the editor
20140 // a `workspaceEdit` request.
20141 fake_server
20142 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
20143 let fake = fake_server.clone();
20144 move |params, _| {
20145 assert_eq!(params.command, "_the/command");
20146 let fake = fake.clone();
20147 async move {
20148 fake.server
20149 .request::<lsp::request::ApplyWorkspaceEdit>(
20150 lsp::ApplyWorkspaceEditParams {
20151 label: None,
20152 edit: lsp::WorkspaceEdit {
20153 changes: Some(
20154 [(
20155 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
20156 vec![lsp::TextEdit {
20157 range: lsp::Range::new(
20158 lsp::Position::new(0, 0),
20159 lsp::Position::new(0, 0),
20160 ),
20161 new_text: "X".into(),
20162 }],
20163 )]
20164 .into_iter()
20165 .collect(),
20166 ),
20167 ..Default::default()
20168 },
20169 },
20170 )
20171 .await
20172 .into_response()
20173 .unwrap();
20174 Ok(Some(json!(null)))
20175 }
20176 }
20177 })
20178 .next()
20179 .await;
20180
20181 // Applying the code lens command returns a project transaction containing the edits
20182 // sent by the language server in its `workspaceEdit` request.
20183 let transaction = apply.await.unwrap();
20184 assert!(transaction.0.contains_key(&buffer));
20185 buffer.update(cx, |buffer, cx| {
20186 assert_eq!(buffer.text(), "Xa");
20187 buffer.undo(cx);
20188 assert_eq!(buffer.text(), "a");
20189 });
20190}
20191
20192#[gpui::test]
20193async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
20194 init_test(cx, |_| {});
20195
20196 let fs = FakeFs::new(cx.executor());
20197 let main_text = r#"fn main() {
20198println!("1");
20199println!("2");
20200println!("3");
20201println!("4");
20202println!("5");
20203}"#;
20204 let lib_text = "mod foo {}";
20205 fs.insert_tree(
20206 path!("/a"),
20207 json!({
20208 "lib.rs": lib_text,
20209 "main.rs": main_text,
20210 }),
20211 )
20212 .await;
20213
20214 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20215 let (workspace, cx) =
20216 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20217 let worktree_id = workspace.update(cx, |workspace, cx| {
20218 workspace.project().update(cx, |project, cx| {
20219 project.worktrees(cx).next().unwrap().read(cx).id()
20220 })
20221 });
20222
20223 let expected_ranges = vec![
20224 Point::new(0, 0)..Point::new(0, 0),
20225 Point::new(1, 0)..Point::new(1, 1),
20226 Point::new(2, 0)..Point::new(2, 2),
20227 Point::new(3, 0)..Point::new(3, 3),
20228 ];
20229
20230 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20231 let editor_1 = workspace
20232 .update_in(cx, |workspace, window, cx| {
20233 workspace.open_path(
20234 (worktree_id, "main.rs"),
20235 Some(pane_1.downgrade()),
20236 true,
20237 window,
20238 cx,
20239 )
20240 })
20241 .unwrap()
20242 .await
20243 .downcast::<Editor>()
20244 .unwrap();
20245 pane_1.update(cx, |pane, cx| {
20246 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20247 open_editor.update(cx, |editor, cx| {
20248 assert_eq!(
20249 editor.display_text(cx),
20250 main_text,
20251 "Original main.rs text on initial open",
20252 );
20253 assert_eq!(
20254 editor
20255 .selections
20256 .all::<Point>(cx)
20257 .into_iter()
20258 .map(|s| s.range())
20259 .collect::<Vec<_>>(),
20260 vec![Point::zero()..Point::zero()],
20261 "Default selections on initial open",
20262 );
20263 })
20264 });
20265 editor_1.update_in(cx, |editor, window, cx| {
20266 editor.change_selections(None, window, cx, |s| {
20267 s.select_ranges(expected_ranges.clone());
20268 });
20269 });
20270
20271 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
20272 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
20273 });
20274 let editor_2 = workspace
20275 .update_in(cx, |workspace, window, cx| {
20276 workspace.open_path(
20277 (worktree_id, "main.rs"),
20278 Some(pane_2.downgrade()),
20279 true,
20280 window,
20281 cx,
20282 )
20283 })
20284 .unwrap()
20285 .await
20286 .downcast::<Editor>()
20287 .unwrap();
20288 pane_2.update(cx, |pane, cx| {
20289 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20290 open_editor.update(cx, |editor, cx| {
20291 assert_eq!(
20292 editor.display_text(cx),
20293 main_text,
20294 "Original main.rs text on initial open in another panel",
20295 );
20296 assert_eq!(
20297 editor
20298 .selections
20299 .all::<Point>(cx)
20300 .into_iter()
20301 .map(|s| s.range())
20302 .collect::<Vec<_>>(),
20303 vec![Point::zero()..Point::zero()],
20304 "Default selections on initial open in another panel",
20305 );
20306 })
20307 });
20308
20309 editor_2.update_in(cx, |editor, window, cx| {
20310 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
20311 });
20312
20313 let _other_editor_1 = workspace
20314 .update_in(cx, |workspace, window, cx| {
20315 workspace.open_path(
20316 (worktree_id, "lib.rs"),
20317 Some(pane_1.downgrade()),
20318 true,
20319 window,
20320 cx,
20321 )
20322 })
20323 .unwrap()
20324 .await
20325 .downcast::<Editor>()
20326 .unwrap();
20327 pane_1
20328 .update_in(cx, |pane, window, cx| {
20329 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
20330 })
20331 .await
20332 .unwrap();
20333 drop(editor_1);
20334 pane_1.update(cx, |pane, cx| {
20335 pane.active_item()
20336 .unwrap()
20337 .downcast::<Editor>()
20338 .unwrap()
20339 .update(cx, |editor, cx| {
20340 assert_eq!(
20341 editor.display_text(cx),
20342 lib_text,
20343 "Other file should be open and active",
20344 );
20345 });
20346 assert_eq!(pane.items().count(), 1, "No other editors should be open");
20347 });
20348
20349 let _other_editor_2 = workspace
20350 .update_in(cx, |workspace, window, cx| {
20351 workspace.open_path(
20352 (worktree_id, "lib.rs"),
20353 Some(pane_2.downgrade()),
20354 true,
20355 window,
20356 cx,
20357 )
20358 })
20359 .unwrap()
20360 .await
20361 .downcast::<Editor>()
20362 .unwrap();
20363 pane_2
20364 .update_in(cx, |pane, window, cx| {
20365 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
20366 })
20367 .await
20368 .unwrap();
20369 drop(editor_2);
20370 pane_2.update(cx, |pane, cx| {
20371 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20372 open_editor.update(cx, |editor, cx| {
20373 assert_eq!(
20374 editor.display_text(cx),
20375 lib_text,
20376 "Other file should be open and active in another panel too",
20377 );
20378 });
20379 assert_eq!(
20380 pane.items().count(),
20381 1,
20382 "No other editors should be open in another pane",
20383 );
20384 });
20385
20386 let _editor_1_reopened = workspace
20387 .update_in(cx, |workspace, window, cx| {
20388 workspace.open_path(
20389 (worktree_id, "main.rs"),
20390 Some(pane_1.downgrade()),
20391 true,
20392 window,
20393 cx,
20394 )
20395 })
20396 .unwrap()
20397 .await
20398 .downcast::<Editor>()
20399 .unwrap();
20400 let _editor_2_reopened = workspace
20401 .update_in(cx, |workspace, window, cx| {
20402 workspace.open_path(
20403 (worktree_id, "main.rs"),
20404 Some(pane_2.downgrade()),
20405 true,
20406 window,
20407 cx,
20408 )
20409 })
20410 .unwrap()
20411 .await
20412 .downcast::<Editor>()
20413 .unwrap();
20414 pane_1.update(cx, |pane, cx| {
20415 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20416 open_editor.update(cx, |editor, cx| {
20417 assert_eq!(
20418 editor.display_text(cx),
20419 main_text,
20420 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
20421 );
20422 assert_eq!(
20423 editor
20424 .selections
20425 .all::<Point>(cx)
20426 .into_iter()
20427 .map(|s| s.range())
20428 .collect::<Vec<_>>(),
20429 expected_ranges,
20430 "Previous editor in the 1st panel had selections and should get them restored on reopen",
20431 );
20432 })
20433 });
20434 pane_2.update(cx, |pane, cx| {
20435 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20436 open_editor.update(cx, |editor, cx| {
20437 assert_eq!(
20438 editor.display_text(cx),
20439 r#"fn main() {
20440⋯rintln!("1");
20441⋯intln!("2");
20442⋯ntln!("3");
20443println!("4");
20444println!("5");
20445}"#,
20446 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
20447 );
20448 assert_eq!(
20449 editor
20450 .selections
20451 .all::<Point>(cx)
20452 .into_iter()
20453 .map(|s| s.range())
20454 .collect::<Vec<_>>(),
20455 vec![Point::zero()..Point::zero()],
20456 "Previous editor in the 2nd pane had no selections changed hence should restore none",
20457 );
20458 })
20459 });
20460}
20461
20462#[gpui::test]
20463async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
20464 init_test(cx, |_| {});
20465
20466 let fs = FakeFs::new(cx.executor());
20467 let main_text = r#"fn main() {
20468println!("1");
20469println!("2");
20470println!("3");
20471println!("4");
20472println!("5");
20473}"#;
20474 let lib_text = "mod foo {}";
20475 fs.insert_tree(
20476 path!("/a"),
20477 json!({
20478 "lib.rs": lib_text,
20479 "main.rs": main_text,
20480 }),
20481 )
20482 .await;
20483
20484 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20485 let (workspace, cx) =
20486 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20487 let worktree_id = workspace.update(cx, |workspace, cx| {
20488 workspace.project().update(cx, |project, cx| {
20489 project.worktrees(cx).next().unwrap().read(cx).id()
20490 })
20491 });
20492
20493 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20494 let editor = workspace
20495 .update_in(cx, |workspace, window, cx| {
20496 workspace.open_path(
20497 (worktree_id, "main.rs"),
20498 Some(pane.downgrade()),
20499 true,
20500 window,
20501 cx,
20502 )
20503 })
20504 .unwrap()
20505 .await
20506 .downcast::<Editor>()
20507 .unwrap();
20508 pane.update(cx, |pane, cx| {
20509 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20510 open_editor.update(cx, |editor, cx| {
20511 assert_eq!(
20512 editor.display_text(cx),
20513 main_text,
20514 "Original main.rs text on initial open",
20515 );
20516 })
20517 });
20518 editor.update_in(cx, |editor, window, cx| {
20519 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
20520 });
20521
20522 cx.update_global(|store: &mut SettingsStore, cx| {
20523 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
20524 s.restore_on_file_reopen = Some(false);
20525 });
20526 });
20527 editor.update_in(cx, |editor, window, cx| {
20528 editor.fold_ranges(
20529 vec![
20530 Point::new(1, 0)..Point::new(1, 1),
20531 Point::new(2, 0)..Point::new(2, 2),
20532 Point::new(3, 0)..Point::new(3, 3),
20533 ],
20534 false,
20535 window,
20536 cx,
20537 );
20538 });
20539 pane.update_in(cx, |pane, window, cx| {
20540 pane.close_all_items(&CloseAllItems::default(), window, cx)
20541 })
20542 .await
20543 .unwrap();
20544 pane.update(cx, |pane, _| {
20545 assert!(pane.active_item().is_none());
20546 });
20547 cx.update_global(|store: &mut SettingsStore, cx| {
20548 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
20549 s.restore_on_file_reopen = Some(true);
20550 });
20551 });
20552
20553 let _editor_reopened = workspace
20554 .update_in(cx, |workspace, window, cx| {
20555 workspace.open_path(
20556 (worktree_id, "main.rs"),
20557 Some(pane.downgrade()),
20558 true,
20559 window,
20560 cx,
20561 )
20562 })
20563 .unwrap()
20564 .await
20565 .downcast::<Editor>()
20566 .unwrap();
20567 pane.update(cx, |pane, cx| {
20568 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20569 open_editor.update(cx, |editor, cx| {
20570 assert_eq!(
20571 editor.display_text(cx),
20572 main_text,
20573 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
20574 );
20575 })
20576 });
20577}
20578
20579#[gpui::test]
20580async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
20581 struct EmptyModalView {
20582 focus_handle: gpui::FocusHandle,
20583 }
20584 impl EventEmitter<DismissEvent> for EmptyModalView {}
20585 impl Render for EmptyModalView {
20586 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
20587 div()
20588 }
20589 }
20590 impl Focusable for EmptyModalView {
20591 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
20592 self.focus_handle.clone()
20593 }
20594 }
20595 impl workspace::ModalView for EmptyModalView {}
20596 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
20597 EmptyModalView {
20598 focus_handle: cx.focus_handle(),
20599 }
20600 }
20601
20602 init_test(cx, |_| {});
20603
20604 let fs = FakeFs::new(cx.executor());
20605 let project = Project::test(fs, [], cx).await;
20606 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20607 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
20608 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20609 let editor = cx.new_window_entity(|window, cx| {
20610 Editor::new(
20611 EditorMode::full(),
20612 buffer,
20613 Some(project.clone()),
20614 window,
20615 cx,
20616 )
20617 });
20618 workspace
20619 .update(cx, |workspace, window, cx| {
20620 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
20621 })
20622 .unwrap();
20623 editor.update_in(cx, |editor, window, cx| {
20624 editor.open_context_menu(&OpenContextMenu, window, cx);
20625 assert!(editor.mouse_context_menu.is_some());
20626 });
20627 workspace
20628 .update(cx, |workspace, window, cx| {
20629 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
20630 })
20631 .unwrap();
20632 cx.read(|cx| {
20633 assert!(editor.read(cx).mouse_context_menu.is_none());
20634 });
20635}
20636
20637#[gpui::test]
20638async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
20639 init_test(cx, |_| {});
20640
20641 let fs = FakeFs::new(cx.executor());
20642 fs.insert_file(path!("/file.html"), Default::default())
20643 .await;
20644
20645 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
20646
20647 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20648 let html_language = Arc::new(Language::new(
20649 LanguageConfig {
20650 name: "HTML".into(),
20651 matcher: LanguageMatcher {
20652 path_suffixes: vec!["html".to_string()],
20653 ..LanguageMatcher::default()
20654 },
20655 brackets: BracketPairConfig {
20656 pairs: vec![BracketPair {
20657 start: "<".into(),
20658 end: ">".into(),
20659 close: true,
20660 ..Default::default()
20661 }],
20662 ..Default::default()
20663 },
20664 ..Default::default()
20665 },
20666 Some(tree_sitter_html::LANGUAGE.into()),
20667 ));
20668 language_registry.add(html_language);
20669 let mut fake_servers = language_registry.register_fake_lsp(
20670 "HTML",
20671 FakeLspAdapter {
20672 capabilities: lsp::ServerCapabilities {
20673 completion_provider: Some(lsp::CompletionOptions {
20674 resolve_provider: Some(true),
20675 ..Default::default()
20676 }),
20677 ..Default::default()
20678 },
20679 ..Default::default()
20680 },
20681 );
20682
20683 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20684 let cx = &mut VisualTestContext::from_window(*workspace, cx);
20685
20686 let worktree_id = workspace
20687 .update(cx, |workspace, _window, cx| {
20688 workspace.project().update(cx, |project, cx| {
20689 project.worktrees(cx).next().unwrap().read(cx).id()
20690 })
20691 })
20692 .unwrap();
20693 project
20694 .update(cx, |project, cx| {
20695 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
20696 })
20697 .await
20698 .unwrap();
20699 let editor = workspace
20700 .update(cx, |workspace, window, cx| {
20701 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
20702 })
20703 .unwrap()
20704 .await
20705 .unwrap()
20706 .downcast::<Editor>()
20707 .unwrap();
20708
20709 let fake_server = fake_servers.next().await.unwrap();
20710 editor.update_in(cx, |editor, window, cx| {
20711 editor.set_text("<ad></ad>", window, cx);
20712 editor.change_selections(None, window, cx, |selections| {
20713 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
20714 });
20715 let Some((buffer, _)) = editor
20716 .buffer
20717 .read(cx)
20718 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
20719 else {
20720 panic!("Failed to get buffer for selection position");
20721 };
20722 let buffer = buffer.read(cx);
20723 let buffer_id = buffer.remote_id();
20724 let opening_range =
20725 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
20726 let closing_range =
20727 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
20728 let mut linked_ranges = HashMap::default();
20729 linked_ranges.insert(
20730 buffer_id,
20731 vec![(opening_range.clone(), vec![closing_range.clone()])],
20732 );
20733 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
20734 });
20735 let mut completion_handle =
20736 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
20737 Ok(Some(lsp::CompletionResponse::Array(vec![
20738 lsp::CompletionItem {
20739 label: "head".to_string(),
20740 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
20741 lsp::InsertReplaceEdit {
20742 new_text: "head".to_string(),
20743 insert: lsp::Range::new(
20744 lsp::Position::new(0, 1),
20745 lsp::Position::new(0, 3),
20746 ),
20747 replace: lsp::Range::new(
20748 lsp::Position::new(0, 1),
20749 lsp::Position::new(0, 3),
20750 ),
20751 },
20752 )),
20753 ..Default::default()
20754 },
20755 ])))
20756 });
20757 editor.update_in(cx, |editor, window, cx| {
20758 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
20759 });
20760 cx.run_until_parked();
20761 completion_handle.next().await.unwrap();
20762 editor.update(cx, |editor, _| {
20763 assert!(
20764 editor.context_menu_visible(),
20765 "Completion menu should be visible"
20766 );
20767 });
20768 editor.update_in(cx, |editor, window, cx| {
20769 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
20770 });
20771 cx.executor().run_until_parked();
20772 editor.update(cx, |editor, cx| {
20773 assert_eq!(editor.text(cx), "<head></head>");
20774 });
20775}
20776
20777#[gpui::test]
20778async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
20779 init_test(cx, |_| {});
20780
20781 let fs = FakeFs::new(cx.executor());
20782 fs.insert_tree(
20783 path!("/root"),
20784 json!({
20785 "a": {
20786 "main.rs": "fn main() {}",
20787 },
20788 "foo": {
20789 "bar": {
20790 "external_file.rs": "pub mod external {}",
20791 }
20792 }
20793 }),
20794 )
20795 .await;
20796
20797 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
20798 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20799 language_registry.add(rust_lang());
20800 let _fake_servers = language_registry.register_fake_lsp(
20801 "Rust",
20802 FakeLspAdapter {
20803 ..FakeLspAdapter::default()
20804 },
20805 );
20806 let (workspace, cx) =
20807 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20808 let worktree_id = workspace.update(cx, |workspace, cx| {
20809 workspace.project().update(cx, |project, cx| {
20810 project.worktrees(cx).next().unwrap().read(cx).id()
20811 })
20812 });
20813
20814 let assert_language_servers_count =
20815 |expected: usize, context: &str, cx: &mut VisualTestContext| {
20816 project.update(cx, |project, cx| {
20817 let current = project
20818 .lsp_store()
20819 .read(cx)
20820 .as_local()
20821 .unwrap()
20822 .language_servers
20823 .len();
20824 assert_eq!(expected, current, "{context}");
20825 });
20826 };
20827
20828 assert_language_servers_count(
20829 0,
20830 "No servers should be running before any file is open",
20831 cx,
20832 );
20833 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20834 let main_editor = workspace
20835 .update_in(cx, |workspace, window, cx| {
20836 workspace.open_path(
20837 (worktree_id, "main.rs"),
20838 Some(pane.downgrade()),
20839 true,
20840 window,
20841 cx,
20842 )
20843 })
20844 .unwrap()
20845 .await
20846 .downcast::<Editor>()
20847 .unwrap();
20848 pane.update(cx, |pane, cx| {
20849 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20850 open_editor.update(cx, |editor, cx| {
20851 assert_eq!(
20852 editor.display_text(cx),
20853 "fn main() {}",
20854 "Original main.rs text on initial open",
20855 );
20856 });
20857 assert_eq!(open_editor, main_editor);
20858 });
20859 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
20860
20861 let external_editor = workspace
20862 .update_in(cx, |workspace, window, cx| {
20863 workspace.open_abs_path(
20864 PathBuf::from("/root/foo/bar/external_file.rs"),
20865 OpenOptions::default(),
20866 window,
20867 cx,
20868 )
20869 })
20870 .await
20871 .expect("opening external file")
20872 .downcast::<Editor>()
20873 .expect("downcasted external file's open element to editor");
20874 pane.update(cx, |pane, cx| {
20875 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20876 open_editor.update(cx, |editor, cx| {
20877 assert_eq!(
20878 editor.display_text(cx),
20879 "pub mod external {}",
20880 "External file is open now",
20881 );
20882 });
20883 assert_eq!(open_editor, external_editor);
20884 });
20885 assert_language_servers_count(
20886 1,
20887 "Second, external, *.rs file should join the existing server",
20888 cx,
20889 );
20890
20891 pane.update_in(cx, |pane, window, cx| {
20892 pane.close_active_item(&CloseActiveItem::default(), window, cx)
20893 })
20894 .await
20895 .unwrap();
20896 pane.update_in(cx, |pane, window, cx| {
20897 pane.navigate_backward(window, cx);
20898 });
20899 cx.run_until_parked();
20900 pane.update(cx, |pane, cx| {
20901 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20902 open_editor.update(cx, |editor, cx| {
20903 assert_eq!(
20904 editor.display_text(cx),
20905 "pub mod external {}",
20906 "External file is open now",
20907 );
20908 });
20909 });
20910 assert_language_servers_count(
20911 1,
20912 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
20913 cx,
20914 );
20915
20916 cx.update(|_, cx| {
20917 workspace::reload(&workspace::Reload::default(), cx);
20918 });
20919 assert_language_servers_count(
20920 1,
20921 "After reloading the worktree with local and external files opened, only one project should be started",
20922 cx,
20923 );
20924}
20925
20926#[gpui::test]
20927async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
20928 init_test(cx, |_| {});
20929
20930 let mut cx = EditorTestContext::new(cx).await;
20931 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
20932 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20933
20934 // test cursor move to start of each line on tab
20935 // for `if`, `elif`, `else`, `while`, `with` and `for`
20936 cx.set_state(indoc! {"
20937 def main():
20938 ˇ for item in items:
20939 ˇ while item.active:
20940 ˇ if item.value > 10:
20941 ˇ continue
20942 ˇ elif item.value < 0:
20943 ˇ break
20944 ˇ else:
20945 ˇ with item.context() as ctx:
20946 ˇ yield count
20947 ˇ else:
20948 ˇ log('while else')
20949 ˇ else:
20950 ˇ log('for else')
20951 "});
20952 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20953 cx.assert_editor_state(indoc! {"
20954 def main():
20955 ˇfor item in items:
20956 ˇwhile item.active:
20957 ˇif item.value > 10:
20958 ˇcontinue
20959 ˇelif item.value < 0:
20960 ˇbreak
20961 ˇelse:
20962 ˇwith item.context() as ctx:
20963 ˇyield count
20964 ˇelse:
20965 ˇlog('while else')
20966 ˇelse:
20967 ˇlog('for else')
20968 "});
20969 // test relative indent is preserved when tab
20970 // for `if`, `elif`, `else`, `while`, `with` and `for`
20971 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20972 cx.assert_editor_state(indoc! {"
20973 def main():
20974 ˇfor item in items:
20975 ˇwhile item.active:
20976 ˇif item.value > 10:
20977 ˇcontinue
20978 ˇelif item.value < 0:
20979 ˇbreak
20980 ˇelse:
20981 ˇwith item.context() as ctx:
20982 ˇyield count
20983 ˇelse:
20984 ˇlog('while else')
20985 ˇelse:
20986 ˇlog('for else')
20987 "});
20988
20989 // test cursor move to start of each line on tab
20990 // for `try`, `except`, `else`, `finally`, `match` and `def`
20991 cx.set_state(indoc! {"
20992 def main():
20993 ˇ try:
20994 ˇ fetch()
20995 ˇ except ValueError:
20996 ˇ handle_error()
20997 ˇ else:
20998 ˇ match value:
20999 ˇ case _:
21000 ˇ finally:
21001 ˇ def status():
21002 ˇ return 0
21003 "});
21004 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21005 cx.assert_editor_state(indoc! {"
21006 def main():
21007 ˇtry:
21008 ˇfetch()
21009 ˇexcept ValueError:
21010 ˇhandle_error()
21011 ˇelse:
21012 ˇmatch value:
21013 ˇcase _:
21014 ˇfinally:
21015 ˇdef status():
21016 ˇreturn 0
21017 "});
21018 // test relative indent is preserved when tab
21019 // for `try`, `except`, `else`, `finally`, `match` and `def`
21020 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21021 cx.assert_editor_state(indoc! {"
21022 def main():
21023 ˇtry:
21024 ˇfetch()
21025 ˇexcept ValueError:
21026 ˇhandle_error()
21027 ˇelse:
21028 ˇmatch value:
21029 ˇcase _:
21030 ˇfinally:
21031 ˇdef status():
21032 ˇreturn 0
21033 "});
21034}
21035
21036#[gpui::test]
21037async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
21038 init_test(cx, |_| {});
21039
21040 let mut cx = EditorTestContext::new(cx).await;
21041 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
21042 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21043
21044 // test `else` auto outdents when typed inside `if` block
21045 cx.set_state(indoc! {"
21046 def main():
21047 if i == 2:
21048 return
21049 ˇ
21050 "});
21051 cx.update_editor(|editor, window, cx| {
21052 editor.handle_input("else:", window, cx);
21053 });
21054 cx.assert_editor_state(indoc! {"
21055 def main():
21056 if i == 2:
21057 return
21058 else:ˇ
21059 "});
21060
21061 // test `except` auto outdents when typed inside `try` block
21062 cx.set_state(indoc! {"
21063 def main():
21064 try:
21065 i = 2
21066 ˇ
21067 "});
21068 cx.update_editor(|editor, window, cx| {
21069 editor.handle_input("except:", window, cx);
21070 });
21071 cx.assert_editor_state(indoc! {"
21072 def main():
21073 try:
21074 i = 2
21075 except:ˇ
21076 "});
21077
21078 // test `else` auto outdents when typed inside `except` block
21079 cx.set_state(indoc! {"
21080 def main():
21081 try:
21082 i = 2
21083 except:
21084 j = 2
21085 ˇ
21086 "});
21087 cx.update_editor(|editor, window, cx| {
21088 editor.handle_input("else:", window, cx);
21089 });
21090 cx.assert_editor_state(indoc! {"
21091 def main():
21092 try:
21093 i = 2
21094 except:
21095 j = 2
21096 else:ˇ
21097 "});
21098
21099 // test `finally` auto outdents when typed inside `else` block
21100 cx.set_state(indoc! {"
21101 def main():
21102 try:
21103 i = 2
21104 except:
21105 j = 2
21106 else:
21107 k = 2
21108 ˇ
21109 "});
21110 cx.update_editor(|editor, window, cx| {
21111 editor.handle_input("finally:", window, cx);
21112 });
21113 cx.assert_editor_state(indoc! {"
21114 def main():
21115 try:
21116 i = 2
21117 except:
21118 j = 2
21119 else:
21120 k = 2
21121 finally:ˇ
21122 "});
21123
21124 // TODO: test `except` auto outdents when typed inside `try` block right after for block
21125 // cx.set_state(indoc! {"
21126 // def main():
21127 // try:
21128 // for i in range(n):
21129 // pass
21130 // ˇ
21131 // "});
21132 // cx.update_editor(|editor, window, cx| {
21133 // editor.handle_input("except:", window, cx);
21134 // });
21135 // cx.assert_editor_state(indoc! {"
21136 // def main():
21137 // try:
21138 // for i in range(n):
21139 // pass
21140 // except:ˇ
21141 // "});
21142
21143 // TODO: test `else` auto outdents when typed inside `except` block right after for block
21144 // cx.set_state(indoc! {"
21145 // def main():
21146 // try:
21147 // i = 2
21148 // except:
21149 // for i in range(n):
21150 // pass
21151 // ˇ
21152 // "});
21153 // cx.update_editor(|editor, window, cx| {
21154 // editor.handle_input("else:", window, cx);
21155 // });
21156 // cx.assert_editor_state(indoc! {"
21157 // def main():
21158 // try:
21159 // i = 2
21160 // except:
21161 // for i in range(n):
21162 // pass
21163 // else:ˇ
21164 // "});
21165
21166 // TODO: test `finally` auto outdents when typed inside `else` block right after for block
21167 // cx.set_state(indoc! {"
21168 // def main():
21169 // try:
21170 // i = 2
21171 // except:
21172 // j = 2
21173 // else:
21174 // for i in range(n):
21175 // pass
21176 // ˇ
21177 // "});
21178 // cx.update_editor(|editor, window, cx| {
21179 // editor.handle_input("finally:", window, cx);
21180 // });
21181 // cx.assert_editor_state(indoc! {"
21182 // def main():
21183 // try:
21184 // i = 2
21185 // except:
21186 // j = 2
21187 // else:
21188 // for i in range(n):
21189 // pass
21190 // finally:ˇ
21191 // "});
21192
21193 // test `else` stays at correct indent when typed after `for` block
21194 cx.set_state(indoc! {"
21195 def main():
21196 for i in range(10):
21197 if i == 3:
21198 break
21199 ˇ
21200 "});
21201 cx.update_editor(|editor, window, cx| {
21202 editor.handle_input("else:", window, cx);
21203 });
21204 cx.assert_editor_state(indoc! {"
21205 def main():
21206 for i in range(10):
21207 if i == 3:
21208 break
21209 else:ˇ
21210 "});
21211
21212 // test does not outdent on typing after line with square brackets
21213 cx.set_state(indoc! {"
21214 def f() -> list[str]:
21215 ˇ
21216 "});
21217 cx.update_editor(|editor, window, cx| {
21218 editor.handle_input("a", window, cx);
21219 });
21220 cx.assert_editor_state(indoc! {"
21221 def f() -> list[str]:
21222 aˇ
21223 "});
21224}
21225
21226#[gpui::test]
21227async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
21228 init_test(cx, |_| {});
21229 update_test_language_settings(cx, |settings| {
21230 settings.defaults.extend_comment_on_newline = Some(false);
21231 });
21232 let mut cx = EditorTestContext::new(cx).await;
21233 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
21234 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21235
21236 // test correct indent after newline on comment
21237 cx.set_state(indoc! {"
21238 # COMMENT:ˇ
21239 "});
21240 cx.update_editor(|editor, window, cx| {
21241 editor.newline(&Newline, window, cx);
21242 });
21243 cx.assert_editor_state(indoc! {"
21244 # COMMENT:
21245 ˇ
21246 "});
21247
21248 // test correct indent after newline in brackets
21249 cx.set_state(indoc! {"
21250 {ˇ}
21251 "});
21252 cx.update_editor(|editor, window, cx| {
21253 editor.newline(&Newline, window, cx);
21254 });
21255 cx.run_until_parked();
21256 cx.assert_editor_state(indoc! {"
21257 {
21258 ˇ
21259 }
21260 "});
21261
21262 cx.set_state(indoc! {"
21263 (ˇ)
21264 "});
21265 cx.update_editor(|editor, window, cx| {
21266 editor.newline(&Newline, window, cx);
21267 });
21268 cx.run_until_parked();
21269 cx.assert_editor_state(indoc! {"
21270 (
21271 ˇ
21272 )
21273 "});
21274
21275 // do not indent after empty lists or dictionaries
21276 cx.set_state(indoc! {"
21277 a = []ˇ
21278 "});
21279 cx.update_editor(|editor, window, cx| {
21280 editor.newline(&Newline, window, cx);
21281 });
21282 cx.run_until_parked();
21283 cx.assert_editor_state(indoc! {"
21284 a = []
21285 ˇ
21286 "});
21287}
21288
21289fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
21290 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
21291 point..point
21292}
21293
21294#[track_caller]
21295fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
21296 let (text, ranges) = marked_text_ranges(marked_text, true);
21297 assert_eq!(editor.text(cx), text);
21298 assert_eq!(
21299 editor.selections.ranges(cx),
21300 ranges,
21301 "Assert selections are {}",
21302 marked_text
21303 );
21304}
21305
21306pub fn handle_signature_help_request(
21307 cx: &mut EditorLspTestContext,
21308 mocked_response: lsp::SignatureHelp,
21309) -> impl Future<Output = ()> + use<> {
21310 let mut request =
21311 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
21312 let mocked_response = mocked_response.clone();
21313 async move { Ok(Some(mocked_response)) }
21314 });
21315
21316 async move {
21317 request.next().await;
21318 }
21319}
21320
21321#[track_caller]
21322pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
21323 cx.update_editor(|editor, _, _| {
21324 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
21325 let entries = menu.entries.borrow();
21326 let entries = entries
21327 .iter()
21328 .map(|entry| entry.string.as_str())
21329 .collect::<Vec<_>>();
21330 assert_eq!(entries, expected);
21331 } else {
21332 panic!("Expected completions menu");
21333 }
21334 });
21335}
21336
21337/// Handle completion request passing a marked string specifying where the completion
21338/// should be triggered from using '|' character, what range should be replaced, and what completions
21339/// should be returned using '<' and '>' to delimit the range.
21340///
21341/// Also see `handle_completion_request_with_insert_and_replace`.
21342#[track_caller]
21343pub fn handle_completion_request(
21344 marked_string: &str,
21345 completions: Vec<&'static str>,
21346 is_incomplete: bool,
21347 counter: Arc<AtomicUsize>,
21348 cx: &mut EditorLspTestContext,
21349) -> impl Future<Output = ()> {
21350 let complete_from_marker: TextRangeMarker = '|'.into();
21351 let replace_range_marker: TextRangeMarker = ('<', '>').into();
21352 let (_, mut marked_ranges) = marked_text_ranges_by(
21353 marked_string,
21354 vec![complete_from_marker.clone(), replace_range_marker.clone()],
21355 );
21356
21357 let complete_from_position =
21358 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
21359 let replace_range =
21360 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
21361
21362 let mut request =
21363 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
21364 let completions = completions.clone();
21365 counter.fetch_add(1, atomic::Ordering::Release);
21366 async move {
21367 assert_eq!(params.text_document_position.text_document.uri, url.clone());
21368 assert_eq!(
21369 params.text_document_position.position,
21370 complete_from_position
21371 );
21372 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
21373 is_incomplete: is_incomplete,
21374 item_defaults: None,
21375 items: completions
21376 .iter()
21377 .map(|completion_text| lsp::CompletionItem {
21378 label: completion_text.to_string(),
21379 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
21380 range: replace_range,
21381 new_text: completion_text.to_string(),
21382 })),
21383 ..Default::default()
21384 })
21385 .collect(),
21386 })))
21387 }
21388 });
21389
21390 async move {
21391 request.next().await;
21392 }
21393}
21394
21395/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
21396/// given instead, which also contains an `insert` range.
21397///
21398/// This function uses markers to define ranges:
21399/// - `|` marks the cursor position
21400/// - `<>` marks the replace range
21401/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
21402pub fn handle_completion_request_with_insert_and_replace(
21403 cx: &mut EditorLspTestContext,
21404 marked_string: &str,
21405 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
21406 counter: Arc<AtomicUsize>,
21407) -> impl Future<Output = ()> {
21408 let complete_from_marker: TextRangeMarker = '|'.into();
21409 let replace_range_marker: TextRangeMarker = ('<', '>').into();
21410 let insert_range_marker: TextRangeMarker = ('{', '}').into();
21411
21412 let (_, mut marked_ranges) = marked_text_ranges_by(
21413 marked_string,
21414 vec![
21415 complete_from_marker.clone(),
21416 replace_range_marker.clone(),
21417 insert_range_marker.clone(),
21418 ],
21419 );
21420
21421 let complete_from_position =
21422 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
21423 let replace_range =
21424 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
21425
21426 let insert_range = match marked_ranges.remove(&insert_range_marker) {
21427 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
21428 _ => lsp::Range {
21429 start: replace_range.start,
21430 end: complete_from_position,
21431 },
21432 };
21433
21434 let mut request =
21435 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
21436 let completions = completions.clone();
21437 counter.fetch_add(1, atomic::Ordering::Release);
21438 async move {
21439 assert_eq!(params.text_document_position.text_document.uri, url.clone());
21440 assert_eq!(
21441 params.text_document_position.position, complete_from_position,
21442 "marker `|` position doesn't match",
21443 );
21444 Ok(Some(lsp::CompletionResponse::Array(
21445 completions
21446 .iter()
21447 .map(|(label, new_text)| lsp::CompletionItem {
21448 label: label.to_string(),
21449 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
21450 lsp::InsertReplaceEdit {
21451 insert: insert_range,
21452 replace: replace_range,
21453 new_text: new_text.to_string(),
21454 },
21455 )),
21456 ..Default::default()
21457 })
21458 .collect(),
21459 )))
21460 }
21461 });
21462
21463 async move {
21464 request.next().await;
21465 }
21466}
21467
21468fn handle_resolve_completion_request(
21469 cx: &mut EditorLspTestContext,
21470 edits: Option<Vec<(&'static str, &'static str)>>,
21471) -> impl Future<Output = ()> {
21472 let edits = edits.map(|edits| {
21473 edits
21474 .iter()
21475 .map(|(marked_string, new_text)| {
21476 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
21477 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
21478 lsp::TextEdit::new(replace_range, new_text.to_string())
21479 })
21480 .collect::<Vec<_>>()
21481 });
21482
21483 let mut request =
21484 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
21485 let edits = edits.clone();
21486 async move {
21487 Ok(lsp::CompletionItem {
21488 additional_text_edits: edits,
21489 ..Default::default()
21490 })
21491 }
21492 });
21493
21494 async move {
21495 request.next().await;
21496 }
21497}
21498
21499pub(crate) fn update_test_language_settings(
21500 cx: &mut TestAppContext,
21501 f: impl Fn(&mut AllLanguageSettingsContent),
21502) {
21503 cx.update(|cx| {
21504 SettingsStore::update_global(cx, |store, cx| {
21505 store.update_user_settings::<AllLanguageSettings>(cx, f);
21506 });
21507 });
21508}
21509
21510pub(crate) fn update_test_project_settings(
21511 cx: &mut TestAppContext,
21512 f: impl Fn(&mut ProjectSettings),
21513) {
21514 cx.update(|cx| {
21515 SettingsStore::update_global(cx, |store, cx| {
21516 store.update_user_settings::<ProjectSettings>(cx, f);
21517 });
21518 });
21519}
21520
21521pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
21522 cx.update(|cx| {
21523 assets::Assets.load_test_fonts(cx);
21524 let store = SettingsStore::test(cx);
21525 cx.set_global(store);
21526 theme::init(theme::LoadThemes::JustBase, cx);
21527 release_channel::init(SemanticVersion::default(), cx);
21528 client::init_settings(cx);
21529 language::init(cx);
21530 Project::init_settings(cx);
21531 workspace::init_settings(cx);
21532 crate::init(cx);
21533 });
21534
21535 update_test_language_settings(cx, f);
21536}
21537
21538#[track_caller]
21539fn assert_hunk_revert(
21540 not_reverted_text_with_selections: &str,
21541 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
21542 expected_reverted_text_with_selections: &str,
21543 base_text: &str,
21544 cx: &mut EditorLspTestContext,
21545) {
21546 cx.set_state(not_reverted_text_with_selections);
21547 cx.set_head_text(base_text);
21548 cx.executor().run_until_parked();
21549
21550 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
21551 let snapshot = editor.snapshot(window, cx);
21552 let reverted_hunk_statuses = snapshot
21553 .buffer_snapshot
21554 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
21555 .map(|hunk| hunk.status().kind)
21556 .collect::<Vec<_>>();
21557
21558 editor.git_restore(&Default::default(), window, cx);
21559 reverted_hunk_statuses
21560 });
21561 cx.executor().run_until_parked();
21562 cx.assert_editor_state(expected_reverted_text_with_selections);
21563 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
21564}