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 DiagnosticSourceKind::Pushed,
13654 &[],
13655 cx,
13656 )
13657 .unwrap()
13658 });
13659 });
13660
13661 executor.run_until_parked();
13662
13663 cx.update_editor(|editor, window, cx| {
13664 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13665 });
13666
13667 cx.assert_editor_state(indoc! {"
13668 fn func(abc def: i32) -> ˇu32 {
13669 }
13670 "});
13671
13672 cx.update_editor(|editor, window, cx| {
13673 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13674 });
13675
13676 cx.assert_editor_state(indoc! {"
13677 fn func(abc ˇdef: i32) -> u32 {
13678 }
13679 "});
13680
13681 cx.update_editor(|editor, window, cx| {
13682 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13683 });
13684
13685 cx.assert_editor_state(indoc! {"
13686 fn func(abcˇ def: i32) -> u32 {
13687 }
13688 "});
13689
13690 cx.update_editor(|editor, window, cx| {
13691 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13692 });
13693
13694 cx.assert_editor_state(indoc! {"
13695 fn func(abc def: i32) -> ˇu32 {
13696 }
13697 "});
13698}
13699
13700#[gpui::test]
13701async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13702 init_test(cx, |_| {});
13703
13704 let mut cx = EditorTestContext::new(cx).await;
13705
13706 let diff_base = r#"
13707 use some::mod;
13708
13709 const A: u32 = 42;
13710
13711 fn main() {
13712 println!("hello");
13713
13714 println!("world");
13715 }
13716 "#
13717 .unindent();
13718
13719 // Edits are modified, removed, modified, added
13720 cx.set_state(
13721 &r#"
13722 use some::modified;
13723
13724 ˇ
13725 fn main() {
13726 println!("hello there");
13727
13728 println!("around the");
13729 println!("world");
13730 }
13731 "#
13732 .unindent(),
13733 );
13734
13735 cx.set_head_text(&diff_base);
13736 executor.run_until_parked();
13737
13738 cx.update_editor(|editor, window, cx| {
13739 //Wrap around the bottom of the buffer
13740 for _ in 0..3 {
13741 editor.go_to_next_hunk(&GoToHunk, window, cx);
13742 }
13743 });
13744
13745 cx.assert_editor_state(
13746 &r#"
13747 ˇuse some::modified;
13748
13749
13750 fn main() {
13751 println!("hello there");
13752
13753 println!("around the");
13754 println!("world");
13755 }
13756 "#
13757 .unindent(),
13758 );
13759
13760 cx.update_editor(|editor, window, cx| {
13761 //Wrap around the top of the buffer
13762 for _ in 0..2 {
13763 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13764 }
13765 });
13766
13767 cx.assert_editor_state(
13768 &r#"
13769 use some::modified;
13770
13771
13772 fn main() {
13773 ˇ println!("hello there");
13774
13775 println!("around the");
13776 println!("world");
13777 }
13778 "#
13779 .unindent(),
13780 );
13781
13782 cx.update_editor(|editor, window, cx| {
13783 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13784 });
13785
13786 cx.assert_editor_state(
13787 &r#"
13788 use some::modified;
13789
13790 ˇ
13791 fn main() {
13792 println!("hello there");
13793
13794 println!("around the");
13795 println!("world");
13796 }
13797 "#
13798 .unindent(),
13799 );
13800
13801 cx.update_editor(|editor, window, cx| {
13802 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13803 });
13804
13805 cx.assert_editor_state(
13806 &r#"
13807 ˇuse some::modified;
13808
13809
13810 fn main() {
13811 println!("hello there");
13812
13813 println!("around the");
13814 println!("world");
13815 }
13816 "#
13817 .unindent(),
13818 );
13819
13820 cx.update_editor(|editor, window, cx| {
13821 for _ in 0..2 {
13822 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13823 }
13824 });
13825
13826 cx.assert_editor_state(
13827 &r#"
13828 use some::modified;
13829
13830
13831 fn main() {
13832 ˇ println!("hello there");
13833
13834 println!("around the");
13835 println!("world");
13836 }
13837 "#
13838 .unindent(),
13839 );
13840
13841 cx.update_editor(|editor, window, cx| {
13842 editor.fold(&Fold, window, cx);
13843 });
13844
13845 cx.update_editor(|editor, window, cx| {
13846 editor.go_to_next_hunk(&GoToHunk, window, cx);
13847 });
13848
13849 cx.assert_editor_state(
13850 &r#"
13851 ˇuse some::modified;
13852
13853
13854 fn main() {
13855 println!("hello there");
13856
13857 println!("around the");
13858 println!("world");
13859 }
13860 "#
13861 .unindent(),
13862 );
13863}
13864
13865#[test]
13866fn test_split_words() {
13867 fn split(text: &str) -> Vec<&str> {
13868 split_words(text).collect()
13869 }
13870
13871 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
13872 assert_eq!(split("hello_world"), &["hello_", "world"]);
13873 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
13874 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
13875 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
13876 assert_eq!(split("helloworld"), &["helloworld"]);
13877
13878 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
13879}
13880
13881#[gpui::test]
13882async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
13883 init_test(cx, |_| {});
13884
13885 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
13886 let mut assert = |before, after| {
13887 let _state_context = cx.set_state(before);
13888 cx.run_until_parked();
13889 cx.update_editor(|editor, window, cx| {
13890 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
13891 });
13892 cx.run_until_parked();
13893 cx.assert_editor_state(after);
13894 };
13895
13896 // Outside bracket jumps to outside of matching bracket
13897 assert("console.logˇ(var);", "console.log(var)ˇ;");
13898 assert("console.log(var)ˇ;", "console.logˇ(var);");
13899
13900 // Inside bracket jumps to inside of matching bracket
13901 assert("console.log(ˇvar);", "console.log(varˇ);");
13902 assert("console.log(varˇ);", "console.log(ˇvar);");
13903
13904 // When outside a bracket and inside, favor jumping to the inside bracket
13905 assert(
13906 "console.log('foo', [1, 2, 3]ˇ);",
13907 "console.log(ˇ'foo', [1, 2, 3]);",
13908 );
13909 assert(
13910 "console.log(ˇ'foo', [1, 2, 3]);",
13911 "console.log('foo', [1, 2, 3]ˇ);",
13912 );
13913
13914 // Bias forward if two options are equally likely
13915 assert(
13916 "let result = curried_fun()ˇ();",
13917 "let result = curried_fun()()ˇ;",
13918 );
13919
13920 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
13921 assert(
13922 indoc! {"
13923 function test() {
13924 console.log('test')ˇ
13925 }"},
13926 indoc! {"
13927 function test() {
13928 console.logˇ('test')
13929 }"},
13930 );
13931}
13932
13933#[gpui::test]
13934async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
13935 init_test(cx, |_| {});
13936
13937 let fs = FakeFs::new(cx.executor());
13938 fs.insert_tree(
13939 path!("/a"),
13940 json!({
13941 "main.rs": "fn main() { let a = 5; }",
13942 "other.rs": "// Test file",
13943 }),
13944 )
13945 .await;
13946 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13947
13948 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13949 language_registry.add(Arc::new(Language::new(
13950 LanguageConfig {
13951 name: "Rust".into(),
13952 matcher: LanguageMatcher {
13953 path_suffixes: vec!["rs".to_string()],
13954 ..Default::default()
13955 },
13956 brackets: BracketPairConfig {
13957 pairs: vec![BracketPair {
13958 start: "{".to_string(),
13959 end: "}".to_string(),
13960 close: true,
13961 surround: true,
13962 newline: true,
13963 }],
13964 disabled_scopes_by_bracket_ix: Vec::new(),
13965 },
13966 ..Default::default()
13967 },
13968 Some(tree_sitter_rust::LANGUAGE.into()),
13969 )));
13970 let mut fake_servers = language_registry.register_fake_lsp(
13971 "Rust",
13972 FakeLspAdapter {
13973 capabilities: lsp::ServerCapabilities {
13974 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
13975 first_trigger_character: "{".to_string(),
13976 more_trigger_character: None,
13977 }),
13978 ..Default::default()
13979 },
13980 ..Default::default()
13981 },
13982 );
13983
13984 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13985
13986 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13987
13988 let worktree_id = workspace
13989 .update(cx, |workspace, _, cx| {
13990 workspace.project().update(cx, |project, cx| {
13991 project.worktrees(cx).next().unwrap().read(cx).id()
13992 })
13993 })
13994 .unwrap();
13995
13996 let buffer = project
13997 .update(cx, |project, cx| {
13998 project.open_local_buffer(path!("/a/main.rs"), cx)
13999 })
14000 .await
14001 .unwrap();
14002 let editor_handle = workspace
14003 .update(cx, |workspace, window, cx| {
14004 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
14005 })
14006 .unwrap()
14007 .await
14008 .unwrap()
14009 .downcast::<Editor>()
14010 .unwrap();
14011
14012 cx.executor().start_waiting();
14013 let fake_server = fake_servers.next().await.unwrap();
14014
14015 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
14016 |params, _| async move {
14017 assert_eq!(
14018 params.text_document_position.text_document.uri,
14019 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
14020 );
14021 assert_eq!(
14022 params.text_document_position.position,
14023 lsp::Position::new(0, 21),
14024 );
14025
14026 Ok(Some(vec![lsp::TextEdit {
14027 new_text: "]".to_string(),
14028 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14029 }]))
14030 },
14031 );
14032
14033 editor_handle.update_in(cx, |editor, window, cx| {
14034 window.focus(&editor.focus_handle(cx));
14035 editor.change_selections(None, window, cx, |s| {
14036 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
14037 });
14038 editor.handle_input("{", window, cx);
14039 });
14040
14041 cx.executor().run_until_parked();
14042
14043 buffer.update(cx, |buffer, _| {
14044 assert_eq!(
14045 buffer.text(),
14046 "fn main() { let a = {5}; }",
14047 "No extra braces from on type formatting should appear in the buffer"
14048 )
14049 });
14050}
14051
14052#[gpui::test]
14053async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
14054 init_test(cx, |_| {});
14055
14056 let fs = FakeFs::new(cx.executor());
14057 fs.insert_tree(
14058 path!("/a"),
14059 json!({
14060 "main.rs": "fn main() { let a = 5; }",
14061 "other.rs": "// Test file",
14062 }),
14063 )
14064 .await;
14065
14066 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14067
14068 let server_restarts = Arc::new(AtomicUsize::new(0));
14069 let closure_restarts = Arc::clone(&server_restarts);
14070 let language_server_name = "test language server";
14071 let language_name: LanguageName = "Rust".into();
14072
14073 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14074 language_registry.add(Arc::new(Language::new(
14075 LanguageConfig {
14076 name: language_name.clone(),
14077 matcher: LanguageMatcher {
14078 path_suffixes: vec!["rs".to_string()],
14079 ..Default::default()
14080 },
14081 ..Default::default()
14082 },
14083 Some(tree_sitter_rust::LANGUAGE.into()),
14084 )));
14085 let mut fake_servers = language_registry.register_fake_lsp(
14086 "Rust",
14087 FakeLspAdapter {
14088 name: language_server_name,
14089 initialization_options: Some(json!({
14090 "testOptionValue": true
14091 })),
14092 initializer: Some(Box::new(move |fake_server| {
14093 let task_restarts = Arc::clone(&closure_restarts);
14094 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
14095 task_restarts.fetch_add(1, atomic::Ordering::Release);
14096 futures::future::ready(Ok(()))
14097 });
14098 })),
14099 ..Default::default()
14100 },
14101 );
14102
14103 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14104 let _buffer = project
14105 .update(cx, |project, cx| {
14106 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
14107 })
14108 .await
14109 .unwrap();
14110 let _fake_server = fake_servers.next().await.unwrap();
14111 update_test_language_settings(cx, |language_settings| {
14112 language_settings.languages.insert(
14113 language_name.clone(),
14114 LanguageSettingsContent {
14115 tab_size: NonZeroU32::new(8),
14116 ..Default::default()
14117 },
14118 );
14119 });
14120 cx.executor().run_until_parked();
14121 assert_eq!(
14122 server_restarts.load(atomic::Ordering::Acquire),
14123 0,
14124 "Should not restart LSP server on an unrelated change"
14125 );
14126
14127 update_test_project_settings(cx, |project_settings| {
14128 project_settings.lsp.insert(
14129 "Some other server name".into(),
14130 LspSettings {
14131 binary: None,
14132 settings: None,
14133 initialization_options: Some(json!({
14134 "some other init value": false
14135 })),
14136 enable_lsp_tasks: false,
14137 },
14138 );
14139 });
14140 cx.executor().run_until_parked();
14141 assert_eq!(
14142 server_restarts.load(atomic::Ordering::Acquire),
14143 0,
14144 "Should not restart LSP server on an unrelated LSP settings change"
14145 );
14146
14147 update_test_project_settings(cx, |project_settings| {
14148 project_settings.lsp.insert(
14149 language_server_name.into(),
14150 LspSettings {
14151 binary: None,
14152 settings: None,
14153 initialization_options: Some(json!({
14154 "anotherInitValue": false
14155 })),
14156 enable_lsp_tasks: false,
14157 },
14158 );
14159 });
14160 cx.executor().run_until_parked();
14161 assert_eq!(
14162 server_restarts.load(atomic::Ordering::Acquire),
14163 1,
14164 "Should restart LSP server on a related LSP settings change"
14165 );
14166
14167 update_test_project_settings(cx, |project_settings| {
14168 project_settings.lsp.insert(
14169 language_server_name.into(),
14170 LspSettings {
14171 binary: None,
14172 settings: None,
14173 initialization_options: Some(json!({
14174 "anotherInitValue": false
14175 })),
14176 enable_lsp_tasks: false,
14177 },
14178 );
14179 });
14180 cx.executor().run_until_parked();
14181 assert_eq!(
14182 server_restarts.load(atomic::Ordering::Acquire),
14183 1,
14184 "Should not restart LSP server on a related LSP settings change that is the same"
14185 );
14186
14187 update_test_project_settings(cx, |project_settings| {
14188 project_settings.lsp.insert(
14189 language_server_name.into(),
14190 LspSettings {
14191 binary: None,
14192 settings: None,
14193 initialization_options: None,
14194 enable_lsp_tasks: false,
14195 },
14196 );
14197 });
14198 cx.executor().run_until_parked();
14199 assert_eq!(
14200 server_restarts.load(atomic::Ordering::Acquire),
14201 2,
14202 "Should restart LSP server on another related LSP settings change"
14203 );
14204}
14205
14206#[gpui::test]
14207async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
14208 init_test(cx, |_| {});
14209
14210 let mut cx = EditorLspTestContext::new_rust(
14211 lsp::ServerCapabilities {
14212 completion_provider: Some(lsp::CompletionOptions {
14213 trigger_characters: Some(vec![".".to_string()]),
14214 resolve_provider: Some(true),
14215 ..Default::default()
14216 }),
14217 ..Default::default()
14218 },
14219 cx,
14220 )
14221 .await;
14222
14223 cx.set_state("fn main() { let a = 2ˇ; }");
14224 cx.simulate_keystroke(".");
14225 let completion_item = lsp::CompletionItem {
14226 label: "some".into(),
14227 kind: Some(lsp::CompletionItemKind::SNIPPET),
14228 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
14229 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
14230 kind: lsp::MarkupKind::Markdown,
14231 value: "```rust\nSome(2)\n```".to_string(),
14232 })),
14233 deprecated: Some(false),
14234 sort_text: Some("fffffff2".to_string()),
14235 filter_text: Some("some".to_string()),
14236 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
14237 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14238 range: lsp::Range {
14239 start: lsp::Position {
14240 line: 0,
14241 character: 22,
14242 },
14243 end: lsp::Position {
14244 line: 0,
14245 character: 22,
14246 },
14247 },
14248 new_text: "Some(2)".to_string(),
14249 })),
14250 additional_text_edits: Some(vec![lsp::TextEdit {
14251 range: lsp::Range {
14252 start: lsp::Position {
14253 line: 0,
14254 character: 20,
14255 },
14256 end: lsp::Position {
14257 line: 0,
14258 character: 22,
14259 },
14260 },
14261 new_text: "".to_string(),
14262 }]),
14263 ..Default::default()
14264 };
14265
14266 let closure_completion_item = completion_item.clone();
14267 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14268 let task_completion_item = closure_completion_item.clone();
14269 async move {
14270 Ok(Some(lsp::CompletionResponse::Array(vec![
14271 task_completion_item,
14272 ])))
14273 }
14274 });
14275
14276 request.next().await;
14277
14278 cx.condition(|editor, _| editor.context_menu_visible())
14279 .await;
14280 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14281 editor
14282 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14283 .unwrap()
14284 });
14285 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
14286
14287 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
14288 let task_completion_item = completion_item.clone();
14289 async move { Ok(task_completion_item) }
14290 })
14291 .next()
14292 .await
14293 .unwrap();
14294 apply_additional_edits.await.unwrap();
14295 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
14296}
14297
14298#[gpui::test]
14299async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
14300 init_test(cx, |_| {});
14301
14302 let mut cx = EditorLspTestContext::new_rust(
14303 lsp::ServerCapabilities {
14304 completion_provider: Some(lsp::CompletionOptions {
14305 trigger_characters: Some(vec![".".to_string()]),
14306 resolve_provider: Some(true),
14307 ..Default::default()
14308 }),
14309 ..Default::default()
14310 },
14311 cx,
14312 )
14313 .await;
14314
14315 cx.set_state("fn main() { let a = 2ˇ; }");
14316 cx.simulate_keystroke(".");
14317
14318 let item1 = lsp::CompletionItem {
14319 label: "method id()".to_string(),
14320 filter_text: Some("id".to_string()),
14321 detail: None,
14322 documentation: None,
14323 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14324 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14325 new_text: ".id".to_string(),
14326 })),
14327 ..lsp::CompletionItem::default()
14328 };
14329
14330 let item2 = lsp::CompletionItem {
14331 label: "other".to_string(),
14332 filter_text: Some("other".to_string()),
14333 detail: None,
14334 documentation: None,
14335 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14336 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14337 new_text: ".other".to_string(),
14338 })),
14339 ..lsp::CompletionItem::default()
14340 };
14341
14342 let item1 = item1.clone();
14343 cx.set_request_handler::<lsp::request::Completion, _, _>({
14344 let item1 = item1.clone();
14345 move |_, _, _| {
14346 let item1 = item1.clone();
14347 let item2 = item2.clone();
14348 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
14349 }
14350 })
14351 .next()
14352 .await;
14353
14354 cx.condition(|editor, _| editor.context_menu_visible())
14355 .await;
14356 cx.update_editor(|editor, _, _| {
14357 let context_menu = editor.context_menu.borrow_mut();
14358 let context_menu = context_menu
14359 .as_ref()
14360 .expect("Should have the context menu deployed");
14361 match context_menu {
14362 CodeContextMenu::Completions(completions_menu) => {
14363 let completions = completions_menu.completions.borrow_mut();
14364 assert_eq!(
14365 completions
14366 .iter()
14367 .map(|completion| &completion.label.text)
14368 .collect::<Vec<_>>(),
14369 vec!["method id()", "other"]
14370 )
14371 }
14372 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14373 }
14374 });
14375
14376 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
14377 let item1 = item1.clone();
14378 move |_, item_to_resolve, _| {
14379 let item1 = item1.clone();
14380 async move {
14381 if item1 == item_to_resolve {
14382 Ok(lsp::CompletionItem {
14383 label: "method id()".to_string(),
14384 filter_text: Some("id".to_string()),
14385 detail: Some("Now resolved!".to_string()),
14386 documentation: Some(lsp::Documentation::String("Docs".to_string())),
14387 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14388 range: lsp::Range::new(
14389 lsp::Position::new(0, 22),
14390 lsp::Position::new(0, 22),
14391 ),
14392 new_text: ".id".to_string(),
14393 })),
14394 ..lsp::CompletionItem::default()
14395 })
14396 } else {
14397 Ok(item_to_resolve)
14398 }
14399 }
14400 }
14401 })
14402 .next()
14403 .await
14404 .unwrap();
14405 cx.run_until_parked();
14406
14407 cx.update_editor(|editor, window, cx| {
14408 editor.context_menu_next(&Default::default(), window, cx);
14409 });
14410
14411 cx.update_editor(|editor, _, _| {
14412 let context_menu = editor.context_menu.borrow_mut();
14413 let context_menu = context_menu
14414 .as_ref()
14415 .expect("Should have the context menu deployed");
14416 match context_menu {
14417 CodeContextMenu::Completions(completions_menu) => {
14418 let completions = completions_menu.completions.borrow_mut();
14419 assert_eq!(
14420 completions
14421 .iter()
14422 .map(|completion| &completion.label.text)
14423 .collect::<Vec<_>>(),
14424 vec!["method id() Now resolved!", "other"],
14425 "Should update first completion label, but not second as the filter text did not match."
14426 );
14427 }
14428 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14429 }
14430 });
14431}
14432
14433#[gpui::test]
14434async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
14435 init_test(cx, |_| {});
14436 let mut cx = EditorLspTestContext::new_rust(
14437 lsp::ServerCapabilities {
14438 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
14439 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
14440 completion_provider: Some(lsp::CompletionOptions {
14441 resolve_provider: Some(true),
14442 ..Default::default()
14443 }),
14444 ..Default::default()
14445 },
14446 cx,
14447 )
14448 .await;
14449 cx.set_state(indoc! {"
14450 struct TestStruct {
14451 field: i32
14452 }
14453
14454 fn mainˇ() {
14455 let unused_var = 42;
14456 let test_struct = TestStruct { field: 42 };
14457 }
14458 "});
14459 let symbol_range = cx.lsp_range(indoc! {"
14460 struct TestStruct {
14461 field: i32
14462 }
14463
14464 «fn main»() {
14465 let unused_var = 42;
14466 let test_struct = TestStruct { field: 42 };
14467 }
14468 "});
14469 let mut hover_requests =
14470 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
14471 Ok(Some(lsp::Hover {
14472 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
14473 kind: lsp::MarkupKind::Markdown,
14474 value: "Function documentation".to_string(),
14475 }),
14476 range: Some(symbol_range),
14477 }))
14478 });
14479
14480 // Case 1: Test that code action menu hide hover popover
14481 cx.dispatch_action(Hover);
14482 hover_requests.next().await;
14483 cx.condition(|editor, _| editor.hover_state.visible()).await;
14484 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
14485 move |_, _, _| async move {
14486 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
14487 lsp::CodeAction {
14488 title: "Remove unused variable".to_string(),
14489 kind: Some(CodeActionKind::QUICKFIX),
14490 edit: Some(lsp::WorkspaceEdit {
14491 changes: Some(
14492 [(
14493 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
14494 vec![lsp::TextEdit {
14495 range: lsp::Range::new(
14496 lsp::Position::new(5, 4),
14497 lsp::Position::new(5, 27),
14498 ),
14499 new_text: "".to_string(),
14500 }],
14501 )]
14502 .into_iter()
14503 .collect(),
14504 ),
14505 ..Default::default()
14506 }),
14507 ..Default::default()
14508 },
14509 )]))
14510 },
14511 );
14512 cx.update_editor(|editor, window, cx| {
14513 editor.toggle_code_actions(
14514 &ToggleCodeActions {
14515 deployed_from: None,
14516 quick_launch: false,
14517 },
14518 window,
14519 cx,
14520 );
14521 });
14522 code_action_requests.next().await;
14523 cx.run_until_parked();
14524 cx.condition(|editor, _| editor.context_menu_visible())
14525 .await;
14526 cx.update_editor(|editor, _, _| {
14527 assert!(
14528 !editor.hover_state.visible(),
14529 "Hover popover should be hidden when code action menu is shown"
14530 );
14531 // Hide code actions
14532 editor.context_menu.take();
14533 });
14534
14535 // Case 2: Test that code completions hide hover popover
14536 cx.dispatch_action(Hover);
14537 hover_requests.next().await;
14538 cx.condition(|editor, _| editor.hover_state.visible()).await;
14539 let counter = Arc::new(AtomicUsize::new(0));
14540 let mut completion_requests =
14541 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14542 let counter = counter.clone();
14543 async move {
14544 counter.fetch_add(1, atomic::Ordering::Release);
14545 Ok(Some(lsp::CompletionResponse::Array(vec![
14546 lsp::CompletionItem {
14547 label: "main".into(),
14548 kind: Some(lsp::CompletionItemKind::FUNCTION),
14549 detail: Some("() -> ()".to_string()),
14550 ..Default::default()
14551 },
14552 lsp::CompletionItem {
14553 label: "TestStruct".into(),
14554 kind: Some(lsp::CompletionItemKind::STRUCT),
14555 detail: Some("struct TestStruct".to_string()),
14556 ..Default::default()
14557 },
14558 ])))
14559 }
14560 });
14561 cx.update_editor(|editor, window, cx| {
14562 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14563 });
14564 completion_requests.next().await;
14565 cx.condition(|editor, _| editor.context_menu_visible())
14566 .await;
14567 cx.update_editor(|editor, _, _| {
14568 assert!(
14569 !editor.hover_state.visible(),
14570 "Hover popover should be hidden when completion menu is shown"
14571 );
14572 });
14573}
14574
14575#[gpui::test]
14576async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
14577 init_test(cx, |_| {});
14578
14579 let mut cx = EditorLspTestContext::new_rust(
14580 lsp::ServerCapabilities {
14581 completion_provider: Some(lsp::CompletionOptions {
14582 trigger_characters: Some(vec![".".to_string()]),
14583 resolve_provider: Some(true),
14584 ..Default::default()
14585 }),
14586 ..Default::default()
14587 },
14588 cx,
14589 )
14590 .await;
14591
14592 cx.set_state("fn main() { let a = 2ˇ; }");
14593 cx.simulate_keystroke(".");
14594
14595 let unresolved_item_1 = lsp::CompletionItem {
14596 label: "id".to_string(),
14597 filter_text: Some("id".to_string()),
14598 detail: None,
14599 documentation: None,
14600 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14601 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14602 new_text: ".id".to_string(),
14603 })),
14604 ..lsp::CompletionItem::default()
14605 };
14606 let resolved_item_1 = lsp::CompletionItem {
14607 additional_text_edits: Some(vec![lsp::TextEdit {
14608 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
14609 new_text: "!!".to_string(),
14610 }]),
14611 ..unresolved_item_1.clone()
14612 };
14613 let unresolved_item_2 = lsp::CompletionItem {
14614 label: "other".to_string(),
14615 filter_text: Some("other".to_string()),
14616 detail: None,
14617 documentation: None,
14618 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14619 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14620 new_text: ".other".to_string(),
14621 })),
14622 ..lsp::CompletionItem::default()
14623 };
14624 let resolved_item_2 = lsp::CompletionItem {
14625 additional_text_edits: Some(vec![lsp::TextEdit {
14626 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
14627 new_text: "??".to_string(),
14628 }]),
14629 ..unresolved_item_2.clone()
14630 };
14631
14632 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
14633 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
14634 cx.lsp
14635 .server
14636 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
14637 let unresolved_item_1 = unresolved_item_1.clone();
14638 let resolved_item_1 = resolved_item_1.clone();
14639 let unresolved_item_2 = unresolved_item_2.clone();
14640 let resolved_item_2 = resolved_item_2.clone();
14641 let resolve_requests_1 = resolve_requests_1.clone();
14642 let resolve_requests_2 = resolve_requests_2.clone();
14643 move |unresolved_request, _| {
14644 let unresolved_item_1 = unresolved_item_1.clone();
14645 let resolved_item_1 = resolved_item_1.clone();
14646 let unresolved_item_2 = unresolved_item_2.clone();
14647 let resolved_item_2 = resolved_item_2.clone();
14648 let resolve_requests_1 = resolve_requests_1.clone();
14649 let resolve_requests_2 = resolve_requests_2.clone();
14650 async move {
14651 if unresolved_request == unresolved_item_1 {
14652 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
14653 Ok(resolved_item_1.clone())
14654 } else if unresolved_request == unresolved_item_2 {
14655 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
14656 Ok(resolved_item_2.clone())
14657 } else {
14658 panic!("Unexpected completion item {unresolved_request:?}")
14659 }
14660 }
14661 }
14662 })
14663 .detach();
14664
14665 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14666 let unresolved_item_1 = unresolved_item_1.clone();
14667 let unresolved_item_2 = unresolved_item_2.clone();
14668 async move {
14669 Ok(Some(lsp::CompletionResponse::Array(vec![
14670 unresolved_item_1,
14671 unresolved_item_2,
14672 ])))
14673 }
14674 })
14675 .next()
14676 .await;
14677
14678 cx.condition(|editor, _| editor.context_menu_visible())
14679 .await;
14680 cx.update_editor(|editor, _, _| {
14681 let context_menu = editor.context_menu.borrow_mut();
14682 let context_menu = context_menu
14683 .as_ref()
14684 .expect("Should have the context menu deployed");
14685 match context_menu {
14686 CodeContextMenu::Completions(completions_menu) => {
14687 let completions = completions_menu.completions.borrow_mut();
14688 assert_eq!(
14689 completions
14690 .iter()
14691 .map(|completion| &completion.label.text)
14692 .collect::<Vec<_>>(),
14693 vec!["id", "other"]
14694 )
14695 }
14696 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14697 }
14698 });
14699 cx.run_until_parked();
14700
14701 cx.update_editor(|editor, window, cx| {
14702 editor.context_menu_next(&ContextMenuNext, window, cx);
14703 });
14704 cx.run_until_parked();
14705 cx.update_editor(|editor, window, cx| {
14706 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
14707 });
14708 cx.run_until_parked();
14709 cx.update_editor(|editor, window, cx| {
14710 editor.context_menu_next(&ContextMenuNext, window, cx);
14711 });
14712 cx.run_until_parked();
14713 cx.update_editor(|editor, window, cx| {
14714 editor
14715 .compose_completion(&ComposeCompletion::default(), window, cx)
14716 .expect("No task returned")
14717 })
14718 .await
14719 .expect("Completion failed");
14720 cx.run_until_parked();
14721
14722 cx.update_editor(|editor, _, cx| {
14723 assert_eq!(
14724 resolve_requests_1.load(atomic::Ordering::Acquire),
14725 1,
14726 "Should always resolve once despite multiple selections"
14727 );
14728 assert_eq!(
14729 resolve_requests_2.load(atomic::Ordering::Acquire),
14730 1,
14731 "Should always resolve once after multiple selections and applying the completion"
14732 );
14733 assert_eq!(
14734 editor.text(cx),
14735 "fn main() { let a = ??.other; }",
14736 "Should use resolved data when applying the completion"
14737 );
14738 });
14739}
14740
14741#[gpui::test]
14742async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
14743 init_test(cx, |_| {});
14744
14745 let item_0 = lsp::CompletionItem {
14746 label: "abs".into(),
14747 insert_text: Some("abs".into()),
14748 data: Some(json!({ "very": "special"})),
14749 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
14750 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14751 lsp::InsertReplaceEdit {
14752 new_text: "abs".to_string(),
14753 insert: lsp::Range::default(),
14754 replace: lsp::Range::default(),
14755 },
14756 )),
14757 ..lsp::CompletionItem::default()
14758 };
14759 let items = iter::once(item_0.clone())
14760 .chain((11..51).map(|i| lsp::CompletionItem {
14761 label: format!("item_{}", i),
14762 insert_text: Some(format!("item_{}", i)),
14763 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
14764 ..lsp::CompletionItem::default()
14765 }))
14766 .collect::<Vec<_>>();
14767
14768 let default_commit_characters = vec!["?".to_string()];
14769 let default_data = json!({ "default": "data"});
14770 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
14771 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
14772 let default_edit_range = lsp::Range {
14773 start: lsp::Position {
14774 line: 0,
14775 character: 5,
14776 },
14777 end: lsp::Position {
14778 line: 0,
14779 character: 5,
14780 },
14781 };
14782
14783 let mut cx = EditorLspTestContext::new_rust(
14784 lsp::ServerCapabilities {
14785 completion_provider: Some(lsp::CompletionOptions {
14786 trigger_characters: Some(vec![".".to_string()]),
14787 resolve_provider: Some(true),
14788 ..Default::default()
14789 }),
14790 ..Default::default()
14791 },
14792 cx,
14793 )
14794 .await;
14795
14796 cx.set_state("fn main() { let a = 2ˇ; }");
14797 cx.simulate_keystroke(".");
14798
14799 let completion_data = default_data.clone();
14800 let completion_characters = default_commit_characters.clone();
14801 let completion_items = items.clone();
14802 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14803 let default_data = completion_data.clone();
14804 let default_commit_characters = completion_characters.clone();
14805 let items = completion_items.clone();
14806 async move {
14807 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14808 items,
14809 item_defaults: Some(lsp::CompletionListItemDefaults {
14810 data: Some(default_data.clone()),
14811 commit_characters: Some(default_commit_characters.clone()),
14812 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
14813 default_edit_range,
14814 )),
14815 insert_text_format: Some(default_insert_text_format),
14816 insert_text_mode: Some(default_insert_text_mode),
14817 }),
14818 ..lsp::CompletionList::default()
14819 })))
14820 }
14821 })
14822 .next()
14823 .await;
14824
14825 let resolved_items = Arc::new(Mutex::new(Vec::new()));
14826 cx.lsp
14827 .server
14828 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
14829 let closure_resolved_items = resolved_items.clone();
14830 move |item_to_resolve, _| {
14831 let closure_resolved_items = closure_resolved_items.clone();
14832 async move {
14833 closure_resolved_items.lock().push(item_to_resolve.clone());
14834 Ok(item_to_resolve)
14835 }
14836 }
14837 })
14838 .detach();
14839
14840 cx.condition(|editor, _| editor.context_menu_visible())
14841 .await;
14842 cx.run_until_parked();
14843 cx.update_editor(|editor, _, _| {
14844 let menu = editor.context_menu.borrow_mut();
14845 match menu.as_ref().expect("should have the completions menu") {
14846 CodeContextMenu::Completions(completions_menu) => {
14847 assert_eq!(
14848 completions_menu
14849 .entries
14850 .borrow()
14851 .iter()
14852 .map(|mat| mat.string.clone())
14853 .collect::<Vec<String>>(),
14854 items
14855 .iter()
14856 .map(|completion| completion.label.clone())
14857 .collect::<Vec<String>>()
14858 );
14859 }
14860 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
14861 }
14862 });
14863 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
14864 // with 4 from the end.
14865 assert_eq!(
14866 *resolved_items.lock(),
14867 [&items[0..16], &items[items.len() - 4..items.len()]]
14868 .concat()
14869 .iter()
14870 .cloned()
14871 .map(|mut item| {
14872 if item.data.is_none() {
14873 item.data = Some(default_data.clone());
14874 }
14875 item
14876 })
14877 .collect::<Vec<lsp::CompletionItem>>(),
14878 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
14879 );
14880 resolved_items.lock().clear();
14881
14882 cx.update_editor(|editor, window, cx| {
14883 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
14884 });
14885 cx.run_until_parked();
14886 // Completions that have already been resolved are skipped.
14887 assert_eq!(
14888 *resolved_items.lock(),
14889 items[items.len() - 16..items.len() - 4]
14890 .iter()
14891 .cloned()
14892 .map(|mut item| {
14893 if item.data.is_none() {
14894 item.data = Some(default_data.clone());
14895 }
14896 item
14897 })
14898 .collect::<Vec<lsp::CompletionItem>>()
14899 );
14900 resolved_items.lock().clear();
14901}
14902
14903#[gpui::test]
14904async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
14905 init_test(cx, |_| {});
14906
14907 let mut cx = EditorLspTestContext::new(
14908 Language::new(
14909 LanguageConfig {
14910 matcher: LanguageMatcher {
14911 path_suffixes: vec!["jsx".into()],
14912 ..Default::default()
14913 },
14914 overrides: [(
14915 "element".into(),
14916 LanguageConfigOverride {
14917 completion_query_characters: Override::Set(['-'].into_iter().collect()),
14918 ..Default::default()
14919 },
14920 )]
14921 .into_iter()
14922 .collect(),
14923 ..Default::default()
14924 },
14925 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
14926 )
14927 .with_override_query("(jsx_self_closing_element) @element")
14928 .unwrap(),
14929 lsp::ServerCapabilities {
14930 completion_provider: Some(lsp::CompletionOptions {
14931 trigger_characters: Some(vec![":".to_string()]),
14932 ..Default::default()
14933 }),
14934 ..Default::default()
14935 },
14936 cx,
14937 )
14938 .await;
14939
14940 cx.lsp
14941 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14942 Ok(Some(lsp::CompletionResponse::Array(vec![
14943 lsp::CompletionItem {
14944 label: "bg-blue".into(),
14945 ..Default::default()
14946 },
14947 lsp::CompletionItem {
14948 label: "bg-red".into(),
14949 ..Default::default()
14950 },
14951 lsp::CompletionItem {
14952 label: "bg-yellow".into(),
14953 ..Default::default()
14954 },
14955 ])))
14956 });
14957
14958 cx.set_state(r#"<p class="bgˇ" />"#);
14959
14960 // Trigger completion when typing a dash, because the dash is an extra
14961 // word character in the 'element' scope, which contains the cursor.
14962 cx.simulate_keystroke("-");
14963 cx.executor().run_until_parked();
14964 cx.update_editor(|editor, _, _| {
14965 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14966 {
14967 assert_eq!(
14968 completion_menu_entries(&menu),
14969 &["bg-red", "bg-blue", "bg-yellow"]
14970 );
14971 } else {
14972 panic!("expected completion menu to be open");
14973 }
14974 });
14975
14976 cx.simulate_keystroke("l");
14977 cx.executor().run_until_parked();
14978 cx.update_editor(|editor, _, _| {
14979 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14980 {
14981 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
14982 } else {
14983 panic!("expected completion menu to be open");
14984 }
14985 });
14986
14987 // When filtering completions, consider the character after the '-' to
14988 // be the start of a subword.
14989 cx.set_state(r#"<p class="yelˇ" />"#);
14990 cx.simulate_keystroke("l");
14991 cx.executor().run_until_parked();
14992 cx.update_editor(|editor, _, _| {
14993 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14994 {
14995 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
14996 } else {
14997 panic!("expected completion menu to be open");
14998 }
14999 });
15000}
15001
15002fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
15003 let entries = menu.entries.borrow();
15004 entries.iter().map(|mat| mat.string.clone()).collect()
15005}
15006
15007#[gpui::test]
15008async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
15009 init_test(cx, |settings| {
15010 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
15011 FormatterList(vec![Formatter::Prettier].into()),
15012 ))
15013 });
15014
15015 let fs = FakeFs::new(cx.executor());
15016 fs.insert_file(path!("/file.ts"), Default::default()).await;
15017
15018 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
15019 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15020
15021 language_registry.add(Arc::new(Language::new(
15022 LanguageConfig {
15023 name: "TypeScript".into(),
15024 matcher: LanguageMatcher {
15025 path_suffixes: vec!["ts".to_string()],
15026 ..Default::default()
15027 },
15028 ..Default::default()
15029 },
15030 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
15031 )));
15032 update_test_language_settings(cx, |settings| {
15033 settings.defaults.prettier = Some(PrettierSettings {
15034 allowed: true,
15035 ..PrettierSettings::default()
15036 });
15037 });
15038
15039 let test_plugin = "test_plugin";
15040 let _ = language_registry.register_fake_lsp(
15041 "TypeScript",
15042 FakeLspAdapter {
15043 prettier_plugins: vec![test_plugin],
15044 ..Default::default()
15045 },
15046 );
15047
15048 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
15049 let buffer = project
15050 .update(cx, |project, cx| {
15051 project.open_local_buffer(path!("/file.ts"), cx)
15052 })
15053 .await
15054 .unwrap();
15055
15056 let buffer_text = "one\ntwo\nthree\n";
15057 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
15058 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
15059 editor.update_in(cx, |editor, window, cx| {
15060 editor.set_text(buffer_text, window, cx)
15061 });
15062
15063 editor
15064 .update_in(cx, |editor, window, cx| {
15065 editor.perform_format(
15066 project.clone(),
15067 FormatTrigger::Manual,
15068 FormatTarget::Buffers,
15069 window,
15070 cx,
15071 )
15072 })
15073 .unwrap()
15074 .await;
15075 assert_eq!(
15076 editor.update(cx, |editor, cx| editor.text(cx)),
15077 buffer_text.to_string() + prettier_format_suffix,
15078 "Test prettier formatting was not applied to the original buffer text",
15079 );
15080
15081 update_test_language_settings(cx, |settings| {
15082 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
15083 });
15084 let format = editor.update_in(cx, |editor, window, cx| {
15085 editor.perform_format(
15086 project.clone(),
15087 FormatTrigger::Manual,
15088 FormatTarget::Buffers,
15089 window,
15090 cx,
15091 )
15092 });
15093 format.await.unwrap();
15094 assert_eq!(
15095 editor.update(cx, |editor, cx| editor.text(cx)),
15096 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
15097 "Autoformatting (via test prettier) was not applied to the original buffer text",
15098 );
15099}
15100
15101#[gpui::test]
15102async fn test_addition_reverts(cx: &mut TestAppContext) {
15103 init_test(cx, |_| {});
15104 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15105 let base_text = indoc! {r#"
15106 struct Row;
15107 struct Row1;
15108 struct Row2;
15109
15110 struct Row4;
15111 struct Row5;
15112 struct Row6;
15113
15114 struct Row8;
15115 struct Row9;
15116 struct Row10;"#};
15117
15118 // When addition hunks are not adjacent to carets, no hunk revert is performed
15119 assert_hunk_revert(
15120 indoc! {r#"struct Row;
15121 struct Row1;
15122 struct Row1.1;
15123 struct Row1.2;
15124 struct Row2;ˇ
15125
15126 struct Row4;
15127 struct Row5;
15128 struct Row6;
15129
15130 struct Row8;
15131 ˇstruct Row9;
15132 struct Row9.1;
15133 struct Row9.2;
15134 struct Row9.3;
15135 struct Row10;"#},
15136 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
15137 indoc! {r#"struct Row;
15138 struct Row1;
15139 struct Row1.1;
15140 struct Row1.2;
15141 struct Row2;ˇ
15142
15143 struct Row4;
15144 struct Row5;
15145 struct Row6;
15146
15147 struct Row8;
15148 ˇstruct Row9;
15149 struct Row9.1;
15150 struct Row9.2;
15151 struct Row9.3;
15152 struct Row10;"#},
15153 base_text,
15154 &mut cx,
15155 );
15156 // Same for selections
15157 assert_hunk_revert(
15158 indoc! {r#"struct Row;
15159 struct Row1;
15160 struct Row2;
15161 struct Row2.1;
15162 struct Row2.2;
15163 «ˇ
15164 struct Row4;
15165 struct» Row5;
15166 «struct Row6;
15167 ˇ»
15168 struct Row9.1;
15169 struct Row9.2;
15170 struct Row9.3;
15171 struct Row8;
15172 struct Row9;
15173 struct Row10;"#},
15174 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
15175 indoc! {r#"struct Row;
15176 struct Row1;
15177 struct Row2;
15178 struct Row2.1;
15179 struct Row2.2;
15180 «ˇ
15181 struct Row4;
15182 struct» Row5;
15183 «struct Row6;
15184 ˇ»
15185 struct Row9.1;
15186 struct Row9.2;
15187 struct Row9.3;
15188 struct Row8;
15189 struct Row9;
15190 struct Row10;"#},
15191 base_text,
15192 &mut cx,
15193 );
15194
15195 // When carets and selections intersect the addition hunks, those are reverted.
15196 // Adjacent carets got merged.
15197 assert_hunk_revert(
15198 indoc! {r#"struct Row;
15199 ˇ// something on the top
15200 struct Row1;
15201 struct Row2;
15202 struct Roˇw3.1;
15203 struct Row2.2;
15204 struct Row2.3;ˇ
15205
15206 struct Row4;
15207 struct ˇRow5.1;
15208 struct Row5.2;
15209 struct «Rowˇ»5.3;
15210 struct Row5;
15211 struct Row6;
15212 ˇ
15213 struct Row9.1;
15214 struct «Rowˇ»9.2;
15215 struct «ˇRow»9.3;
15216 struct Row8;
15217 struct Row9;
15218 «ˇ// something on bottom»
15219 struct Row10;"#},
15220 vec![
15221 DiffHunkStatusKind::Added,
15222 DiffHunkStatusKind::Added,
15223 DiffHunkStatusKind::Added,
15224 DiffHunkStatusKind::Added,
15225 DiffHunkStatusKind::Added,
15226 ],
15227 indoc! {r#"struct Row;
15228 ˇstruct Row1;
15229 struct Row2;
15230 ˇ
15231 struct Row4;
15232 ˇstruct Row5;
15233 struct Row6;
15234 ˇ
15235 ˇstruct Row8;
15236 struct Row9;
15237 ˇstruct Row10;"#},
15238 base_text,
15239 &mut cx,
15240 );
15241}
15242
15243#[gpui::test]
15244async fn test_modification_reverts(cx: &mut TestAppContext) {
15245 init_test(cx, |_| {});
15246 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15247 let base_text = indoc! {r#"
15248 struct Row;
15249 struct Row1;
15250 struct Row2;
15251
15252 struct Row4;
15253 struct Row5;
15254 struct Row6;
15255
15256 struct Row8;
15257 struct Row9;
15258 struct Row10;"#};
15259
15260 // Modification hunks behave the same as the addition ones.
15261 assert_hunk_revert(
15262 indoc! {r#"struct Row;
15263 struct Row1;
15264 struct Row33;
15265 ˇ
15266 struct Row4;
15267 struct Row5;
15268 struct Row6;
15269 ˇ
15270 struct Row99;
15271 struct Row9;
15272 struct Row10;"#},
15273 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
15274 indoc! {r#"struct Row;
15275 struct Row1;
15276 struct Row33;
15277 ˇ
15278 struct Row4;
15279 struct Row5;
15280 struct Row6;
15281 ˇ
15282 struct Row99;
15283 struct Row9;
15284 struct Row10;"#},
15285 base_text,
15286 &mut cx,
15287 );
15288 assert_hunk_revert(
15289 indoc! {r#"struct Row;
15290 struct Row1;
15291 struct Row33;
15292 «ˇ
15293 struct Row4;
15294 struct» Row5;
15295 «struct Row6;
15296 ˇ»
15297 struct Row99;
15298 struct Row9;
15299 struct Row10;"#},
15300 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
15301 indoc! {r#"struct Row;
15302 struct Row1;
15303 struct Row33;
15304 «ˇ
15305 struct Row4;
15306 struct» Row5;
15307 «struct Row6;
15308 ˇ»
15309 struct Row99;
15310 struct Row9;
15311 struct Row10;"#},
15312 base_text,
15313 &mut cx,
15314 );
15315
15316 assert_hunk_revert(
15317 indoc! {r#"ˇstruct Row1.1;
15318 struct Row1;
15319 «ˇstr»uct Row22;
15320
15321 struct ˇRow44;
15322 struct Row5;
15323 struct «Rˇ»ow66;ˇ
15324
15325 «struˇ»ct Row88;
15326 struct Row9;
15327 struct Row1011;ˇ"#},
15328 vec![
15329 DiffHunkStatusKind::Modified,
15330 DiffHunkStatusKind::Modified,
15331 DiffHunkStatusKind::Modified,
15332 DiffHunkStatusKind::Modified,
15333 DiffHunkStatusKind::Modified,
15334 DiffHunkStatusKind::Modified,
15335 ],
15336 indoc! {r#"struct Row;
15337 ˇstruct Row1;
15338 struct Row2;
15339 ˇ
15340 struct Row4;
15341 ˇstruct Row5;
15342 struct Row6;
15343 ˇ
15344 struct Row8;
15345 ˇstruct Row9;
15346 struct Row10;ˇ"#},
15347 base_text,
15348 &mut cx,
15349 );
15350}
15351
15352#[gpui::test]
15353async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
15354 init_test(cx, |_| {});
15355 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15356 let base_text = indoc! {r#"
15357 one
15358
15359 two
15360 three
15361 "#};
15362
15363 cx.set_head_text(base_text);
15364 cx.set_state("\nˇ\n");
15365 cx.executor().run_until_parked();
15366 cx.update_editor(|editor, _window, cx| {
15367 editor.expand_selected_diff_hunks(cx);
15368 });
15369 cx.executor().run_until_parked();
15370 cx.update_editor(|editor, window, cx| {
15371 editor.backspace(&Default::default(), window, cx);
15372 });
15373 cx.run_until_parked();
15374 cx.assert_state_with_diff(
15375 indoc! {r#"
15376
15377 - two
15378 - threeˇ
15379 +
15380 "#}
15381 .to_string(),
15382 );
15383}
15384
15385#[gpui::test]
15386async fn test_deletion_reverts(cx: &mut TestAppContext) {
15387 init_test(cx, |_| {});
15388 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15389 let base_text = indoc! {r#"struct Row;
15390struct Row1;
15391struct Row2;
15392
15393struct Row4;
15394struct Row5;
15395struct Row6;
15396
15397struct Row8;
15398struct Row9;
15399struct Row10;"#};
15400
15401 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
15402 assert_hunk_revert(
15403 indoc! {r#"struct Row;
15404 struct Row2;
15405
15406 ˇstruct Row4;
15407 struct Row5;
15408 struct Row6;
15409 ˇ
15410 struct Row8;
15411 struct Row10;"#},
15412 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15413 indoc! {r#"struct Row;
15414 struct Row2;
15415
15416 ˇstruct Row4;
15417 struct Row5;
15418 struct Row6;
15419 ˇ
15420 struct Row8;
15421 struct Row10;"#},
15422 base_text,
15423 &mut cx,
15424 );
15425 assert_hunk_revert(
15426 indoc! {r#"struct Row;
15427 struct Row2;
15428
15429 «ˇstruct Row4;
15430 struct» Row5;
15431 «struct Row6;
15432 ˇ»
15433 struct Row8;
15434 struct Row10;"#},
15435 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15436 indoc! {r#"struct Row;
15437 struct Row2;
15438
15439 «ˇstruct Row4;
15440 struct» Row5;
15441 «struct Row6;
15442 ˇ»
15443 struct Row8;
15444 struct Row10;"#},
15445 base_text,
15446 &mut cx,
15447 );
15448
15449 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
15450 assert_hunk_revert(
15451 indoc! {r#"struct Row;
15452 ˇstruct Row2;
15453
15454 struct Row4;
15455 struct Row5;
15456 struct Row6;
15457
15458 struct Row8;ˇ
15459 struct Row10;"#},
15460 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15461 indoc! {r#"struct Row;
15462 struct Row1;
15463 ˇstruct Row2;
15464
15465 struct Row4;
15466 struct Row5;
15467 struct Row6;
15468
15469 struct Row8;ˇ
15470 struct Row9;
15471 struct Row10;"#},
15472 base_text,
15473 &mut cx,
15474 );
15475 assert_hunk_revert(
15476 indoc! {r#"struct Row;
15477 struct Row2«ˇ;
15478 struct Row4;
15479 struct» Row5;
15480 «struct Row6;
15481
15482 struct Row8;ˇ»
15483 struct Row10;"#},
15484 vec![
15485 DiffHunkStatusKind::Deleted,
15486 DiffHunkStatusKind::Deleted,
15487 DiffHunkStatusKind::Deleted,
15488 ],
15489 indoc! {r#"struct Row;
15490 struct Row1;
15491 struct Row2«ˇ;
15492
15493 struct Row4;
15494 struct» Row5;
15495 «struct Row6;
15496
15497 struct Row8;ˇ»
15498 struct Row9;
15499 struct Row10;"#},
15500 base_text,
15501 &mut cx,
15502 );
15503}
15504
15505#[gpui::test]
15506async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
15507 init_test(cx, |_| {});
15508
15509 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
15510 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
15511 let base_text_3 =
15512 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
15513
15514 let text_1 = edit_first_char_of_every_line(base_text_1);
15515 let text_2 = edit_first_char_of_every_line(base_text_2);
15516 let text_3 = edit_first_char_of_every_line(base_text_3);
15517
15518 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
15519 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
15520 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
15521
15522 let multibuffer = cx.new(|cx| {
15523 let mut multibuffer = MultiBuffer::new(ReadWrite);
15524 multibuffer.push_excerpts(
15525 buffer_1.clone(),
15526 [
15527 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15528 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15529 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15530 ],
15531 cx,
15532 );
15533 multibuffer.push_excerpts(
15534 buffer_2.clone(),
15535 [
15536 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15537 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15538 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15539 ],
15540 cx,
15541 );
15542 multibuffer.push_excerpts(
15543 buffer_3.clone(),
15544 [
15545 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15546 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15547 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15548 ],
15549 cx,
15550 );
15551 multibuffer
15552 });
15553
15554 let fs = FakeFs::new(cx.executor());
15555 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
15556 let (editor, cx) = cx
15557 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
15558 editor.update_in(cx, |editor, _window, cx| {
15559 for (buffer, diff_base) in [
15560 (buffer_1.clone(), base_text_1),
15561 (buffer_2.clone(), base_text_2),
15562 (buffer_3.clone(), base_text_3),
15563 ] {
15564 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
15565 editor
15566 .buffer
15567 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
15568 }
15569 });
15570 cx.executor().run_until_parked();
15571
15572 editor.update_in(cx, |editor, window, cx| {
15573 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}");
15574 editor.select_all(&SelectAll, window, cx);
15575 editor.git_restore(&Default::default(), window, cx);
15576 });
15577 cx.executor().run_until_parked();
15578
15579 // When all ranges are selected, all buffer hunks are reverted.
15580 editor.update(cx, |editor, cx| {
15581 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");
15582 });
15583 buffer_1.update(cx, |buffer, _| {
15584 assert_eq!(buffer.text(), base_text_1);
15585 });
15586 buffer_2.update(cx, |buffer, _| {
15587 assert_eq!(buffer.text(), base_text_2);
15588 });
15589 buffer_3.update(cx, |buffer, _| {
15590 assert_eq!(buffer.text(), base_text_3);
15591 });
15592
15593 editor.update_in(cx, |editor, window, cx| {
15594 editor.undo(&Default::default(), window, cx);
15595 });
15596
15597 editor.update_in(cx, |editor, window, cx| {
15598 editor.change_selections(None, window, cx, |s| {
15599 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
15600 });
15601 editor.git_restore(&Default::default(), window, cx);
15602 });
15603
15604 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
15605 // but not affect buffer_2 and its related excerpts.
15606 editor.update(cx, |editor, cx| {
15607 assert_eq!(
15608 editor.text(cx),
15609 "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}"
15610 );
15611 });
15612 buffer_1.update(cx, |buffer, _| {
15613 assert_eq!(buffer.text(), base_text_1);
15614 });
15615 buffer_2.update(cx, |buffer, _| {
15616 assert_eq!(
15617 buffer.text(),
15618 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
15619 );
15620 });
15621 buffer_3.update(cx, |buffer, _| {
15622 assert_eq!(
15623 buffer.text(),
15624 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
15625 );
15626 });
15627
15628 fn edit_first_char_of_every_line(text: &str) -> String {
15629 text.split('\n')
15630 .map(|line| format!("X{}", &line[1..]))
15631 .collect::<Vec<_>>()
15632 .join("\n")
15633 }
15634}
15635
15636#[gpui::test]
15637async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
15638 init_test(cx, |_| {});
15639
15640 let cols = 4;
15641 let rows = 10;
15642 let sample_text_1 = sample_text(rows, cols, 'a');
15643 assert_eq!(
15644 sample_text_1,
15645 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
15646 );
15647 let sample_text_2 = sample_text(rows, cols, 'l');
15648 assert_eq!(
15649 sample_text_2,
15650 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
15651 );
15652 let sample_text_3 = sample_text(rows, cols, 'v');
15653 assert_eq!(
15654 sample_text_3,
15655 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
15656 );
15657
15658 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
15659 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
15660 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
15661
15662 let multi_buffer = cx.new(|cx| {
15663 let mut multibuffer = MultiBuffer::new(ReadWrite);
15664 multibuffer.push_excerpts(
15665 buffer_1.clone(),
15666 [
15667 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15668 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15669 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15670 ],
15671 cx,
15672 );
15673 multibuffer.push_excerpts(
15674 buffer_2.clone(),
15675 [
15676 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15677 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15678 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15679 ],
15680 cx,
15681 );
15682 multibuffer.push_excerpts(
15683 buffer_3.clone(),
15684 [
15685 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15686 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15687 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15688 ],
15689 cx,
15690 );
15691 multibuffer
15692 });
15693
15694 let fs = FakeFs::new(cx.executor());
15695 fs.insert_tree(
15696 "/a",
15697 json!({
15698 "main.rs": sample_text_1,
15699 "other.rs": sample_text_2,
15700 "lib.rs": sample_text_3,
15701 }),
15702 )
15703 .await;
15704 let project = Project::test(fs, ["/a".as_ref()], cx).await;
15705 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15706 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15707 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15708 Editor::new(
15709 EditorMode::full(),
15710 multi_buffer,
15711 Some(project.clone()),
15712 window,
15713 cx,
15714 )
15715 });
15716 let multibuffer_item_id = workspace
15717 .update(cx, |workspace, window, cx| {
15718 assert!(
15719 workspace.active_item(cx).is_none(),
15720 "active item should be None before the first item is added"
15721 );
15722 workspace.add_item_to_active_pane(
15723 Box::new(multi_buffer_editor.clone()),
15724 None,
15725 true,
15726 window,
15727 cx,
15728 );
15729 let active_item = workspace
15730 .active_item(cx)
15731 .expect("should have an active item after adding the multi buffer");
15732 assert!(
15733 !active_item.is_singleton(cx),
15734 "A multi buffer was expected to active after adding"
15735 );
15736 active_item.item_id()
15737 })
15738 .unwrap();
15739 cx.executor().run_until_parked();
15740
15741 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15742 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15743 s.select_ranges(Some(1..2))
15744 });
15745 editor.open_excerpts(&OpenExcerpts, window, cx);
15746 });
15747 cx.executor().run_until_parked();
15748 let first_item_id = workspace
15749 .update(cx, |workspace, window, cx| {
15750 let active_item = workspace
15751 .active_item(cx)
15752 .expect("should have an active item after navigating into the 1st buffer");
15753 let first_item_id = active_item.item_id();
15754 assert_ne!(
15755 first_item_id, multibuffer_item_id,
15756 "Should navigate into the 1st buffer and activate it"
15757 );
15758 assert!(
15759 active_item.is_singleton(cx),
15760 "New active item should be a singleton buffer"
15761 );
15762 assert_eq!(
15763 active_item
15764 .act_as::<Editor>(cx)
15765 .expect("should have navigated into an editor for the 1st buffer")
15766 .read(cx)
15767 .text(cx),
15768 sample_text_1
15769 );
15770
15771 workspace
15772 .go_back(workspace.active_pane().downgrade(), window, cx)
15773 .detach_and_log_err(cx);
15774
15775 first_item_id
15776 })
15777 .unwrap();
15778 cx.executor().run_until_parked();
15779 workspace
15780 .update(cx, |workspace, _, cx| {
15781 let active_item = workspace
15782 .active_item(cx)
15783 .expect("should have an active item after navigating back");
15784 assert_eq!(
15785 active_item.item_id(),
15786 multibuffer_item_id,
15787 "Should navigate back to the multi buffer"
15788 );
15789 assert!(!active_item.is_singleton(cx));
15790 })
15791 .unwrap();
15792
15793 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15794 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15795 s.select_ranges(Some(39..40))
15796 });
15797 editor.open_excerpts(&OpenExcerpts, window, cx);
15798 });
15799 cx.executor().run_until_parked();
15800 let second_item_id = workspace
15801 .update(cx, |workspace, window, cx| {
15802 let active_item = workspace
15803 .active_item(cx)
15804 .expect("should have an active item after navigating into the 2nd buffer");
15805 let second_item_id = active_item.item_id();
15806 assert_ne!(
15807 second_item_id, multibuffer_item_id,
15808 "Should navigate away from the multibuffer"
15809 );
15810 assert_ne!(
15811 second_item_id, first_item_id,
15812 "Should navigate into the 2nd buffer and activate it"
15813 );
15814 assert!(
15815 active_item.is_singleton(cx),
15816 "New active item should be a singleton buffer"
15817 );
15818 assert_eq!(
15819 active_item
15820 .act_as::<Editor>(cx)
15821 .expect("should have navigated into an editor")
15822 .read(cx)
15823 .text(cx),
15824 sample_text_2
15825 );
15826
15827 workspace
15828 .go_back(workspace.active_pane().downgrade(), window, cx)
15829 .detach_and_log_err(cx);
15830
15831 second_item_id
15832 })
15833 .unwrap();
15834 cx.executor().run_until_parked();
15835 workspace
15836 .update(cx, |workspace, _, cx| {
15837 let active_item = workspace
15838 .active_item(cx)
15839 .expect("should have an active item after navigating back from the 2nd buffer");
15840 assert_eq!(
15841 active_item.item_id(),
15842 multibuffer_item_id,
15843 "Should navigate back from the 2nd buffer to the multi buffer"
15844 );
15845 assert!(!active_item.is_singleton(cx));
15846 })
15847 .unwrap();
15848
15849 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15850 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15851 s.select_ranges(Some(70..70))
15852 });
15853 editor.open_excerpts(&OpenExcerpts, window, cx);
15854 });
15855 cx.executor().run_until_parked();
15856 workspace
15857 .update(cx, |workspace, window, cx| {
15858 let active_item = workspace
15859 .active_item(cx)
15860 .expect("should have an active item after navigating into the 3rd buffer");
15861 let third_item_id = active_item.item_id();
15862 assert_ne!(
15863 third_item_id, multibuffer_item_id,
15864 "Should navigate into the 3rd buffer and activate it"
15865 );
15866 assert_ne!(third_item_id, first_item_id);
15867 assert_ne!(third_item_id, second_item_id);
15868 assert!(
15869 active_item.is_singleton(cx),
15870 "New active item should be a singleton buffer"
15871 );
15872 assert_eq!(
15873 active_item
15874 .act_as::<Editor>(cx)
15875 .expect("should have navigated into an editor")
15876 .read(cx)
15877 .text(cx),
15878 sample_text_3
15879 );
15880
15881 workspace
15882 .go_back(workspace.active_pane().downgrade(), window, cx)
15883 .detach_and_log_err(cx);
15884 })
15885 .unwrap();
15886 cx.executor().run_until_parked();
15887 workspace
15888 .update(cx, |workspace, _, cx| {
15889 let active_item = workspace
15890 .active_item(cx)
15891 .expect("should have an active item after navigating back from the 3rd buffer");
15892 assert_eq!(
15893 active_item.item_id(),
15894 multibuffer_item_id,
15895 "Should navigate back from the 3rd buffer to the multi buffer"
15896 );
15897 assert!(!active_item.is_singleton(cx));
15898 })
15899 .unwrap();
15900}
15901
15902#[gpui::test]
15903async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15904 init_test(cx, |_| {});
15905
15906 let mut cx = EditorTestContext::new(cx).await;
15907
15908 let diff_base = r#"
15909 use some::mod;
15910
15911 const A: u32 = 42;
15912
15913 fn main() {
15914 println!("hello");
15915
15916 println!("world");
15917 }
15918 "#
15919 .unindent();
15920
15921 cx.set_state(
15922 &r#"
15923 use some::modified;
15924
15925 ˇ
15926 fn main() {
15927 println!("hello there");
15928
15929 println!("around the");
15930 println!("world");
15931 }
15932 "#
15933 .unindent(),
15934 );
15935
15936 cx.set_head_text(&diff_base);
15937 executor.run_until_parked();
15938
15939 cx.update_editor(|editor, window, cx| {
15940 editor.go_to_next_hunk(&GoToHunk, window, cx);
15941 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15942 });
15943 executor.run_until_parked();
15944 cx.assert_state_with_diff(
15945 r#"
15946 use some::modified;
15947
15948
15949 fn main() {
15950 - println!("hello");
15951 + ˇ println!("hello there");
15952
15953 println!("around the");
15954 println!("world");
15955 }
15956 "#
15957 .unindent(),
15958 );
15959
15960 cx.update_editor(|editor, window, cx| {
15961 for _ in 0..2 {
15962 editor.go_to_next_hunk(&GoToHunk, window, cx);
15963 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15964 }
15965 });
15966 executor.run_until_parked();
15967 cx.assert_state_with_diff(
15968 r#"
15969 - use some::mod;
15970 + ˇuse some::modified;
15971
15972
15973 fn main() {
15974 - println!("hello");
15975 + println!("hello there");
15976
15977 + println!("around the");
15978 println!("world");
15979 }
15980 "#
15981 .unindent(),
15982 );
15983
15984 cx.update_editor(|editor, window, cx| {
15985 editor.go_to_next_hunk(&GoToHunk, window, cx);
15986 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15987 });
15988 executor.run_until_parked();
15989 cx.assert_state_with_diff(
15990 r#"
15991 - use some::mod;
15992 + use some::modified;
15993
15994 - const A: u32 = 42;
15995 ˇ
15996 fn main() {
15997 - println!("hello");
15998 + println!("hello there");
15999
16000 + println!("around the");
16001 println!("world");
16002 }
16003 "#
16004 .unindent(),
16005 );
16006
16007 cx.update_editor(|editor, window, cx| {
16008 editor.cancel(&Cancel, window, cx);
16009 });
16010
16011 cx.assert_state_with_diff(
16012 r#"
16013 use some::modified;
16014
16015 ˇ
16016 fn main() {
16017 println!("hello there");
16018
16019 println!("around the");
16020 println!("world");
16021 }
16022 "#
16023 .unindent(),
16024 );
16025}
16026
16027#[gpui::test]
16028async fn test_diff_base_change_with_expanded_diff_hunks(
16029 executor: BackgroundExecutor,
16030 cx: &mut TestAppContext,
16031) {
16032 init_test(cx, |_| {});
16033
16034 let mut cx = EditorTestContext::new(cx).await;
16035
16036 let diff_base = r#"
16037 use some::mod1;
16038 use some::mod2;
16039
16040 const A: u32 = 42;
16041 const B: u32 = 42;
16042 const C: u32 = 42;
16043
16044 fn main() {
16045 println!("hello");
16046
16047 println!("world");
16048 }
16049 "#
16050 .unindent();
16051
16052 cx.set_state(
16053 &r#"
16054 use some::mod2;
16055
16056 const A: u32 = 42;
16057 const C: u32 = 42;
16058
16059 fn main(ˇ) {
16060 //println!("hello");
16061
16062 println!("world");
16063 //
16064 //
16065 }
16066 "#
16067 .unindent(),
16068 );
16069
16070 cx.set_head_text(&diff_base);
16071 executor.run_until_parked();
16072
16073 cx.update_editor(|editor, window, cx| {
16074 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16075 });
16076 executor.run_until_parked();
16077 cx.assert_state_with_diff(
16078 r#"
16079 - use some::mod1;
16080 use some::mod2;
16081
16082 const A: u32 = 42;
16083 - const B: u32 = 42;
16084 const C: u32 = 42;
16085
16086 fn main(ˇ) {
16087 - println!("hello");
16088 + //println!("hello");
16089
16090 println!("world");
16091 + //
16092 + //
16093 }
16094 "#
16095 .unindent(),
16096 );
16097
16098 cx.set_head_text("new diff base!");
16099 executor.run_until_parked();
16100 cx.assert_state_with_diff(
16101 r#"
16102 - new diff base!
16103 + use some::mod2;
16104 +
16105 + const A: u32 = 42;
16106 + const C: u32 = 42;
16107 +
16108 + fn main(ˇ) {
16109 + //println!("hello");
16110 +
16111 + println!("world");
16112 + //
16113 + //
16114 + }
16115 "#
16116 .unindent(),
16117 );
16118}
16119
16120#[gpui::test]
16121async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
16122 init_test(cx, |_| {});
16123
16124 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
16125 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
16126 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
16127 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
16128 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
16129 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
16130
16131 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
16132 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
16133 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
16134
16135 let multi_buffer = cx.new(|cx| {
16136 let mut multibuffer = MultiBuffer::new(ReadWrite);
16137 multibuffer.push_excerpts(
16138 buffer_1.clone(),
16139 [
16140 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16141 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16142 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16143 ],
16144 cx,
16145 );
16146 multibuffer.push_excerpts(
16147 buffer_2.clone(),
16148 [
16149 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16150 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16151 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16152 ],
16153 cx,
16154 );
16155 multibuffer.push_excerpts(
16156 buffer_3.clone(),
16157 [
16158 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16159 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16160 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16161 ],
16162 cx,
16163 );
16164 multibuffer
16165 });
16166
16167 let editor =
16168 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
16169 editor
16170 .update(cx, |editor, _window, cx| {
16171 for (buffer, diff_base) in [
16172 (buffer_1.clone(), file_1_old),
16173 (buffer_2.clone(), file_2_old),
16174 (buffer_3.clone(), file_3_old),
16175 ] {
16176 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
16177 editor
16178 .buffer
16179 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
16180 }
16181 })
16182 .unwrap();
16183
16184 let mut cx = EditorTestContext::for_editor(editor, cx).await;
16185 cx.run_until_parked();
16186
16187 cx.assert_editor_state(
16188 &"
16189 ˇaaa
16190 ccc
16191 ddd
16192
16193 ggg
16194 hhh
16195
16196
16197 lll
16198 mmm
16199 NNN
16200
16201 qqq
16202 rrr
16203
16204 uuu
16205 111
16206 222
16207 333
16208
16209 666
16210 777
16211
16212 000
16213 !!!"
16214 .unindent(),
16215 );
16216
16217 cx.update_editor(|editor, window, cx| {
16218 editor.select_all(&SelectAll, window, cx);
16219 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16220 });
16221 cx.executor().run_until_parked();
16222
16223 cx.assert_state_with_diff(
16224 "
16225 «aaa
16226 - bbb
16227 ccc
16228 ddd
16229
16230 ggg
16231 hhh
16232
16233
16234 lll
16235 mmm
16236 - nnn
16237 + NNN
16238
16239 qqq
16240 rrr
16241
16242 uuu
16243 111
16244 222
16245 333
16246
16247 + 666
16248 777
16249
16250 000
16251 !!!ˇ»"
16252 .unindent(),
16253 );
16254}
16255
16256#[gpui::test]
16257async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
16258 init_test(cx, |_| {});
16259
16260 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
16261 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
16262
16263 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
16264 let multi_buffer = cx.new(|cx| {
16265 let mut multibuffer = MultiBuffer::new(ReadWrite);
16266 multibuffer.push_excerpts(
16267 buffer.clone(),
16268 [
16269 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
16270 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
16271 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
16272 ],
16273 cx,
16274 );
16275 multibuffer
16276 });
16277
16278 let editor =
16279 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
16280 editor
16281 .update(cx, |editor, _window, cx| {
16282 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
16283 editor
16284 .buffer
16285 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
16286 })
16287 .unwrap();
16288
16289 let mut cx = EditorTestContext::for_editor(editor, cx).await;
16290 cx.run_until_parked();
16291
16292 cx.update_editor(|editor, window, cx| {
16293 editor.expand_all_diff_hunks(&Default::default(), window, cx)
16294 });
16295 cx.executor().run_until_parked();
16296
16297 // When the start of a hunk coincides with the start of its excerpt,
16298 // the hunk is expanded. When the start of a a hunk is earlier than
16299 // the start of its excerpt, the hunk is not expanded.
16300 cx.assert_state_with_diff(
16301 "
16302 ˇaaa
16303 - bbb
16304 + BBB
16305
16306 - ddd
16307 - eee
16308 + DDD
16309 + EEE
16310 fff
16311
16312 iii
16313 "
16314 .unindent(),
16315 );
16316}
16317
16318#[gpui::test]
16319async fn test_edits_around_expanded_insertion_hunks(
16320 executor: BackgroundExecutor,
16321 cx: &mut TestAppContext,
16322) {
16323 init_test(cx, |_| {});
16324
16325 let mut cx = EditorTestContext::new(cx).await;
16326
16327 let diff_base = r#"
16328 use some::mod1;
16329 use some::mod2;
16330
16331 const A: u32 = 42;
16332
16333 fn main() {
16334 println!("hello");
16335
16336 println!("world");
16337 }
16338 "#
16339 .unindent();
16340 executor.run_until_parked();
16341 cx.set_state(
16342 &r#"
16343 use some::mod1;
16344 use some::mod2;
16345
16346 const A: u32 = 42;
16347 const B: u32 = 42;
16348 const C: u32 = 42;
16349 ˇ
16350
16351 fn main() {
16352 println!("hello");
16353
16354 println!("world");
16355 }
16356 "#
16357 .unindent(),
16358 );
16359
16360 cx.set_head_text(&diff_base);
16361 executor.run_until_parked();
16362
16363 cx.update_editor(|editor, window, cx| {
16364 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16365 });
16366 executor.run_until_parked();
16367
16368 cx.assert_state_with_diff(
16369 r#"
16370 use some::mod1;
16371 use some::mod2;
16372
16373 const A: u32 = 42;
16374 + const B: u32 = 42;
16375 + const C: u32 = 42;
16376 + ˇ
16377
16378 fn main() {
16379 println!("hello");
16380
16381 println!("world");
16382 }
16383 "#
16384 .unindent(),
16385 );
16386
16387 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
16388 executor.run_until_parked();
16389
16390 cx.assert_state_with_diff(
16391 r#"
16392 use some::mod1;
16393 use some::mod2;
16394
16395 const A: u32 = 42;
16396 + const B: u32 = 42;
16397 + const C: u32 = 42;
16398 + const D: u32 = 42;
16399 + ˇ
16400
16401 fn main() {
16402 println!("hello");
16403
16404 println!("world");
16405 }
16406 "#
16407 .unindent(),
16408 );
16409
16410 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
16411 executor.run_until_parked();
16412
16413 cx.assert_state_with_diff(
16414 r#"
16415 use some::mod1;
16416 use some::mod2;
16417
16418 const A: u32 = 42;
16419 + const B: u32 = 42;
16420 + const C: u32 = 42;
16421 + const D: u32 = 42;
16422 + const E: u32 = 42;
16423 + ˇ
16424
16425 fn main() {
16426 println!("hello");
16427
16428 println!("world");
16429 }
16430 "#
16431 .unindent(),
16432 );
16433
16434 cx.update_editor(|editor, window, cx| {
16435 editor.delete_line(&DeleteLine, window, cx);
16436 });
16437 executor.run_until_parked();
16438
16439 cx.assert_state_with_diff(
16440 r#"
16441 use some::mod1;
16442 use some::mod2;
16443
16444 const A: u32 = 42;
16445 + const B: u32 = 42;
16446 + const C: u32 = 42;
16447 + const D: u32 = 42;
16448 + const E: u32 = 42;
16449 ˇ
16450 fn main() {
16451 println!("hello");
16452
16453 println!("world");
16454 }
16455 "#
16456 .unindent(),
16457 );
16458
16459 cx.update_editor(|editor, window, cx| {
16460 editor.move_up(&MoveUp, window, cx);
16461 editor.delete_line(&DeleteLine, window, cx);
16462 editor.move_up(&MoveUp, window, cx);
16463 editor.delete_line(&DeleteLine, window, cx);
16464 editor.move_up(&MoveUp, window, cx);
16465 editor.delete_line(&DeleteLine, window, cx);
16466 });
16467 executor.run_until_parked();
16468 cx.assert_state_with_diff(
16469 r#"
16470 use some::mod1;
16471 use some::mod2;
16472
16473 const A: u32 = 42;
16474 + const B: u32 = 42;
16475 ˇ
16476 fn main() {
16477 println!("hello");
16478
16479 println!("world");
16480 }
16481 "#
16482 .unindent(),
16483 );
16484
16485 cx.update_editor(|editor, window, cx| {
16486 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
16487 editor.delete_line(&DeleteLine, window, cx);
16488 });
16489 executor.run_until_parked();
16490 cx.assert_state_with_diff(
16491 r#"
16492 ˇ
16493 fn main() {
16494 println!("hello");
16495
16496 println!("world");
16497 }
16498 "#
16499 .unindent(),
16500 );
16501}
16502
16503#[gpui::test]
16504async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
16505 init_test(cx, |_| {});
16506
16507 let mut cx = EditorTestContext::new(cx).await;
16508 cx.set_head_text(indoc! { "
16509 one
16510 two
16511 three
16512 four
16513 five
16514 "
16515 });
16516 cx.set_state(indoc! { "
16517 one
16518 ˇthree
16519 five
16520 "});
16521 cx.run_until_parked();
16522 cx.update_editor(|editor, window, cx| {
16523 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16524 });
16525 cx.assert_state_with_diff(
16526 indoc! { "
16527 one
16528 - two
16529 ˇthree
16530 - four
16531 five
16532 "}
16533 .to_string(),
16534 );
16535 cx.update_editor(|editor, window, cx| {
16536 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16537 });
16538
16539 cx.assert_state_with_diff(
16540 indoc! { "
16541 one
16542 ˇthree
16543 five
16544 "}
16545 .to_string(),
16546 );
16547
16548 cx.set_state(indoc! { "
16549 one
16550 ˇTWO
16551 three
16552 four
16553 five
16554 "});
16555 cx.run_until_parked();
16556 cx.update_editor(|editor, window, cx| {
16557 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16558 });
16559
16560 cx.assert_state_with_diff(
16561 indoc! { "
16562 one
16563 - two
16564 + ˇTWO
16565 three
16566 four
16567 five
16568 "}
16569 .to_string(),
16570 );
16571 cx.update_editor(|editor, window, cx| {
16572 editor.move_up(&Default::default(), window, cx);
16573 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16574 });
16575 cx.assert_state_with_diff(
16576 indoc! { "
16577 one
16578 ˇTWO
16579 three
16580 four
16581 five
16582 "}
16583 .to_string(),
16584 );
16585}
16586
16587#[gpui::test]
16588async fn test_edits_around_expanded_deletion_hunks(
16589 executor: BackgroundExecutor,
16590 cx: &mut TestAppContext,
16591) {
16592 init_test(cx, |_| {});
16593
16594 let mut cx = EditorTestContext::new(cx).await;
16595
16596 let diff_base = r#"
16597 use some::mod1;
16598 use some::mod2;
16599
16600 const A: u32 = 42;
16601 const B: u32 = 42;
16602 const C: u32 = 42;
16603
16604
16605 fn main() {
16606 println!("hello");
16607
16608 println!("world");
16609 }
16610 "#
16611 .unindent();
16612 executor.run_until_parked();
16613 cx.set_state(
16614 &r#"
16615 use some::mod1;
16616 use some::mod2;
16617
16618 ˇconst B: u32 = 42;
16619 const C: u32 = 42;
16620
16621
16622 fn main() {
16623 println!("hello");
16624
16625 println!("world");
16626 }
16627 "#
16628 .unindent(),
16629 );
16630
16631 cx.set_head_text(&diff_base);
16632 executor.run_until_parked();
16633
16634 cx.update_editor(|editor, window, cx| {
16635 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16636 });
16637 executor.run_until_parked();
16638
16639 cx.assert_state_with_diff(
16640 r#"
16641 use some::mod1;
16642 use some::mod2;
16643
16644 - const A: u32 = 42;
16645 ˇconst B: u32 = 42;
16646 const C: u32 = 42;
16647
16648
16649 fn main() {
16650 println!("hello");
16651
16652 println!("world");
16653 }
16654 "#
16655 .unindent(),
16656 );
16657
16658 cx.update_editor(|editor, window, cx| {
16659 editor.delete_line(&DeleteLine, window, cx);
16660 });
16661 executor.run_until_parked();
16662 cx.assert_state_with_diff(
16663 r#"
16664 use some::mod1;
16665 use some::mod2;
16666
16667 - const A: u32 = 42;
16668 - const B: u32 = 42;
16669 ˇconst C: u32 = 42;
16670
16671
16672 fn main() {
16673 println!("hello");
16674
16675 println!("world");
16676 }
16677 "#
16678 .unindent(),
16679 );
16680
16681 cx.update_editor(|editor, window, cx| {
16682 editor.delete_line(&DeleteLine, window, cx);
16683 });
16684 executor.run_until_parked();
16685 cx.assert_state_with_diff(
16686 r#"
16687 use some::mod1;
16688 use some::mod2;
16689
16690 - const A: u32 = 42;
16691 - const B: u32 = 42;
16692 - const C: u32 = 42;
16693 ˇ
16694
16695 fn main() {
16696 println!("hello");
16697
16698 println!("world");
16699 }
16700 "#
16701 .unindent(),
16702 );
16703
16704 cx.update_editor(|editor, window, cx| {
16705 editor.handle_input("replacement", window, cx);
16706 });
16707 executor.run_until_parked();
16708 cx.assert_state_with_diff(
16709 r#"
16710 use some::mod1;
16711 use some::mod2;
16712
16713 - const A: u32 = 42;
16714 - const B: u32 = 42;
16715 - const C: u32 = 42;
16716 -
16717 + replacementˇ
16718
16719 fn main() {
16720 println!("hello");
16721
16722 println!("world");
16723 }
16724 "#
16725 .unindent(),
16726 );
16727}
16728
16729#[gpui::test]
16730async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16731 init_test(cx, |_| {});
16732
16733 let mut cx = EditorTestContext::new(cx).await;
16734
16735 let base_text = r#"
16736 one
16737 two
16738 three
16739 four
16740 five
16741 "#
16742 .unindent();
16743 executor.run_until_parked();
16744 cx.set_state(
16745 &r#"
16746 one
16747 two
16748 fˇour
16749 five
16750 "#
16751 .unindent(),
16752 );
16753
16754 cx.set_head_text(&base_text);
16755 executor.run_until_parked();
16756
16757 cx.update_editor(|editor, window, cx| {
16758 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16759 });
16760 executor.run_until_parked();
16761
16762 cx.assert_state_with_diff(
16763 r#"
16764 one
16765 two
16766 - three
16767 fˇour
16768 five
16769 "#
16770 .unindent(),
16771 );
16772
16773 cx.update_editor(|editor, window, cx| {
16774 editor.backspace(&Backspace, window, cx);
16775 editor.backspace(&Backspace, window, cx);
16776 });
16777 executor.run_until_parked();
16778 cx.assert_state_with_diff(
16779 r#"
16780 one
16781 two
16782 - threeˇ
16783 - four
16784 + our
16785 five
16786 "#
16787 .unindent(),
16788 );
16789}
16790
16791#[gpui::test]
16792async fn test_edit_after_expanded_modification_hunk(
16793 executor: BackgroundExecutor,
16794 cx: &mut TestAppContext,
16795) {
16796 init_test(cx, |_| {});
16797
16798 let mut cx = EditorTestContext::new(cx).await;
16799
16800 let diff_base = r#"
16801 use some::mod1;
16802 use some::mod2;
16803
16804 const A: u32 = 42;
16805 const B: u32 = 42;
16806 const C: u32 = 42;
16807 const D: u32 = 42;
16808
16809
16810 fn main() {
16811 println!("hello");
16812
16813 println!("world");
16814 }"#
16815 .unindent();
16816
16817 cx.set_state(
16818 &r#"
16819 use some::mod1;
16820 use some::mod2;
16821
16822 const A: u32 = 42;
16823 const B: u32 = 42;
16824 const C: u32 = 43ˇ
16825 const D: u32 = 42;
16826
16827
16828 fn main() {
16829 println!("hello");
16830
16831 println!("world");
16832 }"#
16833 .unindent(),
16834 );
16835
16836 cx.set_head_text(&diff_base);
16837 executor.run_until_parked();
16838 cx.update_editor(|editor, window, cx| {
16839 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16840 });
16841 executor.run_until_parked();
16842
16843 cx.assert_state_with_diff(
16844 r#"
16845 use some::mod1;
16846 use some::mod2;
16847
16848 const A: u32 = 42;
16849 const B: u32 = 42;
16850 - const C: u32 = 42;
16851 + const C: u32 = 43ˇ
16852 const D: u32 = 42;
16853
16854
16855 fn main() {
16856 println!("hello");
16857
16858 println!("world");
16859 }"#
16860 .unindent(),
16861 );
16862
16863 cx.update_editor(|editor, window, cx| {
16864 editor.handle_input("\nnew_line\n", window, cx);
16865 });
16866 executor.run_until_parked();
16867
16868 cx.assert_state_with_diff(
16869 r#"
16870 use some::mod1;
16871 use some::mod2;
16872
16873 const A: u32 = 42;
16874 const B: u32 = 42;
16875 - const C: u32 = 42;
16876 + const C: u32 = 43
16877 + new_line
16878 + ˇ
16879 const D: u32 = 42;
16880
16881
16882 fn main() {
16883 println!("hello");
16884
16885 println!("world");
16886 }"#
16887 .unindent(),
16888 );
16889}
16890
16891#[gpui::test]
16892async fn test_stage_and_unstage_added_file_hunk(
16893 executor: BackgroundExecutor,
16894 cx: &mut TestAppContext,
16895) {
16896 init_test(cx, |_| {});
16897
16898 let mut cx = EditorTestContext::new(cx).await;
16899 cx.update_editor(|editor, _, cx| {
16900 editor.set_expand_all_diff_hunks(cx);
16901 });
16902
16903 let working_copy = r#"
16904 ˇfn main() {
16905 println!("hello, world!");
16906 }
16907 "#
16908 .unindent();
16909
16910 cx.set_state(&working_copy);
16911 executor.run_until_parked();
16912
16913 cx.assert_state_with_diff(
16914 r#"
16915 + ˇfn main() {
16916 + println!("hello, world!");
16917 + }
16918 "#
16919 .unindent(),
16920 );
16921 cx.assert_index_text(None);
16922
16923 cx.update_editor(|editor, window, cx| {
16924 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16925 });
16926 executor.run_until_parked();
16927 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
16928 cx.assert_state_with_diff(
16929 r#"
16930 + ˇfn main() {
16931 + println!("hello, world!");
16932 + }
16933 "#
16934 .unindent(),
16935 );
16936
16937 cx.update_editor(|editor, window, cx| {
16938 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16939 });
16940 executor.run_until_parked();
16941 cx.assert_index_text(None);
16942}
16943
16944async fn setup_indent_guides_editor(
16945 text: &str,
16946 cx: &mut TestAppContext,
16947) -> (BufferId, EditorTestContext) {
16948 init_test(cx, |_| {});
16949
16950 let mut cx = EditorTestContext::new(cx).await;
16951
16952 let buffer_id = cx.update_editor(|editor, window, cx| {
16953 editor.set_text(text, window, cx);
16954 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
16955
16956 buffer_ids[0]
16957 });
16958
16959 (buffer_id, cx)
16960}
16961
16962fn assert_indent_guides(
16963 range: Range<u32>,
16964 expected: Vec<IndentGuide>,
16965 active_indices: Option<Vec<usize>>,
16966 cx: &mut EditorTestContext,
16967) {
16968 let indent_guides = cx.update_editor(|editor, window, cx| {
16969 let snapshot = editor.snapshot(window, cx).display_snapshot;
16970 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
16971 editor,
16972 MultiBufferRow(range.start)..MultiBufferRow(range.end),
16973 true,
16974 &snapshot,
16975 cx,
16976 );
16977
16978 indent_guides.sort_by(|a, b| {
16979 a.depth.cmp(&b.depth).then(
16980 a.start_row
16981 .cmp(&b.start_row)
16982 .then(a.end_row.cmp(&b.end_row)),
16983 )
16984 });
16985 indent_guides
16986 });
16987
16988 if let Some(expected) = active_indices {
16989 let active_indices = cx.update_editor(|editor, window, cx| {
16990 let snapshot = editor.snapshot(window, cx).display_snapshot;
16991 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
16992 });
16993
16994 assert_eq!(
16995 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
16996 expected,
16997 "Active indent guide indices do not match"
16998 );
16999 }
17000
17001 assert_eq!(indent_guides, expected, "Indent guides do not match");
17002}
17003
17004fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
17005 IndentGuide {
17006 buffer_id,
17007 start_row: MultiBufferRow(start_row),
17008 end_row: MultiBufferRow(end_row),
17009 depth,
17010 tab_size: 4,
17011 settings: IndentGuideSettings {
17012 enabled: true,
17013 line_width: 1,
17014 active_line_width: 1,
17015 ..Default::default()
17016 },
17017 }
17018}
17019
17020#[gpui::test]
17021async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
17022 let (buffer_id, mut cx) = setup_indent_guides_editor(
17023 &"
17024 fn main() {
17025 let a = 1;
17026 }"
17027 .unindent(),
17028 cx,
17029 )
17030 .await;
17031
17032 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
17033}
17034
17035#[gpui::test]
17036async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
17037 let (buffer_id, mut cx) = setup_indent_guides_editor(
17038 &"
17039 fn main() {
17040 let a = 1;
17041 let b = 2;
17042 }"
17043 .unindent(),
17044 cx,
17045 )
17046 .await;
17047
17048 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
17049}
17050
17051#[gpui::test]
17052async fn test_indent_guide_nested(cx: &mut TestAppContext) {
17053 let (buffer_id, mut cx) = setup_indent_guides_editor(
17054 &"
17055 fn main() {
17056 let a = 1;
17057 if a == 3 {
17058 let b = 2;
17059 } else {
17060 let c = 3;
17061 }
17062 }"
17063 .unindent(),
17064 cx,
17065 )
17066 .await;
17067
17068 assert_indent_guides(
17069 0..8,
17070 vec![
17071 indent_guide(buffer_id, 1, 6, 0),
17072 indent_guide(buffer_id, 3, 3, 1),
17073 indent_guide(buffer_id, 5, 5, 1),
17074 ],
17075 None,
17076 &mut cx,
17077 );
17078}
17079
17080#[gpui::test]
17081async fn test_indent_guide_tab(cx: &mut TestAppContext) {
17082 let (buffer_id, mut cx) = setup_indent_guides_editor(
17083 &"
17084 fn main() {
17085 let a = 1;
17086 let b = 2;
17087 let c = 3;
17088 }"
17089 .unindent(),
17090 cx,
17091 )
17092 .await;
17093
17094 assert_indent_guides(
17095 0..5,
17096 vec![
17097 indent_guide(buffer_id, 1, 3, 0),
17098 indent_guide(buffer_id, 2, 2, 1),
17099 ],
17100 None,
17101 &mut cx,
17102 );
17103}
17104
17105#[gpui::test]
17106async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
17107 let (buffer_id, mut cx) = setup_indent_guides_editor(
17108 &"
17109 fn main() {
17110 let a = 1;
17111
17112 let c = 3;
17113 }"
17114 .unindent(),
17115 cx,
17116 )
17117 .await;
17118
17119 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
17120}
17121
17122#[gpui::test]
17123async fn test_indent_guide_complex(cx: &mut TestAppContext) {
17124 let (buffer_id, mut cx) = setup_indent_guides_editor(
17125 &"
17126 fn main() {
17127 let a = 1;
17128
17129 let c = 3;
17130
17131 if a == 3 {
17132 let b = 2;
17133 } else {
17134 let c = 3;
17135 }
17136 }"
17137 .unindent(),
17138 cx,
17139 )
17140 .await;
17141
17142 assert_indent_guides(
17143 0..11,
17144 vec![
17145 indent_guide(buffer_id, 1, 9, 0),
17146 indent_guide(buffer_id, 6, 6, 1),
17147 indent_guide(buffer_id, 8, 8, 1),
17148 ],
17149 None,
17150 &mut cx,
17151 );
17152}
17153
17154#[gpui::test]
17155async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
17156 let (buffer_id, mut cx) = setup_indent_guides_editor(
17157 &"
17158 fn main() {
17159 let a = 1;
17160
17161 let c = 3;
17162
17163 if a == 3 {
17164 let b = 2;
17165 } else {
17166 let c = 3;
17167 }
17168 }"
17169 .unindent(),
17170 cx,
17171 )
17172 .await;
17173
17174 assert_indent_guides(
17175 1..11,
17176 vec![
17177 indent_guide(buffer_id, 1, 9, 0),
17178 indent_guide(buffer_id, 6, 6, 1),
17179 indent_guide(buffer_id, 8, 8, 1),
17180 ],
17181 None,
17182 &mut cx,
17183 );
17184}
17185
17186#[gpui::test]
17187async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
17188 let (buffer_id, mut cx) = setup_indent_guides_editor(
17189 &"
17190 fn main() {
17191 let a = 1;
17192
17193 let c = 3;
17194
17195 if a == 3 {
17196 let b = 2;
17197 } else {
17198 let c = 3;
17199 }
17200 }"
17201 .unindent(),
17202 cx,
17203 )
17204 .await;
17205
17206 assert_indent_guides(
17207 1..10,
17208 vec![
17209 indent_guide(buffer_id, 1, 9, 0),
17210 indent_guide(buffer_id, 6, 6, 1),
17211 indent_guide(buffer_id, 8, 8, 1),
17212 ],
17213 None,
17214 &mut cx,
17215 );
17216}
17217
17218#[gpui::test]
17219async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
17220 let (buffer_id, mut cx) = setup_indent_guides_editor(
17221 &"
17222 fn main() {
17223 if a {
17224 b(
17225 c,
17226 d,
17227 )
17228 } else {
17229 e(
17230 f
17231 )
17232 }
17233 }"
17234 .unindent(),
17235 cx,
17236 )
17237 .await;
17238
17239 assert_indent_guides(
17240 0..11,
17241 vec![
17242 indent_guide(buffer_id, 1, 10, 0),
17243 indent_guide(buffer_id, 2, 5, 1),
17244 indent_guide(buffer_id, 7, 9, 1),
17245 indent_guide(buffer_id, 3, 4, 2),
17246 indent_guide(buffer_id, 8, 8, 2),
17247 ],
17248 None,
17249 &mut cx,
17250 );
17251
17252 cx.update_editor(|editor, window, cx| {
17253 editor.fold_at(MultiBufferRow(2), window, cx);
17254 assert_eq!(
17255 editor.display_text(cx),
17256 "
17257 fn main() {
17258 if a {
17259 b(⋯
17260 )
17261 } else {
17262 e(
17263 f
17264 )
17265 }
17266 }"
17267 .unindent()
17268 );
17269 });
17270
17271 assert_indent_guides(
17272 0..11,
17273 vec![
17274 indent_guide(buffer_id, 1, 10, 0),
17275 indent_guide(buffer_id, 2, 5, 1),
17276 indent_guide(buffer_id, 7, 9, 1),
17277 indent_guide(buffer_id, 8, 8, 2),
17278 ],
17279 None,
17280 &mut cx,
17281 );
17282}
17283
17284#[gpui::test]
17285async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
17286 let (buffer_id, mut cx) = setup_indent_guides_editor(
17287 &"
17288 block1
17289 block2
17290 block3
17291 block4
17292 block2
17293 block1
17294 block1"
17295 .unindent(),
17296 cx,
17297 )
17298 .await;
17299
17300 assert_indent_guides(
17301 1..10,
17302 vec![
17303 indent_guide(buffer_id, 1, 4, 0),
17304 indent_guide(buffer_id, 2, 3, 1),
17305 indent_guide(buffer_id, 3, 3, 2),
17306 ],
17307 None,
17308 &mut cx,
17309 );
17310}
17311
17312#[gpui::test]
17313async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
17314 let (buffer_id, mut cx) = setup_indent_guides_editor(
17315 &"
17316 block1
17317 block2
17318 block3
17319
17320 block1
17321 block1"
17322 .unindent(),
17323 cx,
17324 )
17325 .await;
17326
17327 assert_indent_guides(
17328 0..6,
17329 vec![
17330 indent_guide(buffer_id, 1, 2, 0),
17331 indent_guide(buffer_id, 2, 2, 1),
17332 ],
17333 None,
17334 &mut cx,
17335 );
17336}
17337
17338#[gpui::test]
17339async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
17340 let (buffer_id, mut cx) = setup_indent_guides_editor(
17341 &"
17342 function component() {
17343 \treturn (
17344 \t\t\t
17345 \t\t<div>
17346 \t\t\t<abc></abc>
17347 \t\t</div>
17348 \t)
17349 }"
17350 .unindent(),
17351 cx,
17352 )
17353 .await;
17354
17355 assert_indent_guides(
17356 0..8,
17357 vec![
17358 indent_guide(buffer_id, 1, 6, 0),
17359 indent_guide(buffer_id, 2, 5, 1),
17360 indent_guide(buffer_id, 4, 4, 2),
17361 ],
17362 None,
17363 &mut cx,
17364 );
17365}
17366
17367#[gpui::test]
17368async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
17369 let (buffer_id, mut cx) = setup_indent_guides_editor(
17370 &"
17371 function component() {
17372 \treturn (
17373 \t
17374 \t\t<div>
17375 \t\t\t<abc></abc>
17376 \t\t</div>
17377 \t)
17378 }"
17379 .unindent(),
17380 cx,
17381 )
17382 .await;
17383
17384 assert_indent_guides(
17385 0..8,
17386 vec![
17387 indent_guide(buffer_id, 1, 6, 0),
17388 indent_guide(buffer_id, 2, 5, 1),
17389 indent_guide(buffer_id, 4, 4, 2),
17390 ],
17391 None,
17392 &mut cx,
17393 );
17394}
17395
17396#[gpui::test]
17397async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
17398 let (buffer_id, mut cx) = setup_indent_guides_editor(
17399 &"
17400 block1
17401
17402
17403
17404 block2
17405 "
17406 .unindent(),
17407 cx,
17408 )
17409 .await;
17410
17411 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
17412}
17413
17414#[gpui::test]
17415async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
17416 let (buffer_id, mut cx) = setup_indent_guides_editor(
17417 &"
17418 def a:
17419 \tb = 3
17420 \tif True:
17421 \t\tc = 4
17422 \t\td = 5
17423 \tprint(b)
17424 "
17425 .unindent(),
17426 cx,
17427 )
17428 .await;
17429
17430 assert_indent_guides(
17431 0..6,
17432 vec![
17433 indent_guide(buffer_id, 1, 5, 0),
17434 indent_guide(buffer_id, 3, 4, 1),
17435 ],
17436 None,
17437 &mut cx,
17438 );
17439}
17440
17441#[gpui::test]
17442async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
17443 let (buffer_id, mut cx) = setup_indent_guides_editor(
17444 &"
17445 fn main() {
17446 let a = 1;
17447 }"
17448 .unindent(),
17449 cx,
17450 )
17451 .await;
17452
17453 cx.update_editor(|editor, window, cx| {
17454 editor.change_selections(None, window, cx, |s| {
17455 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17456 });
17457 });
17458
17459 assert_indent_guides(
17460 0..3,
17461 vec![indent_guide(buffer_id, 1, 1, 0)],
17462 Some(vec![0]),
17463 &mut cx,
17464 );
17465}
17466
17467#[gpui::test]
17468async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
17469 let (buffer_id, mut cx) = setup_indent_guides_editor(
17470 &"
17471 fn main() {
17472 if 1 == 2 {
17473 let a = 1;
17474 }
17475 }"
17476 .unindent(),
17477 cx,
17478 )
17479 .await;
17480
17481 cx.update_editor(|editor, window, cx| {
17482 editor.change_selections(None, window, cx, |s| {
17483 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17484 });
17485 });
17486
17487 assert_indent_guides(
17488 0..4,
17489 vec![
17490 indent_guide(buffer_id, 1, 3, 0),
17491 indent_guide(buffer_id, 2, 2, 1),
17492 ],
17493 Some(vec![1]),
17494 &mut cx,
17495 );
17496
17497 cx.update_editor(|editor, window, cx| {
17498 editor.change_selections(None, window, cx, |s| {
17499 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
17500 });
17501 });
17502
17503 assert_indent_guides(
17504 0..4,
17505 vec![
17506 indent_guide(buffer_id, 1, 3, 0),
17507 indent_guide(buffer_id, 2, 2, 1),
17508 ],
17509 Some(vec![1]),
17510 &mut cx,
17511 );
17512
17513 cx.update_editor(|editor, window, cx| {
17514 editor.change_selections(None, window, cx, |s| {
17515 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
17516 });
17517 });
17518
17519 assert_indent_guides(
17520 0..4,
17521 vec![
17522 indent_guide(buffer_id, 1, 3, 0),
17523 indent_guide(buffer_id, 2, 2, 1),
17524 ],
17525 Some(vec![0]),
17526 &mut cx,
17527 );
17528}
17529
17530#[gpui::test]
17531async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
17532 let (buffer_id, mut cx) = setup_indent_guides_editor(
17533 &"
17534 fn main() {
17535 let a = 1;
17536
17537 let b = 2;
17538 }"
17539 .unindent(),
17540 cx,
17541 )
17542 .await;
17543
17544 cx.update_editor(|editor, window, cx| {
17545 editor.change_selections(None, window, cx, |s| {
17546 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
17547 });
17548 });
17549
17550 assert_indent_guides(
17551 0..5,
17552 vec![indent_guide(buffer_id, 1, 3, 0)],
17553 Some(vec![0]),
17554 &mut cx,
17555 );
17556}
17557
17558#[gpui::test]
17559async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
17560 let (buffer_id, mut cx) = setup_indent_guides_editor(
17561 &"
17562 def m:
17563 a = 1
17564 pass"
17565 .unindent(),
17566 cx,
17567 )
17568 .await;
17569
17570 cx.update_editor(|editor, window, cx| {
17571 editor.change_selections(None, window, cx, |s| {
17572 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17573 });
17574 });
17575
17576 assert_indent_guides(
17577 0..3,
17578 vec![indent_guide(buffer_id, 1, 2, 0)],
17579 Some(vec![0]),
17580 &mut cx,
17581 );
17582}
17583
17584#[gpui::test]
17585async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
17586 init_test(cx, |_| {});
17587 let mut cx = EditorTestContext::new(cx).await;
17588 let text = indoc! {
17589 "
17590 impl A {
17591 fn b() {
17592 0;
17593 3;
17594 5;
17595 6;
17596 7;
17597 }
17598 }
17599 "
17600 };
17601 let base_text = indoc! {
17602 "
17603 impl A {
17604 fn b() {
17605 0;
17606 1;
17607 2;
17608 3;
17609 4;
17610 }
17611 fn c() {
17612 5;
17613 6;
17614 7;
17615 }
17616 }
17617 "
17618 };
17619
17620 cx.update_editor(|editor, window, cx| {
17621 editor.set_text(text, window, cx);
17622
17623 editor.buffer().update(cx, |multibuffer, cx| {
17624 let buffer = multibuffer.as_singleton().unwrap();
17625 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
17626
17627 multibuffer.set_all_diff_hunks_expanded(cx);
17628 multibuffer.add_diff(diff, cx);
17629
17630 buffer.read(cx).remote_id()
17631 })
17632 });
17633 cx.run_until_parked();
17634
17635 cx.assert_state_with_diff(
17636 indoc! { "
17637 impl A {
17638 fn b() {
17639 0;
17640 - 1;
17641 - 2;
17642 3;
17643 - 4;
17644 - }
17645 - fn c() {
17646 5;
17647 6;
17648 7;
17649 }
17650 }
17651 ˇ"
17652 }
17653 .to_string(),
17654 );
17655
17656 let mut actual_guides = cx.update_editor(|editor, window, cx| {
17657 editor
17658 .snapshot(window, cx)
17659 .buffer_snapshot
17660 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
17661 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
17662 .collect::<Vec<_>>()
17663 });
17664 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
17665 assert_eq!(
17666 actual_guides,
17667 vec![
17668 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
17669 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
17670 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
17671 ]
17672 );
17673}
17674
17675#[gpui::test]
17676async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17677 init_test(cx, |_| {});
17678 let mut cx = EditorTestContext::new(cx).await;
17679
17680 let diff_base = r#"
17681 a
17682 b
17683 c
17684 "#
17685 .unindent();
17686
17687 cx.set_state(
17688 &r#"
17689 ˇA
17690 b
17691 C
17692 "#
17693 .unindent(),
17694 );
17695 cx.set_head_text(&diff_base);
17696 cx.update_editor(|editor, window, cx| {
17697 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17698 });
17699 executor.run_until_parked();
17700
17701 let both_hunks_expanded = r#"
17702 - a
17703 + ˇA
17704 b
17705 - c
17706 + C
17707 "#
17708 .unindent();
17709
17710 cx.assert_state_with_diff(both_hunks_expanded.clone());
17711
17712 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17713 let snapshot = editor.snapshot(window, cx);
17714 let hunks = editor
17715 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17716 .collect::<Vec<_>>();
17717 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17718 let buffer_id = hunks[0].buffer_id;
17719 hunks
17720 .into_iter()
17721 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17722 .collect::<Vec<_>>()
17723 });
17724 assert_eq!(hunk_ranges.len(), 2);
17725
17726 cx.update_editor(|editor, _, cx| {
17727 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17728 });
17729 executor.run_until_parked();
17730
17731 let second_hunk_expanded = r#"
17732 ˇA
17733 b
17734 - c
17735 + C
17736 "#
17737 .unindent();
17738
17739 cx.assert_state_with_diff(second_hunk_expanded);
17740
17741 cx.update_editor(|editor, _, cx| {
17742 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17743 });
17744 executor.run_until_parked();
17745
17746 cx.assert_state_with_diff(both_hunks_expanded.clone());
17747
17748 cx.update_editor(|editor, _, cx| {
17749 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17750 });
17751 executor.run_until_parked();
17752
17753 let first_hunk_expanded = r#"
17754 - a
17755 + ˇA
17756 b
17757 C
17758 "#
17759 .unindent();
17760
17761 cx.assert_state_with_diff(first_hunk_expanded);
17762
17763 cx.update_editor(|editor, _, cx| {
17764 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17765 });
17766 executor.run_until_parked();
17767
17768 cx.assert_state_with_diff(both_hunks_expanded);
17769
17770 cx.set_state(
17771 &r#"
17772 ˇA
17773 b
17774 "#
17775 .unindent(),
17776 );
17777 cx.run_until_parked();
17778
17779 // TODO this cursor position seems bad
17780 cx.assert_state_with_diff(
17781 r#"
17782 - ˇa
17783 + A
17784 b
17785 "#
17786 .unindent(),
17787 );
17788
17789 cx.update_editor(|editor, window, cx| {
17790 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17791 });
17792
17793 cx.assert_state_with_diff(
17794 r#"
17795 - ˇa
17796 + A
17797 b
17798 - c
17799 "#
17800 .unindent(),
17801 );
17802
17803 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17804 let snapshot = editor.snapshot(window, cx);
17805 let hunks = editor
17806 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17807 .collect::<Vec<_>>();
17808 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17809 let buffer_id = hunks[0].buffer_id;
17810 hunks
17811 .into_iter()
17812 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17813 .collect::<Vec<_>>()
17814 });
17815 assert_eq!(hunk_ranges.len(), 2);
17816
17817 cx.update_editor(|editor, _, cx| {
17818 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17819 });
17820 executor.run_until_parked();
17821
17822 cx.assert_state_with_diff(
17823 r#"
17824 - ˇa
17825 + A
17826 b
17827 "#
17828 .unindent(),
17829 );
17830}
17831
17832#[gpui::test]
17833async fn test_toggle_deletion_hunk_at_start_of_file(
17834 executor: BackgroundExecutor,
17835 cx: &mut TestAppContext,
17836) {
17837 init_test(cx, |_| {});
17838 let mut cx = EditorTestContext::new(cx).await;
17839
17840 let diff_base = r#"
17841 a
17842 b
17843 c
17844 "#
17845 .unindent();
17846
17847 cx.set_state(
17848 &r#"
17849 ˇb
17850 c
17851 "#
17852 .unindent(),
17853 );
17854 cx.set_head_text(&diff_base);
17855 cx.update_editor(|editor, window, cx| {
17856 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17857 });
17858 executor.run_until_parked();
17859
17860 let hunk_expanded = r#"
17861 - a
17862 ˇb
17863 c
17864 "#
17865 .unindent();
17866
17867 cx.assert_state_with_diff(hunk_expanded.clone());
17868
17869 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17870 let snapshot = editor.snapshot(window, cx);
17871 let hunks = editor
17872 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17873 .collect::<Vec<_>>();
17874 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17875 let buffer_id = hunks[0].buffer_id;
17876 hunks
17877 .into_iter()
17878 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17879 .collect::<Vec<_>>()
17880 });
17881 assert_eq!(hunk_ranges.len(), 1);
17882
17883 cx.update_editor(|editor, _, cx| {
17884 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17885 });
17886 executor.run_until_parked();
17887
17888 let hunk_collapsed = r#"
17889 ˇb
17890 c
17891 "#
17892 .unindent();
17893
17894 cx.assert_state_with_diff(hunk_collapsed);
17895
17896 cx.update_editor(|editor, _, cx| {
17897 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17898 });
17899 executor.run_until_parked();
17900
17901 cx.assert_state_with_diff(hunk_expanded.clone());
17902}
17903
17904#[gpui::test]
17905async fn test_display_diff_hunks(cx: &mut TestAppContext) {
17906 init_test(cx, |_| {});
17907
17908 let fs = FakeFs::new(cx.executor());
17909 fs.insert_tree(
17910 path!("/test"),
17911 json!({
17912 ".git": {},
17913 "file-1": "ONE\n",
17914 "file-2": "TWO\n",
17915 "file-3": "THREE\n",
17916 }),
17917 )
17918 .await;
17919
17920 fs.set_head_for_repo(
17921 path!("/test/.git").as_ref(),
17922 &[
17923 ("file-1".into(), "one\n".into()),
17924 ("file-2".into(), "two\n".into()),
17925 ("file-3".into(), "three\n".into()),
17926 ],
17927 "deadbeef",
17928 );
17929
17930 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
17931 let mut buffers = vec![];
17932 for i in 1..=3 {
17933 let buffer = project
17934 .update(cx, |project, cx| {
17935 let path = format!(path!("/test/file-{}"), i);
17936 project.open_local_buffer(path, cx)
17937 })
17938 .await
17939 .unwrap();
17940 buffers.push(buffer);
17941 }
17942
17943 let multibuffer = cx.new(|cx| {
17944 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
17945 multibuffer.set_all_diff_hunks_expanded(cx);
17946 for buffer in &buffers {
17947 let snapshot = buffer.read(cx).snapshot();
17948 multibuffer.set_excerpts_for_path(
17949 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
17950 buffer.clone(),
17951 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
17952 DEFAULT_MULTIBUFFER_CONTEXT,
17953 cx,
17954 );
17955 }
17956 multibuffer
17957 });
17958
17959 let editor = cx.add_window(|window, cx| {
17960 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
17961 });
17962 cx.run_until_parked();
17963
17964 let snapshot = editor
17965 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17966 .unwrap();
17967 let hunks = snapshot
17968 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
17969 .map(|hunk| match hunk {
17970 DisplayDiffHunk::Unfolded {
17971 display_row_range, ..
17972 } => display_row_range,
17973 DisplayDiffHunk::Folded { .. } => unreachable!(),
17974 })
17975 .collect::<Vec<_>>();
17976 assert_eq!(
17977 hunks,
17978 [
17979 DisplayRow(2)..DisplayRow(4),
17980 DisplayRow(7)..DisplayRow(9),
17981 DisplayRow(12)..DisplayRow(14),
17982 ]
17983 );
17984}
17985
17986#[gpui::test]
17987async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
17988 init_test(cx, |_| {});
17989
17990 let mut cx = EditorTestContext::new(cx).await;
17991 cx.set_head_text(indoc! { "
17992 one
17993 two
17994 three
17995 four
17996 five
17997 "
17998 });
17999 cx.set_index_text(indoc! { "
18000 one
18001 two
18002 three
18003 four
18004 five
18005 "
18006 });
18007 cx.set_state(indoc! {"
18008 one
18009 TWO
18010 ˇTHREE
18011 FOUR
18012 five
18013 "});
18014 cx.run_until_parked();
18015 cx.update_editor(|editor, window, cx| {
18016 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18017 });
18018 cx.run_until_parked();
18019 cx.assert_index_text(Some(indoc! {"
18020 one
18021 TWO
18022 THREE
18023 FOUR
18024 five
18025 "}));
18026 cx.set_state(indoc! { "
18027 one
18028 TWO
18029 ˇTHREE-HUNDRED
18030 FOUR
18031 five
18032 "});
18033 cx.run_until_parked();
18034 cx.update_editor(|editor, window, cx| {
18035 let snapshot = editor.snapshot(window, cx);
18036 let hunks = editor
18037 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18038 .collect::<Vec<_>>();
18039 assert_eq!(hunks.len(), 1);
18040 assert_eq!(
18041 hunks[0].status(),
18042 DiffHunkStatus {
18043 kind: DiffHunkStatusKind::Modified,
18044 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
18045 }
18046 );
18047
18048 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18049 });
18050 cx.run_until_parked();
18051 cx.assert_index_text(Some(indoc! {"
18052 one
18053 TWO
18054 THREE-HUNDRED
18055 FOUR
18056 five
18057 "}));
18058}
18059
18060#[gpui::test]
18061fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
18062 init_test(cx, |_| {});
18063
18064 let editor = cx.add_window(|window, cx| {
18065 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
18066 build_editor(buffer, window, cx)
18067 });
18068
18069 let render_args = Arc::new(Mutex::new(None));
18070 let snapshot = editor
18071 .update(cx, |editor, window, cx| {
18072 let snapshot = editor.buffer().read(cx).snapshot(cx);
18073 let range =
18074 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
18075
18076 struct RenderArgs {
18077 row: MultiBufferRow,
18078 folded: bool,
18079 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
18080 }
18081
18082 let crease = Crease::inline(
18083 range,
18084 FoldPlaceholder::test(),
18085 {
18086 let toggle_callback = render_args.clone();
18087 move |row, folded, callback, _window, _cx| {
18088 *toggle_callback.lock() = Some(RenderArgs {
18089 row,
18090 folded,
18091 callback,
18092 });
18093 div()
18094 }
18095 },
18096 |_row, _folded, _window, _cx| div(),
18097 );
18098
18099 editor.insert_creases(Some(crease), cx);
18100 let snapshot = editor.snapshot(window, cx);
18101 let _div = snapshot.render_crease_toggle(
18102 MultiBufferRow(1),
18103 false,
18104 cx.entity().clone(),
18105 window,
18106 cx,
18107 );
18108 snapshot
18109 })
18110 .unwrap();
18111
18112 let render_args = render_args.lock().take().unwrap();
18113 assert_eq!(render_args.row, MultiBufferRow(1));
18114 assert!(!render_args.folded);
18115 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
18116
18117 cx.update_window(*editor, |_, window, cx| {
18118 (render_args.callback)(true, window, cx)
18119 })
18120 .unwrap();
18121 let snapshot = editor
18122 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18123 .unwrap();
18124 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
18125
18126 cx.update_window(*editor, |_, window, cx| {
18127 (render_args.callback)(false, window, cx)
18128 })
18129 .unwrap();
18130 let snapshot = editor
18131 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18132 .unwrap();
18133 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
18134}
18135
18136#[gpui::test]
18137async fn test_input_text(cx: &mut TestAppContext) {
18138 init_test(cx, |_| {});
18139 let mut cx = EditorTestContext::new(cx).await;
18140
18141 cx.set_state(
18142 &r#"ˇone
18143 two
18144
18145 three
18146 fourˇ
18147 five
18148
18149 siˇx"#
18150 .unindent(),
18151 );
18152
18153 cx.dispatch_action(HandleInput(String::new()));
18154 cx.assert_editor_state(
18155 &r#"ˇone
18156 two
18157
18158 three
18159 fourˇ
18160 five
18161
18162 siˇx"#
18163 .unindent(),
18164 );
18165
18166 cx.dispatch_action(HandleInput("AAAA".to_string()));
18167 cx.assert_editor_state(
18168 &r#"AAAAˇone
18169 two
18170
18171 three
18172 fourAAAAˇ
18173 five
18174
18175 siAAAAˇx"#
18176 .unindent(),
18177 );
18178}
18179
18180#[gpui::test]
18181async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
18182 init_test(cx, |_| {});
18183
18184 let mut cx = EditorTestContext::new(cx).await;
18185 cx.set_state(
18186 r#"let foo = 1;
18187let foo = 2;
18188let foo = 3;
18189let fooˇ = 4;
18190let foo = 5;
18191let foo = 6;
18192let foo = 7;
18193let foo = 8;
18194let foo = 9;
18195let foo = 10;
18196let foo = 11;
18197let foo = 12;
18198let foo = 13;
18199let foo = 14;
18200let foo = 15;"#,
18201 );
18202
18203 cx.update_editor(|e, window, cx| {
18204 assert_eq!(
18205 e.next_scroll_position,
18206 NextScrollCursorCenterTopBottom::Center,
18207 "Default next scroll direction is center",
18208 );
18209
18210 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18211 assert_eq!(
18212 e.next_scroll_position,
18213 NextScrollCursorCenterTopBottom::Top,
18214 "After center, next scroll direction should be top",
18215 );
18216
18217 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18218 assert_eq!(
18219 e.next_scroll_position,
18220 NextScrollCursorCenterTopBottom::Bottom,
18221 "After top, next scroll direction should be bottom",
18222 );
18223
18224 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18225 assert_eq!(
18226 e.next_scroll_position,
18227 NextScrollCursorCenterTopBottom::Center,
18228 "After bottom, scrolling should start over",
18229 );
18230
18231 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18232 assert_eq!(
18233 e.next_scroll_position,
18234 NextScrollCursorCenterTopBottom::Top,
18235 "Scrolling continues if retriggered fast enough"
18236 );
18237 });
18238
18239 cx.executor()
18240 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
18241 cx.executor().run_until_parked();
18242 cx.update_editor(|e, _, _| {
18243 assert_eq!(
18244 e.next_scroll_position,
18245 NextScrollCursorCenterTopBottom::Center,
18246 "If scrolling is not triggered fast enough, it should reset"
18247 );
18248 });
18249}
18250
18251#[gpui::test]
18252async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
18253 init_test(cx, |_| {});
18254 let mut cx = EditorLspTestContext::new_rust(
18255 lsp::ServerCapabilities {
18256 definition_provider: Some(lsp::OneOf::Left(true)),
18257 references_provider: Some(lsp::OneOf::Left(true)),
18258 ..lsp::ServerCapabilities::default()
18259 },
18260 cx,
18261 )
18262 .await;
18263
18264 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
18265 let go_to_definition = cx
18266 .lsp
18267 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
18268 move |params, _| async move {
18269 if empty_go_to_definition {
18270 Ok(None)
18271 } else {
18272 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
18273 uri: params.text_document_position_params.text_document.uri,
18274 range: lsp::Range::new(
18275 lsp::Position::new(4, 3),
18276 lsp::Position::new(4, 6),
18277 ),
18278 })))
18279 }
18280 },
18281 );
18282 let references = cx
18283 .lsp
18284 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
18285 Ok(Some(vec![lsp::Location {
18286 uri: params.text_document_position.text_document.uri,
18287 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
18288 }]))
18289 });
18290 (go_to_definition, references)
18291 };
18292
18293 cx.set_state(
18294 &r#"fn one() {
18295 let mut a = ˇtwo();
18296 }
18297
18298 fn two() {}"#
18299 .unindent(),
18300 );
18301 set_up_lsp_handlers(false, &mut cx);
18302 let navigated = cx
18303 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
18304 .await
18305 .expect("Failed to navigate to definition");
18306 assert_eq!(
18307 navigated,
18308 Navigated::Yes,
18309 "Should have navigated to definition from the GetDefinition response"
18310 );
18311 cx.assert_editor_state(
18312 &r#"fn one() {
18313 let mut a = two();
18314 }
18315
18316 fn «twoˇ»() {}"#
18317 .unindent(),
18318 );
18319
18320 let editors = cx.update_workspace(|workspace, _, cx| {
18321 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
18322 });
18323 cx.update_editor(|_, _, test_editor_cx| {
18324 assert_eq!(
18325 editors.len(),
18326 1,
18327 "Initially, only one, test, editor should be open in the workspace"
18328 );
18329 assert_eq!(
18330 test_editor_cx.entity(),
18331 editors.last().expect("Asserted len is 1").clone()
18332 );
18333 });
18334
18335 set_up_lsp_handlers(true, &mut cx);
18336 let navigated = cx
18337 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
18338 .await
18339 .expect("Failed to navigate to lookup references");
18340 assert_eq!(
18341 navigated,
18342 Navigated::Yes,
18343 "Should have navigated to references as a fallback after empty GoToDefinition response"
18344 );
18345 // We should not change the selections in the existing file,
18346 // if opening another milti buffer with the references
18347 cx.assert_editor_state(
18348 &r#"fn one() {
18349 let mut a = two();
18350 }
18351
18352 fn «twoˇ»() {}"#
18353 .unindent(),
18354 );
18355 let editors = cx.update_workspace(|workspace, _, cx| {
18356 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
18357 });
18358 cx.update_editor(|_, _, test_editor_cx| {
18359 assert_eq!(
18360 editors.len(),
18361 2,
18362 "After falling back to references search, we open a new editor with the results"
18363 );
18364 let references_fallback_text = editors
18365 .into_iter()
18366 .find(|new_editor| *new_editor != test_editor_cx.entity())
18367 .expect("Should have one non-test editor now")
18368 .read(test_editor_cx)
18369 .text(test_editor_cx);
18370 assert_eq!(
18371 references_fallback_text, "fn one() {\n let mut a = two();\n}",
18372 "Should use the range from the references response and not the GoToDefinition one"
18373 );
18374 });
18375}
18376
18377#[gpui::test]
18378async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
18379 init_test(cx, |_| {});
18380 cx.update(|cx| {
18381 let mut editor_settings = EditorSettings::get_global(cx).clone();
18382 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
18383 EditorSettings::override_global(editor_settings, cx);
18384 });
18385 let mut cx = EditorLspTestContext::new_rust(
18386 lsp::ServerCapabilities {
18387 definition_provider: Some(lsp::OneOf::Left(true)),
18388 references_provider: Some(lsp::OneOf::Left(true)),
18389 ..lsp::ServerCapabilities::default()
18390 },
18391 cx,
18392 )
18393 .await;
18394 let original_state = r#"fn one() {
18395 let mut a = ˇtwo();
18396 }
18397
18398 fn two() {}"#
18399 .unindent();
18400 cx.set_state(&original_state);
18401
18402 let mut go_to_definition = cx
18403 .lsp
18404 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
18405 move |_, _| async move { Ok(None) },
18406 );
18407 let _references = cx
18408 .lsp
18409 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
18410 panic!("Should not call for references with no go to definition fallback")
18411 });
18412
18413 let navigated = cx
18414 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
18415 .await
18416 .expect("Failed to navigate to lookup references");
18417 go_to_definition
18418 .next()
18419 .await
18420 .expect("Should have called the go_to_definition handler");
18421
18422 assert_eq!(
18423 navigated,
18424 Navigated::No,
18425 "Should have navigated to references as a fallback after empty GoToDefinition response"
18426 );
18427 cx.assert_editor_state(&original_state);
18428 let editors = cx.update_workspace(|workspace, _, cx| {
18429 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
18430 });
18431 cx.update_editor(|_, _, _| {
18432 assert_eq!(
18433 editors.len(),
18434 1,
18435 "After unsuccessful fallback, no other editor should have been opened"
18436 );
18437 });
18438}
18439
18440#[gpui::test]
18441async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
18442 init_test(cx, |_| {});
18443
18444 let language = Arc::new(Language::new(
18445 LanguageConfig::default(),
18446 Some(tree_sitter_rust::LANGUAGE.into()),
18447 ));
18448
18449 let text = r#"
18450 #[cfg(test)]
18451 mod tests() {
18452 #[test]
18453 fn runnable_1() {
18454 let a = 1;
18455 }
18456
18457 #[test]
18458 fn runnable_2() {
18459 let a = 1;
18460 let b = 2;
18461 }
18462 }
18463 "#
18464 .unindent();
18465
18466 let fs = FakeFs::new(cx.executor());
18467 fs.insert_file("/file.rs", Default::default()).await;
18468
18469 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18470 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18471 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18472 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
18473 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
18474
18475 let editor = cx.new_window_entity(|window, cx| {
18476 Editor::new(
18477 EditorMode::full(),
18478 multi_buffer,
18479 Some(project.clone()),
18480 window,
18481 cx,
18482 )
18483 });
18484
18485 editor.update_in(cx, |editor, window, cx| {
18486 let snapshot = editor.buffer().read(cx).snapshot(cx);
18487 editor.tasks.insert(
18488 (buffer.read(cx).remote_id(), 3),
18489 RunnableTasks {
18490 templates: vec![],
18491 offset: snapshot.anchor_before(43),
18492 column: 0,
18493 extra_variables: HashMap::default(),
18494 context_range: BufferOffset(43)..BufferOffset(85),
18495 },
18496 );
18497 editor.tasks.insert(
18498 (buffer.read(cx).remote_id(), 8),
18499 RunnableTasks {
18500 templates: vec![],
18501 offset: snapshot.anchor_before(86),
18502 column: 0,
18503 extra_variables: HashMap::default(),
18504 context_range: BufferOffset(86)..BufferOffset(191),
18505 },
18506 );
18507
18508 // Test finding task when cursor is inside function body
18509 editor.change_selections(None, window, cx, |s| {
18510 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
18511 });
18512 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
18513 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
18514
18515 // Test finding task when cursor is on function name
18516 editor.change_selections(None, window, cx, |s| {
18517 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
18518 });
18519 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
18520 assert_eq!(row, 8, "Should find task when cursor is on function name");
18521 });
18522}
18523
18524#[gpui::test]
18525async fn test_folding_buffers(cx: &mut TestAppContext) {
18526 init_test(cx, |_| {});
18527
18528 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
18529 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
18530 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
18531
18532 let fs = FakeFs::new(cx.executor());
18533 fs.insert_tree(
18534 path!("/a"),
18535 json!({
18536 "first.rs": sample_text_1,
18537 "second.rs": sample_text_2,
18538 "third.rs": sample_text_3,
18539 }),
18540 )
18541 .await;
18542 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18543 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18544 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18545 let worktree = project.update(cx, |project, cx| {
18546 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18547 assert_eq!(worktrees.len(), 1);
18548 worktrees.pop().unwrap()
18549 });
18550 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18551
18552 let buffer_1 = project
18553 .update(cx, |project, cx| {
18554 project.open_buffer((worktree_id, "first.rs"), cx)
18555 })
18556 .await
18557 .unwrap();
18558 let buffer_2 = project
18559 .update(cx, |project, cx| {
18560 project.open_buffer((worktree_id, "second.rs"), cx)
18561 })
18562 .await
18563 .unwrap();
18564 let buffer_3 = project
18565 .update(cx, |project, cx| {
18566 project.open_buffer((worktree_id, "third.rs"), cx)
18567 })
18568 .await
18569 .unwrap();
18570
18571 let multi_buffer = cx.new(|cx| {
18572 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18573 multi_buffer.push_excerpts(
18574 buffer_1.clone(),
18575 [
18576 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18577 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18578 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18579 ],
18580 cx,
18581 );
18582 multi_buffer.push_excerpts(
18583 buffer_2.clone(),
18584 [
18585 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18586 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18587 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18588 ],
18589 cx,
18590 );
18591 multi_buffer.push_excerpts(
18592 buffer_3.clone(),
18593 [
18594 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18595 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18596 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18597 ],
18598 cx,
18599 );
18600 multi_buffer
18601 });
18602 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18603 Editor::new(
18604 EditorMode::full(),
18605 multi_buffer.clone(),
18606 Some(project.clone()),
18607 window,
18608 cx,
18609 )
18610 });
18611
18612 assert_eq!(
18613 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18614 "\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",
18615 );
18616
18617 multi_buffer_editor.update(cx, |editor, cx| {
18618 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
18619 });
18620 assert_eq!(
18621 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18622 "\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",
18623 "After folding the first buffer, its text should not be displayed"
18624 );
18625
18626 multi_buffer_editor.update(cx, |editor, cx| {
18627 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
18628 });
18629 assert_eq!(
18630 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18631 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
18632 "After folding the second buffer, its text should not be displayed"
18633 );
18634
18635 multi_buffer_editor.update(cx, |editor, cx| {
18636 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
18637 });
18638 assert_eq!(
18639 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18640 "\n\n\n\n\n",
18641 "After folding the third buffer, its text should not be displayed"
18642 );
18643
18644 // Emulate selection inside the fold logic, that should work
18645 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18646 editor
18647 .snapshot(window, cx)
18648 .next_line_boundary(Point::new(0, 4));
18649 });
18650
18651 multi_buffer_editor.update(cx, |editor, cx| {
18652 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
18653 });
18654 assert_eq!(
18655 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18656 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
18657 "After unfolding the second buffer, its text should be displayed"
18658 );
18659
18660 // Typing inside of buffer 1 causes that buffer to be unfolded.
18661 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18662 assert_eq!(
18663 multi_buffer
18664 .read(cx)
18665 .snapshot(cx)
18666 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
18667 .collect::<String>(),
18668 "bbbb"
18669 );
18670 editor.change_selections(None, window, cx, |selections| {
18671 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
18672 });
18673 editor.handle_input("B", window, cx);
18674 });
18675
18676 assert_eq!(
18677 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18678 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
18679 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
18680 );
18681
18682 multi_buffer_editor.update(cx, |editor, cx| {
18683 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
18684 });
18685 assert_eq!(
18686 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18687 "\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",
18688 "After unfolding the all buffers, all original text should be displayed"
18689 );
18690}
18691
18692#[gpui::test]
18693async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
18694 init_test(cx, |_| {});
18695
18696 let sample_text_1 = "1111\n2222\n3333".to_string();
18697 let sample_text_2 = "4444\n5555\n6666".to_string();
18698 let sample_text_3 = "7777\n8888\n9999".to_string();
18699
18700 let fs = FakeFs::new(cx.executor());
18701 fs.insert_tree(
18702 path!("/a"),
18703 json!({
18704 "first.rs": sample_text_1,
18705 "second.rs": sample_text_2,
18706 "third.rs": sample_text_3,
18707 }),
18708 )
18709 .await;
18710 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18711 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18712 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18713 let worktree = project.update(cx, |project, cx| {
18714 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18715 assert_eq!(worktrees.len(), 1);
18716 worktrees.pop().unwrap()
18717 });
18718 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18719
18720 let buffer_1 = project
18721 .update(cx, |project, cx| {
18722 project.open_buffer((worktree_id, "first.rs"), cx)
18723 })
18724 .await
18725 .unwrap();
18726 let buffer_2 = project
18727 .update(cx, |project, cx| {
18728 project.open_buffer((worktree_id, "second.rs"), cx)
18729 })
18730 .await
18731 .unwrap();
18732 let buffer_3 = project
18733 .update(cx, |project, cx| {
18734 project.open_buffer((worktree_id, "third.rs"), cx)
18735 })
18736 .await
18737 .unwrap();
18738
18739 let multi_buffer = cx.new(|cx| {
18740 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18741 multi_buffer.push_excerpts(
18742 buffer_1.clone(),
18743 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18744 cx,
18745 );
18746 multi_buffer.push_excerpts(
18747 buffer_2.clone(),
18748 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18749 cx,
18750 );
18751 multi_buffer.push_excerpts(
18752 buffer_3.clone(),
18753 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18754 cx,
18755 );
18756 multi_buffer
18757 });
18758
18759 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18760 Editor::new(
18761 EditorMode::full(),
18762 multi_buffer,
18763 Some(project.clone()),
18764 window,
18765 cx,
18766 )
18767 });
18768
18769 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
18770 assert_eq!(
18771 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18772 full_text,
18773 );
18774
18775 multi_buffer_editor.update(cx, |editor, cx| {
18776 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
18777 });
18778 assert_eq!(
18779 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18780 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
18781 "After folding the first buffer, its text should not be displayed"
18782 );
18783
18784 multi_buffer_editor.update(cx, |editor, cx| {
18785 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
18786 });
18787
18788 assert_eq!(
18789 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18790 "\n\n\n\n\n\n7777\n8888\n9999",
18791 "After folding the second buffer, its text should not be displayed"
18792 );
18793
18794 multi_buffer_editor.update(cx, |editor, cx| {
18795 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
18796 });
18797 assert_eq!(
18798 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18799 "\n\n\n\n\n",
18800 "After folding the third buffer, its text should not be displayed"
18801 );
18802
18803 multi_buffer_editor.update(cx, |editor, cx| {
18804 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
18805 });
18806 assert_eq!(
18807 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18808 "\n\n\n\n4444\n5555\n6666\n\n",
18809 "After unfolding the second buffer, its text should be displayed"
18810 );
18811
18812 multi_buffer_editor.update(cx, |editor, cx| {
18813 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
18814 });
18815 assert_eq!(
18816 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18817 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
18818 "After unfolding the first buffer, its text should be displayed"
18819 );
18820
18821 multi_buffer_editor.update(cx, |editor, cx| {
18822 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
18823 });
18824 assert_eq!(
18825 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18826 full_text,
18827 "After unfolding all buffers, all original text should be displayed"
18828 );
18829}
18830
18831#[gpui::test]
18832async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
18833 init_test(cx, |_| {});
18834
18835 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
18836
18837 let fs = FakeFs::new(cx.executor());
18838 fs.insert_tree(
18839 path!("/a"),
18840 json!({
18841 "main.rs": sample_text,
18842 }),
18843 )
18844 .await;
18845 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18846 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18847 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18848 let worktree = project.update(cx, |project, cx| {
18849 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18850 assert_eq!(worktrees.len(), 1);
18851 worktrees.pop().unwrap()
18852 });
18853 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18854
18855 let buffer_1 = project
18856 .update(cx, |project, cx| {
18857 project.open_buffer((worktree_id, "main.rs"), cx)
18858 })
18859 .await
18860 .unwrap();
18861
18862 let multi_buffer = cx.new(|cx| {
18863 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18864 multi_buffer.push_excerpts(
18865 buffer_1.clone(),
18866 [ExcerptRange::new(
18867 Point::new(0, 0)
18868 ..Point::new(
18869 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
18870 0,
18871 ),
18872 )],
18873 cx,
18874 );
18875 multi_buffer
18876 });
18877 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18878 Editor::new(
18879 EditorMode::full(),
18880 multi_buffer,
18881 Some(project.clone()),
18882 window,
18883 cx,
18884 )
18885 });
18886
18887 let selection_range = Point::new(1, 0)..Point::new(2, 0);
18888 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18889 enum TestHighlight {}
18890 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
18891 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
18892 editor.highlight_text::<TestHighlight>(
18893 vec![highlight_range.clone()],
18894 HighlightStyle::color(Hsla::green()),
18895 cx,
18896 );
18897 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
18898 });
18899
18900 let full_text = format!("\n\n{sample_text}");
18901 assert_eq!(
18902 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18903 full_text,
18904 );
18905}
18906
18907#[gpui::test]
18908async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
18909 init_test(cx, |_| {});
18910 cx.update(|cx| {
18911 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
18912 "keymaps/default-linux.json",
18913 cx,
18914 )
18915 .unwrap();
18916 cx.bind_keys(default_key_bindings);
18917 });
18918
18919 let (editor, cx) = cx.add_window_view(|window, cx| {
18920 let multi_buffer = MultiBuffer::build_multi(
18921 [
18922 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
18923 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
18924 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
18925 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
18926 ],
18927 cx,
18928 );
18929 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
18930
18931 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
18932 // fold all but the second buffer, so that we test navigating between two
18933 // adjacent folded buffers, as well as folded buffers at the start and
18934 // end the multibuffer
18935 editor.fold_buffer(buffer_ids[0], cx);
18936 editor.fold_buffer(buffer_ids[2], cx);
18937 editor.fold_buffer(buffer_ids[3], cx);
18938
18939 editor
18940 });
18941 cx.simulate_resize(size(px(1000.), px(1000.)));
18942
18943 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
18944 cx.assert_excerpts_with_selections(indoc! {"
18945 [EXCERPT]
18946 ˇ[FOLDED]
18947 [EXCERPT]
18948 a1
18949 b1
18950 [EXCERPT]
18951 [FOLDED]
18952 [EXCERPT]
18953 [FOLDED]
18954 "
18955 });
18956 cx.simulate_keystroke("down");
18957 cx.assert_excerpts_with_selections(indoc! {"
18958 [EXCERPT]
18959 [FOLDED]
18960 [EXCERPT]
18961 ˇa1
18962 b1
18963 [EXCERPT]
18964 [FOLDED]
18965 [EXCERPT]
18966 [FOLDED]
18967 "
18968 });
18969 cx.simulate_keystroke("down");
18970 cx.assert_excerpts_with_selections(indoc! {"
18971 [EXCERPT]
18972 [FOLDED]
18973 [EXCERPT]
18974 a1
18975 ˇb1
18976 [EXCERPT]
18977 [FOLDED]
18978 [EXCERPT]
18979 [FOLDED]
18980 "
18981 });
18982 cx.simulate_keystroke("down");
18983 cx.assert_excerpts_with_selections(indoc! {"
18984 [EXCERPT]
18985 [FOLDED]
18986 [EXCERPT]
18987 a1
18988 b1
18989 ˇ[EXCERPT]
18990 [FOLDED]
18991 [EXCERPT]
18992 [FOLDED]
18993 "
18994 });
18995 cx.simulate_keystroke("down");
18996 cx.assert_excerpts_with_selections(indoc! {"
18997 [EXCERPT]
18998 [FOLDED]
18999 [EXCERPT]
19000 a1
19001 b1
19002 [EXCERPT]
19003 ˇ[FOLDED]
19004 [EXCERPT]
19005 [FOLDED]
19006 "
19007 });
19008 for _ in 0..5 {
19009 cx.simulate_keystroke("down");
19010 cx.assert_excerpts_with_selections(indoc! {"
19011 [EXCERPT]
19012 [FOLDED]
19013 [EXCERPT]
19014 a1
19015 b1
19016 [EXCERPT]
19017 [FOLDED]
19018 [EXCERPT]
19019 ˇ[FOLDED]
19020 "
19021 });
19022 }
19023
19024 cx.simulate_keystroke("up");
19025 cx.assert_excerpts_with_selections(indoc! {"
19026 [EXCERPT]
19027 [FOLDED]
19028 [EXCERPT]
19029 a1
19030 b1
19031 [EXCERPT]
19032 ˇ[FOLDED]
19033 [EXCERPT]
19034 [FOLDED]
19035 "
19036 });
19037 cx.simulate_keystroke("up");
19038 cx.assert_excerpts_with_selections(indoc! {"
19039 [EXCERPT]
19040 [FOLDED]
19041 [EXCERPT]
19042 a1
19043 b1
19044 ˇ[EXCERPT]
19045 [FOLDED]
19046 [EXCERPT]
19047 [FOLDED]
19048 "
19049 });
19050 cx.simulate_keystroke("up");
19051 cx.assert_excerpts_with_selections(indoc! {"
19052 [EXCERPT]
19053 [FOLDED]
19054 [EXCERPT]
19055 a1
19056 ˇb1
19057 [EXCERPT]
19058 [FOLDED]
19059 [EXCERPT]
19060 [FOLDED]
19061 "
19062 });
19063 cx.simulate_keystroke("up");
19064 cx.assert_excerpts_with_selections(indoc! {"
19065 [EXCERPT]
19066 [FOLDED]
19067 [EXCERPT]
19068 ˇa1
19069 b1
19070 [EXCERPT]
19071 [FOLDED]
19072 [EXCERPT]
19073 [FOLDED]
19074 "
19075 });
19076 for _ in 0..5 {
19077 cx.simulate_keystroke("up");
19078 cx.assert_excerpts_with_selections(indoc! {"
19079 [EXCERPT]
19080 ˇ[FOLDED]
19081 [EXCERPT]
19082 a1
19083 b1
19084 [EXCERPT]
19085 [FOLDED]
19086 [EXCERPT]
19087 [FOLDED]
19088 "
19089 });
19090 }
19091}
19092
19093#[gpui::test]
19094async fn test_inline_completion_text(cx: &mut TestAppContext) {
19095 init_test(cx, |_| {});
19096
19097 // Simple insertion
19098 assert_highlighted_edits(
19099 "Hello, world!",
19100 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
19101 true,
19102 cx,
19103 |highlighted_edits, cx| {
19104 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
19105 assert_eq!(highlighted_edits.highlights.len(), 1);
19106 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
19107 assert_eq!(
19108 highlighted_edits.highlights[0].1.background_color,
19109 Some(cx.theme().status().created_background)
19110 );
19111 },
19112 )
19113 .await;
19114
19115 // Replacement
19116 assert_highlighted_edits(
19117 "This is a test.",
19118 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
19119 false,
19120 cx,
19121 |highlighted_edits, cx| {
19122 assert_eq!(highlighted_edits.text, "That is a test.");
19123 assert_eq!(highlighted_edits.highlights.len(), 1);
19124 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
19125 assert_eq!(
19126 highlighted_edits.highlights[0].1.background_color,
19127 Some(cx.theme().status().created_background)
19128 );
19129 },
19130 )
19131 .await;
19132
19133 // Multiple edits
19134 assert_highlighted_edits(
19135 "Hello, world!",
19136 vec![
19137 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
19138 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
19139 ],
19140 false,
19141 cx,
19142 |highlighted_edits, cx| {
19143 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
19144 assert_eq!(highlighted_edits.highlights.len(), 2);
19145 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
19146 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
19147 assert_eq!(
19148 highlighted_edits.highlights[0].1.background_color,
19149 Some(cx.theme().status().created_background)
19150 );
19151 assert_eq!(
19152 highlighted_edits.highlights[1].1.background_color,
19153 Some(cx.theme().status().created_background)
19154 );
19155 },
19156 )
19157 .await;
19158
19159 // Multiple lines with edits
19160 assert_highlighted_edits(
19161 "First line\nSecond line\nThird line\nFourth line",
19162 vec![
19163 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
19164 (
19165 Point::new(2, 0)..Point::new(2, 10),
19166 "New third line".to_string(),
19167 ),
19168 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
19169 ],
19170 false,
19171 cx,
19172 |highlighted_edits, cx| {
19173 assert_eq!(
19174 highlighted_edits.text,
19175 "Second modified\nNew third line\nFourth updated line"
19176 );
19177 assert_eq!(highlighted_edits.highlights.len(), 3);
19178 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
19179 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
19180 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
19181 for highlight in &highlighted_edits.highlights {
19182 assert_eq!(
19183 highlight.1.background_color,
19184 Some(cx.theme().status().created_background)
19185 );
19186 }
19187 },
19188 )
19189 .await;
19190}
19191
19192#[gpui::test]
19193async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
19194 init_test(cx, |_| {});
19195
19196 // Deletion
19197 assert_highlighted_edits(
19198 "Hello, world!",
19199 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
19200 true,
19201 cx,
19202 |highlighted_edits, cx| {
19203 assert_eq!(highlighted_edits.text, "Hello, world!");
19204 assert_eq!(highlighted_edits.highlights.len(), 1);
19205 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
19206 assert_eq!(
19207 highlighted_edits.highlights[0].1.background_color,
19208 Some(cx.theme().status().deleted_background)
19209 );
19210 },
19211 )
19212 .await;
19213
19214 // Insertion
19215 assert_highlighted_edits(
19216 "Hello, world!",
19217 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
19218 true,
19219 cx,
19220 |highlighted_edits, cx| {
19221 assert_eq!(highlighted_edits.highlights.len(), 1);
19222 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
19223 assert_eq!(
19224 highlighted_edits.highlights[0].1.background_color,
19225 Some(cx.theme().status().created_background)
19226 );
19227 },
19228 )
19229 .await;
19230}
19231
19232async fn assert_highlighted_edits(
19233 text: &str,
19234 edits: Vec<(Range<Point>, String)>,
19235 include_deletions: bool,
19236 cx: &mut TestAppContext,
19237 assertion_fn: impl Fn(HighlightedText, &App),
19238) {
19239 let window = cx.add_window(|window, cx| {
19240 let buffer = MultiBuffer::build_simple(text, cx);
19241 Editor::new(EditorMode::full(), buffer, None, window, cx)
19242 });
19243 let cx = &mut VisualTestContext::from_window(*window, cx);
19244
19245 let (buffer, snapshot) = window
19246 .update(cx, |editor, _window, cx| {
19247 (
19248 editor.buffer().clone(),
19249 editor.buffer().read(cx).snapshot(cx),
19250 )
19251 })
19252 .unwrap();
19253
19254 let edits = edits
19255 .into_iter()
19256 .map(|(range, edit)| {
19257 (
19258 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
19259 edit,
19260 )
19261 })
19262 .collect::<Vec<_>>();
19263
19264 let text_anchor_edits = edits
19265 .clone()
19266 .into_iter()
19267 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
19268 .collect::<Vec<_>>();
19269
19270 let edit_preview = window
19271 .update(cx, |_, _window, cx| {
19272 buffer
19273 .read(cx)
19274 .as_singleton()
19275 .unwrap()
19276 .read(cx)
19277 .preview_edits(text_anchor_edits.into(), cx)
19278 })
19279 .unwrap()
19280 .await;
19281
19282 cx.update(|_window, cx| {
19283 let highlighted_edits = inline_completion_edit_text(
19284 &snapshot.as_singleton().unwrap().2,
19285 &edits,
19286 &edit_preview,
19287 include_deletions,
19288 cx,
19289 );
19290 assertion_fn(highlighted_edits, cx)
19291 });
19292}
19293
19294#[track_caller]
19295fn assert_breakpoint(
19296 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
19297 path: &Arc<Path>,
19298 expected: Vec<(u32, Breakpoint)>,
19299) {
19300 if expected.len() == 0usize {
19301 assert!(!breakpoints.contains_key(path), "{}", path.display());
19302 } else {
19303 let mut breakpoint = breakpoints
19304 .get(path)
19305 .unwrap()
19306 .into_iter()
19307 .map(|breakpoint| {
19308 (
19309 breakpoint.row,
19310 Breakpoint {
19311 message: breakpoint.message.clone(),
19312 state: breakpoint.state,
19313 condition: breakpoint.condition.clone(),
19314 hit_condition: breakpoint.hit_condition.clone(),
19315 },
19316 )
19317 })
19318 .collect::<Vec<_>>();
19319
19320 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
19321
19322 assert_eq!(expected, breakpoint);
19323 }
19324}
19325
19326fn add_log_breakpoint_at_cursor(
19327 editor: &mut Editor,
19328 log_message: &str,
19329 window: &mut Window,
19330 cx: &mut Context<Editor>,
19331) {
19332 let (anchor, bp) = editor
19333 .breakpoints_at_cursors(window, cx)
19334 .first()
19335 .and_then(|(anchor, bp)| {
19336 if let Some(bp) = bp {
19337 Some((*anchor, bp.clone()))
19338 } else {
19339 None
19340 }
19341 })
19342 .unwrap_or_else(|| {
19343 let cursor_position: Point = editor.selections.newest(cx).head();
19344
19345 let breakpoint_position = editor
19346 .snapshot(window, cx)
19347 .display_snapshot
19348 .buffer_snapshot
19349 .anchor_before(Point::new(cursor_position.row, 0));
19350
19351 (breakpoint_position, Breakpoint::new_log(&log_message))
19352 });
19353
19354 editor.edit_breakpoint_at_anchor(
19355 anchor,
19356 bp,
19357 BreakpointEditAction::EditLogMessage(log_message.into()),
19358 cx,
19359 );
19360}
19361
19362#[gpui::test]
19363async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
19364 init_test(cx, |_| {});
19365
19366 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19367 let fs = FakeFs::new(cx.executor());
19368 fs.insert_tree(
19369 path!("/a"),
19370 json!({
19371 "main.rs": sample_text,
19372 }),
19373 )
19374 .await;
19375 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19376 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19377 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19378
19379 let fs = FakeFs::new(cx.executor());
19380 fs.insert_tree(
19381 path!("/a"),
19382 json!({
19383 "main.rs": sample_text,
19384 }),
19385 )
19386 .await;
19387 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19388 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19389 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19390 let worktree_id = workspace
19391 .update(cx, |workspace, _window, cx| {
19392 workspace.project().update(cx, |project, cx| {
19393 project.worktrees(cx).next().unwrap().read(cx).id()
19394 })
19395 })
19396 .unwrap();
19397
19398 let buffer = project
19399 .update(cx, |project, cx| {
19400 project.open_buffer((worktree_id, "main.rs"), cx)
19401 })
19402 .await
19403 .unwrap();
19404
19405 let (editor, cx) = cx.add_window_view(|window, cx| {
19406 Editor::new(
19407 EditorMode::full(),
19408 MultiBuffer::build_from_buffer(buffer, cx),
19409 Some(project.clone()),
19410 window,
19411 cx,
19412 )
19413 });
19414
19415 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19416 let abs_path = project.read_with(cx, |project, cx| {
19417 project
19418 .absolute_path(&project_path, cx)
19419 .map(|path_buf| Arc::from(path_buf.to_owned()))
19420 .unwrap()
19421 });
19422
19423 // assert we can add breakpoint on the first line
19424 editor.update_in(cx, |editor, window, cx| {
19425 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19426 editor.move_to_end(&MoveToEnd, window, cx);
19427 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19428 });
19429
19430 let breakpoints = editor.update(cx, |editor, cx| {
19431 editor
19432 .breakpoint_store()
19433 .as_ref()
19434 .unwrap()
19435 .read(cx)
19436 .all_source_breakpoints(cx)
19437 .clone()
19438 });
19439
19440 assert_eq!(1, breakpoints.len());
19441 assert_breakpoint(
19442 &breakpoints,
19443 &abs_path,
19444 vec![
19445 (0, Breakpoint::new_standard()),
19446 (3, Breakpoint::new_standard()),
19447 ],
19448 );
19449
19450 editor.update_in(cx, |editor, window, cx| {
19451 editor.move_to_beginning(&MoveToBeginning, window, cx);
19452 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19453 });
19454
19455 let breakpoints = editor.update(cx, |editor, cx| {
19456 editor
19457 .breakpoint_store()
19458 .as_ref()
19459 .unwrap()
19460 .read(cx)
19461 .all_source_breakpoints(cx)
19462 .clone()
19463 });
19464
19465 assert_eq!(1, breakpoints.len());
19466 assert_breakpoint(
19467 &breakpoints,
19468 &abs_path,
19469 vec![(3, Breakpoint::new_standard())],
19470 );
19471
19472 editor.update_in(cx, |editor, window, cx| {
19473 editor.move_to_end(&MoveToEnd, window, cx);
19474 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19475 });
19476
19477 let breakpoints = editor.update(cx, |editor, cx| {
19478 editor
19479 .breakpoint_store()
19480 .as_ref()
19481 .unwrap()
19482 .read(cx)
19483 .all_source_breakpoints(cx)
19484 .clone()
19485 });
19486
19487 assert_eq!(0, breakpoints.len());
19488 assert_breakpoint(&breakpoints, &abs_path, vec![]);
19489}
19490
19491#[gpui::test]
19492async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
19493 init_test(cx, |_| {});
19494
19495 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19496
19497 let fs = FakeFs::new(cx.executor());
19498 fs.insert_tree(
19499 path!("/a"),
19500 json!({
19501 "main.rs": sample_text,
19502 }),
19503 )
19504 .await;
19505 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19506 let (workspace, cx) =
19507 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19508
19509 let worktree_id = workspace.update(cx, |workspace, cx| {
19510 workspace.project().update(cx, |project, cx| {
19511 project.worktrees(cx).next().unwrap().read(cx).id()
19512 })
19513 });
19514
19515 let buffer = project
19516 .update(cx, |project, cx| {
19517 project.open_buffer((worktree_id, "main.rs"), cx)
19518 })
19519 .await
19520 .unwrap();
19521
19522 let (editor, cx) = cx.add_window_view(|window, cx| {
19523 Editor::new(
19524 EditorMode::full(),
19525 MultiBuffer::build_from_buffer(buffer, cx),
19526 Some(project.clone()),
19527 window,
19528 cx,
19529 )
19530 });
19531
19532 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19533 let abs_path = project.read_with(cx, |project, cx| {
19534 project
19535 .absolute_path(&project_path, cx)
19536 .map(|path_buf| Arc::from(path_buf.to_owned()))
19537 .unwrap()
19538 });
19539
19540 editor.update_in(cx, |editor, window, cx| {
19541 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
19542 });
19543
19544 let breakpoints = editor.update(cx, |editor, cx| {
19545 editor
19546 .breakpoint_store()
19547 .as_ref()
19548 .unwrap()
19549 .read(cx)
19550 .all_source_breakpoints(cx)
19551 .clone()
19552 });
19553
19554 assert_breakpoint(
19555 &breakpoints,
19556 &abs_path,
19557 vec![(0, Breakpoint::new_log("hello world"))],
19558 );
19559
19560 // Removing a log message from a log breakpoint should remove it
19561 editor.update_in(cx, |editor, window, cx| {
19562 add_log_breakpoint_at_cursor(editor, "", window, cx);
19563 });
19564
19565 let breakpoints = editor.update(cx, |editor, cx| {
19566 editor
19567 .breakpoint_store()
19568 .as_ref()
19569 .unwrap()
19570 .read(cx)
19571 .all_source_breakpoints(cx)
19572 .clone()
19573 });
19574
19575 assert_breakpoint(&breakpoints, &abs_path, vec![]);
19576
19577 editor.update_in(cx, |editor, window, cx| {
19578 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19579 editor.move_to_end(&MoveToEnd, window, cx);
19580 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19581 // Not adding a log message to a standard breakpoint shouldn't remove it
19582 add_log_breakpoint_at_cursor(editor, "", window, cx);
19583 });
19584
19585 let breakpoints = editor.update(cx, |editor, cx| {
19586 editor
19587 .breakpoint_store()
19588 .as_ref()
19589 .unwrap()
19590 .read(cx)
19591 .all_source_breakpoints(cx)
19592 .clone()
19593 });
19594
19595 assert_breakpoint(
19596 &breakpoints,
19597 &abs_path,
19598 vec![
19599 (0, Breakpoint::new_standard()),
19600 (3, Breakpoint::new_standard()),
19601 ],
19602 );
19603
19604 editor.update_in(cx, |editor, window, cx| {
19605 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
19606 });
19607
19608 let breakpoints = editor.update(cx, |editor, cx| {
19609 editor
19610 .breakpoint_store()
19611 .as_ref()
19612 .unwrap()
19613 .read(cx)
19614 .all_source_breakpoints(cx)
19615 .clone()
19616 });
19617
19618 assert_breakpoint(
19619 &breakpoints,
19620 &abs_path,
19621 vec![
19622 (0, Breakpoint::new_standard()),
19623 (3, Breakpoint::new_log("hello world")),
19624 ],
19625 );
19626
19627 editor.update_in(cx, |editor, window, cx| {
19628 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
19629 });
19630
19631 let breakpoints = editor.update(cx, |editor, cx| {
19632 editor
19633 .breakpoint_store()
19634 .as_ref()
19635 .unwrap()
19636 .read(cx)
19637 .all_source_breakpoints(cx)
19638 .clone()
19639 });
19640
19641 assert_breakpoint(
19642 &breakpoints,
19643 &abs_path,
19644 vec![
19645 (0, Breakpoint::new_standard()),
19646 (3, Breakpoint::new_log("hello Earth!!")),
19647 ],
19648 );
19649}
19650
19651/// This also tests that Editor::breakpoint_at_cursor_head is working properly
19652/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
19653/// or when breakpoints were placed out of order. This tests for a regression too
19654#[gpui::test]
19655async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
19656 init_test(cx, |_| {});
19657
19658 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19659 let fs = FakeFs::new(cx.executor());
19660 fs.insert_tree(
19661 path!("/a"),
19662 json!({
19663 "main.rs": sample_text,
19664 }),
19665 )
19666 .await;
19667 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19668 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19669 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19670
19671 let fs = FakeFs::new(cx.executor());
19672 fs.insert_tree(
19673 path!("/a"),
19674 json!({
19675 "main.rs": sample_text,
19676 }),
19677 )
19678 .await;
19679 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19680 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19681 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19682 let worktree_id = workspace
19683 .update(cx, |workspace, _window, cx| {
19684 workspace.project().update(cx, |project, cx| {
19685 project.worktrees(cx).next().unwrap().read(cx).id()
19686 })
19687 })
19688 .unwrap();
19689
19690 let buffer = project
19691 .update(cx, |project, cx| {
19692 project.open_buffer((worktree_id, "main.rs"), cx)
19693 })
19694 .await
19695 .unwrap();
19696
19697 let (editor, cx) = cx.add_window_view(|window, cx| {
19698 Editor::new(
19699 EditorMode::full(),
19700 MultiBuffer::build_from_buffer(buffer, cx),
19701 Some(project.clone()),
19702 window,
19703 cx,
19704 )
19705 });
19706
19707 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19708 let abs_path = project.read_with(cx, |project, cx| {
19709 project
19710 .absolute_path(&project_path, cx)
19711 .map(|path_buf| Arc::from(path_buf.to_owned()))
19712 .unwrap()
19713 });
19714
19715 // assert we can add breakpoint on the first line
19716 editor.update_in(cx, |editor, window, cx| {
19717 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19718 editor.move_to_end(&MoveToEnd, window, cx);
19719 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19720 editor.move_up(&MoveUp, window, cx);
19721 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19722 });
19723
19724 let breakpoints = editor.update(cx, |editor, cx| {
19725 editor
19726 .breakpoint_store()
19727 .as_ref()
19728 .unwrap()
19729 .read(cx)
19730 .all_source_breakpoints(cx)
19731 .clone()
19732 });
19733
19734 assert_eq!(1, breakpoints.len());
19735 assert_breakpoint(
19736 &breakpoints,
19737 &abs_path,
19738 vec![
19739 (0, Breakpoint::new_standard()),
19740 (2, Breakpoint::new_standard()),
19741 (3, Breakpoint::new_standard()),
19742 ],
19743 );
19744
19745 editor.update_in(cx, |editor, window, cx| {
19746 editor.move_to_beginning(&MoveToBeginning, window, cx);
19747 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19748 editor.move_to_end(&MoveToEnd, window, cx);
19749 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19750 // Disabling a breakpoint that doesn't exist should do nothing
19751 editor.move_up(&MoveUp, window, cx);
19752 editor.move_up(&MoveUp, window, cx);
19753 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19754 });
19755
19756 let breakpoints = editor.update(cx, |editor, cx| {
19757 editor
19758 .breakpoint_store()
19759 .as_ref()
19760 .unwrap()
19761 .read(cx)
19762 .all_source_breakpoints(cx)
19763 .clone()
19764 });
19765
19766 let disable_breakpoint = {
19767 let mut bp = Breakpoint::new_standard();
19768 bp.state = BreakpointState::Disabled;
19769 bp
19770 };
19771
19772 assert_eq!(1, breakpoints.len());
19773 assert_breakpoint(
19774 &breakpoints,
19775 &abs_path,
19776 vec![
19777 (0, disable_breakpoint.clone()),
19778 (2, Breakpoint::new_standard()),
19779 (3, disable_breakpoint.clone()),
19780 ],
19781 );
19782
19783 editor.update_in(cx, |editor, window, cx| {
19784 editor.move_to_beginning(&MoveToBeginning, window, cx);
19785 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
19786 editor.move_to_end(&MoveToEnd, window, cx);
19787 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
19788 editor.move_up(&MoveUp, window, cx);
19789 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19790 });
19791
19792 let breakpoints = editor.update(cx, |editor, cx| {
19793 editor
19794 .breakpoint_store()
19795 .as_ref()
19796 .unwrap()
19797 .read(cx)
19798 .all_source_breakpoints(cx)
19799 .clone()
19800 });
19801
19802 assert_eq!(1, breakpoints.len());
19803 assert_breakpoint(
19804 &breakpoints,
19805 &abs_path,
19806 vec![
19807 (0, Breakpoint::new_standard()),
19808 (2, disable_breakpoint),
19809 (3, Breakpoint::new_standard()),
19810 ],
19811 );
19812}
19813
19814#[gpui::test]
19815async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
19816 init_test(cx, |_| {});
19817 let capabilities = lsp::ServerCapabilities {
19818 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
19819 prepare_provider: Some(true),
19820 work_done_progress_options: Default::default(),
19821 })),
19822 ..Default::default()
19823 };
19824 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
19825
19826 cx.set_state(indoc! {"
19827 struct Fˇoo {}
19828 "});
19829
19830 cx.update_editor(|editor, _, cx| {
19831 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
19832 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
19833 editor.highlight_background::<DocumentHighlightRead>(
19834 &[highlight_range],
19835 |c| c.editor_document_highlight_read_background,
19836 cx,
19837 );
19838 });
19839
19840 let mut prepare_rename_handler = cx
19841 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
19842 move |_, _, _| async move {
19843 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
19844 start: lsp::Position {
19845 line: 0,
19846 character: 7,
19847 },
19848 end: lsp::Position {
19849 line: 0,
19850 character: 10,
19851 },
19852 })))
19853 },
19854 );
19855 let prepare_rename_task = cx
19856 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
19857 .expect("Prepare rename was not started");
19858 prepare_rename_handler.next().await.unwrap();
19859 prepare_rename_task.await.expect("Prepare rename failed");
19860
19861 let mut rename_handler =
19862 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
19863 let edit = lsp::TextEdit {
19864 range: lsp::Range {
19865 start: lsp::Position {
19866 line: 0,
19867 character: 7,
19868 },
19869 end: lsp::Position {
19870 line: 0,
19871 character: 10,
19872 },
19873 },
19874 new_text: "FooRenamed".to_string(),
19875 };
19876 Ok(Some(lsp::WorkspaceEdit::new(
19877 // Specify the same edit twice
19878 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
19879 )))
19880 });
19881 let rename_task = cx
19882 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
19883 .expect("Confirm rename was not started");
19884 rename_handler.next().await.unwrap();
19885 rename_task.await.expect("Confirm rename failed");
19886 cx.run_until_parked();
19887
19888 // Despite two edits, only one is actually applied as those are identical
19889 cx.assert_editor_state(indoc! {"
19890 struct FooRenamedˇ {}
19891 "});
19892}
19893
19894#[gpui::test]
19895async fn test_rename_without_prepare(cx: &mut TestAppContext) {
19896 init_test(cx, |_| {});
19897 // These capabilities indicate that the server does not support prepare rename.
19898 let capabilities = lsp::ServerCapabilities {
19899 rename_provider: Some(lsp::OneOf::Left(true)),
19900 ..Default::default()
19901 };
19902 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
19903
19904 cx.set_state(indoc! {"
19905 struct Fˇoo {}
19906 "});
19907
19908 cx.update_editor(|editor, _window, cx| {
19909 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
19910 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
19911 editor.highlight_background::<DocumentHighlightRead>(
19912 &[highlight_range],
19913 |c| c.editor_document_highlight_read_background,
19914 cx,
19915 );
19916 });
19917
19918 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
19919 .expect("Prepare rename was not started")
19920 .await
19921 .expect("Prepare rename failed");
19922
19923 let mut rename_handler =
19924 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
19925 let edit = lsp::TextEdit {
19926 range: lsp::Range {
19927 start: lsp::Position {
19928 line: 0,
19929 character: 7,
19930 },
19931 end: lsp::Position {
19932 line: 0,
19933 character: 10,
19934 },
19935 },
19936 new_text: "FooRenamed".to_string(),
19937 };
19938 Ok(Some(lsp::WorkspaceEdit::new(
19939 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
19940 )))
19941 });
19942 let rename_task = cx
19943 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
19944 .expect("Confirm rename was not started");
19945 rename_handler.next().await.unwrap();
19946 rename_task.await.expect("Confirm rename failed");
19947 cx.run_until_parked();
19948
19949 // Correct range is renamed, as `surrounding_word` is used to find it.
19950 cx.assert_editor_state(indoc! {"
19951 struct FooRenamedˇ {}
19952 "});
19953}
19954
19955#[gpui::test]
19956async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
19957 init_test(cx, |_| {});
19958 let mut cx = EditorTestContext::new(cx).await;
19959
19960 let language = Arc::new(
19961 Language::new(
19962 LanguageConfig::default(),
19963 Some(tree_sitter_html::LANGUAGE.into()),
19964 )
19965 .with_brackets_query(
19966 r#"
19967 ("<" @open "/>" @close)
19968 ("</" @open ">" @close)
19969 ("<" @open ">" @close)
19970 ("\"" @open "\"" @close)
19971 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
19972 "#,
19973 )
19974 .unwrap(),
19975 );
19976 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
19977
19978 cx.set_state(indoc! {"
19979 <span>ˇ</span>
19980 "});
19981 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19982 cx.assert_editor_state(indoc! {"
19983 <span>
19984 ˇ
19985 </span>
19986 "});
19987
19988 cx.set_state(indoc! {"
19989 <span><span></span>ˇ</span>
19990 "});
19991 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19992 cx.assert_editor_state(indoc! {"
19993 <span><span></span>
19994 ˇ</span>
19995 "});
19996
19997 cx.set_state(indoc! {"
19998 <span>ˇ
19999 </span>
20000 "});
20001 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
20002 cx.assert_editor_state(indoc! {"
20003 <span>
20004 ˇ
20005 </span>
20006 "});
20007}
20008
20009#[gpui::test(iterations = 10)]
20010async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
20011 init_test(cx, |_| {});
20012
20013 let fs = FakeFs::new(cx.executor());
20014 fs.insert_tree(
20015 path!("/dir"),
20016 json!({
20017 "a.ts": "a",
20018 }),
20019 )
20020 .await;
20021
20022 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
20023 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20024 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20025
20026 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20027 language_registry.add(Arc::new(Language::new(
20028 LanguageConfig {
20029 name: "TypeScript".into(),
20030 matcher: LanguageMatcher {
20031 path_suffixes: vec!["ts".to_string()],
20032 ..Default::default()
20033 },
20034 ..Default::default()
20035 },
20036 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
20037 )));
20038 let mut fake_language_servers = language_registry.register_fake_lsp(
20039 "TypeScript",
20040 FakeLspAdapter {
20041 capabilities: lsp::ServerCapabilities {
20042 code_lens_provider: Some(lsp::CodeLensOptions {
20043 resolve_provider: Some(true),
20044 }),
20045 execute_command_provider: Some(lsp::ExecuteCommandOptions {
20046 commands: vec!["_the/command".to_string()],
20047 ..lsp::ExecuteCommandOptions::default()
20048 }),
20049 ..lsp::ServerCapabilities::default()
20050 },
20051 ..FakeLspAdapter::default()
20052 },
20053 );
20054
20055 let (buffer, _handle) = project
20056 .update(cx, |p, cx| {
20057 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
20058 })
20059 .await
20060 .unwrap();
20061 cx.executor().run_until_parked();
20062
20063 let fake_server = fake_language_servers.next().await.unwrap();
20064
20065 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
20066 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
20067 drop(buffer_snapshot);
20068 let actions = cx
20069 .update_window(*workspace, |_, window, cx| {
20070 project.code_actions(&buffer, anchor..anchor, window, cx)
20071 })
20072 .unwrap();
20073
20074 fake_server
20075 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
20076 Ok(Some(vec![
20077 lsp::CodeLens {
20078 range: lsp::Range::default(),
20079 command: Some(lsp::Command {
20080 title: "Code lens command".to_owned(),
20081 command: "_the/command".to_owned(),
20082 arguments: None,
20083 }),
20084 data: None,
20085 },
20086 lsp::CodeLens {
20087 range: lsp::Range::default(),
20088 command: Some(lsp::Command {
20089 title: "Command not in capabilities".to_owned(),
20090 command: "not in capabilities".to_owned(),
20091 arguments: None,
20092 }),
20093 data: None,
20094 },
20095 lsp::CodeLens {
20096 range: lsp::Range {
20097 start: lsp::Position {
20098 line: 1,
20099 character: 1,
20100 },
20101 end: lsp::Position {
20102 line: 1,
20103 character: 1,
20104 },
20105 },
20106 command: Some(lsp::Command {
20107 title: "Command not in range".to_owned(),
20108 command: "_the/command".to_owned(),
20109 arguments: None,
20110 }),
20111 data: None,
20112 },
20113 ]))
20114 })
20115 .next()
20116 .await;
20117
20118 let actions = actions.await.unwrap();
20119 assert_eq!(
20120 actions.len(),
20121 1,
20122 "Should have only one valid action for the 0..0 range"
20123 );
20124 let action = actions[0].clone();
20125 let apply = project.update(cx, |project, cx| {
20126 project.apply_code_action(buffer.clone(), action, true, cx)
20127 });
20128
20129 // Resolving the code action does not populate its edits. In absence of
20130 // edits, we must execute the given command.
20131 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
20132 |mut lens, _| async move {
20133 let lens_command = lens.command.as_mut().expect("should have a command");
20134 assert_eq!(lens_command.title, "Code lens command");
20135 lens_command.arguments = Some(vec![json!("the-argument")]);
20136 Ok(lens)
20137 },
20138 );
20139
20140 // While executing the command, the language server sends the editor
20141 // a `workspaceEdit` request.
20142 fake_server
20143 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
20144 let fake = fake_server.clone();
20145 move |params, _| {
20146 assert_eq!(params.command, "_the/command");
20147 let fake = fake.clone();
20148 async move {
20149 fake.server
20150 .request::<lsp::request::ApplyWorkspaceEdit>(
20151 lsp::ApplyWorkspaceEditParams {
20152 label: None,
20153 edit: lsp::WorkspaceEdit {
20154 changes: Some(
20155 [(
20156 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
20157 vec![lsp::TextEdit {
20158 range: lsp::Range::new(
20159 lsp::Position::new(0, 0),
20160 lsp::Position::new(0, 0),
20161 ),
20162 new_text: "X".into(),
20163 }],
20164 )]
20165 .into_iter()
20166 .collect(),
20167 ),
20168 ..Default::default()
20169 },
20170 },
20171 )
20172 .await
20173 .into_response()
20174 .unwrap();
20175 Ok(Some(json!(null)))
20176 }
20177 }
20178 })
20179 .next()
20180 .await;
20181
20182 // Applying the code lens command returns a project transaction containing the edits
20183 // sent by the language server in its `workspaceEdit` request.
20184 let transaction = apply.await.unwrap();
20185 assert!(transaction.0.contains_key(&buffer));
20186 buffer.update(cx, |buffer, cx| {
20187 assert_eq!(buffer.text(), "Xa");
20188 buffer.undo(cx);
20189 assert_eq!(buffer.text(), "a");
20190 });
20191}
20192
20193#[gpui::test]
20194async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
20195 init_test(cx, |_| {});
20196
20197 let fs = FakeFs::new(cx.executor());
20198 let main_text = r#"fn main() {
20199println!("1");
20200println!("2");
20201println!("3");
20202println!("4");
20203println!("5");
20204}"#;
20205 let lib_text = "mod foo {}";
20206 fs.insert_tree(
20207 path!("/a"),
20208 json!({
20209 "lib.rs": lib_text,
20210 "main.rs": main_text,
20211 }),
20212 )
20213 .await;
20214
20215 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20216 let (workspace, cx) =
20217 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20218 let worktree_id = workspace.update(cx, |workspace, cx| {
20219 workspace.project().update(cx, |project, cx| {
20220 project.worktrees(cx).next().unwrap().read(cx).id()
20221 })
20222 });
20223
20224 let expected_ranges = vec![
20225 Point::new(0, 0)..Point::new(0, 0),
20226 Point::new(1, 0)..Point::new(1, 1),
20227 Point::new(2, 0)..Point::new(2, 2),
20228 Point::new(3, 0)..Point::new(3, 3),
20229 ];
20230
20231 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20232 let editor_1 = workspace
20233 .update_in(cx, |workspace, window, cx| {
20234 workspace.open_path(
20235 (worktree_id, "main.rs"),
20236 Some(pane_1.downgrade()),
20237 true,
20238 window,
20239 cx,
20240 )
20241 })
20242 .unwrap()
20243 .await
20244 .downcast::<Editor>()
20245 .unwrap();
20246 pane_1.update(cx, |pane, cx| {
20247 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20248 open_editor.update(cx, |editor, cx| {
20249 assert_eq!(
20250 editor.display_text(cx),
20251 main_text,
20252 "Original main.rs text on initial open",
20253 );
20254 assert_eq!(
20255 editor
20256 .selections
20257 .all::<Point>(cx)
20258 .into_iter()
20259 .map(|s| s.range())
20260 .collect::<Vec<_>>(),
20261 vec![Point::zero()..Point::zero()],
20262 "Default selections on initial open",
20263 );
20264 })
20265 });
20266 editor_1.update_in(cx, |editor, window, cx| {
20267 editor.change_selections(None, window, cx, |s| {
20268 s.select_ranges(expected_ranges.clone());
20269 });
20270 });
20271
20272 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
20273 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
20274 });
20275 let editor_2 = workspace
20276 .update_in(cx, |workspace, window, cx| {
20277 workspace.open_path(
20278 (worktree_id, "main.rs"),
20279 Some(pane_2.downgrade()),
20280 true,
20281 window,
20282 cx,
20283 )
20284 })
20285 .unwrap()
20286 .await
20287 .downcast::<Editor>()
20288 .unwrap();
20289 pane_2.update(cx, |pane, cx| {
20290 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20291 open_editor.update(cx, |editor, cx| {
20292 assert_eq!(
20293 editor.display_text(cx),
20294 main_text,
20295 "Original main.rs text on initial open in another panel",
20296 );
20297 assert_eq!(
20298 editor
20299 .selections
20300 .all::<Point>(cx)
20301 .into_iter()
20302 .map(|s| s.range())
20303 .collect::<Vec<_>>(),
20304 vec![Point::zero()..Point::zero()],
20305 "Default selections on initial open in another panel",
20306 );
20307 })
20308 });
20309
20310 editor_2.update_in(cx, |editor, window, cx| {
20311 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
20312 });
20313
20314 let _other_editor_1 = workspace
20315 .update_in(cx, |workspace, window, cx| {
20316 workspace.open_path(
20317 (worktree_id, "lib.rs"),
20318 Some(pane_1.downgrade()),
20319 true,
20320 window,
20321 cx,
20322 )
20323 })
20324 .unwrap()
20325 .await
20326 .downcast::<Editor>()
20327 .unwrap();
20328 pane_1
20329 .update_in(cx, |pane, window, cx| {
20330 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
20331 })
20332 .await
20333 .unwrap();
20334 drop(editor_1);
20335 pane_1.update(cx, |pane, cx| {
20336 pane.active_item()
20337 .unwrap()
20338 .downcast::<Editor>()
20339 .unwrap()
20340 .update(cx, |editor, cx| {
20341 assert_eq!(
20342 editor.display_text(cx),
20343 lib_text,
20344 "Other file should be open and active",
20345 );
20346 });
20347 assert_eq!(pane.items().count(), 1, "No other editors should be open");
20348 });
20349
20350 let _other_editor_2 = workspace
20351 .update_in(cx, |workspace, window, cx| {
20352 workspace.open_path(
20353 (worktree_id, "lib.rs"),
20354 Some(pane_2.downgrade()),
20355 true,
20356 window,
20357 cx,
20358 )
20359 })
20360 .unwrap()
20361 .await
20362 .downcast::<Editor>()
20363 .unwrap();
20364 pane_2
20365 .update_in(cx, |pane, window, cx| {
20366 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
20367 })
20368 .await
20369 .unwrap();
20370 drop(editor_2);
20371 pane_2.update(cx, |pane, cx| {
20372 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20373 open_editor.update(cx, |editor, cx| {
20374 assert_eq!(
20375 editor.display_text(cx),
20376 lib_text,
20377 "Other file should be open and active in another panel too",
20378 );
20379 });
20380 assert_eq!(
20381 pane.items().count(),
20382 1,
20383 "No other editors should be open in another pane",
20384 );
20385 });
20386
20387 let _editor_1_reopened = workspace
20388 .update_in(cx, |workspace, window, cx| {
20389 workspace.open_path(
20390 (worktree_id, "main.rs"),
20391 Some(pane_1.downgrade()),
20392 true,
20393 window,
20394 cx,
20395 )
20396 })
20397 .unwrap()
20398 .await
20399 .downcast::<Editor>()
20400 .unwrap();
20401 let _editor_2_reopened = workspace
20402 .update_in(cx, |workspace, window, cx| {
20403 workspace.open_path(
20404 (worktree_id, "main.rs"),
20405 Some(pane_2.downgrade()),
20406 true,
20407 window,
20408 cx,
20409 )
20410 })
20411 .unwrap()
20412 .await
20413 .downcast::<Editor>()
20414 .unwrap();
20415 pane_1.update(cx, |pane, cx| {
20416 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20417 open_editor.update(cx, |editor, cx| {
20418 assert_eq!(
20419 editor.display_text(cx),
20420 main_text,
20421 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
20422 );
20423 assert_eq!(
20424 editor
20425 .selections
20426 .all::<Point>(cx)
20427 .into_iter()
20428 .map(|s| s.range())
20429 .collect::<Vec<_>>(),
20430 expected_ranges,
20431 "Previous editor in the 1st panel had selections and should get them restored on reopen",
20432 );
20433 })
20434 });
20435 pane_2.update(cx, |pane, cx| {
20436 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20437 open_editor.update(cx, |editor, cx| {
20438 assert_eq!(
20439 editor.display_text(cx),
20440 r#"fn main() {
20441⋯rintln!("1");
20442⋯intln!("2");
20443⋯ntln!("3");
20444println!("4");
20445println!("5");
20446}"#,
20447 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
20448 );
20449 assert_eq!(
20450 editor
20451 .selections
20452 .all::<Point>(cx)
20453 .into_iter()
20454 .map(|s| s.range())
20455 .collect::<Vec<_>>(),
20456 vec![Point::zero()..Point::zero()],
20457 "Previous editor in the 2nd pane had no selections changed hence should restore none",
20458 );
20459 })
20460 });
20461}
20462
20463#[gpui::test]
20464async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
20465 init_test(cx, |_| {});
20466
20467 let fs = FakeFs::new(cx.executor());
20468 let main_text = r#"fn main() {
20469println!("1");
20470println!("2");
20471println!("3");
20472println!("4");
20473println!("5");
20474}"#;
20475 let lib_text = "mod foo {}";
20476 fs.insert_tree(
20477 path!("/a"),
20478 json!({
20479 "lib.rs": lib_text,
20480 "main.rs": main_text,
20481 }),
20482 )
20483 .await;
20484
20485 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20486 let (workspace, cx) =
20487 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20488 let worktree_id = workspace.update(cx, |workspace, cx| {
20489 workspace.project().update(cx, |project, cx| {
20490 project.worktrees(cx).next().unwrap().read(cx).id()
20491 })
20492 });
20493
20494 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20495 let editor = workspace
20496 .update_in(cx, |workspace, window, cx| {
20497 workspace.open_path(
20498 (worktree_id, "main.rs"),
20499 Some(pane.downgrade()),
20500 true,
20501 window,
20502 cx,
20503 )
20504 })
20505 .unwrap()
20506 .await
20507 .downcast::<Editor>()
20508 .unwrap();
20509 pane.update(cx, |pane, cx| {
20510 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20511 open_editor.update(cx, |editor, cx| {
20512 assert_eq!(
20513 editor.display_text(cx),
20514 main_text,
20515 "Original main.rs text on initial open",
20516 );
20517 })
20518 });
20519 editor.update_in(cx, |editor, window, cx| {
20520 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
20521 });
20522
20523 cx.update_global(|store: &mut SettingsStore, cx| {
20524 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
20525 s.restore_on_file_reopen = Some(false);
20526 });
20527 });
20528 editor.update_in(cx, |editor, window, cx| {
20529 editor.fold_ranges(
20530 vec![
20531 Point::new(1, 0)..Point::new(1, 1),
20532 Point::new(2, 0)..Point::new(2, 2),
20533 Point::new(3, 0)..Point::new(3, 3),
20534 ],
20535 false,
20536 window,
20537 cx,
20538 );
20539 });
20540 pane.update_in(cx, |pane, window, cx| {
20541 pane.close_all_items(&CloseAllItems::default(), window, cx)
20542 })
20543 .await
20544 .unwrap();
20545 pane.update(cx, |pane, _| {
20546 assert!(pane.active_item().is_none());
20547 });
20548 cx.update_global(|store: &mut SettingsStore, cx| {
20549 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
20550 s.restore_on_file_reopen = Some(true);
20551 });
20552 });
20553
20554 let _editor_reopened = workspace
20555 .update_in(cx, |workspace, window, cx| {
20556 workspace.open_path(
20557 (worktree_id, "main.rs"),
20558 Some(pane.downgrade()),
20559 true,
20560 window,
20561 cx,
20562 )
20563 })
20564 .unwrap()
20565 .await
20566 .downcast::<Editor>()
20567 .unwrap();
20568 pane.update(cx, |pane, cx| {
20569 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20570 open_editor.update(cx, |editor, cx| {
20571 assert_eq!(
20572 editor.display_text(cx),
20573 main_text,
20574 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
20575 );
20576 })
20577 });
20578}
20579
20580#[gpui::test]
20581async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
20582 struct EmptyModalView {
20583 focus_handle: gpui::FocusHandle,
20584 }
20585 impl EventEmitter<DismissEvent> for EmptyModalView {}
20586 impl Render for EmptyModalView {
20587 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
20588 div()
20589 }
20590 }
20591 impl Focusable for EmptyModalView {
20592 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
20593 self.focus_handle.clone()
20594 }
20595 }
20596 impl workspace::ModalView for EmptyModalView {}
20597 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
20598 EmptyModalView {
20599 focus_handle: cx.focus_handle(),
20600 }
20601 }
20602
20603 init_test(cx, |_| {});
20604
20605 let fs = FakeFs::new(cx.executor());
20606 let project = Project::test(fs, [], cx).await;
20607 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20608 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
20609 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20610 let editor = cx.new_window_entity(|window, cx| {
20611 Editor::new(
20612 EditorMode::full(),
20613 buffer,
20614 Some(project.clone()),
20615 window,
20616 cx,
20617 )
20618 });
20619 workspace
20620 .update(cx, |workspace, window, cx| {
20621 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
20622 })
20623 .unwrap();
20624 editor.update_in(cx, |editor, window, cx| {
20625 editor.open_context_menu(&OpenContextMenu, window, cx);
20626 assert!(editor.mouse_context_menu.is_some());
20627 });
20628 workspace
20629 .update(cx, |workspace, window, cx| {
20630 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
20631 })
20632 .unwrap();
20633 cx.read(|cx| {
20634 assert!(editor.read(cx).mouse_context_menu.is_none());
20635 });
20636}
20637
20638#[gpui::test]
20639async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
20640 init_test(cx, |_| {});
20641
20642 let fs = FakeFs::new(cx.executor());
20643 fs.insert_file(path!("/file.html"), Default::default())
20644 .await;
20645
20646 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
20647
20648 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20649 let html_language = Arc::new(Language::new(
20650 LanguageConfig {
20651 name: "HTML".into(),
20652 matcher: LanguageMatcher {
20653 path_suffixes: vec!["html".to_string()],
20654 ..LanguageMatcher::default()
20655 },
20656 brackets: BracketPairConfig {
20657 pairs: vec![BracketPair {
20658 start: "<".into(),
20659 end: ">".into(),
20660 close: true,
20661 ..Default::default()
20662 }],
20663 ..Default::default()
20664 },
20665 ..Default::default()
20666 },
20667 Some(tree_sitter_html::LANGUAGE.into()),
20668 ));
20669 language_registry.add(html_language);
20670 let mut fake_servers = language_registry.register_fake_lsp(
20671 "HTML",
20672 FakeLspAdapter {
20673 capabilities: lsp::ServerCapabilities {
20674 completion_provider: Some(lsp::CompletionOptions {
20675 resolve_provider: Some(true),
20676 ..Default::default()
20677 }),
20678 ..Default::default()
20679 },
20680 ..Default::default()
20681 },
20682 );
20683
20684 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20685 let cx = &mut VisualTestContext::from_window(*workspace, cx);
20686
20687 let worktree_id = workspace
20688 .update(cx, |workspace, _window, cx| {
20689 workspace.project().update(cx, |project, cx| {
20690 project.worktrees(cx).next().unwrap().read(cx).id()
20691 })
20692 })
20693 .unwrap();
20694 project
20695 .update(cx, |project, cx| {
20696 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
20697 })
20698 .await
20699 .unwrap();
20700 let editor = workspace
20701 .update(cx, |workspace, window, cx| {
20702 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
20703 })
20704 .unwrap()
20705 .await
20706 .unwrap()
20707 .downcast::<Editor>()
20708 .unwrap();
20709
20710 let fake_server = fake_servers.next().await.unwrap();
20711 editor.update_in(cx, |editor, window, cx| {
20712 editor.set_text("<ad></ad>", window, cx);
20713 editor.change_selections(None, window, cx, |selections| {
20714 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
20715 });
20716 let Some((buffer, _)) = editor
20717 .buffer
20718 .read(cx)
20719 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
20720 else {
20721 panic!("Failed to get buffer for selection position");
20722 };
20723 let buffer = buffer.read(cx);
20724 let buffer_id = buffer.remote_id();
20725 let opening_range =
20726 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
20727 let closing_range =
20728 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
20729 let mut linked_ranges = HashMap::default();
20730 linked_ranges.insert(
20731 buffer_id,
20732 vec![(opening_range.clone(), vec![closing_range.clone()])],
20733 );
20734 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
20735 });
20736 let mut completion_handle =
20737 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
20738 Ok(Some(lsp::CompletionResponse::Array(vec![
20739 lsp::CompletionItem {
20740 label: "head".to_string(),
20741 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
20742 lsp::InsertReplaceEdit {
20743 new_text: "head".to_string(),
20744 insert: lsp::Range::new(
20745 lsp::Position::new(0, 1),
20746 lsp::Position::new(0, 3),
20747 ),
20748 replace: lsp::Range::new(
20749 lsp::Position::new(0, 1),
20750 lsp::Position::new(0, 3),
20751 ),
20752 },
20753 )),
20754 ..Default::default()
20755 },
20756 ])))
20757 });
20758 editor.update_in(cx, |editor, window, cx| {
20759 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
20760 });
20761 cx.run_until_parked();
20762 completion_handle.next().await.unwrap();
20763 editor.update(cx, |editor, _| {
20764 assert!(
20765 editor.context_menu_visible(),
20766 "Completion menu should be visible"
20767 );
20768 });
20769 editor.update_in(cx, |editor, window, cx| {
20770 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
20771 });
20772 cx.executor().run_until_parked();
20773 editor.update(cx, |editor, cx| {
20774 assert_eq!(editor.text(cx), "<head></head>");
20775 });
20776}
20777
20778#[gpui::test]
20779async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
20780 init_test(cx, |_| {});
20781
20782 let fs = FakeFs::new(cx.executor());
20783 fs.insert_tree(
20784 path!("/root"),
20785 json!({
20786 "a": {
20787 "main.rs": "fn main() {}",
20788 },
20789 "foo": {
20790 "bar": {
20791 "external_file.rs": "pub mod external {}",
20792 }
20793 }
20794 }),
20795 )
20796 .await;
20797
20798 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
20799 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20800 language_registry.add(rust_lang());
20801 let _fake_servers = language_registry.register_fake_lsp(
20802 "Rust",
20803 FakeLspAdapter {
20804 ..FakeLspAdapter::default()
20805 },
20806 );
20807 let (workspace, cx) =
20808 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20809 let worktree_id = workspace.update(cx, |workspace, cx| {
20810 workspace.project().update(cx, |project, cx| {
20811 project.worktrees(cx).next().unwrap().read(cx).id()
20812 })
20813 });
20814
20815 let assert_language_servers_count =
20816 |expected: usize, context: &str, cx: &mut VisualTestContext| {
20817 project.update(cx, |project, cx| {
20818 let current = project
20819 .lsp_store()
20820 .read(cx)
20821 .as_local()
20822 .unwrap()
20823 .language_servers
20824 .len();
20825 assert_eq!(expected, current, "{context}");
20826 });
20827 };
20828
20829 assert_language_servers_count(
20830 0,
20831 "No servers should be running before any file is open",
20832 cx,
20833 );
20834 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20835 let main_editor = workspace
20836 .update_in(cx, |workspace, window, cx| {
20837 workspace.open_path(
20838 (worktree_id, "main.rs"),
20839 Some(pane.downgrade()),
20840 true,
20841 window,
20842 cx,
20843 )
20844 })
20845 .unwrap()
20846 .await
20847 .downcast::<Editor>()
20848 .unwrap();
20849 pane.update(cx, |pane, cx| {
20850 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20851 open_editor.update(cx, |editor, cx| {
20852 assert_eq!(
20853 editor.display_text(cx),
20854 "fn main() {}",
20855 "Original main.rs text on initial open",
20856 );
20857 });
20858 assert_eq!(open_editor, main_editor);
20859 });
20860 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
20861
20862 let external_editor = workspace
20863 .update_in(cx, |workspace, window, cx| {
20864 workspace.open_abs_path(
20865 PathBuf::from("/root/foo/bar/external_file.rs"),
20866 OpenOptions::default(),
20867 window,
20868 cx,
20869 )
20870 })
20871 .await
20872 .expect("opening external file")
20873 .downcast::<Editor>()
20874 .expect("downcasted external file's open element to editor");
20875 pane.update(cx, |pane, cx| {
20876 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20877 open_editor.update(cx, |editor, cx| {
20878 assert_eq!(
20879 editor.display_text(cx),
20880 "pub mod external {}",
20881 "External file is open now",
20882 );
20883 });
20884 assert_eq!(open_editor, external_editor);
20885 });
20886 assert_language_servers_count(
20887 1,
20888 "Second, external, *.rs file should join the existing server",
20889 cx,
20890 );
20891
20892 pane.update_in(cx, |pane, window, cx| {
20893 pane.close_active_item(&CloseActiveItem::default(), window, cx)
20894 })
20895 .await
20896 .unwrap();
20897 pane.update_in(cx, |pane, window, cx| {
20898 pane.navigate_backward(window, cx);
20899 });
20900 cx.run_until_parked();
20901 pane.update(cx, |pane, cx| {
20902 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20903 open_editor.update(cx, |editor, cx| {
20904 assert_eq!(
20905 editor.display_text(cx),
20906 "pub mod external {}",
20907 "External file is open now",
20908 );
20909 });
20910 });
20911 assert_language_servers_count(
20912 1,
20913 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
20914 cx,
20915 );
20916
20917 cx.update(|_, cx| {
20918 workspace::reload(&workspace::Reload::default(), cx);
20919 });
20920 assert_language_servers_count(
20921 1,
20922 "After reloading the worktree with local and external files opened, only one project should be started",
20923 cx,
20924 );
20925}
20926
20927#[gpui::test]
20928async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
20929 init_test(cx, |_| {});
20930
20931 let mut cx = EditorTestContext::new(cx).await;
20932 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
20933 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20934
20935 // test cursor move to start of each line on tab
20936 // for `if`, `elif`, `else`, `while`, `with` and `for`
20937 cx.set_state(indoc! {"
20938 def main():
20939 ˇ for item in items:
20940 ˇ while item.active:
20941 ˇ if item.value > 10:
20942 ˇ continue
20943 ˇ elif item.value < 0:
20944 ˇ break
20945 ˇ else:
20946 ˇ with item.context() as ctx:
20947 ˇ yield count
20948 ˇ else:
20949 ˇ log('while else')
20950 ˇ else:
20951 ˇ log('for else')
20952 "});
20953 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20954 cx.assert_editor_state(indoc! {"
20955 def main():
20956 ˇfor item in items:
20957 ˇwhile item.active:
20958 ˇif item.value > 10:
20959 ˇcontinue
20960 ˇelif item.value < 0:
20961 ˇbreak
20962 ˇelse:
20963 ˇwith item.context() as ctx:
20964 ˇyield count
20965 ˇelse:
20966 ˇlog('while else')
20967 ˇelse:
20968 ˇlog('for else')
20969 "});
20970 // test relative indent is preserved when tab
20971 // for `if`, `elif`, `else`, `while`, `with` and `for`
20972 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20973 cx.assert_editor_state(indoc! {"
20974 def main():
20975 ˇfor item in items:
20976 ˇwhile item.active:
20977 ˇif item.value > 10:
20978 ˇcontinue
20979 ˇelif item.value < 0:
20980 ˇbreak
20981 ˇelse:
20982 ˇwith item.context() as ctx:
20983 ˇyield count
20984 ˇelse:
20985 ˇlog('while else')
20986 ˇelse:
20987 ˇlog('for else')
20988 "});
20989
20990 // test cursor move to start of each line on tab
20991 // for `try`, `except`, `else`, `finally`, `match` and `def`
20992 cx.set_state(indoc! {"
20993 def main():
20994 ˇ try:
20995 ˇ fetch()
20996 ˇ except ValueError:
20997 ˇ handle_error()
20998 ˇ else:
20999 ˇ match value:
21000 ˇ case _:
21001 ˇ finally:
21002 ˇ def status():
21003 ˇ return 0
21004 "});
21005 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21006 cx.assert_editor_state(indoc! {"
21007 def main():
21008 ˇtry:
21009 ˇfetch()
21010 ˇexcept ValueError:
21011 ˇhandle_error()
21012 ˇelse:
21013 ˇmatch value:
21014 ˇcase _:
21015 ˇfinally:
21016 ˇdef status():
21017 ˇreturn 0
21018 "});
21019 // test relative indent is preserved when tab
21020 // for `try`, `except`, `else`, `finally`, `match` and `def`
21021 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21022 cx.assert_editor_state(indoc! {"
21023 def main():
21024 ˇtry:
21025 ˇfetch()
21026 ˇexcept ValueError:
21027 ˇhandle_error()
21028 ˇelse:
21029 ˇmatch value:
21030 ˇcase _:
21031 ˇfinally:
21032 ˇdef status():
21033 ˇreturn 0
21034 "});
21035}
21036
21037#[gpui::test]
21038async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
21039 init_test(cx, |_| {});
21040
21041 let mut cx = EditorTestContext::new(cx).await;
21042 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
21043 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21044
21045 // test `else` auto outdents when typed inside `if` block
21046 cx.set_state(indoc! {"
21047 def main():
21048 if i == 2:
21049 return
21050 ˇ
21051 "});
21052 cx.update_editor(|editor, window, cx| {
21053 editor.handle_input("else:", window, cx);
21054 });
21055 cx.assert_editor_state(indoc! {"
21056 def main():
21057 if i == 2:
21058 return
21059 else:ˇ
21060 "});
21061
21062 // test `except` auto outdents when typed inside `try` block
21063 cx.set_state(indoc! {"
21064 def main():
21065 try:
21066 i = 2
21067 ˇ
21068 "});
21069 cx.update_editor(|editor, window, cx| {
21070 editor.handle_input("except:", window, cx);
21071 });
21072 cx.assert_editor_state(indoc! {"
21073 def main():
21074 try:
21075 i = 2
21076 except:ˇ
21077 "});
21078
21079 // test `else` auto outdents when typed inside `except` block
21080 cx.set_state(indoc! {"
21081 def main():
21082 try:
21083 i = 2
21084 except:
21085 j = 2
21086 ˇ
21087 "});
21088 cx.update_editor(|editor, window, cx| {
21089 editor.handle_input("else:", window, cx);
21090 });
21091 cx.assert_editor_state(indoc! {"
21092 def main():
21093 try:
21094 i = 2
21095 except:
21096 j = 2
21097 else:ˇ
21098 "});
21099
21100 // test `finally` auto outdents when typed inside `else` block
21101 cx.set_state(indoc! {"
21102 def main():
21103 try:
21104 i = 2
21105 except:
21106 j = 2
21107 else:
21108 k = 2
21109 ˇ
21110 "});
21111 cx.update_editor(|editor, window, cx| {
21112 editor.handle_input("finally:", window, cx);
21113 });
21114 cx.assert_editor_state(indoc! {"
21115 def main():
21116 try:
21117 i = 2
21118 except:
21119 j = 2
21120 else:
21121 k = 2
21122 finally:ˇ
21123 "});
21124
21125 // TODO: test `except` auto outdents when typed inside `try` block right after for block
21126 // cx.set_state(indoc! {"
21127 // def main():
21128 // try:
21129 // for i in range(n):
21130 // pass
21131 // ˇ
21132 // "});
21133 // cx.update_editor(|editor, window, cx| {
21134 // editor.handle_input("except:", window, cx);
21135 // });
21136 // cx.assert_editor_state(indoc! {"
21137 // def main():
21138 // try:
21139 // for i in range(n):
21140 // pass
21141 // except:ˇ
21142 // "});
21143
21144 // TODO: test `else` auto outdents when typed inside `except` block right after for block
21145 // cx.set_state(indoc! {"
21146 // def main():
21147 // try:
21148 // i = 2
21149 // except:
21150 // for i in range(n):
21151 // pass
21152 // ˇ
21153 // "});
21154 // cx.update_editor(|editor, window, cx| {
21155 // editor.handle_input("else:", window, cx);
21156 // });
21157 // cx.assert_editor_state(indoc! {"
21158 // def main():
21159 // try:
21160 // i = 2
21161 // except:
21162 // for i in range(n):
21163 // pass
21164 // else:ˇ
21165 // "});
21166
21167 // TODO: test `finally` auto outdents when typed inside `else` block right after for block
21168 // cx.set_state(indoc! {"
21169 // def main():
21170 // try:
21171 // i = 2
21172 // except:
21173 // j = 2
21174 // else:
21175 // for i in range(n):
21176 // pass
21177 // ˇ
21178 // "});
21179 // cx.update_editor(|editor, window, cx| {
21180 // editor.handle_input("finally:", window, cx);
21181 // });
21182 // cx.assert_editor_state(indoc! {"
21183 // def main():
21184 // try:
21185 // i = 2
21186 // except:
21187 // j = 2
21188 // else:
21189 // for i in range(n):
21190 // pass
21191 // finally:ˇ
21192 // "});
21193
21194 // test `else` stays at correct indent when typed after `for` block
21195 cx.set_state(indoc! {"
21196 def main():
21197 for i in range(10):
21198 if i == 3:
21199 break
21200 ˇ
21201 "});
21202 cx.update_editor(|editor, window, cx| {
21203 editor.handle_input("else:", window, cx);
21204 });
21205 cx.assert_editor_state(indoc! {"
21206 def main():
21207 for i in range(10):
21208 if i == 3:
21209 break
21210 else:ˇ
21211 "});
21212
21213 // test does not outdent on typing after line with square brackets
21214 cx.set_state(indoc! {"
21215 def f() -> list[str]:
21216 ˇ
21217 "});
21218 cx.update_editor(|editor, window, cx| {
21219 editor.handle_input("a", window, cx);
21220 });
21221 cx.assert_editor_state(indoc! {"
21222 def f() -> list[str]:
21223 aˇ
21224 "});
21225}
21226
21227#[gpui::test]
21228async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
21229 init_test(cx, |_| {});
21230 update_test_language_settings(cx, |settings| {
21231 settings.defaults.extend_comment_on_newline = Some(false);
21232 });
21233 let mut cx = EditorTestContext::new(cx).await;
21234 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
21235 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21236
21237 // test correct indent after newline on comment
21238 cx.set_state(indoc! {"
21239 # COMMENT:ˇ
21240 "});
21241 cx.update_editor(|editor, window, cx| {
21242 editor.newline(&Newline, window, cx);
21243 });
21244 cx.assert_editor_state(indoc! {"
21245 # COMMENT:
21246 ˇ
21247 "});
21248
21249 // test correct indent after newline in brackets
21250 cx.set_state(indoc! {"
21251 {ˇ}
21252 "});
21253 cx.update_editor(|editor, window, cx| {
21254 editor.newline(&Newline, window, cx);
21255 });
21256 cx.run_until_parked();
21257 cx.assert_editor_state(indoc! {"
21258 {
21259 ˇ
21260 }
21261 "});
21262
21263 cx.set_state(indoc! {"
21264 (ˇ)
21265 "});
21266 cx.update_editor(|editor, window, cx| {
21267 editor.newline(&Newline, window, cx);
21268 });
21269 cx.run_until_parked();
21270 cx.assert_editor_state(indoc! {"
21271 (
21272 ˇ
21273 )
21274 "});
21275
21276 // do not indent after empty lists or dictionaries
21277 cx.set_state(indoc! {"
21278 a = []ˇ
21279 "});
21280 cx.update_editor(|editor, window, cx| {
21281 editor.newline(&Newline, window, cx);
21282 });
21283 cx.run_until_parked();
21284 cx.assert_editor_state(indoc! {"
21285 a = []
21286 ˇ
21287 "});
21288}
21289
21290fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
21291 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
21292 point..point
21293}
21294
21295#[track_caller]
21296fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
21297 let (text, ranges) = marked_text_ranges(marked_text, true);
21298 assert_eq!(editor.text(cx), text);
21299 assert_eq!(
21300 editor.selections.ranges(cx),
21301 ranges,
21302 "Assert selections are {}",
21303 marked_text
21304 );
21305}
21306
21307pub fn handle_signature_help_request(
21308 cx: &mut EditorLspTestContext,
21309 mocked_response: lsp::SignatureHelp,
21310) -> impl Future<Output = ()> + use<> {
21311 let mut request =
21312 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
21313 let mocked_response = mocked_response.clone();
21314 async move { Ok(Some(mocked_response)) }
21315 });
21316
21317 async move {
21318 request.next().await;
21319 }
21320}
21321
21322#[track_caller]
21323pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
21324 cx.update_editor(|editor, _, _| {
21325 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
21326 let entries = menu.entries.borrow();
21327 let entries = entries
21328 .iter()
21329 .map(|entry| entry.string.as_str())
21330 .collect::<Vec<_>>();
21331 assert_eq!(entries, expected);
21332 } else {
21333 panic!("Expected completions menu");
21334 }
21335 });
21336}
21337
21338/// Handle completion request passing a marked string specifying where the completion
21339/// should be triggered from using '|' character, what range should be replaced, and what completions
21340/// should be returned using '<' and '>' to delimit the range.
21341///
21342/// Also see `handle_completion_request_with_insert_and_replace`.
21343#[track_caller]
21344pub fn handle_completion_request(
21345 marked_string: &str,
21346 completions: Vec<&'static str>,
21347 is_incomplete: bool,
21348 counter: Arc<AtomicUsize>,
21349 cx: &mut EditorLspTestContext,
21350) -> impl Future<Output = ()> {
21351 let complete_from_marker: TextRangeMarker = '|'.into();
21352 let replace_range_marker: TextRangeMarker = ('<', '>').into();
21353 let (_, mut marked_ranges) = marked_text_ranges_by(
21354 marked_string,
21355 vec![complete_from_marker.clone(), replace_range_marker.clone()],
21356 );
21357
21358 let complete_from_position =
21359 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
21360 let replace_range =
21361 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
21362
21363 let mut request =
21364 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
21365 let completions = completions.clone();
21366 counter.fetch_add(1, atomic::Ordering::Release);
21367 async move {
21368 assert_eq!(params.text_document_position.text_document.uri, url.clone());
21369 assert_eq!(
21370 params.text_document_position.position,
21371 complete_from_position
21372 );
21373 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
21374 is_incomplete: is_incomplete,
21375 item_defaults: None,
21376 items: completions
21377 .iter()
21378 .map(|completion_text| lsp::CompletionItem {
21379 label: completion_text.to_string(),
21380 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
21381 range: replace_range,
21382 new_text: completion_text.to_string(),
21383 })),
21384 ..Default::default()
21385 })
21386 .collect(),
21387 })))
21388 }
21389 });
21390
21391 async move {
21392 request.next().await;
21393 }
21394}
21395
21396/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
21397/// given instead, which also contains an `insert` range.
21398///
21399/// This function uses markers to define ranges:
21400/// - `|` marks the cursor position
21401/// - `<>` marks the replace range
21402/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
21403pub fn handle_completion_request_with_insert_and_replace(
21404 cx: &mut EditorLspTestContext,
21405 marked_string: &str,
21406 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
21407 counter: Arc<AtomicUsize>,
21408) -> impl Future<Output = ()> {
21409 let complete_from_marker: TextRangeMarker = '|'.into();
21410 let replace_range_marker: TextRangeMarker = ('<', '>').into();
21411 let insert_range_marker: TextRangeMarker = ('{', '}').into();
21412
21413 let (_, mut marked_ranges) = marked_text_ranges_by(
21414 marked_string,
21415 vec![
21416 complete_from_marker.clone(),
21417 replace_range_marker.clone(),
21418 insert_range_marker.clone(),
21419 ],
21420 );
21421
21422 let complete_from_position =
21423 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
21424 let replace_range =
21425 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
21426
21427 let insert_range = match marked_ranges.remove(&insert_range_marker) {
21428 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
21429 _ => lsp::Range {
21430 start: replace_range.start,
21431 end: complete_from_position,
21432 },
21433 };
21434
21435 let mut request =
21436 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
21437 let completions = completions.clone();
21438 counter.fetch_add(1, atomic::Ordering::Release);
21439 async move {
21440 assert_eq!(params.text_document_position.text_document.uri, url.clone());
21441 assert_eq!(
21442 params.text_document_position.position, complete_from_position,
21443 "marker `|` position doesn't match",
21444 );
21445 Ok(Some(lsp::CompletionResponse::Array(
21446 completions
21447 .iter()
21448 .map(|(label, new_text)| lsp::CompletionItem {
21449 label: label.to_string(),
21450 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
21451 lsp::InsertReplaceEdit {
21452 insert: insert_range,
21453 replace: replace_range,
21454 new_text: new_text.to_string(),
21455 },
21456 )),
21457 ..Default::default()
21458 })
21459 .collect(),
21460 )))
21461 }
21462 });
21463
21464 async move {
21465 request.next().await;
21466 }
21467}
21468
21469fn handle_resolve_completion_request(
21470 cx: &mut EditorLspTestContext,
21471 edits: Option<Vec<(&'static str, &'static str)>>,
21472) -> impl Future<Output = ()> {
21473 let edits = edits.map(|edits| {
21474 edits
21475 .iter()
21476 .map(|(marked_string, new_text)| {
21477 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
21478 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
21479 lsp::TextEdit::new(replace_range, new_text.to_string())
21480 })
21481 .collect::<Vec<_>>()
21482 });
21483
21484 let mut request =
21485 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
21486 let edits = edits.clone();
21487 async move {
21488 Ok(lsp::CompletionItem {
21489 additional_text_edits: edits,
21490 ..Default::default()
21491 })
21492 }
21493 });
21494
21495 async move {
21496 request.next().await;
21497 }
21498}
21499
21500pub(crate) fn update_test_language_settings(
21501 cx: &mut TestAppContext,
21502 f: impl Fn(&mut AllLanguageSettingsContent),
21503) {
21504 cx.update(|cx| {
21505 SettingsStore::update_global(cx, |store, cx| {
21506 store.update_user_settings::<AllLanguageSettings>(cx, f);
21507 });
21508 });
21509}
21510
21511pub(crate) fn update_test_project_settings(
21512 cx: &mut TestAppContext,
21513 f: impl Fn(&mut ProjectSettings),
21514) {
21515 cx.update(|cx| {
21516 SettingsStore::update_global(cx, |store, cx| {
21517 store.update_user_settings::<ProjectSettings>(cx, f);
21518 });
21519 });
21520}
21521
21522pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
21523 cx.update(|cx| {
21524 assets::Assets.load_test_fonts(cx);
21525 let store = SettingsStore::test(cx);
21526 cx.set_global(store);
21527 theme::init(theme::LoadThemes::JustBase, cx);
21528 release_channel::init(SemanticVersion::default(), cx);
21529 client::init_settings(cx);
21530 language::init(cx);
21531 Project::init_settings(cx);
21532 workspace::init_settings(cx);
21533 crate::init(cx);
21534 });
21535
21536 update_test_language_settings(cx, f);
21537}
21538
21539#[track_caller]
21540fn assert_hunk_revert(
21541 not_reverted_text_with_selections: &str,
21542 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
21543 expected_reverted_text_with_selections: &str,
21544 base_text: &str,
21545 cx: &mut EditorLspTestContext,
21546) {
21547 cx.set_state(not_reverted_text_with_selections);
21548 cx.set_head_text(base_text);
21549 cx.executor().run_until_parked();
21550
21551 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
21552 let snapshot = editor.snapshot(window, cx);
21553 let reverted_hunk_statuses = snapshot
21554 .buffer_snapshot
21555 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
21556 .map(|hunk| hunk.status().kind)
21557 .collect::<Vec<_>>();
21558
21559 editor.git_restore(&Default::default(), window, cx);
21560 reverted_hunk_statuses
21561 });
21562 cx.executor().run_until_parked();
21563 cx.assert_editor_state(expected_reverted_text_with_selections);
21564 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
21565}
21566
21567#[gpui::test]
21568async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
21569 init_test(cx, |_| {});
21570
21571 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
21572 let counter = diagnostic_requests.clone();
21573
21574 let fs = FakeFs::new(cx.executor());
21575 fs.insert_tree(
21576 path!("/a"),
21577 json!({
21578 "first.rs": "fn main() { let a = 5; }",
21579 "second.rs": "// Test file",
21580 }),
21581 )
21582 .await;
21583
21584 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21585 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21586 let cx = &mut VisualTestContext::from_window(*workspace, cx);
21587
21588 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21589 language_registry.add(rust_lang());
21590 let mut fake_servers = language_registry.register_fake_lsp(
21591 "Rust",
21592 FakeLspAdapter {
21593 capabilities: lsp::ServerCapabilities {
21594 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
21595 lsp::DiagnosticOptions {
21596 identifier: None,
21597 inter_file_dependencies: true,
21598 workspace_diagnostics: true,
21599 work_done_progress_options: Default::default(),
21600 },
21601 )),
21602 ..Default::default()
21603 },
21604 ..Default::default()
21605 },
21606 );
21607
21608 let editor = workspace
21609 .update(cx, |workspace, window, cx| {
21610 workspace.open_abs_path(
21611 PathBuf::from(path!("/a/first.rs")),
21612 OpenOptions::default(),
21613 window,
21614 cx,
21615 )
21616 })
21617 .unwrap()
21618 .await
21619 .unwrap()
21620 .downcast::<Editor>()
21621 .unwrap();
21622 let fake_server = fake_servers.next().await.unwrap();
21623 let mut first_request = fake_server
21624 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
21625 counter.fetch_add(1, atomic::Ordering::Release);
21626 assert_eq!(
21627 params.text_document.uri,
21628 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
21629 );
21630 async move {
21631 Ok(lsp::DocumentDiagnosticReportResult::Report(
21632 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
21633 related_documents: None,
21634 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
21635 items: Vec::new(),
21636 result_id: None,
21637 },
21638 }),
21639 ))
21640 }
21641 });
21642
21643 cx.executor().advance_clock(Duration::from_millis(60));
21644 cx.executor().run_until_parked();
21645 assert_eq!(
21646 diagnostic_requests.load(atomic::Ordering::Acquire),
21647 1,
21648 "Opening file should trigger diagnostic request"
21649 );
21650 first_request
21651 .next()
21652 .await
21653 .expect("should have sent the first diagnostics pull request");
21654
21655 // Editing should trigger diagnostics
21656 editor.update_in(cx, |editor, window, cx| {
21657 editor.handle_input("2", window, cx)
21658 });
21659 cx.executor().advance_clock(Duration::from_millis(60));
21660 cx.executor().run_until_parked();
21661 assert_eq!(
21662 diagnostic_requests.load(atomic::Ordering::Acquire),
21663 2,
21664 "Editing should trigger diagnostic request"
21665 );
21666
21667 // Moving cursor should not trigger diagnostic request
21668 editor.update_in(cx, |editor, window, cx| {
21669 editor.change_selections(None, window, cx, |s| {
21670 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
21671 });
21672 });
21673 cx.executor().advance_clock(Duration::from_millis(60));
21674 cx.executor().run_until_parked();
21675 assert_eq!(
21676 diagnostic_requests.load(atomic::Ordering::Acquire),
21677 2,
21678 "Cursor movement should not trigger diagnostic request"
21679 );
21680
21681 // Multiple rapid edits should be debounced
21682 for _ in 0..5 {
21683 editor.update_in(cx, |editor, window, cx| {
21684 editor.handle_input("x", window, cx)
21685 });
21686 }
21687 cx.executor().advance_clock(Duration::from_millis(60));
21688 cx.executor().run_until_parked();
21689
21690 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
21691 assert!(
21692 final_requests <= 4,
21693 "Multiple rapid edits should be debounced (got {} requests)",
21694 final_requests
21695 );
21696}