1use super::*;
2use crate::{
3 JoinLines,
4 linked_editing_ranges::LinkedEditingRanges,
5 scroll::scroll_amount::ScrollAmount,
6 test::{
7 assert_text_with_selections, build_editor,
8 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
9 editor_test_context::EditorTestContext,
10 select_ranges,
11 },
12};
13use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
14use futures::StreamExt;
15use gpui::{
16 BackgroundExecutor, DismissEvent, SemanticVersion, TestAppContext, UpdateGlobal,
17 VisualTestContext, WindowBounds, WindowOptions, div,
18};
19use indoc::indoc;
20use language::{
21 BracketPairConfig,
22 Capability::ReadWrite,
23 FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName,
24 Override, Point,
25 language_settings::{
26 AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
27 LanguageSettingsContent, LspInsertMode, PrettierSettings,
28 },
29};
30use language_settings::{Formatter, FormatterList, IndentGuideSettings};
31use lsp::CompletionParams;
32use multi_buffer::{IndentGuide, PathKey};
33use parking_lot::Mutex;
34use pretty_assertions::{assert_eq, assert_ne};
35use project::{
36 FakeFs,
37 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
38 project_settings::{LspSettings, ProjectSettings},
39};
40use serde_json::{self, json};
41use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
42use std::{
43 iter,
44 sync::atomic::{self, AtomicUsize},
45};
46use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
47use text::ToPoint as _;
48use unindent::Unindent;
49use util::{
50 assert_set_eq, path,
51 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
52 uri,
53};
54use workspace::{
55 CloseAllItems, CloseInactiveItems, NavigationEntry, ViewId,
56 item::{FollowEvent, FollowableItem, Item, ItemHandle},
57};
58
59#[gpui::test]
60fn test_edit_events(cx: &mut TestAppContext) {
61 init_test(cx, |_| {});
62
63 let buffer = cx.new(|cx| {
64 let mut buffer = language::Buffer::local("123456", cx);
65 buffer.set_group_interval(Duration::from_secs(1));
66 buffer
67 });
68
69 let events = Rc::new(RefCell::new(Vec::new()));
70 let editor1 = cx.add_window({
71 let events = events.clone();
72 |window, cx| {
73 let entity = cx.entity().clone();
74 cx.subscribe_in(
75 &entity,
76 window,
77 move |_, _, event: &EditorEvent, _, _| match event {
78 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
79 EditorEvent::BufferEdited => {
80 events.borrow_mut().push(("editor1", "buffer edited"))
81 }
82 _ => {}
83 },
84 )
85 .detach();
86 Editor::for_buffer(buffer.clone(), None, window, cx)
87 }
88 });
89
90 let editor2 = cx.add_window({
91 let events = events.clone();
92 |window, cx| {
93 cx.subscribe_in(
94 &cx.entity().clone(),
95 window,
96 move |_, _, event: &EditorEvent, _, _| match event {
97 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
98 EditorEvent::BufferEdited => {
99 events.borrow_mut().push(("editor2", "buffer edited"))
100 }
101 _ => {}
102 },
103 )
104 .detach();
105 Editor::for_buffer(buffer.clone(), None, window, cx)
106 }
107 });
108
109 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
110
111 // Mutating editor 1 will emit an `Edited` event only for that editor.
112 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
113 assert_eq!(
114 mem::take(&mut *events.borrow_mut()),
115 [
116 ("editor1", "edited"),
117 ("editor1", "buffer edited"),
118 ("editor2", "buffer edited"),
119 ]
120 );
121
122 // Mutating editor 2 will emit an `Edited` event only for that editor.
123 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
124 assert_eq!(
125 mem::take(&mut *events.borrow_mut()),
126 [
127 ("editor2", "edited"),
128 ("editor1", "buffer edited"),
129 ("editor2", "buffer edited"),
130 ]
131 );
132
133 // Undoing on editor 1 will emit an `Edited` event only for that editor.
134 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
135 assert_eq!(
136 mem::take(&mut *events.borrow_mut()),
137 [
138 ("editor1", "edited"),
139 ("editor1", "buffer edited"),
140 ("editor2", "buffer edited"),
141 ]
142 );
143
144 // Redoing on editor 1 will emit an `Edited` event only for that editor.
145 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
146 assert_eq!(
147 mem::take(&mut *events.borrow_mut()),
148 [
149 ("editor1", "edited"),
150 ("editor1", "buffer edited"),
151 ("editor2", "buffer edited"),
152 ]
153 );
154
155 // Undoing on editor 2 will emit an `Edited` event only for that editor.
156 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
157 assert_eq!(
158 mem::take(&mut *events.borrow_mut()),
159 [
160 ("editor2", "edited"),
161 ("editor1", "buffer edited"),
162 ("editor2", "buffer edited"),
163 ]
164 );
165
166 // Redoing on editor 2 will emit an `Edited` event only for that editor.
167 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
168 assert_eq!(
169 mem::take(&mut *events.borrow_mut()),
170 [
171 ("editor2", "edited"),
172 ("editor1", "buffer edited"),
173 ("editor2", "buffer edited"),
174 ]
175 );
176
177 // No event is emitted when the mutation is a no-op.
178 _ = editor2.update(cx, |editor, window, cx| {
179 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
180
181 editor.backspace(&Backspace, window, cx);
182 });
183 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
184}
185
186#[gpui::test]
187fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
188 init_test(cx, |_| {});
189
190 let mut now = Instant::now();
191 let group_interval = Duration::from_millis(1);
192 let buffer = cx.new(|cx| {
193 let mut buf = language::Buffer::local("123456", cx);
194 buf.set_group_interval(group_interval);
195 buf
196 });
197 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
198 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
199
200 _ = editor.update(cx, |editor, window, cx| {
201 editor.start_transaction_at(now, window, cx);
202 editor.change_selections(None, window, cx, |s| s.select_ranges([2..4]));
203
204 editor.insert("cd", window, cx);
205 editor.end_transaction_at(now, cx);
206 assert_eq!(editor.text(cx), "12cd56");
207 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
208
209 editor.start_transaction_at(now, window, cx);
210 editor.change_selections(None, window, cx, |s| s.select_ranges([4..5]));
211 editor.insert("e", window, cx);
212 editor.end_transaction_at(now, cx);
213 assert_eq!(editor.text(cx), "12cde6");
214 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
215
216 now += group_interval + Duration::from_millis(1);
217 editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]));
218
219 // Simulate an edit in another editor
220 buffer.update(cx, |buffer, cx| {
221 buffer.start_transaction_at(now, cx);
222 buffer.edit([(0..1, "a")], None, cx);
223 buffer.edit([(1..1, "b")], None, cx);
224 buffer.end_transaction_at(now, cx);
225 });
226
227 assert_eq!(editor.text(cx), "ab2cde6");
228 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
229
230 // Last transaction happened past the group interval in a different editor.
231 // Undo it individually and don't restore selections.
232 editor.undo(&Undo, window, cx);
233 assert_eq!(editor.text(cx), "12cde6");
234 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
235
236 // First two transactions happened within the group interval in this editor.
237 // Undo them together and restore selections.
238 editor.undo(&Undo, window, cx);
239 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
240 assert_eq!(editor.text(cx), "123456");
241 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
242
243 // Redo the first two transactions together.
244 editor.redo(&Redo, window, cx);
245 assert_eq!(editor.text(cx), "12cde6");
246 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
247
248 // Redo the last transaction on its own.
249 editor.redo(&Redo, window, cx);
250 assert_eq!(editor.text(cx), "ab2cde6");
251 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
252
253 // Test empty transactions.
254 editor.start_transaction_at(now, window, cx);
255 editor.end_transaction_at(now, cx);
256 editor.undo(&Undo, window, cx);
257 assert_eq!(editor.text(cx), "12cde6");
258 });
259}
260
261#[gpui::test]
262fn test_ime_composition(cx: &mut TestAppContext) {
263 init_test(cx, |_| {});
264
265 let buffer = cx.new(|cx| {
266 let mut buffer = language::Buffer::local("abcde", cx);
267 // Ensure automatic grouping doesn't occur.
268 buffer.set_group_interval(Duration::ZERO);
269 buffer
270 });
271
272 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
273 cx.add_window(|window, cx| {
274 let mut editor = build_editor(buffer.clone(), window, cx);
275
276 // Start a new IME composition.
277 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
278 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
279 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
280 assert_eq!(editor.text(cx), "äbcde");
281 assert_eq!(
282 editor.marked_text_ranges(cx),
283 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
284 );
285
286 // Finalize IME composition.
287 editor.replace_text_in_range(None, "ā", window, cx);
288 assert_eq!(editor.text(cx), "ābcde");
289 assert_eq!(editor.marked_text_ranges(cx), None);
290
291 // IME composition edits are grouped and are undone/redone at once.
292 editor.undo(&Default::default(), window, cx);
293 assert_eq!(editor.text(cx), "abcde");
294 assert_eq!(editor.marked_text_ranges(cx), None);
295 editor.redo(&Default::default(), window, cx);
296 assert_eq!(editor.text(cx), "ābcde");
297 assert_eq!(editor.marked_text_ranges(cx), None);
298
299 // Start a new IME composition.
300 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
301 assert_eq!(
302 editor.marked_text_ranges(cx),
303 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
304 );
305
306 // Undoing during an IME composition cancels it.
307 editor.undo(&Default::default(), window, cx);
308 assert_eq!(editor.text(cx), "ābcde");
309 assert_eq!(editor.marked_text_ranges(cx), None);
310
311 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
312 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
313 assert_eq!(editor.text(cx), "ābcdè");
314 assert_eq!(
315 editor.marked_text_ranges(cx),
316 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
317 );
318
319 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
320 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
321 assert_eq!(editor.text(cx), "ābcdę");
322 assert_eq!(editor.marked_text_ranges(cx), None);
323
324 // Start a new IME composition with multiple cursors.
325 editor.change_selections(None, window, cx, |s| {
326 s.select_ranges([
327 OffsetUtf16(1)..OffsetUtf16(1),
328 OffsetUtf16(3)..OffsetUtf16(3),
329 OffsetUtf16(5)..OffsetUtf16(5),
330 ])
331 });
332 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
333 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
334 assert_eq!(
335 editor.marked_text_ranges(cx),
336 Some(vec![
337 OffsetUtf16(0)..OffsetUtf16(3),
338 OffsetUtf16(4)..OffsetUtf16(7),
339 OffsetUtf16(8)..OffsetUtf16(11)
340 ])
341 );
342
343 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
344 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
345 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
346 assert_eq!(
347 editor.marked_text_ranges(cx),
348 Some(vec![
349 OffsetUtf16(1)..OffsetUtf16(2),
350 OffsetUtf16(5)..OffsetUtf16(6),
351 OffsetUtf16(9)..OffsetUtf16(10)
352 ])
353 );
354
355 // Finalize IME composition with multiple cursors.
356 editor.replace_text_in_range(Some(9..10), "2", window, cx);
357 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
358 assert_eq!(editor.marked_text_ranges(cx), None);
359
360 editor
361 });
362}
363
364#[gpui::test]
365fn test_selection_with_mouse(cx: &mut TestAppContext) {
366 init_test(cx, |_| {});
367
368 let editor = cx.add_window(|window, cx| {
369 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
370 build_editor(buffer, window, cx)
371 });
372
373 _ = editor.update(cx, |editor, window, cx| {
374 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
375 });
376 assert_eq!(
377 editor
378 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
379 .unwrap(),
380 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
381 );
382
383 _ = editor.update(cx, |editor, window, cx| {
384 editor.update_selection(
385 DisplayPoint::new(DisplayRow(3), 3),
386 0,
387 gpui::Point::<f32>::default(),
388 window,
389 cx,
390 );
391 });
392
393 assert_eq!(
394 editor
395 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
396 .unwrap(),
397 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
398 );
399
400 _ = editor.update(cx, |editor, window, cx| {
401 editor.update_selection(
402 DisplayPoint::new(DisplayRow(1), 1),
403 0,
404 gpui::Point::<f32>::default(),
405 window,
406 cx,
407 );
408 });
409
410 assert_eq!(
411 editor
412 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
413 .unwrap(),
414 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
415 );
416
417 _ = editor.update(cx, |editor, window, cx| {
418 editor.end_selection(window, cx);
419 editor.update_selection(
420 DisplayPoint::new(DisplayRow(3), 3),
421 0,
422 gpui::Point::<f32>::default(),
423 window,
424 cx,
425 );
426 });
427
428 assert_eq!(
429 editor
430 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
431 .unwrap(),
432 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
433 );
434
435 _ = editor.update(cx, |editor, window, cx| {
436 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
437 editor.update_selection(
438 DisplayPoint::new(DisplayRow(0), 0),
439 0,
440 gpui::Point::<f32>::default(),
441 window,
442 cx,
443 );
444 });
445
446 assert_eq!(
447 editor
448 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
449 .unwrap(),
450 [
451 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
452 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
453 ]
454 );
455
456 _ = editor.update(cx, |editor, window, cx| {
457 editor.end_selection(window, cx);
458 });
459
460 assert_eq!(
461 editor
462 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
463 .unwrap(),
464 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
465 );
466}
467
468#[gpui::test]
469fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
470 init_test(cx, |_| {});
471
472 let editor = cx.add_window(|window, cx| {
473 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
474 build_editor(buffer, window, cx)
475 });
476
477 _ = editor.update(cx, |editor, window, cx| {
478 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
479 });
480
481 _ = editor.update(cx, |editor, window, cx| {
482 editor.end_selection(window, cx);
483 });
484
485 _ = editor.update(cx, |editor, window, cx| {
486 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
487 });
488
489 _ = editor.update(cx, |editor, window, cx| {
490 editor.end_selection(window, cx);
491 });
492
493 assert_eq!(
494 editor
495 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
496 .unwrap(),
497 [
498 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
499 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
500 ]
501 );
502
503 _ = editor.update(cx, |editor, window, cx| {
504 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
505 });
506
507 _ = editor.update(cx, |editor, window, cx| {
508 editor.end_selection(window, cx);
509 });
510
511 assert_eq!(
512 editor
513 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
514 .unwrap(),
515 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
516 );
517}
518
519#[gpui::test]
520fn test_canceling_pending_selection(cx: &mut TestAppContext) {
521 init_test(cx, |_| {});
522
523 let editor = cx.add_window(|window, cx| {
524 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
525 build_editor(buffer, window, cx)
526 });
527
528 _ = editor.update(cx, |editor, window, cx| {
529 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
530 assert_eq!(
531 editor.selections.display_ranges(cx),
532 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
533 );
534 });
535
536 _ = editor.update(cx, |editor, window, cx| {
537 editor.update_selection(
538 DisplayPoint::new(DisplayRow(3), 3),
539 0,
540 gpui::Point::<f32>::default(),
541 window,
542 cx,
543 );
544 assert_eq!(
545 editor.selections.display_ranges(cx),
546 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
547 );
548 });
549
550 _ = editor.update(cx, |editor, window, cx| {
551 editor.cancel(&Cancel, window, cx);
552 editor.update_selection(
553 DisplayPoint::new(DisplayRow(1), 1),
554 0,
555 gpui::Point::<f32>::default(),
556 window,
557 cx,
558 );
559 assert_eq!(
560 editor.selections.display_ranges(cx),
561 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
562 );
563 });
564}
565
566#[gpui::test]
567fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
568 init_test(cx, |_| {});
569
570 let editor = cx.add_window(|window, cx| {
571 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
572 build_editor(buffer, window, cx)
573 });
574
575 _ = editor.update(cx, |editor, window, cx| {
576 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
577 assert_eq!(
578 editor.selections.display_ranges(cx),
579 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
580 );
581
582 editor.move_down(&Default::default(), window, cx);
583 assert_eq!(
584 editor.selections.display_ranges(cx),
585 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
586 );
587
588 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
589 assert_eq!(
590 editor.selections.display_ranges(cx),
591 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
592 );
593
594 editor.move_up(&Default::default(), window, cx);
595 assert_eq!(
596 editor.selections.display_ranges(cx),
597 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
598 );
599 });
600}
601
602#[gpui::test]
603fn test_clone(cx: &mut TestAppContext) {
604 init_test(cx, |_| {});
605
606 let (text, selection_ranges) = marked_text_ranges(
607 indoc! {"
608 one
609 two
610 threeˇ
611 four
612 fiveˇ
613 "},
614 true,
615 );
616
617 let editor = cx.add_window(|window, cx| {
618 let buffer = MultiBuffer::build_simple(&text, cx);
619 build_editor(buffer, window, cx)
620 });
621
622 _ = editor.update(cx, |editor, window, cx| {
623 editor.change_selections(None, window, cx, |s| {
624 s.select_ranges(selection_ranges.clone())
625 });
626 editor.fold_creases(
627 vec![
628 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
629 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
630 ],
631 true,
632 window,
633 cx,
634 );
635 });
636
637 let cloned_editor = editor
638 .update(cx, |editor, _, cx| {
639 cx.open_window(Default::default(), |window, cx| {
640 cx.new(|cx| editor.clone(window, cx))
641 })
642 })
643 .unwrap()
644 .unwrap();
645
646 let snapshot = editor
647 .update(cx, |e, window, cx| e.snapshot(window, cx))
648 .unwrap();
649 let cloned_snapshot = cloned_editor
650 .update(cx, |e, window, cx| e.snapshot(window, cx))
651 .unwrap();
652
653 assert_eq!(
654 cloned_editor
655 .update(cx, |e, _, cx| e.display_text(cx))
656 .unwrap(),
657 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
658 );
659 assert_eq!(
660 cloned_snapshot
661 .folds_in_range(0..text.len())
662 .collect::<Vec<_>>(),
663 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
664 );
665 assert_set_eq!(
666 cloned_editor
667 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
668 .unwrap(),
669 editor
670 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
671 .unwrap()
672 );
673 assert_set_eq!(
674 cloned_editor
675 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
676 .unwrap(),
677 editor
678 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
679 .unwrap()
680 );
681}
682
683#[gpui::test]
684async fn test_navigation_history(cx: &mut TestAppContext) {
685 init_test(cx, |_| {});
686
687 use workspace::item::Item;
688
689 let fs = FakeFs::new(cx.executor());
690 let project = Project::test(fs, [], cx).await;
691 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
692 let pane = workspace
693 .update(cx, |workspace, _, _| workspace.active_pane().clone())
694 .unwrap();
695
696 _ = workspace.update(cx, |_v, window, cx| {
697 cx.new(|cx| {
698 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
699 let mut editor = build_editor(buffer.clone(), window, cx);
700 let handle = cx.entity();
701 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
702
703 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
704 editor.nav_history.as_mut().unwrap().pop_backward(cx)
705 }
706
707 // Move the cursor a small distance.
708 // Nothing is added to the navigation history.
709 editor.change_selections(None, window, cx, |s| {
710 s.select_display_ranges([
711 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
712 ])
713 });
714 editor.change_selections(None, window, cx, |s| {
715 s.select_display_ranges([
716 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
717 ])
718 });
719 assert!(pop_history(&mut editor, cx).is_none());
720
721 // Move the cursor a large distance.
722 // The history can jump back to the previous position.
723 editor.change_selections(None, window, cx, |s| {
724 s.select_display_ranges([
725 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
726 ])
727 });
728 let nav_entry = pop_history(&mut editor, cx).unwrap();
729 editor.navigate(nav_entry.data.unwrap(), window, cx);
730 assert_eq!(nav_entry.item.id(), cx.entity_id());
731 assert_eq!(
732 editor.selections.display_ranges(cx),
733 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
734 );
735 assert!(pop_history(&mut editor, cx).is_none());
736
737 // Move the cursor a small distance via the mouse.
738 // Nothing is added to the navigation history.
739 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
740 editor.end_selection(window, cx);
741 assert_eq!(
742 editor.selections.display_ranges(cx),
743 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
744 );
745 assert!(pop_history(&mut editor, cx).is_none());
746
747 // Move the cursor a large distance via the mouse.
748 // The history can jump back to the previous position.
749 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
750 editor.end_selection(window, cx);
751 assert_eq!(
752 editor.selections.display_ranges(cx),
753 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
754 );
755 let nav_entry = pop_history(&mut editor, cx).unwrap();
756 editor.navigate(nav_entry.data.unwrap(), window, cx);
757 assert_eq!(nav_entry.item.id(), cx.entity_id());
758 assert_eq!(
759 editor.selections.display_ranges(cx),
760 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
761 );
762 assert!(pop_history(&mut editor, cx).is_none());
763
764 // Set scroll position to check later
765 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
766 let original_scroll_position = editor.scroll_manager.anchor();
767
768 // Jump to the end of the document and adjust scroll
769 editor.move_to_end(&MoveToEnd, window, cx);
770 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
771 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
772
773 let nav_entry = pop_history(&mut editor, cx).unwrap();
774 editor.navigate(nav_entry.data.unwrap(), window, cx);
775 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
776
777 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
778 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
779 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
780 let invalid_point = Point::new(9999, 0);
781 editor.navigate(
782 Box::new(NavigationData {
783 cursor_anchor: invalid_anchor,
784 cursor_position: invalid_point,
785 scroll_anchor: ScrollAnchor {
786 anchor: invalid_anchor,
787 offset: Default::default(),
788 },
789 scroll_top_row: invalid_point.row,
790 }),
791 window,
792 cx,
793 );
794 assert_eq!(
795 editor.selections.display_ranges(cx),
796 &[editor.max_point(cx)..editor.max_point(cx)]
797 );
798 assert_eq!(
799 editor.scroll_position(cx),
800 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
801 );
802
803 editor
804 })
805 });
806}
807
808#[gpui::test]
809fn test_cancel(cx: &mut TestAppContext) {
810 init_test(cx, |_| {});
811
812 let editor = cx.add_window(|window, cx| {
813 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
814 build_editor(buffer, window, cx)
815 });
816
817 _ = editor.update(cx, |editor, window, cx| {
818 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
819 editor.update_selection(
820 DisplayPoint::new(DisplayRow(1), 1),
821 0,
822 gpui::Point::<f32>::default(),
823 window,
824 cx,
825 );
826 editor.end_selection(window, cx);
827
828 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
829 editor.update_selection(
830 DisplayPoint::new(DisplayRow(0), 3),
831 0,
832 gpui::Point::<f32>::default(),
833 window,
834 cx,
835 );
836 editor.end_selection(window, cx);
837 assert_eq!(
838 editor.selections.display_ranges(cx),
839 [
840 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
841 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
842 ]
843 );
844 });
845
846 _ = editor.update(cx, |editor, window, cx| {
847 editor.cancel(&Cancel, window, cx);
848 assert_eq!(
849 editor.selections.display_ranges(cx),
850 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
851 );
852 });
853
854 _ = editor.update(cx, |editor, window, cx| {
855 editor.cancel(&Cancel, window, cx);
856 assert_eq!(
857 editor.selections.display_ranges(cx),
858 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
859 );
860 });
861}
862
863#[gpui::test]
864fn test_fold_action(cx: &mut TestAppContext) {
865 init_test(cx, |_| {});
866
867 let editor = cx.add_window(|window, cx| {
868 let buffer = MultiBuffer::build_simple(
869 &"
870 impl Foo {
871 // Hello!
872
873 fn a() {
874 1
875 }
876
877 fn b() {
878 2
879 }
880
881 fn c() {
882 3
883 }
884 }
885 "
886 .unindent(),
887 cx,
888 );
889 build_editor(buffer.clone(), window, cx)
890 });
891
892 _ = editor.update(cx, |editor, window, cx| {
893 editor.change_selections(None, window, cx, |s| {
894 s.select_display_ranges([
895 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
896 ]);
897 });
898 editor.fold(&Fold, window, cx);
899 assert_eq!(
900 editor.display_text(cx),
901 "
902 impl Foo {
903 // Hello!
904
905 fn a() {
906 1
907 }
908
909 fn b() {⋯
910 }
911
912 fn c() {⋯
913 }
914 }
915 "
916 .unindent(),
917 );
918
919 editor.fold(&Fold, window, cx);
920 assert_eq!(
921 editor.display_text(cx),
922 "
923 impl Foo {⋯
924 }
925 "
926 .unindent(),
927 );
928
929 editor.unfold_lines(&UnfoldLines, window, cx);
930 assert_eq!(
931 editor.display_text(cx),
932 "
933 impl Foo {
934 // Hello!
935
936 fn a() {
937 1
938 }
939
940 fn b() {⋯
941 }
942
943 fn c() {⋯
944 }
945 }
946 "
947 .unindent(),
948 );
949
950 editor.unfold_lines(&UnfoldLines, window, cx);
951 assert_eq!(
952 editor.display_text(cx),
953 editor.buffer.read(cx).read(cx).text()
954 );
955 });
956}
957
958#[gpui::test]
959fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
960 init_test(cx, |_| {});
961
962 let editor = cx.add_window(|window, cx| {
963 let buffer = MultiBuffer::build_simple(
964 &"
965 class Foo:
966 # Hello!
967
968 def a():
969 print(1)
970
971 def b():
972 print(2)
973
974 def c():
975 print(3)
976 "
977 .unindent(),
978 cx,
979 );
980 build_editor(buffer.clone(), window, cx)
981 });
982
983 _ = editor.update(cx, |editor, window, cx| {
984 editor.change_selections(None, window, cx, |s| {
985 s.select_display_ranges([
986 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
987 ]);
988 });
989 editor.fold(&Fold, window, cx);
990 assert_eq!(
991 editor.display_text(cx),
992 "
993 class Foo:
994 # Hello!
995
996 def a():
997 print(1)
998
999 def b():⋯
1000
1001 def c():⋯
1002 "
1003 .unindent(),
1004 );
1005
1006 editor.fold(&Fold, window, cx);
1007 assert_eq!(
1008 editor.display_text(cx),
1009 "
1010 class Foo:⋯
1011 "
1012 .unindent(),
1013 );
1014
1015 editor.unfold_lines(&UnfoldLines, window, cx);
1016 assert_eq!(
1017 editor.display_text(cx),
1018 "
1019 class Foo:
1020 # Hello!
1021
1022 def a():
1023 print(1)
1024
1025 def b():⋯
1026
1027 def c():⋯
1028 "
1029 .unindent(),
1030 );
1031
1032 editor.unfold_lines(&UnfoldLines, window, cx);
1033 assert_eq!(
1034 editor.display_text(cx),
1035 editor.buffer.read(cx).read(cx).text()
1036 );
1037 });
1038}
1039
1040#[gpui::test]
1041fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1042 init_test(cx, |_| {});
1043
1044 let editor = cx.add_window(|window, cx| {
1045 let buffer = MultiBuffer::build_simple(
1046 &"
1047 class Foo:
1048 # Hello!
1049
1050 def a():
1051 print(1)
1052
1053 def b():
1054 print(2)
1055
1056
1057 def c():
1058 print(3)
1059
1060
1061 "
1062 .unindent(),
1063 cx,
1064 );
1065 build_editor(buffer.clone(), window, cx)
1066 });
1067
1068 _ = editor.update(cx, |editor, window, cx| {
1069 editor.change_selections(None, window, cx, |s| {
1070 s.select_display_ranges([
1071 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1072 ]);
1073 });
1074 editor.fold(&Fold, window, cx);
1075 assert_eq!(
1076 editor.display_text(cx),
1077 "
1078 class Foo:
1079 # Hello!
1080
1081 def a():
1082 print(1)
1083
1084 def b():⋯
1085
1086
1087 def c():⋯
1088
1089
1090 "
1091 .unindent(),
1092 );
1093
1094 editor.fold(&Fold, window, cx);
1095 assert_eq!(
1096 editor.display_text(cx),
1097 "
1098 class Foo:⋯
1099
1100
1101 "
1102 .unindent(),
1103 );
1104
1105 editor.unfold_lines(&UnfoldLines, window, cx);
1106 assert_eq!(
1107 editor.display_text(cx),
1108 "
1109 class Foo:
1110 # Hello!
1111
1112 def a():
1113 print(1)
1114
1115 def b():⋯
1116
1117
1118 def c():⋯
1119
1120
1121 "
1122 .unindent(),
1123 );
1124
1125 editor.unfold_lines(&UnfoldLines, window, cx);
1126 assert_eq!(
1127 editor.display_text(cx),
1128 editor.buffer.read(cx).read(cx).text()
1129 );
1130 });
1131}
1132
1133#[gpui::test]
1134fn test_fold_at_level(cx: &mut TestAppContext) {
1135 init_test(cx, |_| {});
1136
1137 let editor = cx.add_window(|window, cx| {
1138 let buffer = MultiBuffer::build_simple(
1139 &"
1140 class Foo:
1141 # Hello!
1142
1143 def a():
1144 print(1)
1145
1146 def b():
1147 print(2)
1148
1149
1150 class Bar:
1151 # World!
1152
1153 def a():
1154 print(1)
1155
1156 def b():
1157 print(2)
1158
1159
1160 "
1161 .unindent(),
1162 cx,
1163 );
1164 build_editor(buffer.clone(), window, cx)
1165 });
1166
1167 _ = editor.update(cx, |editor, window, cx| {
1168 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1169 assert_eq!(
1170 editor.display_text(cx),
1171 "
1172 class Foo:
1173 # Hello!
1174
1175 def a():⋯
1176
1177 def b():⋯
1178
1179
1180 class Bar:
1181 # World!
1182
1183 def a():⋯
1184
1185 def b():⋯
1186
1187
1188 "
1189 .unindent(),
1190 );
1191
1192 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1193 assert_eq!(
1194 editor.display_text(cx),
1195 "
1196 class Foo:⋯
1197
1198
1199 class Bar:⋯
1200
1201
1202 "
1203 .unindent(),
1204 );
1205
1206 editor.unfold_all(&UnfoldAll, window, cx);
1207 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1208 assert_eq!(
1209 editor.display_text(cx),
1210 "
1211 class Foo:
1212 # Hello!
1213
1214 def a():
1215 print(1)
1216
1217 def b():
1218 print(2)
1219
1220
1221 class Bar:
1222 # World!
1223
1224 def a():
1225 print(1)
1226
1227 def b():
1228 print(2)
1229
1230
1231 "
1232 .unindent(),
1233 );
1234
1235 assert_eq!(
1236 editor.display_text(cx),
1237 editor.buffer.read(cx).read(cx).text()
1238 );
1239 });
1240}
1241
1242#[gpui::test]
1243fn test_move_cursor(cx: &mut TestAppContext) {
1244 init_test(cx, |_| {});
1245
1246 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1247 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1248
1249 buffer.update(cx, |buffer, cx| {
1250 buffer.edit(
1251 vec![
1252 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1253 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1254 ],
1255 None,
1256 cx,
1257 );
1258 });
1259 _ = editor.update(cx, |editor, window, cx| {
1260 assert_eq!(
1261 editor.selections.display_ranges(cx),
1262 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1263 );
1264
1265 editor.move_down(&MoveDown, window, cx);
1266 assert_eq!(
1267 editor.selections.display_ranges(cx),
1268 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1269 );
1270
1271 editor.move_right(&MoveRight, window, cx);
1272 assert_eq!(
1273 editor.selections.display_ranges(cx),
1274 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1275 );
1276
1277 editor.move_left(&MoveLeft, window, cx);
1278 assert_eq!(
1279 editor.selections.display_ranges(cx),
1280 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1281 );
1282
1283 editor.move_up(&MoveUp, window, cx);
1284 assert_eq!(
1285 editor.selections.display_ranges(cx),
1286 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1287 );
1288
1289 editor.move_to_end(&MoveToEnd, window, cx);
1290 assert_eq!(
1291 editor.selections.display_ranges(cx),
1292 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1293 );
1294
1295 editor.move_to_beginning(&MoveToBeginning, window, cx);
1296 assert_eq!(
1297 editor.selections.display_ranges(cx),
1298 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1299 );
1300
1301 editor.change_selections(None, window, cx, |s| {
1302 s.select_display_ranges([
1303 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1304 ]);
1305 });
1306 editor.select_to_beginning(&SelectToBeginning, window, cx);
1307 assert_eq!(
1308 editor.selections.display_ranges(cx),
1309 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1310 );
1311
1312 editor.select_to_end(&SelectToEnd, window, cx);
1313 assert_eq!(
1314 editor.selections.display_ranges(cx),
1315 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1316 );
1317 });
1318}
1319
1320#[gpui::test]
1321fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1322 init_test(cx, |_| {});
1323
1324 let editor = cx.add_window(|window, cx| {
1325 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1326 build_editor(buffer.clone(), window, cx)
1327 });
1328
1329 assert_eq!('🟥'.len_utf8(), 4);
1330 assert_eq!('α'.len_utf8(), 2);
1331
1332 _ = editor.update(cx, |editor, window, cx| {
1333 editor.fold_creases(
1334 vec![
1335 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1336 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1337 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1338 ],
1339 true,
1340 window,
1341 cx,
1342 );
1343 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1344
1345 editor.move_right(&MoveRight, window, cx);
1346 assert_eq!(
1347 editor.selections.display_ranges(cx),
1348 &[empty_range(0, "🟥".len())]
1349 );
1350 editor.move_right(&MoveRight, window, cx);
1351 assert_eq!(
1352 editor.selections.display_ranges(cx),
1353 &[empty_range(0, "🟥🟧".len())]
1354 );
1355 editor.move_right(&MoveRight, window, cx);
1356 assert_eq!(
1357 editor.selections.display_ranges(cx),
1358 &[empty_range(0, "🟥🟧⋯".len())]
1359 );
1360
1361 editor.move_down(&MoveDown, window, cx);
1362 assert_eq!(
1363 editor.selections.display_ranges(cx),
1364 &[empty_range(1, "ab⋯e".len())]
1365 );
1366 editor.move_left(&MoveLeft, window, cx);
1367 assert_eq!(
1368 editor.selections.display_ranges(cx),
1369 &[empty_range(1, "ab⋯".len())]
1370 );
1371 editor.move_left(&MoveLeft, window, cx);
1372 assert_eq!(
1373 editor.selections.display_ranges(cx),
1374 &[empty_range(1, "ab".len())]
1375 );
1376 editor.move_left(&MoveLeft, window, cx);
1377 assert_eq!(
1378 editor.selections.display_ranges(cx),
1379 &[empty_range(1, "a".len())]
1380 );
1381
1382 editor.move_down(&MoveDown, window, cx);
1383 assert_eq!(
1384 editor.selections.display_ranges(cx),
1385 &[empty_range(2, "α".len())]
1386 );
1387 editor.move_right(&MoveRight, window, cx);
1388 assert_eq!(
1389 editor.selections.display_ranges(cx),
1390 &[empty_range(2, "αβ".len())]
1391 );
1392 editor.move_right(&MoveRight, window, cx);
1393 assert_eq!(
1394 editor.selections.display_ranges(cx),
1395 &[empty_range(2, "αβ⋯".len())]
1396 );
1397 editor.move_right(&MoveRight, window, cx);
1398 assert_eq!(
1399 editor.selections.display_ranges(cx),
1400 &[empty_range(2, "αβ⋯ε".len())]
1401 );
1402
1403 editor.move_up(&MoveUp, window, cx);
1404 assert_eq!(
1405 editor.selections.display_ranges(cx),
1406 &[empty_range(1, "ab⋯e".len())]
1407 );
1408 editor.move_down(&MoveDown, window, cx);
1409 assert_eq!(
1410 editor.selections.display_ranges(cx),
1411 &[empty_range(2, "αβ⋯ε".len())]
1412 );
1413 editor.move_up(&MoveUp, window, cx);
1414 assert_eq!(
1415 editor.selections.display_ranges(cx),
1416 &[empty_range(1, "ab⋯e".len())]
1417 );
1418
1419 editor.move_up(&MoveUp, window, cx);
1420 assert_eq!(
1421 editor.selections.display_ranges(cx),
1422 &[empty_range(0, "🟥🟧".len())]
1423 );
1424 editor.move_left(&MoveLeft, window, cx);
1425 assert_eq!(
1426 editor.selections.display_ranges(cx),
1427 &[empty_range(0, "🟥".len())]
1428 );
1429 editor.move_left(&MoveLeft, window, cx);
1430 assert_eq!(
1431 editor.selections.display_ranges(cx),
1432 &[empty_range(0, "".len())]
1433 );
1434 });
1435}
1436
1437#[gpui::test]
1438fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1439 init_test(cx, |_| {});
1440
1441 let editor = cx.add_window(|window, cx| {
1442 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1443 build_editor(buffer.clone(), window, cx)
1444 });
1445 _ = editor.update(cx, |editor, window, cx| {
1446 editor.change_selections(None, window, cx, |s| {
1447 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1448 });
1449
1450 // moving above start of document should move selection to start of document,
1451 // but the next move down should still be at the original goal_x
1452 editor.move_up(&MoveUp, window, cx);
1453 assert_eq!(
1454 editor.selections.display_ranges(cx),
1455 &[empty_range(0, "".len())]
1456 );
1457
1458 editor.move_down(&MoveDown, window, cx);
1459 assert_eq!(
1460 editor.selections.display_ranges(cx),
1461 &[empty_range(1, "abcd".len())]
1462 );
1463
1464 editor.move_down(&MoveDown, window, cx);
1465 assert_eq!(
1466 editor.selections.display_ranges(cx),
1467 &[empty_range(2, "αβγ".len())]
1468 );
1469
1470 editor.move_down(&MoveDown, window, cx);
1471 assert_eq!(
1472 editor.selections.display_ranges(cx),
1473 &[empty_range(3, "abcd".len())]
1474 );
1475
1476 editor.move_down(&MoveDown, window, cx);
1477 assert_eq!(
1478 editor.selections.display_ranges(cx),
1479 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1480 );
1481
1482 // moving past end of document should not change goal_x
1483 editor.move_down(&MoveDown, window, cx);
1484 assert_eq!(
1485 editor.selections.display_ranges(cx),
1486 &[empty_range(5, "".len())]
1487 );
1488
1489 editor.move_down(&MoveDown, window, cx);
1490 assert_eq!(
1491 editor.selections.display_ranges(cx),
1492 &[empty_range(5, "".len())]
1493 );
1494
1495 editor.move_up(&MoveUp, window, cx);
1496 assert_eq!(
1497 editor.selections.display_ranges(cx),
1498 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1499 );
1500
1501 editor.move_up(&MoveUp, window, cx);
1502 assert_eq!(
1503 editor.selections.display_ranges(cx),
1504 &[empty_range(3, "abcd".len())]
1505 );
1506
1507 editor.move_up(&MoveUp, window, cx);
1508 assert_eq!(
1509 editor.selections.display_ranges(cx),
1510 &[empty_range(2, "αβγ".len())]
1511 );
1512 });
1513}
1514
1515#[gpui::test]
1516fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1517 init_test(cx, |_| {});
1518 let move_to_beg = MoveToBeginningOfLine {
1519 stop_at_soft_wraps: true,
1520 stop_at_indent: true,
1521 };
1522
1523 let delete_to_beg = DeleteToBeginningOfLine {
1524 stop_at_indent: false,
1525 };
1526
1527 let move_to_end = MoveToEndOfLine {
1528 stop_at_soft_wraps: true,
1529 };
1530
1531 let editor = cx.add_window(|window, cx| {
1532 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1533 build_editor(buffer, window, cx)
1534 });
1535 _ = editor.update(cx, |editor, window, cx| {
1536 editor.change_selections(None, window, cx, |s| {
1537 s.select_display_ranges([
1538 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1539 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1540 ]);
1541 });
1542 });
1543
1544 _ = editor.update(cx, |editor, window, cx| {
1545 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1546 assert_eq!(
1547 editor.selections.display_ranges(cx),
1548 &[
1549 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1550 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1551 ]
1552 );
1553 });
1554
1555 _ = editor.update(cx, |editor, window, cx| {
1556 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1557 assert_eq!(
1558 editor.selections.display_ranges(cx),
1559 &[
1560 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1561 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1562 ]
1563 );
1564 });
1565
1566 _ = editor.update(cx, |editor, window, cx| {
1567 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1568 assert_eq!(
1569 editor.selections.display_ranges(cx),
1570 &[
1571 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1572 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1573 ]
1574 );
1575 });
1576
1577 _ = editor.update(cx, |editor, window, cx| {
1578 editor.move_to_end_of_line(&move_to_end, window, cx);
1579 assert_eq!(
1580 editor.selections.display_ranges(cx),
1581 &[
1582 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1583 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1584 ]
1585 );
1586 });
1587
1588 // Moving to the end of line again is a no-op.
1589 _ = editor.update(cx, |editor, window, cx| {
1590 editor.move_to_end_of_line(&move_to_end, window, cx);
1591 assert_eq!(
1592 editor.selections.display_ranges(cx),
1593 &[
1594 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1595 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1596 ]
1597 );
1598 });
1599
1600 _ = editor.update(cx, |editor, window, cx| {
1601 editor.move_left(&MoveLeft, window, cx);
1602 editor.select_to_beginning_of_line(
1603 &SelectToBeginningOfLine {
1604 stop_at_soft_wraps: true,
1605 stop_at_indent: true,
1606 },
1607 window,
1608 cx,
1609 );
1610 assert_eq!(
1611 editor.selections.display_ranges(cx),
1612 &[
1613 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1614 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1615 ]
1616 );
1617 });
1618
1619 _ = editor.update(cx, |editor, window, cx| {
1620 editor.select_to_beginning_of_line(
1621 &SelectToBeginningOfLine {
1622 stop_at_soft_wraps: true,
1623 stop_at_indent: true,
1624 },
1625 window,
1626 cx,
1627 );
1628 assert_eq!(
1629 editor.selections.display_ranges(cx),
1630 &[
1631 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1632 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1633 ]
1634 );
1635 });
1636
1637 _ = editor.update(cx, |editor, window, cx| {
1638 editor.select_to_beginning_of_line(
1639 &SelectToBeginningOfLine {
1640 stop_at_soft_wraps: true,
1641 stop_at_indent: true,
1642 },
1643 window,
1644 cx,
1645 );
1646 assert_eq!(
1647 editor.selections.display_ranges(cx),
1648 &[
1649 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1650 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1651 ]
1652 );
1653 });
1654
1655 _ = editor.update(cx, |editor, window, cx| {
1656 editor.select_to_end_of_line(
1657 &SelectToEndOfLine {
1658 stop_at_soft_wraps: true,
1659 },
1660 window,
1661 cx,
1662 );
1663 assert_eq!(
1664 editor.selections.display_ranges(cx),
1665 &[
1666 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1667 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1668 ]
1669 );
1670 });
1671
1672 _ = editor.update(cx, |editor, window, cx| {
1673 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1674 assert_eq!(editor.display_text(cx), "ab\n de");
1675 assert_eq!(
1676 editor.selections.display_ranges(cx),
1677 &[
1678 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1679 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1680 ]
1681 );
1682 });
1683
1684 _ = editor.update(cx, |editor, window, cx| {
1685 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1686 assert_eq!(editor.display_text(cx), "\n");
1687 assert_eq!(
1688 editor.selections.display_ranges(cx),
1689 &[
1690 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1691 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1692 ]
1693 );
1694 });
1695}
1696
1697#[gpui::test]
1698fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1699 init_test(cx, |_| {});
1700 let move_to_beg = MoveToBeginningOfLine {
1701 stop_at_soft_wraps: false,
1702 stop_at_indent: false,
1703 };
1704
1705 let move_to_end = MoveToEndOfLine {
1706 stop_at_soft_wraps: false,
1707 };
1708
1709 let editor = cx.add_window(|window, cx| {
1710 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1711 build_editor(buffer, window, cx)
1712 });
1713
1714 _ = editor.update(cx, |editor, window, cx| {
1715 editor.set_wrap_width(Some(140.0.into()), cx);
1716
1717 // We expect the following lines after wrapping
1718 // ```
1719 // thequickbrownfox
1720 // jumpedoverthelazydo
1721 // gs
1722 // ```
1723 // The final `gs` was soft-wrapped onto a new line.
1724 assert_eq!(
1725 "thequickbrownfox\njumpedoverthelaz\nydogs",
1726 editor.display_text(cx),
1727 );
1728
1729 // First, let's assert behavior on the first line, that was not soft-wrapped.
1730 // Start the cursor at the `k` on the first line
1731 editor.change_selections(None, window, cx, |s| {
1732 s.select_display_ranges([
1733 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1734 ]);
1735 });
1736
1737 // Moving to the beginning of the line should put us at the beginning of the line.
1738 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1739 assert_eq!(
1740 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1741 editor.selections.display_ranges(cx)
1742 );
1743
1744 // Moving to the end of the line should put us at the end of the line.
1745 editor.move_to_end_of_line(&move_to_end, window, cx);
1746 assert_eq!(
1747 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1748 editor.selections.display_ranges(cx)
1749 );
1750
1751 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1752 // Start the cursor at the last line (`y` that was wrapped to a new line)
1753 editor.change_selections(None, window, cx, |s| {
1754 s.select_display_ranges([
1755 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1756 ]);
1757 });
1758
1759 // Moving to the beginning of the line should put us at the start of the second line of
1760 // display text, i.e., the `j`.
1761 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1762 assert_eq!(
1763 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1764 editor.selections.display_ranges(cx)
1765 );
1766
1767 // Moving to the beginning of the line again should be a no-op.
1768 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1769 assert_eq!(
1770 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1771 editor.selections.display_ranges(cx)
1772 );
1773
1774 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1775 // next display line.
1776 editor.move_to_end_of_line(&move_to_end, window, cx);
1777 assert_eq!(
1778 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1779 editor.selections.display_ranges(cx)
1780 );
1781
1782 // Moving to the end of the line again should be a no-op.
1783 editor.move_to_end_of_line(&move_to_end, window, cx);
1784 assert_eq!(
1785 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1786 editor.selections.display_ranges(cx)
1787 );
1788 });
1789}
1790
1791#[gpui::test]
1792fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1793 init_test(cx, |_| {});
1794
1795 let move_to_beg = MoveToBeginningOfLine {
1796 stop_at_soft_wraps: true,
1797 stop_at_indent: true,
1798 };
1799
1800 let select_to_beg = SelectToBeginningOfLine {
1801 stop_at_soft_wraps: true,
1802 stop_at_indent: true,
1803 };
1804
1805 let delete_to_beg = DeleteToBeginningOfLine {
1806 stop_at_indent: true,
1807 };
1808
1809 let move_to_end = MoveToEndOfLine {
1810 stop_at_soft_wraps: false,
1811 };
1812
1813 let editor = cx.add_window(|window, cx| {
1814 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1815 build_editor(buffer, window, cx)
1816 });
1817
1818 _ = editor.update(cx, |editor, window, cx| {
1819 editor.change_selections(None, window, cx, |s| {
1820 s.select_display_ranges([
1821 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1822 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1823 ]);
1824 });
1825
1826 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1827 // and the second cursor at the first non-whitespace character in the line.
1828 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1829 assert_eq!(
1830 editor.selections.display_ranges(cx),
1831 &[
1832 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1833 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1834 ]
1835 );
1836
1837 // Moving to the beginning of the line again should be a no-op for the first cursor,
1838 // and should move the second cursor to the beginning of the line.
1839 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1840 assert_eq!(
1841 editor.selections.display_ranges(cx),
1842 &[
1843 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1844 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1845 ]
1846 );
1847
1848 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1849 // and should move the second cursor back to the first non-whitespace character in the line.
1850 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1851 assert_eq!(
1852 editor.selections.display_ranges(cx),
1853 &[
1854 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1855 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1856 ]
1857 );
1858
1859 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1860 // and to the first non-whitespace character in the line for the second cursor.
1861 editor.move_to_end_of_line(&move_to_end, window, cx);
1862 editor.move_left(&MoveLeft, window, cx);
1863 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1864 assert_eq!(
1865 editor.selections.display_ranges(cx),
1866 &[
1867 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1868 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1869 ]
1870 );
1871
1872 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1873 // and should select to the beginning of the line for the second cursor.
1874 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1875 assert_eq!(
1876 editor.selections.display_ranges(cx),
1877 &[
1878 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1879 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1880 ]
1881 );
1882
1883 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1884 // and should delete to the first non-whitespace character in the line for the second cursor.
1885 editor.move_to_end_of_line(&move_to_end, window, cx);
1886 editor.move_left(&MoveLeft, window, cx);
1887 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1888 assert_eq!(editor.text(cx), "c\n f");
1889 });
1890}
1891
1892#[gpui::test]
1893fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1894 init_test(cx, |_| {});
1895
1896 let editor = cx.add_window(|window, cx| {
1897 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1898 build_editor(buffer, window, cx)
1899 });
1900 _ = editor.update(cx, |editor, window, cx| {
1901 editor.change_selections(None, window, cx, |s| {
1902 s.select_display_ranges([
1903 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1904 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1905 ])
1906 });
1907
1908 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1909 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
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_next_word_end(&MoveToNextWordEnd, 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_right(&MoveRight, window, cx);
1933 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1934 assert_selection_ranges(
1935 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1936 editor,
1937 cx,
1938 );
1939
1940 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1941 assert_selection_ranges(
1942 "use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}",
1943 editor,
1944 cx,
1945 );
1946
1947 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1948 assert_selection_ranges(
1949 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1950 editor,
1951 cx,
1952 );
1953 });
1954}
1955
1956#[gpui::test]
1957fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1958 init_test(cx, |_| {});
1959
1960 let editor = cx.add_window(|window, cx| {
1961 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1962 build_editor(buffer, window, cx)
1963 });
1964
1965 _ = editor.update(cx, |editor, window, cx| {
1966 editor.set_wrap_width(Some(140.0.into()), cx);
1967 assert_eq!(
1968 editor.display_text(cx),
1969 "use one::{\n two::three::\n four::five\n};"
1970 );
1971
1972 editor.change_selections(None, window, cx, |s| {
1973 s.select_display_ranges([
1974 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1975 ]);
1976 });
1977
1978 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1979 assert_eq!(
1980 editor.selections.display_ranges(cx),
1981 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1982 );
1983
1984 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1985 assert_eq!(
1986 editor.selections.display_ranges(cx),
1987 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1988 );
1989
1990 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1991 assert_eq!(
1992 editor.selections.display_ranges(cx),
1993 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1994 );
1995
1996 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1997 assert_eq!(
1998 editor.selections.display_ranges(cx),
1999 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2000 );
2001
2002 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2003 assert_eq!(
2004 editor.selections.display_ranges(cx),
2005 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2006 );
2007
2008 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2009 assert_eq!(
2010 editor.selections.display_ranges(cx),
2011 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2012 );
2013 });
2014}
2015
2016#[gpui::test]
2017async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2018 init_test(cx, |_| {});
2019 let mut cx = EditorTestContext::new(cx).await;
2020
2021 let line_height = cx.editor(|editor, window, _| {
2022 editor
2023 .style()
2024 .unwrap()
2025 .text
2026 .line_height_in_pixels(window.rem_size())
2027 });
2028 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2029
2030 cx.set_state(
2031 &r#"ˇone
2032 two
2033
2034 three
2035 fourˇ
2036 five
2037
2038 six"#
2039 .unindent(),
2040 );
2041
2042 cx.update_editor(|editor, window, cx| {
2043 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2044 });
2045 cx.assert_editor_state(
2046 &r#"one
2047 two
2048 ˇ
2049 three
2050 four
2051 five
2052 ˇ
2053 six"#
2054 .unindent(),
2055 );
2056
2057 cx.update_editor(|editor, window, cx| {
2058 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2059 });
2060 cx.assert_editor_state(
2061 &r#"one
2062 two
2063
2064 three
2065 four
2066 five
2067 ˇ
2068 sixˇ"#
2069 .unindent(),
2070 );
2071
2072 cx.update_editor(|editor, window, cx| {
2073 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2074 });
2075 cx.assert_editor_state(
2076 &r#"one
2077 two
2078
2079 three
2080 four
2081 five
2082
2083 sixˇ"#
2084 .unindent(),
2085 );
2086
2087 cx.update_editor(|editor, window, cx| {
2088 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2089 });
2090 cx.assert_editor_state(
2091 &r#"one
2092 two
2093
2094 three
2095 four
2096 five
2097 ˇ
2098 six"#
2099 .unindent(),
2100 );
2101
2102 cx.update_editor(|editor, window, cx| {
2103 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2104 });
2105 cx.assert_editor_state(
2106 &r#"one
2107 two
2108 ˇ
2109 three
2110 four
2111 five
2112
2113 six"#
2114 .unindent(),
2115 );
2116
2117 cx.update_editor(|editor, window, cx| {
2118 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2119 });
2120 cx.assert_editor_state(
2121 &r#"ˇone
2122 two
2123
2124 three
2125 four
2126 five
2127
2128 six"#
2129 .unindent(),
2130 );
2131}
2132
2133#[gpui::test]
2134async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2135 init_test(cx, |_| {});
2136 let mut cx = EditorTestContext::new(cx).await;
2137 let line_height = cx.editor(|editor, window, _| {
2138 editor
2139 .style()
2140 .unwrap()
2141 .text
2142 .line_height_in_pixels(window.rem_size())
2143 });
2144 let window = cx.window;
2145 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2146
2147 cx.set_state(
2148 r#"ˇone
2149 two
2150 three
2151 four
2152 five
2153 six
2154 seven
2155 eight
2156 nine
2157 ten
2158 "#,
2159 );
2160
2161 cx.update_editor(|editor, window, cx| {
2162 assert_eq!(
2163 editor.snapshot(window, cx).scroll_position(),
2164 gpui::Point::new(0., 0.)
2165 );
2166 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2167 assert_eq!(
2168 editor.snapshot(window, cx).scroll_position(),
2169 gpui::Point::new(0., 3.)
2170 );
2171 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2172 assert_eq!(
2173 editor.snapshot(window, cx).scroll_position(),
2174 gpui::Point::new(0., 6.)
2175 );
2176 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2177 assert_eq!(
2178 editor.snapshot(window, cx).scroll_position(),
2179 gpui::Point::new(0., 3.)
2180 );
2181
2182 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2183 assert_eq!(
2184 editor.snapshot(window, cx).scroll_position(),
2185 gpui::Point::new(0., 1.)
2186 );
2187 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2188 assert_eq!(
2189 editor.snapshot(window, cx).scroll_position(),
2190 gpui::Point::new(0., 3.)
2191 );
2192 });
2193}
2194
2195#[gpui::test]
2196async fn test_autoscroll(cx: &mut TestAppContext) {
2197 init_test(cx, |_| {});
2198 let mut cx = EditorTestContext::new(cx).await;
2199
2200 let line_height = cx.update_editor(|editor, window, cx| {
2201 editor.set_vertical_scroll_margin(2, cx);
2202 editor
2203 .style()
2204 .unwrap()
2205 .text
2206 .line_height_in_pixels(window.rem_size())
2207 });
2208 let window = cx.window;
2209 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2210
2211 cx.set_state(
2212 r#"ˇone
2213 two
2214 three
2215 four
2216 five
2217 six
2218 seven
2219 eight
2220 nine
2221 ten
2222 "#,
2223 );
2224 cx.update_editor(|editor, window, cx| {
2225 assert_eq!(
2226 editor.snapshot(window, cx).scroll_position(),
2227 gpui::Point::new(0., 0.0)
2228 );
2229 });
2230
2231 // Add a cursor below the visible area. Since both cursors cannot fit
2232 // on screen, the editor autoscrolls to reveal the newest cursor, and
2233 // allows the vertical scroll margin below that cursor.
2234 cx.update_editor(|editor, window, cx| {
2235 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2236 selections.select_ranges([
2237 Point::new(0, 0)..Point::new(0, 0),
2238 Point::new(6, 0)..Point::new(6, 0),
2239 ]);
2240 })
2241 });
2242 cx.update_editor(|editor, window, cx| {
2243 assert_eq!(
2244 editor.snapshot(window, cx).scroll_position(),
2245 gpui::Point::new(0., 3.0)
2246 );
2247 });
2248
2249 // Move down. The editor cursor scrolls down to track the newest cursor.
2250 cx.update_editor(|editor, window, cx| {
2251 editor.move_down(&Default::default(), window, cx);
2252 });
2253 cx.update_editor(|editor, window, cx| {
2254 assert_eq!(
2255 editor.snapshot(window, cx).scroll_position(),
2256 gpui::Point::new(0., 4.0)
2257 );
2258 });
2259
2260 // Add a cursor above the visible area. Since both cursors fit on screen,
2261 // the editor scrolls to show both.
2262 cx.update_editor(|editor, window, cx| {
2263 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2264 selections.select_ranges([
2265 Point::new(1, 0)..Point::new(1, 0),
2266 Point::new(6, 0)..Point::new(6, 0),
2267 ]);
2268 })
2269 });
2270 cx.update_editor(|editor, window, cx| {
2271 assert_eq!(
2272 editor.snapshot(window, cx).scroll_position(),
2273 gpui::Point::new(0., 1.0)
2274 );
2275 });
2276}
2277
2278#[gpui::test]
2279async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2280 init_test(cx, |_| {});
2281 let mut cx = EditorTestContext::new(cx).await;
2282
2283 let line_height = cx.editor(|editor, window, _cx| {
2284 editor
2285 .style()
2286 .unwrap()
2287 .text
2288 .line_height_in_pixels(window.rem_size())
2289 });
2290 let window = cx.window;
2291 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2292 cx.set_state(
2293 &r#"
2294 ˇone
2295 two
2296 threeˇ
2297 four
2298 five
2299 six
2300 seven
2301 eight
2302 nine
2303 ten
2304 "#
2305 .unindent(),
2306 );
2307
2308 cx.update_editor(|editor, window, cx| {
2309 editor.move_page_down(&MovePageDown::default(), window, cx)
2310 });
2311 cx.assert_editor_state(
2312 &r#"
2313 one
2314 two
2315 three
2316 ˇfour
2317 five
2318 sixˇ
2319 seven
2320 eight
2321 nine
2322 ten
2323 "#
2324 .unindent(),
2325 );
2326
2327 cx.update_editor(|editor, window, cx| {
2328 editor.move_page_down(&MovePageDown::default(), window, cx)
2329 });
2330 cx.assert_editor_state(
2331 &r#"
2332 one
2333 two
2334 three
2335 four
2336 five
2337 six
2338 ˇseven
2339 eight
2340 nineˇ
2341 ten
2342 "#
2343 .unindent(),
2344 );
2345
2346 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2347 cx.assert_editor_state(
2348 &r#"
2349 one
2350 two
2351 three
2352 ˇfour
2353 five
2354 sixˇ
2355 seven
2356 eight
2357 nine
2358 ten
2359 "#
2360 .unindent(),
2361 );
2362
2363 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2364 cx.assert_editor_state(
2365 &r#"
2366 ˇone
2367 two
2368 threeˇ
2369 four
2370 five
2371 six
2372 seven
2373 eight
2374 nine
2375 ten
2376 "#
2377 .unindent(),
2378 );
2379
2380 // Test select collapsing
2381 cx.update_editor(|editor, window, cx| {
2382 editor.move_page_down(&MovePageDown::default(), window, cx);
2383 editor.move_page_down(&MovePageDown::default(), window, cx);
2384 editor.move_page_down(&MovePageDown::default(), window, cx);
2385 });
2386 cx.assert_editor_state(
2387 &r#"
2388 one
2389 two
2390 three
2391 four
2392 five
2393 six
2394 seven
2395 eight
2396 nine
2397 ˇten
2398 ˇ"#
2399 .unindent(),
2400 );
2401}
2402
2403#[gpui::test]
2404async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2405 init_test(cx, |_| {});
2406 let mut cx = EditorTestContext::new(cx).await;
2407 cx.set_state("one «two threeˇ» four");
2408 cx.update_editor(|editor, window, cx| {
2409 editor.delete_to_beginning_of_line(
2410 &DeleteToBeginningOfLine {
2411 stop_at_indent: false,
2412 },
2413 window,
2414 cx,
2415 );
2416 assert_eq!(editor.text(cx), " four");
2417 });
2418}
2419
2420#[gpui::test]
2421fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2422 init_test(cx, |_| {});
2423
2424 let editor = cx.add_window(|window, cx| {
2425 let buffer = MultiBuffer::build_simple("one two three four", cx);
2426 build_editor(buffer.clone(), window, cx)
2427 });
2428
2429 _ = editor.update(cx, |editor, window, cx| {
2430 editor.change_selections(None, window, cx, |s| {
2431 s.select_display_ranges([
2432 // an empty selection - the preceding word fragment is deleted
2433 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2434 // characters selected - they are deleted
2435 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2436 ])
2437 });
2438 editor.delete_to_previous_word_start(
2439 &DeleteToPreviousWordStart {
2440 ignore_newlines: false,
2441 },
2442 window,
2443 cx,
2444 );
2445 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2446 });
2447
2448 _ = editor.update(cx, |editor, window, cx| {
2449 editor.change_selections(None, window, cx, |s| {
2450 s.select_display_ranges([
2451 // an empty selection - the following word fragment is deleted
2452 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2453 // characters selected - they are deleted
2454 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2455 ])
2456 });
2457 editor.delete_to_next_word_end(
2458 &DeleteToNextWordEnd {
2459 ignore_newlines: false,
2460 },
2461 window,
2462 cx,
2463 );
2464 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2465 });
2466}
2467
2468#[gpui::test]
2469fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2470 init_test(cx, |_| {});
2471
2472 let editor = cx.add_window(|window, cx| {
2473 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2474 build_editor(buffer.clone(), window, cx)
2475 });
2476 let del_to_prev_word_start = DeleteToPreviousWordStart {
2477 ignore_newlines: false,
2478 };
2479 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2480 ignore_newlines: true,
2481 };
2482
2483 _ = editor.update(cx, |editor, window, cx| {
2484 editor.change_selections(None, window, cx, |s| {
2485 s.select_display_ranges([
2486 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2487 ])
2488 });
2489 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2490 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2491 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2492 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2493 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2494 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2495 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2496 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2497 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2498 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2499 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2500 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2501 });
2502}
2503
2504#[gpui::test]
2505fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2506 init_test(cx, |_| {});
2507
2508 let editor = cx.add_window(|window, cx| {
2509 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2510 build_editor(buffer.clone(), window, cx)
2511 });
2512 let del_to_next_word_end = DeleteToNextWordEnd {
2513 ignore_newlines: false,
2514 };
2515 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2516 ignore_newlines: true,
2517 };
2518
2519 _ = editor.update(cx, |editor, window, cx| {
2520 editor.change_selections(None, window, cx, |s| {
2521 s.select_display_ranges([
2522 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2523 ])
2524 });
2525 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2526 assert_eq!(
2527 editor.buffer.read(cx).read(cx).text(),
2528 "one\n two\nthree\n four"
2529 );
2530 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2531 assert_eq!(
2532 editor.buffer.read(cx).read(cx).text(),
2533 "\n two\nthree\n four"
2534 );
2535 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2536 assert_eq!(
2537 editor.buffer.read(cx).read(cx).text(),
2538 "two\nthree\n four"
2539 );
2540 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2541 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2542 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2543 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2544 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2545 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2546 });
2547}
2548
2549#[gpui::test]
2550fn test_newline(cx: &mut TestAppContext) {
2551 init_test(cx, |_| {});
2552
2553 let editor = cx.add_window(|window, cx| {
2554 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2555 build_editor(buffer.clone(), window, cx)
2556 });
2557
2558 _ = editor.update(cx, |editor, window, cx| {
2559 editor.change_selections(None, window, cx, |s| {
2560 s.select_display_ranges([
2561 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2562 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2563 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2564 ])
2565 });
2566
2567 editor.newline(&Newline, window, cx);
2568 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2569 });
2570}
2571
2572#[gpui::test]
2573fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2574 init_test(cx, |_| {});
2575
2576 let editor = cx.add_window(|window, cx| {
2577 let buffer = MultiBuffer::build_simple(
2578 "
2579 a
2580 b(
2581 X
2582 )
2583 c(
2584 X
2585 )
2586 "
2587 .unindent()
2588 .as_str(),
2589 cx,
2590 );
2591 let mut editor = build_editor(buffer.clone(), window, cx);
2592 editor.change_selections(None, window, cx, |s| {
2593 s.select_ranges([
2594 Point::new(2, 4)..Point::new(2, 5),
2595 Point::new(5, 4)..Point::new(5, 5),
2596 ])
2597 });
2598 editor
2599 });
2600
2601 _ = editor.update(cx, |editor, window, cx| {
2602 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2603 editor.buffer.update(cx, |buffer, cx| {
2604 buffer.edit(
2605 [
2606 (Point::new(1, 2)..Point::new(3, 0), ""),
2607 (Point::new(4, 2)..Point::new(6, 0), ""),
2608 ],
2609 None,
2610 cx,
2611 );
2612 assert_eq!(
2613 buffer.read(cx).text(),
2614 "
2615 a
2616 b()
2617 c()
2618 "
2619 .unindent()
2620 );
2621 });
2622 assert_eq!(
2623 editor.selections.ranges(cx),
2624 &[
2625 Point::new(1, 2)..Point::new(1, 2),
2626 Point::new(2, 2)..Point::new(2, 2),
2627 ],
2628 );
2629
2630 editor.newline(&Newline, window, cx);
2631 assert_eq!(
2632 editor.text(cx),
2633 "
2634 a
2635 b(
2636 )
2637 c(
2638 )
2639 "
2640 .unindent()
2641 );
2642
2643 // The selections are moved after the inserted newlines
2644 assert_eq!(
2645 editor.selections.ranges(cx),
2646 &[
2647 Point::new(2, 0)..Point::new(2, 0),
2648 Point::new(4, 0)..Point::new(4, 0),
2649 ],
2650 );
2651 });
2652}
2653
2654#[gpui::test]
2655async fn test_newline_above(cx: &mut TestAppContext) {
2656 init_test(cx, |settings| {
2657 settings.defaults.tab_size = NonZeroU32::new(4)
2658 });
2659
2660 let language = Arc::new(
2661 Language::new(
2662 LanguageConfig::default(),
2663 Some(tree_sitter_rust::LANGUAGE.into()),
2664 )
2665 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2666 .unwrap(),
2667 );
2668
2669 let mut cx = EditorTestContext::new(cx).await;
2670 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2671 cx.set_state(indoc! {"
2672 const a: ˇA = (
2673 (ˇ
2674 «const_functionˇ»(ˇ),
2675 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2676 )ˇ
2677 ˇ);ˇ
2678 "});
2679
2680 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2681 cx.assert_editor_state(indoc! {"
2682 ˇ
2683 const a: A = (
2684 ˇ
2685 (
2686 ˇ
2687 ˇ
2688 const_function(),
2689 ˇ
2690 ˇ
2691 ˇ
2692 ˇ
2693 something_else,
2694 ˇ
2695 )
2696 ˇ
2697 ˇ
2698 );
2699 "});
2700}
2701
2702#[gpui::test]
2703async fn test_newline_below(cx: &mut TestAppContext) {
2704 init_test(cx, |settings| {
2705 settings.defaults.tab_size = NonZeroU32::new(4)
2706 });
2707
2708 let language = Arc::new(
2709 Language::new(
2710 LanguageConfig::default(),
2711 Some(tree_sitter_rust::LANGUAGE.into()),
2712 )
2713 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2714 .unwrap(),
2715 );
2716
2717 let mut cx = EditorTestContext::new(cx).await;
2718 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2719 cx.set_state(indoc! {"
2720 const a: ˇA = (
2721 (ˇ
2722 «const_functionˇ»(ˇ),
2723 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2724 )ˇ
2725 ˇ);ˇ
2726 "});
2727
2728 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2729 cx.assert_editor_state(indoc! {"
2730 const a: A = (
2731 ˇ
2732 (
2733 ˇ
2734 const_function(),
2735 ˇ
2736 ˇ
2737 something_else,
2738 ˇ
2739 ˇ
2740 ˇ
2741 ˇ
2742 )
2743 ˇ
2744 );
2745 ˇ
2746 ˇ
2747 "});
2748}
2749
2750#[gpui::test]
2751async fn test_newline_comments(cx: &mut TestAppContext) {
2752 init_test(cx, |settings| {
2753 settings.defaults.tab_size = NonZeroU32::new(4)
2754 });
2755
2756 let language = Arc::new(Language::new(
2757 LanguageConfig {
2758 line_comments: vec!["//".into()],
2759 ..LanguageConfig::default()
2760 },
2761 None,
2762 ));
2763 {
2764 let mut cx = EditorTestContext::new(cx).await;
2765 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2766 cx.set_state(indoc! {"
2767 // Fooˇ
2768 "});
2769
2770 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2771 cx.assert_editor_state(indoc! {"
2772 // Foo
2773 //ˇ
2774 "});
2775 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2776 cx.set_state(indoc! {"
2777 ˇ// Foo
2778 "});
2779 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2780 cx.assert_editor_state(indoc! {"
2781
2782 ˇ// Foo
2783 "});
2784 }
2785 // Ensure that comment continuations can be disabled.
2786 update_test_language_settings(cx, |settings| {
2787 settings.defaults.extend_comment_on_newline = Some(false);
2788 });
2789 let mut cx = EditorTestContext::new(cx).await;
2790 cx.set_state(indoc! {"
2791 // Fooˇ
2792 "});
2793 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2794 cx.assert_editor_state(indoc! {"
2795 // Foo
2796 ˇ
2797 "});
2798}
2799
2800#[gpui::test]
2801fn test_insert_with_old_selections(cx: &mut TestAppContext) {
2802 init_test(cx, |_| {});
2803
2804 let editor = cx.add_window(|window, cx| {
2805 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
2806 let mut editor = build_editor(buffer.clone(), window, cx);
2807 editor.change_selections(None, window, cx, |s| {
2808 s.select_ranges([3..4, 11..12, 19..20])
2809 });
2810 editor
2811 });
2812
2813 _ = editor.update(cx, |editor, window, cx| {
2814 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2815 editor.buffer.update(cx, |buffer, cx| {
2816 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
2817 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
2818 });
2819 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
2820
2821 editor.insert("Z", window, cx);
2822 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
2823
2824 // The selections are moved after the inserted characters
2825 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
2826 });
2827}
2828
2829#[gpui::test]
2830async fn test_tab(cx: &mut TestAppContext) {
2831 init_test(cx, |settings| {
2832 settings.defaults.tab_size = NonZeroU32::new(3)
2833 });
2834
2835 let mut cx = EditorTestContext::new(cx).await;
2836 cx.set_state(indoc! {"
2837 ˇabˇc
2838 ˇ🏀ˇ🏀ˇefg
2839 dˇ
2840 "});
2841 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2842 cx.assert_editor_state(indoc! {"
2843 ˇab ˇc
2844 ˇ🏀 ˇ🏀 ˇefg
2845 d ˇ
2846 "});
2847
2848 cx.set_state(indoc! {"
2849 a
2850 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2851 "});
2852 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2853 cx.assert_editor_state(indoc! {"
2854 a
2855 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2856 "});
2857}
2858
2859#[gpui::test]
2860async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
2861 init_test(cx, |_| {});
2862
2863 let mut cx = EditorTestContext::new(cx).await;
2864 let language = Arc::new(
2865 Language::new(
2866 LanguageConfig::default(),
2867 Some(tree_sitter_rust::LANGUAGE.into()),
2868 )
2869 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2870 .unwrap(),
2871 );
2872 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2873
2874 // when all cursors are to the left of the suggested indent, then auto-indent all.
2875 cx.set_state(indoc! {"
2876 const a: B = (
2877 c(
2878 ˇ
2879 ˇ )
2880 );
2881 "});
2882 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2883 cx.assert_editor_state(indoc! {"
2884 const a: B = (
2885 c(
2886 ˇ
2887 ˇ)
2888 );
2889 "});
2890
2891 // cursors that are already at the suggested indent level do not move
2892 // until other cursors that are to the left of the suggested indent
2893 // auto-indent.
2894 cx.set_state(indoc! {"
2895 ˇ
2896 const a: B = (
2897 c(
2898 d(
2899 ˇ
2900 )
2901 ˇ
2902 ˇ )
2903 );
2904 "});
2905 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2906 cx.assert_editor_state(indoc! {"
2907 ˇ
2908 const a: B = (
2909 c(
2910 d(
2911 ˇ
2912 )
2913 ˇ
2914 ˇ)
2915 );
2916 "});
2917 // once all multi-cursors are at the suggested
2918 // indent level, they all insert a soft tab together.
2919 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2920 cx.assert_editor_state(indoc! {"
2921 ˇ
2922 const a: B = (
2923 c(
2924 d(
2925 ˇ
2926 )
2927 ˇ
2928 ˇ)
2929 );
2930 "});
2931
2932 // handle auto-indent when there are multiple cursors on the same line
2933 cx.set_state(indoc! {"
2934 const a: B = (
2935 c(
2936 ˇ ˇ
2937 ˇ )
2938 );
2939 "});
2940 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2941 cx.assert_editor_state(indoc! {"
2942 const a: B = (
2943 c(
2944 ˇ
2945 ˇ)
2946 );
2947 "});
2948}
2949
2950#[gpui::test]
2951async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
2952 init_test(cx, |settings| {
2953 settings.defaults.tab_size = NonZeroU32::new(3)
2954 });
2955
2956 let mut cx = EditorTestContext::new(cx).await;
2957 cx.set_state(indoc! {"
2958 ˇ
2959 \t ˇ
2960 \t ˇ
2961 \t ˇ
2962 \t \t\t \t \t\t \t\t \t \t ˇ
2963 "});
2964
2965 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2966 cx.assert_editor_state(indoc! {"
2967 ˇ
2968 \t ˇ
2969 \t ˇ
2970 \t ˇ
2971 \t \t\t \t \t\t \t\t \t \t ˇ
2972 "});
2973}
2974
2975#[gpui::test]
2976async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
2977 init_test(cx, |settings| {
2978 settings.defaults.tab_size = NonZeroU32::new(4)
2979 });
2980
2981 let language = Arc::new(
2982 Language::new(
2983 LanguageConfig::default(),
2984 Some(tree_sitter_rust::LANGUAGE.into()),
2985 )
2986 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
2987 .unwrap(),
2988 );
2989
2990 let mut cx = EditorTestContext::new(cx).await;
2991 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2992 cx.set_state(indoc! {"
2993 fn a() {
2994 if b {
2995 \t ˇc
2996 }
2997 }
2998 "});
2999
3000 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3001 cx.assert_editor_state(indoc! {"
3002 fn a() {
3003 if b {
3004 ˇc
3005 }
3006 }
3007 "});
3008}
3009
3010#[gpui::test]
3011async fn test_indent_outdent(cx: &mut TestAppContext) {
3012 init_test(cx, |settings| {
3013 settings.defaults.tab_size = NonZeroU32::new(4);
3014 });
3015
3016 let mut cx = EditorTestContext::new(cx).await;
3017
3018 cx.set_state(indoc! {"
3019 «oneˇ» «twoˇ»
3020 three
3021 four
3022 "});
3023 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3024 cx.assert_editor_state(indoc! {"
3025 «oneˇ» «twoˇ»
3026 three
3027 four
3028 "});
3029
3030 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3031 cx.assert_editor_state(indoc! {"
3032 «oneˇ» «twoˇ»
3033 three
3034 four
3035 "});
3036
3037 // select across line ending
3038 cx.set_state(indoc! {"
3039 one two
3040 t«hree
3041 ˇ» four
3042 "});
3043 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3044 cx.assert_editor_state(indoc! {"
3045 one two
3046 t«hree
3047 ˇ» four
3048 "});
3049
3050 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3051 cx.assert_editor_state(indoc! {"
3052 one two
3053 t«hree
3054 ˇ» four
3055 "});
3056
3057 // Ensure that indenting/outdenting works when the cursor is at column 0.
3058 cx.set_state(indoc! {"
3059 one two
3060 ˇthree
3061 four
3062 "});
3063 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3064 cx.assert_editor_state(indoc! {"
3065 one two
3066 ˇthree
3067 four
3068 "});
3069
3070 cx.set_state(indoc! {"
3071 one two
3072 ˇ three
3073 four
3074 "});
3075 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3076 cx.assert_editor_state(indoc! {"
3077 one two
3078 ˇthree
3079 four
3080 "});
3081}
3082
3083#[gpui::test]
3084async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3085 init_test(cx, |settings| {
3086 settings.defaults.hard_tabs = Some(true);
3087 });
3088
3089 let mut cx = EditorTestContext::new(cx).await;
3090
3091 // select two ranges on one line
3092 cx.set_state(indoc! {"
3093 «oneˇ» «twoˇ»
3094 three
3095 four
3096 "});
3097 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3098 cx.assert_editor_state(indoc! {"
3099 \t«oneˇ» «twoˇ»
3100 three
3101 four
3102 "});
3103 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3104 cx.assert_editor_state(indoc! {"
3105 \t\t«oneˇ» «twoˇ»
3106 three
3107 four
3108 "});
3109 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3110 cx.assert_editor_state(indoc! {"
3111 \t«oneˇ» «twoˇ»
3112 three
3113 four
3114 "});
3115 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3116 cx.assert_editor_state(indoc! {"
3117 «oneˇ» «twoˇ»
3118 three
3119 four
3120 "});
3121
3122 // select across a line ending
3123 cx.set_state(indoc! {"
3124 one two
3125 t«hree
3126 ˇ»four
3127 "});
3128 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3129 cx.assert_editor_state(indoc! {"
3130 one two
3131 \tt«hree
3132 ˇ»four
3133 "});
3134 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3135 cx.assert_editor_state(indoc! {"
3136 one two
3137 \t\tt«hree
3138 ˇ»four
3139 "});
3140 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3141 cx.assert_editor_state(indoc! {"
3142 one two
3143 \tt«hree
3144 ˇ»four
3145 "});
3146 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3147 cx.assert_editor_state(indoc! {"
3148 one two
3149 t«hree
3150 ˇ»four
3151 "});
3152
3153 // Ensure that indenting/outdenting works when the cursor is at column 0.
3154 cx.set_state(indoc! {"
3155 one two
3156 ˇthree
3157 four
3158 "});
3159 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3160 cx.assert_editor_state(indoc! {"
3161 one two
3162 ˇthree
3163 four
3164 "});
3165 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3166 cx.assert_editor_state(indoc! {"
3167 one two
3168 \tˇthree
3169 four
3170 "});
3171 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3172 cx.assert_editor_state(indoc! {"
3173 one two
3174 ˇthree
3175 four
3176 "});
3177}
3178
3179#[gpui::test]
3180fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3181 init_test(cx, |settings| {
3182 settings.languages.extend([
3183 (
3184 "TOML".into(),
3185 LanguageSettingsContent {
3186 tab_size: NonZeroU32::new(2),
3187 ..Default::default()
3188 },
3189 ),
3190 (
3191 "Rust".into(),
3192 LanguageSettingsContent {
3193 tab_size: NonZeroU32::new(4),
3194 ..Default::default()
3195 },
3196 ),
3197 ]);
3198 });
3199
3200 let toml_language = Arc::new(Language::new(
3201 LanguageConfig {
3202 name: "TOML".into(),
3203 ..Default::default()
3204 },
3205 None,
3206 ));
3207 let rust_language = Arc::new(Language::new(
3208 LanguageConfig {
3209 name: "Rust".into(),
3210 ..Default::default()
3211 },
3212 None,
3213 ));
3214
3215 let toml_buffer =
3216 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3217 let rust_buffer =
3218 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3219 let multibuffer = cx.new(|cx| {
3220 let mut multibuffer = MultiBuffer::new(ReadWrite);
3221 multibuffer.push_excerpts(
3222 toml_buffer.clone(),
3223 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3224 cx,
3225 );
3226 multibuffer.push_excerpts(
3227 rust_buffer.clone(),
3228 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3229 cx,
3230 );
3231 multibuffer
3232 });
3233
3234 cx.add_window(|window, cx| {
3235 let mut editor = build_editor(multibuffer, window, cx);
3236
3237 assert_eq!(
3238 editor.text(cx),
3239 indoc! {"
3240 a = 1
3241 b = 2
3242
3243 const c: usize = 3;
3244 "}
3245 );
3246
3247 select_ranges(
3248 &mut editor,
3249 indoc! {"
3250 «aˇ» = 1
3251 b = 2
3252
3253 «const c:ˇ» usize = 3;
3254 "},
3255 window,
3256 cx,
3257 );
3258
3259 editor.tab(&Tab, window, cx);
3260 assert_text_with_selections(
3261 &mut editor,
3262 indoc! {"
3263 «aˇ» = 1
3264 b = 2
3265
3266 «const c:ˇ» usize = 3;
3267 "},
3268 cx,
3269 );
3270 editor.backtab(&Backtab, window, cx);
3271 assert_text_with_selections(
3272 &mut editor,
3273 indoc! {"
3274 «aˇ» = 1
3275 b = 2
3276
3277 «const c:ˇ» usize = 3;
3278 "},
3279 cx,
3280 );
3281
3282 editor
3283 });
3284}
3285
3286#[gpui::test]
3287async fn test_backspace(cx: &mut TestAppContext) {
3288 init_test(cx, |_| {});
3289
3290 let mut cx = EditorTestContext::new(cx).await;
3291
3292 // Basic backspace
3293 cx.set_state(indoc! {"
3294 onˇe two three
3295 fou«rˇ» five six
3296 seven «ˇeight nine
3297 »ten
3298 "});
3299 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3300 cx.assert_editor_state(indoc! {"
3301 oˇe two three
3302 fouˇ five six
3303 seven ˇten
3304 "});
3305
3306 // Test backspace inside and around indents
3307 cx.set_state(indoc! {"
3308 zero
3309 ˇone
3310 ˇtwo
3311 ˇ ˇ ˇ three
3312 ˇ ˇ four
3313 "});
3314 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3315 cx.assert_editor_state(indoc! {"
3316 zero
3317 ˇone
3318 ˇtwo
3319 ˇ threeˇ four
3320 "});
3321}
3322
3323#[gpui::test]
3324async fn test_delete(cx: &mut TestAppContext) {
3325 init_test(cx, |_| {});
3326
3327 let mut cx = EditorTestContext::new(cx).await;
3328 cx.set_state(indoc! {"
3329 onˇe two three
3330 fou«rˇ» five six
3331 seven «ˇeight nine
3332 »ten
3333 "});
3334 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3335 cx.assert_editor_state(indoc! {"
3336 onˇ two three
3337 fouˇ five six
3338 seven ˇten
3339 "});
3340}
3341
3342#[gpui::test]
3343fn test_delete_line(cx: &mut TestAppContext) {
3344 init_test(cx, |_| {});
3345
3346 let editor = cx.add_window(|window, cx| {
3347 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3348 build_editor(buffer, window, cx)
3349 });
3350 _ = editor.update(cx, |editor, window, cx| {
3351 editor.change_selections(None, window, cx, |s| {
3352 s.select_display_ranges([
3353 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3354 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3355 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3356 ])
3357 });
3358 editor.delete_line(&DeleteLine, window, cx);
3359 assert_eq!(editor.display_text(cx), "ghi");
3360 assert_eq!(
3361 editor.selections.display_ranges(cx),
3362 vec![
3363 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3364 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3365 ]
3366 );
3367 });
3368
3369 let editor = cx.add_window(|window, cx| {
3370 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3371 build_editor(buffer, window, cx)
3372 });
3373 _ = editor.update(cx, |editor, window, cx| {
3374 editor.change_selections(None, window, cx, |s| {
3375 s.select_display_ranges([
3376 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3377 ])
3378 });
3379 editor.delete_line(&DeleteLine, window, cx);
3380 assert_eq!(editor.display_text(cx), "ghi\n");
3381 assert_eq!(
3382 editor.selections.display_ranges(cx),
3383 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3384 );
3385 });
3386}
3387
3388#[gpui::test]
3389fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3390 init_test(cx, |_| {});
3391
3392 cx.add_window(|window, cx| {
3393 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3394 let mut editor = build_editor(buffer.clone(), window, cx);
3395 let buffer = buffer.read(cx).as_singleton().unwrap();
3396
3397 assert_eq!(
3398 editor.selections.ranges::<Point>(cx),
3399 &[Point::new(0, 0)..Point::new(0, 0)]
3400 );
3401
3402 // When on single line, replace newline at end by space
3403 editor.join_lines(&JoinLines, window, cx);
3404 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3405 assert_eq!(
3406 editor.selections.ranges::<Point>(cx),
3407 &[Point::new(0, 3)..Point::new(0, 3)]
3408 );
3409
3410 // When multiple lines are selected, remove newlines that are spanned by the selection
3411 editor.change_selections(None, window, cx, |s| {
3412 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3413 });
3414 editor.join_lines(&JoinLines, window, cx);
3415 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3416 assert_eq!(
3417 editor.selections.ranges::<Point>(cx),
3418 &[Point::new(0, 11)..Point::new(0, 11)]
3419 );
3420
3421 // Undo should be transactional
3422 editor.undo(&Undo, window, cx);
3423 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3424 assert_eq!(
3425 editor.selections.ranges::<Point>(cx),
3426 &[Point::new(0, 5)..Point::new(2, 2)]
3427 );
3428
3429 // When joining an empty line don't insert a space
3430 editor.change_selections(None, window, cx, |s| {
3431 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3432 });
3433 editor.join_lines(&JoinLines, window, cx);
3434 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3435 assert_eq!(
3436 editor.selections.ranges::<Point>(cx),
3437 [Point::new(2, 3)..Point::new(2, 3)]
3438 );
3439
3440 // We can remove trailing newlines
3441 editor.join_lines(&JoinLines, window, cx);
3442 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3443 assert_eq!(
3444 editor.selections.ranges::<Point>(cx),
3445 [Point::new(2, 3)..Point::new(2, 3)]
3446 );
3447
3448 // We don't blow up on the last line
3449 editor.join_lines(&JoinLines, window, cx);
3450 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3451 assert_eq!(
3452 editor.selections.ranges::<Point>(cx),
3453 [Point::new(2, 3)..Point::new(2, 3)]
3454 );
3455
3456 // reset to test indentation
3457 editor.buffer.update(cx, |buffer, cx| {
3458 buffer.edit(
3459 [
3460 (Point::new(1, 0)..Point::new(1, 2), " "),
3461 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3462 ],
3463 None,
3464 cx,
3465 )
3466 });
3467
3468 // We remove any leading spaces
3469 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3470 editor.change_selections(None, window, cx, |s| {
3471 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3472 });
3473 editor.join_lines(&JoinLines, window, cx);
3474 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3475
3476 // We don't insert a space for a line containing only spaces
3477 editor.join_lines(&JoinLines, window, cx);
3478 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3479
3480 // We ignore any leading tabs
3481 editor.join_lines(&JoinLines, window, cx);
3482 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3483
3484 editor
3485 });
3486}
3487
3488#[gpui::test]
3489fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3490 init_test(cx, |_| {});
3491
3492 cx.add_window(|window, cx| {
3493 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3494 let mut editor = build_editor(buffer.clone(), window, cx);
3495 let buffer = buffer.read(cx).as_singleton().unwrap();
3496
3497 editor.change_selections(None, window, cx, |s| {
3498 s.select_ranges([
3499 Point::new(0, 2)..Point::new(1, 1),
3500 Point::new(1, 2)..Point::new(1, 2),
3501 Point::new(3, 1)..Point::new(3, 2),
3502 ])
3503 });
3504
3505 editor.join_lines(&JoinLines, window, cx);
3506 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3507
3508 assert_eq!(
3509 editor.selections.ranges::<Point>(cx),
3510 [
3511 Point::new(0, 7)..Point::new(0, 7),
3512 Point::new(1, 3)..Point::new(1, 3)
3513 ]
3514 );
3515 editor
3516 });
3517}
3518
3519#[gpui::test]
3520async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3521 init_test(cx, |_| {});
3522
3523 let mut cx = EditorTestContext::new(cx).await;
3524
3525 let diff_base = r#"
3526 Line 0
3527 Line 1
3528 Line 2
3529 Line 3
3530 "#
3531 .unindent();
3532
3533 cx.set_state(
3534 &r#"
3535 ˇLine 0
3536 Line 1
3537 Line 2
3538 Line 3
3539 "#
3540 .unindent(),
3541 );
3542
3543 cx.set_head_text(&diff_base);
3544 executor.run_until_parked();
3545
3546 // Join lines
3547 cx.update_editor(|editor, window, cx| {
3548 editor.join_lines(&JoinLines, window, cx);
3549 });
3550 executor.run_until_parked();
3551
3552 cx.assert_editor_state(
3553 &r#"
3554 Line 0ˇ Line 1
3555 Line 2
3556 Line 3
3557 "#
3558 .unindent(),
3559 );
3560 // Join again
3561 cx.update_editor(|editor, window, cx| {
3562 editor.join_lines(&JoinLines, window, cx);
3563 });
3564 executor.run_until_parked();
3565
3566 cx.assert_editor_state(
3567 &r#"
3568 Line 0 Line 1ˇ Line 2
3569 Line 3
3570 "#
3571 .unindent(),
3572 );
3573}
3574
3575#[gpui::test]
3576async fn test_custom_newlines_cause_no_false_positive_diffs(
3577 executor: BackgroundExecutor,
3578 cx: &mut TestAppContext,
3579) {
3580 init_test(cx, |_| {});
3581 let mut cx = EditorTestContext::new(cx).await;
3582 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3583 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3584 executor.run_until_parked();
3585
3586 cx.update_editor(|editor, window, cx| {
3587 let snapshot = editor.snapshot(window, cx);
3588 assert_eq!(
3589 snapshot
3590 .buffer_snapshot
3591 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3592 .collect::<Vec<_>>(),
3593 Vec::new(),
3594 "Should not have any diffs for files with custom newlines"
3595 );
3596 });
3597}
3598
3599#[gpui::test]
3600async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3601 init_test(cx, |_| {});
3602
3603 let mut cx = EditorTestContext::new(cx).await;
3604
3605 // Test sort_lines_case_insensitive()
3606 cx.set_state(indoc! {"
3607 «z
3608 y
3609 x
3610 Z
3611 Y
3612 Xˇ»
3613 "});
3614 cx.update_editor(|e, window, cx| {
3615 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3616 });
3617 cx.assert_editor_state(indoc! {"
3618 «x
3619 X
3620 y
3621 Y
3622 z
3623 Zˇ»
3624 "});
3625
3626 // Test reverse_lines()
3627 cx.set_state(indoc! {"
3628 «5
3629 4
3630 3
3631 2
3632 1ˇ»
3633 "});
3634 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
3635 cx.assert_editor_state(indoc! {"
3636 «1
3637 2
3638 3
3639 4
3640 5ˇ»
3641 "});
3642
3643 // Skip testing shuffle_line()
3644
3645 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
3646 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
3647
3648 // Don't manipulate when cursor is on single line, but expand the selection
3649 cx.set_state(indoc! {"
3650 ddˇdd
3651 ccc
3652 bb
3653 a
3654 "});
3655 cx.update_editor(|e, window, cx| {
3656 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3657 });
3658 cx.assert_editor_state(indoc! {"
3659 «ddddˇ»
3660 ccc
3661 bb
3662 a
3663 "});
3664
3665 // Basic manipulate case
3666 // Start selection moves to column 0
3667 // End of selection shrinks to fit shorter line
3668 cx.set_state(indoc! {"
3669 dd«d
3670 ccc
3671 bb
3672 aaaaaˇ»
3673 "});
3674 cx.update_editor(|e, window, cx| {
3675 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3676 });
3677 cx.assert_editor_state(indoc! {"
3678 «aaaaa
3679 bb
3680 ccc
3681 dddˇ»
3682 "});
3683
3684 // Manipulate case with newlines
3685 cx.set_state(indoc! {"
3686 dd«d
3687 ccc
3688
3689 bb
3690 aaaaa
3691
3692 ˇ»
3693 "});
3694 cx.update_editor(|e, window, cx| {
3695 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3696 });
3697 cx.assert_editor_state(indoc! {"
3698 «
3699
3700 aaaaa
3701 bb
3702 ccc
3703 dddˇ»
3704
3705 "});
3706
3707 // Adding new line
3708 cx.set_state(indoc! {"
3709 aa«a
3710 bbˇ»b
3711 "});
3712 cx.update_editor(|e, window, cx| {
3713 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
3714 });
3715 cx.assert_editor_state(indoc! {"
3716 «aaa
3717 bbb
3718 added_lineˇ»
3719 "});
3720
3721 // Removing line
3722 cx.set_state(indoc! {"
3723 aa«a
3724 bbbˇ»
3725 "});
3726 cx.update_editor(|e, window, cx| {
3727 e.manipulate_lines(window, cx, |lines| {
3728 lines.pop();
3729 })
3730 });
3731 cx.assert_editor_state(indoc! {"
3732 «aaaˇ»
3733 "});
3734
3735 // Removing all lines
3736 cx.set_state(indoc! {"
3737 aa«a
3738 bbbˇ»
3739 "});
3740 cx.update_editor(|e, window, cx| {
3741 e.manipulate_lines(window, cx, |lines| {
3742 lines.drain(..);
3743 })
3744 });
3745 cx.assert_editor_state(indoc! {"
3746 ˇ
3747 "});
3748}
3749
3750#[gpui::test]
3751async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
3752 init_test(cx, |_| {});
3753
3754 let mut cx = EditorTestContext::new(cx).await;
3755
3756 // Consider continuous selection as single selection
3757 cx.set_state(indoc! {"
3758 Aaa«aa
3759 cˇ»c«c
3760 bb
3761 aaaˇ»aa
3762 "});
3763 cx.update_editor(|e, window, cx| {
3764 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3765 });
3766 cx.assert_editor_state(indoc! {"
3767 «Aaaaa
3768 ccc
3769 bb
3770 aaaaaˇ»
3771 "});
3772
3773 cx.set_state(indoc! {"
3774 Aaa«aa
3775 cˇ»c«c
3776 bb
3777 aaaˇ»aa
3778 "});
3779 cx.update_editor(|e, window, cx| {
3780 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3781 });
3782 cx.assert_editor_state(indoc! {"
3783 «Aaaaa
3784 ccc
3785 bbˇ»
3786 "});
3787
3788 // Consider non continuous selection as distinct dedup operations
3789 cx.set_state(indoc! {"
3790 «aaaaa
3791 bb
3792 aaaaa
3793 aaaaaˇ»
3794
3795 aaa«aaˇ»
3796 "});
3797 cx.update_editor(|e, window, cx| {
3798 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3799 });
3800 cx.assert_editor_state(indoc! {"
3801 «aaaaa
3802 bbˇ»
3803
3804 «aaaaaˇ»
3805 "});
3806}
3807
3808#[gpui::test]
3809async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
3810 init_test(cx, |_| {});
3811
3812 let mut cx = EditorTestContext::new(cx).await;
3813
3814 cx.set_state(indoc! {"
3815 «Aaa
3816 aAa
3817 Aaaˇ»
3818 "});
3819 cx.update_editor(|e, window, cx| {
3820 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3821 });
3822 cx.assert_editor_state(indoc! {"
3823 «Aaa
3824 aAaˇ»
3825 "});
3826
3827 cx.set_state(indoc! {"
3828 «Aaa
3829 aAa
3830 aaAˇ»
3831 "});
3832 cx.update_editor(|e, window, cx| {
3833 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3834 });
3835 cx.assert_editor_state(indoc! {"
3836 «Aaaˇ»
3837 "});
3838}
3839
3840#[gpui::test]
3841async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
3842 init_test(cx, |_| {});
3843
3844 let mut cx = EditorTestContext::new(cx).await;
3845
3846 // Manipulate with multiple selections on a single line
3847 cx.set_state(indoc! {"
3848 dd«dd
3849 cˇ»c«c
3850 bb
3851 aaaˇ»aa
3852 "});
3853 cx.update_editor(|e, window, cx| {
3854 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3855 });
3856 cx.assert_editor_state(indoc! {"
3857 «aaaaa
3858 bb
3859 ccc
3860 ddddˇ»
3861 "});
3862
3863 // Manipulate with multiple disjoin selections
3864 cx.set_state(indoc! {"
3865 5«
3866 4
3867 3
3868 2
3869 1ˇ»
3870
3871 dd«dd
3872 ccc
3873 bb
3874 aaaˇ»aa
3875 "});
3876 cx.update_editor(|e, window, cx| {
3877 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3878 });
3879 cx.assert_editor_state(indoc! {"
3880 «1
3881 2
3882 3
3883 4
3884 5ˇ»
3885
3886 «aaaaa
3887 bb
3888 ccc
3889 ddddˇ»
3890 "});
3891
3892 // Adding lines on each selection
3893 cx.set_state(indoc! {"
3894 2«
3895 1ˇ»
3896
3897 bb«bb
3898 aaaˇ»aa
3899 "});
3900 cx.update_editor(|e, window, cx| {
3901 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
3902 });
3903 cx.assert_editor_state(indoc! {"
3904 «2
3905 1
3906 added lineˇ»
3907
3908 «bbbb
3909 aaaaa
3910 added lineˇ»
3911 "});
3912
3913 // Removing lines on each selection
3914 cx.set_state(indoc! {"
3915 2«
3916 1ˇ»
3917
3918 bb«bb
3919 aaaˇ»aa
3920 "});
3921 cx.update_editor(|e, window, cx| {
3922 e.manipulate_lines(window, cx, |lines| {
3923 lines.pop();
3924 })
3925 });
3926 cx.assert_editor_state(indoc! {"
3927 «2ˇ»
3928
3929 «bbbbˇ»
3930 "});
3931}
3932
3933#[gpui::test]
3934async fn test_toggle_case(cx: &mut TestAppContext) {
3935 init_test(cx, |_| {});
3936
3937 let mut cx = EditorTestContext::new(cx).await;
3938
3939 // If all lower case -> upper case
3940 cx.set_state(indoc! {"
3941 «hello worldˇ»
3942 "});
3943 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
3944 cx.assert_editor_state(indoc! {"
3945 «HELLO WORLDˇ»
3946 "});
3947
3948 // If all upper case -> lower case
3949 cx.set_state(indoc! {"
3950 «HELLO WORLDˇ»
3951 "});
3952 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
3953 cx.assert_editor_state(indoc! {"
3954 «hello worldˇ»
3955 "});
3956
3957 // If any upper case characters are identified -> lower case
3958 // This matches JetBrains IDEs
3959 cx.set_state(indoc! {"
3960 «hEllo worldˇ»
3961 "});
3962 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
3963 cx.assert_editor_state(indoc! {"
3964 «hello worldˇ»
3965 "});
3966}
3967
3968#[gpui::test]
3969async fn test_manipulate_text(cx: &mut TestAppContext) {
3970 init_test(cx, |_| {});
3971
3972 let mut cx = EditorTestContext::new(cx).await;
3973
3974 // Test convert_to_upper_case()
3975 cx.set_state(indoc! {"
3976 «hello worldˇ»
3977 "});
3978 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3979 cx.assert_editor_state(indoc! {"
3980 «HELLO WORLDˇ»
3981 "});
3982
3983 // Test convert_to_lower_case()
3984 cx.set_state(indoc! {"
3985 «HELLO WORLDˇ»
3986 "});
3987 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
3988 cx.assert_editor_state(indoc! {"
3989 «hello worldˇ»
3990 "});
3991
3992 // Test multiple line, single selection case
3993 cx.set_state(indoc! {"
3994 «The quick brown
3995 fox jumps over
3996 the lazy dogˇ»
3997 "});
3998 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
3999 cx.assert_editor_state(indoc! {"
4000 «The Quick Brown
4001 Fox Jumps Over
4002 The Lazy Dogˇ»
4003 "});
4004
4005 // Test multiple line, single selection case
4006 cx.set_state(indoc! {"
4007 «The quick brown
4008 fox jumps over
4009 the lazy dogˇ»
4010 "});
4011 cx.update_editor(|e, window, cx| {
4012 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4013 });
4014 cx.assert_editor_state(indoc! {"
4015 «TheQuickBrown
4016 FoxJumpsOver
4017 TheLazyDogˇ»
4018 "});
4019
4020 // From here on out, test more complex cases of manipulate_text()
4021
4022 // Test no selection case - should affect words cursors are in
4023 // Cursor at beginning, middle, and end of word
4024 cx.set_state(indoc! {"
4025 ˇhello big beauˇtiful worldˇ
4026 "});
4027 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4028 cx.assert_editor_state(indoc! {"
4029 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4030 "});
4031
4032 // Test multiple selections on a single line and across multiple lines
4033 cx.set_state(indoc! {"
4034 «Theˇ» quick «brown
4035 foxˇ» jumps «overˇ»
4036 the «lazyˇ» dog
4037 "});
4038 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4039 cx.assert_editor_state(indoc! {"
4040 «THEˇ» quick «BROWN
4041 FOXˇ» jumps «OVERˇ»
4042 the «LAZYˇ» dog
4043 "});
4044
4045 // Test case where text length grows
4046 cx.set_state(indoc! {"
4047 «tschüߡ»
4048 "});
4049 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4050 cx.assert_editor_state(indoc! {"
4051 «TSCHÜSSˇ»
4052 "});
4053
4054 // Test to make sure we don't crash when text shrinks
4055 cx.set_state(indoc! {"
4056 aaa_bbbˇ
4057 "});
4058 cx.update_editor(|e, window, cx| {
4059 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4060 });
4061 cx.assert_editor_state(indoc! {"
4062 «aaaBbbˇ»
4063 "});
4064
4065 // Test to make sure we all aware of the fact that each word can grow and shrink
4066 // Final selections should be aware of this fact
4067 cx.set_state(indoc! {"
4068 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4069 "});
4070 cx.update_editor(|e, window, cx| {
4071 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4072 });
4073 cx.assert_editor_state(indoc! {"
4074 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4075 "});
4076
4077 cx.set_state(indoc! {"
4078 «hElLo, WoRld!ˇ»
4079 "});
4080 cx.update_editor(|e, window, cx| {
4081 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4082 });
4083 cx.assert_editor_state(indoc! {"
4084 «HeLlO, wOrLD!ˇ»
4085 "});
4086}
4087
4088#[gpui::test]
4089fn test_duplicate_line(cx: &mut TestAppContext) {
4090 init_test(cx, |_| {});
4091
4092 let editor = cx.add_window(|window, cx| {
4093 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4094 build_editor(buffer, window, cx)
4095 });
4096 _ = editor.update(cx, |editor, window, cx| {
4097 editor.change_selections(None, window, cx, |s| {
4098 s.select_display_ranges([
4099 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4100 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4101 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4102 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4103 ])
4104 });
4105 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4106 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4107 assert_eq!(
4108 editor.selections.display_ranges(cx),
4109 vec![
4110 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4111 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4112 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4113 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4114 ]
4115 );
4116 });
4117
4118 let editor = cx.add_window(|window, cx| {
4119 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4120 build_editor(buffer, window, cx)
4121 });
4122 _ = editor.update(cx, |editor, window, cx| {
4123 editor.change_selections(None, window, cx, |s| {
4124 s.select_display_ranges([
4125 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4126 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4127 ])
4128 });
4129 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4130 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4131 assert_eq!(
4132 editor.selections.display_ranges(cx),
4133 vec![
4134 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4135 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4136 ]
4137 );
4138 });
4139
4140 // With `move_upwards` the selections stay in place, except for
4141 // the lines inserted above them
4142 let editor = cx.add_window(|window, cx| {
4143 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4144 build_editor(buffer, window, cx)
4145 });
4146 _ = editor.update(cx, |editor, window, cx| {
4147 editor.change_selections(None, window, cx, |s| {
4148 s.select_display_ranges([
4149 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4150 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4151 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4152 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4153 ])
4154 });
4155 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4156 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4157 assert_eq!(
4158 editor.selections.display_ranges(cx),
4159 vec![
4160 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4161 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4162 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4163 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4164 ]
4165 );
4166 });
4167
4168 let editor = cx.add_window(|window, cx| {
4169 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4170 build_editor(buffer, window, cx)
4171 });
4172 _ = editor.update(cx, |editor, window, cx| {
4173 editor.change_selections(None, window, cx, |s| {
4174 s.select_display_ranges([
4175 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4176 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4177 ])
4178 });
4179 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4180 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4181 assert_eq!(
4182 editor.selections.display_ranges(cx),
4183 vec![
4184 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4185 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4186 ]
4187 );
4188 });
4189
4190 let editor = cx.add_window(|window, cx| {
4191 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4192 build_editor(buffer, window, cx)
4193 });
4194 _ = editor.update(cx, |editor, window, cx| {
4195 editor.change_selections(None, window, cx, |s| {
4196 s.select_display_ranges([
4197 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4198 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4199 ])
4200 });
4201 editor.duplicate_selection(&DuplicateSelection, window, cx);
4202 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4203 assert_eq!(
4204 editor.selections.display_ranges(cx),
4205 vec![
4206 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4207 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4208 ]
4209 );
4210 });
4211}
4212
4213#[gpui::test]
4214fn test_move_line_up_down(cx: &mut TestAppContext) {
4215 init_test(cx, |_| {});
4216
4217 let editor = cx.add_window(|window, cx| {
4218 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4219 build_editor(buffer, window, cx)
4220 });
4221 _ = editor.update(cx, |editor, window, cx| {
4222 editor.fold_creases(
4223 vec![
4224 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4225 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4226 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4227 ],
4228 true,
4229 window,
4230 cx,
4231 );
4232 editor.change_selections(None, window, cx, |s| {
4233 s.select_display_ranges([
4234 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4235 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4236 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4237 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4238 ])
4239 });
4240 assert_eq!(
4241 editor.display_text(cx),
4242 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4243 );
4244
4245 editor.move_line_up(&MoveLineUp, window, cx);
4246 assert_eq!(
4247 editor.display_text(cx),
4248 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4249 );
4250 assert_eq!(
4251 editor.selections.display_ranges(cx),
4252 vec![
4253 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4254 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4255 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4256 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4257 ]
4258 );
4259 });
4260
4261 _ = editor.update(cx, |editor, window, cx| {
4262 editor.move_line_down(&MoveLineDown, window, cx);
4263 assert_eq!(
4264 editor.display_text(cx),
4265 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4266 );
4267 assert_eq!(
4268 editor.selections.display_ranges(cx),
4269 vec![
4270 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4271 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4272 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4273 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4274 ]
4275 );
4276 });
4277
4278 _ = editor.update(cx, |editor, window, cx| {
4279 editor.move_line_down(&MoveLineDown, window, cx);
4280 assert_eq!(
4281 editor.display_text(cx),
4282 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4283 );
4284 assert_eq!(
4285 editor.selections.display_ranges(cx),
4286 vec![
4287 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4288 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4289 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4290 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4291 ]
4292 );
4293 });
4294
4295 _ = editor.update(cx, |editor, window, cx| {
4296 editor.move_line_up(&MoveLineUp, window, cx);
4297 assert_eq!(
4298 editor.display_text(cx),
4299 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4300 );
4301 assert_eq!(
4302 editor.selections.display_ranges(cx),
4303 vec![
4304 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4305 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4306 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4307 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4308 ]
4309 );
4310 });
4311}
4312
4313#[gpui::test]
4314fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4315 init_test(cx, |_| {});
4316
4317 let editor = cx.add_window(|window, cx| {
4318 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4319 build_editor(buffer, window, cx)
4320 });
4321 _ = editor.update(cx, |editor, window, cx| {
4322 let snapshot = editor.buffer.read(cx).snapshot(cx);
4323 editor.insert_blocks(
4324 [BlockProperties {
4325 style: BlockStyle::Fixed,
4326 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4327 height: Some(1),
4328 render: Arc::new(|_| div().into_any()),
4329 priority: 0,
4330 }],
4331 Some(Autoscroll::fit()),
4332 cx,
4333 );
4334 editor.change_selections(None, window, cx, |s| {
4335 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4336 });
4337 editor.move_line_down(&MoveLineDown, window, cx);
4338 });
4339}
4340
4341#[gpui::test]
4342async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4343 init_test(cx, |_| {});
4344
4345 let mut cx = EditorTestContext::new(cx).await;
4346 cx.set_state(
4347 &"
4348 ˇzero
4349 one
4350 two
4351 three
4352 four
4353 five
4354 "
4355 .unindent(),
4356 );
4357
4358 // Create a four-line block that replaces three lines of text.
4359 cx.update_editor(|editor, window, cx| {
4360 let snapshot = editor.snapshot(window, cx);
4361 let snapshot = &snapshot.buffer_snapshot;
4362 let placement = BlockPlacement::Replace(
4363 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4364 );
4365 editor.insert_blocks(
4366 [BlockProperties {
4367 placement,
4368 height: Some(4),
4369 style: BlockStyle::Sticky,
4370 render: Arc::new(|_| gpui::div().into_any_element()),
4371 priority: 0,
4372 }],
4373 None,
4374 cx,
4375 );
4376 });
4377
4378 // Move down so that the cursor touches the block.
4379 cx.update_editor(|editor, window, cx| {
4380 editor.move_down(&Default::default(), window, cx);
4381 });
4382 cx.assert_editor_state(
4383 &"
4384 zero
4385 «one
4386 two
4387 threeˇ»
4388 four
4389 five
4390 "
4391 .unindent(),
4392 );
4393
4394 // Move down past the block.
4395 cx.update_editor(|editor, window, cx| {
4396 editor.move_down(&Default::default(), window, cx);
4397 });
4398 cx.assert_editor_state(
4399 &"
4400 zero
4401 one
4402 two
4403 three
4404 ˇfour
4405 five
4406 "
4407 .unindent(),
4408 );
4409}
4410
4411#[gpui::test]
4412fn test_transpose(cx: &mut TestAppContext) {
4413 init_test(cx, |_| {});
4414
4415 _ = cx.add_window(|window, cx| {
4416 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4417 editor.set_style(EditorStyle::default(), window, cx);
4418 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4419 editor.transpose(&Default::default(), window, cx);
4420 assert_eq!(editor.text(cx), "bac");
4421 assert_eq!(editor.selections.ranges(cx), [2..2]);
4422
4423 editor.transpose(&Default::default(), window, cx);
4424 assert_eq!(editor.text(cx), "bca");
4425 assert_eq!(editor.selections.ranges(cx), [3..3]);
4426
4427 editor.transpose(&Default::default(), window, cx);
4428 assert_eq!(editor.text(cx), "bac");
4429 assert_eq!(editor.selections.ranges(cx), [3..3]);
4430
4431 editor
4432 });
4433
4434 _ = cx.add_window(|window, cx| {
4435 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4436 editor.set_style(EditorStyle::default(), window, cx);
4437 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4438 editor.transpose(&Default::default(), window, cx);
4439 assert_eq!(editor.text(cx), "acb\nde");
4440 assert_eq!(editor.selections.ranges(cx), [3..3]);
4441
4442 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4443 editor.transpose(&Default::default(), window, cx);
4444 assert_eq!(editor.text(cx), "acbd\ne");
4445 assert_eq!(editor.selections.ranges(cx), [5..5]);
4446
4447 editor.transpose(&Default::default(), window, cx);
4448 assert_eq!(editor.text(cx), "acbde\n");
4449 assert_eq!(editor.selections.ranges(cx), [6..6]);
4450
4451 editor.transpose(&Default::default(), window, cx);
4452 assert_eq!(editor.text(cx), "acbd\ne");
4453 assert_eq!(editor.selections.ranges(cx), [6..6]);
4454
4455 editor
4456 });
4457
4458 _ = cx.add_window(|window, cx| {
4459 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4460 editor.set_style(EditorStyle::default(), window, cx);
4461 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4462 editor.transpose(&Default::default(), window, cx);
4463 assert_eq!(editor.text(cx), "bacd\ne");
4464 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4465
4466 editor.transpose(&Default::default(), window, cx);
4467 assert_eq!(editor.text(cx), "bcade\n");
4468 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4469
4470 editor.transpose(&Default::default(), window, cx);
4471 assert_eq!(editor.text(cx), "bcda\ne");
4472 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4473
4474 editor.transpose(&Default::default(), window, cx);
4475 assert_eq!(editor.text(cx), "bcade\n");
4476 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4477
4478 editor.transpose(&Default::default(), window, cx);
4479 assert_eq!(editor.text(cx), "bcaed\n");
4480 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4481
4482 editor
4483 });
4484
4485 _ = cx.add_window(|window, cx| {
4486 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4487 editor.set_style(EditorStyle::default(), window, cx);
4488 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4489 editor.transpose(&Default::default(), window, cx);
4490 assert_eq!(editor.text(cx), "🏀🍐✋");
4491 assert_eq!(editor.selections.ranges(cx), [8..8]);
4492
4493 editor.transpose(&Default::default(), window, cx);
4494 assert_eq!(editor.text(cx), "🏀✋🍐");
4495 assert_eq!(editor.selections.ranges(cx), [11..11]);
4496
4497 editor.transpose(&Default::default(), window, cx);
4498 assert_eq!(editor.text(cx), "🏀🍐✋");
4499 assert_eq!(editor.selections.ranges(cx), [11..11]);
4500
4501 editor
4502 });
4503}
4504
4505#[gpui::test]
4506async fn test_rewrap(cx: &mut TestAppContext) {
4507 init_test(cx, |settings| {
4508 settings.languages.extend([
4509 (
4510 "Markdown".into(),
4511 LanguageSettingsContent {
4512 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4513 ..Default::default()
4514 },
4515 ),
4516 (
4517 "Plain Text".into(),
4518 LanguageSettingsContent {
4519 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4520 ..Default::default()
4521 },
4522 ),
4523 ])
4524 });
4525
4526 let mut cx = EditorTestContext::new(cx).await;
4527
4528 let language_with_c_comments = Arc::new(Language::new(
4529 LanguageConfig {
4530 line_comments: vec!["// ".into()],
4531 ..LanguageConfig::default()
4532 },
4533 None,
4534 ));
4535 let language_with_pound_comments = Arc::new(Language::new(
4536 LanguageConfig {
4537 line_comments: vec!["# ".into()],
4538 ..LanguageConfig::default()
4539 },
4540 None,
4541 ));
4542 let markdown_language = Arc::new(Language::new(
4543 LanguageConfig {
4544 name: "Markdown".into(),
4545 ..LanguageConfig::default()
4546 },
4547 None,
4548 ));
4549 let language_with_doc_comments = Arc::new(Language::new(
4550 LanguageConfig {
4551 line_comments: vec!["// ".into(), "/// ".into()],
4552 ..LanguageConfig::default()
4553 },
4554 Some(tree_sitter_rust::LANGUAGE.into()),
4555 ));
4556
4557 let plaintext_language = Arc::new(Language::new(
4558 LanguageConfig {
4559 name: "Plain Text".into(),
4560 ..LanguageConfig::default()
4561 },
4562 None,
4563 ));
4564
4565 assert_rewrap(
4566 indoc! {"
4567 // ˇ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.
4568 "},
4569 indoc! {"
4570 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4571 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4572 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4573 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4574 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4575 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4576 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4577 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4578 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4579 // porttitor id. Aliquam id accumsan eros.
4580 "},
4581 language_with_c_comments.clone(),
4582 &mut cx,
4583 );
4584
4585 // Test that rewrapping works inside of a selection
4586 assert_rewrap(
4587 indoc! {"
4588 «// 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.ˇ»
4589 "},
4590 indoc! {"
4591 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4592 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4593 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4594 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4595 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4596 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4597 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4598 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4599 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4600 // porttitor id. Aliquam id accumsan eros.ˇ»
4601 "},
4602 language_with_c_comments.clone(),
4603 &mut cx,
4604 );
4605
4606 // Test that cursors that expand to the same region are collapsed.
4607 assert_rewrap(
4608 indoc! {"
4609 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4610 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4611 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4612 // ˇ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.
4613 "},
4614 indoc! {"
4615 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4616 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4617 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
4618 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4619 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
4620 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4621 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4622 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4623 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4624 // porttitor id. Aliquam id accumsan eros.
4625 "},
4626 language_with_c_comments.clone(),
4627 &mut cx,
4628 );
4629
4630 // Test that non-contiguous selections are treated separately.
4631 assert_rewrap(
4632 indoc! {"
4633 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4634 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4635 //
4636 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4637 // ˇ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.
4638 "},
4639 indoc! {"
4640 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4641 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4642 // auctor, eu lacinia sapien scelerisque.
4643 //
4644 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
4645 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4646 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
4647 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
4648 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
4649 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
4650 // vulputate turpis porttitor id. Aliquam id accumsan eros.
4651 "},
4652 language_with_c_comments.clone(),
4653 &mut cx,
4654 );
4655
4656 // Test that different comment prefixes are supported.
4657 assert_rewrap(
4658 indoc! {"
4659 # ˇ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.
4660 "},
4661 indoc! {"
4662 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4663 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4664 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4665 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4666 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
4667 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
4668 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
4669 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
4670 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
4671 # accumsan eros.
4672 "},
4673 language_with_pound_comments.clone(),
4674 &mut cx,
4675 );
4676
4677 // Test that rewrapping is ignored outside of comments in most languages.
4678 assert_rewrap(
4679 indoc! {"
4680 /// Adds two numbers.
4681 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4682 fn add(a: u32, b: u32) -> u32 {
4683 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ˇ
4684 }
4685 "},
4686 indoc! {"
4687 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
4688 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4689 fn add(a: u32, b: u32) -> u32 {
4690 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ˇ
4691 }
4692 "},
4693 language_with_doc_comments.clone(),
4694 &mut cx,
4695 );
4696
4697 // Test that rewrapping works in Markdown and Plain Text languages.
4698 assert_rewrap(
4699 indoc! {"
4700 # Hello
4701
4702 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.
4703 "},
4704 indoc! {"
4705 # Hello
4706
4707 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4708 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4709 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4710 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4711 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4712 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4713 Integer sit amet scelerisque nisi.
4714 "},
4715 markdown_language,
4716 &mut cx,
4717 );
4718
4719 assert_rewrap(
4720 indoc! {"
4721 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.
4722 "},
4723 indoc! {"
4724 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4725 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4726 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4727 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4728 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4729 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4730 Integer sit amet scelerisque nisi.
4731 "},
4732 plaintext_language,
4733 &mut cx,
4734 );
4735
4736 // Test rewrapping unaligned comments in a selection.
4737 assert_rewrap(
4738 indoc! {"
4739 fn foo() {
4740 if true {
4741 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4742 // Praesent semper egestas tellus id dignissim.ˇ»
4743 do_something();
4744 } else {
4745 //
4746 }
4747 }
4748 "},
4749 indoc! {"
4750 fn foo() {
4751 if true {
4752 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4753 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4754 // egestas tellus id dignissim.ˇ»
4755 do_something();
4756 } else {
4757 //
4758 }
4759 }
4760 "},
4761 language_with_doc_comments.clone(),
4762 &mut cx,
4763 );
4764
4765 assert_rewrap(
4766 indoc! {"
4767 fn foo() {
4768 if true {
4769 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4770 // Praesent semper egestas tellus id dignissim.»
4771 do_something();
4772 } else {
4773 //
4774 }
4775
4776 }
4777 "},
4778 indoc! {"
4779 fn foo() {
4780 if true {
4781 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4782 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4783 // egestas tellus id dignissim.»
4784 do_something();
4785 } else {
4786 //
4787 }
4788
4789 }
4790 "},
4791 language_with_doc_comments.clone(),
4792 &mut cx,
4793 );
4794
4795 #[track_caller]
4796 fn assert_rewrap(
4797 unwrapped_text: &str,
4798 wrapped_text: &str,
4799 language: Arc<Language>,
4800 cx: &mut EditorTestContext,
4801 ) {
4802 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4803 cx.set_state(unwrapped_text);
4804 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
4805 cx.assert_editor_state(wrapped_text);
4806 }
4807}
4808
4809#[gpui::test]
4810async fn test_hard_wrap(cx: &mut TestAppContext) {
4811 init_test(cx, |_| {});
4812 let mut cx = EditorTestContext::new(cx).await;
4813
4814 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
4815 cx.update_editor(|editor, _, cx| {
4816 editor.set_hard_wrap(Some(14), cx);
4817 });
4818
4819 cx.set_state(indoc!(
4820 "
4821 one two three ˇ
4822 "
4823 ));
4824 cx.simulate_input("four");
4825 cx.run_until_parked();
4826
4827 cx.assert_editor_state(indoc!(
4828 "
4829 one two three
4830 fourˇ
4831 "
4832 ));
4833
4834 cx.update_editor(|editor, window, cx| {
4835 editor.newline(&Default::default(), window, cx);
4836 });
4837 cx.run_until_parked();
4838 cx.assert_editor_state(indoc!(
4839 "
4840 one two three
4841 four
4842 ˇ
4843 "
4844 ));
4845
4846 cx.simulate_input("five");
4847 cx.run_until_parked();
4848 cx.assert_editor_state(indoc!(
4849 "
4850 one two three
4851 four
4852 fiveˇ
4853 "
4854 ));
4855
4856 cx.update_editor(|editor, window, cx| {
4857 editor.newline(&Default::default(), window, cx);
4858 });
4859 cx.run_until_parked();
4860 cx.simulate_input("# ");
4861 cx.run_until_parked();
4862 cx.assert_editor_state(indoc!(
4863 "
4864 one two three
4865 four
4866 five
4867 # ˇ
4868 "
4869 ));
4870
4871 cx.update_editor(|editor, window, cx| {
4872 editor.newline(&Default::default(), window, cx);
4873 });
4874 cx.run_until_parked();
4875 cx.assert_editor_state(indoc!(
4876 "
4877 one two three
4878 four
4879 five
4880 #\x20
4881 #ˇ
4882 "
4883 ));
4884
4885 cx.simulate_input(" 6");
4886 cx.run_until_parked();
4887 cx.assert_editor_state(indoc!(
4888 "
4889 one two three
4890 four
4891 five
4892 #
4893 # 6ˇ
4894 "
4895 ));
4896}
4897
4898#[gpui::test]
4899async fn test_clipboard(cx: &mut TestAppContext) {
4900 init_test(cx, |_| {});
4901
4902 let mut cx = EditorTestContext::new(cx).await;
4903
4904 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
4905 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4906 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
4907
4908 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
4909 cx.set_state("two ˇfour ˇsix ˇ");
4910 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4911 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
4912
4913 // Paste again but with only two cursors. Since the number of cursors doesn't
4914 // match the number of slices in the clipboard, the entire clipboard text
4915 // is pasted at each cursor.
4916 cx.set_state("ˇtwo one✅ four three six five ˇ");
4917 cx.update_editor(|e, window, cx| {
4918 e.handle_input("( ", window, cx);
4919 e.paste(&Paste, window, cx);
4920 e.handle_input(") ", window, cx);
4921 });
4922 cx.assert_editor_state(
4923 &([
4924 "( one✅ ",
4925 "three ",
4926 "five ) ˇtwo one✅ four three six five ( one✅ ",
4927 "three ",
4928 "five ) ˇ",
4929 ]
4930 .join("\n")),
4931 );
4932
4933 // Cut with three selections, one of which is full-line.
4934 cx.set_state(indoc! {"
4935 1«2ˇ»3
4936 4ˇ567
4937 «8ˇ»9"});
4938 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4939 cx.assert_editor_state(indoc! {"
4940 1ˇ3
4941 ˇ9"});
4942
4943 // Paste with three selections, noticing how the copied selection that was full-line
4944 // gets inserted before the second cursor.
4945 cx.set_state(indoc! {"
4946 1ˇ3
4947 9ˇ
4948 «oˇ»ne"});
4949 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4950 cx.assert_editor_state(indoc! {"
4951 12ˇ3
4952 4567
4953 9ˇ
4954 8ˇne"});
4955
4956 // Copy with a single cursor only, which writes the whole line into the clipboard.
4957 cx.set_state(indoc! {"
4958 The quick brown
4959 fox juˇmps over
4960 the lazy dog"});
4961 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4962 assert_eq!(
4963 cx.read_from_clipboard()
4964 .and_then(|item| item.text().as_deref().map(str::to_string)),
4965 Some("fox jumps over\n".to_string())
4966 );
4967
4968 // Paste with three selections, noticing how the copied full-line selection is inserted
4969 // before the empty selections but replaces the selection that is non-empty.
4970 cx.set_state(indoc! {"
4971 Tˇhe quick brown
4972 «foˇ»x jumps over
4973 tˇhe lazy dog"});
4974 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4975 cx.assert_editor_state(indoc! {"
4976 fox jumps over
4977 Tˇhe quick brown
4978 fox jumps over
4979 ˇx jumps over
4980 fox jumps over
4981 tˇhe lazy dog"});
4982}
4983
4984#[gpui::test]
4985async fn test_copy_trim(cx: &mut TestAppContext) {
4986 init_test(cx, |_| {});
4987
4988 let mut cx = EditorTestContext::new(cx).await;
4989 cx.set_state(
4990 r#" «for selection in selections.iter() {
4991 let mut start = selection.start;
4992 let mut end = selection.end;
4993 let is_entire_line = selection.is_empty();
4994 if is_entire_line {
4995 start = Point::new(start.row, 0);ˇ»
4996 end = cmp::min(max_point, Point::new(end.row + 1, 0));
4997 }
4998 "#,
4999 );
5000 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5001 assert_eq!(
5002 cx.read_from_clipboard()
5003 .and_then(|item| item.text().as_deref().map(str::to_string)),
5004 Some(
5005 "for selection in selections.iter() {
5006 let mut start = selection.start;
5007 let mut end = selection.end;
5008 let is_entire_line = selection.is_empty();
5009 if is_entire_line {
5010 start = Point::new(start.row, 0);"
5011 .to_string()
5012 ),
5013 "Regular copying preserves all indentation selected",
5014 );
5015 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5016 assert_eq!(
5017 cx.read_from_clipboard()
5018 .and_then(|item| item.text().as_deref().map(str::to_string)),
5019 Some(
5020 "for selection in selections.iter() {
5021let mut start = selection.start;
5022let mut end = selection.end;
5023let is_entire_line = selection.is_empty();
5024if is_entire_line {
5025 start = Point::new(start.row, 0);"
5026 .to_string()
5027 ),
5028 "Copying with stripping should strip all leading whitespaces"
5029 );
5030
5031 cx.set_state(
5032 r#" « for selection in selections.iter() {
5033 let mut start = selection.start;
5034 let mut end = selection.end;
5035 let is_entire_line = selection.is_empty();
5036 if is_entire_line {
5037 start = Point::new(start.row, 0);ˇ»
5038 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5039 }
5040 "#,
5041 );
5042 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5043 assert_eq!(
5044 cx.read_from_clipboard()
5045 .and_then(|item| item.text().as_deref().map(str::to_string)),
5046 Some(
5047 " for selection in selections.iter() {
5048 let mut start = selection.start;
5049 let mut end = selection.end;
5050 let is_entire_line = selection.is_empty();
5051 if is_entire_line {
5052 start = Point::new(start.row, 0);"
5053 .to_string()
5054 ),
5055 "Regular copying preserves all indentation selected",
5056 );
5057 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5058 assert_eq!(
5059 cx.read_from_clipboard()
5060 .and_then(|item| item.text().as_deref().map(str::to_string)),
5061 Some(
5062 "for selection in selections.iter() {
5063let mut start = selection.start;
5064let mut end = selection.end;
5065let is_entire_line = selection.is_empty();
5066if is_entire_line {
5067 start = Point::new(start.row, 0);"
5068 .to_string()
5069 ),
5070 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5071 );
5072
5073 cx.set_state(
5074 r#" «ˇ for selection in selections.iter() {
5075 let mut start = selection.start;
5076 let mut end = selection.end;
5077 let is_entire_line = selection.is_empty();
5078 if is_entire_line {
5079 start = Point::new(start.row, 0);»
5080 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5081 }
5082 "#,
5083 );
5084 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5085 assert_eq!(
5086 cx.read_from_clipboard()
5087 .and_then(|item| item.text().as_deref().map(str::to_string)),
5088 Some(
5089 " for selection in selections.iter() {
5090 let mut start = selection.start;
5091 let mut end = selection.end;
5092 let is_entire_line = selection.is_empty();
5093 if is_entire_line {
5094 start = Point::new(start.row, 0);"
5095 .to_string()
5096 ),
5097 "Regular copying for reverse selection works the same",
5098 );
5099 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5100 assert_eq!(
5101 cx.read_from_clipboard()
5102 .and_then(|item| item.text().as_deref().map(str::to_string)),
5103 Some(
5104 "for selection in selections.iter() {
5105let mut start = selection.start;
5106let mut end = selection.end;
5107let is_entire_line = selection.is_empty();
5108if is_entire_line {
5109 start = Point::new(start.row, 0);"
5110 .to_string()
5111 ),
5112 "Copying with stripping for reverse selection works the same"
5113 );
5114
5115 cx.set_state(
5116 r#" for selection «in selections.iter() {
5117 let mut start = selection.start;
5118 let mut end = selection.end;
5119 let is_entire_line = selection.is_empty();
5120 if is_entire_line {
5121 start = Point::new(start.row, 0);ˇ»
5122 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5123 }
5124 "#,
5125 );
5126 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5127 assert_eq!(
5128 cx.read_from_clipboard()
5129 .and_then(|item| item.text().as_deref().map(str::to_string)),
5130 Some(
5131 "in selections.iter() {
5132 let mut start = selection.start;
5133 let mut end = selection.end;
5134 let is_entire_line = selection.is_empty();
5135 if is_entire_line {
5136 start = Point::new(start.row, 0);"
5137 .to_string()
5138 ),
5139 "When selecting past the indent, the copying works as usual",
5140 );
5141 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5142 assert_eq!(
5143 cx.read_from_clipboard()
5144 .and_then(|item| item.text().as_deref().map(str::to_string)),
5145 Some(
5146 "in selections.iter() {
5147 let mut start = selection.start;
5148 let mut end = selection.end;
5149 let is_entire_line = selection.is_empty();
5150 if is_entire_line {
5151 start = Point::new(start.row, 0);"
5152 .to_string()
5153 ),
5154 "When selecting past the indent, nothing is trimmed"
5155 );
5156
5157 cx.set_state(
5158 r#" «for selection in selections.iter() {
5159 let mut start = selection.start;
5160
5161 let mut end = selection.end;
5162 let is_entire_line = selection.is_empty();
5163 if is_entire_line {
5164 start = Point::new(start.row, 0);
5165ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
5166 }
5167 "#,
5168 );
5169 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5170 assert_eq!(
5171 cx.read_from_clipboard()
5172 .and_then(|item| item.text().as_deref().map(str::to_string)),
5173 Some(
5174 "for selection in selections.iter() {
5175let mut start = selection.start;
5176
5177let mut end = selection.end;
5178let is_entire_line = selection.is_empty();
5179if is_entire_line {
5180 start = Point::new(start.row, 0);
5181"
5182 .to_string()
5183 ),
5184 "Copying with stripping should ignore empty lines"
5185 );
5186}
5187
5188#[gpui::test]
5189async fn test_paste_multiline(cx: &mut TestAppContext) {
5190 init_test(cx, |_| {});
5191
5192 let mut cx = EditorTestContext::new(cx).await;
5193 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5194
5195 // Cut an indented block, without the leading whitespace.
5196 cx.set_state(indoc! {"
5197 const a: B = (
5198 c(),
5199 «d(
5200 e,
5201 f
5202 )ˇ»
5203 );
5204 "});
5205 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5206 cx.assert_editor_state(indoc! {"
5207 const a: B = (
5208 c(),
5209 ˇ
5210 );
5211 "});
5212
5213 // Paste it at the same position.
5214 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5215 cx.assert_editor_state(indoc! {"
5216 const a: B = (
5217 c(),
5218 d(
5219 e,
5220 f
5221 )ˇ
5222 );
5223 "});
5224
5225 // Paste it at a line with a lower indent level.
5226 cx.set_state(indoc! {"
5227 ˇ
5228 const a: B = (
5229 c(),
5230 );
5231 "});
5232 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5233 cx.assert_editor_state(indoc! {"
5234 d(
5235 e,
5236 f
5237 )ˇ
5238 const a: B = (
5239 c(),
5240 );
5241 "});
5242
5243 // Cut an indented block, with the leading whitespace.
5244 cx.set_state(indoc! {"
5245 const a: B = (
5246 c(),
5247 « d(
5248 e,
5249 f
5250 )
5251 ˇ»);
5252 "});
5253 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5254 cx.assert_editor_state(indoc! {"
5255 const a: B = (
5256 c(),
5257 ˇ);
5258 "});
5259
5260 // Paste it at the same position.
5261 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5262 cx.assert_editor_state(indoc! {"
5263 const a: B = (
5264 c(),
5265 d(
5266 e,
5267 f
5268 )
5269 ˇ);
5270 "});
5271
5272 // Paste it at a line with a higher indent level.
5273 cx.set_state(indoc! {"
5274 const a: B = (
5275 c(),
5276 d(
5277 e,
5278 fˇ
5279 )
5280 );
5281 "});
5282 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5283 cx.assert_editor_state(indoc! {"
5284 const a: B = (
5285 c(),
5286 d(
5287 e,
5288 f d(
5289 e,
5290 f
5291 )
5292 ˇ
5293 )
5294 );
5295 "});
5296
5297 // Copy an indented block, starting mid-line
5298 cx.set_state(indoc! {"
5299 const a: B = (
5300 c(),
5301 somethin«g(
5302 e,
5303 f
5304 )ˇ»
5305 );
5306 "});
5307 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5308
5309 // Paste it on a line with a lower indent level
5310 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
5311 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5312 cx.assert_editor_state(indoc! {"
5313 const a: B = (
5314 c(),
5315 something(
5316 e,
5317 f
5318 )
5319 );
5320 g(
5321 e,
5322 f
5323 )ˇ"});
5324}
5325
5326#[gpui::test]
5327async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
5328 init_test(cx, |_| {});
5329
5330 cx.write_to_clipboard(ClipboardItem::new_string(
5331 " d(\n e\n );\n".into(),
5332 ));
5333
5334 let mut cx = EditorTestContext::new(cx).await;
5335 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5336
5337 cx.set_state(indoc! {"
5338 fn a() {
5339 b();
5340 if c() {
5341 ˇ
5342 }
5343 }
5344 "});
5345
5346 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5347 cx.assert_editor_state(indoc! {"
5348 fn a() {
5349 b();
5350 if c() {
5351 d(
5352 e
5353 );
5354 ˇ
5355 }
5356 }
5357 "});
5358
5359 cx.set_state(indoc! {"
5360 fn a() {
5361 b();
5362 ˇ
5363 }
5364 "});
5365
5366 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5367 cx.assert_editor_state(indoc! {"
5368 fn a() {
5369 b();
5370 d(
5371 e
5372 );
5373 ˇ
5374 }
5375 "});
5376}
5377
5378#[gpui::test]
5379fn test_select_all(cx: &mut TestAppContext) {
5380 init_test(cx, |_| {});
5381
5382 let editor = cx.add_window(|window, cx| {
5383 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
5384 build_editor(buffer, window, cx)
5385 });
5386 _ = editor.update(cx, |editor, window, cx| {
5387 editor.select_all(&SelectAll, window, cx);
5388 assert_eq!(
5389 editor.selections.display_ranges(cx),
5390 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
5391 );
5392 });
5393}
5394
5395#[gpui::test]
5396fn test_select_line(cx: &mut TestAppContext) {
5397 init_test(cx, |_| {});
5398
5399 let editor = cx.add_window(|window, cx| {
5400 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
5401 build_editor(buffer, window, cx)
5402 });
5403 _ = editor.update(cx, |editor, window, cx| {
5404 editor.change_selections(None, window, cx, |s| {
5405 s.select_display_ranges([
5406 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5407 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5408 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5409 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
5410 ])
5411 });
5412 editor.select_line(&SelectLine, window, cx);
5413 assert_eq!(
5414 editor.selections.display_ranges(cx),
5415 vec![
5416 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
5417 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
5418 ]
5419 );
5420 });
5421
5422 _ = editor.update(cx, |editor, window, cx| {
5423 editor.select_line(&SelectLine, window, cx);
5424 assert_eq!(
5425 editor.selections.display_ranges(cx),
5426 vec![
5427 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
5428 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
5429 ]
5430 );
5431 });
5432
5433 _ = editor.update(cx, |editor, window, cx| {
5434 editor.select_line(&SelectLine, window, cx);
5435 assert_eq!(
5436 editor.selections.display_ranges(cx),
5437 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
5438 );
5439 });
5440}
5441
5442#[gpui::test]
5443async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
5444 init_test(cx, |_| {});
5445 let mut cx = EditorTestContext::new(cx).await;
5446
5447 #[track_caller]
5448 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
5449 cx.set_state(initial_state);
5450 cx.update_editor(|e, window, cx| {
5451 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
5452 });
5453 cx.assert_editor_state(expected_state);
5454 }
5455
5456 // Selection starts and ends at the middle of lines, left-to-right
5457 test(
5458 &mut cx,
5459 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
5460 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5461 );
5462 // Same thing, right-to-left
5463 test(
5464 &mut cx,
5465 "aa\nb«b\ncc\ndd\neˇ»e\nff",
5466 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5467 );
5468
5469 // Whole buffer, left-to-right, last line *doesn't* end with newline
5470 test(
5471 &mut cx,
5472 "«ˇaa\nbb\ncc\ndd\nee\nff»",
5473 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5474 );
5475 // Same thing, right-to-left
5476 test(
5477 &mut cx,
5478 "«aa\nbb\ncc\ndd\nee\nffˇ»",
5479 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5480 );
5481
5482 // Whole buffer, left-to-right, last line ends with newline
5483 test(
5484 &mut cx,
5485 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
5486 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5487 );
5488 // Same thing, right-to-left
5489 test(
5490 &mut cx,
5491 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
5492 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5493 );
5494
5495 // Starts at the end of a line, ends at the start of another
5496 test(
5497 &mut cx,
5498 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
5499 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
5500 );
5501}
5502
5503#[gpui::test]
5504async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
5505 init_test(cx, |_| {});
5506
5507 let editor = cx.add_window(|window, cx| {
5508 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
5509 build_editor(buffer, window, cx)
5510 });
5511
5512 // setup
5513 _ = editor.update(cx, |editor, window, cx| {
5514 editor.fold_creases(
5515 vec![
5516 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5517 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5518 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5519 ],
5520 true,
5521 window,
5522 cx,
5523 );
5524 assert_eq!(
5525 editor.display_text(cx),
5526 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5527 );
5528 });
5529
5530 _ = editor.update(cx, |editor, window, cx| {
5531 editor.change_selections(None, window, cx, |s| {
5532 s.select_display_ranges([
5533 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5534 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5535 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5536 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
5537 ])
5538 });
5539 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5540 assert_eq!(
5541 editor.display_text(cx),
5542 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5543 );
5544 });
5545 EditorTestContext::for_editor(editor, cx)
5546 .await
5547 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
5548
5549 _ = editor.update(cx, |editor, window, cx| {
5550 editor.change_selections(None, window, cx, |s| {
5551 s.select_display_ranges([
5552 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
5553 ])
5554 });
5555 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5556 assert_eq!(
5557 editor.display_text(cx),
5558 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
5559 );
5560 assert_eq!(
5561 editor.selections.display_ranges(cx),
5562 [
5563 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
5564 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
5565 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
5566 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
5567 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
5568 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
5569 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
5570 ]
5571 );
5572 });
5573 EditorTestContext::for_editor(editor, cx)
5574 .await
5575 .assert_editor_state(
5576 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
5577 );
5578}
5579
5580#[gpui::test]
5581async fn test_add_selection_above_below(cx: &mut TestAppContext) {
5582 init_test(cx, |_| {});
5583
5584 let mut cx = EditorTestContext::new(cx).await;
5585
5586 cx.set_state(indoc!(
5587 r#"abc
5588 defˇghi
5589
5590 jk
5591 nlmo
5592 "#
5593 ));
5594
5595 cx.update_editor(|editor, window, cx| {
5596 editor.add_selection_above(&Default::default(), window, cx);
5597 });
5598
5599 cx.assert_editor_state(indoc!(
5600 r#"abcˇ
5601 defˇghi
5602
5603 jk
5604 nlmo
5605 "#
5606 ));
5607
5608 cx.update_editor(|editor, window, cx| {
5609 editor.add_selection_above(&Default::default(), window, cx);
5610 });
5611
5612 cx.assert_editor_state(indoc!(
5613 r#"abcˇ
5614 defˇghi
5615
5616 jk
5617 nlmo
5618 "#
5619 ));
5620
5621 cx.update_editor(|editor, window, cx| {
5622 editor.add_selection_below(&Default::default(), window, cx);
5623 });
5624
5625 cx.assert_editor_state(indoc!(
5626 r#"abc
5627 defˇghi
5628
5629 jk
5630 nlmo
5631 "#
5632 ));
5633
5634 cx.update_editor(|editor, window, cx| {
5635 editor.undo_selection(&Default::default(), window, cx);
5636 });
5637
5638 cx.assert_editor_state(indoc!(
5639 r#"abcˇ
5640 defˇghi
5641
5642 jk
5643 nlmo
5644 "#
5645 ));
5646
5647 cx.update_editor(|editor, window, cx| {
5648 editor.redo_selection(&Default::default(), window, cx);
5649 });
5650
5651 cx.assert_editor_state(indoc!(
5652 r#"abc
5653 defˇghi
5654
5655 jk
5656 nlmo
5657 "#
5658 ));
5659
5660 cx.update_editor(|editor, window, cx| {
5661 editor.add_selection_below(&Default::default(), window, cx);
5662 });
5663
5664 cx.assert_editor_state(indoc!(
5665 r#"abc
5666 defˇghi
5667
5668 jk
5669 nlmˇo
5670 "#
5671 ));
5672
5673 cx.update_editor(|editor, window, cx| {
5674 editor.add_selection_below(&Default::default(), window, cx);
5675 });
5676
5677 cx.assert_editor_state(indoc!(
5678 r#"abc
5679 defˇghi
5680
5681 jk
5682 nlmˇo
5683 "#
5684 ));
5685
5686 // change selections
5687 cx.set_state(indoc!(
5688 r#"abc
5689 def«ˇg»hi
5690
5691 jk
5692 nlmo
5693 "#
5694 ));
5695
5696 cx.update_editor(|editor, window, cx| {
5697 editor.add_selection_below(&Default::default(), window, cx);
5698 });
5699
5700 cx.assert_editor_state(indoc!(
5701 r#"abc
5702 def«ˇg»hi
5703
5704 jk
5705 nlm«ˇo»
5706 "#
5707 ));
5708
5709 cx.update_editor(|editor, window, cx| {
5710 editor.add_selection_below(&Default::default(), window, cx);
5711 });
5712
5713 cx.assert_editor_state(indoc!(
5714 r#"abc
5715 def«ˇg»hi
5716
5717 jk
5718 nlm«ˇo»
5719 "#
5720 ));
5721
5722 cx.update_editor(|editor, window, cx| {
5723 editor.add_selection_above(&Default::default(), window, cx);
5724 });
5725
5726 cx.assert_editor_state(indoc!(
5727 r#"abc
5728 def«ˇg»hi
5729
5730 jk
5731 nlmo
5732 "#
5733 ));
5734
5735 cx.update_editor(|editor, window, cx| {
5736 editor.add_selection_above(&Default::default(), window, cx);
5737 });
5738
5739 cx.assert_editor_state(indoc!(
5740 r#"abc
5741 def«ˇg»hi
5742
5743 jk
5744 nlmo
5745 "#
5746 ));
5747
5748 // Change selections again
5749 cx.set_state(indoc!(
5750 r#"a«bc
5751 defgˇ»hi
5752
5753 jk
5754 nlmo
5755 "#
5756 ));
5757
5758 cx.update_editor(|editor, window, cx| {
5759 editor.add_selection_below(&Default::default(), window, cx);
5760 });
5761
5762 cx.assert_editor_state(indoc!(
5763 r#"a«bcˇ»
5764 d«efgˇ»hi
5765
5766 j«kˇ»
5767 nlmo
5768 "#
5769 ));
5770
5771 cx.update_editor(|editor, window, cx| {
5772 editor.add_selection_below(&Default::default(), window, cx);
5773 });
5774 cx.assert_editor_state(indoc!(
5775 r#"a«bcˇ»
5776 d«efgˇ»hi
5777
5778 j«kˇ»
5779 n«lmoˇ»
5780 "#
5781 ));
5782 cx.update_editor(|editor, window, cx| {
5783 editor.add_selection_above(&Default::default(), window, cx);
5784 });
5785
5786 cx.assert_editor_state(indoc!(
5787 r#"a«bcˇ»
5788 d«efgˇ»hi
5789
5790 j«kˇ»
5791 nlmo
5792 "#
5793 ));
5794
5795 // Change selections again
5796 cx.set_state(indoc!(
5797 r#"abc
5798 d«ˇefghi
5799
5800 jk
5801 nlm»o
5802 "#
5803 ));
5804
5805 cx.update_editor(|editor, window, cx| {
5806 editor.add_selection_above(&Default::default(), window, cx);
5807 });
5808
5809 cx.assert_editor_state(indoc!(
5810 r#"a«ˇbc»
5811 d«ˇef»ghi
5812
5813 j«ˇk»
5814 n«ˇlm»o
5815 "#
5816 ));
5817
5818 cx.update_editor(|editor, window, cx| {
5819 editor.add_selection_below(&Default::default(), window, cx);
5820 });
5821
5822 cx.assert_editor_state(indoc!(
5823 r#"abc
5824 d«ˇef»ghi
5825
5826 j«ˇk»
5827 n«ˇlm»o
5828 "#
5829 ));
5830}
5831
5832#[gpui::test]
5833async fn test_select_next(cx: &mut TestAppContext) {
5834 init_test(cx, |_| {});
5835
5836 let mut cx = EditorTestContext::new(cx).await;
5837 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5838
5839 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5840 .unwrap();
5841 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5842
5843 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5844 .unwrap();
5845 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5846
5847 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5848 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5849
5850 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5851 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5852
5853 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5854 .unwrap();
5855 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5856
5857 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5858 .unwrap();
5859 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5860
5861 // Test selection direction should be preserved
5862 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
5863
5864 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5865 .unwrap();
5866 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
5867}
5868
5869#[gpui::test]
5870async fn test_select_all_matches(cx: &mut TestAppContext) {
5871 init_test(cx, |_| {});
5872
5873 let mut cx = EditorTestContext::new(cx).await;
5874
5875 // Test caret-only selections
5876 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5877 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5878 .unwrap();
5879 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5880
5881 // Test left-to-right selections
5882 cx.set_state("abc\n«abcˇ»\nabc");
5883 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5884 .unwrap();
5885 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
5886
5887 // Test right-to-left selections
5888 cx.set_state("abc\n«ˇabc»\nabc");
5889 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5890 .unwrap();
5891 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
5892
5893 // Test selecting whitespace with caret selection
5894 cx.set_state("abc\nˇ abc\nabc");
5895 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5896 .unwrap();
5897 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
5898
5899 // Test selecting whitespace with left-to-right selection
5900 cx.set_state("abc\n«ˇ »abc\nabc");
5901 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5902 .unwrap();
5903 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
5904
5905 // Test no matches with right-to-left selection
5906 cx.set_state("abc\n« ˇ»abc\nabc");
5907 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5908 .unwrap();
5909 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
5910}
5911
5912#[gpui::test]
5913async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
5914 init_test(cx, |_| {});
5915
5916 let mut cx = EditorTestContext::new(cx).await;
5917
5918 let large_body_1 = "\nd".repeat(200);
5919 let large_body_2 = "\ne".repeat(200);
5920
5921 cx.set_state(&format!(
5922 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
5923 ));
5924 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
5925 let scroll_position = editor.scroll_position(cx);
5926 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
5927 scroll_position
5928 });
5929
5930 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5931 .unwrap();
5932 cx.assert_editor_state(&format!(
5933 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
5934 ));
5935 let scroll_position_after_selection =
5936 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
5937 assert_eq!(
5938 initial_scroll_position, scroll_position_after_selection,
5939 "Scroll position should not change after selecting all matches"
5940 );
5941}
5942
5943#[gpui::test]
5944async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
5945 init_test(cx, |_| {});
5946
5947 let mut cx = EditorLspTestContext::new_rust(
5948 lsp::ServerCapabilities {
5949 document_formatting_provider: Some(lsp::OneOf::Left(true)),
5950 ..Default::default()
5951 },
5952 cx,
5953 )
5954 .await;
5955
5956 cx.set_state(indoc! {"
5957 line 1
5958 line 2
5959 linˇe 3
5960 line 4
5961 line 5
5962 "});
5963
5964 // Make an edit
5965 cx.update_editor(|editor, window, cx| {
5966 editor.handle_input("X", window, cx);
5967 });
5968
5969 // Move cursor to a different position
5970 cx.update_editor(|editor, window, cx| {
5971 editor.change_selections(None, window, cx, |s| {
5972 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
5973 });
5974 });
5975
5976 cx.assert_editor_state(indoc! {"
5977 line 1
5978 line 2
5979 linXe 3
5980 line 4
5981 liˇne 5
5982 "});
5983
5984 cx.lsp
5985 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
5986 Ok(Some(vec![lsp::TextEdit::new(
5987 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
5988 "PREFIX ".to_string(),
5989 )]))
5990 });
5991
5992 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
5993 .unwrap()
5994 .await
5995 .unwrap();
5996
5997 cx.assert_editor_state(indoc! {"
5998 PREFIX line 1
5999 line 2
6000 linXe 3
6001 line 4
6002 liˇne 5
6003 "});
6004
6005 // Undo formatting
6006 cx.update_editor(|editor, window, cx| {
6007 editor.undo(&Default::default(), window, cx);
6008 });
6009
6010 // Verify cursor moved back to position after edit
6011 cx.assert_editor_state(indoc! {"
6012 line 1
6013 line 2
6014 linXˇe 3
6015 line 4
6016 line 5
6017 "});
6018}
6019
6020#[gpui::test]
6021async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
6022 init_test(cx, |_| {});
6023
6024 let mut cx = EditorTestContext::new(cx).await;
6025 cx.set_state(
6026 r#"let foo = 2;
6027lˇet foo = 2;
6028let fooˇ = 2;
6029let foo = 2;
6030let foo = ˇ2;"#,
6031 );
6032
6033 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6034 .unwrap();
6035 cx.assert_editor_state(
6036 r#"let foo = 2;
6037«letˇ» foo = 2;
6038let «fooˇ» = 2;
6039let foo = 2;
6040let foo = «2ˇ»;"#,
6041 );
6042
6043 // noop for multiple selections with different contents
6044 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6045 .unwrap();
6046 cx.assert_editor_state(
6047 r#"let foo = 2;
6048«letˇ» foo = 2;
6049let «fooˇ» = 2;
6050let foo = 2;
6051let foo = «2ˇ»;"#,
6052 );
6053
6054 // Test last selection direction should be preserved
6055 cx.set_state(
6056 r#"let foo = 2;
6057let foo = 2;
6058let «fooˇ» = 2;
6059let «ˇfoo» = 2;
6060let foo = 2;"#,
6061 );
6062
6063 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6064 .unwrap();
6065 cx.assert_editor_state(
6066 r#"let foo = 2;
6067let foo = 2;
6068let «fooˇ» = 2;
6069let «ˇfoo» = 2;
6070let «ˇfoo» = 2;"#,
6071 );
6072}
6073
6074#[gpui::test]
6075async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
6076 init_test(cx, |_| {});
6077
6078 let mut cx =
6079 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
6080
6081 cx.assert_editor_state(indoc! {"
6082 ˇbbb
6083 ccc
6084
6085 bbb
6086 ccc
6087 "});
6088 cx.dispatch_action(SelectPrevious::default());
6089 cx.assert_editor_state(indoc! {"
6090 «bbbˇ»
6091 ccc
6092
6093 bbb
6094 ccc
6095 "});
6096 cx.dispatch_action(SelectPrevious::default());
6097 cx.assert_editor_state(indoc! {"
6098 «bbbˇ»
6099 ccc
6100
6101 «bbbˇ»
6102 ccc
6103 "});
6104}
6105
6106#[gpui::test]
6107async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
6108 init_test(cx, |_| {});
6109
6110 let mut cx = EditorTestContext::new(cx).await;
6111 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6112
6113 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6114 .unwrap();
6115 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6116
6117 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6118 .unwrap();
6119 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6120
6121 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6122 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6123
6124 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6125 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6126
6127 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6128 .unwrap();
6129 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
6130
6131 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6132 .unwrap();
6133 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6134}
6135
6136#[gpui::test]
6137async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
6138 init_test(cx, |_| {});
6139
6140 let mut cx = EditorTestContext::new(cx).await;
6141 cx.set_state("aˇ");
6142
6143 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6144 .unwrap();
6145 cx.assert_editor_state("«aˇ»");
6146 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6147 .unwrap();
6148 cx.assert_editor_state("«aˇ»");
6149}
6150
6151#[gpui::test]
6152async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
6153 init_test(cx, |_| {});
6154
6155 let mut cx = EditorTestContext::new(cx).await;
6156 cx.set_state(
6157 r#"let foo = 2;
6158lˇet foo = 2;
6159let fooˇ = 2;
6160let foo = 2;
6161let foo = ˇ2;"#,
6162 );
6163
6164 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6165 .unwrap();
6166 cx.assert_editor_state(
6167 r#"let foo = 2;
6168«letˇ» foo = 2;
6169let «fooˇ» = 2;
6170let foo = 2;
6171let foo = «2ˇ»;"#,
6172 );
6173
6174 // noop for multiple selections with different contents
6175 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6176 .unwrap();
6177 cx.assert_editor_state(
6178 r#"let foo = 2;
6179«letˇ» foo = 2;
6180let «fooˇ» = 2;
6181let foo = 2;
6182let foo = «2ˇ»;"#,
6183 );
6184}
6185
6186#[gpui::test]
6187async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
6188 init_test(cx, |_| {});
6189
6190 let mut cx = EditorTestContext::new(cx).await;
6191 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6192
6193 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6194 .unwrap();
6195 // selection direction is preserved
6196 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
6197
6198 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6199 .unwrap();
6200 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
6201
6202 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6203 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
6204
6205 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6206 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
6207
6208 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6209 .unwrap();
6210 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
6211
6212 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6213 .unwrap();
6214 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
6215}
6216
6217#[gpui::test]
6218async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
6219 init_test(cx, |_| {});
6220
6221 let language = Arc::new(Language::new(
6222 LanguageConfig::default(),
6223 Some(tree_sitter_rust::LANGUAGE.into()),
6224 ));
6225
6226 let text = r#"
6227 use mod1::mod2::{mod3, mod4};
6228
6229 fn fn_1(param1: bool, param2: &str) {
6230 let var1 = "text";
6231 }
6232 "#
6233 .unindent();
6234
6235 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6236 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6237 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6238
6239 editor
6240 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6241 .await;
6242
6243 editor.update_in(cx, |editor, window, cx| {
6244 editor.change_selections(None, window, cx, |s| {
6245 s.select_display_ranges([
6246 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
6247 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
6248 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
6249 ]);
6250 });
6251 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6252 });
6253 editor.update(cx, |editor, cx| {
6254 assert_text_with_selections(
6255 editor,
6256 indoc! {r#"
6257 use mod1::mod2::{mod3, «mod4ˇ»};
6258
6259 fn fn_1«ˇ(param1: bool, param2: &str)» {
6260 let var1 = "«ˇtext»";
6261 }
6262 "#},
6263 cx,
6264 );
6265 });
6266
6267 editor.update_in(cx, |editor, window, cx| {
6268 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6269 });
6270 editor.update(cx, |editor, cx| {
6271 assert_text_with_selections(
6272 editor,
6273 indoc! {r#"
6274 use mod1::mod2::«{mod3, mod4}ˇ»;
6275
6276 «ˇfn fn_1(param1: bool, param2: &str) {
6277 let var1 = "text";
6278 }»
6279 "#},
6280 cx,
6281 );
6282 });
6283
6284 editor.update_in(cx, |editor, window, cx| {
6285 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6286 });
6287 assert_eq!(
6288 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6289 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6290 );
6291
6292 // Trying to expand the selected syntax node one more time has no effect.
6293 editor.update_in(cx, |editor, window, cx| {
6294 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6295 });
6296 assert_eq!(
6297 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6298 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6299 );
6300
6301 editor.update_in(cx, |editor, window, cx| {
6302 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6303 });
6304 editor.update(cx, |editor, cx| {
6305 assert_text_with_selections(
6306 editor,
6307 indoc! {r#"
6308 use mod1::mod2::«{mod3, mod4}ˇ»;
6309
6310 «ˇfn fn_1(param1: bool, param2: &str) {
6311 let var1 = "text";
6312 }»
6313 "#},
6314 cx,
6315 );
6316 });
6317
6318 editor.update_in(cx, |editor, window, cx| {
6319 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6320 });
6321 editor.update(cx, |editor, cx| {
6322 assert_text_with_selections(
6323 editor,
6324 indoc! {r#"
6325 use mod1::mod2::{mod3, «mod4ˇ»};
6326
6327 fn fn_1«ˇ(param1: bool, param2: &str)» {
6328 let var1 = "«ˇtext»";
6329 }
6330 "#},
6331 cx,
6332 );
6333 });
6334
6335 editor.update_in(cx, |editor, window, cx| {
6336 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6337 });
6338 editor.update(cx, |editor, cx| {
6339 assert_text_with_selections(
6340 editor,
6341 indoc! {r#"
6342 use mod1::mod2::{mod3, mo«ˇ»d4};
6343
6344 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6345 let var1 = "te«ˇ»xt";
6346 }
6347 "#},
6348 cx,
6349 );
6350 });
6351
6352 // Trying to shrink the selected syntax node one more time has no effect.
6353 editor.update_in(cx, |editor, window, cx| {
6354 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6355 });
6356 editor.update_in(cx, |editor, _, cx| {
6357 assert_text_with_selections(
6358 editor,
6359 indoc! {r#"
6360 use mod1::mod2::{mod3, mo«ˇ»d4};
6361
6362 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6363 let var1 = "te«ˇ»xt";
6364 }
6365 "#},
6366 cx,
6367 );
6368 });
6369
6370 // Ensure that we keep expanding the selection if the larger selection starts or ends within
6371 // a fold.
6372 editor.update_in(cx, |editor, window, cx| {
6373 editor.fold_creases(
6374 vec![
6375 Crease::simple(
6376 Point::new(0, 21)..Point::new(0, 24),
6377 FoldPlaceholder::test(),
6378 ),
6379 Crease::simple(
6380 Point::new(3, 20)..Point::new(3, 22),
6381 FoldPlaceholder::test(),
6382 ),
6383 ],
6384 true,
6385 window,
6386 cx,
6387 );
6388 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6389 });
6390 editor.update(cx, |editor, cx| {
6391 assert_text_with_selections(
6392 editor,
6393 indoc! {r#"
6394 use mod1::mod2::«{mod3, mod4}ˇ»;
6395
6396 fn fn_1«ˇ(param1: bool, param2: &str)» {
6397 let var1 = "«ˇtext»";
6398 }
6399 "#},
6400 cx,
6401 );
6402 });
6403}
6404
6405#[gpui::test]
6406async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
6407 init_test(cx, |_| {});
6408
6409 let language = Arc::new(Language::new(
6410 LanguageConfig::default(),
6411 Some(tree_sitter_rust::LANGUAGE.into()),
6412 ));
6413
6414 let text = r#"
6415 use mod1::mod2::{mod3, mod4};
6416
6417 fn fn_1(param1: bool, param2: &str) {
6418 let var1 = "hello world";
6419 }
6420 "#
6421 .unindent();
6422
6423 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6424 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6425 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6426
6427 editor
6428 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6429 .await;
6430
6431 // Test 1: Cursor on a letter of a string word
6432 editor.update_in(cx, |editor, window, cx| {
6433 editor.change_selections(None, window, cx, |s| {
6434 s.select_display_ranges([
6435 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
6436 ]);
6437 });
6438 });
6439 editor.update_in(cx, |editor, window, cx| {
6440 assert_text_with_selections(
6441 editor,
6442 indoc! {r#"
6443 use mod1::mod2::{mod3, mod4};
6444
6445 fn fn_1(param1: bool, param2: &str) {
6446 let var1 = "hˇello world";
6447 }
6448 "#},
6449 cx,
6450 );
6451 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6452 assert_text_with_selections(
6453 editor,
6454 indoc! {r#"
6455 use mod1::mod2::{mod3, mod4};
6456
6457 fn fn_1(param1: bool, param2: &str) {
6458 let var1 = "«ˇhello» world";
6459 }
6460 "#},
6461 cx,
6462 );
6463 });
6464
6465 // Test 2: Partial selection within a word
6466 editor.update_in(cx, |editor, window, cx| {
6467 editor.change_selections(None, window, cx, |s| {
6468 s.select_display_ranges([
6469 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
6470 ]);
6471 });
6472 });
6473 editor.update_in(cx, |editor, window, cx| {
6474 assert_text_with_selections(
6475 editor,
6476 indoc! {r#"
6477 use mod1::mod2::{mod3, mod4};
6478
6479 fn fn_1(param1: bool, param2: &str) {
6480 let var1 = "h«elˇ»lo world";
6481 }
6482 "#},
6483 cx,
6484 );
6485 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6486 assert_text_with_selections(
6487 editor,
6488 indoc! {r#"
6489 use mod1::mod2::{mod3, mod4};
6490
6491 fn fn_1(param1: bool, param2: &str) {
6492 let var1 = "«ˇhello» world";
6493 }
6494 "#},
6495 cx,
6496 );
6497 });
6498
6499 // Test 3: Complete word already selected
6500 editor.update_in(cx, |editor, window, cx| {
6501 editor.change_selections(None, window, cx, |s| {
6502 s.select_display_ranges([
6503 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
6504 ]);
6505 });
6506 });
6507 editor.update_in(cx, |editor, window, cx| {
6508 assert_text_with_selections(
6509 editor,
6510 indoc! {r#"
6511 use mod1::mod2::{mod3, mod4};
6512
6513 fn fn_1(param1: bool, param2: &str) {
6514 let var1 = "«helloˇ» world";
6515 }
6516 "#},
6517 cx,
6518 );
6519 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6520 assert_text_with_selections(
6521 editor,
6522 indoc! {r#"
6523 use mod1::mod2::{mod3, mod4};
6524
6525 fn fn_1(param1: bool, param2: &str) {
6526 let var1 = "«hello worldˇ»";
6527 }
6528 "#},
6529 cx,
6530 );
6531 });
6532
6533 // Test 4: Selection spanning across words
6534 editor.update_in(cx, |editor, window, cx| {
6535 editor.change_selections(None, window, cx, |s| {
6536 s.select_display_ranges([
6537 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
6538 ]);
6539 });
6540 });
6541 editor.update_in(cx, |editor, window, cx| {
6542 assert_text_with_selections(
6543 editor,
6544 indoc! {r#"
6545 use mod1::mod2::{mod3, mod4};
6546
6547 fn fn_1(param1: bool, param2: &str) {
6548 let var1 = "hel«lo woˇ»rld";
6549 }
6550 "#},
6551 cx,
6552 );
6553 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6554 assert_text_with_selections(
6555 editor,
6556 indoc! {r#"
6557 use mod1::mod2::{mod3, mod4};
6558
6559 fn fn_1(param1: bool, param2: &str) {
6560 let var1 = "«ˇhello world»";
6561 }
6562 "#},
6563 cx,
6564 );
6565 });
6566
6567 // Test 5: Expansion beyond string
6568 editor.update_in(cx, |editor, window, cx| {
6569 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6570 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6571 assert_text_with_selections(
6572 editor,
6573 indoc! {r#"
6574 use mod1::mod2::{mod3, mod4};
6575
6576 fn fn_1(param1: bool, param2: &str) {
6577 «ˇlet var1 = "hello world";»
6578 }
6579 "#},
6580 cx,
6581 );
6582 });
6583}
6584
6585#[gpui::test]
6586async fn test_fold_function_bodies(cx: &mut TestAppContext) {
6587 init_test(cx, |_| {});
6588
6589 let base_text = r#"
6590 impl A {
6591 // this is an uncommitted comment
6592
6593 fn b() {
6594 c();
6595 }
6596
6597 // this is another uncommitted comment
6598
6599 fn d() {
6600 // e
6601 // f
6602 }
6603 }
6604
6605 fn g() {
6606 // h
6607 }
6608 "#
6609 .unindent();
6610
6611 let text = r#"
6612 ˇimpl A {
6613
6614 fn b() {
6615 c();
6616 }
6617
6618 fn d() {
6619 // e
6620 // f
6621 }
6622 }
6623
6624 fn g() {
6625 // h
6626 }
6627 "#
6628 .unindent();
6629
6630 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6631 cx.set_state(&text);
6632 cx.set_head_text(&base_text);
6633 cx.update_editor(|editor, window, cx| {
6634 editor.expand_all_diff_hunks(&Default::default(), window, cx);
6635 });
6636
6637 cx.assert_state_with_diff(
6638 "
6639 ˇimpl A {
6640 - // this is an uncommitted comment
6641
6642 fn b() {
6643 c();
6644 }
6645
6646 - // this is another uncommitted comment
6647 -
6648 fn d() {
6649 // e
6650 // f
6651 }
6652 }
6653
6654 fn g() {
6655 // h
6656 }
6657 "
6658 .unindent(),
6659 );
6660
6661 let expected_display_text = "
6662 impl A {
6663 // this is an uncommitted comment
6664
6665 fn b() {
6666 ⋯
6667 }
6668
6669 // this is another uncommitted comment
6670
6671 fn d() {
6672 ⋯
6673 }
6674 }
6675
6676 fn g() {
6677 ⋯
6678 }
6679 "
6680 .unindent();
6681
6682 cx.update_editor(|editor, window, cx| {
6683 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
6684 assert_eq!(editor.display_text(cx), expected_display_text);
6685 });
6686}
6687
6688#[gpui::test]
6689async fn test_autoindent(cx: &mut TestAppContext) {
6690 init_test(cx, |_| {});
6691
6692 let language = Arc::new(
6693 Language::new(
6694 LanguageConfig {
6695 brackets: BracketPairConfig {
6696 pairs: vec![
6697 BracketPair {
6698 start: "{".to_string(),
6699 end: "}".to_string(),
6700 close: false,
6701 surround: false,
6702 newline: true,
6703 },
6704 BracketPair {
6705 start: "(".to_string(),
6706 end: ")".to_string(),
6707 close: false,
6708 surround: false,
6709 newline: true,
6710 },
6711 ],
6712 ..Default::default()
6713 },
6714 ..Default::default()
6715 },
6716 Some(tree_sitter_rust::LANGUAGE.into()),
6717 )
6718 .with_indents_query(
6719 r#"
6720 (_ "(" ")" @end) @indent
6721 (_ "{" "}" @end) @indent
6722 "#,
6723 )
6724 .unwrap(),
6725 );
6726
6727 let text = "fn a() {}";
6728
6729 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6730 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6731 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6732 editor
6733 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6734 .await;
6735
6736 editor.update_in(cx, |editor, window, cx| {
6737 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
6738 editor.newline(&Newline, window, cx);
6739 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
6740 assert_eq!(
6741 editor.selections.ranges(cx),
6742 &[
6743 Point::new(1, 4)..Point::new(1, 4),
6744 Point::new(3, 4)..Point::new(3, 4),
6745 Point::new(5, 0)..Point::new(5, 0)
6746 ]
6747 );
6748 });
6749}
6750
6751#[gpui::test]
6752async fn test_autoindent_selections(cx: &mut TestAppContext) {
6753 init_test(cx, |_| {});
6754
6755 {
6756 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6757 cx.set_state(indoc! {"
6758 impl A {
6759
6760 fn b() {}
6761
6762 «fn c() {
6763
6764 }ˇ»
6765 }
6766 "});
6767
6768 cx.update_editor(|editor, window, cx| {
6769 editor.autoindent(&Default::default(), window, cx);
6770 });
6771
6772 cx.assert_editor_state(indoc! {"
6773 impl A {
6774
6775 fn b() {}
6776
6777 «fn c() {
6778
6779 }ˇ»
6780 }
6781 "});
6782 }
6783
6784 {
6785 let mut cx = EditorTestContext::new_multibuffer(
6786 cx,
6787 [indoc! { "
6788 impl A {
6789 «
6790 // a
6791 fn b(){}
6792 »
6793 «
6794 }
6795 fn c(){}
6796 »
6797 "}],
6798 );
6799
6800 let buffer = cx.update_editor(|editor, _, cx| {
6801 let buffer = editor.buffer().update(cx, |buffer, _| {
6802 buffer.all_buffers().iter().next().unwrap().clone()
6803 });
6804 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6805 buffer
6806 });
6807
6808 cx.run_until_parked();
6809 cx.update_editor(|editor, window, cx| {
6810 editor.select_all(&Default::default(), window, cx);
6811 editor.autoindent(&Default::default(), window, cx)
6812 });
6813 cx.run_until_parked();
6814
6815 cx.update(|_, cx| {
6816 assert_eq!(
6817 buffer.read(cx).text(),
6818 indoc! { "
6819 impl A {
6820
6821 // a
6822 fn b(){}
6823
6824
6825 }
6826 fn c(){}
6827
6828 " }
6829 )
6830 });
6831 }
6832}
6833
6834#[gpui::test]
6835async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
6836 init_test(cx, |_| {});
6837
6838 let mut cx = EditorTestContext::new(cx).await;
6839
6840 let language = Arc::new(Language::new(
6841 LanguageConfig {
6842 brackets: BracketPairConfig {
6843 pairs: vec![
6844 BracketPair {
6845 start: "{".to_string(),
6846 end: "}".to_string(),
6847 close: true,
6848 surround: true,
6849 newline: true,
6850 },
6851 BracketPair {
6852 start: "(".to_string(),
6853 end: ")".to_string(),
6854 close: true,
6855 surround: true,
6856 newline: true,
6857 },
6858 BracketPair {
6859 start: "/*".to_string(),
6860 end: " */".to_string(),
6861 close: true,
6862 surround: true,
6863 newline: true,
6864 },
6865 BracketPair {
6866 start: "[".to_string(),
6867 end: "]".to_string(),
6868 close: false,
6869 surround: false,
6870 newline: true,
6871 },
6872 BracketPair {
6873 start: "\"".to_string(),
6874 end: "\"".to_string(),
6875 close: true,
6876 surround: true,
6877 newline: false,
6878 },
6879 BracketPair {
6880 start: "<".to_string(),
6881 end: ">".to_string(),
6882 close: false,
6883 surround: true,
6884 newline: true,
6885 },
6886 ],
6887 ..Default::default()
6888 },
6889 autoclose_before: "})]".to_string(),
6890 ..Default::default()
6891 },
6892 Some(tree_sitter_rust::LANGUAGE.into()),
6893 ));
6894
6895 cx.language_registry().add(language.clone());
6896 cx.update_buffer(|buffer, cx| {
6897 buffer.set_language(Some(language), cx);
6898 });
6899
6900 cx.set_state(
6901 &r#"
6902 🏀ˇ
6903 εˇ
6904 ❤️ˇ
6905 "#
6906 .unindent(),
6907 );
6908
6909 // autoclose multiple nested brackets at multiple cursors
6910 cx.update_editor(|editor, window, cx| {
6911 editor.handle_input("{", window, cx);
6912 editor.handle_input("{", window, cx);
6913 editor.handle_input("{", window, cx);
6914 });
6915 cx.assert_editor_state(
6916 &"
6917 🏀{{{ˇ}}}
6918 ε{{{ˇ}}}
6919 ❤️{{{ˇ}}}
6920 "
6921 .unindent(),
6922 );
6923
6924 // insert a different closing bracket
6925 cx.update_editor(|editor, window, cx| {
6926 editor.handle_input(")", window, cx);
6927 });
6928 cx.assert_editor_state(
6929 &"
6930 🏀{{{)ˇ}}}
6931 ε{{{)ˇ}}}
6932 ❤️{{{)ˇ}}}
6933 "
6934 .unindent(),
6935 );
6936
6937 // skip over the auto-closed brackets when typing a closing bracket
6938 cx.update_editor(|editor, window, cx| {
6939 editor.move_right(&MoveRight, window, cx);
6940 editor.handle_input("}", window, cx);
6941 editor.handle_input("}", window, cx);
6942 editor.handle_input("}", window, cx);
6943 });
6944 cx.assert_editor_state(
6945 &"
6946 🏀{{{)}}}}ˇ
6947 ε{{{)}}}}ˇ
6948 ❤️{{{)}}}}ˇ
6949 "
6950 .unindent(),
6951 );
6952
6953 // autoclose multi-character pairs
6954 cx.set_state(
6955 &"
6956 ˇ
6957 ˇ
6958 "
6959 .unindent(),
6960 );
6961 cx.update_editor(|editor, window, cx| {
6962 editor.handle_input("/", window, cx);
6963 editor.handle_input("*", window, cx);
6964 });
6965 cx.assert_editor_state(
6966 &"
6967 /*ˇ */
6968 /*ˇ */
6969 "
6970 .unindent(),
6971 );
6972
6973 // one cursor autocloses a multi-character pair, one cursor
6974 // does not autoclose.
6975 cx.set_state(
6976 &"
6977 /ˇ
6978 ˇ
6979 "
6980 .unindent(),
6981 );
6982 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
6983 cx.assert_editor_state(
6984 &"
6985 /*ˇ */
6986 *ˇ
6987 "
6988 .unindent(),
6989 );
6990
6991 // Don't autoclose if the next character isn't whitespace and isn't
6992 // listed in the language's "autoclose_before" section.
6993 cx.set_state("ˇa b");
6994 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6995 cx.assert_editor_state("{ˇa b");
6996
6997 // Don't autoclose if `close` is false for the bracket pair
6998 cx.set_state("ˇ");
6999 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
7000 cx.assert_editor_state("[ˇ");
7001
7002 // Surround with brackets if text is selected
7003 cx.set_state("«aˇ» b");
7004 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7005 cx.assert_editor_state("{«aˇ»} b");
7006
7007 // Autoclose when not immediately after a word character
7008 cx.set_state("a ˇ");
7009 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7010 cx.assert_editor_state("a \"ˇ\"");
7011
7012 // Autoclose pair where the start and end characters are the same
7013 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7014 cx.assert_editor_state("a \"\"ˇ");
7015
7016 // Don't autoclose when immediately after a word character
7017 cx.set_state("aˇ");
7018 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7019 cx.assert_editor_state("a\"ˇ");
7020
7021 // Do autoclose when after a non-word character
7022 cx.set_state("{ˇ");
7023 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7024 cx.assert_editor_state("{\"ˇ\"");
7025
7026 // Non identical pairs autoclose regardless of preceding character
7027 cx.set_state("aˇ");
7028 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7029 cx.assert_editor_state("a{ˇ}");
7030
7031 // Don't autoclose pair if autoclose is disabled
7032 cx.set_state("ˇ");
7033 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7034 cx.assert_editor_state("<ˇ");
7035
7036 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
7037 cx.set_state("«aˇ» b");
7038 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7039 cx.assert_editor_state("<«aˇ»> b");
7040}
7041
7042#[gpui::test]
7043async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
7044 init_test(cx, |settings| {
7045 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7046 });
7047
7048 let mut cx = EditorTestContext::new(cx).await;
7049
7050 let language = Arc::new(Language::new(
7051 LanguageConfig {
7052 brackets: BracketPairConfig {
7053 pairs: vec![
7054 BracketPair {
7055 start: "{".to_string(),
7056 end: "}".to_string(),
7057 close: true,
7058 surround: true,
7059 newline: true,
7060 },
7061 BracketPair {
7062 start: "(".to_string(),
7063 end: ")".to_string(),
7064 close: true,
7065 surround: true,
7066 newline: true,
7067 },
7068 BracketPair {
7069 start: "[".to_string(),
7070 end: "]".to_string(),
7071 close: false,
7072 surround: false,
7073 newline: true,
7074 },
7075 ],
7076 ..Default::default()
7077 },
7078 autoclose_before: "})]".to_string(),
7079 ..Default::default()
7080 },
7081 Some(tree_sitter_rust::LANGUAGE.into()),
7082 ));
7083
7084 cx.language_registry().add(language.clone());
7085 cx.update_buffer(|buffer, cx| {
7086 buffer.set_language(Some(language), cx);
7087 });
7088
7089 cx.set_state(
7090 &"
7091 ˇ
7092 ˇ
7093 ˇ
7094 "
7095 .unindent(),
7096 );
7097
7098 // ensure only matching closing brackets are skipped over
7099 cx.update_editor(|editor, window, cx| {
7100 editor.handle_input("}", window, cx);
7101 editor.move_left(&MoveLeft, window, cx);
7102 editor.handle_input(")", window, cx);
7103 editor.move_left(&MoveLeft, window, cx);
7104 });
7105 cx.assert_editor_state(
7106 &"
7107 ˇ)}
7108 ˇ)}
7109 ˇ)}
7110 "
7111 .unindent(),
7112 );
7113
7114 // skip-over closing brackets at multiple cursors
7115 cx.update_editor(|editor, window, cx| {
7116 editor.handle_input(")", window, cx);
7117 editor.handle_input("}", window, cx);
7118 });
7119 cx.assert_editor_state(
7120 &"
7121 )}ˇ
7122 )}ˇ
7123 )}ˇ
7124 "
7125 .unindent(),
7126 );
7127
7128 // ignore non-close brackets
7129 cx.update_editor(|editor, window, cx| {
7130 editor.handle_input("]", window, cx);
7131 editor.move_left(&MoveLeft, window, cx);
7132 editor.handle_input("]", window, cx);
7133 });
7134 cx.assert_editor_state(
7135 &"
7136 )}]ˇ]
7137 )}]ˇ]
7138 )}]ˇ]
7139 "
7140 .unindent(),
7141 );
7142}
7143
7144#[gpui::test]
7145async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
7146 init_test(cx, |_| {});
7147
7148 let mut cx = EditorTestContext::new(cx).await;
7149
7150 let html_language = Arc::new(
7151 Language::new(
7152 LanguageConfig {
7153 name: "HTML".into(),
7154 brackets: BracketPairConfig {
7155 pairs: vec![
7156 BracketPair {
7157 start: "<".into(),
7158 end: ">".into(),
7159 close: true,
7160 ..Default::default()
7161 },
7162 BracketPair {
7163 start: "{".into(),
7164 end: "}".into(),
7165 close: true,
7166 ..Default::default()
7167 },
7168 BracketPair {
7169 start: "(".into(),
7170 end: ")".into(),
7171 close: true,
7172 ..Default::default()
7173 },
7174 ],
7175 ..Default::default()
7176 },
7177 autoclose_before: "})]>".into(),
7178 ..Default::default()
7179 },
7180 Some(tree_sitter_html::LANGUAGE.into()),
7181 )
7182 .with_injection_query(
7183 r#"
7184 (script_element
7185 (raw_text) @injection.content
7186 (#set! injection.language "javascript"))
7187 "#,
7188 )
7189 .unwrap(),
7190 );
7191
7192 let javascript_language = Arc::new(Language::new(
7193 LanguageConfig {
7194 name: "JavaScript".into(),
7195 brackets: BracketPairConfig {
7196 pairs: vec![
7197 BracketPair {
7198 start: "/*".into(),
7199 end: " */".into(),
7200 close: true,
7201 ..Default::default()
7202 },
7203 BracketPair {
7204 start: "{".into(),
7205 end: "}".into(),
7206 close: true,
7207 ..Default::default()
7208 },
7209 BracketPair {
7210 start: "(".into(),
7211 end: ")".into(),
7212 close: true,
7213 ..Default::default()
7214 },
7215 ],
7216 ..Default::default()
7217 },
7218 autoclose_before: "})]>".into(),
7219 ..Default::default()
7220 },
7221 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
7222 ));
7223
7224 cx.language_registry().add(html_language.clone());
7225 cx.language_registry().add(javascript_language.clone());
7226
7227 cx.update_buffer(|buffer, cx| {
7228 buffer.set_language(Some(html_language), cx);
7229 });
7230
7231 cx.set_state(
7232 &r#"
7233 <body>ˇ
7234 <script>
7235 var x = 1;ˇ
7236 </script>
7237 </body>ˇ
7238 "#
7239 .unindent(),
7240 );
7241
7242 // Precondition: different languages are active at different locations.
7243 cx.update_editor(|editor, window, cx| {
7244 let snapshot = editor.snapshot(window, cx);
7245 let cursors = editor.selections.ranges::<usize>(cx);
7246 let languages = cursors
7247 .iter()
7248 .map(|c| snapshot.language_at(c.start).unwrap().name())
7249 .collect::<Vec<_>>();
7250 assert_eq!(
7251 languages,
7252 &["HTML".into(), "JavaScript".into(), "HTML".into()]
7253 );
7254 });
7255
7256 // Angle brackets autoclose in HTML, but not JavaScript.
7257 cx.update_editor(|editor, window, cx| {
7258 editor.handle_input("<", window, cx);
7259 editor.handle_input("a", window, cx);
7260 });
7261 cx.assert_editor_state(
7262 &r#"
7263 <body><aˇ>
7264 <script>
7265 var x = 1;<aˇ
7266 </script>
7267 </body><aˇ>
7268 "#
7269 .unindent(),
7270 );
7271
7272 // Curly braces and parens autoclose in both HTML and JavaScript.
7273 cx.update_editor(|editor, window, cx| {
7274 editor.handle_input(" b=", window, cx);
7275 editor.handle_input("{", window, cx);
7276 editor.handle_input("c", window, cx);
7277 editor.handle_input("(", window, cx);
7278 });
7279 cx.assert_editor_state(
7280 &r#"
7281 <body><a b={c(ˇ)}>
7282 <script>
7283 var x = 1;<a b={c(ˇ)}
7284 </script>
7285 </body><a b={c(ˇ)}>
7286 "#
7287 .unindent(),
7288 );
7289
7290 // Brackets that were already autoclosed are skipped.
7291 cx.update_editor(|editor, window, cx| {
7292 editor.handle_input(")", window, cx);
7293 editor.handle_input("d", window, cx);
7294 editor.handle_input("}", window, cx);
7295 });
7296 cx.assert_editor_state(
7297 &r#"
7298 <body><a b={c()d}ˇ>
7299 <script>
7300 var x = 1;<a b={c()d}ˇ
7301 </script>
7302 </body><a b={c()d}ˇ>
7303 "#
7304 .unindent(),
7305 );
7306 cx.update_editor(|editor, window, cx| {
7307 editor.handle_input(">", window, cx);
7308 });
7309 cx.assert_editor_state(
7310 &r#"
7311 <body><a b={c()d}>ˇ
7312 <script>
7313 var x = 1;<a b={c()d}>ˇ
7314 </script>
7315 </body><a b={c()d}>ˇ
7316 "#
7317 .unindent(),
7318 );
7319
7320 // Reset
7321 cx.set_state(
7322 &r#"
7323 <body>ˇ
7324 <script>
7325 var x = 1;ˇ
7326 </script>
7327 </body>ˇ
7328 "#
7329 .unindent(),
7330 );
7331
7332 cx.update_editor(|editor, window, cx| {
7333 editor.handle_input("<", window, cx);
7334 });
7335 cx.assert_editor_state(
7336 &r#"
7337 <body><ˇ>
7338 <script>
7339 var x = 1;<ˇ
7340 </script>
7341 </body><ˇ>
7342 "#
7343 .unindent(),
7344 );
7345
7346 // When backspacing, the closing angle brackets are removed.
7347 cx.update_editor(|editor, window, cx| {
7348 editor.backspace(&Backspace, window, cx);
7349 });
7350 cx.assert_editor_state(
7351 &r#"
7352 <body>ˇ
7353 <script>
7354 var x = 1;ˇ
7355 </script>
7356 </body>ˇ
7357 "#
7358 .unindent(),
7359 );
7360
7361 // Block comments autoclose in JavaScript, but not HTML.
7362 cx.update_editor(|editor, window, cx| {
7363 editor.handle_input("/", window, cx);
7364 editor.handle_input("*", window, cx);
7365 });
7366 cx.assert_editor_state(
7367 &r#"
7368 <body>/*ˇ
7369 <script>
7370 var x = 1;/*ˇ */
7371 </script>
7372 </body>/*ˇ
7373 "#
7374 .unindent(),
7375 );
7376}
7377
7378#[gpui::test]
7379async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
7380 init_test(cx, |_| {});
7381
7382 let mut cx = EditorTestContext::new(cx).await;
7383
7384 let rust_language = Arc::new(
7385 Language::new(
7386 LanguageConfig {
7387 name: "Rust".into(),
7388 brackets: serde_json::from_value(json!([
7389 { "start": "{", "end": "}", "close": true, "newline": true },
7390 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
7391 ]))
7392 .unwrap(),
7393 autoclose_before: "})]>".into(),
7394 ..Default::default()
7395 },
7396 Some(tree_sitter_rust::LANGUAGE.into()),
7397 )
7398 .with_override_query("(string_literal) @string")
7399 .unwrap(),
7400 );
7401
7402 cx.language_registry().add(rust_language.clone());
7403 cx.update_buffer(|buffer, cx| {
7404 buffer.set_language(Some(rust_language), cx);
7405 });
7406
7407 cx.set_state(
7408 &r#"
7409 let x = ˇ
7410 "#
7411 .unindent(),
7412 );
7413
7414 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
7415 cx.update_editor(|editor, window, cx| {
7416 editor.handle_input("\"", window, cx);
7417 });
7418 cx.assert_editor_state(
7419 &r#"
7420 let x = "ˇ"
7421 "#
7422 .unindent(),
7423 );
7424
7425 // Inserting another quotation mark. The cursor moves across the existing
7426 // automatically-inserted quotation mark.
7427 cx.update_editor(|editor, window, cx| {
7428 editor.handle_input("\"", window, cx);
7429 });
7430 cx.assert_editor_state(
7431 &r#"
7432 let x = ""ˇ
7433 "#
7434 .unindent(),
7435 );
7436
7437 // Reset
7438 cx.set_state(
7439 &r#"
7440 let x = ˇ
7441 "#
7442 .unindent(),
7443 );
7444
7445 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
7446 cx.update_editor(|editor, window, cx| {
7447 editor.handle_input("\"", window, cx);
7448 editor.handle_input(" ", window, cx);
7449 editor.move_left(&Default::default(), window, cx);
7450 editor.handle_input("\\", window, cx);
7451 editor.handle_input("\"", window, cx);
7452 });
7453 cx.assert_editor_state(
7454 &r#"
7455 let x = "\"ˇ "
7456 "#
7457 .unindent(),
7458 );
7459
7460 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
7461 // mark. Nothing is inserted.
7462 cx.update_editor(|editor, window, cx| {
7463 editor.move_right(&Default::default(), window, cx);
7464 editor.handle_input("\"", window, cx);
7465 });
7466 cx.assert_editor_state(
7467 &r#"
7468 let x = "\" "ˇ
7469 "#
7470 .unindent(),
7471 );
7472}
7473
7474#[gpui::test]
7475async fn test_surround_with_pair(cx: &mut TestAppContext) {
7476 init_test(cx, |_| {});
7477
7478 let language = Arc::new(Language::new(
7479 LanguageConfig {
7480 brackets: BracketPairConfig {
7481 pairs: vec![
7482 BracketPair {
7483 start: "{".to_string(),
7484 end: "}".to_string(),
7485 close: true,
7486 surround: true,
7487 newline: true,
7488 },
7489 BracketPair {
7490 start: "/* ".to_string(),
7491 end: "*/".to_string(),
7492 close: true,
7493 surround: true,
7494 ..Default::default()
7495 },
7496 ],
7497 ..Default::default()
7498 },
7499 ..Default::default()
7500 },
7501 Some(tree_sitter_rust::LANGUAGE.into()),
7502 ));
7503
7504 let text = r#"
7505 a
7506 b
7507 c
7508 "#
7509 .unindent();
7510
7511 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7512 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7513 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7514 editor
7515 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7516 .await;
7517
7518 editor.update_in(cx, |editor, window, cx| {
7519 editor.change_selections(None, window, cx, |s| {
7520 s.select_display_ranges([
7521 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7522 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7523 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
7524 ])
7525 });
7526
7527 editor.handle_input("{", window, cx);
7528 editor.handle_input("{", window, cx);
7529 editor.handle_input("{", window, cx);
7530 assert_eq!(
7531 editor.text(cx),
7532 "
7533 {{{a}}}
7534 {{{b}}}
7535 {{{c}}}
7536 "
7537 .unindent()
7538 );
7539 assert_eq!(
7540 editor.selections.display_ranges(cx),
7541 [
7542 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
7543 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
7544 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
7545 ]
7546 );
7547
7548 editor.undo(&Undo, window, cx);
7549 editor.undo(&Undo, window, cx);
7550 editor.undo(&Undo, window, cx);
7551 assert_eq!(
7552 editor.text(cx),
7553 "
7554 a
7555 b
7556 c
7557 "
7558 .unindent()
7559 );
7560 assert_eq!(
7561 editor.selections.display_ranges(cx),
7562 [
7563 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7564 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7565 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7566 ]
7567 );
7568
7569 // Ensure inserting the first character of a multi-byte bracket pair
7570 // doesn't surround the selections with the bracket.
7571 editor.handle_input("/", window, cx);
7572 assert_eq!(
7573 editor.text(cx),
7574 "
7575 /
7576 /
7577 /
7578 "
7579 .unindent()
7580 );
7581 assert_eq!(
7582 editor.selections.display_ranges(cx),
7583 [
7584 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7585 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7586 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7587 ]
7588 );
7589
7590 editor.undo(&Undo, window, cx);
7591 assert_eq!(
7592 editor.text(cx),
7593 "
7594 a
7595 b
7596 c
7597 "
7598 .unindent()
7599 );
7600 assert_eq!(
7601 editor.selections.display_ranges(cx),
7602 [
7603 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7604 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7605 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7606 ]
7607 );
7608
7609 // Ensure inserting the last character of a multi-byte bracket pair
7610 // doesn't surround the selections with the bracket.
7611 editor.handle_input("*", window, cx);
7612 assert_eq!(
7613 editor.text(cx),
7614 "
7615 *
7616 *
7617 *
7618 "
7619 .unindent()
7620 );
7621 assert_eq!(
7622 editor.selections.display_ranges(cx),
7623 [
7624 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7625 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7626 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7627 ]
7628 );
7629 });
7630}
7631
7632#[gpui::test]
7633async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
7634 init_test(cx, |_| {});
7635
7636 let language = Arc::new(Language::new(
7637 LanguageConfig {
7638 brackets: BracketPairConfig {
7639 pairs: vec![BracketPair {
7640 start: "{".to_string(),
7641 end: "}".to_string(),
7642 close: true,
7643 surround: true,
7644 newline: true,
7645 }],
7646 ..Default::default()
7647 },
7648 autoclose_before: "}".to_string(),
7649 ..Default::default()
7650 },
7651 Some(tree_sitter_rust::LANGUAGE.into()),
7652 ));
7653
7654 let text = r#"
7655 a
7656 b
7657 c
7658 "#
7659 .unindent();
7660
7661 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7662 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7663 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7664 editor
7665 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7666 .await;
7667
7668 editor.update_in(cx, |editor, window, cx| {
7669 editor.change_selections(None, window, cx, |s| {
7670 s.select_ranges([
7671 Point::new(0, 1)..Point::new(0, 1),
7672 Point::new(1, 1)..Point::new(1, 1),
7673 Point::new(2, 1)..Point::new(2, 1),
7674 ])
7675 });
7676
7677 editor.handle_input("{", window, cx);
7678 editor.handle_input("{", window, cx);
7679 editor.handle_input("_", window, cx);
7680 assert_eq!(
7681 editor.text(cx),
7682 "
7683 a{{_}}
7684 b{{_}}
7685 c{{_}}
7686 "
7687 .unindent()
7688 );
7689 assert_eq!(
7690 editor.selections.ranges::<Point>(cx),
7691 [
7692 Point::new(0, 4)..Point::new(0, 4),
7693 Point::new(1, 4)..Point::new(1, 4),
7694 Point::new(2, 4)..Point::new(2, 4)
7695 ]
7696 );
7697
7698 editor.backspace(&Default::default(), window, cx);
7699 editor.backspace(&Default::default(), window, cx);
7700 assert_eq!(
7701 editor.text(cx),
7702 "
7703 a{}
7704 b{}
7705 c{}
7706 "
7707 .unindent()
7708 );
7709 assert_eq!(
7710 editor.selections.ranges::<Point>(cx),
7711 [
7712 Point::new(0, 2)..Point::new(0, 2),
7713 Point::new(1, 2)..Point::new(1, 2),
7714 Point::new(2, 2)..Point::new(2, 2)
7715 ]
7716 );
7717
7718 editor.delete_to_previous_word_start(&Default::default(), window, cx);
7719 assert_eq!(
7720 editor.text(cx),
7721 "
7722 a
7723 b
7724 c
7725 "
7726 .unindent()
7727 );
7728 assert_eq!(
7729 editor.selections.ranges::<Point>(cx),
7730 [
7731 Point::new(0, 1)..Point::new(0, 1),
7732 Point::new(1, 1)..Point::new(1, 1),
7733 Point::new(2, 1)..Point::new(2, 1)
7734 ]
7735 );
7736 });
7737}
7738
7739#[gpui::test]
7740async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
7741 init_test(cx, |settings| {
7742 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7743 });
7744
7745 let mut cx = EditorTestContext::new(cx).await;
7746
7747 let language = Arc::new(Language::new(
7748 LanguageConfig {
7749 brackets: BracketPairConfig {
7750 pairs: vec![
7751 BracketPair {
7752 start: "{".to_string(),
7753 end: "}".to_string(),
7754 close: true,
7755 surround: true,
7756 newline: true,
7757 },
7758 BracketPair {
7759 start: "(".to_string(),
7760 end: ")".to_string(),
7761 close: true,
7762 surround: true,
7763 newline: true,
7764 },
7765 BracketPair {
7766 start: "[".to_string(),
7767 end: "]".to_string(),
7768 close: false,
7769 surround: true,
7770 newline: true,
7771 },
7772 ],
7773 ..Default::default()
7774 },
7775 autoclose_before: "})]".to_string(),
7776 ..Default::default()
7777 },
7778 Some(tree_sitter_rust::LANGUAGE.into()),
7779 ));
7780
7781 cx.language_registry().add(language.clone());
7782 cx.update_buffer(|buffer, cx| {
7783 buffer.set_language(Some(language), cx);
7784 });
7785
7786 cx.set_state(
7787 &"
7788 {(ˇ)}
7789 [[ˇ]]
7790 {(ˇ)}
7791 "
7792 .unindent(),
7793 );
7794
7795 cx.update_editor(|editor, window, cx| {
7796 editor.backspace(&Default::default(), window, cx);
7797 editor.backspace(&Default::default(), window, cx);
7798 });
7799
7800 cx.assert_editor_state(
7801 &"
7802 ˇ
7803 ˇ]]
7804 ˇ
7805 "
7806 .unindent(),
7807 );
7808
7809 cx.update_editor(|editor, window, cx| {
7810 editor.handle_input("{", window, cx);
7811 editor.handle_input("{", window, cx);
7812 editor.move_right(&MoveRight, window, cx);
7813 editor.move_right(&MoveRight, window, cx);
7814 editor.move_left(&MoveLeft, window, cx);
7815 editor.move_left(&MoveLeft, window, cx);
7816 editor.backspace(&Default::default(), window, cx);
7817 });
7818
7819 cx.assert_editor_state(
7820 &"
7821 {ˇ}
7822 {ˇ}]]
7823 {ˇ}
7824 "
7825 .unindent(),
7826 );
7827
7828 cx.update_editor(|editor, window, cx| {
7829 editor.backspace(&Default::default(), window, cx);
7830 });
7831
7832 cx.assert_editor_state(
7833 &"
7834 ˇ
7835 ˇ]]
7836 ˇ
7837 "
7838 .unindent(),
7839 );
7840}
7841
7842#[gpui::test]
7843async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
7844 init_test(cx, |_| {});
7845
7846 let language = Arc::new(Language::new(
7847 LanguageConfig::default(),
7848 Some(tree_sitter_rust::LANGUAGE.into()),
7849 ));
7850
7851 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
7852 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7853 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7854 editor
7855 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7856 .await;
7857
7858 editor.update_in(cx, |editor, window, cx| {
7859 editor.set_auto_replace_emoji_shortcode(true);
7860
7861 editor.handle_input("Hello ", window, cx);
7862 editor.handle_input(":wave", window, cx);
7863 assert_eq!(editor.text(cx), "Hello :wave".unindent());
7864
7865 editor.handle_input(":", window, cx);
7866 assert_eq!(editor.text(cx), "Hello 👋".unindent());
7867
7868 editor.handle_input(" :smile", window, cx);
7869 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
7870
7871 editor.handle_input(":", window, cx);
7872 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
7873
7874 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
7875 editor.handle_input(":wave", window, cx);
7876 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
7877
7878 editor.handle_input(":", window, cx);
7879 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
7880
7881 editor.handle_input(":1", window, cx);
7882 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
7883
7884 editor.handle_input(":", window, cx);
7885 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
7886
7887 // Ensure shortcode does not get replaced when it is part of a word
7888 editor.handle_input(" Test:wave", window, cx);
7889 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
7890
7891 editor.handle_input(":", window, cx);
7892 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
7893
7894 editor.set_auto_replace_emoji_shortcode(false);
7895
7896 // Ensure shortcode does not get replaced when auto replace is off
7897 editor.handle_input(" :wave", window, cx);
7898 assert_eq!(
7899 editor.text(cx),
7900 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
7901 );
7902
7903 editor.handle_input(":", window, cx);
7904 assert_eq!(
7905 editor.text(cx),
7906 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
7907 );
7908 });
7909}
7910
7911#[gpui::test]
7912async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
7913 init_test(cx, |_| {});
7914
7915 let (text, insertion_ranges) = marked_text_ranges(
7916 indoc! {"
7917 ˇ
7918 "},
7919 false,
7920 );
7921
7922 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7923 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7924
7925 _ = editor.update_in(cx, |editor, window, cx| {
7926 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
7927
7928 editor
7929 .insert_snippet(&insertion_ranges, snippet, window, cx)
7930 .unwrap();
7931
7932 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7933 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7934 assert_eq!(editor.text(cx), expected_text);
7935 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7936 }
7937
7938 assert(
7939 editor,
7940 cx,
7941 indoc! {"
7942 type «» =•
7943 "},
7944 );
7945
7946 assert!(editor.context_menu_visible(), "There should be a matches");
7947 });
7948}
7949
7950#[gpui::test]
7951async fn test_snippets(cx: &mut TestAppContext) {
7952 init_test(cx, |_| {});
7953
7954 let (text, insertion_ranges) = marked_text_ranges(
7955 indoc! {"
7956 a.ˇ b
7957 a.ˇ b
7958 a.ˇ b
7959 "},
7960 false,
7961 );
7962
7963 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7964 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7965
7966 editor.update_in(cx, |editor, window, cx| {
7967 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
7968
7969 editor
7970 .insert_snippet(&insertion_ranges, snippet, window, cx)
7971 .unwrap();
7972
7973 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7974 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7975 assert_eq!(editor.text(cx), expected_text);
7976 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7977 }
7978
7979 assert(
7980 editor,
7981 cx,
7982 indoc! {"
7983 a.f(«one», two, «three») b
7984 a.f(«one», two, «three») b
7985 a.f(«one», two, «three») b
7986 "},
7987 );
7988
7989 // Can't move earlier than the first tab stop
7990 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
7991 assert(
7992 editor,
7993 cx,
7994 indoc! {"
7995 a.f(«one», two, «three») b
7996 a.f(«one», two, «three») b
7997 a.f(«one», two, «three») b
7998 "},
7999 );
8000
8001 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8002 assert(
8003 editor,
8004 cx,
8005 indoc! {"
8006 a.f(one, «two», three) b
8007 a.f(one, «two», three) b
8008 a.f(one, «two», three) b
8009 "},
8010 );
8011
8012 editor.move_to_prev_snippet_tabstop(window, cx);
8013 assert(
8014 editor,
8015 cx,
8016 indoc! {"
8017 a.f(«one», two, «three») b
8018 a.f(«one», two, «three») b
8019 a.f(«one», two, «three») b
8020 "},
8021 );
8022
8023 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8024 assert(
8025 editor,
8026 cx,
8027 indoc! {"
8028 a.f(one, «two», three) b
8029 a.f(one, «two», three) b
8030 a.f(one, «two», three) b
8031 "},
8032 );
8033 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8034 assert(
8035 editor,
8036 cx,
8037 indoc! {"
8038 a.f(one, two, three)ˇ b
8039 a.f(one, two, three)ˇ b
8040 a.f(one, two, three)ˇ b
8041 "},
8042 );
8043
8044 // As soon as the last tab stop is reached, snippet state is gone
8045 editor.move_to_prev_snippet_tabstop(window, cx);
8046 assert(
8047 editor,
8048 cx,
8049 indoc! {"
8050 a.f(one, two, three)ˇ b
8051 a.f(one, two, three)ˇ b
8052 a.f(one, two, three)ˇ b
8053 "},
8054 );
8055 });
8056}
8057
8058#[gpui::test]
8059async fn test_document_format_during_save(cx: &mut TestAppContext) {
8060 init_test(cx, |_| {});
8061
8062 let fs = FakeFs::new(cx.executor());
8063 fs.insert_file(path!("/file.rs"), Default::default()).await;
8064
8065 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
8066
8067 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8068 language_registry.add(rust_lang());
8069 let mut fake_servers = language_registry.register_fake_lsp(
8070 "Rust",
8071 FakeLspAdapter {
8072 capabilities: lsp::ServerCapabilities {
8073 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8074 ..Default::default()
8075 },
8076 ..Default::default()
8077 },
8078 );
8079
8080 let buffer = project
8081 .update(cx, |project, cx| {
8082 project.open_local_buffer(path!("/file.rs"), cx)
8083 })
8084 .await
8085 .unwrap();
8086
8087 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8088 let (editor, cx) = cx.add_window_view(|window, cx| {
8089 build_editor_with_project(project.clone(), buffer, window, cx)
8090 });
8091 editor.update_in(cx, |editor, window, cx| {
8092 editor.set_text("one\ntwo\nthree\n", window, cx)
8093 });
8094 assert!(cx.read(|cx| editor.is_dirty(cx)));
8095
8096 cx.executor().start_waiting();
8097 let fake_server = fake_servers.next().await.unwrap();
8098
8099 {
8100 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8101 move |params, _| async move {
8102 assert_eq!(
8103 params.text_document.uri,
8104 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8105 );
8106 assert_eq!(params.options.tab_size, 4);
8107 Ok(Some(vec![lsp::TextEdit::new(
8108 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8109 ", ".to_string(),
8110 )]))
8111 },
8112 );
8113 let save = editor
8114 .update_in(cx, |editor, window, cx| {
8115 editor.save(true, project.clone(), window, cx)
8116 })
8117 .unwrap();
8118 cx.executor().start_waiting();
8119 save.await;
8120
8121 assert_eq!(
8122 editor.update(cx, |editor, cx| editor.text(cx)),
8123 "one, two\nthree\n"
8124 );
8125 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8126 }
8127
8128 {
8129 editor.update_in(cx, |editor, window, cx| {
8130 editor.set_text("one\ntwo\nthree\n", window, cx)
8131 });
8132 assert!(cx.read(|cx| editor.is_dirty(cx)));
8133
8134 // Ensure we can still save even if formatting hangs.
8135 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8136 move |params, _| async move {
8137 assert_eq!(
8138 params.text_document.uri,
8139 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8140 );
8141 futures::future::pending::<()>().await;
8142 unreachable!()
8143 },
8144 );
8145 let save = editor
8146 .update_in(cx, |editor, window, cx| {
8147 editor.save(true, project.clone(), window, cx)
8148 })
8149 .unwrap();
8150 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8151 cx.executor().start_waiting();
8152 save.await;
8153 assert_eq!(
8154 editor.update(cx, |editor, cx| editor.text(cx)),
8155 "one\ntwo\nthree\n"
8156 );
8157 }
8158
8159 // For non-dirty buffer, no formatting request should be sent
8160 {
8161 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8162
8163 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8164 panic!("Should not be invoked on non-dirty buffer");
8165 });
8166 let save = editor
8167 .update_in(cx, |editor, window, cx| {
8168 editor.save(true, project.clone(), window, cx)
8169 })
8170 .unwrap();
8171 cx.executor().start_waiting();
8172 save.await;
8173 }
8174
8175 // Set rust language override and assert overridden tabsize is sent to language server
8176 update_test_language_settings(cx, |settings| {
8177 settings.languages.insert(
8178 "Rust".into(),
8179 LanguageSettingsContent {
8180 tab_size: NonZeroU32::new(8),
8181 ..Default::default()
8182 },
8183 );
8184 });
8185
8186 {
8187 editor.update_in(cx, |editor, window, cx| {
8188 editor.set_text("somehting_new\n", window, cx)
8189 });
8190 assert!(cx.read(|cx| editor.is_dirty(cx)));
8191 let _formatting_request_signal = fake_server
8192 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8193 assert_eq!(
8194 params.text_document.uri,
8195 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8196 );
8197 assert_eq!(params.options.tab_size, 8);
8198 Ok(Some(vec![]))
8199 });
8200 let save = editor
8201 .update_in(cx, |editor, window, cx| {
8202 editor.save(true, project.clone(), window, cx)
8203 })
8204 .unwrap();
8205 cx.executor().start_waiting();
8206 save.await;
8207 }
8208}
8209
8210#[gpui::test]
8211async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
8212 init_test(cx, |_| {});
8213
8214 let cols = 4;
8215 let rows = 10;
8216 let sample_text_1 = sample_text(rows, cols, 'a');
8217 assert_eq!(
8218 sample_text_1,
8219 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
8220 );
8221 let sample_text_2 = sample_text(rows, cols, 'l');
8222 assert_eq!(
8223 sample_text_2,
8224 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
8225 );
8226 let sample_text_3 = sample_text(rows, cols, 'v');
8227 assert_eq!(
8228 sample_text_3,
8229 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
8230 );
8231
8232 let fs = FakeFs::new(cx.executor());
8233 fs.insert_tree(
8234 path!("/a"),
8235 json!({
8236 "main.rs": sample_text_1,
8237 "other.rs": sample_text_2,
8238 "lib.rs": sample_text_3,
8239 }),
8240 )
8241 .await;
8242
8243 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
8244 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
8245 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
8246
8247 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8248 language_registry.add(rust_lang());
8249 let mut fake_servers = language_registry.register_fake_lsp(
8250 "Rust",
8251 FakeLspAdapter {
8252 capabilities: lsp::ServerCapabilities {
8253 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8254 ..Default::default()
8255 },
8256 ..Default::default()
8257 },
8258 );
8259
8260 let worktree = project.update(cx, |project, cx| {
8261 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
8262 assert_eq!(worktrees.len(), 1);
8263 worktrees.pop().unwrap()
8264 });
8265 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
8266
8267 let buffer_1 = project
8268 .update(cx, |project, cx| {
8269 project.open_buffer((worktree_id, "main.rs"), cx)
8270 })
8271 .await
8272 .unwrap();
8273 let buffer_2 = project
8274 .update(cx, |project, cx| {
8275 project.open_buffer((worktree_id, "other.rs"), cx)
8276 })
8277 .await
8278 .unwrap();
8279 let buffer_3 = project
8280 .update(cx, |project, cx| {
8281 project.open_buffer((worktree_id, "lib.rs"), cx)
8282 })
8283 .await
8284 .unwrap();
8285
8286 let multi_buffer = cx.new(|cx| {
8287 let mut multi_buffer = MultiBuffer::new(ReadWrite);
8288 multi_buffer.push_excerpts(
8289 buffer_1.clone(),
8290 [
8291 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8292 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8293 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8294 ],
8295 cx,
8296 );
8297 multi_buffer.push_excerpts(
8298 buffer_2.clone(),
8299 [
8300 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8301 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8302 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8303 ],
8304 cx,
8305 );
8306 multi_buffer.push_excerpts(
8307 buffer_3.clone(),
8308 [
8309 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8310 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8311 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8312 ],
8313 cx,
8314 );
8315 multi_buffer
8316 });
8317 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
8318 Editor::new(
8319 EditorMode::full(),
8320 multi_buffer,
8321 Some(project.clone()),
8322 window,
8323 cx,
8324 )
8325 });
8326
8327 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8328 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8329 s.select_ranges(Some(1..2))
8330 });
8331 editor.insert("|one|two|three|", window, cx);
8332 });
8333 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8334 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8335 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8336 s.select_ranges(Some(60..70))
8337 });
8338 editor.insert("|four|five|six|", window, cx);
8339 });
8340 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8341
8342 // First two buffers should be edited, but not the third one.
8343 assert_eq!(
8344 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8345 "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}",
8346 );
8347 buffer_1.update(cx, |buffer, _| {
8348 assert!(buffer.is_dirty());
8349 assert_eq!(
8350 buffer.text(),
8351 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
8352 )
8353 });
8354 buffer_2.update(cx, |buffer, _| {
8355 assert!(buffer.is_dirty());
8356 assert_eq!(
8357 buffer.text(),
8358 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
8359 )
8360 });
8361 buffer_3.update(cx, |buffer, _| {
8362 assert!(!buffer.is_dirty());
8363 assert_eq!(buffer.text(), sample_text_3,)
8364 });
8365 cx.executor().run_until_parked();
8366
8367 cx.executor().start_waiting();
8368 let save = multi_buffer_editor
8369 .update_in(cx, |editor, window, cx| {
8370 editor.save(true, project.clone(), window, cx)
8371 })
8372 .unwrap();
8373
8374 let fake_server = fake_servers.next().await.unwrap();
8375 fake_server
8376 .server
8377 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
8378 Ok(Some(vec![lsp::TextEdit::new(
8379 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8380 format!("[{} formatted]", params.text_document.uri),
8381 )]))
8382 })
8383 .detach();
8384 save.await;
8385
8386 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
8387 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
8388 assert_eq!(
8389 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8390 uri!(
8391 "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}"
8392 ),
8393 );
8394 buffer_1.update(cx, |buffer, _| {
8395 assert!(!buffer.is_dirty());
8396 assert_eq!(
8397 buffer.text(),
8398 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
8399 )
8400 });
8401 buffer_2.update(cx, |buffer, _| {
8402 assert!(!buffer.is_dirty());
8403 assert_eq!(
8404 buffer.text(),
8405 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
8406 )
8407 });
8408 buffer_3.update(cx, |buffer, _| {
8409 assert!(!buffer.is_dirty());
8410 assert_eq!(buffer.text(), sample_text_3,)
8411 });
8412}
8413
8414#[gpui::test]
8415async fn test_range_format_during_save(cx: &mut TestAppContext) {
8416 init_test(cx, |_| {});
8417
8418 let fs = FakeFs::new(cx.executor());
8419 fs.insert_file(path!("/file.rs"), Default::default()).await;
8420
8421 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8422
8423 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8424 language_registry.add(rust_lang());
8425 let mut fake_servers = language_registry.register_fake_lsp(
8426 "Rust",
8427 FakeLspAdapter {
8428 capabilities: lsp::ServerCapabilities {
8429 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
8430 ..Default::default()
8431 },
8432 ..Default::default()
8433 },
8434 );
8435
8436 let buffer = project
8437 .update(cx, |project, cx| {
8438 project.open_local_buffer(path!("/file.rs"), cx)
8439 })
8440 .await
8441 .unwrap();
8442
8443 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8444 let (editor, cx) = cx.add_window_view(|window, cx| {
8445 build_editor_with_project(project.clone(), buffer, window, cx)
8446 });
8447 editor.update_in(cx, |editor, window, cx| {
8448 editor.set_text("one\ntwo\nthree\n", window, cx)
8449 });
8450 assert!(cx.read(|cx| editor.is_dirty(cx)));
8451
8452 cx.executor().start_waiting();
8453 let fake_server = fake_servers.next().await.unwrap();
8454
8455 let save = editor
8456 .update_in(cx, |editor, window, cx| {
8457 editor.save(true, project.clone(), window, cx)
8458 })
8459 .unwrap();
8460 fake_server
8461 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8462 assert_eq!(
8463 params.text_document.uri,
8464 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8465 );
8466 assert_eq!(params.options.tab_size, 4);
8467 Ok(Some(vec![lsp::TextEdit::new(
8468 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8469 ", ".to_string(),
8470 )]))
8471 })
8472 .next()
8473 .await;
8474 cx.executor().start_waiting();
8475 save.await;
8476 assert_eq!(
8477 editor.update(cx, |editor, cx| editor.text(cx)),
8478 "one, two\nthree\n"
8479 );
8480 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8481
8482 editor.update_in(cx, |editor, window, cx| {
8483 editor.set_text("one\ntwo\nthree\n", window, cx)
8484 });
8485 assert!(cx.read(|cx| editor.is_dirty(cx)));
8486
8487 // Ensure we can still save even if formatting hangs.
8488 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
8489 move |params, _| async move {
8490 assert_eq!(
8491 params.text_document.uri,
8492 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8493 );
8494 futures::future::pending::<()>().await;
8495 unreachable!()
8496 },
8497 );
8498 let save = editor
8499 .update_in(cx, |editor, window, cx| {
8500 editor.save(true, project.clone(), window, cx)
8501 })
8502 .unwrap();
8503 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8504 cx.executor().start_waiting();
8505 save.await;
8506 assert_eq!(
8507 editor.update(cx, |editor, cx| editor.text(cx)),
8508 "one\ntwo\nthree\n"
8509 );
8510 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8511
8512 // For non-dirty buffer, no formatting request should be sent
8513 let save = editor
8514 .update_in(cx, |editor, window, cx| {
8515 editor.save(true, project.clone(), window, cx)
8516 })
8517 .unwrap();
8518 let _pending_format_request = fake_server
8519 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
8520 panic!("Should not be invoked on non-dirty buffer");
8521 })
8522 .next();
8523 cx.executor().start_waiting();
8524 save.await;
8525
8526 // Set Rust language override and assert overridden tabsize is sent to language server
8527 update_test_language_settings(cx, |settings| {
8528 settings.languages.insert(
8529 "Rust".into(),
8530 LanguageSettingsContent {
8531 tab_size: NonZeroU32::new(8),
8532 ..Default::default()
8533 },
8534 );
8535 });
8536
8537 editor.update_in(cx, |editor, window, cx| {
8538 editor.set_text("somehting_new\n", window, cx)
8539 });
8540 assert!(cx.read(|cx| editor.is_dirty(cx)));
8541 let save = editor
8542 .update_in(cx, |editor, window, cx| {
8543 editor.save(true, project.clone(), window, cx)
8544 })
8545 .unwrap();
8546 fake_server
8547 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8548 assert_eq!(
8549 params.text_document.uri,
8550 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8551 );
8552 assert_eq!(params.options.tab_size, 8);
8553 Ok(Some(vec![]))
8554 })
8555 .next()
8556 .await;
8557 cx.executor().start_waiting();
8558 save.await;
8559}
8560
8561#[gpui::test]
8562async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
8563 init_test(cx, |settings| {
8564 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8565 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8566 ))
8567 });
8568
8569 let fs = FakeFs::new(cx.executor());
8570 fs.insert_file(path!("/file.rs"), Default::default()).await;
8571
8572 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8573
8574 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8575 language_registry.add(Arc::new(Language::new(
8576 LanguageConfig {
8577 name: "Rust".into(),
8578 matcher: LanguageMatcher {
8579 path_suffixes: vec!["rs".to_string()],
8580 ..Default::default()
8581 },
8582 ..LanguageConfig::default()
8583 },
8584 Some(tree_sitter_rust::LANGUAGE.into()),
8585 )));
8586 update_test_language_settings(cx, |settings| {
8587 // Enable Prettier formatting for the same buffer, and ensure
8588 // LSP is called instead of Prettier.
8589 settings.defaults.prettier = Some(PrettierSettings {
8590 allowed: true,
8591 ..PrettierSettings::default()
8592 });
8593 });
8594 let mut fake_servers = language_registry.register_fake_lsp(
8595 "Rust",
8596 FakeLspAdapter {
8597 capabilities: lsp::ServerCapabilities {
8598 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8599 ..Default::default()
8600 },
8601 ..Default::default()
8602 },
8603 );
8604
8605 let buffer = project
8606 .update(cx, |project, cx| {
8607 project.open_local_buffer(path!("/file.rs"), cx)
8608 })
8609 .await
8610 .unwrap();
8611
8612 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8613 let (editor, cx) = cx.add_window_view(|window, cx| {
8614 build_editor_with_project(project.clone(), buffer, window, cx)
8615 });
8616 editor.update_in(cx, |editor, window, cx| {
8617 editor.set_text("one\ntwo\nthree\n", window, cx)
8618 });
8619
8620 cx.executor().start_waiting();
8621 let fake_server = fake_servers.next().await.unwrap();
8622
8623 let format = editor
8624 .update_in(cx, |editor, window, cx| {
8625 editor.perform_format(
8626 project.clone(),
8627 FormatTrigger::Manual,
8628 FormatTarget::Buffers,
8629 window,
8630 cx,
8631 )
8632 })
8633 .unwrap();
8634 fake_server
8635 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8636 assert_eq!(
8637 params.text_document.uri,
8638 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8639 );
8640 assert_eq!(params.options.tab_size, 4);
8641 Ok(Some(vec![lsp::TextEdit::new(
8642 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8643 ", ".to_string(),
8644 )]))
8645 })
8646 .next()
8647 .await;
8648 cx.executor().start_waiting();
8649 format.await;
8650 assert_eq!(
8651 editor.update(cx, |editor, cx| editor.text(cx)),
8652 "one, two\nthree\n"
8653 );
8654
8655 editor.update_in(cx, |editor, window, cx| {
8656 editor.set_text("one\ntwo\nthree\n", window, cx)
8657 });
8658 // Ensure we don't lock if formatting hangs.
8659 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8660 move |params, _| async move {
8661 assert_eq!(
8662 params.text_document.uri,
8663 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8664 );
8665 futures::future::pending::<()>().await;
8666 unreachable!()
8667 },
8668 );
8669 let format = editor
8670 .update_in(cx, |editor, window, cx| {
8671 editor.perform_format(
8672 project,
8673 FormatTrigger::Manual,
8674 FormatTarget::Buffers,
8675 window,
8676 cx,
8677 )
8678 })
8679 .unwrap();
8680 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8681 cx.executor().start_waiting();
8682 format.await;
8683 assert_eq!(
8684 editor.update(cx, |editor, cx| editor.text(cx)),
8685 "one\ntwo\nthree\n"
8686 );
8687}
8688
8689#[gpui::test]
8690async fn test_multiple_formatters(cx: &mut TestAppContext) {
8691 init_test(cx, |settings| {
8692 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
8693 settings.defaults.formatter =
8694 Some(language_settings::SelectedFormatter::List(FormatterList(
8695 vec![
8696 Formatter::LanguageServer { name: None },
8697 Formatter::CodeActions(
8698 [
8699 ("code-action-1".into(), true),
8700 ("code-action-2".into(), true),
8701 ]
8702 .into_iter()
8703 .collect(),
8704 ),
8705 ]
8706 .into(),
8707 )))
8708 });
8709
8710 let fs = FakeFs::new(cx.executor());
8711 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
8712 .await;
8713
8714 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8715 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8716 language_registry.add(rust_lang());
8717
8718 let mut fake_servers = language_registry.register_fake_lsp(
8719 "Rust",
8720 FakeLspAdapter {
8721 capabilities: lsp::ServerCapabilities {
8722 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8723 execute_command_provider: Some(lsp::ExecuteCommandOptions {
8724 commands: vec!["the-command-for-code-action-1".into()],
8725 ..Default::default()
8726 }),
8727 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
8728 ..Default::default()
8729 },
8730 ..Default::default()
8731 },
8732 );
8733
8734 let buffer = project
8735 .update(cx, |project, cx| {
8736 project.open_local_buffer(path!("/file.rs"), cx)
8737 })
8738 .await
8739 .unwrap();
8740
8741 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8742 let (editor, cx) = cx.add_window_view(|window, cx| {
8743 build_editor_with_project(project.clone(), buffer, window, cx)
8744 });
8745
8746 cx.executor().start_waiting();
8747
8748 let fake_server = fake_servers.next().await.unwrap();
8749 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8750 move |_params, _| async move {
8751 Ok(Some(vec![lsp::TextEdit::new(
8752 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8753 "applied-formatting\n".to_string(),
8754 )]))
8755 },
8756 );
8757 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
8758 move |params, _| async move {
8759 assert_eq!(
8760 params.context.only,
8761 Some(vec!["code-action-1".into(), "code-action-2".into()])
8762 );
8763 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
8764 Ok(Some(vec![
8765 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
8766 kind: Some("code-action-1".into()),
8767 edit: Some(lsp::WorkspaceEdit::new(
8768 [(
8769 uri.clone(),
8770 vec![lsp::TextEdit::new(
8771 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8772 "applied-code-action-1-edit\n".to_string(),
8773 )],
8774 )]
8775 .into_iter()
8776 .collect(),
8777 )),
8778 command: Some(lsp::Command {
8779 command: "the-command-for-code-action-1".into(),
8780 ..Default::default()
8781 }),
8782 ..Default::default()
8783 }),
8784 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
8785 kind: Some("code-action-2".into()),
8786 edit: Some(lsp::WorkspaceEdit::new(
8787 [(
8788 uri.clone(),
8789 vec![lsp::TextEdit::new(
8790 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8791 "applied-code-action-2-edit\n".to_string(),
8792 )],
8793 )]
8794 .into_iter()
8795 .collect(),
8796 )),
8797 ..Default::default()
8798 }),
8799 ]))
8800 },
8801 );
8802
8803 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
8804 move |params, _| async move { Ok(params) }
8805 });
8806
8807 let command_lock = Arc::new(futures::lock::Mutex::new(()));
8808 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
8809 let fake = fake_server.clone();
8810 let lock = command_lock.clone();
8811 move |params, _| {
8812 assert_eq!(params.command, "the-command-for-code-action-1");
8813 let fake = fake.clone();
8814 let lock = lock.clone();
8815 async move {
8816 lock.lock().await;
8817 fake.server
8818 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
8819 label: None,
8820 edit: lsp::WorkspaceEdit {
8821 changes: Some(
8822 [(
8823 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
8824 vec![lsp::TextEdit {
8825 range: lsp::Range::new(
8826 lsp::Position::new(0, 0),
8827 lsp::Position::new(0, 0),
8828 ),
8829 new_text: "applied-code-action-1-command\n".into(),
8830 }],
8831 )]
8832 .into_iter()
8833 .collect(),
8834 ),
8835 ..Default::default()
8836 },
8837 })
8838 .await
8839 .unwrap();
8840 Ok(Some(json!(null)))
8841 }
8842 }
8843 });
8844
8845 cx.executor().start_waiting();
8846 editor
8847 .update_in(cx, |editor, window, cx| {
8848 editor.perform_format(
8849 project.clone(),
8850 FormatTrigger::Manual,
8851 FormatTarget::Buffers,
8852 window,
8853 cx,
8854 )
8855 })
8856 .unwrap()
8857 .await;
8858 editor.update(cx, |editor, cx| {
8859 assert_eq!(
8860 editor.text(cx),
8861 r#"
8862 applied-code-action-2-edit
8863 applied-code-action-1-command
8864 applied-code-action-1-edit
8865 applied-formatting
8866 one
8867 two
8868 three
8869 "#
8870 .unindent()
8871 );
8872 });
8873
8874 editor.update_in(cx, |editor, window, cx| {
8875 editor.undo(&Default::default(), window, cx);
8876 assert_eq!(editor.text(cx), "one \ntwo \nthree");
8877 });
8878
8879 // Perform a manual edit while waiting for an LSP command
8880 // that's being run as part of a formatting code action.
8881 let lock_guard = command_lock.lock().await;
8882 let format = editor
8883 .update_in(cx, |editor, window, cx| {
8884 editor.perform_format(
8885 project.clone(),
8886 FormatTrigger::Manual,
8887 FormatTarget::Buffers,
8888 window,
8889 cx,
8890 )
8891 })
8892 .unwrap();
8893 cx.run_until_parked();
8894 editor.update(cx, |editor, cx| {
8895 assert_eq!(
8896 editor.text(cx),
8897 r#"
8898 applied-code-action-1-edit
8899 applied-formatting
8900 one
8901 two
8902 three
8903 "#
8904 .unindent()
8905 );
8906
8907 editor.buffer.update(cx, |buffer, cx| {
8908 let ix = buffer.len(cx);
8909 buffer.edit([(ix..ix, "edited\n")], None, cx);
8910 });
8911 });
8912
8913 // Allow the LSP command to proceed. Because the buffer was edited,
8914 // the second code action will not be run.
8915 drop(lock_guard);
8916 format.await;
8917 editor.update_in(cx, |editor, window, cx| {
8918 assert_eq!(
8919 editor.text(cx),
8920 r#"
8921 applied-code-action-1-command
8922 applied-code-action-1-edit
8923 applied-formatting
8924 one
8925 two
8926 three
8927 edited
8928 "#
8929 .unindent()
8930 );
8931
8932 // The manual edit is undone first, because it is the last thing the user did
8933 // (even though the command completed afterwards).
8934 editor.undo(&Default::default(), window, cx);
8935 assert_eq!(
8936 editor.text(cx),
8937 r#"
8938 applied-code-action-1-command
8939 applied-code-action-1-edit
8940 applied-formatting
8941 one
8942 two
8943 three
8944 "#
8945 .unindent()
8946 );
8947
8948 // All the formatting (including the command, which completed after the manual edit)
8949 // is undone together.
8950 editor.undo(&Default::default(), window, cx);
8951 assert_eq!(editor.text(cx), "one \ntwo \nthree");
8952 });
8953}
8954
8955#[gpui::test]
8956async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
8957 init_test(cx, |settings| {
8958 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8959 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8960 ))
8961 });
8962
8963 let fs = FakeFs::new(cx.executor());
8964 fs.insert_file(path!("/file.ts"), Default::default()).await;
8965
8966 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8967
8968 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8969 language_registry.add(Arc::new(Language::new(
8970 LanguageConfig {
8971 name: "TypeScript".into(),
8972 matcher: LanguageMatcher {
8973 path_suffixes: vec!["ts".to_string()],
8974 ..Default::default()
8975 },
8976 ..LanguageConfig::default()
8977 },
8978 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
8979 )));
8980 update_test_language_settings(cx, |settings| {
8981 settings.defaults.prettier = Some(PrettierSettings {
8982 allowed: true,
8983 ..PrettierSettings::default()
8984 });
8985 });
8986 let mut fake_servers = language_registry.register_fake_lsp(
8987 "TypeScript",
8988 FakeLspAdapter {
8989 capabilities: lsp::ServerCapabilities {
8990 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
8991 ..Default::default()
8992 },
8993 ..Default::default()
8994 },
8995 );
8996
8997 let buffer = project
8998 .update(cx, |project, cx| {
8999 project.open_local_buffer(path!("/file.ts"), cx)
9000 })
9001 .await
9002 .unwrap();
9003
9004 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9005 let (editor, cx) = cx.add_window_view(|window, cx| {
9006 build_editor_with_project(project.clone(), buffer, window, cx)
9007 });
9008 editor.update_in(cx, |editor, window, cx| {
9009 editor.set_text(
9010 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9011 window,
9012 cx,
9013 )
9014 });
9015
9016 cx.executor().start_waiting();
9017 let fake_server = fake_servers.next().await.unwrap();
9018
9019 let format = editor
9020 .update_in(cx, |editor, window, cx| {
9021 editor.perform_code_action_kind(
9022 project.clone(),
9023 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9024 window,
9025 cx,
9026 )
9027 })
9028 .unwrap();
9029 fake_server
9030 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
9031 assert_eq!(
9032 params.text_document.uri,
9033 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9034 );
9035 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
9036 lsp::CodeAction {
9037 title: "Organize Imports".to_string(),
9038 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
9039 edit: Some(lsp::WorkspaceEdit {
9040 changes: Some(
9041 [(
9042 params.text_document.uri.clone(),
9043 vec![lsp::TextEdit::new(
9044 lsp::Range::new(
9045 lsp::Position::new(1, 0),
9046 lsp::Position::new(2, 0),
9047 ),
9048 "".to_string(),
9049 )],
9050 )]
9051 .into_iter()
9052 .collect(),
9053 ),
9054 ..Default::default()
9055 }),
9056 ..Default::default()
9057 },
9058 )]))
9059 })
9060 .next()
9061 .await;
9062 cx.executor().start_waiting();
9063 format.await;
9064 assert_eq!(
9065 editor.update(cx, |editor, cx| editor.text(cx)),
9066 "import { a } from 'module';\n\nconst x = a;\n"
9067 );
9068
9069 editor.update_in(cx, |editor, window, cx| {
9070 editor.set_text(
9071 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9072 window,
9073 cx,
9074 )
9075 });
9076 // Ensure we don't lock if code action hangs.
9077 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9078 move |params, _| async move {
9079 assert_eq!(
9080 params.text_document.uri,
9081 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9082 );
9083 futures::future::pending::<()>().await;
9084 unreachable!()
9085 },
9086 );
9087 let format = editor
9088 .update_in(cx, |editor, window, cx| {
9089 editor.perform_code_action_kind(
9090 project,
9091 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9092 window,
9093 cx,
9094 )
9095 })
9096 .unwrap();
9097 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
9098 cx.executor().start_waiting();
9099 format.await;
9100 assert_eq!(
9101 editor.update(cx, |editor, cx| editor.text(cx)),
9102 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
9103 );
9104}
9105
9106#[gpui::test]
9107async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
9108 init_test(cx, |_| {});
9109
9110 let mut cx = EditorLspTestContext::new_rust(
9111 lsp::ServerCapabilities {
9112 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9113 ..Default::default()
9114 },
9115 cx,
9116 )
9117 .await;
9118
9119 cx.set_state(indoc! {"
9120 one.twoˇ
9121 "});
9122
9123 // The format request takes a long time. When it completes, it inserts
9124 // a newline and an indent before the `.`
9125 cx.lsp
9126 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
9127 let executor = cx.background_executor().clone();
9128 async move {
9129 executor.timer(Duration::from_millis(100)).await;
9130 Ok(Some(vec![lsp::TextEdit {
9131 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
9132 new_text: "\n ".into(),
9133 }]))
9134 }
9135 });
9136
9137 // Submit a format request.
9138 let format_1 = cx
9139 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9140 .unwrap();
9141 cx.executor().run_until_parked();
9142
9143 // Submit a second format request.
9144 let format_2 = cx
9145 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9146 .unwrap();
9147 cx.executor().run_until_parked();
9148
9149 // Wait for both format requests to complete
9150 cx.executor().advance_clock(Duration::from_millis(200));
9151 cx.executor().start_waiting();
9152 format_1.await.unwrap();
9153 cx.executor().start_waiting();
9154 format_2.await.unwrap();
9155
9156 // The formatting edits only happens once.
9157 cx.assert_editor_state(indoc! {"
9158 one
9159 .twoˇ
9160 "});
9161}
9162
9163#[gpui::test]
9164async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
9165 init_test(cx, |settings| {
9166 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
9167 });
9168
9169 let mut cx = EditorLspTestContext::new_rust(
9170 lsp::ServerCapabilities {
9171 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9172 ..Default::default()
9173 },
9174 cx,
9175 )
9176 .await;
9177
9178 // Set up a buffer white some trailing whitespace and no trailing newline.
9179 cx.set_state(
9180 &[
9181 "one ", //
9182 "twoˇ", //
9183 "three ", //
9184 "four", //
9185 ]
9186 .join("\n"),
9187 );
9188
9189 // Submit a format request.
9190 let format = cx
9191 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9192 .unwrap();
9193
9194 // Record which buffer changes have been sent to the language server
9195 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
9196 cx.lsp
9197 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
9198 let buffer_changes = buffer_changes.clone();
9199 move |params, _| {
9200 buffer_changes.lock().extend(
9201 params
9202 .content_changes
9203 .into_iter()
9204 .map(|e| (e.range.unwrap(), e.text)),
9205 );
9206 }
9207 });
9208
9209 // Handle formatting requests to the language server.
9210 cx.lsp
9211 .set_request_handler::<lsp::request::Formatting, _, _>({
9212 let buffer_changes = buffer_changes.clone();
9213 move |_, _| {
9214 // When formatting is requested, trailing whitespace has already been stripped,
9215 // and the trailing newline has already been added.
9216 assert_eq!(
9217 &buffer_changes.lock()[1..],
9218 &[
9219 (
9220 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
9221 "".into()
9222 ),
9223 (
9224 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
9225 "".into()
9226 ),
9227 (
9228 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
9229 "\n".into()
9230 ),
9231 ]
9232 );
9233
9234 // Insert blank lines between each line of the buffer.
9235 async move {
9236 Ok(Some(vec![
9237 lsp::TextEdit {
9238 range: lsp::Range::new(
9239 lsp::Position::new(1, 0),
9240 lsp::Position::new(1, 0),
9241 ),
9242 new_text: "\n".into(),
9243 },
9244 lsp::TextEdit {
9245 range: lsp::Range::new(
9246 lsp::Position::new(2, 0),
9247 lsp::Position::new(2, 0),
9248 ),
9249 new_text: "\n".into(),
9250 },
9251 ]))
9252 }
9253 }
9254 });
9255
9256 // After formatting the buffer, the trailing whitespace is stripped,
9257 // a newline is appended, and the edits provided by the language server
9258 // have been applied.
9259 format.await.unwrap();
9260 cx.assert_editor_state(
9261 &[
9262 "one", //
9263 "", //
9264 "twoˇ", //
9265 "", //
9266 "three", //
9267 "four", //
9268 "", //
9269 ]
9270 .join("\n"),
9271 );
9272
9273 // Undoing the formatting undoes the trailing whitespace removal, the
9274 // trailing newline, and the LSP edits.
9275 cx.update_buffer(|buffer, cx| buffer.undo(cx));
9276 cx.assert_editor_state(
9277 &[
9278 "one ", //
9279 "twoˇ", //
9280 "three ", //
9281 "four", //
9282 ]
9283 .join("\n"),
9284 );
9285}
9286
9287#[gpui::test]
9288async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
9289 cx: &mut TestAppContext,
9290) {
9291 init_test(cx, |_| {});
9292
9293 cx.update(|cx| {
9294 cx.update_global::<SettingsStore, _>(|settings, cx| {
9295 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9296 settings.auto_signature_help = Some(true);
9297 });
9298 });
9299 });
9300
9301 let mut cx = EditorLspTestContext::new_rust(
9302 lsp::ServerCapabilities {
9303 signature_help_provider: Some(lsp::SignatureHelpOptions {
9304 ..Default::default()
9305 }),
9306 ..Default::default()
9307 },
9308 cx,
9309 )
9310 .await;
9311
9312 let language = Language::new(
9313 LanguageConfig {
9314 name: "Rust".into(),
9315 brackets: BracketPairConfig {
9316 pairs: vec![
9317 BracketPair {
9318 start: "{".to_string(),
9319 end: "}".to_string(),
9320 close: true,
9321 surround: true,
9322 newline: true,
9323 },
9324 BracketPair {
9325 start: "(".to_string(),
9326 end: ")".to_string(),
9327 close: true,
9328 surround: true,
9329 newline: true,
9330 },
9331 BracketPair {
9332 start: "/*".to_string(),
9333 end: " */".to_string(),
9334 close: true,
9335 surround: true,
9336 newline: true,
9337 },
9338 BracketPair {
9339 start: "[".to_string(),
9340 end: "]".to_string(),
9341 close: false,
9342 surround: false,
9343 newline: true,
9344 },
9345 BracketPair {
9346 start: "\"".to_string(),
9347 end: "\"".to_string(),
9348 close: true,
9349 surround: true,
9350 newline: false,
9351 },
9352 BracketPair {
9353 start: "<".to_string(),
9354 end: ">".to_string(),
9355 close: false,
9356 surround: true,
9357 newline: true,
9358 },
9359 ],
9360 ..Default::default()
9361 },
9362 autoclose_before: "})]".to_string(),
9363 ..Default::default()
9364 },
9365 Some(tree_sitter_rust::LANGUAGE.into()),
9366 );
9367 let language = Arc::new(language);
9368
9369 cx.language_registry().add(language.clone());
9370 cx.update_buffer(|buffer, cx| {
9371 buffer.set_language(Some(language), cx);
9372 });
9373
9374 cx.set_state(
9375 &r#"
9376 fn main() {
9377 sampleˇ
9378 }
9379 "#
9380 .unindent(),
9381 );
9382
9383 cx.update_editor(|editor, window, cx| {
9384 editor.handle_input("(", window, cx);
9385 });
9386 cx.assert_editor_state(
9387 &"
9388 fn main() {
9389 sample(ˇ)
9390 }
9391 "
9392 .unindent(),
9393 );
9394
9395 let mocked_response = lsp::SignatureHelp {
9396 signatures: vec![lsp::SignatureInformation {
9397 label: "fn sample(param1: u8, param2: u8)".to_string(),
9398 documentation: None,
9399 parameters: Some(vec![
9400 lsp::ParameterInformation {
9401 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9402 documentation: None,
9403 },
9404 lsp::ParameterInformation {
9405 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9406 documentation: None,
9407 },
9408 ]),
9409 active_parameter: None,
9410 }],
9411 active_signature: Some(0),
9412 active_parameter: Some(0),
9413 };
9414 handle_signature_help_request(&mut cx, mocked_response).await;
9415
9416 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9417 .await;
9418
9419 cx.editor(|editor, _, _| {
9420 let signature_help_state = editor.signature_help_state.popover().cloned();
9421 assert_eq!(
9422 signature_help_state.unwrap().label,
9423 "param1: u8, param2: u8"
9424 );
9425 });
9426}
9427
9428#[gpui::test]
9429async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
9430 init_test(cx, |_| {});
9431
9432 cx.update(|cx| {
9433 cx.update_global::<SettingsStore, _>(|settings, cx| {
9434 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9435 settings.auto_signature_help = Some(false);
9436 settings.show_signature_help_after_edits = Some(false);
9437 });
9438 });
9439 });
9440
9441 let mut cx = EditorLspTestContext::new_rust(
9442 lsp::ServerCapabilities {
9443 signature_help_provider: Some(lsp::SignatureHelpOptions {
9444 ..Default::default()
9445 }),
9446 ..Default::default()
9447 },
9448 cx,
9449 )
9450 .await;
9451
9452 let language = Language::new(
9453 LanguageConfig {
9454 name: "Rust".into(),
9455 brackets: BracketPairConfig {
9456 pairs: vec![
9457 BracketPair {
9458 start: "{".to_string(),
9459 end: "}".to_string(),
9460 close: true,
9461 surround: true,
9462 newline: true,
9463 },
9464 BracketPair {
9465 start: "(".to_string(),
9466 end: ")".to_string(),
9467 close: true,
9468 surround: true,
9469 newline: true,
9470 },
9471 BracketPair {
9472 start: "/*".to_string(),
9473 end: " */".to_string(),
9474 close: true,
9475 surround: true,
9476 newline: true,
9477 },
9478 BracketPair {
9479 start: "[".to_string(),
9480 end: "]".to_string(),
9481 close: false,
9482 surround: false,
9483 newline: true,
9484 },
9485 BracketPair {
9486 start: "\"".to_string(),
9487 end: "\"".to_string(),
9488 close: true,
9489 surround: true,
9490 newline: false,
9491 },
9492 BracketPair {
9493 start: "<".to_string(),
9494 end: ">".to_string(),
9495 close: false,
9496 surround: true,
9497 newline: true,
9498 },
9499 ],
9500 ..Default::default()
9501 },
9502 autoclose_before: "})]".to_string(),
9503 ..Default::default()
9504 },
9505 Some(tree_sitter_rust::LANGUAGE.into()),
9506 );
9507 let language = Arc::new(language);
9508
9509 cx.language_registry().add(language.clone());
9510 cx.update_buffer(|buffer, cx| {
9511 buffer.set_language(Some(language), cx);
9512 });
9513
9514 // Ensure that signature_help is not called when no signature help is enabled.
9515 cx.set_state(
9516 &r#"
9517 fn main() {
9518 sampleˇ
9519 }
9520 "#
9521 .unindent(),
9522 );
9523 cx.update_editor(|editor, window, cx| {
9524 editor.handle_input("(", window, cx);
9525 });
9526 cx.assert_editor_state(
9527 &"
9528 fn main() {
9529 sample(ˇ)
9530 }
9531 "
9532 .unindent(),
9533 );
9534 cx.editor(|editor, _, _| {
9535 assert!(editor.signature_help_state.task().is_none());
9536 });
9537
9538 let mocked_response = lsp::SignatureHelp {
9539 signatures: vec![lsp::SignatureInformation {
9540 label: "fn sample(param1: u8, param2: u8)".to_string(),
9541 documentation: None,
9542 parameters: Some(vec![
9543 lsp::ParameterInformation {
9544 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9545 documentation: None,
9546 },
9547 lsp::ParameterInformation {
9548 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9549 documentation: None,
9550 },
9551 ]),
9552 active_parameter: None,
9553 }],
9554 active_signature: Some(0),
9555 active_parameter: Some(0),
9556 };
9557
9558 // Ensure that signature_help is called when enabled afte edits
9559 cx.update(|_, cx| {
9560 cx.update_global::<SettingsStore, _>(|settings, cx| {
9561 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9562 settings.auto_signature_help = Some(false);
9563 settings.show_signature_help_after_edits = Some(true);
9564 });
9565 });
9566 });
9567 cx.set_state(
9568 &r#"
9569 fn main() {
9570 sampleˇ
9571 }
9572 "#
9573 .unindent(),
9574 );
9575 cx.update_editor(|editor, window, cx| {
9576 editor.handle_input("(", window, cx);
9577 });
9578 cx.assert_editor_state(
9579 &"
9580 fn main() {
9581 sample(ˇ)
9582 }
9583 "
9584 .unindent(),
9585 );
9586 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9587 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9588 .await;
9589 cx.update_editor(|editor, _, _| {
9590 let signature_help_state = editor.signature_help_state.popover().cloned();
9591 assert!(signature_help_state.is_some());
9592 assert_eq!(
9593 signature_help_state.unwrap().label,
9594 "param1: u8, param2: u8"
9595 );
9596 editor.signature_help_state = SignatureHelpState::default();
9597 });
9598
9599 // Ensure that signature_help is called when auto signature help override is enabled
9600 cx.update(|_, cx| {
9601 cx.update_global::<SettingsStore, _>(|settings, cx| {
9602 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9603 settings.auto_signature_help = Some(true);
9604 settings.show_signature_help_after_edits = Some(false);
9605 });
9606 });
9607 });
9608 cx.set_state(
9609 &r#"
9610 fn main() {
9611 sampleˇ
9612 }
9613 "#
9614 .unindent(),
9615 );
9616 cx.update_editor(|editor, window, cx| {
9617 editor.handle_input("(", window, cx);
9618 });
9619 cx.assert_editor_state(
9620 &"
9621 fn main() {
9622 sample(ˇ)
9623 }
9624 "
9625 .unindent(),
9626 );
9627 handle_signature_help_request(&mut cx, mocked_response).await;
9628 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9629 .await;
9630 cx.editor(|editor, _, _| {
9631 let signature_help_state = editor.signature_help_state.popover().cloned();
9632 assert!(signature_help_state.is_some());
9633 assert_eq!(
9634 signature_help_state.unwrap().label,
9635 "param1: u8, param2: u8"
9636 );
9637 });
9638}
9639
9640#[gpui::test]
9641async fn test_signature_help(cx: &mut TestAppContext) {
9642 init_test(cx, |_| {});
9643 cx.update(|cx| {
9644 cx.update_global::<SettingsStore, _>(|settings, cx| {
9645 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9646 settings.auto_signature_help = Some(true);
9647 });
9648 });
9649 });
9650
9651 let mut cx = EditorLspTestContext::new_rust(
9652 lsp::ServerCapabilities {
9653 signature_help_provider: Some(lsp::SignatureHelpOptions {
9654 ..Default::default()
9655 }),
9656 ..Default::default()
9657 },
9658 cx,
9659 )
9660 .await;
9661
9662 // A test that directly calls `show_signature_help`
9663 cx.update_editor(|editor, window, cx| {
9664 editor.show_signature_help(&ShowSignatureHelp, window, cx);
9665 });
9666
9667 let mocked_response = lsp::SignatureHelp {
9668 signatures: vec![lsp::SignatureInformation {
9669 label: "fn sample(param1: u8, param2: u8)".to_string(),
9670 documentation: None,
9671 parameters: Some(vec![
9672 lsp::ParameterInformation {
9673 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9674 documentation: None,
9675 },
9676 lsp::ParameterInformation {
9677 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9678 documentation: None,
9679 },
9680 ]),
9681 active_parameter: None,
9682 }],
9683 active_signature: Some(0),
9684 active_parameter: Some(0),
9685 };
9686 handle_signature_help_request(&mut cx, mocked_response).await;
9687
9688 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9689 .await;
9690
9691 cx.editor(|editor, _, _| {
9692 let signature_help_state = editor.signature_help_state.popover().cloned();
9693 assert!(signature_help_state.is_some());
9694 assert_eq!(
9695 signature_help_state.unwrap().label,
9696 "param1: u8, param2: u8"
9697 );
9698 });
9699
9700 // When exiting outside from inside the brackets, `signature_help` is closed.
9701 cx.set_state(indoc! {"
9702 fn main() {
9703 sample(ˇ);
9704 }
9705
9706 fn sample(param1: u8, param2: u8) {}
9707 "});
9708
9709 cx.update_editor(|editor, window, cx| {
9710 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
9711 });
9712
9713 let mocked_response = lsp::SignatureHelp {
9714 signatures: Vec::new(),
9715 active_signature: None,
9716 active_parameter: None,
9717 };
9718 handle_signature_help_request(&mut cx, mocked_response).await;
9719
9720 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
9721 .await;
9722
9723 cx.editor(|editor, _, _| {
9724 assert!(!editor.signature_help_state.is_shown());
9725 });
9726
9727 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
9728 cx.set_state(indoc! {"
9729 fn main() {
9730 sample(ˇ);
9731 }
9732
9733 fn sample(param1: u8, param2: u8) {}
9734 "});
9735
9736 let mocked_response = lsp::SignatureHelp {
9737 signatures: vec![lsp::SignatureInformation {
9738 label: "fn sample(param1: u8, param2: u8)".to_string(),
9739 documentation: None,
9740 parameters: Some(vec![
9741 lsp::ParameterInformation {
9742 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9743 documentation: None,
9744 },
9745 lsp::ParameterInformation {
9746 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9747 documentation: None,
9748 },
9749 ]),
9750 active_parameter: None,
9751 }],
9752 active_signature: Some(0),
9753 active_parameter: Some(0),
9754 };
9755 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9756 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9757 .await;
9758 cx.editor(|editor, _, _| {
9759 assert!(editor.signature_help_state.is_shown());
9760 });
9761
9762 // Restore the popover with more parameter input
9763 cx.set_state(indoc! {"
9764 fn main() {
9765 sample(param1, param2ˇ);
9766 }
9767
9768 fn sample(param1: u8, param2: u8) {}
9769 "});
9770
9771 let mocked_response = lsp::SignatureHelp {
9772 signatures: vec![lsp::SignatureInformation {
9773 label: "fn sample(param1: u8, param2: u8)".to_string(),
9774 documentation: None,
9775 parameters: Some(vec![
9776 lsp::ParameterInformation {
9777 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9778 documentation: None,
9779 },
9780 lsp::ParameterInformation {
9781 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9782 documentation: None,
9783 },
9784 ]),
9785 active_parameter: None,
9786 }],
9787 active_signature: Some(0),
9788 active_parameter: Some(1),
9789 };
9790 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9791 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9792 .await;
9793
9794 // When selecting a range, the popover is gone.
9795 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
9796 cx.update_editor(|editor, window, cx| {
9797 editor.change_selections(None, window, cx, |s| {
9798 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
9799 })
9800 });
9801 cx.assert_editor_state(indoc! {"
9802 fn main() {
9803 sample(param1, «ˇparam2»);
9804 }
9805
9806 fn sample(param1: u8, param2: u8) {}
9807 "});
9808 cx.editor(|editor, _, _| {
9809 assert!(!editor.signature_help_state.is_shown());
9810 });
9811
9812 // When unselecting again, the popover is back if within the brackets.
9813 cx.update_editor(|editor, window, cx| {
9814 editor.change_selections(None, window, cx, |s| {
9815 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9816 })
9817 });
9818 cx.assert_editor_state(indoc! {"
9819 fn main() {
9820 sample(param1, ˇparam2);
9821 }
9822
9823 fn sample(param1: u8, param2: u8) {}
9824 "});
9825 handle_signature_help_request(&mut cx, mocked_response).await;
9826 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9827 .await;
9828 cx.editor(|editor, _, _| {
9829 assert!(editor.signature_help_state.is_shown());
9830 });
9831
9832 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
9833 cx.update_editor(|editor, window, cx| {
9834 editor.change_selections(None, window, cx, |s| {
9835 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
9836 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9837 })
9838 });
9839 cx.assert_editor_state(indoc! {"
9840 fn main() {
9841 sample(param1, ˇparam2);
9842 }
9843
9844 fn sample(param1: u8, param2: u8) {}
9845 "});
9846
9847 let mocked_response = lsp::SignatureHelp {
9848 signatures: vec![lsp::SignatureInformation {
9849 label: "fn sample(param1: u8, param2: u8)".to_string(),
9850 documentation: None,
9851 parameters: Some(vec![
9852 lsp::ParameterInformation {
9853 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9854 documentation: None,
9855 },
9856 lsp::ParameterInformation {
9857 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9858 documentation: None,
9859 },
9860 ]),
9861 active_parameter: None,
9862 }],
9863 active_signature: Some(0),
9864 active_parameter: Some(1),
9865 };
9866 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9867 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9868 .await;
9869 cx.update_editor(|editor, _, cx| {
9870 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
9871 });
9872 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
9873 .await;
9874 cx.update_editor(|editor, window, cx| {
9875 editor.change_selections(None, window, cx, |s| {
9876 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
9877 })
9878 });
9879 cx.assert_editor_state(indoc! {"
9880 fn main() {
9881 sample(param1, «ˇparam2»);
9882 }
9883
9884 fn sample(param1: u8, param2: u8) {}
9885 "});
9886 cx.update_editor(|editor, window, cx| {
9887 editor.change_selections(None, window, cx, |s| {
9888 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9889 })
9890 });
9891 cx.assert_editor_state(indoc! {"
9892 fn main() {
9893 sample(param1, ˇparam2);
9894 }
9895
9896 fn sample(param1: u8, param2: u8) {}
9897 "});
9898 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
9899 .await;
9900}
9901
9902#[gpui::test]
9903async fn test_completion_mode(cx: &mut TestAppContext) {
9904 init_test(cx, |_| {});
9905 let mut cx = EditorLspTestContext::new_rust(
9906 lsp::ServerCapabilities {
9907 completion_provider: Some(lsp::CompletionOptions {
9908 resolve_provider: Some(true),
9909 ..Default::default()
9910 }),
9911 ..Default::default()
9912 },
9913 cx,
9914 )
9915 .await;
9916
9917 struct Run {
9918 run_description: &'static str,
9919 initial_state: String,
9920 buffer_marked_text: String,
9921 completion_text: &'static str,
9922 expected_with_insert_mode: String,
9923 expected_with_replace_mode: String,
9924 expected_with_replace_subsequence_mode: String,
9925 expected_with_replace_suffix_mode: String,
9926 }
9927
9928 let runs = [
9929 Run {
9930 run_description: "Start of word matches completion text",
9931 initial_state: "before ediˇ after".into(),
9932 buffer_marked_text: "before <edi|> after".into(),
9933 completion_text: "editor",
9934 expected_with_insert_mode: "before editorˇ after".into(),
9935 expected_with_replace_mode: "before editorˇ after".into(),
9936 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
9937 expected_with_replace_suffix_mode: "before editorˇ after".into(),
9938 },
9939 Run {
9940 run_description: "Accept same text at the middle of the word",
9941 initial_state: "before ediˇtor after".into(),
9942 buffer_marked_text: "before <edi|tor> after".into(),
9943 completion_text: "editor",
9944 expected_with_insert_mode: "before editorˇtor after".into(),
9945 expected_with_replace_mode: "before editorˇ after".into(),
9946 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
9947 expected_with_replace_suffix_mode: "before editorˇ after".into(),
9948 },
9949 Run {
9950 run_description: "End of word matches completion text -- cursor at end",
9951 initial_state: "before torˇ after".into(),
9952 buffer_marked_text: "before <tor|> after".into(),
9953 completion_text: "editor",
9954 expected_with_insert_mode: "before editorˇ after".into(),
9955 expected_with_replace_mode: "before editorˇ after".into(),
9956 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
9957 expected_with_replace_suffix_mode: "before editorˇ after".into(),
9958 },
9959 Run {
9960 run_description: "End of word matches completion text -- cursor at start",
9961 initial_state: "before ˇtor after".into(),
9962 buffer_marked_text: "before <|tor> after".into(),
9963 completion_text: "editor",
9964 expected_with_insert_mode: "before editorˇtor after".into(),
9965 expected_with_replace_mode: "before editorˇ after".into(),
9966 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
9967 expected_with_replace_suffix_mode: "before editorˇ after".into(),
9968 },
9969 Run {
9970 run_description: "Prepend text containing whitespace",
9971 initial_state: "pˇfield: bool".into(),
9972 buffer_marked_text: "<p|field>: bool".into(),
9973 completion_text: "pub ",
9974 expected_with_insert_mode: "pub ˇfield: bool".into(),
9975 expected_with_replace_mode: "pub ˇ: bool".into(),
9976 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
9977 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
9978 },
9979 Run {
9980 run_description: "Add element to start of list",
9981 initial_state: "[element_ˇelement_2]".into(),
9982 buffer_marked_text: "[<element_|element_2>]".into(),
9983 completion_text: "element_1",
9984 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
9985 expected_with_replace_mode: "[element_1ˇ]".into(),
9986 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
9987 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
9988 },
9989 Run {
9990 run_description: "Add element to start of list -- first and second elements are equal",
9991 initial_state: "[elˇelement]".into(),
9992 buffer_marked_text: "[<el|element>]".into(),
9993 completion_text: "element",
9994 expected_with_insert_mode: "[elementˇelement]".into(),
9995 expected_with_replace_mode: "[elementˇ]".into(),
9996 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
9997 expected_with_replace_suffix_mode: "[elementˇ]".into(),
9998 },
9999 Run {
10000 run_description: "Ends with matching suffix",
10001 initial_state: "SubˇError".into(),
10002 buffer_marked_text: "<Sub|Error>".into(),
10003 completion_text: "SubscriptionError",
10004 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
10005 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10006 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10007 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
10008 },
10009 Run {
10010 run_description: "Suffix is a subsequence -- contiguous",
10011 initial_state: "SubˇErr".into(),
10012 buffer_marked_text: "<Sub|Err>".into(),
10013 completion_text: "SubscriptionError",
10014 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
10015 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10016 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10017 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
10018 },
10019 Run {
10020 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
10021 initial_state: "Suˇscrirr".into(),
10022 buffer_marked_text: "<Su|scrirr>".into(),
10023 completion_text: "SubscriptionError",
10024 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
10025 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10026 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10027 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
10028 },
10029 Run {
10030 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
10031 initial_state: "foo(indˇix)".into(),
10032 buffer_marked_text: "foo(<ind|ix>)".into(),
10033 completion_text: "node_index",
10034 expected_with_insert_mode: "foo(node_indexˇix)".into(),
10035 expected_with_replace_mode: "foo(node_indexˇ)".into(),
10036 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
10037 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
10038 },
10039 ];
10040
10041 for run in runs {
10042 let run_variations = [
10043 (LspInsertMode::Insert, run.expected_with_insert_mode),
10044 (LspInsertMode::Replace, run.expected_with_replace_mode),
10045 (
10046 LspInsertMode::ReplaceSubsequence,
10047 run.expected_with_replace_subsequence_mode,
10048 ),
10049 (
10050 LspInsertMode::ReplaceSuffix,
10051 run.expected_with_replace_suffix_mode,
10052 ),
10053 ];
10054
10055 for (lsp_insert_mode, expected_text) in run_variations {
10056 eprintln!(
10057 "run = {:?}, mode = {lsp_insert_mode:.?}",
10058 run.run_description,
10059 );
10060
10061 update_test_language_settings(&mut cx, |settings| {
10062 settings.defaults.completions = Some(CompletionSettings {
10063 lsp_insert_mode,
10064 words: WordsCompletionMode::Disabled,
10065 lsp: true,
10066 lsp_fetch_timeout_ms: 0,
10067 });
10068 });
10069
10070 cx.set_state(&run.initial_state);
10071 cx.update_editor(|editor, window, cx| {
10072 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10073 });
10074
10075 let counter = Arc::new(AtomicUsize::new(0));
10076 handle_completion_request_with_insert_and_replace(
10077 &mut cx,
10078 &run.buffer_marked_text,
10079 vec![run.completion_text],
10080 counter.clone(),
10081 )
10082 .await;
10083 cx.condition(|editor, _| editor.context_menu_visible())
10084 .await;
10085 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10086
10087 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10088 editor
10089 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10090 .unwrap()
10091 });
10092 cx.assert_editor_state(&expected_text);
10093 handle_resolve_completion_request(&mut cx, None).await;
10094 apply_additional_edits.await.unwrap();
10095 }
10096 }
10097}
10098
10099#[gpui::test]
10100async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
10101 init_test(cx, |_| {});
10102 let mut cx = EditorLspTestContext::new_rust(
10103 lsp::ServerCapabilities {
10104 completion_provider: Some(lsp::CompletionOptions {
10105 resolve_provider: Some(true),
10106 ..Default::default()
10107 }),
10108 ..Default::default()
10109 },
10110 cx,
10111 )
10112 .await;
10113
10114 let initial_state = "SubˇError";
10115 let buffer_marked_text = "<Sub|Error>";
10116 let completion_text = "SubscriptionError";
10117 let expected_with_insert_mode = "SubscriptionErrorˇError";
10118 let expected_with_replace_mode = "SubscriptionErrorˇ";
10119
10120 update_test_language_settings(&mut cx, |settings| {
10121 settings.defaults.completions = Some(CompletionSettings {
10122 words: WordsCompletionMode::Disabled,
10123 // set the opposite here to ensure that the action is overriding the default behavior
10124 lsp_insert_mode: LspInsertMode::Insert,
10125 lsp: true,
10126 lsp_fetch_timeout_ms: 0,
10127 });
10128 });
10129
10130 cx.set_state(initial_state);
10131 cx.update_editor(|editor, window, cx| {
10132 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10133 });
10134
10135 let counter = Arc::new(AtomicUsize::new(0));
10136 handle_completion_request_with_insert_and_replace(
10137 &mut cx,
10138 &buffer_marked_text,
10139 vec![completion_text],
10140 counter.clone(),
10141 )
10142 .await;
10143 cx.condition(|editor, _| editor.context_menu_visible())
10144 .await;
10145 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10146
10147 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10148 editor
10149 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10150 .unwrap()
10151 });
10152 cx.assert_editor_state(&expected_with_replace_mode);
10153 handle_resolve_completion_request(&mut cx, None).await;
10154 apply_additional_edits.await.unwrap();
10155
10156 update_test_language_settings(&mut cx, |settings| {
10157 settings.defaults.completions = Some(CompletionSettings {
10158 words: WordsCompletionMode::Disabled,
10159 // set the opposite here to ensure that the action is overriding the default behavior
10160 lsp_insert_mode: LspInsertMode::Replace,
10161 lsp: true,
10162 lsp_fetch_timeout_ms: 0,
10163 });
10164 });
10165
10166 cx.set_state(initial_state);
10167 cx.update_editor(|editor, window, cx| {
10168 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10169 });
10170 handle_completion_request_with_insert_and_replace(
10171 &mut cx,
10172 &buffer_marked_text,
10173 vec![completion_text],
10174 counter.clone(),
10175 )
10176 .await;
10177 cx.condition(|editor, _| editor.context_menu_visible())
10178 .await;
10179 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
10180
10181 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10182 editor
10183 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
10184 .unwrap()
10185 });
10186 cx.assert_editor_state(&expected_with_insert_mode);
10187 handle_resolve_completion_request(&mut cx, None).await;
10188 apply_additional_edits.await.unwrap();
10189}
10190
10191#[gpui::test]
10192async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
10193 init_test(cx, |_| {});
10194 let mut cx = EditorLspTestContext::new_rust(
10195 lsp::ServerCapabilities {
10196 completion_provider: Some(lsp::CompletionOptions {
10197 resolve_provider: Some(true),
10198 ..Default::default()
10199 }),
10200 ..Default::default()
10201 },
10202 cx,
10203 )
10204 .await;
10205
10206 // scenario: surrounding text matches completion text
10207 let completion_text = "to_offset";
10208 let initial_state = indoc! {"
10209 1. buf.to_offˇsuffix
10210 2. buf.to_offˇsuf
10211 3. buf.to_offˇfix
10212 4. buf.to_offˇ
10213 5. into_offˇensive
10214 6. ˇsuffix
10215 7. let ˇ //
10216 8. aaˇzz
10217 9. buf.to_off«zzzzzˇ»suffix
10218 10. buf.«ˇzzzzz»suffix
10219 11. to_off«ˇzzzzz»
10220
10221 buf.to_offˇsuffix // newest cursor
10222 "};
10223 let completion_marked_buffer = indoc! {"
10224 1. buf.to_offsuffix
10225 2. buf.to_offsuf
10226 3. buf.to_offfix
10227 4. buf.to_off
10228 5. into_offensive
10229 6. suffix
10230 7. let //
10231 8. aazz
10232 9. buf.to_offzzzzzsuffix
10233 10. buf.zzzzzsuffix
10234 11. to_offzzzzz
10235
10236 buf.<to_off|suffix> // newest cursor
10237 "};
10238 let expected = indoc! {"
10239 1. buf.to_offsetˇ
10240 2. buf.to_offsetˇsuf
10241 3. buf.to_offsetˇfix
10242 4. buf.to_offsetˇ
10243 5. into_offsetˇensive
10244 6. to_offsetˇsuffix
10245 7. let to_offsetˇ //
10246 8. aato_offsetˇzz
10247 9. buf.to_offsetˇ
10248 10. buf.to_offsetˇsuffix
10249 11. to_offsetˇ
10250
10251 buf.to_offsetˇ // newest cursor
10252 "};
10253 cx.set_state(initial_state);
10254 cx.update_editor(|editor, window, cx| {
10255 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10256 });
10257 handle_completion_request_with_insert_and_replace(
10258 &mut cx,
10259 completion_marked_buffer,
10260 vec![completion_text],
10261 Arc::new(AtomicUsize::new(0)),
10262 )
10263 .await;
10264 cx.condition(|editor, _| editor.context_menu_visible())
10265 .await;
10266 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10267 editor
10268 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10269 .unwrap()
10270 });
10271 cx.assert_editor_state(expected);
10272 handle_resolve_completion_request(&mut cx, None).await;
10273 apply_additional_edits.await.unwrap();
10274
10275 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
10276 let completion_text = "foo_and_bar";
10277 let initial_state = indoc! {"
10278 1. ooanbˇ
10279 2. zooanbˇ
10280 3. ooanbˇz
10281 4. zooanbˇz
10282 5. ooanˇ
10283 6. oanbˇ
10284
10285 ooanbˇ
10286 "};
10287 let completion_marked_buffer = indoc! {"
10288 1. ooanb
10289 2. zooanb
10290 3. ooanbz
10291 4. zooanbz
10292 5. ooan
10293 6. oanb
10294
10295 <ooanb|>
10296 "};
10297 let expected = indoc! {"
10298 1. foo_and_barˇ
10299 2. zfoo_and_barˇ
10300 3. foo_and_barˇz
10301 4. zfoo_and_barˇz
10302 5. ooanfoo_and_barˇ
10303 6. oanbfoo_and_barˇ
10304
10305 foo_and_barˇ
10306 "};
10307 cx.set_state(initial_state);
10308 cx.update_editor(|editor, window, cx| {
10309 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10310 });
10311 handle_completion_request_with_insert_and_replace(
10312 &mut cx,
10313 completion_marked_buffer,
10314 vec![completion_text],
10315 Arc::new(AtomicUsize::new(0)),
10316 )
10317 .await;
10318 cx.condition(|editor, _| editor.context_menu_visible())
10319 .await;
10320 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10321 editor
10322 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10323 .unwrap()
10324 });
10325 cx.assert_editor_state(expected);
10326 handle_resolve_completion_request(&mut cx, None).await;
10327 apply_additional_edits.await.unwrap();
10328
10329 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
10330 // (expects the same as if it was inserted at the end)
10331 let completion_text = "foo_and_bar";
10332 let initial_state = indoc! {"
10333 1. ooˇanb
10334 2. zooˇanb
10335 3. ooˇanbz
10336 4. zooˇanbz
10337
10338 ooˇanb
10339 "};
10340 let completion_marked_buffer = indoc! {"
10341 1. ooanb
10342 2. zooanb
10343 3. ooanbz
10344 4. zooanbz
10345
10346 <oo|anb>
10347 "};
10348 let expected = indoc! {"
10349 1. foo_and_barˇ
10350 2. zfoo_and_barˇ
10351 3. foo_and_barˇz
10352 4. zfoo_and_barˇz
10353
10354 foo_and_barˇ
10355 "};
10356 cx.set_state(initial_state);
10357 cx.update_editor(|editor, window, cx| {
10358 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10359 });
10360 handle_completion_request_with_insert_and_replace(
10361 &mut cx,
10362 completion_marked_buffer,
10363 vec![completion_text],
10364 Arc::new(AtomicUsize::new(0)),
10365 )
10366 .await;
10367 cx.condition(|editor, _| editor.context_menu_visible())
10368 .await;
10369 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10370 editor
10371 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10372 .unwrap()
10373 });
10374 cx.assert_editor_state(expected);
10375 handle_resolve_completion_request(&mut cx, None).await;
10376 apply_additional_edits.await.unwrap();
10377}
10378
10379// This used to crash
10380#[gpui::test]
10381async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
10382 init_test(cx, |_| {});
10383
10384 let buffer_text = indoc! {"
10385 fn main() {
10386 10.satu;
10387
10388 //
10389 // separate cursors so they open in different excerpts (manually reproducible)
10390 //
10391
10392 10.satu20;
10393 }
10394 "};
10395 let multibuffer_text_with_selections = indoc! {"
10396 fn main() {
10397 10.satuˇ;
10398
10399 //
10400
10401 //
10402
10403 10.satuˇ20;
10404 }
10405 "};
10406 let expected_multibuffer = indoc! {"
10407 fn main() {
10408 10.saturating_sub()ˇ;
10409
10410 //
10411
10412 //
10413
10414 10.saturating_sub()ˇ;
10415 }
10416 "};
10417
10418 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
10419 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
10420
10421 let fs = FakeFs::new(cx.executor());
10422 fs.insert_tree(
10423 path!("/a"),
10424 json!({
10425 "main.rs": buffer_text,
10426 }),
10427 )
10428 .await;
10429
10430 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
10431 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10432 language_registry.add(rust_lang());
10433 let mut fake_servers = language_registry.register_fake_lsp(
10434 "Rust",
10435 FakeLspAdapter {
10436 capabilities: lsp::ServerCapabilities {
10437 completion_provider: Some(lsp::CompletionOptions {
10438 resolve_provider: None,
10439 ..lsp::CompletionOptions::default()
10440 }),
10441 ..lsp::ServerCapabilities::default()
10442 },
10443 ..FakeLspAdapter::default()
10444 },
10445 );
10446 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10447 let cx = &mut VisualTestContext::from_window(*workspace, cx);
10448 let buffer = project
10449 .update(cx, |project, cx| {
10450 project.open_local_buffer(path!("/a/main.rs"), cx)
10451 })
10452 .await
10453 .unwrap();
10454
10455 let multi_buffer = cx.new(|cx| {
10456 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
10457 multi_buffer.push_excerpts(
10458 buffer.clone(),
10459 [ExcerptRange::new(0..first_excerpt_end)],
10460 cx,
10461 );
10462 multi_buffer.push_excerpts(
10463 buffer.clone(),
10464 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
10465 cx,
10466 );
10467 multi_buffer
10468 });
10469
10470 let editor = workspace
10471 .update(cx, |_, window, cx| {
10472 cx.new(|cx| {
10473 Editor::new(
10474 EditorMode::Full {
10475 scale_ui_elements_with_buffer_font_size: false,
10476 show_active_line_background: false,
10477 sized_by_content: false,
10478 },
10479 multi_buffer.clone(),
10480 Some(project.clone()),
10481 window,
10482 cx,
10483 )
10484 })
10485 })
10486 .unwrap();
10487
10488 let pane = workspace
10489 .update(cx, |workspace, _, _| workspace.active_pane().clone())
10490 .unwrap();
10491 pane.update_in(cx, |pane, window, cx| {
10492 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
10493 });
10494
10495 let fake_server = fake_servers.next().await.unwrap();
10496
10497 editor.update_in(cx, |editor, window, cx| {
10498 editor.change_selections(None, window, cx, |s| {
10499 s.select_ranges([
10500 Point::new(1, 11)..Point::new(1, 11),
10501 Point::new(7, 11)..Point::new(7, 11),
10502 ])
10503 });
10504
10505 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
10506 });
10507
10508 editor.update_in(cx, |editor, window, cx| {
10509 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10510 });
10511
10512 fake_server
10513 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
10514 let completion_item = lsp::CompletionItem {
10515 label: "saturating_sub()".into(),
10516 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
10517 lsp::InsertReplaceEdit {
10518 new_text: "saturating_sub()".to_owned(),
10519 insert: lsp::Range::new(
10520 lsp::Position::new(7, 7),
10521 lsp::Position::new(7, 11),
10522 ),
10523 replace: lsp::Range::new(
10524 lsp::Position::new(7, 7),
10525 lsp::Position::new(7, 13),
10526 ),
10527 },
10528 )),
10529 ..lsp::CompletionItem::default()
10530 };
10531
10532 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
10533 })
10534 .next()
10535 .await
10536 .unwrap();
10537
10538 cx.condition(&editor, |editor, _| editor.context_menu_visible())
10539 .await;
10540
10541 editor
10542 .update_in(cx, |editor, window, cx| {
10543 editor
10544 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10545 .unwrap()
10546 })
10547 .await
10548 .unwrap();
10549
10550 editor.update(cx, |editor, cx| {
10551 assert_text_with_selections(editor, expected_multibuffer, cx);
10552 })
10553}
10554
10555#[gpui::test]
10556async fn test_completion(cx: &mut TestAppContext) {
10557 init_test(cx, |_| {});
10558
10559 let mut cx = EditorLspTestContext::new_rust(
10560 lsp::ServerCapabilities {
10561 completion_provider: Some(lsp::CompletionOptions {
10562 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10563 resolve_provider: Some(true),
10564 ..Default::default()
10565 }),
10566 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10567 ..Default::default()
10568 },
10569 cx,
10570 )
10571 .await;
10572 let counter = Arc::new(AtomicUsize::new(0));
10573
10574 cx.set_state(indoc! {"
10575 oneˇ
10576 two
10577 three
10578 "});
10579 cx.simulate_keystroke(".");
10580 handle_completion_request(
10581 &mut cx,
10582 indoc! {"
10583 one.|<>
10584 two
10585 three
10586 "},
10587 vec!["first_completion", "second_completion"],
10588 counter.clone(),
10589 )
10590 .await;
10591 cx.condition(|editor, _| editor.context_menu_visible())
10592 .await;
10593 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10594
10595 let _handler = handle_signature_help_request(
10596 &mut cx,
10597 lsp::SignatureHelp {
10598 signatures: vec![lsp::SignatureInformation {
10599 label: "test signature".to_string(),
10600 documentation: None,
10601 parameters: Some(vec![lsp::ParameterInformation {
10602 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
10603 documentation: None,
10604 }]),
10605 active_parameter: None,
10606 }],
10607 active_signature: None,
10608 active_parameter: None,
10609 },
10610 );
10611 cx.update_editor(|editor, window, cx| {
10612 assert!(
10613 !editor.signature_help_state.is_shown(),
10614 "No signature help was called for"
10615 );
10616 editor.show_signature_help(&ShowSignatureHelp, window, cx);
10617 });
10618 cx.run_until_parked();
10619 cx.update_editor(|editor, _, _| {
10620 assert!(
10621 !editor.signature_help_state.is_shown(),
10622 "No signature help should be shown when completions menu is open"
10623 );
10624 });
10625
10626 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10627 editor.context_menu_next(&Default::default(), window, cx);
10628 editor
10629 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10630 .unwrap()
10631 });
10632 cx.assert_editor_state(indoc! {"
10633 one.second_completionˇ
10634 two
10635 three
10636 "});
10637
10638 handle_resolve_completion_request(
10639 &mut cx,
10640 Some(vec![
10641 (
10642 //This overlaps with the primary completion edit which is
10643 //misbehavior from the LSP spec, test that we filter it out
10644 indoc! {"
10645 one.second_ˇcompletion
10646 two
10647 threeˇ
10648 "},
10649 "overlapping additional edit",
10650 ),
10651 (
10652 indoc! {"
10653 one.second_completion
10654 two
10655 threeˇ
10656 "},
10657 "\nadditional edit",
10658 ),
10659 ]),
10660 )
10661 .await;
10662 apply_additional_edits.await.unwrap();
10663 cx.assert_editor_state(indoc! {"
10664 one.second_completionˇ
10665 two
10666 three
10667 additional edit
10668 "});
10669
10670 cx.set_state(indoc! {"
10671 one.second_completion
10672 twoˇ
10673 threeˇ
10674 additional edit
10675 "});
10676 cx.simulate_keystroke(" ");
10677 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
10678 cx.simulate_keystroke("s");
10679 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
10680
10681 cx.assert_editor_state(indoc! {"
10682 one.second_completion
10683 two sˇ
10684 three sˇ
10685 additional edit
10686 "});
10687 handle_completion_request(
10688 &mut cx,
10689 indoc! {"
10690 one.second_completion
10691 two s
10692 three <s|>
10693 additional edit
10694 "},
10695 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
10696 counter.clone(),
10697 )
10698 .await;
10699 cx.condition(|editor, _| editor.context_menu_visible())
10700 .await;
10701 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
10702
10703 cx.simulate_keystroke("i");
10704
10705 handle_completion_request(
10706 &mut cx,
10707 indoc! {"
10708 one.second_completion
10709 two si
10710 three <si|>
10711 additional edit
10712 "},
10713 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
10714 counter.clone(),
10715 )
10716 .await;
10717 cx.condition(|editor, _| editor.context_menu_visible())
10718 .await;
10719 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
10720
10721 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10722 editor
10723 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10724 .unwrap()
10725 });
10726 cx.assert_editor_state(indoc! {"
10727 one.second_completion
10728 two sixth_completionˇ
10729 three sixth_completionˇ
10730 additional edit
10731 "});
10732
10733 apply_additional_edits.await.unwrap();
10734
10735 update_test_language_settings(&mut cx, |settings| {
10736 settings.defaults.show_completions_on_input = Some(false);
10737 });
10738 cx.set_state("editorˇ");
10739 cx.simulate_keystroke(".");
10740 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
10741 cx.simulate_keystrokes("c l o");
10742 cx.assert_editor_state("editor.cloˇ");
10743 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
10744 cx.update_editor(|editor, window, cx| {
10745 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10746 });
10747 handle_completion_request(
10748 &mut cx,
10749 "editor.<clo|>",
10750 vec!["close", "clobber"],
10751 counter.clone(),
10752 )
10753 .await;
10754 cx.condition(|editor, _| editor.context_menu_visible())
10755 .await;
10756 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
10757
10758 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10759 editor
10760 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10761 .unwrap()
10762 });
10763 cx.assert_editor_state("editor.closeˇ");
10764 handle_resolve_completion_request(&mut cx, None).await;
10765 apply_additional_edits.await.unwrap();
10766}
10767
10768#[gpui::test]
10769async fn test_word_completion(cx: &mut TestAppContext) {
10770 let lsp_fetch_timeout_ms = 10;
10771 init_test(cx, |language_settings| {
10772 language_settings.defaults.completions = Some(CompletionSettings {
10773 words: WordsCompletionMode::Fallback,
10774 lsp: true,
10775 lsp_fetch_timeout_ms: 10,
10776 lsp_insert_mode: LspInsertMode::Insert,
10777 });
10778 });
10779
10780 let mut cx = EditorLspTestContext::new_rust(
10781 lsp::ServerCapabilities {
10782 completion_provider: Some(lsp::CompletionOptions {
10783 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10784 ..lsp::CompletionOptions::default()
10785 }),
10786 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10787 ..lsp::ServerCapabilities::default()
10788 },
10789 cx,
10790 )
10791 .await;
10792
10793 let throttle_completions = Arc::new(AtomicBool::new(false));
10794
10795 let lsp_throttle_completions = throttle_completions.clone();
10796 let _completion_requests_handler =
10797 cx.lsp
10798 .server
10799 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
10800 let lsp_throttle_completions = lsp_throttle_completions.clone();
10801 let cx = cx.clone();
10802 async move {
10803 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
10804 cx.background_executor()
10805 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
10806 .await;
10807 }
10808 Ok(Some(lsp::CompletionResponse::Array(vec![
10809 lsp::CompletionItem {
10810 label: "first".into(),
10811 ..lsp::CompletionItem::default()
10812 },
10813 lsp::CompletionItem {
10814 label: "last".into(),
10815 ..lsp::CompletionItem::default()
10816 },
10817 ])))
10818 }
10819 });
10820
10821 cx.set_state(indoc! {"
10822 oneˇ
10823 two
10824 three
10825 "});
10826 cx.simulate_keystroke(".");
10827 cx.executor().run_until_parked();
10828 cx.condition(|editor, _| editor.context_menu_visible())
10829 .await;
10830 cx.update_editor(|editor, window, cx| {
10831 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10832 {
10833 assert_eq!(
10834 completion_menu_entries(&menu),
10835 &["first", "last"],
10836 "When LSP server is fast to reply, no fallback word completions are used"
10837 );
10838 } else {
10839 panic!("expected completion menu to be open");
10840 }
10841 editor.cancel(&Cancel, window, cx);
10842 });
10843 cx.executor().run_until_parked();
10844 cx.condition(|editor, _| !editor.context_menu_visible())
10845 .await;
10846
10847 throttle_completions.store(true, atomic::Ordering::Release);
10848 cx.simulate_keystroke(".");
10849 cx.executor()
10850 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
10851 cx.executor().run_until_parked();
10852 cx.condition(|editor, _| editor.context_menu_visible())
10853 .await;
10854 cx.update_editor(|editor, _, _| {
10855 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10856 {
10857 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
10858 "When LSP server is slow, document words can be shown instead, if configured accordingly");
10859 } else {
10860 panic!("expected completion menu to be open");
10861 }
10862 });
10863}
10864
10865#[gpui::test]
10866async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
10867 init_test(cx, |language_settings| {
10868 language_settings.defaults.completions = Some(CompletionSettings {
10869 words: WordsCompletionMode::Enabled,
10870 lsp: true,
10871 lsp_fetch_timeout_ms: 0,
10872 lsp_insert_mode: LspInsertMode::Insert,
10873 });
10874 });
10875
10876 let mut cx = EditorLspTestContext::new_rust(
10877 lsp::ServerCapabilities {
10878 completion_provider: Some(lsp::CompletionOptions {
10879 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10880 ..lsp::CompletionOptions::default()
10881 }),
10882 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10883 ..lsp::ServerCapabilities::default()
10884 },
10885 cx,
10886 )
10887 .await;
10888
10889 let _completion_requests_handler =
10890 cx.lsp
10891 .server
10892 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
10893 Ok(Some(lsp::CompletionResponse::Array(vec![
10894 lsp::CompletionItem {
10895 label: "first".into(),
10896 ..lsp::CompletionItem::default()
10897 },
10898 lsp::CompletionItem {
10899 label: "last".into(),
10900 ..lsp::CompletionItem::default()
10901 },
10902 ])))
10903 });
10904
10905 cx.set_state(indoc! {"ˇ
10906 first
10907 last
10908 second
10909 "});
10910 cx.simulate_keystroke(".");
10911 cx.executor().run_until_parked();
10912 cx.condition(|editor, _| editor.context_menu_visible())
10913 .await;
10914 cx.update_editor(|editor, _, _| {
10915 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10916 {
10917 assert_eq!(
10918 completion_menu_entries(&menu),
10919 &["first", "last", "second"],
10920 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
10921 );
10922 } else {
10923 panic!("expected completion menu to be open");
10924 }
10925 });
10926}
10927
10928#[gpui::test]
10929async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
10930 init_test(cx, |language_settings| {
10931 language_settings.defaults.completions = Some(CompletionSettings {
10932 words: WordsCompletionMode::Disabled,
10933 lsp: true,
10934 lsp_fetch_timeout_ms: 0,
10935 lsp_insert_mode: LspInsertMode::Insert,
10936 });
10937 });
10938
10939 let mut cx = EditorLspTestContext::new_rust(
10940 lsp::ServerCapabilities {
10941 completion_provider: Some(lsp::CompletionOptions {
10942 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10943 ..lsp::CompletionOptions::default()
10944 }),
10945 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10946 ..lsp::ServerCapabilities::default()
10947 },
10948 cx,
10949 )
10950 .await;
10951
10952 let _completion_requests_handler =
10953 cx.lsp
10954 .server
10955 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
10956 panic!("LSP completions should not be queried when dealing with word completions")
10957 });
10958
10959 cx.set_state(indoc! {"ˇ
10960 first
10961 last
10962 second
10963 "});
10964 cx.update_editor(|editor, window, cx| {
10965 editor.show_word_completions(&ShowWordCompletions, window, cx);
10966 });
10967 cx.executor().run_until_parked();
10968 cx.condition(|editor, _| editor.context_menu_visible())
10969 .await;
10970 cx.update_editor(|editor, _, _| {
10971 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10972 {
10973 assert_eq!(
10974 completion_menu_entries(&menu),
10975 &["first", "last", "second"],
10976 "`ShowWordCompletions` action should show word completions"
10977 );
10978 } else {
10979 panic!("expected completion menu to be open");
10980 }
10981 });
10982
10983 cx.simulate_keystroke("l");
10984 cx.executor().run_until_parked();
10985 cx.condition(|editor, _| editor.context_menu_visible())
10986 .await;
10987 cx.update_editor(|editor, _, _| {
10988 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10989 {
10990 assert_eq!(
10991 completion_menu_entries(&menu),
10992 &["last"],
10993 "After showing word completions, further editing should filter them and not query the LSP"
10994 );
10995 } else {
10996 panic!("expected completion menu to be open");
10997 }
10998 });
10999}
11000
11001#[gpui::test]
11002async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
11003 init_test(cx, |language_settings| {
11004 language_settings.defaults.completions = Some(CompletionSettings {
11005 words: WordsCompletionMode::Fallback,
11006 lsp: false,
11007 lsp_fetch_timeout_ms: 0,
11008 lsp_insert_mode: LspInsertMode::Insert,
11009 });
11010 });
11011
11012 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
11013
11014 cx.set_state(indoc! {"ˇ
11015 0_usize
11016 let
11017 33
11018 4.5f32
11019 "});
11020 cx.update_editor(|editor, window, cx| {
11021 editor.show_completions(&ShowCompletions::default(), window, cx);
11022 });
11023 cx.executor().run_until_parked();
11024 cx.condition(|editor, _| editor.context_menu_visible())
11025 .await;
11026 cx.update_editor(|editor, window, cx| {
11027 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11028 {
11029 assert_eq!(
11030 completion_menu_entries(&menu),
11031 &["let"],
11032 "With no digits in the completion query, no digits should be in the word completions"
11033 );
11034 } else {
11035 panic!("expected completion menu to be open");
11036 }
11037 editor.cancel(&Cancel, window, cx);
11038 });
11039
11040 cx.set_state(indoc! {"3ˇ
11041 0_usize
11042 let
11043 3
11044 33.35f32
11045 "});
11046 cx.update_editor(|editor, window, cx| {
11047 editor.show_completions(&ShowCompletions::default(), window, cx);
11048 });
11049 cx.executor().run_until_parked();
11050 cx.condition(|editor, _| editor.context_menu_visible())
11051 .await;
11052 cx.update_editor(|editor, _, _| {
11053 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11054 {
11055 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
11056 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
11057 } else {
11058 panic!("expected completion menu to be open");
11059 }
11060 });
11061}
11062
11063fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
11064 let position = || lsp::Position {
11065 line: params.text_document_position.position.line,
11066 character: params.text_document_position.position.character,
11067 };
11068 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11069 range: lsp::Range {
11070 start: position(),
11071 end: position(),
11072 },
11073 new_text: text.to_string(),
11074 }))
11075}
11076
11077#[gpui::test]
11078async fn test_multiline_completion(cx: &mut TestAppContext) {
11079 init_test(cx, |_| {});
11080
11081 let fs = FakeFs::new(cx.executor());
11082 fs.insert_tree(
11083 path!("/a"),
11084 json!({
11085 "main.ts": "a",
11086 }),
11087 )
11088 .await;
11089
11090 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11091 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11092 let typescript_language = Arc::new(Language::new(
11093 LanguageConfig {
11094 name: "TypeScript".into(),
11095 matcher: LanguageMatcher {
11096 path_suffixes: vec!["ts".to_string()],
11097 ..LanguageMatcher::default()
11098 },
11099 line_comments: vec!["// ".into()],
11100 ..LanguageConfig::default()
11101 },
11102 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
11103 ));
11104 language_registry.add(typescript_language.clone());
11105 let mut fake_servers = language_registry.register_fake_lsp(
11106 "TypeScript",
11107 FakeLspAdapter {
11108 capabilities: lsp::ServerCapabilities {
11109 completion_provider: Some(lsp::CompletionOptions {
11110 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11111 ..lsp::CompletionOptions::default()
11112 }),
11113 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11114 ..lsp::ServerCapabilities::default()
11115 },
11116 // Emulate vtsls label generation
11117 label_for_completion: Some(Box::new(|item, _| {
11118 let text = if let Some(description) = item
11119 .label_details
11120 .as_ref()
11121 .and_then(|label_details| label_details.description.as_ref())
11122 {
11123 format!("{} {}", item.label, description)
11124 } else if let Some(detail) = &item.detail {
11125 format!("{} {}", item.label, detail)
11126 } else {
11127 item.label.clone()
11128 };
11129 let len = text.len();
11130 Some(language::CodeLabel {
11131 text,
11132 runs: Vec::new(),
11133 filter_range: 0..len,
11134 })
11135 })),
11136 ..FakeLspAdapter::default()
11137 },
11138 );
11139 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11140 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11141 let worktree_id = workspace
11142 .update(cx, |workspace, _window, cx| {
11143 workspace.project().update(cx, |project, cx| {
11144 project.worktrees(cx).next().unwrap().read(cx).id()
11145 })
11146 })
11147 .unwrap();
11148 let _buffer = project
11149 .update(cx, |project, cx| {
11150 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
11151 })
11152 .await
11153 .unwrap();
11154 let editor = workspace
11155 .update(cx, |workspace, window, cx| {
11156 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
11157 })
11158 .unwrap()
11159 .await
11160 .unwrap()
11161 .downcast::<Editor>()
11162 .unwrap();
11163 let fake_server = fake_servers.next().await.unwrap();
11164
11165 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
11166 let multiline_label_2 = "a\nb\nc\n";
11167 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
11168 let multiline_description = "d\ne\nf\n";
11169 let multiline_detail_2 = "g\nh\ni\n";
11170
11171 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
11172 move |params, _| async move {
11173 Ok(Some(lsp::CompletionResponse::Array(vec![
11174 lsp::CompletionItem {
11175 label: multiline_label.to_string(),
11176 text_edit: gen_text_edit(¶ms, "new_text_1"),
11177 ..lsp::CompletionItem::default()
11178 },
11179 lsp::CompletionItem {
11180 label: "single line label 1".to_string(),
11181 detail: Some(multiline_detail.to_string()),
11182 text_edit: gen_text_edit(¶ms, "new_text_2"),
11183 ..lsp::CompletionItem::default()
11184 },
11185 lsp::CompletionItem {
11186 label: "single line label 2".to_string(),
11187 label_details: Some(lsp::CompletionItemLabelDetails {
11188 description: Some(multiline_description.to_string()),
11189 detail: None,
11190 }),
11191 text_edit: gen_text_edit(¶ms, "new_text_2"),
11192 ..lsp::CompletionItem::default()
11193 },
11194 lsp::CompletionItem {
11195 label: multiline_label_2.to_string(),
11196 detail: Some(multiline_detail_2.to_string()),
11197 text_edit: gen_text_edit(¶ms, "new_text_3"),
11198 ..lsp::CompletionItem::default()
11199 },
11200 lsp::CompletionItem {
11201 label: "Label with many spaces and \t but without newlines".to_string(),
11202 detail: Some(
11203 "Details with many spaces and \t but without newlines".to_string(),
11204 ),
11205 text_edit: gen_text_edit(¶ms, "new_text_4"),
11206 ..lsp::CompletionItem::default()
11207 },
11208 ])))
11209 },
11210 );
11211
11212 editor.update_in(cx, |editor, window, cx| {
11213 cx.focus_self(window);
11214 editor.move_to_end(&MoveToEnd, window, cx);
11215 editor.handle_input(".", window, cx);
11216 });
11217 cx.run_until_parked();
11218 completion_handle.next().await.unwrap();
11219
11220 editor.update(cx, |editor, _| {
11221 assert!(editor.context_menu_visible());
11222 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11223 {
11224 let completion_labels = menu
11225 .completions
11226 .borrow()
11227 .iter()
11228 .map(|c| c.label.text.clone())
11229 .collect::<Vec<_>>();
11230 assert_eq!(
11231 completion_labels,
11232 &[
11233 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
11234 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
11235 "single line label 2 d e f ",
11236 "a b c g h i ",
11237 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
11238 ],
11239 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
11240 );
11241
11242 for completion in menu
11243 .completions
11244 .borrow()
11245 .iter() {
11246 assert_eq!(
11247 completion.label.filter_range,
11248 0..completion.label.text.len(),
11249 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
11250 );
11251 }
11252 } else {
11253 panic!("expected completion menu to be open");
11254 }
11255 });
11256}
11257
11258#[gpui::test]
11259async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
11260 init_test(cx, |_| {});
11261 let mut cx = EditorLspTestContext::new_rust(
11262 lsp::ServerCapabilities {
11263 completion_provider: Some(lsp::CompletionOptions {
11264 trigger_characters: Some(vec![".".to_string()]),
11265 ..Default::default()
11266 }),
11267 ..Default::default()
11268 },
11269 cx,
11270 )
11271 .await;
11272 cx.lsp
11273 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11274 Ok(Some(lsp::CompletionResponse::Array(vec![
11275 lsp::CompletionItem {
11276 label: "first".into(),
11277 ..Default::default()
11278 },
11279 lsp::CompletionItem {
11280 label: "last".into(),
11281 ..Default::default()
11282 },
11283 ])))
11284 });
11285 cx.set_state("variableˇ");
11286 cx.simulate_keystroke(".");
11287 cx.executor().run_until_parked();
11288
11289 cx.update_editor(|editor, _, _| {
11290 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11291 {
11292 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
11293 } else {
11294 panic!("expected completion menu to be open");
11295 }
11296 });
11297
11298 cx.update_editor(|editor, window, cx| {
11299 editor.move_page_down(&MovePageDown::default(), window, cx);
11300 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11301 {
11302 assert!(
11303 menu.selected_item == 1,
11304 "expected PageDown to select the last item from the context menu"
11305 );
11306 } else {
11307 panic!("expected completion menu to stay open after PageDown");
11308 }
11309 });
11310
11311 cx.update_editor(|editor, window, cx| {
11312 editor.move_page_up(&MovePageUp::default(), window, cx);
11313 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11314 {
11315 assert!(
11316 menu.selected_item == 0,
11317 "expected PageUp to select the first item from the context menu"
11318 );
11319 } else {
11320 panic!("expected completion menu to stay open after PageUp");
11321 }
11322 });
11323}
11324
11325#[gpui::test]
11326async fn test_as_is_completions(cx: &mut TestAppContext) {
11327 init_test(cx, |_| {});
11328 let mut cx = EditorLspTestContext::new_rust(
11329 lsp::ServerCapabilities {
11330 completion_provider: Some(lsp::CompletionOptions {
11331 ..Default::default()
11332 }),
11333 ..Default::default()
11334 },
11335 cx,
11336 )
11337 .await;
11338 cx.lsp
11339 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11340 Ok(Some(lsp::CompletionResponse::Array(vec![
11341 lsp::CompletionItem {
11342 label: "unsafe".into(),
11343 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11344 range: lsp::Range {
11345 start: lsp::Position {
11346 line: 1,
11347 character: 2,
11348 },
11349 end: lsp::Position {
11350 line: 1,
11351 character: 3,
11352 },
11353 },
11354 new_text: "unsafe".to_string(),
11355 })),
11356 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
11357 ..Default::default()
11358 },
11359 ])))
11360 });
11361 cx.set_state("fn a() {}\n nˇ");
11362 cx.executor().run_until_parked();
11363 cx.update_editor(|editor, window, cx| {
11364 editor.show_completions(
11365 &ShowCompletions {
11366 trigger: Some("\n".into()),
11367 },
11368 window,
11369 cx,
11370 );
11371 });
11372 cx.executor().run_until_parked();
11373
11374 cx.update_editor(|editor, window, cx| {
11375 editor.confirm_completion(&Default::default(), window, cx)
11376 });
11377 cx.executor().run_until_parked();
11378 cx.assert_editor_state("fn a() {}\n unsafeˇ");
11379}
11380
11381#[gpui::test]
11382async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
11383 init_test(cx, |_| {});
11384
11385 let mut cx = EditorLspTestContext::new_rust(
11386 lsp::ServerCapabilities {
11387 completion_provider: Some(lsp::CompletionOptions {
11388 trigger_characters: Some(vec![".".to_string()]),
11389 resolve_provider: Some(true),
11390 ..Default::default()
11391 }),
11392 ..Default::default()
11393 },
11394 cx,
11395 )
11396 .await;
11397
11398 cx.set_state("fn main() { let a = 2ˇ; }");
11399 cx.simulate_keystroke(".");
11400 let completion_item = lsp::CompletionItem {
11401 label: "Some".into(),
11402 kind: Some(lsp::CompletionItemKind::SNIPPET),
11403 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
11404 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
11405 kind: lsp::MarkupKind::Markdown,
11406 value: "```rust\nSome(2)\n```".to_string(),
11407 })),
11408 deprecated: Some(false),
11409 sort_text: Some("Some".to_string()),
11410 filter_text: Some("Some".to_string()),
11411 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
11412 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11413 range: lsp::Range {
11414 start: lsp::Position {
11415 line: 0,
11416 character: 22,
11417 },
11418 end: lsp::Position {
11419 line: 0,
11420 character: 22,
11421 },
11422 },
11423 new_text: "Some(2)".to_string(),
11424 })),
11425 additional_text_edits: Some(vec![lsp::TextEdit {
11426 range: lsp::Range {
11427 start: lsp::Position {
11428 line: 0,
11429 character: 20,
11430 },
11431 end: lsp::Position {
11432 line: 0,
11433 character: 22,
11434 },
11435 },
11436 new_text: "".to_string(),
11437 }]),
11438 ..Default::default()
11439 };
11440
11441 let closure_completion_item = completion_item.clone();
11442 let counter = Arc::new(AtomicUsize::new(0));
11443 let counter_clone = counter.clone();
11444 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
11445 let task_completion_item = closure_completion_item.clone();
11446 counter_clone.fetch_add(1, atomic::Ordering::Release);
11447 async move {
11448 Ok(Some(lsp::CompletionResponse::Array(vec![
11449 task_completion_item,
11450 ])))
11451 }
11452 });
11453
11454 cx.condition(|editor, _| editor.context_menu_visible())
11455 .await;
11456 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
11457 assert!(request.next().await.is_some());
11458 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11459
11460 cx.simulate_keystrokes("S o m");
11461 cx.condition(|editor, _| editor.context_menu_visible())
11462 .await;
11463 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
11464 assert!(request.next().await.is_some());
11465 assert!(request.next().await.is_some());
11466 assert!(request.next().await.is_some());
11467 request.close();
11468 assert!(request.next().await.is_none());
11469 assert_eq!(
11470 counter.load(atomic::Ordering::Acquire),
11471 4,
11472 "With the completions menu open, only one LSP request should happen per input"
11473 );
11474}
11475
11476#[gpui::test]
11477async fn test_toggle_comment(cx: &mut TestAppContext) {
11478 init_test(cx, |_| {});
11479 let mut cx = EditorTestContext::new(cx).await;
11480 let language = Arc::new(Language::new(
11481 LanguageConfig {
11482 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
11483 ..Default::default()
11484 },
11485 Some(tree_sitter_rust::LANGUAGE.into()),
11486 ));
11487 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
11488
11489 // If multiple selections intersect a line, the line is only toggled once.
11490 cx.set_state(indoc! {"
11491 fn a() {
11492 «//b();
11493 ˇ»// «c();
11494 //ˇ» d();
11495 }
11496 "});
11497
11498 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11499
11500 cx.assert_editor_state(indoc! {"
11501 fn a() {
11502 «b();
11503 c();
11504 ˇ» d();
11505 }
11506 "});
11507
11508 // The comment prefix is inserted at the same column for every line in a
11509 // selection.
11510 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11511
11512 cx.assert_editor_state(indoc! {"
11513 fn a() {
11514 // «b();
11515 // c();
11516 ˇ»// d();
11517 }
11518 "});
11519
11520 // If a selection ends at the beginning of a line, that line is not toggled.
11521 cx.set_selections_state(indoc! {"
11522 fn a() {
11523 // b();
11524 «// c();
11525 ˇ» // d();
11526 }
11527 "});
11528
11529 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11530
11531 cx.assert_editor_state(indoc! {"
11532 fn a() {
11533 // b();
11534 «c();
11535 ˇ» // d();
11536 }
11537 "});
11538
11539 // If a selection span a single line and is empty, the line is toggled.
11540 cx.set_state(indoc! {"
11541 fn a() {
11542 a();
11543 b();
11544 ˇ
11545 }
11546 "});
11547
11548 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11549
11550 cx.assert_editor_state(indoc! {"
11551 fn a() {
11552 a();
11553 b();
11554 //•ˇ
11555 }
11556 "});
11557
11558 // If a selection span multiple lines, empty lines are not toggled.
11559 cx.set_state(indoc! {"
11560 fn a() {
11561 «a();
11562
11563 c();ˇ»
11564 }
11565 "});
11566
11567 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11568
11569 cx.assert_editor_state(indoc! {"
11570 fn a() {
11571 // «a();
11572
11573 // c();ˇ»
11574 }
11575 "});
11576
11577 // If a selection includes multiple comment prefixes, all lines are uncommented.
11578 cx.set_state(indoc! {"
11579 fn a() {
11580 «// a();
11581 /// b();
11582 //! c();ˇ»
11583 }
11584 "});
11585
11586 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11587
11588 cx.assert_editor_state(indoc! {"
11589 fn a() {
11590 «a();
11591 b();
11592 c();ˇ»
11593 }
11594 "});
11595}
11596
11597#[gpui::test]
11598async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
11599 init_test(cx, |_| {});
11600 let mut cx = EditorTestContext::new(cx).await;
11601 let language = Arc::new(Language::new(
11602 LanguageConfig {
11603 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
11604 ..Default::default()
11605 },
11606 Some(tree_sitter_rust::LANGUAGE.into()),
11607 ));
11608 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
11609
11610 let toggle_comments = &ToggleComments {
11611 advance_downwards: false,
11612 ignore_indent: true,
11613 };
11614
11615 // If multiple selections intersect a line, the line is only toggled once.
11616 cx.set_state(indoc! {"
11617 fn a() {
11618 // «b();
11619 // c();
11620 // ˇ» d();
11621 }
11622 "});
11623
11624 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11625
11626 cx.assert_editor_state(indoc! {"
11627 fn a() {
11628 «b();
11629 c();
11630 ˇ» d();
11631 }
11632 "});
11633
11634 // The comment prefix is inserted at the beginning of each line
11635 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11636
11637 cx.assert_editor_state(indoc! {"
11638 fn a() {
11639 // «b();
11640 // c();
11641 // ˇ» d();
11642 }
11643 "});
11644
11645 // If a selection ends at the beginning of a line, that line is not toggled.
11646 cx.set_selections_state(indoc! {"
11647 fn a() {
11648 // b();
11649 // «c();
11650 ˇ»// d();
11651 }
11652 "});
11653
11654 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11655
11656 cx.assert_editor_state(indoc! {"
11657 fn a() {
11658 // b();
11659 «c();
11660 ˇ»// d();
11661 }
11662 "});
11663
11664 // If a selection span a single line and is empty, the line is toggled.
11665 cx.set_state(indoc! {"
11666 fn a() {
11667 a();
11668 b();
11669 ˇ
11670 }
11671 "});
11672
11673 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11674
11675 cx.assert_editor_state(indoc! {"
11676 fn a() {
11677 a();
11678 b();
11679 //ˇ
11680 }
11681 "});
11682
11683 // If a selection span multiple lines, empty lines are not toggled.
11684 cx.set_state(indoc! {"
11685 fn a() {
11686 «a();
11687
11688 c();ˇ»
11689 }
11690 "});
11691
11692 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11693
11694 cx.assert_editor_state(indoc! {"
11695 fn a() {
11696 // «a();
11697
11698 // c();ˇ»
11699 }
11700 "});
11701
11702 // If a selection includes multiple comment prefixes, all lines are uncommented.
11703 cx.set_state(indoc! {"
11704 fn a() {
11705 // «a();
11706 /// b();
11707 //! c();ˇ»
11708 }
11709 "});
11710
11711 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11712
11713 cx.assert_editor_state(indoc! {"
11714 fn a() {
11715 «a();
11716 b();
11717 c();ˇ»
11718 }
11719 "});
11720}
11721
11722#[gpui::test]
11723async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
11724 init_test(cx, |_| {});
11725
11726 let language = Arc::new(Language::new(
11727 LanguageConfig {
11728 line_comments: vec!["// ".into()],
11729 ..Default::default()
11730 },
11731 Some(tree_sitter_rust::LANGUAGE.into()),
11732 ));
11733
11734 let mut cx = EditorTestContext::new(cx).await;
11735
11736 cx.language_registry().add(language.clone());
11737 cx.update_buffer(|buffer, cx| {
11738 buffer.set_language(Some(language), cx);
11739 });
11740
11741 let toggle_comments = &ToggleComments {
11742 advance_downwards: true,
11743 ignore_indent: false,
11744 };
11745
11746 // Single cursor on one line -> advance
11747 // Cursor moves horizontally 3 characters as well on non-blank line
11748 cx.set_state(indoc!(
11749 "fn a() {
11750 ˇdog();
11751 cat();
11752 }"
11753 ));
11754 cx.update_editor(|editor, window, cx| {
11755 editor.toggle_comments(toggle_comments, window, cx);
11756 });
11757 cx.assert_editor_state(indoc!(
11758 "fn a() {
11759 // dog();
11760 catˇ();
11761 }"
11762 ));
11763
11764 // Single selection on one line -> don't advance
11765 cx.set_state(indoc!(
11766 "fn a() {
11767 «dog()ˇ»;
11768 cat();
11769 }"
11770 ));
11771 cx.update_editor(|editor, window, cx| {
11772 editor.toggle_comments(toggle_comments, window, cx);
11773 });
11774 cx.assert_editor_state(indoc!(
11775 "fn a() {
11776 // «dog()ˇ»;
11777 cat();
11778 }"
11779 ));
11780
11781 // Multiple cursors on one line -> advance
11782 cx.set_state(indoc!(
11783 "fn a() {
11784 ˇdˇog();
11785 cat();
11786 }"
11787 ));
11788 cx.update_editor(|editor, window, cx| {
11789 editor.toggle_comments(toggle_comments, window, cx);
11790 });
11791 cx.assert_editor_state(indoc!(
11792 "fn a() {
11793 // dog();
11794 catˇ(ˇ);
11795 }"
11796 ));
11797
11798 // Multiple cursors on one line, with selection -> don't advance
11799 cx.set_state(indoc!(
11800 "fn a() {
11801 ˇdˇog«()ˇ»;
11802 cat();
11803 }"
11804 ));
11805 cx.update_editor(|editor, window, cx| {
11806 editor.toggle_comments(toggle_comments, window, cx);
11807 });
11808 cx.assert_editor_state(indoc!(
11809 "fn a() {
11810 // ˇdˇog«()ˇ»;
11811 cat();
11812 }"
11813 ));
11814
11815 // Single cursor on one line -> advance
11816 // Cursor moves to column 0 on blank line
11817 cx.set_state(indoc!(
11818 "fn a() {
11819 ˇdog();
11820
11821 cat();
11822 }"
11823 ));
11824 cx.update_editor(|editor, window, cx| {
11825 editor.toggle_comments(toggle_comments, window, cx);
11826 });
11827 cx.assert_editor_state(indoc!(
11828 "fn a() {
11829 // dog();
11830 ˇ
11831 cat();
11832 }"
11833 ));
11834
11835 // Single cursor on one line -> advance
11836 // Cursor starts and ends at column 0
11837 cx.set_state(indoc!(
11838 "fn a() {
11839 ˇ dog();
11840 cat();
11841 }"
11842 ));
11843 cx.update_editor(|editor, window, cx| {
11844 editor.toggle_comments(toggle_comments, window, cx);
11845 });
11846 cx.assert_editor_state(indoc!(
11847 "fn a() {
11848 // dog();
11849 ˇ cat();
11850 }"
11851 ));
11852}
11853
11854#[gpui::test]
11855async fn test_toggle_block_comment(cx: &mut TestAppContext) {
11856 init_test(cx, |_| {});
11857
11858 let mut cx = EditorTestContext::new(cx).await;
11859
11860 let html_language = Arc::new(
11861 Language::new(
11862 LanguageConfig {
11863 name: "HTML".into(),
11864 block_comment: Some(("<!-- ".into(), " -->".into())),
11865 ..Default::default()
11866 },
11867 Some(tree_sitter_html::LANGUAGE.into()),
11868 )
11869 .with_injection_query(
11870 r#"
11871 (script_element
11872 (raw_text) @injection.content
11873 (#set! injection.language "javascript"))
11874 "#,
11875 )
11876 .unwrap(),
11877 );
11878
11879 let javascript_language = Arc::new(Language::new(
11880 LanguageConfig {
11881 name: "JavaScript".into(),
11882 line_comments: vec!["// ".into()],
11883 ..Default::default()
11884 },
11885 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
11886 ));
11887
11888 cx.language_registry().add(html_language.clone());
11889 cx.language_registry().add(javascript_language.clone());
11890 cx.update_buffer(|buffer, cx| {
11891 buffer.set_language(Some(html_language), cx);
11892 });
11893
11894 // Toggle comments for empty selections
11895 cx.set_state(
11896 &r#"
11897 <p>A</p>ˇ
11898 <p>B</p>ˇ
11899 <p>C</p>ˇ
11900 "#
11901 .unindent(),
11902 );
11903 cx.update_editor(|editor, window, cx| {
11904 editor.toggle_comments(&ToggleComments::default(), window, cx)
11905 });
11906 cx.assert_editor_state(
11907 &r#"
11908 <!-- <p>A</p>ˇ -->
11909 <!-- <p>B</p>ˇ -->
11910 <!-- <p>C</p>ˇ -->
11911 "#
11912 .unindent(),
11913 );
11914 cx.update_editor(|editor, window, cx| {
11915 editor.toggle_comments(&ToggleComments::default(), window, cx)
11916 });
11917 cx.assert_editor_state(
11918 &r#"
11919 <p>A</p>ˇ
11920 <p>B</p>ˇ
11921 <p>C</p>ˇ
11922 "#
11923 .unindent(),
11924 );
11925
11926 // Toggle comments for mixture of empty and non-empty selections, where
11927 // multiple selections occupy a given line.
11928 cx.set_state(
11929 &r#"
11930 <p>A«</p>
11931 <p>ˇ»B</p>ˇ
11932 <p>C«</p>
11933 <p>ˇ»D</p>ˇ
11934 "#
11935 .unindent(),
11936 );
11937
11938 cx.update_editor(|editor, window, cx| {
11939 editor.toggle_comments(&ToggleComments::default(), window, cx)
11940 });
11941 cx.assert_editor_state(
11942 &r#"
11943 <!-- <p>A«</p>
11944 <p>ˇ»B</p>ˇ -->
11945 <!-- <p>C«</p>
11946 <p>ˇ»D</p>ˇ -->
11947 "#
11948 .unindent(),
11949 );
11950 cx.update_editor(|editor, window, cx| {
11951 editor.toggle_comments(&ToggleComments::default(), window, cx)
11952 });
11953 cx.assert_editor_state(
11954 &r#"
11955 <p>A«</p>
11956 <p>ˇ»B</p>ˇ
11957 <p>C«</p>
11958 <p>ˇ»D</p>ˇ
11959 "#
11960 .unindent(),
11961 );
11962
11963 // Toggle comments when different languages are active for different
11964 // selections.
11965 cx.set_state(
11966 &r#"
11967 ˇ<script>
11968 ˇvar x = new Y();
11969 ˇ</script>
11970 "#
11971 .unindent(),
11972 );
11973 cx.executor().run_until_parked();
11974 cx.update_editor(|editor, window, cx| {
11975 editor.toggle_comments(&ToggleComments::default(), window, cx)
11976 });
11977 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
11978 // Uncommenting and commenting from this position brings in even more wrong artifacts.
11979 cx.assert_editor_state(
11980 &r#"
11981 <!-- ˇ<script> -->
11982 // ˇvar x = new Y();
11983 <!-- ˇ</script> -->
11984 "#
11985 .unindent(),
11986 );
11987}
11988
11989#[gpui::test]
11990fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
11991 init_test(cx, |_| {});
11992
11993 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
11994 let multibuffer = cx.new(|cx| {
11995 let mut multibuffer = MultiBuffer::new(ReadWrite);
11996 multibuffer.push_excerpts(
11997 buffer.clone(),
11998 [
11999 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
12000 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
12001 ],
12002 cx,
12003 );
12004 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
12005 multibuffer
12006 });
12007
12008 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12009 editor.update_in(cx, |editor, window, cx| {
12010 assert_eq!(editor.text(cx), "aaaa\nbbbb");
12011 editor.change_selections(None, window, cx, |s| {
12012 s.select_ranges([
12013 Point::new(0, 0)..Point::new(0, 0),
12014 Point::new(1, 0)..Point::new(1, 0),
12015 ])
12016 });
12017
12018 editor.handle_input("X", window, cx);
12019 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
12020 assert_eq!(
12021 editor.selections.ranges(cx),
12022 [
12023 Point::new(0, 1)..Point::new(0, 1),
12024 Point::new(1, 1)..Point::new(1, 1),
12025 ]
12026 );
12027
12028 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
12029 editor.change_selections(None, window, cx, |s| {
12030 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
12031 });
12032 editor.backspace(&Default::default(), window, cx);
12033 assert_eq!(editor.text(cx), "Xa\nbbb");
12034 assert_eq!(
12035 editor.selections.ranges(cx),
12036 [Point::new(1, 0)..Point::new(1, 0)]
12037 );
12038
12039 editor.change_selections(None, window, cx, |s| {
12040 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
12041 });
12042 editor.backspace(&Default::default(), window, cx);
12043 assert_eq!(editor.text(cx), "X\nbb");
12044 assert_eq!(
12045 editor.selections.ranges(cx),
12046 [Point::new(0, 1)..Point::new(0, 1)]
12047 );
12048 });
12049}
12050
12051#[gpui::test]
12052fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
12053 init_test(cx, |_| {});
12054
12055 let markers = vec![('[', ']').into(), ('(', ')').into()];
12056 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
12057 indoc! {"
12058 [aaaa
12059 (bbbb]
12060 cccc)",
12061 },
12062 markers.clone(),
12063 );
12064 let excerpt_ranges = markers.into_iter().map(|marker| {
12065 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
12066 ExcerptRange::new(context.clone())
12067 });
12068 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
12069 let multibuffer = cx.new(|cx| {
12070 let mut multibuffer = MultiBuffer::new(ReadWrite);
12071 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
12072 multibuffer
12073 });
12074
12075 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12076 editor.update_in(cx, |editor, window, cx| {
12077 let (expected_text, selection_ranges) = marked_text_ranges(
12078 indoc! {"
12079 aaaa
12080 bˇbbb
12081 bˇbbˇb
12082 cccc"
12083 },
12084 true,
12085 );
12086 assert_eq!(editor.text(cx), expected_text);
12087 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
12088
12089 editor.handle_input("X", window, cx);
12090
12091 let (expected_text, expected_selections) = marked_text_ranges(
12092 indoc! {"
12093 aaaa
12094 bXˇbbXb
12095 bXˇbbXˇb
12096 cccc"
12097 },
12098 false,
12099 );
12100 assert_eq!(editor.text(cx), expected_text);
12101 assert_eq!(editor.selections.ranges(cx), expected_selections);
12102
12103 editor.newline(&Newline, window, cx);
12104 let (expected_text, expected_selections) = marked_text_ranges(
12105 indoc! {"
12106 aaaa
12107 bX
12108 ˇbbX
12109 b
12110 bX
12111 ˇbbX
12112 ˇb
12113 cccc"
12114 },
12115 false,
12116 );
12117 assert_eq!(editor.text(cx), expected_text);
12118 assert_eq!(editor.selections.ranges(cx), expected_selections);
12119 });
12120}
12121
12122#[gpui::test]
12123fn test_refresh_selections(cx: &mut TestAppContext) {
12124 init_test(cx, |_| {});
12125
12126 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12127 let mut excerpt1_id = None;
12128 let multibuffer = cx.new(|cx| {
12129 let mut multibuffer = MultiBuffer::new(ReadWrite);
12130 excerpt1_id = multibuffer
12131 .push_excerpts(
12132 buffer.clone(),
12133 [
12134 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12135 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12136 ],
12137 cx,
12138 )
12139 .into_iter()
12140 .next();
12141 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12142 multibuffer
12143 });
12144
12145 let editor = cx.add_window(|window, cx| {
12146 let mut editor = build_editor(multibuffer.clone(), window, cx);
12147 let snapshot = editor.snapshot(window, cx);
12148 editor.change_selections(None, window, cx, |s| {
12149 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
12150 });
12151 editor.begin_selection(
12152 Point::new(2, 1).to_display_point(&snapshot),
12153 true,
12154 1,
12155 window,
12156 cx,
12157 );
12158 assert_eq!(
12159 editor.selections.ranges(cx),
12160 [
12161 Point::new(1, 3)..Point::new(1, 3),
12162 Point::new(2, 1)..Point::new(2, 1),
12163 ]
12164 );
12165 editor
12166 });
12167
12168 // Refreshing selections is a no-op when excerpts haven't changed.
12169 _ = editor.update(cx, |editor, window, cx| {
12170 editor.change_selections(None, window, cx, |s| s.refresh());
12171 assert_eq!(
12172 editor.selections.ranges(cx),
12173 [
12174 Point::new(1, 3)..Point::new(1, 3),
12175 Point::new(2, 1)..Point::new(2, 1),
12176 ]
12177 );
12178 });
12179
12180 multibuffer.update(cx, |multibuffer, cx| {
12181 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12182 });
12183 _ = editor.update(cx, |editor, window, cx| {
12184 // Removing an excerpt causes the first selection to become degenerate.
12185 assert_eq!(
12186 editor.selections.ranges(cx),
12187 [
12188 Point::new(0, 0)..Point::new(0, 0),
12189 Point::new(0, 1)..Point::new(0, 1)
12190 ]
12191 );
12192
12193 // Refreshing selections will relocate the first selection to the original buffer
12194 // location.
12195 editor.change_selections(None, window, cx, |s| s.refresh());
12196 assert_eq!(
12197 editor.selections.ranges(cx),
12198 [
12199 Point::new(0, 1)..Point::new(0, 1),
12200 Point::new(0, 3)..Point::new(0, 3)
12201 ]
12202 );
12203 assert!(editor.selections.pending_anchor().is_some());
12204 });
12205}
12206
12207#[gpui::test]
12208fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
12209 init_test(cx, |_| {});
12210
12211 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12212 let mut excerpt1_id = None;
12213 let multibuffer = cx.new(|cx| {
12214 let mut multibuffer = MultiBuffer::new(ReadWrite);
12215 excerpt1_id = multibuffer
12216 .push_excerpts(
12217 buffer.clone(),
12218 [
12219 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12220 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12221 ],
12222 cx,
12223 )
12224 .into_iter()
12225 .next();
12226 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12227 multibuffer
12228 });
12229
12230 let editor = cx.add_window(|window, cx| {
12231 let mut editor = build_editor(multibuffer.clone(), window, cx);
12232 let snapshot = editor.snapshot(window, cx);
12233 editor.begin_selection(
12234 Point::new(1, 3).to_display_point(&snapshot),
12235 false,
12236 1,
12237 window,
12238 cx,
12239 );
12240 assert_eq!(
12241 editor.selections.ranges(cx),
12242 [Point::new(1, 3)..Point::new(1, 3)]
12243 );
12244 editor
12245 });
12246
12247 multibuffer.update(cx, |multibuffer, cx| {
12248 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12249 });
12250 _ = editor.update(cx, |editor, window, cx| {
12251 assert_eq!(
12252 editor.selections.ranges(cx),
12253 [Point::new(0, 0)..Point::new(0, 0)]
12254 );
12255
12256 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
12257 editor.change_selections(None, window, cx, |s| s.refresh());
12258 assert_eq!(
12259 editor.selections.ranges(cx),
12260 [Point::new(0, 3)..Point::new(0, 3)]
12261 );
12262 assert!(editor.selections.pending_anchor().is_some());
12263 });
12264}
12265
12266#[gpui::test]
12267async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
12268 init_test(cx, |_| {});
12269
12270 let language = Arc::new(
12271 Language::new(
12272 LanguageConfig {
12273 brackets: BracketPairConfig {
12274 pairs: vec![
12275 BracketPair {
12276 start: "{".to_string(),
12277 end: "}".to_string(),
12278 close: true,
12279 surround: true,
12280 newline: true,
12281 },
12282 BracketPair {
12283 start: "/* ".to_string(),
12284 end: " */".to_string(),
12285 close: true,
12286 surround: true,
12287 newline: true,
12288 },
12289 ],
12290 ..Default::default()
12291 },
12292 ..Default::default()
12293 },
12294 Some(tree_sitter_rust::LANGUAGE.into()),
12295 )
12296 .with_indents_query("")
12297 .unwrap(),
12298 );
12299
12300 let text = concat!(
12301 "{ }\n", //
12302 " x\n", //
12303 " /* */\n", //
12304 "x\n", //
12305 "{{} }\n", //
12306 );
12307
12308 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
12309 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12310 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
12311 editor
12312 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
12313 .await;
12314
12315 editor.update_in(cx, |editor, window, cx| {
12316 editor.change_selections(None, window, cx, |s| {
12317 s.select_display_ranges([
12318 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
12319 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
12320 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
12321 ])
12322 });
12323 editor.newline(&Newline, window, cx);
12324
12325 assert_eq!(
12326 editor.buffer().read(cx).read(cx).text(),
12327 concat!(
12328 "{ \n", // Suppress rustfmt
12329 "\n", //
12330 "}\n", //
12331 " x\n", //
12332 " /* \n", //
12333 " \n", //
12334 " */\n", //
12335 "x\n", //
12336 "{{} \n", //
12337 "}\n", //
12338 )
12339 );
12340 });
12341}
12342
12343#[gpui::test]
12344fn test_highlighted_ranges(cx: &mut TestAppContext) {
12345 init_test(cx, |_| {});
12346
12347 let editor = cx.add_window(|window, cx| {
12348 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
12349 build_editor(buffer.clone(), window, cx)
12350 });
12351
12352 _ = editor.update(cx, |editor, window, cx| {
12353 struct Type1;
12354 struct Type2;
12355
12356 let buffer = editor.buffer.read(cx).snapshot(cx);
12357
12358 let anchor_range =
12359 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
12360
12361 editor.highlight_background::<Type1>(
12362 &[
12363 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
12364 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
12365 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
12366 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
12367 ],
12368 |_| Hsla::red(),
12369 cx,
12370 );
12371 editor.highlight_background::<Type2>(
12372 &[
12373 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
12374 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
12375 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
12376 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
12377 ],
12378 |_| Hsla::green(),
12379 cx,
12380 );
12381
12382 let snapshot = editor.snapshot(window, cx);
12383 let mut highlighted_ranges = editor.background_highlights_in_range(
12384 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
12385 &snapshot,
12386 cx.theme().colors(),
12387 );
12388 // Enforce a consistent ordering based on color without relying on the ordering of the
12389 // highlight's `TypeId` which is non-executor.
12390 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
12391 assert_eq!(
12392 highlighted_ranges,
12393 &[
12394 (
12395 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
12396 Hsla::red(),
12397 ),
12398 (
12399 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
12400 Hsla::red(),
12401 ),
12402 (
12403 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
12404 Hsla::green(),
12405 ),
12406 (
12407 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
12408 Hsla::green(),
12409 ),
12410 ]
12411 );
12412 assert_eq!(
12413 editor.background_highlights_in_range(
12414 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
12415 &snapshot,
12416 cx.theme().colors(),
12417 ),
12418 &[(
12419 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
12420 Hsla::red(),
12421 )]
12422 );
12423 });
12424}
12425
12426#[gpui::test]
12427async fn test_following(cx: &mut TestAppContext) {
12428 init_test(cx, |_| {});
12429
12430 let fs = FakeFs::new(cx.executor());
12431 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
12432
12433 let buffer = project.update(cx, |project, cx| {
12434 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
12435 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
12436 });
12437 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
12438 let follower = cx.update(|cx| {
12439 cx.open_window(
12440 WindowOptions {
12441 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
12442 gpui::Point::new(px(0.), px(0.)),
12443 gpui::Point::new(px(10.), px(80.)),
12444 ))),
12445 ..Default::default()
12446 },
12447 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
12448 )
12449 .unwrap()
12450 });
12451
12452 let is_still_following = Rc::new(RefCell::new(true));
12453 let follower_edit_event_count = Rc::new(RefCell::new(0));
12454 let pending_update = Rc::new(RefCell::new(None));
12455 let leader_entity = leader.root(cx).unwrap();
12456 let follower_entity = follower.root(cx).unwrap();
12457 _ = follower.update(cx, {
12458 let update = pending_update.clone();
12459 let is_still_following = is_still_following.clone();
12460 let follower_edit_event_count = follower_edit_event_count.clone();
12461 |_, window, cx| {
12462 cx.subscribe_in(
12463 &leader_entity,
12464 window,
12465 move |_, leader, event, window, cx| {
12466 leader.read(cx).add_event_to_update_proto(
12467 event,
12468 &mut update.borrow_mut(),
12469 window,
12470 cx,
12471 );
12472 },
12473 )
12474 .detach();
12475
12476 cx.subscribe_in(
12477 &follower_entity,
12478 window,
12479 move |_, _, event: &EditorEvent, _window, _cx| {
12480 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
12481 *is_still_following.borrow_mut() = false;
12482 }
12483
12484 if let EditorEvent::BufferEdited = event {
12485 *follower_edit_event_count.borrow_mut() += 1;
12486 }
12487 },
12488 )
12489 .detach();
12490 }
12491 });
12492
12493 // Update the selections only
12494 _ = leader.update(cx, |leader, window, cx| {
12495 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
12496 });
12497 follower
12498 .update(cx, |follower, window, cx| {
12499 follower.apply_update_proto(
12500 &project,
12501 pending_update.borrow_mut().take().unwrap(),
12502 window,
12503 cx,
12504 )
12505 })
12506 .unwrap()
12507 .await
12508 .unwrap();
12509 _ = follower.update(cx, |follower, _, cx| {
12510 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
12511 });
12512 assert!(*is_still_following.borrow());
12513 assert_eq!(*follower_edit_event_count.borrow(), 0);
12514
12515 // Update the scroll position only
12516 _ = leader.update(cx, |leader, window, cx| {
12517 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
12518 });
12519 follower
12520 .update(cx, |follower, window, cx| {
12521 follower.apply_update_proto(
12522 &project,
12523 pending_update.borrow_mut().take().unwrap(),
12524 window,
12525 cx,
12526 )
12527 })
12528 .unwrap()
12529 .await
12530 .unwrap();
12531 assert_eq!(
12532 follower
12533 .update(cx, |follower, _, cx| follower.scroll_position(cx))
12534 .unwrap(),
12535 gpui::Point::new(1.5, 3.5)
12536 );
12537 assert!(*is_still_following.borrow());
12538 assert_eq!(*follower_edit_event_count.borrow(), 0);
12539
12540 // Update the selections and scroll position. The follower's scroll position is updated
12541 // via autoscroll, not via the leader's exact scroll position.
12542 _ = leader.update(cx, |leader, window, cx| {
12543 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
12544 leader.request_autoscroll(Autoscroll::newest(), cx);
12545 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
12546 });
12547 follower
12548 .update(cx, |follower, window, cx| {
12549 follower.apply_update_proto(
12550 &project,
12551 pending_update.borrow_mut().take().unwrap(),
12552 window,
12553 cx,
12554 )
12555 })
12556 .unwrap()
12557 .await
12558 .unwrap();
12559 _ = follower.update(cx, |follower, _, cx| {
12560 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
12561 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
12562 });
12563 assert!(*is_still_following.borrow());
12564
12565 // Creating a pending selection that precedes another selection
12566 _ = leader.update(cx, |leader, window, cx| {
12567 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
12568 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
12569 });
12570 follower
12571 .update(cx, |follower, window, cx| {
12572 follower.apply_update_proto(
12573 &project,
12574 pending_update.borrow_mut().take().unwrap(),
12575 window,
12576 cx,
12577 )
12578 })
12579 .unwrap()
12580 .await
12581 .unwrap();
12582 _ = follower.update(cx, |follower, _, cx| {
12583 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
12584 });
12585 assert!(*is_still_following.borrow());
12586
12587 // Extend the pending selection so that it surrounds another selection
12588 _ = leader.update(cx, |leader, window, cx| {
12589 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
12590 });
12591 follower
12592 .update(cx, |follower, window, cx| {
12593 follower.apply_update_proto(
12594 &project,
12595 pending_update.borrow_mut().take().unwrap(),
12596 window,
12597 cx,
12598 )
12599 })
12600 .unwrap()
12601 .await
12602 .unwrap();
12603 _ = follower.update(cx, |follower, _, cx| {
12604 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
12605 });
12606
12607 // Scrolling locally breaks the follow
12608 _ = follower.update(cx, |follower, window, cx| {
12609 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
12610 follower.set_scroll_anchor(
12611 ScrollAnchor {
12612 anchor: top_anchor,
12613 offset: gpui::Point::new(0.0, 0.5),
12614 },
12615 window,
12616 cx,
12617 );
12618 });
12619 assert!(!(*is_still_following.borrow()));
12620}
12621
12622#[gpui::test]
12623async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
12624 init_test(cx, |_| {});
12625
12626 let fs = FakeFs::new(cx.executor());
12627 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
12628 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12629 let pane = workspace
12630 .update(cx, |workspace, _, _| workspace.active_pane().clone())
12631 .unwrap();
12632
12633 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12634
12635 let leader = pane.update_in(cx, |_, window, cx| {
12636 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
12637 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
12638 });
12639
12640 // Start following the editor when it has no excerpts.
12641 let mut state_message =
12642 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
12643 let workspace_entity = workspace.root(cx).unwrap();
12644 let follower_1 = cx
12645 .update_window(*workspace.deref(), |_, window, cx| {
12646 Editor::from_state_proto(
12647 workspace_entity,
12648 ViewId {
12649 creator: CollaboratorId::PeerId(PeerId::default()),
12650 id: 0,
12651 },
12652 &mut state_message,
12653 window,
12654 cx,
12655 )
12656 })
12657 .unwrap()
12658 .unwrap()
12659 .await
12660 .unwrap();
12661
12662 let update_message = Rc::new(RefCell::new(None));
12663 follower_1.update_in(cx, {
12664 let update = update_message.clone();
12665 |_, window, cx| {
12666 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
12667 leader.read(cx).add_event_to_update_proto(
12668 event,
12669 &mut update.borrow_mut(),
12670 window,
12671 cx,
12672 );
12673 })
12674 .detach();
12675 }
12676 });
12677
12678 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
12679 (
12680 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
12681 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
12682 )
12683 });
12684
12685 // Insert some excerpts.
12686 leader.update(cx, |leader, cx| {
12687 leader.buffer.update(cx, |multibuffer, cx| {
12688 multibuffer.set_excerpts_for_path(
12689 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
12690 buffer_1.clone(),
12691 vec![
12692 Point::row_range(0..3),
12693 Point::row_range(1..6),
12694 Point::row_range(12..15),
12695 ],
12696 0,
12697 cx,
12698 );
12699 multibuffer.set_excerpts_for_path(
12700 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
12701 buffer_2.clone(),
12702 vec![Point::row_range(0..6), Point::row_range(8..12)],
12703 0,
12704 cx,
12705 );
12706 });
12707 });
12708
12709 // Apply the update of adding the excerpts.
12710 follower_1
12711 .update_in(cx, |follower, window, cx| {
12712 follower.apply_update_proto(
12713 &project,
12714 update_message.borrow().clone().unwrap(),
12715 window,
12716 cx,
12717 )
12718 })
12719 .await
12720 .unwrap();
12721 assert_eq!(
12722 follower_1.update(cx, |editor, cx| editor.text(cx)),
12723 leader.update(cx, |editor, cx| editor.text(cx))
12724 );
12725 update_message.borrow_mut().take();
12726
12727 // Start following separately after it already has excerpts.
12728 let mut state_message =
12729 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
12730 let workspace_entity = workspace.root(cx).unwrap();
12731 let follower_2 = cx
12732 .update_window(*workspace.deref(), |_, window, cx| {
12733 Editor::from_state_proto(
12734 workspace_entity,
12735 ViewId {
12736 creator: CollaboratorId::PeerId(PeerId::default()),
12737 id: 0,
12738 },
12739 &mut state_message,
12740 window,
12741 cx,
12742 )
12743 })
12744 .unwrap()
12745 .unwrap()
12746 .await
12747 .unwrap();
12748 assert_eq!(
12749 follower_2.update(cx, |editor, cx| editor.text(cx)),
12750 leader.update(cx, |editor, cx| editor.text(cx))
12751 );
12752
12753 // Remove some excerpts.
12754 leader.update(cx, |leader, cx| {
12755 leader.buffer.update(cx, |multibuffer, cx| {
12756 let excerpt_ids = multibuffer.excerpt_ids();
12757 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
12758 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
12759 });
12760 });
12761
12762 // Apply the update of removing the excerpts.
12763 follower_1
12764 .update_in(cx, |follower, window, cx| {
12765 follower.apply_update_proto(
12766 &project,
12767 update_message.borrow().clone().unwrap(),
12768 window,
12769 cx,
12770 )
12771 })
12772 .await
12773 .unwrap();
12774 follower_2
12775 .update_in(cx, |follower, window, cx| {
12776 follower.apply_update_proto(
12777 &project,
12778 update_message.borrow().clone().unwrap(),
12779 window,
12780 cx,
12781 )
12782 })
12783 .await
12784 .unwrap();
12785 update_message.borrow_mut().take();
12786 assert_eq!(
12787 follower_1.update(cx, |editor, cx| editor.text(cx)),
12788 leader.update(cx, |editor, cx| editor.text(cx))
12789 );
12790}
12791
12792#[gpui::test]
12793async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
12794 init_test(cx, |_| {});
12795
12796 let mut cx = EditorTestContext::new(cx).await;
12797 let lsp_store =
12798 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
12799
12800 cx.set_state(indoc! {"
12801 ˇfn func(abc def: i32) -> u32 {
12802 }
12803 "});
12804
12805 cx.update(|_, cx| {
12806 lsp_store.update(cx, |lsp_store, cx| {
12807 lsp_store
12808 .update_diagnostics(
12809 LanguageServerId(0),
12810 lsp::PublishDiagnosticsParams {
12811 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
12812 version: None,
12813 diagnostics: vec![
12814 lsp::Diagnostic {
12815 range: lsp::Range::new(
12816 lsp::Position::new(0, 11),
12817 lsp::Position::new(0, 12),
12818 ),
12819 severity: Some(lsp::DiagnosticSeverity::ERROR),
12820 ..Default::default()
12821 },
12822 lsp::Diagnostic {
12823 range: lsp::Range::new(
12824 lsp::Position::new(0, 12),
12825 lsp::Position::new(0, 15),
12826 ),
12827 severity: Some(lsp::DiagnosticSeverity::ERROR),
12828 ..Default::default()
12829 },
12830 lsp::Diagnostic {
12831 range: lsp::Range::new(
12832 lsp::Position::new(0, 25),
12833 lsp::Position::new(0, 28),
12834 ),
12835 severity: Some(lsp::DiagnosticSeverity::ERROR),
12836 ..Default::default()
12837 },
12838 ],
12839 },
12840 &[],
12841 cx,
12842 )
12843 .unwrap()
12844 });
12845 });
12846
12847 executor.run_until_parked();
12848
12849 cx.update_editor(|editor, window, cx| {
12850 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12851 });
12852
12853 cx.assert_editor_state(indoc! {"
12854 fn func(abc def: i32) -> ˇu32 {
12855 }
12856 "});
12857
12858 cx.update_editor(|editor, window, cx| {
12859 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12860 });
12861
12862 cx.assert_editor_state(indoc! {"
12863 fn func(abc ˇdef: i32) -> u32 {
12864 }
12865 "});
12866
12867 cx.update_editor(|editor, window, cx| {
12868 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12869 });
12870
12871 cx.assert_editor_state(indoc! {"
12872 fn func(abcˇ def: i32) -> u32 {
12873 }
12874 "});
12875
12876 cx.update_editor(|editor, window, cx| {
12877 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12878 });
12879
12880 cx.assert_editor_state(indoc! {"
12881 fn func(abc def: i32) -> ˇu32 {
12882 }
12883 "});
12884}
12885
12886#[gpui::test]
12887async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
12888 init_test(cx, |_| {});
12889
12890 let mut cx = EditorTestContext::new(cx).await;
12891
12892 let diff_base = r#"
12893 use some::mod;
12894
12895 const A: u32 = 42;
12896
12897 fn main() {
12898 println!("hello");
12899
12900 println!("world");
12901 }
12902 "#
12903 .unindent();
12904
12905 // Edits are modified, removed, modified, added
12906 cx.set_state(
12907 &r#"
12908 use some::modified;
12909
12910 ˇ
12911 fn main() {
12912 println!("hello there");
12913
12914 println!("around the");
12915 println!("world");
12916 }
12917 "#
12918 .unindent(),
12919 );
12920
12921 cx.set_head_text(&diff_base);
12922 executor.run_until_parked();
12923
12924 cx.update_editor(|editor, window, cx| {
12925 //Wrap around the bottom of the buffer
12926 for _ in 0..3 {
12927 editor.go_to_next_hunk(&GoToHunk, window, cx);
12928 }
12929 });
12930
12931 cx.assert_editor_state(
12932 &r#"
12933 ˇuse some::modified;
12934
12935
12936 fn main() {
12937 println!("hello there");
12938
12939 println!("around the");
12940 println!("world");
12941 }
12942 "#
12943 .unindent(),
12944 );
12945
12946 cx.update_editor(|editor, window, cx| {
12947 //Wrap around the top of the buffer
12948 for _ in 0..2 {
12949 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12950 }
12951 });
12952
12953 cx.assert_editor_state(
12954 &r#"
12955 use some::modified;
12956
12957
12958 fn main() {
12959 ˇ println!("hello there");
12960
12961 println!("around the");
12962 println!("world");
12963 }
12964 "#
12965 .unindent(),
12966 );
12967
12968 cx.update_editor(|editor, window, cx| {
12969 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12970 });
12971
12972 cx.assert_editor_state(
12973 &r#"
12974 use some::modified;
12975
12976 ˇ
12977 fn main() {
12978 println!("hello there");
12979
12980 println!("around the");
12981 println!("world");
12982 }
12983 "#
12984 .unindent(),
12985 );
12986
12987 cx.update_editor(|editor, window, cx| {
12988 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12989 });
12990
12991 cx.assert_editor_state(
12992 &r#"
12993 ˇuse some::modified;
12994
12995
12996 fn main() {
12997 println!("hello there");
12998
12999 println!("around the");
13000 println!("world");
13001 }
13002 "#
13003 .unindent(),
13004 );
13005
13006 cx.update_editor(|editor, window, cx| {
13007 for _ in 0..2 {
13008 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13009 }
13010 });
13011
13012 cx.assert_editor_state(
13013 &r#"
13014 use some::modified;
13015
13016
13017 fn main() {
13018 ˇ println!("hello there");
13019
13020 println!("around the");
13021 println!("world");
13022 }
13023 "#
13024 .unindent(),
13025 );
13026
13027 cx.update_editor(|editor, window, cx| {
13028 editor.fold(&Fold, window, cx);
13029 });
13030
13031 cx.update_editor(|editor, window, cx| {
13032 editor.go_to_next_hunk(&GoToHunk, window, cx);
13033 });
13034
13035 cx.assert_editor_state(
13036 &r#"
13037 ˇuse some::modified;
13038
13039
13040 fn main() {
13041 println!("hello there");
13042
13043 println!("around the");
13044 println!("world");
13045 }
13046 "#
13047 .unindent(),
13048 );
13049}
13050
13051#[test]
13052fn test_split_words() {
13053 fn split(text: &str) -> Vec<&str> {
13054 split_words(text).collect()
13055 }
13056
13057 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
13058 assert_eq!(split("hello_world"), &["hello_", "world"]);
13059 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
13060 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
13061 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
13062 assert_eq!(split("helloworld"), &["helloworld"]);
13063
13064 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
13065}
13066
13067#[gpui::test]
13068async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
13069 init_test(cx, |_| {});
13070
13071 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
13072 let mut assert = |before, after| {
13073 let _state_context = cx.set_state(before);
13074 cx.run_until_parked();
13075 cx.update_editor(|editor, window, cx| {
13076 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
13077 });
13078 cx.run_until_parked();
13079 cx.assert_editor_state(after);
13080 };
13081
13082 // Outside bracket jumps to outside of matching bracket
13083 assert("console.logˇ(var);", "console.log(var)ˇ;");
13084 assert("console.log(var)ˇ;", "console.logˇ(var);");
13085
13086 // Inside bracket jumps to inside of matching bracket
13087 assert("console.log(ˇvar);", "console.log(varˇ);");
13088 assert("console.log(varˇ);", "console.log(ˇvar);");
13089
13090 // When outside a bracket and inside, favor jumping to the inside bracket
13091 assert(
13092 "console.log('foo', [1, 2, 3]ˇ);",
13093 "console.log(ˇ'foo', [1, 2, 3]);",
13094 );
13095 assert(
13096 "console.log(ˇ'foo', [1, 2, 3]);",
13097 "console.log('foo', [1, 2, 3]ˇ);",
13098 );
13099
13100 // Bias forward if two options are equally likely
13101 assert(
13102 "let result = curried_fun()ˇ();",
13103 "let result = curried_fun()()ˇ;",
13104 );
13105
13106 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
13107 assert(
13108 indoc! {"
13109 function test() {
13110 console.log('test')ˇ
13111 }"},
13112 indoc! {"
13113 function test() {
13114 console.logˇ('test')
13115 }"},
13116 );
13117}
13118
13119#[gpui::test]
13120async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
13121 init_test(cx, |_| {});
13122
13123 let fs = FakeFs::new(cx.executor());
13124 fs.insert_tree(
13125 path!("/a"),
13126 json!({
13127 "main.rs": "fn main() { let a = 5; }",
13128 "other.rs": "// Test file",
13129 }),
13130 )
13131 .await;
13132 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13133
13134 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13135 language_registry.add(Arc::new(Language::new(
13136 LanguageConfig {
13137 name: "Rust".into(),
13138 matcher: LanguageMatcher {
13139 path_suffixes: vec!["rs".to_string()],
13140 ..Default::default()
13141 },
13142 brackets: BracketPairConfig {
13143 pairs: vec![BracketPair {
13144 start: "{".to_string(),
13145 end: "}".to_string(),
13146 close: true,
13147 surround: true,
13148 newline: true,
13149 }],
13150 disabled_scopes_by_bracket_ix: Vec::new(),
13151 },
13152 ..Default::default()
13153 },
13154 Some(tree_sitter_rust::LANGUAGE.into()),
13155 )));
13156 let mut fake_servers = language_registry.register_fake_lsp(
13157 "Rust",
13158 FakeLspAdapter {
13159 capabilities: lsp::ServerCapabilities {
13160 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
13161 first_trigger_character: "{".to_string(),
13162 more_trigger_character: None,
13163 }),
13164 ..Default::default()
13165 },
13166 ..Default::default()
13167 },
13168 );
13169
13170 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13171
13172 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13173
13174 let worktree_id = workspace
13175 .update(cx, |workspace, _, cx| {
13176 workspace.project().update(cx, |project, cx| {
13177 project.worktrees(cx).next().unwrap().read(cx).id()
13178 })
13179 })
13180 .unwrap();
13181
13182 let buffer = project
13183 .update(cx, |project, cx| {
13184 project.open_local_buffer(path!("/a/main.rs"), cx)
13185 })
13186 .await
13187 .unwrap();
13188 let editor_handle = workspace
13189 .update(cx, |workspace, window, cx| {
13190 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
13191 })
13192 .unwrap()
13193 .await
13194 .unwrap()
13195 .downcast::<Editor>()
13196 .unwrap();
13197
13198 cx.executor().start_waiting();
13199 let fake_server = fake_servers.next().await.unwrap();
13200
13201 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
13202 |params, _| async move {
13203 assert_eq!(
13204 params.text_document_position.text_document.uri,
13205 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
13206 );
13207 assert_eq!(
13208 params.text_document_position.position,
13209 lsp::Position::new(0, 21),
13210 );
13211
13212 Ok(Some(vec![lsp::TextEdit {
13213 new_text: "]".to_string(),
13214 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13215 }]))
13216 },
13217 );
13218
13219 editor_handle.update_in(cx, |editor, window, cx| {
13220 window.focus(&editor.focus_handle(cx));
13221 editor.change_selections(None, window, cx, |s| {
13222 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
13223 });
13224 editor.handle_input("{", window, cx);
13225 });
13226
13227 cx.executor().run_until_parked();
13228
13229 buffer.update(cx, |buffer, _| {
13230 assert_eq!(
13231 buffer.text(),
13232 "fn main() { let a = {5}; }",
13233 "No extra braces from on type formatting should appear in the buffer"
13234 )
13235 });
13236}
13237
13238#[gpui::test]
13239async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
13240 init_test(cx, |_| {});
13241
13242 let fs = FakeFs::new(cx.executor());
13243 fs.insert_tree(
13244 path!("/a"),
13245 json!({
13246 "main.rs": "fn main() { let a = 5; }",
13247 "other.rs": "// Test file",
13248 }),
13249 )
13250 .await;
13251
13252 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13253
13254 let server_restarts = Arc::new(AtomicUsize::new(0));
13255 let closure_restarts = Arc::clone(&server_restarts);
13256 let language_server_name = "test language server";
13257 let language_name: LanguageName = "Rust".into();
13258
13259 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13260 language_registry.add(Arc::new(Language::new(
13261 LanguageConfig {
13262 name: language_name.clone(),
13263 matcher: LanguageMatcher {
13264 path_suffixes: vec!["rs".to_string()],
13265 ..Default::default()
13266 },
13267 ..Default::default()
13268 },
13269 Some(tree_sitter_rust::LANGUAGE.into()),
13270 )));
13271 let mut fake_servers = language_registry.register_fake_lsp(
13272 "Rust",
13273 FakeLspAdapter {
13274 name: language_server_name,
13275 initialization_options: Some(json!({
13276 "testOptionValue": true
13277 })),
13278 initializer: Some(Box::new(move |fake_server| {
13279 let task_restarts = Arc::clone(&closure_restarts);
13280 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
13281 task_restarts.fetch_add(1, atomic::Ordering::Release);
13282 futures::future::ready(Ok(()))
13283 });
13284 })),
13285 ..Default::default()
13286 },
13287 );
13288
13289 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13290 let _buffer = project
13291 .update(cx, |project, cx| {
13292 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
13293 })
13294 .await
13295 .unwrap();
13296 let _fake_server = fake_servers.next().await.unwrap();
13297 update_test_language_settings(cx, |language_settings| {
13298 language_settings.languages.insert(
13299 language_name.clone(),
13300 LanguageSettingsContent {
13301 tab_size: NonZeroU32::new(8),
13302 ..Default::default()
13303 },
13304 );
13305 });
13306 cx.executor().run_until_parked();
13307 assert_eq!(
13308 server_restarts.load(atomic::Ordering::Acquire),
13309 0,
13310 "Should not restart LSP server on an unrelated change"
13311 );
13312
13313 update_test_project_settings(cx, |project_settings| {
13314 project_settings.lsp.insert(
13315 "Some other server name".into(),
13316 LspSettings {
13317 binary: None,
13318 settings: None,
13319 initialization_options: Some(json!({
13320 "some other init value": false
13321 })),
13322 enable_lsp_tasks: false,
13323 },
13324 );
13325 });
13326 cx.executor().run_until_parked();
13327 assert_eq!(
13328 server_restarts.load(atomic::Ordering::Acquire),
13329 0,
13330 "Should not restart LSP server on an unrelated LSP settings change"
13331 );
13332
13333 update_test_project_settings(cx, |project_settings| {
13334 project_settings.lsp.insert(
13335 language_server_name.into(),
13336 LspSettings {
13337 binary: None,
13338 settings: None,
13339 initialization_options: Some(json!({
13340 "anotherInitValue": false
13341 })),
13342 enable_lsp_tasks: false,
13343 },
13344 );
13345 });
13346 cx.executor().run_until_parked();
13347 assert_eq!(
13348 server_restarts.load(atomic::Ordering::Acquire),
13349 1,
13350 "Should restart LSP server on a related LSP settings change"
13351 );
13352
13353 update_test_project_settings(cx, |project_settings| {
13354 project_settings.lsp.insert(
13355 language_server_name.into(),
13356 LspSettings {
13357 binary: None,
13358 settings: None,
13359 initialization_options: Some(json!({
13360 "anotherInitValue": false
13361 })),
13362 enable_lsp_tasks: false,
13363 },
13364 );
13365 });
13366 cx.executor().run_until_parked();
13367 assert_eq!(
13368 server_restarts.load(atomic::Ordering::Acquire),
13369 1,
13370 "Should not restart LSP server on a related LSP settings change that is the same"
13371 );
13372
13373 update_test_project_settings(cx, |project_settings| {
13374 project_settings.lsp.insert(
13375 language_server_name.into(),
13376 LspSettings {
13377 binary: None,
13378 settings: None,
13379 initialization_options: None,
13380 enable_lsp_tasks: false,
13381 },
13382 );
13383 });
13384 cx.executor().run_until_parked();
13385 assert_eq!(
13386 server_restarts.load(atomic::Ordering::Acquire),
13387 2,
13388 "Should restart LSP server on another related LSP settings change"
13389 );
13390}
13391
13392#[gpui::test]
13393async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
13394 init_test(cx, |_| {});
13395
13396 let mut cx = EditorLspTestContext::new_rust(
13397 lsp::ServerCapabilities {
13398 completion_provider: Some(lsp::CompletionOptions {
13399 trigger_characters: Some(vec![".".to_string()]),
13400 resolve_provider: Some(true),
13401 ..Default::default()
13402 }),
13403 ..Default::default()
13404 },
13405 cx,
13406 )
13407 .await;
13408
13409 cx.set_state("fn main() { let a = 2ˇ; }");
13410 cx.simulate_keystroke(".");
13411 let completion_item = lsp::CompletionItem {
13412 label: "some".into(),
13413 kind: Some(lsp::CompletionItemKind::SNIPPET),
13414 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13415 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13416 kind: lsp::MarkupKind::Markdown,
13417 value: "```rust\nSome(2)\n```".to_string(),
13418 })),
13419 deprecated: Some(false),
13420 sort_text: Some("fffffff2".to_string()),
13421 filter_text: Some("some".to_string()),
13422 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13423 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13424 range: lsp::Range {
13425 start: lsp::Position {
13426 line: 0,
13427 character: 22,
13428 },
13429 end: lsp::Position {
13430 line: 0,
13431 character: 22,
13432 },
13433 },
13434 new_text: "Some(2)".to_string(),
13435 })),
13436 additional_text_edits: Some(vec![lsp::TextEdit {
13437 range: lsp::Range {
13438 start: lsp::Position {
13439 line: 0,
13440 character: 20,
13441 },
13442 end: lsp::Position {
13443 line: 0,
13444 character: 22,
13445 },
13446 },
13447 new_text: "".to_string(),
13448 }]),
13449 ..Default::default()
13450 };
13451
13452 let closure_completion_item = completion_item.clone();
13453 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13454 let task_completion_item = closure_completion_item.clone();
13455 async move {
13456 Ok(Some(lsp::CompletionResponse::Array(vec![
13457 task_completion_item,
13458 ])))
13459 }
13460 });
13461
13462 request.next().await;
13463
13464 cx.condition(|editor, _| editor.context_menu_visible())
13465 .await;
13466 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13467 editor
13468 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13469 .unwrap()
13470 });
13471 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
13472
13473 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
13474 let task_completion_item = completion_item.clone();
13475 async move { Ok(task_completion_item) }
13476 })
13477 .next()
13478 .await
13479 .unwrap();
13480 apply_additional_edits.await.unwrap();
13481 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
13482}
13483
13484#[gpui::test]
13485async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
13486 init_test(cx, |_| {});
13487
13488 let mut cx = EditorLspTestContext::new_rust(
13489 lsp::ServerCapabilities {
13490 completion_provider: Some(lsp::CompletionOptions {
13491 trigger_characters: Some(vec![".".to_string()]),
13492 resolve_provider: Some(true),
13493 ..Default::default()
13494 }),
13495 ..Default::default()
13496 },
13497 cx,
13498 )
13499 .await;
13500
13501 cx.set_state("fn main() { let a = 2ˇ; }");
13502 cx.simulate_keystroke(".");
13503
13504 let item1 = lsp::CompletionItem {
13505 label: "method id()".to_string(),
13506 filter_text: Some("id".to_string()),
13507 detail: None,
13508 documentation: None,
13509 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13510 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13511 new_text: ".id".to_string(),
13512 })),
13513 ..lsp::CompletionItem::default()
13514 };
13515
13516 let item2 = lsp::CompletionItem {
13517 label: "other".to_string(),
13518 filter_text: Some("other".to_string()),
13519 detail: None,
13520 documentation: None,
13521 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13522 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13523 new_text: ".other".to_string(),
13524 })),
13525 ..lsp::CompletionItem::default()
13526 };
13527
13528 let item1 = item1.clone();
13529 cx.set_request_handler::<lsp::request::Completion, _, _>({
13530 let item1 = item1.clone();
13531 move |_, _, _| {
13532 let item1 = item1.clone();
13533 let item2 = item2.clone();
13534 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
13535 }
13536 })
13537 .next()
13538 .await;
13539
13540 cx.condition(|editor, _| editor.context_menu_visible())
13541 .await;
13542 cx.update_editor(|editor, _, _| {
13543 let context_menu = editor.context_menu.borrow_mut();
13544 let context_menu = context_menu
13545 .as_ref()
13546 .expect("Should have the context menu deployed");
13547 match context_menu {
13548 CodeContextMenu::Completions(completions_menu) => {
13549 let completions = completions_menu.completions.borrow_mut();
13550 assert_eq!(
13551 completions
13552 .iter()
13553 .map(|completion| &completion.label.text)
13554 .collect::<Vec<_>>(),
13555 vec!["method id()", "other"]
13556 )
13557 }
13558 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13559 }
13560 });
13561
13562 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
13563 let item1 = item1.clone();
13564 move |_, item_to_resolve, _| {
13565 let item1 = item1.clone();
13566 async move {
13567 if item1 == item_to_resolve {
13568 Ok(lsp::CompletionItem {
13569 label: "method id()".to_string(),
13570 filter_text: Some("id".to_string()),
13571 detail: Some("Now resolved!".to_string()),
13572 documentation: Some(lsp::Documentation::String("Docs".to_string())),
13573 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13574 range: lsp::Range::new(
13575 lsp::Position::new(0, 22),
13576 lsp::Position::new(0, 22),
13577 ),
13578 new_text: ".id".to_string(),
13579 })),
13580 ..lsp::CompletionItem::default()
13581 })
13582 } else {
13583 Ok(item_to_resolve)
13584 }
13585 }
13586 }
13587 })
13588 .next()
13589 .await
13590 .unwrap();
13591 cx.run_until_parked();
13592
13593 cx.update_editor(|editor, window, cx| {
13594 editor.context_menu_next(&Default::default(), window, cx);
13595 });
13596
13597 cx.update_editor(|editor, _, _| {
13598 let context_menu = editor.context_menu.borrow_mut();
13599 let context_menu = context_menu
13600 .as_ref()
13601 .expect("Should have the context menu deployed");
13602 match context_menu {
13603 CodeContextMenu::Completions(completions_menu) => {
13604 let completions = completions_menu.completions.borrow_mut();
13605 assert_eq!(
13606 completions
13607 .iter()
13608 .map(|completion| &completion.label.text)
13609 .collect::<Vec<_>>(),
13610 vec!["method id() Now resolved!", "other"],
13611 "Should update first completion label, but not second as the filter text did not match."
13612 );
13613 }
13614 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13615 }
13616 });
13617}
13618
13619#[gpui::test]
13620async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
13621 init_test(cx, |_| {});
13622
13623 let mut cx = EditorLspTestContext::new_rust(
13624 lsp::ServerCapabilities {
13625 completion_provider: Some(lsp::CompletionOptions {
13626 trigger_characters: Some(vec![".".to_string()]),
13627 resolve_provider: Some(true),
13628 ..Default::default()
13629 }),
13630 ..Default::default()
13631 },
13632 cx,
13633 )
13634 .await;
13635
13636 cx.set_state("fn main() { let a = 2ˇ; }");
13637 cx.simulate_keystroke(".");
13638
13639 let unresolved_item_1 = lsp::CompletionItem {
13640 label: "id".to_string(),
13641 filter_text: Some("id".to_string()),
13642 detail: None,
13643 documentation: None,
13644 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13645 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13646 new_text: ".id".to_string(),
13647 })),
13648 ..lsp::CompletionItem::default()
13649 };
13650 let resolved_item_1 = lsp::CompletionItem {
13651 additional_text_edits: Some(vec![lsp::TextEdit {
13652 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
13653 new_text: "!!".to_string(),
13654 }]),
13655 ..unresolved_item_1.clone()
13656 };
13657 let unresolved_item_2 = lsp::CompletionItem {
13658 label: "other".to_string(),
13659 filter_text: Some("other".to_string()),
13660 detail: None,
13661 documentation: None,
13662 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13663 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13664 new_text: ".other".to_string(),
13665 })),
13666 ..lsp::CompletionItem::default()
13667 };
13668 let resolved_item_2 = lsp::CompletionItem {
13669 additional_text_edits: Some(vec![lsp::TextEdit {
13670 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
13671 new_text: "??".to_string(),
13672 }]),
13673 ..unresolved_item_2.clone()
13674 };
13675
13676 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
13677 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
13678 cx.lsp
13679 .server
13680 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
13681 let unresolved_item_1 = unresolved_item_1.clone();
13682 let resolved_item_1 = resolved_item_1.clone();
13683 let unresolved_item_2 = unresolved_item_2.clone();
13684 let resolved_item_2 = resolved_item_2.clone();
13685 let resolve_requests_1 = resolve_requests_1.clone();
13686 let resolve_requests_2 = resolve_requests_2.clone();
13687 move |unresolved_request, _| {
13688 let unresolved_item_1 = unresolved_item_1.clone();
13689 let resolved_item_1 = resolved_item_1.clone();
13690 let unresolved_item_2 = unresolved_item_2.clone();
13691 let resolved_item_2 = resolved_item_2.clone();
13692 let resolve_requests_1 = resolve_requests_1.clone();
13693 let resolve_requests_2 = resolve_requests_2.clone();
13694 async move {
13695 if unresolved_request == unresolved_item_1 {
13696 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
13697 Ok(resolved_item_1.clone())
13698 } else if unresolved_request == unresolved_item_2 {
13699 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
13700 Ok(resolved_item_2.clone())
13701 } else {
13702 panic!("Unexpected completion item {unresolved_request:?}")
13703 }
13704 }
13705 }
13706 })
13707 .detach();
13708
13709 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13710 let unresolved_item_1 = unresolved_item_1.clone();
13711 let unresolved_item_2 = unresolved_item_2.clone();
13712 async move {
13713 Ok(Some(lsp::CompletionResponse::Array(vec![
13714 unresolved_item_1,
13715 unresolved_item_2,
13716 ])))
13717 }
13718 })
13719 .next()
13720 .await;
13721
13722 cx.condition(|editor, _| editor.context_menu_visible())
13723 .await;
13724 cx.update_editor(|editor, _, _| {
13725 let context_menu = editor.context_menu.borrow_mut();
13726 let context_menu = context_menu
13727 .as_ref()
13728 .expect("Should have the context menu deployed");
13729 match context_menu {
13730 CodeContextMenu::Completions(completions_menu) => {
13731 let completions = completions_menu.completions.borrow_mut();
13732 assert_eq!(
13733 completions
13734 .iter()
13735 .map(|completion| &completion.label.text)
13736 .collect::<Vec<_>>(),
13737 vec!["id", "other"]
13738 )
13739 }
13740 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13741 }
13742 });
13743 cx.run_until_parked();
13744
13745 cx.update_editor(|editor, window, cx| {
13746 editor.context_menu_next(&ContextMenuNext, window, cx);
13747 });
13748 cx.run_until_parked();
13749 cx.update_editor(|editor, window, cx| {
13750 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
13751 });
13752 cx.run_until_parked();
13753 cx.update_editor(|editor, window, cx| {
13754 editor.context_menu_next(&ContextMenuNext, window, cx);
13755 });
13756 cx.run_until_parked();
13757 cx.update_editor(|editor, window, cx| {
13758 editor
13759 .compose_completion(&ComposeCompletion::default(), window, cx)
13760 .expect("No task returned")
13761 })
13762 .await
13763 .expect("Completion failed");
13764 cx.run_until_parked();
13765
13766 cx.update_editor(|editor, _, cx| {
13767 assert_eq!(
13768 resolve_requests_1.load(atomic::Ordering::Acquire),
13769 1,
13770 "Should always resolve once despite multiple selections"
13771 );
13772 assert_eq!(
13773 resolve_requests_2.load(atomic::Ordering::Acquire),
13774 1,
13775 "Should always resolve once after multiple selections and applying the completion"
13776 );
13777 assert_eq!(
13778 editor.text(cx),
13779 "fn main() { let a = ??.other; }",
13780 "Should use resolved data when applying the completion"
13781 );
13782 });
13783}
13784
13785#[gpui::test]
13786async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
13787 init_test(cx, |_| {});
13788
13789 let item_0 = lsp::CompletionItem {
13790 label: "abs".into(),
13791 insert_text: Some("abs".into()),
13792 data: Some(json!({ "very": "special"})),
13793 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
13794 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
13795 lsp::InsertReplaceEdit {
13796 new_text: "abs".to_string(),
13797 insert: lsp::Range::default(),
13798 replace: lsp::Range::default(),
13799 },
13800 )),
13801 ..lsp::CompletionItem::default()
13802 };
13803 let items = iter::once(item_0.clone())
13804 .chain((11..51).map(|i| lsp::CompletionItem {
13805 label: format!("item_{}", i),
13806 insert_text: Some(format!("item_{}", i)),
13807 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
13808 ..lsp::CompletionItem::default()
13809 }))
13810 .collect::<Vec<_>>();
13811
13812 let default_commit_characters = vec!["?".to_string()];
13813 let default_data = json!({ "default": "data"});
13814 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
13815 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
13816 let default_edit_range = lsp::Range {
13817 start: lsp::Position {
13818 line: 0,
13819 character: 5,
13820 },
13821 end: lsp::Position {
13822 line: 0,
13823 character: 5,
13824 },
13825 };
13826
13827 let mut cx = EditorLspTestContext::new_rust(
13828 lsp::ServerCapabilities {
13829 completion_provider: Some(lsp::CompletionOptions {
13830 trigger_characters: Some(vec![".".to_string()]),
13831 resolve_provider: Some(true),
13832 ..Default::default()
13833 }),
13834 ..Default::default()
13835 },
13836 cx,
13837 )
13838 .await;
13839
13840 cx.set_state("fn main() { let a = 2ˇ; }");
13841 cx.simulate_keystroke(".");
13842
13843 let completion_data = default_data.clone();
13844 let completion_characters = default_commit_characters.clone();
13845 let completion_items = items.clone();
13846 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13847 let default_data = completion_data.clone();
13848 let default_commit_characters = completion_characters.clone();
13849 let items = completion_items.clone();
13850 async move {
13851 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13852 items,
13853 item_defaults: Some(lsp::CompletionListItemDefaults {
13854 data: Some(default_data.clone()),
13855 commit_characters: Some(default_commit_characters.clone()),
13856 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
13857 default_edit_range,
13858 )),
13859 insert_text_format: Some(default_insert_text_format),
13860 insert_text_mode: Some(default_insert_text_mode),
13861 }),
13862 ..lsp::CompletionList::default()
13863 })))
13864 }
13865 })
13866 .next()
13867 .await;
13868
13869 let resolved_items = Arc::new(Mutex::new(Vec::new()));
13870 cx.lsp
13871 .server
13872 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
13873 let closure_resolved_items = resolved_items.clone();
13874 move |item_to_resolve, _| {
13875 let closure_resolved_items = closure_resolved_items.clone();
13876 async move {
13877 closure_resolved_items.lock().push(item_to_resolve.clone());
13878 Ok(item_to_resolve)
13879 }
13880 }
13881 })
13882 .detach();
13883
13884 cx.condition(|editor, _| editor.context_menu_visible())
13885 .await;
13886 cx.run_until_parked();
13887 cx.update_editor(|editor, _, _| {
13888 let menu = editor.context_menu.borrow_mut();
13889 match menu.as_ref().expect("should have the completions menu") {
13890 CodeContextMenu::Completions(completions_menu) => {
13891 assert_eq!(
13892 completions_menu
13893 .entries
13894 .borrow()
13895 .iter()
13896 .map(|mat| mat.string.clone())
13897 .collect::<Vec<String>>(),
13898 items
13899 .iter()
13900 .map(|completion| completion.label.clone())
13901 .collect::<Vec<String>>()
13902 );
13903 }
13904 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
13905 }
13906 });
13907 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
13908 // with 4 from the end.
13909 assert_eq!(
13910 *resolved_items.lock(),
13911 [&items[0..16], &items[items.len() - 4..items.len()]]
13912 .concat()
13913 .iter()
13914 .cloned()
13915 .map(|mut item| {
13916 if item.data.is_none() {
13917 item.data = Some(default_data.clone());
13918 }
13919 item
13920 })
13921 .collect::<Vec<lsp::CompletionItem>>(),
13922 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
13923 );
13924 resolved_items.lock().clear();
13925
13926 cx.update_editor(|editor, window, cx| {
13927 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
13928 });
13929 cx.run_until_parked();
13930 // Completions that have already been resolved are skipped.
13931 assert_eq!(
13932 *resolved_items.lock(),
13933 items[items.len() - 16..items.len() - 4]
13934 .iter()
13935 .cloned()
13936 .map(|mut item| {
13937 if item.data.is_none() {
13938 item.data = Some(default_data.clone());
13939 }
13940 item
13941 })
13942 .collect::<Vec<lsp::CompletionItem>>()
13943 );
13944 resolved_items.lock().clear();
13945}
13946
13947#[gpui::test]
13948async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
13949 init_test(cx, |_| {});
13950
13951 let mut cx = EditorLspTestContext::new(
13952 Language::new(
13953 LanguageConfig {
13954 matcher: LanguageMatcher {
13955 path_suffixes: vec!["jsx".into()],
13956 ..Default::default()
13957 },
13958 overrides: [(
13959 "element".into(),
13960 LanguageConfigOverride {
13961 completion_query_characters: Override::Set(['-'].into_iter().collect()),
13962 ..Default::default()
13963 },
13964 )]
13965 .into_iter()
13966 .collect(),
13967 ..Default::default()
13968 },
13969 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
13970 )
13971 .with_override_query("(jsx_self_closing_element) @element")
13972 .unwrap(),
13973 lsp::ServerCapabilities {
13974 completion_provider: Some(lsp::CompletionOptions {
13975 trigger_characters: Some(vec![":".to_string()]),
13976 ..Default::default()
13977 }),
13978 ..Default::default()
13979 },
13980 cx,
13981 )
13982 .await;
13983
13984 cx.lsp
13985 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13986 Ok(Some(lsp::CompletionResponse::Array(vec![
13987 lsp::CompletionItem {
13988 label: "bg-blue".into(),
13989 ..Default::default()
13990 },
13991 lsp::CompletionItem {
13992 label: "bg-red".into(),
13993 ..Default::default()
13994 },
13995 lsp::CompletionItem {
13996 label: "bg-yellow".into(),
13997 ..Default::default()
13998 },
13999 ])))
14000 });
14001
14002 cx.set_state(r#"<p class="bgˇ" />"#);
14003
14004 // Trigger completion when typing a dash, because the dash is an extra
14005 // word character in the 'element' scope, which contains the cursor.
14006 cx.simulate_keystroke("-");
14007 cx.executor().run_until_parked();
14008 cx.update_editor(|editor, _, _| {
14009 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14010 {
14011 assert_eq!(
14012 completion_menu_entries(&menu),
14013 &["bg-red", "bg-blue", "bg-yellow"]
14014 );
14015 } else {
14016 panic!("expected completion menu to be open");
14017 }
14018 });
14019
14020 cx.simulate_keystroke("l");
14021 cx.executor().run_until_parked();
14022 cx.update_editor(|editor, _, _| {
14023 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14024 {
14025 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
14026 } else {
14027 panic!("expected completion menu to be open");
14028 }
14029 });
14030
14031 // When filtering completions, consider the character after the '-' to
14032 // be the start of a subword.
14033 cx.set_state(r#"<p class="yelˇ" />"#);
14034 cx.simulate_keystroke("l");
14035 cx.executor().run_until_parked();
14036 cx.update_editor(|editor, _, _| {
14037 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14038 {
14039 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
14040 } else {
14041 panic!("expected completion menu to be open");
14042 }
14043 });
14044}
14045
14046fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
14047 let entries = menu.entries.borrow();
14048 entries.iter().map(|mat| mat.string.clone()).collect()
14049}
14050
14051#[gpui::test]
14052async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
14053 init_test(cx, |settings| {
14054 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
14055 FormatterList(vec![Formatter::Prettier].into()),
14056 ))
14057 });
14058
14059 let fs = FakeFs::new(cx.executor());
14060 fs.insert_file(path!("/file.ts"), Default::default()).await;
14061
14062 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
14063 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14064
14065 language_registry.add(Arc::new(Language::new(
14066 LanguageConfig {
14067 name: "TypeScript".into(),
14068 matcher: LanguageMatcher {
14069 path_suffixes: vec!["ts".to_string()],
14070 ..Default::default()
14071 },
14072 ..Default::default()
14073 },
14074 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14075 )));
14076 update_test_language_settings(cx, |settings| {
14077 settings.defaults.prettier = Some(PrettierSettings {
14078 allowed: true,
14079 ..PrettierSettings::default()
14080 });
14081 });
14082
14083 let test_plugin = "test_plugin";
14084 let _ = language_registry.register_fake_lsp(
14085 "TypeScript",
14086 FakeLspAdapter {
14087 prettier_plugins: vec![test_plugin],
14088 ..Default::default()
14089 },
14090 );
14091
14092 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
14093 let buffer = project
14094 .update(cx, |project, cx| {
14095 project.open_local_buffer(path!("/file.ts"), cx)
14096 })
14097 .await
14098 .unwrap();
14099
14100 let buffer_text = "one\ntwo\nthree\n";
14101 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14102 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14103 editor.update_in(cx, |editor, window, cx| {
14104 editor.set_text(buffer_text, window, cx)
14105 });
14106
14107 editor
14108 .update_in(cx, |editor, window, cx| {
14109 editor.perform_format(
14110 project.clone(),
14111 FormatTrigger::Manual,
14112 FormatTarget::Buffers,
14113 window,
14114 cx,
14115 )
14116 })
14117 .unwrap()
14118 .await;
14119 assert_eq!(
14120 editor.update(cx, |editor, cx| editor.text(cx)),
14121 buffer_text.to_string() + prettier_format_suffix,
14122 "Test prettier formatting was not applied to the original buffer text",
14123 );
14124
14125 update_test_language_settings(cx, |settings| {
14126 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
14127 });
14128 let format = editor.update_in(cx, |editor, window, cx| {
14129 editor.perform_format(
14130 project.clone(),
14131 FormatTrigger::Manual,
14132 FormatTarget::Buffers,
14133 window,
14134 cx,
14135 )
14136 });
14137 format.await.unwrap();
14138 assert_eq!(
14139 editor.update(cx, |editor, cx| editor.text(cx)),
14140 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
14141 "Autoformatting (via test prettier) was not applied to the original buffer text",
14142 );
14143}
14144
14145#[gpui::test]
14146async fn test_addition_reverts(cx: &mut TestAppContext) {
14147 init_test(cx, |_| {});
14148 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14149 let base_text = indoc! {r#"
14150 struct Row;
14151 struct Row1;
14152 struct Row2;
14153
14154 struct Row4;
14155 struct Row5;
14156 struct Row6;
14157
14158 struct Row8;
14159 struct Row9;
14160 struct Row10;"#};
14161
14162 // When addition hunks are not adjacent to carets, no hunk revert is performed
14163 assert_hunk_revert(
14164 indoc! {r#"struct Row;
14165 struct Row1;
14166 struct Row1.1;
14167 struct Row1.2;
14168 struct Row2;ˇ
14169
14170 struct Row4;
14171 struct Row5;
14172 struct Row6;
14173
14174 struct Row8;
14175 ˇstruct Row9;
14176 struct Row9.1;
14177 struct Row9.2;
14178 struct Row9.3;
14179 struct Row10;"#},
14180 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
14181 indoc! {r#"struct Row;
14182 struct Row1;
14183 struct Row1.1;
14184 struct Row1.2;
14185 struct Row2;ˇ
14186
14187 struct Row4;
14188 struct Row5;
14189 struct Row6;
14190
14191 struct Row8;
14192 ˇstruct Row9;
14193 struct Row9.1;
14194 struct Row9.2;
14195 struct Row9.3;
14196 struct Row10;"#},
14197 base_text,
14198 &mut cx,
14199 );
14200 // Same for selections
14201 assert_hunk_revert(
14202 indoc! {r#"struct Row;
14203 struct Row1;
14204 struct Row2;
14205 struct Row2.1;
14206 struct Row2.2;
14207 «ˇ
14208 struct Row4;
14209 struct» Row5;
14210 «struct Row6;
14211 ˇ»
14212 struct Row9.1;
14213 struct Row9.2;
14214 struct Row9.3;
14215 struct Row8;
14216 struct Row9;
14217 struct Row10;"#},
14218 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
14219 indoc! {r#"struct Row;
14220 struct Row1;
14221 struct Row2;
14222 struct Row2.1;
14223 struct Row2.2;
14224 «ˇ
14225 struct Row4;
14226 struct» Row5;
14227 «struct Row6;
14228 ˇ»
14229 struct Row9.1;
14230 struct Row9.2;
14231 struct Row9.3;
14232 struct Row8;
14233 struct Row9;
14234 struct Row10;"#},
14235 base_text,
14236 &mut cx,
14237 );
14238
14239 // When carets and selections intersect the addition hunks, those are reverted.
14240 // Adjacent carets got merged.
14241 assert_hunk_revert(
14242 indoc! {r#"struct Row;
14243 ˇ// something on the top
14244 struct Row1;
14245 struct Row2;
14246 struct Roˇw3.1;
14247 struct Row2.2;
14248 struct Row2.3;ˇ
14249
14250 struct Row4;
14251 struct ˇRow5.1;
14252 struct Row5.2;
14253 struct «Rowˇ»5.3;
14254 struct Row5;
14255 struct Row6;
14256 ˇ
14257 struct Row9.1;
14258 struct «Rowˇ»9.2;
14259 struct «ˇRow»9.3;
14260 struct Row8;
14261 struct Row9;
14262 «ˇ// something on bottom»
14263 struct Row10;"#},
14264 vec![
14265 DiffHunkStatusKind::Added,
14266 DiffHunkStatusKind::Added,
14267 DiffHunkStatusKind::Added,
14268 DiffHunkStatusKind::Added,
14269 DiffHunkStatusKind::Added,
14270 ],
14271 indoc! {r#"struct Row;
14272 ˇstruct Row1;
14273 struct Row2;
14274 ˇ
14275 struct Row4;
14276 ˇstruct Row5;
14277 struct Row6;
14278 ˇ
14279 ˇstruct Row8;
14280 struct Row9;
14281 ˇstruct Row10;"#},
14282 base_text,
14283 &mut cx,
14284 );
14285}
14286
14287#[gpui::test]
14288async fn test_modification_reverts(cx: &mut TestAppContext) {
14289 init_test(cx, |_| {});
14290 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14291 let base_text = indoc! {r#"
14292 struct Row;
14293 struct Row1;
14294 struct Row2;
14295
14296 struct Row4;
14297 struct Row5;
14298 struct Row6;
14299
14300 struct Row8;
14301 struct Row9;
14302 struct Row10;"#};
14303
14304 // Modification hunks behave the same as the addition ones.
14305 assert_hunk_revert(
14306 indoc! {r#"struct Row;
14307 struct Row1;
14308 struct Row33;
14309 ˇ
14310 struct Row4;
14311 struct Row5;
14312 struct Row6;
14313 ˇ
14314 struct Row99;
14315 struct Row9;
14316 struct Row10;"#},
14317 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
14318 indoc! {r#"struct Row;
14319 struct Row1;
14320 struct Row33;
14321 ˇ
14322 struct Row4;
14323 struct Row5;
14324 struct Row6;
14325 ˇ
14326 struct Row99;
14327 struct Row9;
14328 struct Row10;"#},
14329 base_text,
14330 &mut cx,
14331 );
14332 assert_hunk_revert(
14333 indoc! {r#"struct Row;
14334 struct Row1;
14335 struct Row33;
14336 «ˇ
14337 struct Row4;
14338 struct» Row5;
14339 «struct Row6;
14340 ˇ»
14341 struct Row99;
14342 struct Row9;
14343 struct Row10;"#},
14344 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
14345 indoc! {r#"struct Row;
14346 struct Row1;
14347 struct Row33;
14348 «ˇ
14349 struct Row4;
14350 struct» Row5;
14351 «struct Row6;
14352 ˇ»
14353 struct Row99;
14354 struct Row9;
14355 struct Row10;"#},
14356 base_text,
14357 &mut cx,
14358 );
14359
14360 assert_hunk_revert(
14361 indoc! {r#"ˇstruct Row1.1;
14362 struct Row1;
14363 «ˇstr»uct Row22;
14364
14365 struct ˇRow44;
14366 struct Row5;
14367 struct «Rˇ»ow66;ˇ
14368
14369 «struˇ»ct Row88;
14370 struct Row9;
14371 struct Row1011;ˇ"#},
14372 vec![
14373 DiffHunkStatusKind::Modified,
14374 DiffHunkStatusKind::Modified,
14375 DiffHunkStatusKind::Modified,
14376 DiffHunkStatusKind::Modified,
14377 DiffHunkStatusKind::Modified,
14378 DiffHunkStatusKind::Modified,
14379 ],
14380 indoc! {r#"struct Row;
14381 ˇstruct Row1;
14382 struct Row2;
14383 ˇ
14384 struct Row4;
14385 ˇstruct Row5;
14386 struct Row6;
14387 ˇ
14388 struct Row8;
14389 ˇstruct Row9;
14390 struct Row10;ˇ"#},
14391 base_text,
14392 &mut cx,
14393 );
14394}
14395
14396#[gpui::test]
14397async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
14398 init_test(cx, |_| {});
14399 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14400 let base_text = indoc! {r#"
14401 one
14402
14403 two
14404 three
14405 "#};
14406
14407 cx.set_head_text(base_text);
14408 cx.set_state("\nˇ\n");
14409 cx.executor().run_until_parked();
14410 cx.update_editor(|editor, _window, cx| {
14411 editor.expand_selected_diff_hunks(cx);
14412 });
14413 cx.executor().run_until_parked();
14414 cx.update_editor(|editor, window, cx| {
14415 editor.backspace(&Default::default(), window, cx);
14416 });
14417 cx.run_until_parked();
14418 cx.assert_state_with_diff(
14419 indoc! {r#"
14420
14421 - two
14422 - threeˇ
14423 +
14424 "#}
14425 .to_string(),
14426 );
14427}
14428
14429#[gpui::test]
14430async fn test_deletion_reverts(cx: &mut TestAppContext) {
14431 init_test(cx, |_| {});
14432 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14433 let base_text = indoc! {r#"struct Row;
14434struct Row1;
14435struct Row2;
14436
14437struct Row4;
14438struct Row5;
14439struct Row6;
14440
14441struct Row8;
14442struct Row9;
14443struct Row10;"#};
14444
14445 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
14446 assert_hunk_revert(
14447 indoc! {r#"struct Row;
14448 struct Row2;
14449
14450 ˇstruct Row4;
14451 struct Row5;
14452 struct Row6;
14453 ˇ
14454 struct Row8;
14455 struct Row10;"#},
14456 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14457 indoc! {r#"struct Row;
14458 struct Row2;
14459
14460 ˇstruct Row4;
14461 struct Row5;
14462 struct Row6;
14463 ˇ
14464 struct Row8;
14465 struct Row10;"#},
14466 base_text,
14467 &mut cx,
14468 );
14469 assert_hunk_revert(
14470 indoc! {r#"struct Row;
14471 struct Row2;
14472
14473 «ˇstruct Row4;
14474 struct» Row5;
14475 «struct Row6;
14476 ˇ»
14477 struct Row8;
14478 struct Row10;"#},
14479 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14480 indoc! {r#"struct Row;
14481 struct Row2;
14482
14483 «ˇstruct Row4;
14484 struct» Row5;
14485 «struct Row6;
14486 ˇ»
14487 struct Row8;
14488 struct Row10;"#},
14489 base_text,
14490 &mut cx,
14491 );
14492
14493 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
14494 assert_hunk_revert(
14495 indoc! {r#"struct Row;
14496 ˇstruct Row2;
14497
14498 struct Row4;
14499 struct Row5;
14500 struct Row6;
14501
14502 struct Row8;ˇ
14503 struct Row10;"#},
14504 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14505 indoc! {r#"struct Row;
14506 struct Row1;
14507 ˇstruct Row2;
14508
14509 struct Row4;
14510 struct Row5;
14511 struct Row6;
14512
14513 struct Row8;ˇ
14514 struct Row9;
14515 struct Row10;"#},
14516 base_text,
14517 &mut cx,
14518 );
14519 assert_hunk_revert(
14520 indoc! {r#"struct Row;
14521 struct Row2«ˇ;
14522 struct Row4;
14523 struct» Row5;
14524 «struct Row6;
14525
14526 struct Row8;ˇ»
14527 struct Row10;"#},
14528 vec![
14529 DiffHunkStatusKind::Deleted,
14530 DiffHunkStatusKind::Deleted,
14531 DiffHunkStatusKind::Deleted,
14532 ],
14533 indoc! {r#"struct Row;
14534 struct Row1;
14535 struct Row2«ˇ;
14536
14537 struct Row4;
14538 struct» Row5;
14539 «struct Row6;
14540
14541 struct Row8;ˇ»
14542 struct Row9;
14543 struct Row10;"#},
14544 base_text,
14545 &mut cx,
14546 );
14547}
14548
14549#[gpui::test]
14550async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
14551 init_test(cx, |_| {});
14552
14553 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
14554 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
14555 let base_text_3 =
14556 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
14557
14558 let text_1 = edit_first_char_of_every_line(base_text_1);
14559 let text_2 = edit_first_char_of_every_line(base_text_2);
14560 let text_3 = edit_first_char_of_every_line(base_text_3);
14561
14562 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
14563 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
14564 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
14565
14566 let multibuffer = cx.new(|cx| {
14567 let mut multibuffer = MultiBuffer::new(ReadWrite);
14568 multibuffer.push_excerpts(
14569 buffer_1.clone(),
14570 [
14571 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14572 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14573 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14574 ],
14575 cx,
14576 );
14577 multibuffer.push_excerpts(
14578 buffer_2.clone(),
14579 [
14580 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14581 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14582 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14583 ],
14584 cx,
14585 );
14586 multibuffer.push_excerpts(
14587 buffer_3.clone(),
14588 [
14589 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14590 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14591 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14592 ],
14593 cx,
14594 );
14595 multibuffer
14596 });
14597
14598 let fs = FakeFs::new(cx.executor());
14599 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
14600 let (editor, cx) = cx
14601 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
14602 editor.update_in(cx, |editor, _window, cx| {
14603 for (buffer, diff_base) in [
14604 (buffer_1.clone(), base_text_1),
14605 (buffer_2.clone(), base_text_2),
14606 (buffer_3.clone(), base_text_3),
14607 ] {
14608 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
14609 editor
14610 .buffer
14611 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
14612 }
14613 });
14614 cx.executor().run_until_parked();
14615
14616 editor.update_in(cx, |editor, window, cx| {
14617 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}");
14618 editor.select_all(&SelectAll, window, cx);
14619 editor.git_restore(&Default::default(), window, cx);
14620 });
14621 cx.executor().run_until_parked();
14622
14623 // When all ranges are selected, all buffer hunks are reverted.
14624 editor.update(cx, |editor, cx| {
14625 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");
14626 });
14627 buffer_1.update(cx, |buffer, _| {
14628 assert_eq!(buffer.text(), base_text_1);
14629 });
14630 buffer_2.update(cx, |buffer, _| {
14631 assert_eq!(buffer.text(), base_text_2);
14632 });
14633 buffer_3.update(cx, |buffer, _| {
14634 assert_eq!(buffer.text(), base_text_3);
14635 });
14636
14637 editor.update_in(cx, |editor, window, cx| {
14638 editor.undo(&Default::default(), window, cx);
14639 });
14640
14641 editor.update_in(cx, |editor, window, cx| {
14642 editor.change_selections(None, window, cx, |s| {
14643 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
14644 });
14645 editor.git_restore(&Default::default(), window, cx);
14646 });
14647
14648 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
14649 // but not affect buffer_2 and its related excerpts.
14650 editor.update(cx, |editor, cx| {
14651 assert_eq!(
14652 editor.text(cx),
14653 "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}"
14654 );
14655 });
14656 buffer_1.update(cx, |buffer, _| {
14657 assert_eq!(buffer.text(), base_text_1);
14658 });
14659 buffer_2.update(cx, |buffer, _| {
14660 assert_eq!(
14661 buffer.text(),
14662 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
14663 );
14664 });
14665 buffer_3.update(cx, |buffer, _| {
14666 assert_eq!(
14667 buffer.text(),
14668 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
14669 );
14670 });
14671
14672 fn edit_first_char_of_every_line(text: &str) -> String {
14673 text.split('\n')
14674 .map(|line| format!("X{}", &line[1..]))
14675 .collect::<Vec<_>>()
14676 .join("\n")
14677 }
14678}
14679
14680#[gpui::test]
14681async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
14682 init_test(cx, |_| {});
14683
14684 let cols = 4;
14685 let rows = 10;
14686 let sample_text_1 = sample_text(rows, cols, 'a');
14687 assert_eq!(
14688 sample_text_1,
14689 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
14690 );
14691 let sample_text_2 = sample_text(rows, cols, 'l');
14692 assert_eq!(
14693 sample_text_2,
14694 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
14695 );
14696 let sample_text_3 = sample_text(rows, cols, 'v');
14697 assert_eq!(
14698 sample_text_3,
14699 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
14700 );
14701
14702 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
14703 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
14704 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
14705
14706 let multi_buffer = cx.new(|cx| {
14707 let mut multibuffer = MultiBuffer::new(ReadWrite);
14708 multibuffer.push_excerpts(
14709 buffer_1.clone(),
14710 [
14711 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14712 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14713 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14714 ],
14715 cx,
14716 );
14717 multibuffer.push_excerpts(
14718 buffer_2.clone(),
14719 [
14720 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14721 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14722 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14723 ],
14724 cx,
14725 );
14726 multibuffer.push_excerpts(
14727 buffer_3.clone(),
14728 [
14729 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14730 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14731 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14732 ],
14733 cx,
14734 );
14735 multibuffer
14736 });
14737
14738 let fs = FakeFs::new(cx.executor());
14739 fs.insert_tree(
14740 "/a",
14741 json!({
14742 "main.rs": sample_text_1,
14743 "other.rs": sample_text_2,
14744 "lib.rs": sample_text_3,
14745 }),
14746 )
14747 .await;
14748 let project = Project::test(fs, ["/a".as_ref()], cx).await;
14749 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14750 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14751 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
14752 Editor::new(
14753 EditorMode::full(),
14754 multi_buffer,
14755 Some(project.clone()),
14756 window,
14757 cx,
14758 )
14759 });
14760 let multibuffer_item_id = workspace
14761 .update(cx, |workspace, window, cx| {
14762 assert!(
14763 workspace.active_item(cx).is_none(),
14764 "active item should be None before the first item is added"
14765 );
14766 workspace.add_item_to_active_pane(
14767 Box::new(multi_buffer_editor.clone()),
14768 None,
14769 true,
14770 window,
14771 cx,
14772 );
14773 let active_item = workspace
14774 .active_item(cx)
14775 .expect("should have an active item after adding the multi buffer");
14776 assert!(
14777 !active_item.is_singleton(cx),
14778 "A multi buffer was expected to active after adding"
14779 );
14780 active_item.item_id()
14781 })
14782 .unwrap();
14783 cx.executor().run_until_parked();
14784
14785 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14786 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14787 s.select_ranges(Some(1..2))
14788 });
14789 editor.open_excerpts(&OpenExcerpts, window, cx);
14790 });
14791 cx.executor().run_until_parked();
14792 let first_item_id = workspace
14793 .update(cx, |workspace, window, cx| {
14794 let active_item = workspace
14795 .active_item(cx)
14796 .expect("should have an active item after navigating into the 1st buffer");
14797 let first_item_id = active_item.item_id();
14798 assert_ne!(
14799 first_item_id, multibuffer_item_id,
14800 "Should navigate into the 1st buffer and activate it"
14801 );
14802 assert!(
14803 active_item.is_singleton(cx),
14804 "New active item should be a singleton buffer"
14805 );
14806 assert_eq!(
14807 active_item
14808 .act_as::<Editor>(cx)
14809 .expect("should have navigated into an editor for the 1st buffer")
14810 .read(cx)
14811 .text(cx),
14812 sample_text_1
14813 );
14814
14815 workspace
14816 .go_back(workspace.active_pane().downgrade(), window, cx)
14817 .detach_and_log_err(cx);
14818
14819 first_item_id
14820 })
14821 .unwrap();
14822 cx.executor().run_until_parked();
14823 workspace
14824 .update(cx, |workspace, _, cx| {
14825 let active_item = workspace
14826 .active_item(cx)
14827 .expect("should have an active item after navigating back");
14828 assert_eq!(
14829 active_item.item_id(),
14830 multibuffer_item_id,
14831 "Should navigate back to the multi buffer"
14832 );
14833 assert!(!active_item.is_singleton(cx));
14834 })
14835 .unwrap();
14836
14837 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14838 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14839 s.select_ranges(Some(39..40))
14840 });
14841 editor.open_excerpts(&OpenExcerpts, window, cx);
14842 });
14843 cx.executor().run_until_parked();
14844 let second_item_id = workspace
14845 .update(cx, |workspace, window, cx| {
14846 let active_item = workspace
14847 .active_item(cx)
14848 .expect("should have an active item after navigating into the 2nd buffer");
14849 let second_item_id = active_item.item_id();
14850 assert_ne!(
14851 second_item_id, multibuffer_item_id,
14852 "Should navigate away from the multibuffer"
14853 );
14854 assert_ne!(
14855 second_item_id, first_item_id,
14856 "Should navigate into the 2nd buffer and activate it"
14857 );
14858 assert!(
14859 active_item.is_singleton(cx),
14860 "New active item should be a singleton buffer"
14861 );
14862 assert_eq!(
14863 active_item
14864 .act_as::<Editor>(cx)
14865 .expect("should have navigated into an editor")
14866 .read(cx)
14867 .text(cx),
14868 sample_text_2
14869 );
14870
14871 workspace
14872 .go_back(workspace.active_pane().downgrade(), window, cx)
14873 .detach_and_log_err(cx);
14874
14875 second_item_id
14876 })
14877 .unwrap();
14878 cx.executor().run_until_parked();
14879 workspace
14880 .update(cx, |workspace, _, cx| {
14881 let active_item = workspace
14882 .active_item(cx)
14883 .expect("should have an active item after navigating back from the 2nd buffer");
14884 assert_eq!(
14885 active_item.item_id(),
14886 multibuffer_item_id,
14887 "Should navigate back from the 2nd buffer to the multi buffer"
14888 );
14889 assert!(!active_item.is_singleton(cx));
14890 })
14891 .unwrap();
14892
14893 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14894 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14895 s.select_ranges(Some(70..70))
14896 });
14897 editor.open_excerpts(&OpenExcerpts, window, cx);
14898 });
14899 cx.executor().run_until_parked();
14900 workspace
14901 .update(cx, |workspace, window, cx| {
14902 let active_item = workspace
14903 .active_item(cx)
14904 .expect("should have an active item after navigating into the 3rd buffer");
14905 let third_item_id = active_item.item_id();
14906 assert_ne!(
14907 third_item_id, multibuffer_item_id,
14908 "Should navigate into the 3rd buffer and activate it"
14909 );
14910 assert_ne!(third_item_id, first_item_id);
14911 assert_ne!(third_item_id, second_item_id);
14912 assert!(
14913 active_item.is_singleton(cx),
14914 "New active item should be a singleton buffer"
14915 );
14916 assert_eq!(
14917 active_item
14918 .act_as::<Editor>(cx)
14919 .expect("should have navigated into an editor")
14920 .read(cx)
14921 .text(cx),
14922 sample_text_3
14923 );
14924
14925 workspace
14926 .go_back(workspace.active_pane().downgrade(), window, cx)
14927 .detach_and_log_err(cx);
14928 })
14929 .unwrap();
14930 cx.executor().run_until_parked();
14931 workspace
14932 .update(cx, |workspace, _, cx| {
14933 let active_item = workspace
14934 .active_item(cx)
14935 .expect("should have an active item after navigating back from the 3rd buffer");
14936 assert_eq!(
14937 active_item.item_id(),
14938 multibuffer_item_id,
14939 "Should navigate back from the 3rd buffer to the multi buffer"
14940 );
14941 assert!(!active_item.is_singleton(cx));
14942 })
14943 .unwrap();
14944}
14945
14946#[gpui::test]
14947async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14948 init_test(cx, |_| {});
14949
14950 let mut cx = EditorTestContext::new(cx).await;
14951
14952 let diff_base = r#"
14953 use some::mod;
14954
14955 const A: u32 = 42;
14956
14957 fn main() {
14958 println!("hello");
14959
14960 println!("world");
14961 }
14962 "#
14963 .unindent();
14964
14965 cx.set_state(
14966 &r#"
14967 use some::modified;
14968
14969 ˇ
14970 fn main() {
14971 println!("hello there");
14972
14973 println!("around the");
14974 println!("world");
14975 }
14976 "#
14977 .unindent(),
14978 );
14979
14980 cx.set_head_text(&diff_base);
14981 executor.run_until_parked();
14982
14983 cx.update_editor(|editor, window, cx| {
14984 editor.go_to_next_hunk(&GoToHunk, window, cx);
14985 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14986 });
14987 executor.run_until_parked();
14988 cx.assert_state_with_diff(
14989 r#"
14990 use some::modified;
14991
14992
14993 fn main() {
14994 - println!("hello");
14995 + ˇ println!("hello there");
14996
14997 println!("around the");
14998 println!("world");
14999 }
15000 "#
15001 .unindent(),
15002 );
15003
15004 cx.update_editor(|editor, window, cx| {
15005 for _ in 0..2 {
15006 editor.go_to_next_hunk(&GoToHunk, window, cx);
15007 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15008 }
15009 });
15010 executor.run_until_parked();
15011 cx.assert_state_with_diff(
15012 r#"
15013 - use some::mod;
15014 + ˇuse some::modified;
15015
15016
15017 fn main() {
15018 - println!("hello");
15019 + println!("hello there");
15020
15021 + println!("around the");
15022 println!("world");
15023 }
15024 "#
15025 .unindent(),
15026 );
15027
15028 cx.update_editor(|editor, window, cx| {
15029 editor.go_to_next_hunk(&GoToHunk, window, cx);
15030 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15031 });
15032 executor.run_until_parked();
15033 cx.assert_state_with_diff(
15034 r#"
15035 - use some::mod;
15036 + use some::modified;
15037
15038 - const A: u32 = 42;
15039 ˇ
15040 fn main() {
15041 - println!("hello");
15042 + println!("hello there");
15043
15044 + println!("around the");
15045 println!("world");
15046 }
15047 "#
15048 .unindent(),
15049 );
15050
15051 cx.update_editor(|editor, window, cx| {
15052 editor.cancel(&Cancel, window, cx);
15053 });
15054
15055 cx.assert_state_with_diff(
15056 r#"
15057 use some::modified;
15058
15059 ˇ
15060 fn main() {
15061 println!("hello there");
15062
15063 println!("around the");
15064 println!("world");
15065 }
15066 "#
15067 .unindent(),
15068 );
15069}
15070
15071#[gpui::test]
15072async fn test_diff_base_change_with_expanded_diff_hunks(
15073 executor: BackgroundExecutor,
15074 cx: &mut TestAppContext,
15075) {
15076 init_test(cx, |_| {});
15077
15078 let mut cx = EditorTestContext::new(cx).await;
15079
15080 let diff_base = r#"
15081 use some::mod1;
15082 use some::mod2;
15083
15084 const A: u32 = 42;
15085 const B: u32 = 42;
15086 const C: u32 = 42;
15087
15088 fn main() {
15089 println!("hello");
15090
15091 println!("world");
15092 }
15093 "#
15094 .unindent();
15095
15096 cx.set_state(
15097 &r#"
15098 use some::mod2;
15099
15100 const A: u32 = 42;
15101 const C: u32 = 42;
15102
15103 fn main(ˇ) {
15104 //println!("hello");
15105
15106 println!("world");
15107 //
15108 //
15109 }
15110 "#
15111 .unindent(),
15112 );
15113
15114 cx.set_head_text(&diff_base);
15115 executor.run_until_parked();
15116
15117 cx.update_editor(|editor, window, cx| {
15118 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15119 });
15120 executor.run_until_parked();
15121 cx.assert_state_with_diff(
15122 r#"
15123 - use some::mod1;
15124 use some::mod2;
15125
15126 const A: u32 = 42;
15127 - const B: u32 = 42;
15128 const C: u32 = 42;
15129
15130 fn main(ˇ) {
15131 - println!("hello");
15132 + //println!("hello");
15133
15134 println!("world");
15135 + //
15136 + //
15137 }
15138 "#
15139 .unindent(),
15140 );
15141
15142 cx.set_head_text("new diff base!");
15143 executor.run_until_parked();
15144 cx.assert_state_with_diff(
15145 r#"
15146 - new diff base!
15147 + use some::mod2;
15148 +
15149 + const A: u32 = 42;
15150 + const C: u32 = 42;
15151 +
15152 + fn main(ˇ) {
15153 + //println!("hello");
15154 +
15155 + println!("world");
15156 + //
15157 + //
15158 + }
15159 "#
15160 .unindent(),
15161 );
15162}
15163
15164#[gpui::test]
15165async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
15166 init_test(cx, |_| {});
15167
15168 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
15169 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
15170 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
15171 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
15172 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
15173 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
15174
15175 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
15176 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
15177 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
15178
15179 let multi_buffer = cx.new(|cx| {
15180 let mut multibuffer = MultiBuffer::new(ReadWrite);
15181 multibuffer.push_excerpts(
15182 buffer_1.clone(),
15183 [
15184 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15185 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15186 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15187 ],
15188 cx,
15189 );
15190 multibuffer.push_excerpts(
15191 buffer_2.clone(),
15192 [
15193 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15194 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15195 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15196 ],
15197 cx,
15198 );
15199 multibuffer.push_excerpts(
15200 buffer_3.clone(),
15201 [
15202 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15203 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15204 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15205 ],
15206 cx,
15207 );
15208 multibuffer
15209 });
15210
15211 let editor =
15212 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
15213 editor
15214 .update(cx, |editor, _window, cx| {
15215 for (buffer, diff_base) in [
15216 (buffer_1.clone(), file_1_old),
15217 (buffer_2.clone(), file_2_old),
15218 (buffer_3.clone(), file_3_old),
15219 ] {
15220 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
15221 editor
15222 .buffer
15223 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
15224 }
15225 })
15226 .unwrap();
15227
15228 let mut cx = EditorTestContext::for_editor(editor, cx).await;
15229 cx.run_until_parked();
15230
15231 cx.assert_editor_state(
15232 &"
15233 ˇaaa
15234 ccc
15235 ddd
15236
15237 ggg
15238 hhh
15239
15240
15241 lll
15242 mmm
15243 NNN
15244
15245 qqq
15246 rrr
15247
15248 uuu
15249 111
15250 222
15251 333
15252
15253 666
15254 777
15255
15256 000
15257 !!!"
15258 .unindent(),
15259 );
15260
15261 cx.update_editor(|editor, window, cx| {
15262 editor.select_all(&SelectAll, window, cx);
15263 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15264 });
15265 cx.executor().run_until_parked();
15266
15267 cx.assert_state_with_diff(
15268 "
15269 «aaa
15270 - bbb
15271 ccc
15272 ddd
15273
15274 ggg
15275 hhh
15276
15277
15278 lll
15279 mmm
15280 - nnn
15281 + NNN
15282
15283 qqq
15284 rrr
15285
15286 uuu
15287 111
15288 222
15289 333
15290
15291 + 666
15292 777
15293
15294 000
15295 !!!ˇ»"
15296 .unindent(),
15297 );
15298}
15299
15300#[gpui::test]
15301async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
15302 init_test(cx, |_| {});
15303
15304 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
15305 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
15306
15307 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
15308 let multi_buffer = cx.new(|cx| {
15309 let mut multibuffer = MultiBuffer::new(ReadWrite);
15310 multibuffer.push_excerpts(
15311 buffer.clone(),
15312 [
15313 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
15314 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
15315 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
15316 ],
15317 cx,
15318 );
15319 multibuffer
15320 });
15321
15322 let editor =
15323 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
15324 editor
15325 .update(cx, |editor, _window, cx| {
15326 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
15327 editor
15328 .buffer
15329 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
15330 })
15331 .unwrap();
15332
15333 let mut cx = EditorTestContext::for_editor(editor, cx).await;
15334 cx.run_until_parked();
15335
15336 cx.update_editor(|editor, window, cx| {
15337 editor.expand_all_diff_hunks(&Default::default(), window, cx)
15338 });
15339 cx.executor().run_until_parked();
15340
15341 // When the start of a hunk coincides with the start of its excerpt,
15342 // the hunk is expanded. When the start of a a hunk is earlier than
15343 // the start of its excerpt, the hunk is not expanded.
15344 cx.assert_state_with_diff(
15345 "
15346 ˇaaa
15347 - bbb
15348 + BBB
15349
15350 - ddd
15351 - eee
15352 + DDD
15353 + EEE
15354 fff
15355
15356 iii
15357 "
15358 .unindent(),
15359 );
15360}
15361
15362#[gpui::test]
15363async fn test_edits_around_expanded_insertion_hunks(
15364 executor: BackgroundExecutor,
15365 cx: &mut TestAppContext,
15366) {
15367 init_test(cx, |_| {});
15368
15369 let mut cx = EditorTestContext::new(cx).await;
15370
15371 let diff_base = r#"
15372 use some::mod1;
15373 use some::mod2;
15374
15375 const A: u32 = 42;
15376
15377 fn main() {
15378 println!("hello");
15379
15380 println!("world");
15381 }
15382 "#
15383 .unindent();
15384 executor.run_until_parked();
15385 cx.set_state(
15386 &r#"
15387 use some::mod1;
15388 use some::mod2;
15389
15390 const A: u32 = 42;
15391 const B: u32 = 42;
15392 const C: u32 = 42;
15393 ˇ
15394
15395 fn main() {
15396 println!("hello");
15397
15398 println!("world");
15399 }
15400 "#
15401 .unindent(),
15402 );
15403
15404 cx.set_head_text(&diff_base);
15405 executor.run_until_parked();
15406
15407 cx.update_editor(|editor, window, cx| {
15408 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15409 });
15410 executor.run_until_parked();
15411
15412 cx.assert_state_with_diff(
15413 r#"
15414 use some::mod1;
15415 use some::mod2;
15416
15417 const A: u32 = 42;
15418 + const B: u32 = 42;
15419 + const C: u32 = 42;
15420 + ˇ
15421
15422 fn main() {
15423 println!("hello");
15424
15425 println!("world");
15426 }
15427 "#
15428 .unindent(),
15429 );
15430
15431 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
15432 executor.run_until_parked();
15433
15434 cx.assert_state_with_diff(
15435 r#"
15436 use some::mod1;
15437 use some::mod2;
15438
15439 const A: u32 = 42;
15440 + const B: u32 = 42;
15441 + const C: u32 = 42;
15442 + const D: u32 = 42;
15443 + ˇ
15444
15445 fn main() {
15446 println!("hello");
15447
15448 println!("world");
15449 }
15450 "#
15451 .unindent(),
15452 );
15453
15454 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
15455 executor.run_until_parked();
15456
15457 cx.assert_state_with_diff(
15458 r#"
15459 use some::mod1;
15460 use some::mod2;
15461
15462 const A: u32 = 42;
15463 + const B: u32 = 42;
15464 + const C: u32 = 42;
15465 + const D: u32 = 42;
15466 + const E: u32 = 42;
15467 + ˇ
15468
15469 fn main() {
15470 println!("hello");
15471
15472 println!("world");
15473 }
15474 "#
15475 .unindent(),
15476 );
15477
15478 cx.update_editor(|editor, window, cx| {
15479 editor.delete_line(&DeleteLine, window, cx);
15480 });
15481 executor.run_until_parked();
15482
15483 cx.assert_state_with_diff(
15484 r#"
15485 use some::mod1;
15486 use some::mod2;
15487
15488 const A: u32 = 42;
15489 + const B: u32 = 42;
15490 + const C: u32 = 42;
15491 + const D: u32 = 42;
15492 + const E: u32 = 42;
15493 ˇ
15494 fn main() {
15495 println!("hello");
15496
15497 println!("world");
15498 }
15499 "#
15500 .unindent(),
15501 );
15502
15503 cx.update_editor(|editor, window, cx| {
15504 editor.move_up(&MoveUp, window, cx);
15505 editor.delete_line(&DeleteLine, window, cx);
15506 editor.move_up(&MoveUp, window, cx);
15507 editor.delete_line(&DeleteLine, window, cx);
15508 editor.move_up(&MoveUp, window, cx);
15509 editor.delete_line(&DeleteLine, window, cx);
15510 });
15511 executor.run_until_parked();
15512 cx.assert_state_with_diff(
15513 r#"
15514 use some::mod1;
15515 use some::mod2;
15516
15517 const A: u32 = 42;
15518 + const B: u32 = 42;
15519 ˇ
15520 fn main() {
15521 println!("hello");
15522
15523 println!("world");
15524 }
15525 "#
15526 .unindent(),
15527 );
15528
15529 cx.update_editor(|editor, window, cx| {
15530 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
15531 editor.delete_line(&DeleteLine, window, cx);
15532 });
15533 executor.run_until_parked();
15534 cx.assert_state_with_diff(
15535 r#"
15536 ˇ
15537 fn main() {
15538 println!("hello");
15539
15540 println!("world");
15541 }
15542 "#
15543 .unindent(),
15544 );
15545}
15546
15547#[gpui::test]
15548async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
15549 init_test(cx, |_| {});
15550
15551 let mut cx = EditorTestContext::new(cx).await;
15552 cx.set_head_text(indoc! { "
15553 one
15554 two
15555 three
15556 four
15557 five
15558 "
15559 });
15560 cx.set_state(indoc! { "
15561 one
15562 ˇthree
15563 five
15564 "});
15565 cx.run_until_parked();
15566 cx.update_editor(|editor, window, cx| {
15567 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15568 });
15569 cx.assert_state_with_diff(
15570 indoc! { "
15571 one
15572 - two
15573 ˇthree
15574 - four
15575 five
15576 "}
15577 .to_string(),
15578 );
15579 cx.update_editor(|editor, window, cx| {
15580 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15581 });
15582
15583 cx.assert_state_with_diff(
15584 indoc! { "
15585 one
15586 ˇthree
15587 five
15588 "}
15589 .to_string(),
15590 );
15591
15592 cx.set_state(indoc! { "
15593 one
15594 ˇTWO
15595 three
15596 four
15597 five
15598 "});
15599 cx.run_until_parked();
15600 cx.update_editor(|editor, window, cx| {
15601 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15602 });
15603
15604 cx.assert_state_with_diff(
15605 indoc! { "
15606 one
15607 - two
15608 + ˇTWO
15609 three
15610 four
15611 five
15612 "}
15613 .to_string(),
15614 );
15615 cx.update_editor(|editor, window, cx| {
15616 editor.move_up(&Default::default(), window, cx);
15617 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15618 });
15619 cx.assert_state_with_diff(
15620 indoc! { "
15621 one
15622 ˇTWO
15623 three
15624 four
15625 five
15626 "}
15627 .to_string(),
15628 );
15629}
15630
15631#[gpui::test]
15632async fn test_edits_around_expanded_deletion_hunks(
15633 executor: BackgroundExecutor,
15634 cx: &mut TestAppContext,
15635) {
15636 init_test(cx, |_| {});
15637
15638 let mut cx = EditorTestContext::new(cx).await;
15639
15640 let diff_base = r#"
15641 use some::mod1;
15642 use some::mod2;
15643
15644 const A: u32 = 42;
15645 const B: u32 = 42;
15646 const C: u32 = 42;
15647
15648
15649 fn main() {
15650 println!("hello");
15651
15652 println!("world");
15653 }
15654 "#
15655 .unindent();
15656 executor.run_until_parked();
15657 cx.set_state(
15658 &r#"
15659 use some::mod1;
15660 use some::mod2;
15661
15662 ˇconst B: u32 = 42;
15663 const C: u32 = 42;
15664
15665
15666 fn main() {
15667 println!("hello");
15668
15669 println!("world");
15670 }
15671 "#
15672 .unindent(),
15673 );
15674
15675 cx.set_head_text(&diff_base);
15676 executor.run_until_parked();
15677
15678 cx.update_editor(|editor, window, cx| {
15679 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15680 });
15681 executor.run_until_parked();
15682
15683 cx.assert_state_with_diff(
15684 r#"
15685 use some::mod1;
15686 use some::mod2;
15687
15688 - const A: u32 = 42;
15689 ˇconst B: u32 = 42;
15690 const C: u32 = 42;
15691
15692
15693 fn main() {
15694 println!("hello");
15695
15696 println!("world");
15697 }
15698 "#
15699 .unindent(),
15700 );
15701
15702 cx.update_editor(|editor, window, cx| {
15703 editor.delete_line(&DeleteLine, window, cx);
15704 });
15705 executor.run_until_parked();
15706 cx.assert_state_with_diff(
15707 r#"
15708 use some::mod1;
15709 use some::mod2;
15710
15711 - const A: u32 = 42;
15712 - const B: u32 = 42;
15713 ˇconst C: u32 = 42;
15714
15715
15716 fn main() {
15717 println!("hello");
15718
15719 println!("world");
15720 }
15721 "#
15722 .unindent(),
15723 );
15724
15725 cx.update_editor(|editor, window, cx| {
15726 editor.delete_line(&DeleteLine, window, cx);
15727 });
15728 executor.run_until_parked();
15729 cx.assert_state_with_diff(
15730 r#"
15731 use some::mod1;
15732 use some::mod2;
15733
15734 - const A: u32 = 42;
15735 - const B: u32 = 42;
15736 - const C: u32 = 42;
15737 ˇ
15738
15739 fn main() {
15740 println!("hello");
15741
15742 println!("world");
15743 }
15744 "#
15745 .unindent(),
15746 );
15747
15748 cx.update_editor(|editor, window, cx| {
15749 editor.handle_input("replacement", window, cx);
15750 });
15751 executor.run_until_parked();
15752 cx.assert_state_with_diff(
15753 r#"
15754 use some::mod1;
15755 use some::mod2;
15756
15757 - const A: u32 = 42;
15758 - const B: u32 = 42;
15759 - const C: u32 = 42;
15760 -
15761 + replacementˇ
15762
15763 fn main() {
15764 println!("hello");
15765
15766 println!("world");
15767 }
15768 "#
15769 .unindent(),
15770 );
15771}
15772
15773#[gpui::test]
15774async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15775 init_test(cx, |_| {});
15776
15777 let mut cx = EditorTestContext::new(cx).await;
15778
15779 let base_text = r#"
15780 one
15781 two
15782 three
15783 four
15784 five
15785 "#
15786 .unindent();
15787 executor.run_until_parked();
15788 cx.set_state(
15789 &r#"
15790 one
15791 two
15792 fˇour
15793 five
15794 "#
15795 .unindent(),
15796 );
15797
15798 cx.set_head_text(&base_text);
15799 executor.run_until_parked();
15800
15801 cx.update_editor(|editor, window, cx| {
15802 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15803 });
15804 executor.run_until_parked();
15805
15806 cx.assert_state_with_diff(
15807 r#"
15808 one
15809 two
15810 - three
15811 fˇour
15812 five
15813 "#
15814 .unindent(),
15815 );
15816
15817 cx.update_editor(|editor, window, cx| {
15818 editor.backspace(&Backspace, window, cx);
15819 editor.backspace(&Backspace, window, cx);
15820 });
15821 executor.run_until_parked();
15822 cx.assert_state_with_diff(
15823 r#"
15824 one
15825 two
15826 - threeˇ
15827 - four
15828 + our
15829 five
15830 "#
15831 .unindent(),
15832 );
15833}
15834
15835#[gpui::test]
15836async fn test_edit_after_expanded_modification_hunk(
15837 executor: BackgroundExecutor,
15838 cx: &mut TestAppContext,
15839) {
15840 init_test(cx, |_| {});
15841
15842 let mut cx = EditorTestContext::new(cx).await;
15843
15844 let diff_base = r#"
15845 use some::mod1;
15846 use some::mod2;
15847
15848 const A: u32 = 42;
15849 const B: u32 = 42;
15850 const C: u32 = 42;
15851 const D: u32 = 42;
15852
15853
15854 fn main() {
15855 println!("hello");
15856
15857 println!("world");
15858 }"#
15859 .unindent();
15860
15861 cx.set_state(
15862 &r#"
15863 use some::mod1;
15864 use some::mod2;
15865
15866 const A: u32 = 42;
15867 const B: u32 = 42;
15868 const C: u32 = 43ˇ
15869 const D: u32 = 42;
15870
15871
15872 fn main() {
15873 println!("hello");
15874
15875 println!("world");
15876 }"#
15877 .unindent(),
15878 );
15879
15880 cx.set_head_text(&diff_base);
15881 executor.run_until_parked();
15882 cx.update_editor(|editor, window, cx| {
15883 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15884 });
15885 executor.run_until_parked();
15886
15887 cx.assert_state_with_diff(
15888 r#"
15889 use some::mod1;
15890 use some::mod2;
15891
15892 const A: u32 = 42;
15893 const B: u32 = 42;
15894 - const C: u32 = 42;
15895 + const C: u32 = 43ˇ
15896 const D: u32 = 42;
15897
15898
15899 fn main() {
15900 println!("hello");
15901
15902 println!("world");
15903 }"#
15904 .unindent(),
15905 );
15906
15907 cx.update_editor(|editor, window, cx| {
15908 editor.handle_input("\nnew_line\n", window, cx);
15909 });
15910 executor.run_until_parked();
15911
15912 cx.assert_state_with_diff(
15913 r#"
15914 use some::mod1;
15915 use some::mod2;
15916
15917 const A: u32 = 42;
15918 const B: u32 = 42;
15919 - const C: u32 = 42;
15920 + const C: u32 = 43
15921 + new_line
15922 + ˇ
15923 const D: u32 = 42;
15924
15925
15926 fn main() {
15927 println!("hello");
15928
15929 println!("world");
15930 }"#
15931 .unindent(),
15932 );
15933}
15934
15935#[gpui::test]
15936async fn test_stage_and_unstage_added_file_hunk(
15937 executor: BackgroundExecutor,
15938 cx: &mut TestAppContext,
15939) {
15940 init_test(cx, |_| {});
15941
15942 let mut cx = EditorTestContext::new(cx).await;
15943 cx.update_editor(|editor, _, cx| {
15944 editor.set_expand_all_diff_hunks(cx);
15945 });
15946
15947 let working_copy = r#"
15948 ˇfn main() {
15949 println!("hello, world!");
15950 }
15951 "#
15952 .unindent();
15953
15954 cx.set_state(&working_copy);
15955 executor.run_until_parked();
15956
15957 cx.assert_state_with_diff(
15958 r#"
15959 + ˇfn main() {
15960 + println!("hello, world!");
15961 + }
15962 "#
15963 .unindent(),
15964 );
15965 cx.assert_index_text(None);
15966
15967 cx.update_editor(|editor, window, cx| {
15968 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
15969 });
15970 executor.run_until_parked();
15971 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
15972 cx.assert_state_with_diff(
15973 r#"
15974 + ˇfn main() {
15975 + println!("hello, world!");
15976 + }
15977 "#
15978 .unindent(),
15979 );
15980
15981 cx.update_editor(|editor, window, cx| {
15982 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
15983 });
15984 executor.run_until_parked();
15985 cx.assert_index_text(None);
15986}
15987
15988async fn setup_indent_guides_editor(
15989 text: &str,
15990 cx: &mut TestAppContext,
15991) -> (BufferId, EditorTestContext) {
15992 init_test(cx, |_| {});
15993
15994 let mut cx = EditorTestContext::new(cx).await;
15995
15996 let buffer_id = cx.update_editor(|editor, window, cx| {
15997 editor.set_text(text, window, cx);
15998 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
15999
16000 buffer_ids[0]
16001 });
16002
16003 (buffer_id, cx)
16004}
16005
16006fn assert_indent_guides(
16007 range: Range<u32>,
16008 expected: Vec<IndentGuide>,
16009 active_indices: Option<Vec<usize>>,
16010 cx: &mut EditorTestContext,
16011) {
16012 let indent_guides = cx.update_editor(|editor, window, cx| {
16013 let snapshot = editor.snapshot(window, cx).display_snapshot;
16014 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
16015 editor,
16016 MultiBufferRow(range.start)..MultiBufferRow(range.end),
16017 true,
16018 &snapshot,
16019 cx,
16020 );
16021
16022 indent_guides.sort_by(|a, b| {
16023 a.depth.cmp(&b.depth).then(
16024 a.start_row
16025 .cmp(&b.start_row)
16026 .then(a.end_row.cmp(&b.end_row)),
16027 )
16028 });
16029 indent_guides
16030 });
16031
16032 if let Some(expected) = active_indices {
16033 let active_indices = cx.update_editor(|editor, window, cx| {
16034 let snapshot = editor.snapshot(window, cx).display_snapshot;
16035 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
16036 });
16037
16038 assert_eq!(
16039 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
16040 expected,
16041 "Active indent guide indices do not match"
16042 );
16043 }
16044
16045 assert_eq!(indent_guides, expected, "Indent guides do not match");
16046}
16047
16048fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
16049 IndentGuide {
16050 buffer_id,
16051 start_row: MultiBufferRow(start_row),
16052 end_row: MultiBufferRow(end_row),
16053 depth,
16054 tab_size: 4,
16055 settings: IndentGuideSettings {
16056 enabled: true,
16057 line_width: 1,
16058 active_line_width: 1,
16059 ..Default::default()
16060 },
16061 }
16062}
16063
16064#[gpui::test]
16065async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
16066 let (buffer_id, mut cx) = setup_indent_guides_editor(
16067 &"
16068 fn main() {
16069 let a = 1;
16070 }"
16071 .unindent(),
16072 cx,
16073 )
16074 .await;
16075
16076 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
16077}
16078
16079#[gpui::test]
16080async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
16081 let (buffer_id, mut cx) = setup_indent_guides_editor(
16082 &"
16083 fn main() {
16084 let a = 1;
16085 let b = 2;
16086 }"
16087 .unindent(),
16088 cx,
16089 )
16090 .await;
16091
16092 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
16093}
16094
16095#[gpui::test]
16096async fn test_indent_guide_nested(cx: &mut TestAppContext) {
16097 let (buffer_id, mut cx) = setup_indent_guides_editor(
16098 &"
16099 fn main() {
16100 let a = 1;
16101 if a == 3 {
16102 let b = 2;
16103 } else {
16104 let c = 3;
16105 }
16106 }"
16107 .unindent(),
16108 cx,
16109 )
16110 .await;
16111
16112 assert_indent_guides(
16113 0..8,
16114 vec![
16115 indent_guide(buffer_id, 1, 6, 0),
16116 indent_guide(buffer_id, 3, 3, 1),
16117 indent_guide(buffer_id, 5, 5, 1),
16118 ],
16119 None,
16120 &mut cx,
16121 );
16122}
16123
16124#[gpui::test]
16125async fn test_indent_guide_tab(cx: &mut TestAppContext) {
16126 let (buffer_id, mut cx) = setup_indent_guides_editor(
16127 &"
16128 fn main() {
16129 let a = 1;
16130 let b = 2;
16131 let c = 3;
16132 }"
16133 .unindent(),
16134 cx,
16135 )
16136 .await;
16137
16138 assert_indent_guides(
16139 0..5,
16140 vec![
16141 indent_guide(buffer_id, 1, 3, 0),
16142 indent_guide(buffer_id, 2, 2, 1),
16143 ],
16144 None,
16145 &mut cx,
16146 );
16147}
16148
16149#[gpui::test]
16150async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
16151 let (buffer_id, mut cx) = setup_indent_guides_editor(
16152 &"
16153 fn main() {
16154 let a = 1;
16155
16156 let c = 3;
16157 }"
16158 .unindent(),
16159 cx,
16160 )
16161 .await;
16162
16163 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
16164}
16165
16166#[gpui::test]
16167async fn test_indent_guide_complex(cx: &mut TestAppContext) {
16168 let (buffer_id, mut cx) = setup_indent_guides_editor(
16169 &"
16170 fn main() {
16171 let a = 1;
16172
16173 let c = 3;
16174
16175 if a == 3 {
16176 let b = 2;
16177 } else {
16178 let c = 3;
16179 }
16180 }"
16181 .unindent(),
16182 cx,
16183 )
16184 .await;
16185
16186 assert_indent_guides(
16187 0..11,
16188 vec![
16189 indent_guide(buffer_id, 1, 9, 0),
16190 indent_guide(buffer_id, 6, 6, 1),
16191 indent_guide(buffer_id, 8, 8, 1),
16192 ],
16193 None,
16194 &mut cx,
16195 );
16196}
16197
16198#[gpui::test]
16199async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
16200 let (buffer_id, mut cx) = setup_indent_guides_editor(
16201 &"
16202 fn main() {
16203 let a = 1;
16204
16205 let c = 3;
16206
16207 if a == 3 {
16208 let b = 2;
16209 } else {
16210 let c = 3;
16211 }
16212 }"
16213 .unindent(),
16214 cx,
16215 )
16216 .await;
16217
16218 assert_indent_guides(
16219 1..11,
16220 vec![
16221 indent_guide(buffer_id, 1, 9, 0),
16222 indent_guide(buffer_id, 6, 6, 1),
16223 indent_guide(buffer_id, 8, 8, 1),
16224 ],
16225 None,
16226 &mut cx,
16227 );
16228}
16229
16230#[gpui::test]
16231async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
16232 let (buffer_id, mut cx) = setup_indent_guides_editor(
16233 &"
16234 fn main() {
16235 let a = 1;
16236
16237 let c = 3;
16238
16239 if a == 3 {
16240 let b = 2;
16241 } else {
16242 let c = 3;
16243 }
16244 }"
16245 .unindent(),
16246 cx,
16247 )
16248 .await;
16249
16250 assert_indent_guides(
16251 1..10,
16252 vec![
16253 indent_guide(buffer_id, 1, 9, 0),
16254 indent_guide(buffer_id, 6, 6, 1),
16255 indent_guide(buffer_id, 8, 8, 1),
16256 ],
16257 None,
16258 &mut cx,
16259 );
16260}
16261
16262#[gpui::test]
16263async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
16264 let (buffer_id, mut cx) = setup_indent_guides_editor(
16265 &"
16266 block1
16267 block2
16268 block3
16269 block4
16270 block2
16271 block1
16272 block1"
16273 .unindent(),
16274 cx,
16275 )
16276 .await;
16277
16278 assert_indent_guides(
16279 1..10,
16280 vec![
16281 indent_guide(buffer_id, 1, 4, 0),
16282 indent_guide(buffer_id, 2, 3, 1),
16283 indent_guide(buffer_id, 3, 3, 2),
16284 ],
16285 None,
16286 &mut cx,
16287 );
16288}
16289
16290#[gpui::test]
16291async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
16292 let (buffer_id, mut cx) = setup_indent_guides_editor(
16293 &"
16294 block1
16295 block2
16296 block3
16297
16298 block1
16299 block1"
16300 .unindent(),
16301 cx,
16302 )
16303 .await;
16304
16305 assert_indent_guides(
16306 0..6,
16307 vec![
16308 indent_guide(buffer_id, 1, 2, 0),
16309 indent_guide(buffer_id, 2, 2, 1),
16310 ],
16311 None,
16312 &mut cx,
16313 );
16314}
16315
16316#[gpui::test]
16317async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
16318 let (buffer_id, mut cx) = setup_indent_guides_editor(
16319 &"
16320 block1
16321
16322
16323
16324 block2
16325 "
16326 .unindent(),
16327 cx,
16328 )
16329 .await;
16330
16331 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
16332}
16333
16334#[gpui::test]
16335async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
16336 let (buffer_id, mut cx) = setup_indent_guides_editor(
16337 &"
16338 def a:
16339 \tb = 3
16340 \tif True:
16341 \t\tc = 4
16342 \t\td = 5
16343 \tprint(b)
16344 "
16345 .unindent(),
16346 cx,
16347 )
16348 .await;
16349
16350 assert_indent_guides(
16351 0..6,
16352 vec![
16353 indent_guide(buffer_id, 1, 6, 0),
16354 indent_guide(buffer_id, 3, 4, 1),
16355 ],
16356 None,
16357 &mut cx,
16358 );
16359}
16360
16361#[gpui::test]
16362async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
16363 let (buffer_id, mut cx) = setup_indent_guides_editor(
16364 &"
16365 fn main() {
16366 let a = 1;
16367 }"
16368 .unindent(),
16369 cx,
16370 )
16371 .await;
16372
16373 cx.update_editor(|editor, window, cx| {
16374 editor.change_selections(None, window, cx, |s| {
16375 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16376 });
16377 });
16378
16379 assert_indent_guides(
16380 0..3,
16381 vec![indent_guide(buffer_id, 1, 1, 0)],
16382 Some(vec![0]),
16383 &mut cx,
16384 );
16385}
16386
16387#[gpui::test]
16388async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
16389 let (buffer_id, mut cx) = setup_indent_guides_editor(
16390 &"
16391 fn main() {
16392 if 1 == 2 {
16393 let a = 1;
16394 }
16395 }"
16396 .unindent(),
16397 cx,
16398 )
16399 .await;
16400
16401 cx.update_editor(|editor, window, cx| {
16402 editor.change_selections(None, window, cx, |s| {
16403 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16404 });
16405 });
16406
16407 assert_indent_guides(
16408 0..4,
16409 vec![
16410 indent_guide(buffer_id, 1, 3, 0),
16411 indent_guide(buffer_id, 2, 2, 1),
16412 ],
16413 Some(vec![1]),
16414 &mut cx,
16415 );
16416
16417 cx.update_editor(|editor, window, cx| {
16418 editor.change_selections(None, window, cx, |s| {
16419 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
16420 });
16421 });
16422
16423 assert_indent_guides(
16424 0..4,
16425 vec![
16426 indent_guide(buffer_id, 1, 3, 0),
16427 indent_guide(buffer_id, 2, 2, 1),
16428 ],
16429 Some(vec![1]),
16430 &mut cx,
16431 );
16432
16433 cx.update_editor(|editor, window, cx| {
16434 editor.change_selections(None, window, cx, |s| {
16435 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
16436 });
16437 });
16438
16439 assert_indent_guides(
16440 0..4,
16441 vec![
16442 indent_guide(buffer_id, 1, 3, 0),
16443 indent_guide(buffer_id, 2, 2, 1),
16444 ],
16445 Some(vec![0]),
16446 &mut cx,
16447 );
16448}
16449
16450#[gpui::test]
16451async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
16452 let (buffer_id, mut cx) = setup_indent_guides_editor(
16453 &"
16454 fn main() {
16455 let a = 1;
16456
16457 let b = 2;
16458 }"
16459 .unindent(),
16460 cx,
16461 )
16462 .await;
16463
16464 cx.update_editor(|editor, window, cx| {
16465 editor.change_selections(None, window, cx, |s| {
16466 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
16467 });
16468 });
16469
16470 assert_indent_guides(
16471 0..5,
16472 vec![indent_guide(buffer_id, 1, 3, 0)],
16473 Some(vec![0]),
16474 &mut cx,
16475 );
16476}
16477
16478#[gpui::test]
16479async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
16480 let (buffer_id, mut cx) = setup_indent_guides_editor(
16481 &"
16482 def m:
16483 a = 1
16484 pass"
16485 .unindent(),
16486 cx,
16487 )
16488 .await;
16489
16490 cx.update_editor(|editor, window, cx| {
16491 editor.change_selections(None, window, cx, |s| {
16492 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16493 });
16494 });
16495
16496 assert_indent_guides(
16497 0..3,
16498 vec![indent_guide(buffer_id, 1, 2, 0)],
16499 Some(vec![0]),
16500 &mut cx,
16501 );
16502}
16503
16504#[gpui::test]
16505async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
16506 init_test(cx, |_| {});
16507 let mut cx = EditorTestContext::new(cx).await;
16508 let text = indoc! {
16509 "
16510 impl A {
16511 fn b() {
16512 0;
16513 3;
16514 5;
16515 6;
16516 7;
16517 }
16518 }
16519 "
16520 };
16521 let base_text = indoc! {
16522 "
16523 impl A {
16524 fn b() {
16525 0;
16526 1;
16527 2;
16528 3;
16529 4;
16530 }
16531 fn c() {
16532 5;
16533 6;
16534 7;
16535 }
16536 }
16537 "
16538 };
16539
16540 cx.update_editor(|editor, window, cx| {
16541 editor.set_text(text, window, cx);
16542
16543 editor.buffer().update(cx, |multibuffer, cx| {
16544 let buffer = multibuffer.as_singleton().unwrap();
16545 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
16546
16547 multibuffer.set_all_diff_hunks_expanded(cx);
16548 multibuffer.add_diff(diff, cx);
16549
16550 buffer.read(cx).remote_id()
16551 })
16552 });
16553 cx.run_until_parked();
16554
16555 cx.assert_state_with_diff(
16556 indoc! { "
16557 impl A {
16558 fn b() {
16559 0;
16560 - 1;
16561 - 2;
16562 3;
16563 - 4;
16564 - }
16565 - fn c() {
16566 5;
16567 6;
16568 7;
16569 }
16570 }
16571 ˇ"
16572 }
16573 .to_string(),
16574 );
16575
16576 let mut actual_guides = cx.update_editor(|editor, window, cx| {
16577 editor
16578 .snapshot(window, cx)
16579 .buffer_snapshot
16580 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
16581 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
16582 .collect::<Vec<_>>()
16583 });
16584 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
16585 assert_eq!(
16586 actual_guides,
16587 vec![
16588 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
16589 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
16590 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
16591 ]
16592 );
16593}
16594
16595#[gpui::test]
16596async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16597 init_test(cx, |_| {});
16598 let mut cx = EditorTestContext::new(cx).await;
16599
16600 let diff_base = r#"
16601 a
16602 b
16603 c
16604 "#
16605 .unindent();
16606
16607 cx.set_state(
16608 &r#"
16609 ˇA
16610 b
16611 C
16612 "#
16613 .unindent(),
16614 );
16615 cx.set_head_text(&diff_base);
16616 cx.update_editor(|editor, window, cx| {
16617 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16618 });
16619 executor.run_until_parked();
16620
16621 let both_hunks_expanded = r#"
16622 - a
16623 + ˇA
16624 b
16625 - c
16626 + C
16627 "#
16628 .unindent();
16629
16630 cx.assert_state_with_diff(both_hunks_expanded.clone());
16631
16632 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16633 let snapshot = editor.snapshot(window, cx);
16634 let hunks = editor
16635 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16636 .collect::<Vec<_>>();
16637 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16638 let buffer_id = hunks[0].buffer_id;
16639 hunks
16640 .into_iter()
16641 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16642 .collect::<Vec<_>>()
16643 });
16644 assert_eq!(hunk_ranges.len(), 2);
16645
16646 cx.update_editor(|editor, _, cx| {
16647 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16648 });
16649 executor.run_until_parked();
16650
16651 let second_hunk_expanded = r#"
16652 ˇA
16653 b
16654 - c
16655 + C
16656 "#
16657 .unindent();
16658
16659 cx.assert_state_with_diff(second_hunk_expanded);
16660
16661 cx.update_editor(|editor, _, cx| {
16662 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16663 });
16664 executor.run_until_parked();
16665
16666 cx.assert_state_with_diff(both_hunks_expanded.clone());
16667
16668 cx.update_editor(|editor, _, cx| {
16669 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
16670 });
16671 executor.run_until_parked();
16672
16673 let first_hunk_expanded = r#"
16674 - a
16675 + ˇA
16676 b
16677 C
16678 "#
16679 .unindent();
16680
16681 cx.assert_state_with_diff(first_hunk_expanded);
16682
16683 cx.update_editor(|editor, _, cx| {
16684 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
16685 });
16686 executor.run_until_parked();
16687
16688 cx.assert_state_with_diff(both_hunks_expanded);
16689
16690 cx.set_state(
16691 &r#"
16692 ˇA
16693 b
16694 "#
16695 .unindent(),
16696 );
16697 cx.run_until_parked();
16698
16699 // TODO this cursor position seems bad
16700 cx.assert_state_with_diff(
16701 r#"
16702 - ˇa
16703 + A
16704 b
16705 "#
16706 .unindent(),
16707 );
16708
16709 cx.update_editor(|editor, window, cx| {
16710 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16711 });
16712
16713 cx.assert_state_with_diff(
16714 r#"
16715 - ˇa
16716 + A
16717 b
16718 - c
16719 "#
16720 .unindent(),
16721 );
16722
16723 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16724 let snapshot = editor.snapshot(window, cx);
16725 let hunks = editor
16726 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16727 .collect::<Vec<_>>();
16728 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16729 let buffer_id = hunks[0].buffer_id;
16730 hunks
16731 .into_iter()
16732 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16733 .collect::<Vec<_>>()
16734 });
16735 assert_eq!(hunk_ranges.len(), 2);
16736
16737 cx.update_editor(|editor, _, cx| {
16738 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
16739 });
16740 executor.run_until_parked();
16741
16742 cx.assert_state_with_diff(
16743 r#"
16744 - ˇa
16745 + A
16746 b
16747 "#
16748 .unindent(),
16749 );
16750}
16751
16752#[gpui::test]
16753async fn test_toggle_deletion_hunk_at_start_of_file(
16754 executor: BackgroundExecutor,
16755 cx: &mut TestAppContext,
16756) {
16757 init_test(cx, |_| {});
16758 let mut cx = EditorTestContext::new(cx).await;
16759
16760 let diff_base = r#"
16761 a
16762 b
16763 c
16764 "#
16765 .unindent();
16766
16767 cx.set_state(
16768 &r#"
16769 ˇb
16770 c
16771 "#
16772 .unindent(),
16773 );
16774 cx.set_head_text(&diff_base);
16775 cx.update_editor(|editor, window, cx| {
16776 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16777 });
16778 executor.run_until_parked();
16779
16780 let hunk_expanded = r#"
16781 - a
16782 ˇb
16783 c
16784 "#
16785 .unindent();
16786
16787 cx.assert_state_with_diff(hunk_expanded.clone());
16788
16789 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16790 let snapshot = editor.snapshot(window, cx);
16791 let hunks = editor
16792 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16793 .collect::<Vec<_>>();
16794 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16795 let buffer_id = hunks[0].buffer_id;
16796 hunks
16797 .into_iter()
16798 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16799 .collect::<Vec<_>>()
16800 });
16801 assert_eq!(hunk_ranges.len(), 1);
16802
16803 cx.update_editor(|editor, _, cx| {
16804 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16805 });
16806 executor.run_until_parked();
16807
16808 let hunk_collapsed = r#"
16809 ˇb
16810 c
16811 "#
16812 .unindent();
16813
16814 cx.assert_state_with_diff(hunk_collapsed);
16815
16816 cx.update_editor(|editor, _, cx| {
16817 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16818 });
16819 executor.run_until_parked();
16820
16821 cx.assert_state_with_diff(hunk_expanded.clone());
16822}
16823
16824#[gpui::test]
16825async fn test_display_diff_hunks(cx: &mut TestAppContext) {
16826 init_test(cx, |_| {});
16827
16828 let fs = FakeFs::new(cx.executor());
16829 fs.insert_tree(
16830 path!("/test"),
16831 json!({
16832 ".git": {},
16833 "file-1": "ONE\n",
16834 "file-2": "TWO\n",
16835 "file-3": "THREE\n",
16836 }),
16837 )
16838 .await;
16839
16840 fs.set_head_for_repo(
16841 path!("/test/.git").as_ref(),
16842 &[
16843 ("file-1".into(), "one\n".into()),
16844 ("file-2".into(), "two\n".into()),
16845 ("file-3".into(), "three\n".into()),
16846 ],
16847 );
16848
16849 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
16850 let mut buffers = vec![];
16851 for i in 1..=3 {
16852 let buffer = project
16853 .update(cx, |project, cx| {
16854 let path = format!(path!("/test/file-{}"), i);
16855 project.open_local_buffer(path, cx)
16856 })
16857 .await
16858 .unwrap();
16859 buffers.push(buffer);
16860 }
16861
16862 let multibuffer = cx.new(|cx| {
16863 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
16864 multibuffer.set_all_diff_hunks_expanded(cx);
16865 for buffer in &buffers {
16866 let snapshot = buffer.read(cx).snapshot();
16867 multibuffer.set_excerpts_for_path(
16868 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
16869 buffer.clone(),
16870 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
16871 DEFAULT_MULTIBUFFER_CONTEXT,
16872 cx,
16873 );
16874 }
16875 multibuffer
16876 });
16877
16878 let editor = cx.add_window(|window, cx| {
16879 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
16880 });
16881 cx.run_until_parked();
16882
16883 let snapshot = editor
16884 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16885 .unwrap();
16886 let hunks = snapshot
16887 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
16888 .map(|hunk| match hunk {
16889 DisplayDiffHunk::Unfolded {
16890 display_row_range, ..
16891 } => display_row_range,
16892 DisplayDiffHunk::Folded { .. } => unreachable!(),
16893 })
16894 .collect::<Vec<_>>();
16895 assert_eq!(
16896 hunks,
16897 [
16898 DisplayRow(2)..DisplayRow(4),
16899 DisplayRow(7)..DisplayRow(9),
16900 DisplayRow(12)..DisplayRow(14),
16901 ]
16902 );
16903}
16904
16905#[gpui::test]
16906async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
16907 init_test(cx, |_| {});
16908
16909 let mut cx = EditorTestContext::new(cx).await;
16910 cx.set_head_text(indoc! { "
16911 one
16912 two
16913 three
16914 four
16915 five
16916 "
16917 });
16918 cx.set_index_text(indoc! { "
16919 one
16920 two
16921 three
16922 four
16923 five
16924 "
16925 });
16926 cx.set_state(indoc! {"
16927 one
16928 TWO
16929 ˇTHREE
16930 FOUR
16931 five
16932 "});
16933 cx.run_until_parked();
16934 cx.update_editor(|editor, window, cx| {
16935 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16936 });
16937 cx.run_until_parked();
16938 cx.assert_index_text(Some(indoc! {"
16939 one
16940 TWO
16941 THREE
16942 FOUR
16943 five
16944 "}));
16945 cx.set_state(indoc! { "
16946 one
16947 TWO
16948 ˇTHREE-HUNDRED
16949 FOUR
16950 five
16951 "});
16952 cx.run_until_parked();
16953 cx.update_editor(|editor, window, cx| {
16954 let snapshot = editor.snapshot(window, cx);
16955 let hunks = editor
16956 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16957 .collect::<Vec<_>>();
16958 assert_eq!(hunks.len(), 1);
16959 assert_eq!(
16960 hunks[0].status(),
16961 DiffHunkStatus {
16962 kind: DiffHunkStatusKind::Modified,
16963 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
16964 }
16965 );
16966
16967 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16968 });
16969 cx.run_until_parked();
16970 cx.assert_index_text(Some(indoc! {"
16971 one
16972 TWO
16973 THREE-HUNDRED
16974 FOUR
16975 five
16976 "}));
16977}
16978
16979#[gpui::test]
16980fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
16981 init_test(cx, |_| {});
16982
16983 let editor = cx.add_window(|window, cx| {
16984 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
16985 build_editor(buffer, window, cx)
16986 });
16987
16988 let render_args = Arc::new(Mutex::new(None));
16989 let snapshot = editor
16990 .update(cx, |editor, window, cx| {
16991 let snapshot = editor.buffer().read(cx).snapshot(cx);
16992 let range =
16993 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
16994
16995 struct RenderArgs {
16996 row: MultiBufferRow,
16997 folded: bool,
16998 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
16999 }
17000
17001 let crease = Crease::inline(
17002 range,
17003 FoldPlaceholder::test(),
17004 {
17005 let toggle_callback = render_args.clone();
17006 move |row, folded, callback, _window, _cx| {
17007 *toggle_callback.lock() = Some(RenderArgs {
17008 row,
17009 folded,
17010 callback,
17011 });
17012 div()
17013 }
17014 },
17015 |_row, _folded, _window, _cx| div(),
17016 );
17017
17018 editor.insert_creases(Some(crease), cx);
17019 let snapshot = editor.snapshot(window, cx);
17020 let _div = snapshot.render_crease_toggle(
17021 MultiBufferRow(1),
17022 false,
17023 cx.entity().clone(),
17024 window,
17025 cx,
17026 );
17027 snapshot
17028 })
17029 .unwrap();
17030
17031 let render_args = render_args.lock().take().unwrap();
17032 assert_eq!(render_args.row, MultiBufferRow(1));
17033 assert!(!render_args.folded);
17034 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
17035
17036 cx.update_window(*editor, |_, window, cx| {
17037 (render_args.callback)(true, window, cx)
17038 })
17039 .unwrap();
17040 let snapshot = editor
17041 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17042 .unwrap();
17043 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
17044
17045 cx.update_window(*editor, |_, window, cx| {
17046 (render_args.callback)(false, window, cx)
17047 })
17048 .unwrap();
17049 let snapshot = editor
17050 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17051 .unwrap();
17052 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
17053}
17054
17055#[gpui::test]
17056async fn test_input_text(cx: &mut TestAppContext) {
17057 init_test(cx, |_| {});
17058 let mut cx = EditorTestContext::new(cx).await;
17059
17060 cx.set_state(
17061 &r#"ˇone
17062 two
17063
17064 three
17065 fourˇ
17066 five
17067
17068 siˇx"#
17069 .unindent(),
17070 );
17071
17072 cx.dispatch_action(HandleInput(String::new()));
17073 cx.assert_editor_state(
17074 &r#"ˇone
17075 two
17076
17077 three
17078 fourˇ
17079 five
17080
17081 siˇx"#
17082 .unindent(),
17083 );
17084
17085 cx.dispatch_action(HandleInput("AAAA".to_string()));
17086 cx.assert_editor_state(
17087 &r#"AAAAˇone
17088 two
17089
17090 three
17091 fourAAAAˇ
17092 five
17093
17094 siAAAAˇx"#
17095 .unindent(),
17096 );
17097}
17098
17099#[gpui::test]
17100async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
17101 init_test(cx, |_| {});
17102
17103 let mut cx = EditorTestContext::new(cx).await;
17104 cx.set_state(
17105 r#"let foo = 1;
17106let foo = 2;
17107let foo = 3;
17108let fooˇ = 4;
17109let foo = 5;
17110let foo = 6;
17111let foo = 7;
17112let foo = 8;
17113let foo = 9;
17114let foo = 10;
17115let foo = 11;
17116let foo = 12;
17117let foo = 13;
17118let foo = 14;
17119let foo = 15;"#,
17120 );
17121
17122 cx.update_editor(|e, window, cx| {
17123 assert_eq!(
17124 e.next_scroll_position,
17125 NextScrollCursorCenterTopBottom::Center,
17126 "Default next scroll direction is center",
17127 );
17128
17129 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17130 assert_eq!(
17131 e.next_scroll_position,
17132 NextScrollCursorCenterTopBottom::Top,
17133 "After center, next scroll direction should be top",
17134 );
17135
17136 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17137 assert_eq!(
17138 e.next_scroll_position,
17139 NextScrollCursorCenterTopBottom::Bottom,
17140 "After top, next scroll direction should be bottom",
17141 );
17142
17143 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17144 assert_eq!(
17145 e.next_scroll_position,
17146 NextScrollCursorCenterTopBottom::Center,
17147 "After bottom, scrolling should start over",
17148 );
17149
17150 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17151 assert_eq!(
17152 e.next_scroll_position,
17153 NextScrollCursorCenterTopBottom::Top,
17154 "Scrolling continues if retriggered fast enough"
17155 );
17156 });
17157
17158 cx.executor()
17159 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
17160 cx.executor().run_until_parked();
17161 cx.update_editor(|e, _, _| {
17162 assert_eq!(
17163 e.next_scroll_position,
17164 NextScrollCursorCenterTopBottom::Center,
17165 "If scrolling is not triggered fast enough, it should reset"
17166 );
17167 });
17168}
17169
17170#[gpui::test]
17171async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
17172 init_test(cx, |_| {});
17173 let mut cx = EditorLspTestContext::new_rust(
17174 lsp::ServerCapabilities {
17175 definition_provider: Some(lsp::OneOf::Left(true)),
17176 references_provider: Some(lsp::OneOf::Left(true)),
17177 ..lsp::ServerCapabilities::default()
17178 },
17179 cx,
17180 )
17181 .await;
17182
17183 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
17184 let go_to_definition = cx
17185 .lsp
17186 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
17187 move |params, _| async move {
17188 if empty_go_to_definition {
17189 Ok(None)
17190 } else {
17191 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
17192 uri: params.text_document_position_params.text_document.uri,
17193 range: lsp::Range::new(
17194 lsp::Position::new(4, 3),
17195 lsp::Position::new(4, 6),
17196 ),
17197 })))
17198 }
17199 },
17200 );
17201 let references = cx
17202 .lsp
17203 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
17204 Ok(Some(vec![lsp::Location {
17205 uri: params.text_document_position.text_document.uri,
17206 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
17207 }]))
17208 });
17209 (go_to_definition, references)
17210 };
17211
17212 cx.set_state(
17213 &r#"fn one() {
17214 let mut a = ˇtwo();
17215 }
17216
17217 fn two() {}"#
17218 .unindent(),
17219 );
17220 set_up_lsp_handlers(false, &mut cx);
17221 let navigated = cx
17222 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17223 .await
17224 .expect("Failed to navigate to definition");
17225 assert_eq!(
17226 navigated,
17227 Navigated::Yes,
17228 "Should have navigated to definition from the GetDefinition response"
17229 );
17230 cx.assert_editor_state(
17231 &r#"fn one() {
17232 let mut a = two();
17233 }
17234
17235 fn «twoˇ»() {}"#
17236 .unindent(),
17237 );
17238
17239 let editors = cx.update_workspace(|workspace, _, cx| {
17240 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17241 });
17242 cx.update_editor(|_, _, test_editor_cx| {
17243 assert_eq!(
17244 editors.len(),
17245 1,
17246 "Initially, only one, test, editor should be open in the workspace"
17247 );
17248 assert_eq!(
17249 test_editor_cx.entity(),
17250 editors.last().expect("Asserted len is 1").clone()
17251 );
17252 });
17253
17254 set_up_lsp_handlers(true, &mut cx);
17255 let navigated = cx
17256 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17257 .await
17258 .expect("Failed to navigate to lookup references");
17259 assert_eq!(
17260 navigated,
17261 Navigated::Yes,
17262 "Should have navigated to references as a fallback after empty GoToDefinition response"
17263 );
17264 // We should not change the selections in the existing file,
17265 // if opening another milti buffer with the references
17266 cx.assert_editor_state(
17267 &r#"fn one() {
17268 let mut a = two();
17269 }
17270
17271 fn «twoˇ»() {}"#
17272 .unindent(),
17273 );
17274 let editors = cx.update_workspace(|workspace, _, cx| {
17275 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17276 });
17277 cx.update_editor(|_, _, test_editor_cx| {
17278 assert_eq!(
17279 editors.len(),
17280 2,
17281 "After falling back to references search, we open a new editor with the results"
17282 );
17283 let references_fallback_text = editors
17284 .into_iter()
17285 .find(|new_editor| *new_editor != test_editor_cx.entity())
17286 .expect("Should have one non-test editor now")
17287 .read(test_editor_cx)
17288 .text(test_editor_cx);
17289 assert_eq!(
17290 references_fallback_text, "fn one() {\n let mut a = two();\n}",
17291 "Should use the range from the references response and not the GoToDefinition one"
17292 );
17293 });
17294}
17295
17296#[gpui::test]
17297async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
17298 init_test(cx, |_| {});
17299 cx.update(|cx| {
17300 let mut editor_settings = EditorSettings::get_global(cx).clone();
17301 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
17302 EditorSettings::override_global(editor_settings, cx);
17303 });
17304 let mut cx = EditorLspTestContext::new_rust(
17305 lsp::ServerCapabilities {
17306 definition_provider: Some(lsp::OneOf::Left(true)),
17307 references_provider: Some(lsp::OneOf::Left(true)),
17308 ..lsp::ServerCapabilities::default()
17309 },
17310 cx,
17311 )
17312 .await;
17313 let original_state = r#"fn one() {
17314 let mut a = ˇtwo();
17315 }
17316
17317 fn two() {}"#
17318 .unindent();
17319 cx.set_state(&original_state);
17320
17321 let mut go_to_definition = cx
17322 .lsp
17323 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
17324 move |_, _| async move { Ok(None) },
17325 );
17326 let _references = cx
17327 .lsp
17328 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
17329 panic!("Should not call for references with no go to definition fallback")
17330 });
17331
17332 let navigated = cx
17333 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17334 .await
17335 .expect("Failed to navigate to lookup references");
17336 go_to_definition
17337 .next()
17338 .await
17339 .expect("Should have called the go_to_definition handler");
17340
17341 assert_eq!(
17342 navigated,
17343 Navigated::No,
17344 "Should have navigated to references as a fallback after empty GoToDefinition response"
17345 );
17346 cx.assert_editor_state(&original_state);
17347 let editors = cx.update_workspace(|workspace, _, cx| {
17348 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17349 });
17350 cx.update_editor(|_, _, _| {
17351 assert_eq!(
17352 editors.len(),
17353 1,
17354 "After unsuccessful fallback, no other editor should have been opened"
17355 );
17356 });
17357}
17358
17359#[gpui::test]
17360async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
17361 init_test(cx, |_| {});
17362
17363 let language = Arc::new(Language::new(
17364 LanguageConfig::default(),
17365 Some(tree_sitter_rust::LANGUAGE.into()),
17366 ));
17367
17368 let text = r#"
17369 #[cfg(test)]
17370 mod tests() {
17371 #[test]
17372 fn runnable_1() {
17373 let a = 1;
17374 }
17375
17376 #[test]
17377 fn runnable_2() {
17378 let a = 1;
17379 let b = 2;
17380 }
17381 }
17382 "#
17383 .unindent();
17384
17385 let fs = FakeFs::new(cx.executor());
17386 fs.insert_file("/file.rs", Default::default()).await;
17387
17388 let project = Project::test(fs, ["/a".as_ref()], cx).await;
17389 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17390 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17391 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
17392 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
17393
17394 let editor = cx.new_window_entity(|window, cx| {
17395 Editor::new(
17396 EditorMode::full(),
17397 multi_buffer,
17398 Some(project.clone()),
17399 window,
17400 cx,
17401 )
17402 });
17403
17404 editor.update_in(cx, |editor, window, cx| {
17405 let snapshot = editor.buffer().read(cx).snapshot(cx);
17406 editor.tasks.insert(
17407 (buffer.read(cx).remote_id(), 3),
17408 RunnableTasks {
17409 templates: vec![],
17410 offset: snapshot.anchor_before(43),
17411 column: 0,
17412 extra_variables: HashMap::default(),
17413 context_range: BufferOffset(43)..BufferOffset(85),
17414 },
17415 );
17416 editor.tasks.insert(
17417 (buffer.read(cx).remote_id(), 8),
17418 RunnableTasks {
17419 templates: vec![],
17420 offset: snapshot.anchor_before(86),
17421 column: 0,
17422 extra_variables: HashMap::default(),
17423 context_range: BufferOffset(86)..BufferOffset(191),
17424 },
17425 );
17426
17427 // Test finding task when cursor is inside function body
17428 editor.change_selections(None, window, cx, |s| {
17429 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
17430 });
17431 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
17432 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
17433
17434 // Test finding task when cursor is on function name
17435 editor.change_selections(None, window, cx, |s| {
17436 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
17437 });
17438 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
17439 assert_eq!(row, 8, "Should find task when cursor is on function name");
17440 });
17441}
17442
17443#[gpui::test]
17444async fn test_folding_buffers(cx: &mut TestAppContext) {
17445 init_test(cx, |_| {});
17446
17447 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
17448 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
17449 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
17450
17451 let fs = FakeFs::new(cx.executor());
17452 fs.insert_tree(
17453 path!("/a"),
17454 json!({
17455 "first.rs": sample_text_1,
17456 "second.rs": sample_text_2,
17457 "third.rs": sample_text_3,
17458 }),
17459 )
17460 .await;
17461 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17462 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17463 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17464 let worktree = project.update(cx, |project, cx| {
17465 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17466 assert_eq!(worktrees.len(), 1);
17467 worktrees.pop().unwrap()
17468 });
17469 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17470
17471 let buffer_1 = project
17472 .update(cx, |project, cx| {
17473 project.open_buffer((worktree_id, "first.rs"), cx)
17474 })
17475 .await
17476 .unwrap();
17477 let buffer_2 = project
17478 .update(cx, |project, cx| {
17479 project.open_buffer((worktree_id, "second.rs"), cx)
17480 })
17481 .await
17482 .unwrap();
17483 let buffer_3 = project
17484 .update(cx, |project, cx| {
17485 project.open_buffer((worktree_id, "third.rs"), cx)
17486 })
17487 .await
17488 .unwrap();
17489
17490 let multi_buffer = cx.new(|cx| {
17491 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17492 multi_buffer.push_excerpts(
17493 buffer_1.clone(),
17494 [
17495 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17496 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17497 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17498 ],
17499 cx,
17500 );
17501 multi_buffer.push_excerpts(
17502 buffer_2.clone(),
17503 [
17504 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17505 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17506 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17507 ],
17508 cx,
17509 );
17510 multi_buffer.push_excerpts(
17511 buffer_3.clone(),
17512 [
17513 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17514 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17515 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17516 ],
17517 cx,
17518 );
17519 multi_buffer
17520 });
17521 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17522 Editor::new(
17523 EditorMode::full(),
17524 multi_buffer.clone(),
17525 Some(project.clone()),
17526 window,
17527 cx,
17528 )
17529 });
17530
17531 assert_eq!(
17532 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17533 "\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",
17534 );
17535
17536 multi_buffer_editor.update(cx, |editor, cx| {
17537 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
17538 });
17539 assert_eq!(
17540 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17541 "\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",
17542 "After folding the first buffer, its text should not be displayed"
17543 );
17544
17545 multi_buffer_editor.update(cx, |editor, cx| {
17546 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
17547 });
17548 assert_eq!(
17549 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17550 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
17551 "After folding the second buffer, its text should not be displayed"
17552 );
17553
17554 multi_buffer_editor.update(cx, |editor, cx| {
17555 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
17556 });
17557 assert_eq!(
17558 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17559 "\n\n\n\n\n",
17560 "After folding the third buffer, its text should not be displayed"
17561 );
17562
17563 // Emulate selection inside the fold logic, that should work
17564 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17565 editor
17566 .snapshot(window, cx)
17567 .next_line_boundary(Point::new(0, 4));
17568 });
17569
17570 multi_buffer_editor.update(cx, |editor, cx| {
17571 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
17572 });
17573 assert_eq!(
17574 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17575 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
17576 "After unfolding the second buffer, its text should be displayed"
17577 );
17578
17579 // Typing inside of buffer 1 causes that buffer to be unfolded.
17580 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17581 assert_eq!(
17582 multi_buffer
17583 .read(cx)
17584 .snapshot(cx)
17585 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
17586 .collect::<String>(),
17587 "bbbb"
17588 );
17589 editor.change_selections(None, window, cx, |selections| {
17590 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
17591 });
17592 editor.handle_input("B", window, cx);
17593 });
17594
17595 assert_eq!(
17596 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17597 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
17598 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
17599 );
17600
17601 multi_buffer_editor.update(cx, |editor, cx| {
17602 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
17603 });
17604 assert_eq!(
17605 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17606 "\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",
17607 "After unfolding the all buffers, all original text should be displayed"
17608 );
17609}
17610
17611#[gpui::test]
17612async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
17613 init_test(cx, |_| {});
17614
17615 let sample_text_1 = "1111\n2222\n3333".to_string();
17616 let sample_text_2 = "4444\n5555\n6666".to_string();
17617 let sample_text_3 = "7777\n8888\n9999".to_string();
17618
17619 let fs = FakeFs::new(cx.executor());
17620 fs.insert_tree(
17621 path!("/a"),
17622 json!({
17623 "first.rs": sample_text_1,
17624 "second.rs": sample_text_2,
17625 "third.rs": sample_text_3,
17626 }),
17627 )
17628 .await;
17629 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17630 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17631 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17632 let worktree = project.update(cx, |project, cx| {
17633 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17634 assert_eq!(worktrees.len(), 1);
17635 worktrees.pop().unwrap()
17636 });
17637 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17638
17639 let buffer_1 = project
17640 .update(cx, |project, cx| {
17641 project.open_buffer((worktree_id, "first.rs"), cx)
17642 })
17643 .await
17644 .unwrap();
17645 let buffer_2 = project
17646 .update(cx, |project, cx| {
17647 project.open_buffer((worktree_id, "second.rs"), cx)
17648 })
17649 .await
17650 .unwrap();
17651 let buffer_3 = project
17652 .update(cx, |project, cx| {
17653 project.open_buffer((worktree_id, "third.rs"), cx)
17654 })
17655 .await
17656 .unwrap();
17657
17658 let multi_buffer = cx.new(|cx| {
17659 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17660 multi_buffer.push_excerpts(
17661 buffer_1.clone(),
17662 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
17663 cx,
17664 );
17665 multi_buffer.push_excerpts(
17666 buffer_2.clone(),
17667 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
17668 cx,
17669 );
17670 multi_buffer.push_excerpts(
17671 buffer_3.clone(),
17672 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
17673 cx,
17674 );
17675 multi_buffer
17676 });
17677
17678 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17679 Editor::new(
17680 EditorMode::full(),
17681 multi_buffer,
17682 Some(project.clone()),
17683 window,
17684 cx,
17685 )
17686 });
17687
17688 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
17689 assert_eq!(
17690 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17691 full_text,
17692 );
17693
17694 multi_buffer_editor.update(cx, |editor, cx| {
17695 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
17696 });
17697 assert_eq!(
17698 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17699 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
17700 "After folding the first buffer, its text should not be displayed"
17701 );
17702
17703 multi_buffer_editor.update(cx, |editor, cx| {
17704 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
17705 });
17706
17707 assert_eq!(
17708 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17709 "\n\n\n\n\n\n7777\n8888\n9999",
17710 "After folding the second buffer, its text should not be displayed"
17711 );
17712
17713 multi_buffer_editor.update(cx, |editor, cx| {
17714 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
17715 });
17716 assert_eq!(
17717 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17718 "\n\n\n\n\n",
17719 "After folding the third buffer, its text should not be displayed"
17720 );
17721
17722 multi_buffer_editor.update(cx, |editor, cx| {
17723 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
17724 });
17725 assert_eq!(
17726 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17727 "\n\n\n\n4444\n5555\n6666\n\n",
17728 "After unfolding the second buffer, its text should be displayed"
17729 );
17730
17731 multi_buffer_editor.update(cx, |editor, cx| {
17732 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
17733 });
17734 assert_eq!(
17735 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17736 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
17737 "After unfolding the first buffer, its text should be displayed"
17738 );
17739
17740 multi_buffer_editor.update(cx, |editor, cx| {
17741 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
17742 });
17743 assert_eq!(
17744 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17745 full_text,
17746 "After unfolding all buffers, all original text should be displayed"
17747 );
17748}
17749
17750#[gpui::test]
17751async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
17752 init_test(cx, |_| {});
17753
17754 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
17755
17756 let fs = FakeFs::new(cx.executor());
17757 fs.insert_tree(
17758 path!("/a"),
17759 json!({
17760 "main.rs": sample_text,
17761 }),
17762 )
17763 .await;
17764 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17765 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17766 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17767 let worktree = project.update(cx, |project, cx| {
17768 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17769 assert_eq!(worktrees.len(), 1);
17770 worktrees.pop().unwrap()
17771 });
17772 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17773
17774 let buffer_1 = project
17775 .update(cx, |project, cx| {
17776 project.open_buffer((worktree_id, "main.rs"), cx)
17777 })
17778 .await
17779 .unwrap();
17780
17781 let multi_buffer = cx.new(|cx| {
17782 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17783 multi_buffer.push_excerpts(
17784 buffer_1.clone(),
17785 [ExcerptRange::new(
17786 Point::new(0, 0)
17787 ..Point::new(
17788 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
17789 0,
17790 ),
17791 )],
17792 cx,
17793 );
17794 multi_buffer
17795 });
17796 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17797 Editor::new(
17798 EditorMode::full(),
17799 multi_buffer,
17800 Some(project.clone()),
17801 window,
17802 cx,
17803 )
17804 });
17805
17806 let selection_range = Point::new(1, 0)..Point::new(2, 0);
17807 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17808 enum TestHighlight {}
17809 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
17810 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
17811 editor.highlight_text::<TestHighlight>(
17812 vec![highlight_range.clone()],
17813 HighlightStyle::color(Hsla::green()),
17814 cx,
17815 );
17816 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
17817 });
17818
17819 let full_text = format!("\n\n{sample_text}");
17820 assert_eq!(
17821 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17822 full_text,
17823 );
17824}
17825
17826#[gpui::test]
17827async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
17828 init_test(cx, |_| {});
17829 cx.update(|cx| {
17830 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
17831 "keymaps/default-linux.json",
17832 cx,
17833 )
17834 .unwrap();
17835 cx.bind_keys(default_key_bindings);
17836 });
17837
17838 let (editor, cx) = cx.add_window_view(|window, cx| {
17839 let multi_buffer = MultiBuffer::build_multi(
17840 [
17841 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
17842 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
17843 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
17844 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
17845 ],
17846 cx,
17847 );
17848 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
17849
17850 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
17851 // fold all but the second buffer, so that we test navigating between two
17852 // adjacent folded buffers, as well as folded buffers at the start and
17853 // end the multibuffer
17854 editor.fold_buffer(buffer_ids[0], cx);
17855 editor.fold_buffer(buffer_ids[2], cx);
17856 editor.fold_buffer(buffer_ids[3], cx);
17857
17858 editor
17859 });
17860 cx.simulate_resize(size(px(1000.), px(1000.)));
17861
17862 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
17863 cx.assert_excerpts_with_selections(indoc! {"
17864 [EXCERPT]
17865 ˇ[FOLDED]
17866 [EXCERPT]
17867 a1
17868 b1
17869 [EXCERPT]
17870 [FOLDED]
17871 [EXCERPT]
17872 [FOLDED]
17873 "
17874 });
17875 cx.simulate_keystroke("down");
17876 cx.assert_excerpts_with_selections(indoc! {"
17877 [EXCERPT]
17878 [FOLDED]
17879 [EXCERPT]
17880 ˇa1
17881 b1
17882 [EXCERPT]
17883 [FOLDED]
17884 [EXCERPT]
17885 [FOLDED]
17886 "
17887 });
17888 cx.simulate_keystroke("down");
17889 cx.assert_excerpts_with_selections(indoc! {"
17890 [EXCERPT]
17891 [FOLDED]
17892 [EXCERPT]
17893 a1
17894 ˇb1
17895 [EXCERPT]
17896 [FOLDED]
17897 [EXCERPT]
17898 [FOLDED]
17899 "
17900 });
17901 cx.simulate_keystroke("down");
17902 cx.assert_excerpts_with_selections(indoc! {"
17903 [EXCERPT]
17904 [FOLDED]
17905 [EXCERPT]
17906 a1
17907 b1
17908 ˇ[EXCERPT]
17909 [FOLDED]
17910 [EXCERPT]
17911 [FOLDED]
17912 "
17913 });
17914 cx.simulate_keystroke("down");
17915 cx.assert_excerpts_with_selections(indoc! {"
17916 [EXCERPT]
17917 [FOLDED]
17918 [EXCERPT]
17919 a1
17920 b1
17921 [EXCERPT]
17922 ˇ[FOLDED]
17923 [EXCERPT]
17924 [FOLDED]
17925 "
17926 });
17927 for _ in 0..5 {
17928 cx.simulate_keystroke("down");
17929 cx.assert_excerpts_with_selections(indoc! {"
17930 [EXCERPT]
17931 [FOLDED]
17932 [EXCERPT]
17933 a1
17934 b1
17935 [EXCERPT]
17936 [FOLDED]
17937 [EXCERPT]
17938 ˇ[FOLDED]
17939 "
17940 });
17941 }
17942
17943 cx.simulate_keystroke("up");
17944 cx.assert_excerpts_with_selections(indoc! {"
17945 [EXCERPT]
17946 [FOLDED]
17947 [EXCERPT]
17948 a1
17949 b1
17950 [EXCERPT]
17951 ˇ[FOLDED]
17952 [EXCERPT]
17953 [FOLDED]
17954 "
17955 });
17956 cx.simulate_keystroke("up");
17957 cx.assert_excerpts_with_selections(indoc! {"
17958 [EXCERPT]
17959 [FOLDED]
17960 [EXCERPT]
17961 a1
17962 b1
17963 ˇ[EXCERPT]
17964 [FOLDED]
17965 [EXCERPT]
17966 [FOLDED]
17967 "
17968 });
17969 cx.simulate_keystroke("up");
17970 cx.assert_excerpts_with_selections(indoc! {"
17971 [EXCERPT]
17972 [FOLDED]
17973 [EXCERPT]
17974 a1
17975 ˇb1
17976 [EXCERPT]
17977 [FOLDED]
17978 [EXCERPT]
17979 [FOLDED]
17980 "
17981 });
17982 cx.simulate_keystroke("up");
17983 cx.assert_excerpts_with_selections(indoc! {"
17984 [EXCERPT]
17985 [FOLDED]
17986 [EXCERPT]
17987 ˇa1
17988 b1
17989 [EXCERPT]
17990 [FOLDED]
17991 [EXCERPT]
17992 [FOLDED]
17993 "
17994 });
17995 for _ in 0..5 {
17996 cx.simulate_keystroke("up");
17997 cx.assert_excerpts_with_selections(indoc! {"
17998 [EXCERPT]
17999 ˇ[FOLDED]
18000 [EXCERPT]
18001 a1
18002 b1
18003 [EXCERPT]
18004 [FOLDED]
18005 [EXCERPT]
18006 [FOLDED]
18007 "
18008 });
18009 }
18010}
18011
18012#[gpui::test]
18013async fn test_inline_completion_text(cx: &mut TestAppContext) {
18014 init_test(cx, |_| {});
18015
18016 // Simple insertion
18017 assert_highlighted_edits(
18018 "Hello, world!",
18019 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
18020 true,
18021 cx,
18022 |highlighted_edits, cx| {
18023 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
18024 assert_eq!(highlighted_edits.highlights.len(), 1);
18025 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
18026 assert_eq!(
18027 highlighted_edits.highlights[0].1.background_color,
18028 Some(cx.theme().status().created_background)
18029 );
18030 },
18031 )
18032 .await;
18033
18034 // Replacement
18035 assert_highlighted_edits(
18036 "This is a test.",
18037 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
18038 false,
18039 cx,
18040 |highlighted_edits, cx| {
18041 assert_eq!(highlighted_edits.text, "That is a test.");
18042 assert_eq!(highlighted_edits.highlights.len(), 1);
18043 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
18044 assert_eq!(
18045 highlighted_edits.highlights[0].1.background_color,
18046 Some(cx.theme().status().created_background)
18047 );
18048 },
18049 )
18050 .await;
18051
18052 // Multiple edits
18053 assert_highlighted_edits(
18054 "Hello, world!",
18055 vec![
18056 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
18057 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
18058 ],
18059 false,
18060 cx,
18061 |highlighted_edits, cx| {
18062 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
18063 assert_eq!(highlighted_edits.highlights.len(), 2);
18064 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
18065 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
18066 assert_eq!(
18067 highlighted_edits.highlights[0].1.background_color,
18068 Some(cx.theme().status().created_background)
18069 );
18070 assert_eq!(
18071 highlighted_edits.highlights[1].1.background_color,
18072 Some(cx.theme().status().created_background)
18073 );
18074 },
18075 )
18076 .await;
18077
18078 // Multiple lines with edits
18079 assert_highlighted_edits(
18080 "First line\nSecond line\nThird line\nFourth line",
18081 vec![
18082 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
18083 (
18084 Point::new(2, 0)..Point::new(2, 10),
18085 "New third line".to_string(),
18086 ),
18087 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
18088 ],
18089 false,
18090 cx,
18091 |highlighted_edits, cx| {
18092 assert_eq!(
18093 highlighted_edits.text,
18094 "Second modified\nNew third line\nFourth updated line"
18095 );
18096 assert_eq!(highlighted_edits.highlights.len(), 3);
18097 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
18098 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
18099 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
18100 for highlight in &highlighted_edits.highlights {
18101 assert_eq!(
18102 highlight.1.background_color,
18103 Some(cx.theme().status().created_background)
18104 );
18105 }
18106 },
18107 )
18108 .await;
18109}
18110
18111#[gpui::test]
18112async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
18113 init_test(cx, |_| {});
18114
18115 // Deletion
18116 assert_highlighted_edits(
18117 "Hello, world!",
18118 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
18119 true,
18120 cx,
18121 |highlighted_edits, cx| {
18122 assert_eq!(highlighted_edits.text, "Hello, world!");
18123 assert_eq!(highlighted_edits.highlights.len(), 1);
18124 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
18125 assert_eq!(
18126 highlighted_edits.highlights[0].1.background_color,
18127 Some(cx.theme().status().deleted_background)
18128 );
18129 },
18130 )
18131 .await;
18132
18133 // Insertion
18134 assert_highlighted_edits(
18135 "Hello, world!",
18136 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
18137 true,
18138 cx,
18139 |highlighted_edits, cx| {
18140 assert_eq!(highlighted_edits.highlights.len(), 1);
18141 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
18142 assert_eq!(
18143 highlighted_edits.highlights[0].1.background_color,
18144 Some(cx.theme().status().created_background)
18145 );
18146 },
18147 )
18148 .await;
18149}
18150
18151async fn assert_highlighted_edits(
18152 text: &str,
18153 edits: Vec<(Range<Point>, String)>,
18154 include_deletions: bool,
18155 cx: &mut TestAppContext,
18156 assertion_fn: impl Fn(HighlightedText, &App),
18157) {
18158 let window = cx.add_window(|window, cx| {
18159 let buffer = MultiBuffer::build_simple(text, cx);
18160 Editor::new(EditorMode::full(), buffer, None, window, cx)
18161 });
18162 let cx = &mut VisualTestContext::from_window(*window, cx);
18163
18164 let (buffer, snapshot) = window
18165 .update(cx, |editor, _window, cx| {
18166 (
18167 editor.buffer().clone(),
18168 editor.buffer().read(cx).snapshot(cx),
18169 )
18170 })
18171 .unwrap();
18172
18173 let edits = edits
18174 .into_iter()
18175 .map(|(range, edit)| {
18176 (
18177 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
18178 edit,
18179 )
18180 })
18181 .collect::<Vec<_>>();
18182
18183 let text_anchor_edits = edits
18184 .clone()
18185 .into_iter()
18186 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
18187 .collect::<Vec<_>>();
18188
18189 let edit_preview = window
18190 .update(cx, |_, _window, cx| {
18191 buffer
18192 .read(cx)
18193 .as_singleton()
18194 .unwrap()
18195 .read(cx)
18196 .preview_edits(text_anchor_edits.into(), cx)
18197 })
18198 .unwrap()
18199 .await;
18200
18201 cx.update(|_window, cx| {
18202 let highlighted_edits = inline_completion_edit_text(
18203 &snapshot.as_singleton().unwrap().2,
18204 &edits,
18205 &edit_preview,
18206 include_deletions,
18207 cx,
18208 );
18209 assertion_fn(highlighted_edits, cx)
18210 });
18211}
18212
18213#[track_caller]
18214fn assert_breakpoint(
18215 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
18216 path: &Arc<Path>,
18217 expected: Vec<(u32, Breakpoint)>,
18218) {
18219 if expected.len() == 0usize {
18220 assert!(!breakpoints.contains_key(path), "{}", path.display());
18221 } else {
18222 let mut breakpoint = breakpoints
18223 .get(path)
18224 .unwrap()
18225 .into_iter()
18226 .map(|breakpoint| {
18227 (
18228 breakpoint.row,
18229 Breakpoint {
18230 message: breakpoint.message.clone(),
18231 state: breakpoint.state,
18232 condition: breakpoint.condition.clone(),
18233 hit_condition: breakpoint.hit_condition.clone(),
18234 },
18235 )
18236 })
18237 .collect::<Vec<_>>();
18238
18239 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
18240
18241 assert_eq!(expected, breakpoint);
18242 }
18243}
18244
18245fn add_log_breakpoint_at_cursor(
18246 editor: &mut Editor,
18247 log_message: &str,
18248 window: &mut Window,
18249 cx: &mut Context<Editor>,
18250) {
18251 let (anchor, bp) = editor
18252 .breakpoints_at_cursors(window, cx)
18253 .first()
18254 .and_then(|(anchor, bp)| {
18255 if let Some(bp) = bp {
18256 Some((*anchor, bp.clone()))
18257 } else {
18258 None
18259 }
18260 })
18261 .unwrap_or_else(|| {
18262 let cursor_position: Point = editor.selections.newest(cx).head();
18263
18264 let breakpoint_position = editor
18265 .snapshot(window, cx)
18266 .display_snapshot
18267 .buffer_snapshot
18268 .anchor_before(Point::new(cursor_position.row, 0));
18269
18270 (breakpoint_position, Breakpoint::new_log(&log_message))
18271 });
18272
18273 editor.edit_breakpoint_at_anchor(
18274 anchor,
18275 bp,
18276 BreakpointEditAction::EditLogMessage(log_message.into()),
18277 cx,
18278 );
18279}
18280
18281#[gpui::test]
18282async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
18283 init_test(cx, |_| {});
18284
18285 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18286 let fs = FakeFs::new(cx.executor());
18287 fs.insert_tree(
18288 path!("/a"),
18289 json!({
18290 "main.rs": sample_text,
18291 }),
18292 )
18293 .await;
18294 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18295 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18296 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18297
18298 let fs = FakeFs::new(cx.executor());
18299 fs.insert_tree(
18300 path!("/a"),
18301 json!({
18302 "main.rs": sample_text,
18303 }),
18304 )
18305 .await;
18306 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18307 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18308 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18309 let worktree_id = workspace
18310 .update(cx, |workspace, _window, cx| {
18311 workspace.project().update(cx, |project, cx| {
18312 project.worktrees(cx).next().unwrap().read(cx).id()
18313 })
18314 })
18315 .unwrap();
18316
18317 let buffer = project
18318 .update(cx, |project, cx| {
18319 project.open_buffer((worktree_id, "main.rs"), cx)
18320 })
18321 .await
18322 .unwrap();
18323
18324 let (editor, cx) = cx.add_window_view(|window, cx| {
18325 Editor::new(
18326 EditorMode::full(),
18327 MultiBuffer::build_from_buffer(buffer, cx),
18328 Some(project.clone()),
18329 window,
18330 cx,
18331 )
18332 });
18333
18334 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18335 let abs_path = project.read_with(cx, |project, cx| {
18336 project
18337 .absolute_path(&project_path, cx)
18338 .map(|path_buf| Arc::from(path_buf.to_owned()))
18339 .unwrap()
18340 });
18341
18342 // assert we can add breakpoint on the first line
18343 editor.update_in(cx, |editor, window, cx| {
18344 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18345 editor.move_to_end(&MoveToEnd, window, cx);
18346 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18347 });
18348
18349 let breakpoints = editor.update(cx, |editor, cx| {
18350 editor
18351 .breakpoint_store()
18352 .as_ref()
18353 .unwrap()
18354 .read(cx)
18355 .all_breakpoints(cx)
18356 .clone()
18357 });
18358
18359 assert_eq!(1, breakpoints.len());
18360 assert_breakpoint(
18361 &breakpoints,
18362 &abs_path,
18363 vec![
18364 (0, Breakpoint::new_standard()),
18365 (3, Breakpoint::new_standard()),
18366 ],
18367 );
18368
18369 editor.update_in(cx, |editor, window, cx| {
18370 editor.move_to_beginning(&MoveToBeginning, window, cx);
18371 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18372 });
18373
18374 let breakpoints = editor.update(cx, |editor, cx| {
18375 editor
18376 .breakpoint_store()
18377 .as_ref()
18378 .unwrap()
18379 .read(cx)
18380 .all_breakpoints(cx)
18381 .clone()
18382 });
18383
18384 assert_eq!(1, breakpoints.len());
18385 assert_breakpoint(
18386 &breakpoints,
18387 &abs_path,
18388 vec![(3, Breakpoint::new_standard())],
18389 );
18390
18391 editor.update_in(cx, |editor, window, cx| {
18392 editor.move_to_end(&MoveToEnd, window, cx);
18393 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18394 });
18395
18396 let breakpoints = editor.update(cx, |editor, cx| {
18397 editor
18398 .breakpoint_store()
18399 .as_ref()
18400 .unwrap()
18401 .read(cx)
18402 .all_breakpoints(cx)
18403 .clone()
18404 });
18405
18406 assert_eq!(0, breakpoints.len());
18407 assert_breakpoint(&breakpoints, &abs_path, vec![]);
18408}
18409
18410#[gpui::test]
18411async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
18412 init_test(cx, |_| {});
18413
18414 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18415
18416 let fs = FakeFs::new(cx.executor());
18417 fs.insert_tree(
18418 path!("/a"),
18419 json!({
18420 "main.rs": sample_text,
18421 }),
18422 )
18423 .await;
18424 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18425 let (workspace, cx) =
18426 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
18427
18428 let worktree_id = workspace.update(cx, |workspace, cx| {
18429 workspace.project().update(cx, |project, cx| {
18430 project.worktrees(cx).next().unwrap().read(cx).id()
18431 })
18432 });
18433
18434 let buffer = project
18435 .update(cx, |project, cx| {
18436 project.open_buffer((worktree_id, "main.rs"), cx)
18437 })
18438 .await
18439 .unwrap();
18440
18441 let (editor, cx) = cx.add_window_view(|window, cx| {
18442 Editor::new(
18443 EditorMode::full(),
18444 MultiBuffer::build_from_buffer(buffer, cx),
18445 Some(project.clone()),
18446 window,
18447 cx,
18448 )
18449 });
18450
18451 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18452 let abs_path = project.read_with(cx, |project, cx| {
18453 project
18454 .absolute_path(&project_path, cx)
18455 .map(|path_buf| Arc::from(path_buf.to_owned()))
18456 .unwrap()
18457 });
18458
18459 editor.update_in(cx, |editor, window, cx| {
18460 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
18461 });
18462
18463 let breakpoints = editor.update(cx, |editor, cx| {
18464 editor
18465 .breakpoint_store()
18466 .as_ref()
18467 .unwrap()
18468 .read(cx)
18469 .all_breakpoints(cx)
18470 .clone()
18471 });
18472
18473 assert_breakpoint(
18474 &breakpoints,
18475 &abs_path,
18476 vec![(0, Breakpoint::new_log("hello world"))],
18477 );
18478
18479 // Removing a log message from a log breakpoint should remove it
18480 editor.update_in(cx, |editor, window, cx| {
18481 add_log_breakpoint_at_cursor(editor, "", window, cx);
18482 });
18483
18484 let breakpoints = editor.update(cx, |editor, cx| {
18485 editor
18486 .breakpoint_store()
18487 .as_ref()
18488 .unwrap()
18489 .read(cx)
18490 .all_breakpoints(cx)
18491 .clone()
18492 });
18493
18494 assert_breakpoint(&breakpoints, &abs_path, vec![]);
18495
18496 editor.update_in(cx, |editor, window, cx| {
18497 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18498 editor.move_to_end(&MoveToEnd, window, cx);
18499 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18500 // Not adding a log message to a standard breakpoint shouldn't remove it
18501 add_log_breakpoint_at_cursor(editor, "", window, cx);
18502 });
18503
18504 let breakpoints = editor.update(cx, |editor, cx| {
18505 editor
18506 .breakpoint_store()
18507 .as_ref()
18508 .unwrap()
18509 .read(cx)
18510 .all_breakpoints(cx)
18511 .clone()
18512 });
18513
18514 assert_breakpoint(
18515 &breakpoints,
18516 &abs_path,
18517 vec![
18518 (0, Breakpoint::new_standard()),
18519 (3, Breakpoint::new_standard()),
18520 ],
18521 );
18522
18523 editor.update_in(cx, |editor, window, cx| {
18524 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
18525 });
18526
18527 let breakpoints = editor.update(cx, |editor, cx| {
18528 editor
18529 .breakpoint_store()
18530 .as_ref()
18531 .unwrap()
18532 .read(cx)
18533 .all_breakpoints(cx)
18534 .clone()
18535 });
18536
18537 assert_breakpoint(
18538 &breakpoints,
18539 &abs_path,
18540 vec![
18541 (0, Breakpoint::new_standard()),
18542 (3, Breakpoint::new_log("hello world")),
18543 ],
18544 );
18545
18546 editor.update_in(cx, |editor, window, cx| {
18547 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
18548 });
18549
18550 let breakpoints = editor.update(cx, |editor, cx| {
18551 editor
18552 .breakpoint_store()
18553 .as_ref()
18554 .unwrap()
18555 .read(cx)
18556 .all_breakpoints(cx)
18557 .clone()
18558 });
18559
18560 assert_breakpoint(
18561 &breakpoints,
18562 &abs_path,
18563 vec![
18564 (0, Breakpoint::new_standard()),
18565 (3, Breakpoint::new_log("hello Earth!!")),
18566 ],
18567 );
18568}
18569
18570/// This also tests that Editor::breakpoint_at_cursor_head is working properly
18571/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
18572/// or when breakpoints were placed out of order. This tests for a regression too
18573#[gpui::test]
18574async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
18575 init_test(cx, |_| {});
18576
18577 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18578 let fs = FakeFs::new(cx.executor());
18579 fs.insert_tree(
18580 path!("/a"),
18581 json!({
18582 "main.rs": sample_text,
18583 }),
18584 )
18585 .await;
18586 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18587 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18588 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18589
18590 let fs = FakeFs::new(cx.executor());
18591 fs.insert_tree(
18592 path!("/a"),
18593 json!({
18594 "main.rs": sample_text,
18595 }),
18596 )
18597 .await;
18598 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18599 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18600 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18601 let worktree_id = workspace
18602 .update(cx, |workspace, _window, cx| {
18603 workspace.project().update(cx, |project, cx| {
18604 project.worktrees(cx).next().unwrap().read(cx).id()
18605 })
18606 })
18607 .unwrap();
18608
18609 let buffer = project
18610 .update(cx, |project, cx| {
18611 project.open_buffer((worktree_id, "main.rs"), cx)
18612 })
18613 .await
18614 .unwrap();
18615
18616 let (editor, cx) = cx.add_window_view(|window, cx| {
18617 Editor::new(
18618 EditorMode::full(),
18619 MultiBuffer::build_from_buffer(buffer, cx),
18620 Some(project.clone()),
18621 window,
18622 cx,
18623 )
18624 });
18625
18626 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18627 let abs_path = project.read_with(cx, |project, cx| {
18628 project
18629 .absolute_path(&project_path, cx)
18630 .map(|path_buf| Arc::from(path_buf.to_owned()))
18631 .unwrap()
18632 });
18633
18634 // assert we can add breakpoint on the first line
18635 editor.update_in(cx, |editor, window, cx| {
18636 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18637 editor.move_to_end(&MoveToEnd, window, cx);
18638 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18639 editor.move_up(&MoveUp, window, cx);
18640 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18641 });
18642
18643 let breakpoints = editor.update(cx, |editor, cx| {
18644 editor
18645 .breakpoint_store()
18646 .as_ref()
18647 .unwrap()
18648 .read(cx)
18649 .all_breakpoints(cx)
18650 .clone()
18651 });
18652
18653 assert_eq!(1, breakpoints.len());
18654 assert_breakpoint(
18655 &breakpoints,
18656 &abs_path,
18657 vec![
18658 (0, Breakpoint::new_standard()),
18659 (2, Breakpoint::new_standard()),
18660 (3, Breakpoint::new_standard()),
18661 ],
18662 );
18663
18664 editor.update_in(cx, |editor, window, cx| {
18665 editor.move_to_beginning(&MoveToBeginning, window, cx);
18666 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18667 editor.move_to_end(&MoveToEnd, window, cx);
18668 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18669 // Disabling a breakpoint that doesn't exist should do nothing
18670 editor.move_up(&MoveUp, window, cx);
18671 editor.move_up(&MoveUp, window, cx);
18672 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18673 });
18674
18675 let breakpoints = editor.update(cx, |editor, cx| {
18676 editor
18677 .breakpoint_store()
18678 .as_ref()
18679 .unwrap()
18680 .read(cx)
18681 .all_breakpoints(cx)
18682 .clone()
18683 });
18684
18685 let disable_breakpoint = {
18686 let mut bp = Breakpoint::new_standard();
18687 bp.state = BreakpointState::Disabled;
18688 bp
18689 };
18690
18691 assert_eq!(1, breakpoints.len());
18692 assert_breakpoint(
18693 &breakpoints,
18694 &abs_path,
18695 vec![
18696 (0, disable_breakpoint.clone()),
18697 (2, Breakpoint::new_standard()),
18698 (3, disable_breakpoint.clone()),
18699 ],
18700 );
18701
18702 editor.update_in(cx, |editor, window, cx| {
18703 editor.move_to_beginning(&MoveToBeginning, window, cx);
18704 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
18705 editor.move_to_end(&MoveToEnd, window, cx);
18706 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
18707 editor.move_up(&MoveUp, window, cx);
18708 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18709 });
18710
18711 let breakpoints = editor.update(cx, |editor, cx| {
18712 editor
18713 .breakpoint_store()
18714 .as_ref()
18715 .unwrap()
18716 .read(cx)
18717 .all_breakpoints(cx)
18718 .clone()
18719 });
18720
18721 assert_eq!(1, breakpoints.len());
18722 assert_breakpoint(
18723 &breakpoints,
18724 &abs_path,
18725 vec![
18726 (0, Breakpoint::new_standard()),
18727 (2, disable_breakpoint),
18728 (3, Breakpoint::new_standard()),
18729 ],
18730 );
18731}
18732
18733#[gpui::test]
18734async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
18735 init_test(cx, |_| {});
18736 let capabilities = lsp::ServerCapabilities {
18737 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
18738 prepare_provider: Some(true),
18739 work_done_progress_options: Default::default(),
18740 })),
18741 ..Default::default()
18742 };
18743 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
18744
18745 cx.set_state(indoc! {"
18746 struct Fˇoo {}
18747 "});
18748
18749 cx.update_editor(|editor, _, cx| {
18750 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
18751 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
18752 editor.highlight_background::<DocumentHighlightRead>(
18753 &[highlight_range],
18754 |c| c.editor_document_highlight_read_background,
18755 cx,
18756 );
18757 });
18758
18759 let mut prepare_rename_handler = cx
18760 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
18761 move |_, _, _| async move {
18762 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
18763 start: lsp::Position {
18764 line: 0,
18765 character: 7,
18766 },
18767 end: lsp::Position {
18768 line: 0,
18769 character: 10,
18770 },
18771 })))
18772 },
18773 );
18774 let prepare_rename_task = cx
18775 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
18776 .expect("Prepare rename was not started");
18777 prepare_rename_handler.next().await.unwrap();
18778 prepare_rename_task.await.expect("Prepare rename failed");
18779
18780 let mut rename_handler =
18781 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
18782 let edit = lsp::TextEdit {
18783 range: lsp::Range {
18784 start: lsp::Position {
18785 line: 0,
18786 character: 7,
18787 },
18788 end: lsp::Position {
18789 line: 0,
18790 character: 10,
18791 },
18792 },
18793 new_text: "FooRenamed".to_string(),
18794 };
18795 Ok(Some(lsp::WorkspaceEdit::new(
18796 // Specify the same edit twice
18797 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
18798 )))
18799 });
18800 let rename_task = cx
18801 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
18802 .expect("Confirm rename was not started");
18803 rename_handler.next().await.unwrap();
18804 rename_task.await.expect("Confirm rename failed");
18805 cx.run_until_parked();
18806
18807 // Despite two edits, only one is actually applied as those are identical
18808 cx.assert_editor_state(indoc! {"
18809 struct FooRenamedˇ {}
18810 "});
18811}
18812
18813#[gpui::test]
18814async fn test_rename_without_prepare(cx: &mut TestAppContext) {
18815 init_test(cx, |_| {});
18816 // These capabilities indicate that the server does not support prepare rename.
18817 let capabilities = lsp::ServerCapabilities {
18818 rename_provider: Some(lsp::OneOf::Left(true)),
18819 ..Default::default()
18820 };
18821 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
18822
18823 cx.set_state(indoc! {"
18824 struct Fˇoo {}
18825 "});
18826
18827 cx.update_editor(|editor, _window, cx| {
18828 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
18829 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
18830 editor.highlight_background::<DocumentHighlightRead>(
18831 &[highlight_range],
18832 |c| c.editor_document_highlight_read_background,
18833 cx,
18834 );
18835 });
18836
18837 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
18838 .expect("Prepare rename was not started")
18839 .await
18840 .expect("Prepare rename failed");
18841
18842 let mut rename_handler =
18843 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
18844 let edit = lsp::TextEdit {
18845 range: lsp::Range {
18846 start: lsp::Position {
18847 line: 0,
18848 character: 7,
18849 },
18850 end: lsp::Position {
18851 line: 0,
18852 character: 10,
18853 },
18854 },
18855 new_text: "FooRenamed".to_string(),
18856 };
18857 Ok(Some(lsp::WorkspaceEdit::new(
18858 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
18859 )))
18860 });
18861 let rename_task = cx
18862 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
18863 .expect("Confirm rename was not started");
18864 rename_handler.next().await.unwrap();
18865 rename_task.await.expect("Confirm rename failed");
18866 cx.run_until_parked();
18867
18868 // Correct range is renamed, as `surrounding_word` is used to find it.
18869 cx.assert_editor_state(indoc! {"
18870 struct FooRenamedˇ {}
18871 "});
18872}
18873
18874#[gpui::test]
18875async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
18876 init_test(cx, |_| {});
18877 let mut cx = EditorTestContext::new(cx).await;
18878
18879 let language = Arc::new(
18880 Language::new(
18881 LanguageConfig::default(),
18882 Some(tree_sitter_html::LANGUAGE.into()),
18883 )
18884 .with_brackets_query(
18885 r#"
18886 ("<" @open "/>" @close)
18887 ("</" @open ">" @close)
18888 ("<" @open ">" @close)
18889 ("\"" @open "\"" @close)
18890 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
18891 "#,
18892 )
18893 .unwrap(),
18894 );
18895 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
18896
18897 cx.set_state(indoc! {"
18898 <span>ˇ</span>
18899 "});
18900 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18901 cx.assert_editor_state(indoc! {"
18902 <span>
18903 ˇ
18904 </span>
18905 "});
18906
18907 cx.set_state(indoc! {"
18908 <span><span></span>ˇ</span>
18909 "});
18910 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18911 cx.assert_editor_state(indoc! {"
18912 <span><span></span>
18913 ˇ</span>
18914 "});
18915
18916 cx.set_state(indoc! {"
18917 <span>ˇ
18918 </span>
18919 "});
18920 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18921 cx.assert_editor_state(indoc! {"
18922 <span>
18923 ˇ
18924 </span>
18925 "});
18926}
18927
18928#[gpui::test(iterations = 10)]
18929async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
18930 init_test(cx, |_| {});
18931
18932 let fs = FakeFs::new(cx.executor());
18933 fs.insert_tree(
18934 path!("/dir"),
18935 json!({
18936 "a.ts": "a",
18937 }),
18938 )
18939 .await;
18940
18941 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
18942 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18943 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18944
18945 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18946 language_registry.add(Arc::new(Language::new(
18947 LanguageConfig {
18948 name: "TypeScript".into(),
18949 matcher: LanguageMatcher {
18950 path_suffixes: vec!["ts".to_string()],
18951 ..Default::default()
18952 },
18953 ..Default::default()
18954 },
18955 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18956 )));
18957 let mut fake_language_servers = language_registry.register_fake_lsp(
18958 "TypeScript",
18959 FakeLspAdapter {
18960 capabilities: lsp::ServerCapabilities {
18961 code_lens_provider: Some(lsp::CodeLensOptions {
18962 resolve_provider: Some(true),
18963 }),
18964 execute_command_provider: Some(lsp::ExecuteCommandOptions {
18965 commands: vec!["_the/command".to_string()],
18966 ..lsp::ExecuteCommandOptions::default()
18967 }),
18968 ..lsp::ServerCapabilities::default()
18969 },
18970 ..FakeLspAdapter::default()
18971 },
18972 );
18973
18974 let (buffer, _handle) = project
18975 .update(cx, |p, cx| {
18976 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
18977 })
18978 .await
18979 .unwrap();
18980 cx.executor().run_until_parked();
18981
18982 let fake_server = fake_language_servers.next().await.unwrap();
18983
18984 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
18985 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
18986 drop(buffer_snapshot);
18987 let actions = cx
18988 .update_window(*workspace, |_, window, cx| {
18989 project.code_actions(&buffer, anchor..anchor, window, cx)
18990 })
18991 .unwrap();
18992
18993 fake_server
18994 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
18995 Ok(Some(vec![
18996 lsp::CodeLens {
18997 range: lsp::Range::default(),
18998 command: Some(lsp::Command {
18999 title: "Code lens command".to_owned(),
19000 command: "_the/command".to_owned(),
19001 arguments: None,
19002 }),
19003 data: None,
19004 },
19005 lsp::CodeLens {
19006 range: lsp::Range::default(),
19007 command: Some(lsp::Command {
19008 title: "Command not in capabilities".to_owned(),
19009 command: "not in capabilities".to_owned(),
19010 arguments: None,
19011 }),
19012 data: None,
19013 },
19014 lsp::CodeLens {
19015 range: lsp::Range {
19016 start: lsp::Position {
19017 line: 1,
19018 character: 1,
19019 },
19020 end: lsp::Position {
19021 line: 1,
19022 character: 1,
19023 },
19024 },
19025 command: Some(lsp::Command {
19026 title: "Command not in range".to_owned(),
19027 command: "_the/command".to_owned(),
19028 arguments: None,
19029 }),
19030 data: None,
19031 },
19032 ]))
19033 })
19034 .next()
19035 .await;
19036
19037 let actions = actions.await.unwrap();
19038 assert_eq!(
19039 actions.len(),
19040 1,
19041 "Should have only one valid action for the 0..0 range"
19042 );
19043 let action = actions[0].clone();
19044 let apply = project.update(cx, |project, cx| {
19045 project.apply_code_action(buffer.clone(), action, true, cx)
19046 });
19047
19048 // Resolving the code action does not populate its edits. In absence of
19049 // edits, we must execute the given command.
19050 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
19051 |mut lens, _| async move {
19052 let lens_command = lens.command.as_mut().expect("should have a command");
19053 assert_eq!(lens_command.title, "Code lens command");
19054 lens_command.arguments = Some(vec![json!("the-argument")]);
19055 Ok(lens)
19056 },
19057 );
19058
19059 // While executing the command, the language server sends the editor
19060 // a `workspaceEdit` request.
19061 fake_server
19062 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
19063 let fake = fake_server.clone();
19064 move |params, _| {
19065 assert_eq!(params.command, "_the/command");
19066 let fake = fake.clone();
19067 async move {
19068 fake.server
19069 .request::<lsp::request::ApplyWorkspaceEdit>(
19070 lsp::ApplyWorkspaceEditParams {
19071 label: None,
19072 edit: lsp::WorkspaceEdit {
19073 changes: Some(
19074 [(
19075 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
19076 vec![lsp::TextEdit {
19077 range: lsp::Range::new(
19078 lsp::Position::new(0, 0),
19079 lsp::Position::new(0, 0),
19080 ),
19081 new_text: "X".into(),
19082 }],
19083 )]
19084 .into_iter()
19085 .collect(),
19086 ),
19087 ..Default::default()
19088 },
19089 },
19090 )
19091 .await
19092 .unwrap();
19093 Ok(Some(json!(null)))
19094 }
19095 }
19096 })
19097 .next()
19098 .await;
19099
19100 // Applying the code lens command returns a project transaction containing the edits
19101 // sent by the language server in its `workspaceEdit` request.
19102 let transaction = apply.await.unwrap();
19103 assert!(transaction.0.contains_key(&buffer));
19104 buffer.update(cx, |buffer, cx| {
19105 assert_eq!(buffer.text(), "Xa");
19106 buffer.undo(cx);
19107 assert_eq!(buffer.text(), "a");
19108 });
19109}
19110
19111#[gpui::test]
19112async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
19113 init_test(cx, |_| {});
19114
19115 let fs = FakeFs::new(cx.executor());
19116 let main_text = r#"fn main() {
19117println!("1");
19118println!("2");
19119println!("3");
19120println!("4");
19121println!("5");
19122}"#;
19123 let lib_text = "mod foo {}";
19124 fs.insert_tree(
19125 path!("/a"),
19126 json!({
19127 "lib.rs": lib_text,
19128 "main.rs": main_text,
19129 }),
19130 )
19131 .await;
19132
19133 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19134 let (workspace, cx) =
19135 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19136 let worktree_id = workspace.update(cx, |workspace, cx| {
19137 workspace.project().update(cx, |project, cx| {
19138 project.worktrees(cx).next().unwrap().read(cx).id()
19139 })
19140 });
19141
19142 let expected_ranges = vec![
19143 Point::new(0, 0)..Point::new(0, 0),
19144 Point::new(1, 0)..Point::new(1, 1),
19145 Point::new(2, 0)..Point::new(2, 2),
19146 Point::new(3, 0)..Point::new(3, 3),
19147 ];
19148
19149 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
19150 let editor_1 = workspace
19151 .update_in(cx, |workspace, window, cx| {
19152 workspace.open_path(
19153 (worktree_id, "main.rs"),
19154 Some(pane_1.downgrade()),
19155 true,
19156 window,
19157 cx,
19158 )
19159 })
19160 .unwrap()
19161 .await
19162 .downcast::<Editor>()
19163 .unwrap();
19164 pane_1.update(cx, |pane, cx| {
19165 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19166 open_editor.update(cx, |editor, cx| {
19167 assert_eq!(
19168 editor.display_text(cx),
19169 main_text,
19170 "Original main.rs text on initial open",
19171 );
19172 assert_eq!(
19173 editor
19174 .selections
19175 .all::<Point>(cx)
19176 .into_iter()
19177 .map(|s| s.range())
19178 .collect::<Vec<_>>(),
19179 vec![Point::zero()..Point::zero()],
19180 "Default selections on initial open",
19181 );
19182 })
19183 });
19184 editor_1.update_in(cx, |editor, window, cx| {
19185 editor.change_selections(None, window, cx, |s| {
19186 s.select_ranges(expected_ranges.clone());
19187 });
19188 });
19189
19190 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
19191 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
19192 });
19193 let editor_2 = workspace
19194 .update_in(cx, |workspace, window, cx| {
19195 workspace.open_path(
19196 (worktree_id, "main.rs"),
19197 Some(pane_2.downgrade()),
19198 true,
19199 window,
19200 cx,
19201 )
19202 })
19203 .unwrap()
19204 .await
19205 .downcast::<Editor>()
19206 .unwrap();
19207 pane_2.update(cx, |pane, cx| {
19208 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19209 open_editor.update(cx, |editor, cx| {
19210 assert_eq!(
19211 editor.display_text(cx),
19212 main_text,
19213 "Original main.rs text on initial open in another panel",
19214 );
19215 assert_eq!(
19216 editor
19217 .selections
19218 .all::<Point>(cx)
19219 .into_iter()
19220 .map(|s| s.range())
19221 .collect::<Vec<_>>(),
19222 vec![Point::zero()..Point::zero()],
19223 "Default selections on initial open in another panel",
19224 );
19225 })
19226 });
19227
19228 editor_2.update_in(cx, |editor, window, cx| {
19229 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
19230 });
19231
19232 let _other_editor_1 = workspace
19233 .update_in(cx, |workspace, window, cx| {
19234 workspace.open_path(
19235 (worktree_id, "lib.rs"),
19236 Some(pane_1.downgrade()),
19237 true,
19238 window,
19239 cx,
19240 )
19241 })
19242 .unwrap()
19243 .await
19244 .downcast::<Editor>()
19245 .unwrap();
19246 pane_1
19247 .update_in(cx, |pane, window, cx| {
19248 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
19249 .unwrap()
19250 })
19251 .await
19252 .unwrap();
19253 drop(editor_1);
19254 pane_1.update(cx, |pane, cx| {
19255 pane.active_item()
19256 .unwrap()
19257 .downcast::<Editor>()
19258 .unwrap()
19259 .update(cx, |editor, cx| {
19260 assert_eq!(
19261 editor.display_text(cx),
19262 lib_text,
19263 "Other file should be open and active",
19264 );
19265 });
19266 assert_eq!(pane.items().count(), 1, "No other editors should be open");
19267 });
19268
19269 let _other_editor_2 = workspace
19270 .update_in(cx, |workspace, window, cx| {
19271 workspace.open_path(
19272 (worktree_id, "lib.rs"),
19273 Some(pane_2.downgrade()),
19274 true,
19275 window,
19276 cx,
19277 )
19278 })
19279 .unwrap()
19280 .await
19281 .downcast::<Editor>()
19282 .unwrap();
19283 pane_2
19284 .update_in(cx, |pane, window, cx| {
19285 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
19286 .unwrap()
19287 })
19288 .await
19289 .unwrap();
19290 drop(editor_2);
19291 pane_2.update(cx, |pane, cx| {
19292 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19293 open_editor.update(cx, |editor, cx| {
19294 assert_eq!(
19295 editor.display_text(cx),
19296 lib_text,
19297 "Other file should be open and active in another panel too",
19298 );
19299 });
19300 assert_eq!(
19301 pane.items().count(),
19302 1,
19303 "No other editors should be open in another pane",
19304 );
19305 });
19306
19307 let _editor_1_reopened = workspace
19308 .update_in(cx, |workspace, window, cx| {
19309 workspace.open_path(
19310 (worktree_id, "main.rs"),
19311 Some(pane_1.downgrade()),
19312 true,
19313 window,
19314 cx,
19315 )
19316 })
19317 .unwrap()
19318 .await
19319 .downcast::<Editor>()
19320 .unwrap();
19321 let _editor_2_reopened = workspace
19322 .update_in(cx, |workspace, window, cx| {
19323 workspace.open_path(
19324 (worktree_id, "main.rs"),
19325 Some(pane_2.downgrade()),
19326 true,
19327 window,
19328 cx,
19329 )
19330 })
19331 .unwrap()
19332 .await
19333 .downcast::<Editor>()
19334 .unwrap();
19335 pane_1.update(cx, |pane, cx| {
19336 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19337 open_editor.update(cx, |editor, cx| {
19338 assert_eq!(
19339 editor.display_text(cx),
19340 main_text,
19341 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
19342 );
19343 assert_eq!(
19344 editor
19345 .selections
19346 .all::<Point>(cx)
19347 .into_iter()
19348 .map(|s| s.range())
19349 .collect::<Vec<_>>(),
19350 expected_ranges,
19351 "Previous editor in the 1st panel had selections and should get them restored on reopen",
19352 );
19353 })
19354 });
19355 pane_2.update(cx, |pane, cx| {
19356 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19357 open_editor.update(cx, |editor, cx| {
19358 assert_eq!(
19359 editor.display_text(cx),
19360 r#"fn main() {
19361⋯rintln!("1");
19362⋯intln!("2");
19363⋯ntln!("3");
19364println!("4");
19365println!("5");
19366}"#,
19367 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
19368 );
19369 assert_eq!(
19370 editor
19371 .selections
19372 .all::<Point>(cx)
19373 .into_iter()
19374 .map(|s| s.range())
19375 .collect::<Vec<_>>(),
19376 vec![Point::zero()..Point::zero()],
19377 "Previous editor in the 2nd pane had no selections changed hence should restore none",
19378 );
19379 })
19380 });
19381}
19382
19383#[gpui::test]
19384async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
19385 init_test(cx, |_| {});
19386
19387 let fs = FakeFs::new(cx.executor());
19388 let main_text = r#"fn main() {
19389println!("1");
19390println!("2");
19391println!("3");
19392println!("4");
19393println!("5");
19394}"#;
19395 let lib_text = "mod foo {}";
19396 fs.insert_tree(
19397 path!("/a"),
19398 json!({
19399 "lib.rs": lib_text,
19400 "main.rs": main_text,
19401 }),
19402 )
19403 .await;
19404
19405 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19406 let (workspace, cx) =
19407 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19408 let worktree_id = workspace.update(cx, |workspace, cx| {
19409 workspace.project().update(cx, |project, cx| {
19410 project.worktrees(cx).next().unwrap().read(cx).id()
19411 })
19412 });
19413
19414 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
19415 let editor = workspace
19416 .update_in(cx, |workspace, window, cx| {
19417 workspace.open_path(
19418 (worktree_id, "main.rs"),
19419 Some(pane.downgrade()),
19420 true,
19421 window,
19422 cx,
19423 )
19424 })
19425 .unwrap()
19426 .await
19427 .downcast::<Editor>()
19428 .unwrap();
19429 pane.update(cx, |pane, cx| {
19430 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19431 open_editor.update(cx, |editor, cx| {
19432 assert_eq!(
19433 editor.display_text(cx),
19434 main_text,
19435 "Original main.rs text on initial open",
19436 );
19437 })
19438 });
19439 editor.update_in(cx, |editor, window, cx| {
19440 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
19441 });
19442
19443 cx.update_global(|store: &mut SettingsStore, cx| {
19444 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
19445 s.restore_on_file_reopen = Some(false);
19446 });
19447 });
19448 editor.update_in(cx, |editor, window, cx| {
19449 editor.fold_ranges(
19450 vec![
19451 Point::new(1, 0)..Point::new(1, 1),
19452 Point::new(2, 0)..Point::new(2, 2),
19453 Point::new(3, 0)..Point::new(3, 3),
19454 ],
19455 false,
19456 window,
19457 cx,
19458 );
19459 });
19460 pane.update_in(cx, |pane, window, cx| {
19461 pane.close_all_items(&CloseAllItems::default(), window, cx)
19462 .unwrap()
19463 })
19464 .await
19465 .unwrap();
19466 pane.update(cx, |pane, _| {
19467 assert!(pane.active_item().is_none());
19468 });
19469 cx.update_global(|store: &mut SettingsStore, cx| {
19470 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
19471 s.restore_on_file_reopen = Some(true);
19472 });
19473 });
19474
19475 let _editor_reopened = workspace
19476 .update_in(cx, |workspace, window, cx| {
19477 workspace.open_path(
19478 (worktree_id, "main.rs"),
19479 Some(pane.downgrade()),
19480 true,
19481 window,
19482 cx,
19483 )
19484 })
19485 .unwrap()
19486 .await
19487 .downcast::<Editor>()
19488 .unwrap();
19489 pane.update(cx, |pane, cx| {
19490 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19491 open_editor.update(cx, |editor, cx| {
19492 assert_eq!(
19493 editor.display_text(cx),
19494 main_text,
19495 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
19496 );
19497 })
19498 });
19499}
19500
19501#[gpui::test]
19502async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
19503 struct EmptyModalView {
19504 focus_handle: gpui::FocusHandle,
19505 }
19506 impl EventEmitter<DismissEvent> for EmptyModalView {}
19507 impl Render for EmptyModalView {
19508 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
19509 div()
19510 }
19511 }
19512 impl Focusable for EmptyModalView {
19513 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
19514 self.focus_handle.clone()
19515 }
19516 }
19517 impl workspace::ModalView for EmptyModalView {}
19518 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
19519 EmptyModalView {
19520 focus_handle: cx.focus_handle(),
19521 }
19522 }
19523
19524 init_test(cx, |_| {});
19525
19526 let fs = FakeFs::new(cx.executor());
19527 let project = Project::test(fs, [], cx).await;
19528 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19529 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
19530 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19531 let editor = cx.new_window_entity(|window, cx| {
19532 Editor::new(
19533 EditorMode::full(),
19534 buffer,
19535 Some(project.clone()),
19536 window,
19537 cx,
19538 )
19539 });
19540 workspace
19541 .update(cx, |workspace, window, cx| {
19542 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
19543 })
19544 .unwrap();
19545 editor.update_in(cx, |editor, window, cx| {
19546 editor.open_context_menu(&OpenContextMenu, window, cx);
19547 assert!(editor.mouse_context_menu.is_some());
19548 });
19549 workspace
19550 .update(cx, |workspace, window, cx| {
19551 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
19552 })
19553 .unwrap();
19554 cx.read(|cx| {
19555 assert!(editor.read(cx).mouse_context_menu.is_none());
19556 });
19557}
19558
19559#[gpui::test]
19560async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
19561 init_test(cx, |_| {});
19562
19563 let fs = FakeFs::new(cx.executor());
19564 fs.insert_file(path!("/file.html"), Default::default())
19565 .await;
19566
19567 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
19568
19569 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19570 let html_language = Arc::new(Language::new(
19571 LanguageConfig {
19572 name: "HTML".into(),
19573 matcher: LanguageMatcher {
19574 path_suffixes: vec!["html".to_string()],
19575 ..LanguageMatcher::default()
19576 },
19577 brackets: BracketPairConfig {
19578 pairs: vec![BracketPair {
19579 start: "<".into(),
19580 end: ">".into(),
19581 close: true,
19582 ..Default::default()
19583 }],
19584 ..Default::default()
19585 },
19586 ..Default::default()
19587 },
19588 Some(tree_sitter_html::LANGUAGE.into()),
19589 ));
19590 language_registry.add(html_language);
19591 let mut fake_servers = language_registry.register_fake_lsp(
19592 "HTML",
19593 FakeLspAdapter {
19594 capabilities: lsp::ServerCapabilities {
19595 completion_provider: Some(lsp::CompletionOptions {
19596 resolve_provider: Some(true),
19597 ..Default::default()
19598 }),
19599 ..Default::default()
19600 },
19601 ..Default::default()
19602 },
19603 );
19604
19605 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19606 let cx = &mut VisualTestContext::from_window(*workspace, cx);
19607
19608 let worktree_id = workspace
19609 .update(cx, |workspace, _window, cx| {
19610 workspace.project().update(cx, |project, cx| {
19611 project.worktrees(cx).next().unwrap().read(cx).id()
19612 })
19613 })
19614 .unwrap();
19615 project
19616 .update(cx, |project, cx| {
19617 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
19618 })
19619 .await
19620 .unwrap();
19621 let editor = workspace
19622 .update(cx, |workspace, window, cx| {
19623 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
19624 })
19625 .unwrap()
19626 .await
19627 .unwrap()
19628 .downcast::<Editor>()
19629 .unwrap();
19630
19631 let fake_server = fake_servers.next().await.unwrap();
19632 editor.update_in(cx, |editor, window, cx| {
19633 editor.set_text("<ad></ad>", window, cx);
19634 editor.change_selections(None, window, cx, |selections| {
19635 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
19636 });
19637 let Some((buffer, _)) = editor
19638 .buffer
19639 .read(cx)
19640 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
19641 else {
19642 panic!("Failed to get buffer for selection position");
19643 };
19644 let buffer = buffer.read(cx);
19645 let buffer_id = buffer.remote_id();
19646 let opening_range =
19647 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
19648 let closing_range =
19649 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
19650 let mut linked_ranges = HashMap::default();
19651 linked_ranges.insert(
19652 buffer_id,
19653 vec![(opening_range.clone(), vec![closing_range.clone()])],
19654 );
19655 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
19656 });
19657 let mut completion_handle =
19658 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
19659 Ok(Some(lsp::CompletionResponse::Array(vec![
19660 lsp::CompletionItem {
19661 label: "head".to_string(),
19662 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
19663 lsp::InsertReplaceEdit {
19664 new_text: "head".to_string(),
19665 insert: lsp::Range::new(
19666 lsp::Position::new(0, 1),
19667 lsp::Position::new(0, 3),
19668 ),
19669 replace: lsp::Range::new(
19670 lsp::Position::new(0, 1),
19671 lsp::Position::new(0, 3),
19672 ),
19673 },
19674 )),
19675 ..Default::default()
19676 },
19677 ])))
19678 });
19679 editor.update_in(cx, |editor, window, cx| {
19680 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
19681 });
19682 cx.run_until_parked();
19683 completion_handle.next().await.unwrap();
19684 editor.update(cx, |editor, _| {
19685 assert!(
19686 editor.context_menu_visible(),
19687 "Completion menu should be visible"
19688 );
19689 });
19690 editor.update_in(cx, |editor, window, cx| {
19691 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
19692 });
19693 cx.executor().run_until_parked();
19694 editor.update(cx, |editor, cx| {
19695 assert_eq!(editor.text(cx), "<head></head>");
19696 });
19697}
19698
19699fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
19700 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
19701 point..point
19702}
19703
19704fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
19705 let (text, ranges) = marked_text_ranges(marked_text, true);
19706 assert_eq!(editor.text(cx), text);
19707 assert_eq!(
19708 editor.selections.ranges(cx),
19709 ranges,
19710 "Assert selections are {}",
19711 marked_text
19712 );
19713}
19714
19715pub fn handle_signature_help_request(
19716 cx: &mut EditorLspTestContext,
19717 mocked_response: lsp::SignatureHelp,
19718) -> impl Future<Output = ()> + use<> {
19719 let mut request =
19720 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
19721 let mocked_response = mocked_response.clone();
19722 async move { Ok(Some(mocked_response)) }
19723 });
19724
19725 async move {
19726 request.next().await;
19727 }
19728}
19729
19730/// Handle completion request passing a marked string specifying where the completion
19731/// should be triggered from using '|' character, what range should be replaced, and what completions
19732/// should be returned using '<' and '>' to delimit the range.
19733///
19734/// Also see `handle_completion_request_with_insert_and_replace`.
19735#[track_caller]
19736pub fn handle_completion_request(
19737 cx: &mut EditorLspTestContext,
19738 marked_string: &str,
19739 completions: Vec<&'static str>,
19740 counter: Arc<AtomicUsize>,
19741) -> impl Future<Output = ()> {
19742 let complete_from_marker: TextRangeMarker = '|'.into();
19743 let replace_range_marker: TextRangeMarker = ('<', '>').into();
19744 let (_, mut marked_ranges) = marked_text_ranges_by(
19745 marked_string,
19746 vec![complete_from_marker.clone(), replace_range_marker.clone()],
19747 );
19748
19749 let complete_from_position =
19750 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
19751 let replace_range =
19752 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
19753
19754 let mut request =
19755 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
19756 let completions = completions.clone();
19757 counter.fetch_add(1, atomic::Ordering::Release);
19758 async move {
19759 assert_eq!(params.text_document_position.text_document.uri, url.clone());
19760 assert_eq!(
19761 params.text_document_position.position,
19762 complete_from_position
19763 );
19764 Ok(Some(lsp::CompletionResponse::Array(
19765 completions
19766 .iter()
19767 .map(|completion_text| lsp::CompletionItem {
19768 label: completion_text.to_string(),
19769 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
19770 range: replace_range,
19771 new_text: completion_text.to_string(),
19772 })),
19773 ..Default::default()
19774 })
19775 .collect(),
19776 )))
19777 }
19778 });
19779
19780 async move {
19781 request.next().await;
19782 }
19783}
19784
19785/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
19786/// given instead, which also contains an `insert` range.
19787///
19788/// This function uses the cursor position to mimic what Rust-Analyzer provides as the `insert` range,
19789/// that is, `replace_range.start..cursor_pos`.
19790pub fn handle_completion_request_with_insert_and_replace(
19791 cx: &mut EditorLspTestContext,
19792 marked_string: &str,
19793 completions: Vec<&'static str>,
19794 counter: Arc<AtomicUsize>,
19795) -> impl Future<Output = ()> {
19796 let complete_from_marker: TextRangeMarker = '|'.into();
19797 let replace_range_marker: TextRangeMarker = ('<', '>').into();
19798 let (_, mut marked_ranges) = marked_text_ranges_by(
19799 marked_string,
19800 vec![complete_from_marker.clone(), replace_range_marker.clone()],
19801 );
19802
19803 let complete_from_position =
19804 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
19805 let replace_range =
19806 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
19807
19808 let mut request =
19809 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
19810 let completions = completions.clone();
19811 counter.fetch_add(1, atomic::Ordering::Release);
19812 async move {
19813 assert_eq!(params.text_document_position.text_document.uri, url.clone());
19814 assert_eq!(
19815 params.text_document_position.position, complete_from_position,
19816 "marker `|` position doesn't match",
19817 );
19818 Ok(Some(lsp::CompletionResponse::Array(
19819 completions
19820 .iter()
19821 .map(|completion_text| lsp::CompletionItem {
19822 label: completion_text.to_string(),
19823 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
19824 lsp::InsertReplaceEdit {
19825 insert: lsp::Range {
19826 start: replace_range.start,
19827 end: complete_from_position,
19828 },
19829 replace: replace_range,
19830 new_text: completion_text.to_string(),
19831 },
19832 )),
19833 ..Default::default()
19834 })
19835 .collect(),
19836 )))
19837 }
19838 });
19839
19840 async move {
19841 request.next().await;
19842 }
19843}
19844
19845fn handle_resolve_completion_request(
19846 cx: &mut EditorLspTestContext,
19847 edits: Option<Vec<(&'static str, &'static str)>>,
19848) -> impl Future<Output = ()> {
19849 let edits = edits.map(|edits| {
19850 edits
19851 .iter()
19852 .map(|(marked_string, new_text)| {
19853 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
19854 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
19855 lsp::TextEdit::new(replace_range, new_text.to_string())
19856 })
19857 .collect::<Vec<_>>()
19858 });
19859
19860 let mut request =
19861 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
19862 let edits = edits.clone();
19863 async move {
19864 Ok(lsp::CompletionItem {
19865 additional_text_edits: edits,
19866 ..Default::default()
19867 })
19868 }
19869 });
19870
19871 async move {
19872 request.next().await;
19873 }
19874}
19875
19876pub(crate) fn update_test_language_settings(
19877 cx: &mut TestAppContext,
19878 f: impl Fn(&mut AllLanguageSettingsContent),
19879) {
19880 cx.update(|cx| {
19881 SettingsStore::update_global(cx, |store, cx| {
19882 store.update_user_settings::<AllLanguageSettings>(cx, f);
19883 });
19884 });
19885}
19886
19887pub(crate) fn update_test_project_settings(
19888 cx: &mut TestAppContext,
19889 f: impl Fn(&mut ProjectSettings),
19890) {
19891 cx.update(|cx| {
19892 SettingsStore::update_global(cx, |store, cx| {
19893 store.update_user_settings::<ProjectSettings>(cx, f);
19894 });
19895 });
19896}
19897
19898pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
19899 cx.update(|cx| {
19900 assets::Assets.load_test_fonts(cx);
19901 let store = SettingsStore::test(cx);
19902 cx.set_global(store);
19903 theme::init(theme::LoadThemes::JustBase, cx);
19904 release_channel::init(SemanticVersion::default(), cx);
19905 client::init_settings(cx);
19906 language::init(cx);
19907 Project::init_settings(cx);
19908 workspace::init_settings(cx);
19909 crate::init(cx);
19910 });
19911
19912 update_test_language_settings(cx, f);
19913}
19914
19915#[track_caller]
19916fn assert_hunk_revert(
19917 not_reverted_text_with_selections: &str,
19918 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
19919 expected_reverted_text_with_selections: &str,
19920 base_text: &str,
19921 cx: &mut EditorLspTestContext,
19922) {
19923 cx.set_state(not_reverted_text_with_selections);
19924 cx.set_head_text(base_text);
19925 cx.executor().run_until_parked();
19926
19927 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
19928 let snapshot = editor.snapshot(window, cx);
19929 let reverted_hunk_statuses = snapshot
19930 .buffer_snapshot
19931 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
19932 .map(|hunk| hunk.status().kind)
19933 .collect::<Vec<_>>();
19934
19935 editor.git_restore(&Default::default(), window, cx);
19936 reverted_hunk_statuses
19937 });
19938 cx.executor().run_until_parked();
19939 cx.assert_editor_state(expected_reverted_text_with_selections);
19940 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
19941}