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\ndef«abcˇ»\n«abcˇ»");
6134
6135 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6136 .unwrap();
6137 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
6138}
6139
6140#[gpui::test]
6141async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
6142 init_test(cx, |_| {});
6143
6144 let mut cx = EditorTestContext::new(cx).await;
6145 cx.set_state("aˇ");
6146
6147 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6148 .unwrap();
6149 cx.assert_editor_state("«aˇ»");
6150 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6151 .unwrap();
6152 cx.assert_editor_state("«aˇ»");
6153}
6154
6155#[gpui::test]
6156async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
6157 init_test(cx, |_| {});
6158
6159 let mut cx = EditorTestContext::new(cx).await;
6160 cx.set_state(
6161 r#"let foo = 2;
6162lˇet foo = 2;
6163let fooˇ = 2;
6164let foo = 2;
6165let foo = ˇ2;"#,
6166 );
6167
6168 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6169 .unwrap();
6170 cx.assert_editor_state(
6171 r#"let foo = 2;
6172«letˇ» foo = 2;
6173let «fooˇ» = 2;
6174let foo = 2;
6175let foo = «2ˇ»;"#,
6176 );
6177
6178 // noop for multiple selections with different contents
6179 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6180 .unwrap();
6181 cx.assert_editor_state(
6182 r#"let foo = 2;
6183«letˇ» foo = 2;
6184let «fooˇ» = 2;
6185let foo = 2;
6186let foo = «2ˇ»;"#,
6187 );
6188}
6189
6190#[gpui::test]
6191async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
6192 init_test(cx, |_| {});
6193
6194 let mut cx = EditorTestContext::new(cx).await;
6195 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6196
6197 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6198 .unwrap();
6199 // selection direction is preserved
6200 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
6201
6202 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6203 .unwrap();
6204 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
6205
6206 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6207 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
6208
6209 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6210 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\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 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6217 .unwrap();
6218 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
6219}
6220
6221#[gpui::test]
6222async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
6223 init_test(cx, |_| {});
6224
6225 let language = Arc::new(Language::new(
6226 LanguageConfig::default(),
6227 Some(tree_sitter_rust::LANGUAGE.into()),
6228 ));
6229
6230 let text = r#"
6231 use mod1::mod2::{mod3, mod4};
6232
6233 fn fn_1(param1: bool, param2: &str) {
6234 let var1 = "text";
6235 }
6236 "#
6237 .unindent();
6238
6239 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6240 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6241 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6242
6243 editor
6244 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6245 .await;
6246
6247 editor.update_in(cx, |editor, window, cx| {
6248 editor.change_selections(None, window, cx, |s| {
6249 s.select_display_ranges([
6250 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
6251 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
6252 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
6253 ]);
6254 });
6255 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6256 });
6257 editor.update(cx, |editor, cx| {
6258 assert_text_with_selections(
6259 editor,
6260 indoc! {r#"
6261 use mod1::mod2::{mod3, «mod4ˇ»};
6262
6263 fn fn_1«ˇ(param1: bool, param2: &str)» {
6264 let var1 = "«ˇtext»";
6265 }
6266 "#},
6267 cx,
6268 );
6269 });
6270
6271 editor.update_in(cx, |editor, window, cx| {
6272 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6273 });
6274 editor.update(cx, |editor, cx| {
6275 assert_text_with_selections(
6276 editor,
6277 indoc! {r#"
6278 use mod1::mod2::«{mod3, mod4}ˇ»;
6279
6280 «ˇfn fn_1(param1: bool, param2: &str) {
6281 let var1 = "text";
6282 }»
6283 "#},
6284 cx,
6285 );
6286 });
6287
6288 editor.update_in(cx, |editor, window, cx| {
6289 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6290 });
6291 assert_eq!(
6292 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6293 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6294 );
6295
6296 // Trying to expand the selected syntax node one more time has no effect.
6297 editor.update_in(cx, |editor, window, cx| {
6298 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6299 });
6300 assert_eq!(
6301 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6302 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6303 );
6304
6305 editor.update_in(cx, |editor, window, cx| {
6306 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6307 });
6308 editor.update(cx, |editor, cx| {
6309 assert_text_with_selections(
6310 editor,
6311 indoc! {r#"
6312 use mod1::mod2::«{mod3, mod4}ˇ»;
6313
6314 «ˇfn fn_1(param1: bool, param2: &str) {
6315 let var1 = "text";
6316 }»
6317 "#},
6318 cx,
6319 );
6320 });
6321
6322 editor.update_in(cx, |editor, window, cx| {
6323 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6324 });
6325 editor.update(cx, |editor, cx| {
6326 assert_text_with_selections(
6327 editor,
6328 indoc! {r#"
6329 use mod1::mod2::{mod3, «mod4ˇ»};
6330
6331 fn fn_1«ˇ(param1: bool, param2: &str)» {
6332 let var1 = "«ˇtext»";
6333 }
6334 "#},
6335 cx,
6336 );
6337 });
6338
6339 editor.update_in(cx, |editor, window, cx| {
6340 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6341 });
6342 editor.update(cx, |editor, cx| {
6343 assert_text_with_selections(
6344 editor,
6345 indoc! {r#"
6346 use mod1::mod2::{mod3, mo«ˇ»d4};
6347
6348 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6349 let var1 = "te«ˇ»xt";
6350 }
6351 "#},
6352 cx,
6353 );
6354 });
6355
6356 // Trying to shrink the selected syntax node one more time has no effect.
6357 editor.update_in(cx, |editor, window, cx| {
6358 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6359 });
6360 editor.update_in(cx, |editor, _, cx| {
6361 assert_text_with_selections(
6362 editor,
6363 indoc! {r#"
6364 use mod1::mod2::{mod3, mo«ˇ»d4};
6365
6366 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6367 let var1 = "te«ˇ»xt";
6368 }
6369 "#},
6370 cx,
6371 );
6372 });
6373
6374 // Ensure that we keep expanding the selection if the larger selection starts or ends within
6375 // a fold.
6376 editor.update_in(cx, |editor, window, cx| {
6377 editor.fold_creases(
6378 vec![
6379 Crease::simple(
6380 Point::new(0, 21)..Point::new(0, 24),
6381 FoldPlaceholder::test(),
6382 ),
6383 Crease::simple(
6384 Point::new(3, 20)..Point::new(3, 22),
6385 FoldPlaceholder::test(),
6386 ),
6387 ],
6388 true,
6389 window,
6390 cx,
6391 );
6392 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6393 });
6394 editor.update(cx, |editor, cx| {
6395 assert_text_with_selections(
6396 editor,
6397 indoc! {r#"
6398 use mod1::mod2::«{mod3, mod4}ˇ»;
6399
6400 fn fn_1«ˇ(param1: bool, param2: &str)» {
6401 let var1 = "«ˇtext»";
6402 }
6403 "#},
6404 cx,
6405 );
6406 });
6407}
6408
6409#[gpui::test]
6410async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
6411 init_test(cx, |_| {});
6412
6413 let language = Arc::new(Language::new(
6414 LanguageConfig::default(),
6415 Some(tree_sitter_rust::LANGUAGE.into()),
6416 ));
6417
6418 let text = r#"
6419 use mod1::mod2::{mod3, mod4};
6420
6421 fn fn_1(param1: bool, param2: &str) {
6422 let var1 = "hello world";
6423 }
6424 "#
6425 .unindent();
6426
6427 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6428 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6429 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6430
6431 editor
6432 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6433 .await;
6434
6435 // Test 1: Cursor on a letter of a string word
6436 editor.update_in(cx, |editor, window, cx| {
6437 editor.change_selections(None, window, cx, |s| {
6438 s.select_display_ranges([
6439 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
6440 ]);
6441 });
6442 });
6443 editor.update_in(cx, |editor, window, cx| {
6444 assert_text_with_selections(
6445 editor,
6446 indoc! {r#"
6447 use mod1::mod2::{mod3, mod4};
6448
6449 fn fn_1(param1: bool, param2: &str) {
6450 let var1 = "hˇello world";
6451 }
6452 "#},
6453 cx,
6454 );
6455 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6456 assert_text_with_selections(
6457 editor,
6458 indoc! {r#"
6459 use mod1::mod2::{mod3, mod4};
6460
6461 fn fn_1(param1: bool, param2: &str) {
6462 let var1 = "«ˇhello» world";
6463 }
6464 "#},
6465 cx,
6466 );
6467 });
6468
6469 // Test 2: Partial selection within a word
6470 editor.update_in(cx, |editor, window, cx| {
6471 editor.change_selections(None, window, cx, |s| {
6472 s.select_display_ranges([
6473 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
6474 ]);
6475 });
6476 });
6477 editor.update_in(cx, |editor, window, cx| {
6478 assert_text_with_selections(
6479 editor,
6480 indoc! {r#"
6481 use mod1::mod2::{mod3, mod4};
6482
6483 fn fn_1(param1: bool, param2: &str) {
6484 let var1 = "h«elˇ»lo world";
6485 }
6486 "#},
6487 cx,
6488 );
6489 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6490 assert_text_with_selections(
6491 editor,
6492 indoc! {r#"
6493 use mod1::mod2::{mod3, mod4};
6494
6495 fn fn_1(param1: bool, param2: &str) {
6496 let var1 = "«ˇhello» world";
6497 }
6498 "#},
6499 cx,
6500 );
6501 });
6502
6503 // Test 3: Complete word already selected
6504 editor.update_in(cx, |editor, window, cx| {
6505 editor.change_selections(None, window, cx, |s| {
6506 s.select_display_ranges([
6507 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
6508 ]);
6509 });
6510 });
6511 editor.update_in(cx, |editor, window, cx| {
6512 assert_text_with_selections(
6513 editor,
6514 indoc! {r#"
6515 use mod1::mod2::{mod3, mod4};
6516
6517 fn fn_1(param1: bool, param2: &str) {
6518 let var1 = "«helloˇ» world";
6519 }
6520 "#},
6521 cx,
6522 );
6523 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6524 assert_text_with_selections(
6525 editor,
6526 indoc! {r#"
6527 use mod1::mod2::{mod3, mod4};
6528
6529 fn fn_1(param1: bool, param2: &str) {
6530 let var1 = "«hello worldˇ»";
6531 }
6532 "#},
6533 cx,
6534 );
6535 });
6536
6537 // Test 4: Selection spanning across words
6538 editor.update_in(cx, |editor, window, cx| {
6539 editor.change_selections(None, window, cx, |s| {
6540 s.select_display_ranges([
6541 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
6542 ]);
6543 });
6544 });
6545 editor.update_in(cx, |editor, window, cx| {
6546 assert_text_with_selections(
6547 editor,
6548 indoc! {r#"
6549 use mod1::mod2::{mod3, mod4};
6550
6551 fn fn_1(param1: bool, param2: &str) {
6552 let var1 = "hel«lo woˇ»rld";
6553 }
6554 "#},
6555 cx,
6556 );
6557 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6558 assert_text_with_selections(
6559 editor,
6560 indoc! {r#"
6561 use mod1::mod2::{mod3, mod4};
6562
6563 fn fn_1(param1: bool, param2: &str) {
6564 let var1 = "«ˇhello world»";
6565 }
6566 "#},
6567 cx,
6568 );
6569 });
6570
6571 // Test 5: Expansion beyond string
6572 editor.update_in(cx, |editor, window, cx| {
6573 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6574 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6575 assert_text_with_selections(
6576 editor,
6577 indoc! {r#"
6578 use mod1::mod2::{mod3, mod4};
6579
6580 fn fn_1(param1: bool, param2: &str) {
6581 «ˇlet var1 = "hello world";»
6582 }
6583 "#},
6584 cx,
6585 );
6586 });
6587}
6588
6589#[gpui::test]
6590async fn test_fold_function_bodies(cx: &mut TestAppContext) {
6591 init_test(cx, |_| {});
6592
6593 let base_text = r#"
6594 impl A {
6595 // this is an uncommitted comment
6596
6597 fn b() {
6598 c();
6599 }
6600
6601 // this is another uncommitted comment
6602
6603 fn d() {
6604 // e
6605 // f
6606 }
6607 }
6608
6609 fn g() {
6610 // h
6611 }
6612 "#
6613 .unindent();
6614
6615 let text = r#"
6616 ˇimpl A {
6617
6618 fn b() {
6619 c();
6620 }
6621
6622 fn d() {
6623 // e
6624 // f
6625 }
6626 }
6627
6628 fn g() {
6629 // h
6630 }
6631 "#
6632 .unindent();
6633
6634 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6635 cx.set_state(&text);
6636 cx.set_head_text(&base_text);
6637 cx.update_editor(|editor, window, cx| {
6638 editor.expand_all_diff_hunks(&Default::default(), window, cx);
6639 });
6640
6641 cx.assert_state_with_diff(
6642 "
6643 ˇimpl A {
6644 - // this is an uncommitted comment
6645
6646 fn b() {
6647 c();
6648 }
6649
6650 - // this is another uncommitted comment
6651 -
6652 fn d() {
6653 // e
6654 // f
6655 }
6656 }
6657
6658 fn g() {
6659 // h
6660 }
6661 "
6662 .unindent(),
6663 );
6664
6665 let expected_display_text = "
6666 impl A {
6667 // this is an uncommitted comment
6668
6669 fn b() {
6670 ⋯
6671 }
6672
6673 // this is another uncommitted comment
6674
6675 fn d() {
6676 ⋯
6677 }
6678 }
6679
6680 fn g() {
6681 ⋯
6682 }
6683 "
6684 .unindent();
6685
6686 cx.update_editor(|editor, window, cx| {
6687 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
6688 assert_eq!(editor.display_text(cx), expected_display_text);
6689 });
6690}
6691
6692#[gpui::test]
6693async fn test_autoindent(cx: &mut TestAppContext) {
6694 init_test(cx, |_| {});
6695
6696 let language = Arc::new(
6697 Language::new(
6698 LanguageConfig {
6699 brackets: BracketPairConfig {
6700 pairs: vec![
6701 BracketPair {
6702 start: "{".to_string(),
6703 end: "}".to_string(),
6704 close: false,
6705 surround: false,
6706 newline: true,
6707 },
6708 BracketPair {
6709 start: "(".to_string(),
6710 end: ")".to_string(),
6711 close: false,
6712 surround: false,
6713 newline: true,
6714 },
6715 ],
6716 ..Default::default()
6717 },
6718 ..Default::default()
6719 },
6720 Some(tree_sitter_rust::LANGUAGE.into()),
6721 )
6722 .with_indents_query(
6723 r#"
6724 (_ "(" ")" @end) @indent
6725 (_ "{" "}" @end) @indent
6726 "#,
6727 )
6728 .unwrap(),
6729 );
6730
6731 let text = "fn a() {}";
6732
6733 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6734 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6735 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6736 editor
6737 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6738 .await;
6739
6740 editor.update_in(cx, |editor, window, cx| {
6741 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
6742 editor.newline(&Newline, window, cx);
6743 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
6744 assert_eq!(
6745 editor.selections.ranges(cx),
6746 &[
6747 Point::new(1, 4)..Point::new(1, 4),
6748 Point::new(3, 4)..Point::new(3, 4),
6749 Point::new(5, 0)..Point::new(5, 0)
6750 ]
6751 );
6752 });
6753}
6754
6755#[gpui::test]
6756async fn test_autoindent_selections(cx: &mut TestAppContext) {
6757 init_test(cx, |_| {});
6758
6759 {
6760 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6761 cx.set_state(indoc! {"
6762 impl A {
6763
6764 fn b() {}
6765
6766 «fn c() {
6767
6768 }ˇ»
6769 }
6770 "});
6771
6772 cx.update_editor(|editor, window, cx| {
6773 editor.autoindent(&Default::default(), window, cx);
6774 });
6775
6776 cx.assert_editor_state(indoc! {"
6777 impl A {
6778
6779 fn b() {}
6780
6781 «fn c() {
6782
6783 }ˇ»
6784 }
6785 "});
6786 }
6787
6788 {
6789 let mut cx = EditorTestContext::new_multibuffer(
6790 cx,
6791 [indoc! { "
6792 impl A {
6793 «
6794 // a
6795 fn b(){}
6796 »
6797 «
6798 }
6799 fn c(){}
6800 »
6801 "}],
6802 );
6803
6804 let buffer = cx.update_editor(|editor, _, cx| {
6805 let buffer = editor.buffer().update(cx, |buffer, _| {
6806 buffer.all_buffers().iter().next().unwrap().clone()
6807 });
6808 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6809 buffer
6810 });
6811
6812 cx.run_until_parked();
6813 cx.update_editor(|editor, window, cx| {
6814 editor.select_all(&Default::default(), window, cx);
6815 editor.autoindent(&Default::default(), window, cx)
6816 });
6817 cx.run_until_parked();
6818
6819 cx.update(|_, cx| {
6820 assert_eq!(
6821 buffer.read(cx).text(),
6822 indoc! { "
6823 impl A {
6824
6825 // a
6826 fn b(){}
6827
6828
6829 }
6830 fn c(){}
6831
6832 " }
6833 )
6834 });
6835 }
6836}
6837
6838#[gpui::test]
6839async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
6840 init_test(cx, |_| {});
6841
6842 let mut cx = EditorTestContext::new(cx).await;
6843
6844 let language = Arc::new(Language::new(
6845 LanguageConfig {
6846 brackets: BracketPairConfig {
6847 pairs: vec![
6848 BracketPair {
6849 start: "{".to_string(),
6850 end: "}".to_string(),
6851 close: true,
6852 surround: true,
6853 newline: true,
6854 },
6855 BracketPair {
6856 start: "(".to_string(),
6857 end: ")".to_string(),
6858 close: true,
6859 surround: true,
6860 newline: true,
6861 },
6862 BracketPair {
6863 start: "/*".to_string(),
6864 end: " */".to_string(),
6865 close: true,
6866 surround: true,
6867 newline: true,
6868 },
6869 BracketPair {
6870 start: "[".to_string(),
6871 end: "]".to_string(),
6872 close: false,
6873 surround: false,
6874 newline: true,
6875 },
6876 BracketPair {
6877 start: "\"".to_string(),
6878 end: "\"".to_string(),
6879 close: true,
6880 surround: true,
6881 newline: false,
6882 },
6883 BracketPair {
6884 start: "<".to_string(),
6885 end: ">".to_string(),
6886 close: false,
6887 surround: true,
6888 newline: true,
6889 },
6890 ],
6891 ..Default::default()
6892 },
6893 autoclose_before: "})]".to_string(),
6894 ..Default::default()
6895 },
6896 Some(tree_sitter_rust::LANGUAGE.into()),
6897 ));
6898
6899 cx.language_registry().add(language.clone());
6900 cx.update_buffer(|buffer, cx| {
6901 buffer.set_language(Some(language), cx);
6902 });
6903
6904 cx.set_state(
6905 &r#"
6906 🏀ˇ
6907 εˇ
6908 ❤️ˇ
6909 "#
6910 .unindent(),
6911 );
6912
6913 // autoclose multiple nested brackets at multiple cursors
6914 cx.update_editor(|editor, window, cx| {
6915 editor.handle_input("{", window, cx);
6916 editor.handle_input("{", window, cx);
6917 editor.handle_input("{", window, cx);
6918 });
6919 cx.assert_editor_state(
6920 &"
6921 🏀{{{ˇ}}}
6922 ε{{{ˇ}}}
6923 ❤️{{{ˇ}}}
6924 "
6925 .unindent(),
6926 );
6927
6928 // insert a different closing bracket
6929 cx.update_editor(|editor, window, cx| {
6930 editor.handle_input(")", window, cx);
6931 });
6932 cx.assert_editor_state(
6933 &"
6934 🏀{{{)ˇ}}}
6935 ε{{{)ˇ}}}
6936 ❤️{{{)ˇ}}}
6937 "
6938 .unindent(),
6939 );
6940
6941 // skip over the auto-closed brackets when typing a closing bracket
6942 cx.update_editor(|editor, window, cx| {
6943 editor.move_right(&MoveRight, window, cx);
6944 editor.handle_input("}", window, cx);
6945 editor.handle_input("}", window, cx);
6946 editor.handle_input("}", window, cx);
6947 });
6948 cx.assert_editor_state(
6949 &"
6950 🏀{{{)}}}}ˇ
6951 ε{{{)}}}}ˇ
6952 ❤️{{{)}}}}ˇ
6953 "
6954 .unindent(),
6955 );
6956
6957 // autoclose multi-character pairs
6958 cx.set_state(
6959 &"
6960 ˇ
6961 ˇ
6962 "
6963 .unindent(),
6964 );
6965 cx.update_editor(|editor, window, cx| {
6966 editor.handle_input("/", window, cx);
6967 editor.handle_input("*", window, cx);
6968 });
6969 cx.assert_editor_state(
6970 &"
6971 /*ˇ */
6972 /*ˇ */
6973 "
6974 .unindent(),
6975 );
6976
6977 // one cursor autocloses a multi-character pair, one cursor
6978 // does not autoclose.
6979 cx.set_state(
6980 &"
6981 /ˇ
6982 ˇ
6983 "
6984 .unindent(),
6985 );
6986 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
6987 cx.assert_editor_state(
6988 &"
6989 /*ˇ */
6990 *ˇ
6991 "
6992 .unindent(),
6993 );
6994
6995 // Don't autoclose if the next character isn't whitespace and isn't
6996 // listed in the language's "autoclose_before" section.
6997 cx.set_state("ˇa b");
6998 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6999 cx.assert_editor_state("{ˇa b");
7000
7001 // Don't autoclose if `close` is false for the bracket pair
7002 cx.set_state("ˇ");
7003 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
7004 cx.assert_editor_state("[ˇ");
7005
7006 // Surround with brackets if text is selected
7007 cx.set_state("«aˇ» b");
7008 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7009 cx.assert_editor_state("{«aˇ»} b");
7010
7011 // Autoclose when not immediately after a word character
7012 cx.set_state("a ˇ");
7013 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7014 cx.assert_editor_state("a \"ˇ\"");
7015
7016 // Autoclose pair where the start and end characters are the same
7017 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7018 cx.assert_editor_state("a \"\"ˇ");
7019
7020 // Don't autoclose when immediately after a word character
7021 cx.set_state("aˇ");
7022 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7023 cx.assert_editor_state("a\"ˇ");
7024
7025 // Do autoclose when after a non-word character
7026 cx.set_state("{ˇ");
7027 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7028 cx.assert_editor_state("{\"ˇ\"");
7029
7030 // Non identical pairs autoclose regardless of preceding character
7031 cx.set_state("aˇ");
7032 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7033 cx.assert_editor_state("a{ˇ}");
7034
7035 // Don't autoclose pair if autoclose is disabled
7036 cx.set_state("ˇ");
7037 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7038 cx.assert_editor_state("<ˇ");
7039
7040 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
7041 cx.set_state("«aˇ» b");
7042 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7043 cx.assert_editor_state("<«aˇ»> b");
7044}
7045
7046#[gpui::test]
7047async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
7048 init_test(cx, |settings| {
7049 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7050 });
7051
7052 let mut cx = EditorTestContext::new(cx).await;
7053
7054 let language = Arc::new(Language::new(
7055 LanguageConfig {
7056 brackets: BracketPairConfig {
7057 pairs: vec![
7058 BracketPair {
7059 start: "{".to_string(),
7060 end: "}".to_string(),
7061 close: true,
7062 surround: true,
7063 newline: true,
7064 },
7065 BracketPair {
7066 start: "(".to_string(),
7067 end: ")".to_string(),
7068 close: true,
7069 surround: true,
7070 newline: true,
7071 },
7072 BracketPair {
7073 start: "[".to_string(),
7074 end: "]".to_string(),
7075 close: false,
7076 surround: false,
7077 newline: true,
7078 },
7079 ],
7080 ..Default::default()
7081 },
7082 autoclose_before: "})]".to_string(),
7083 ..Default::default()
7084 },
7085 Some(tree_sitter_rust::LANGUAGE.into()),
7086 ));
7087
7088 cx.language_registry().add(language.clone());
7089 cx.update_buffer(|buffer, cx| {
7090 buffer.set_language(Some(language), cx);
7091 });
7092
7093 cx.set_state(
7094 &"
7095 ˇ
7096 ˇ
7097 ˇ
7098 "
7099 .unindent(),
7100 );
7101
7102 // ensure only matching closing brackets are skipped over
7103 cx.update_editor(|editor, window, cx| {
7104 editor.handle_input("}", window, cx);
7105 editor.move_left(&MoveLeft, window, cx);
7106 editor.handle_input(")", window, cx);
7107 editor.move_left(&MoveLeft, window, cx);
7108 });
7109 cx.assert_editor_state(
7110 &"
7111 ˇ)}
7112 ˇ)}
7113 ˇ)}
7114 "
7115 .unindent(),
7116 );
7117
7118 // skip-over closing brackets at multiple cursors
7119 cx.update_editor(|editor, window, cx| {
7120 editor.handle_input(")", window, cx);
7121 editor.handle_input("}", window, cx);
7122 });
7123 cx.assert_editor_state(
7124 &"
7125 )}ˇ
7126 )}ˇ
7127 )}ˇ
7128 "
7129 .unindent(),
7130 );
7131
7132 // ignore non-close brackets
7133 cx.update_editor(|editor, window, cx| {
7134 editor.handle_input("]", window, cx);
7135 editor.move_left(&MoveLeft, window, cx);
7136 editor.handle_input("]", window, cx);
7137 });
7138 cx.assert_editor_state(
7139 &"
7140 )}]ˇ]
7141 )}]ˇ]
7142 )}]ˇ]
7143 "
7144 .unindent(),
7145 );
7146}
7147
7148#[gpui::test]
7149async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
7150 init_test(cx, |_| {});
7151
7152 let mut cx = EditorTestContext::new(cx).await;
7153
7154 let html_language = Arc::new(
7155 Language::new(
7156 LanguageConfig {
7157 name: "HTML".into(),
7158 brackets: BracketPairConfig {
7159 pairs: vec![
7160 BracketPair {
7161 start: "<".into(),
7162 end: ">".into(),
7163 close: true,
7164 ..Default::default()
7165 },
7166 BracketPair {
7167 start: "{".into(),
7168 end: "}".into(),
7169 close: true,
7170 ..Default::default()
7171 },
7172 BracketPair {
7173 start: "(".into(),
7174 end: ")".into(),
7175 close: true,
7176 ..Default::default()
7177 },
7178 ],
7179 ..Default::default()
7180 },
7181 autoclose_before: "})]>".into(),
7182 ..Default::default()
7183 },
7184 Some(tree_sitter_html::LANGUAGE.into()),
7185 )
7186 .with_injection_query(
7187 r#"
7188 (script_element
7189 (raw_text) @injection.content
7190 (#set! injection.language "javascript"))
7191 "#,
7192 )
7193 .unwrap(),
7194 );
7195
7196 let javascript_language = Arc::new(Language::new(
7197 LanguageConfig {
7198 name: "JavaScript".into(),
7199 brackets: BracketPairConfig {
7200 pairs: vec![
7201 BracketPair {
7202 start: "/*".into(),
7203 end: " */".into(),
7204 close: true,
7205 ..Default::default()
7206 },
7207 BracketPair {
7208 start: "{".into(),
7209 end: "}".into(),
7210 close: true,
7211 ..Default::default()
7212 },
7213 BracketPair {
7214 start: "(".into(),
7215 end: ")".into(),
7216 close: true,
7217 ..Default::default()
7218 },
7219 ],
7220 ..Default::default()
7221 },
7222 autoclose_before: "})]>".into(),
7223 ..Default::default()
7224 },
7225 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
7226 ));
7227
7228 cx.language_registry().add(html_language.clone());
7229 cx.language_registry().add(javascript_language.clone());
7230
7231 cx.update_buffer(|buffer, cx| {
7232 buffer.set_language(Some(html_language), cx);
7233 });
7234
7235 cx.set_state(
7236 &r#"
7237 <body>ˇ
7238 <script>
7239 var x = 1;ˇ
7240 </script>
7241 </body>ˇ
7242 "#
7243 .unindent(),
7244 );
7245
7246 // Precondition: different languages are active at different locations.
7247 cx.update_editor(|editor, window, cx| {
7248 let snapshot = editor.snapshot(window, cx);
7249 let cursors = editor.selections.ranges::<usize>(cx);
7250 let languages = cursors
7251 .iter()
7252 .map(|c| snapshot.language_at(c.start).unwrap().name())
7253 .collect::<Vec<_>>();
7254 assert_eq!(
7255 languages,
7256 &["HTML".into(), "JavaScript".into(), "HTML".into()]
7257 );
7258 });
7259
7260 // Angle brackets autoclose in HTML, but not JavaScript.
7261 cx.update_editor(|editor, window, cx| {
7262 editor.handle_input("<", window, cx);
7263 editor.handle_input("a", window, cx);
7264 });
7265 cx.assert_editor_state(
7266 &r#"
7267 <body><aˇ>
7268 <script>
7269 var x = 1;<aˇ
7270 </script>
7271 </body><aˇ>
7272 "#
7273 .unindent(),
7274 );
7275
7276 // Curly braces and parens autoclose in both HTML and JavaScript.
7277 cx.update_editor(|editor, window, cx| {
7278 editor.handle_input(" b=", window, cx);
7279 editor.handle_input("{", window, cx);
7280 editor.handle_input("c", window, cx);
7281 editor.handle_input("(", window, cx);
7282 });
7283 cx.assert_editor_state(
7284 &r#"
7285 <body><a b={c(ˇ)}>
7286 <script>
7287 var x = 1;<a b={c(ˇ)}
7288 </script>
7289 </body><a b={c(ˇ)}>
7290 "#
7291 .unindent(),
7292 );
7293
7294 // Brackets that were already autoclosed are skipped.
7295 cx.update_editor(|editor, window, cx| {
7296 editor.handle_input(")", window, cx);
7297 editor.handle_input("d", window, cx);
7298 editor.handle_input("}", window, cx);
7299 });
7300 cx.assert_editor_state(
7301 &r#"
7302 <body><a b={c()d}ˇ>
7303 <script>
7304 var x = 1;<a b={c()d}ˇ
7305 </script>
7306 </body><a b={c()d}ˇ>
7307 "#
7308 .unindent(),
7309 );
7310 cx.update_editor(|editor, window, cx| {
7311 editor.handle_input(">", window, cx);
7312 });
7313 cx.assert_editor_state(
7314 &r#"
7315 <body><a b={c()d}>ˇ
7316 <script>
7317 var x = 1;<a b={c()d}>ˇ
7318 </script>
7319 </body><a b={c()d}>ˇ
7320 "#
7321 .unindent(),
7322 );
7323
7324 // Reset
7325 cx.set_state(
7326 &r#"
7327 <body>ˇ
7328 <script>
7329 var x = 1;ˇ
7330 </script>
7331 </body>ˇ
7332 "#
7333 .unindent(),
7334 );
7335
7336 cx.update_editor(|editor, window, cx| {
7337 editor.handle_input("<", window, cx);
7338 });
7339 cx.assert_editor_state(
7340 &r#"
7341 <body><ˇ>
7342 <script>
7343 var x = 1;<ˇ
7344 </script>
7345 </body><ˇ>
7346 "#
7347 .unindent(),
7348 );
7349
7350 // When backspacing, the closing angle brackets are removed.
7351 cx.update_editor(|editor, window, cx| {
7352 editor.backspace(&Backspace, window, cx);
7353 });
7354 cx.assert_editor_state(
7355 &r#"
7356 <body>ˇ
7357 <script>
7358 var x = 1;ˇ
7359 </script>
7360 </body>ˇ
7361 "#
7362 .unindent(),
7363 );
7364
7365 // Block comments autoclose in JavaScript, but not HTML.
7366 cx.update_editor(|editor, window, cx| {
7367 editor.handle_input("/", window, cx);
7368 editor.handle_input("*", window, cx);
7369 });
7370 cx.assert_editor_state(
7371 &r#"
7372 <body>/*ˇ
7373 <script>
7374 var x = 1;/*ˇ */
7375 </script>
7376 </body>/*ˇ
7377 "#
7378 .unindent(),
7379 );
7380}
7381
7382#[gpui::test]
7383async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
7384 init_test(cx, |_| {});
7385
7386 let mut cx = EditorTestContext::new(cx).await;
7387
7388 let rust_language = Arc::new(
7389 Language::new(
7390 LanguageConfig {
7391 name: "Rust".into(),
7392 brackets: serde_json::from_value(json!([
7393 { "start": "{", "end": "}", "close": true, "newline": true },
7394 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
7395 ]))
7396 .unwrap(),
7397 autoclose_before: "})]>".into(),
7398 ..Default::default()
7399 },
7400 Some(tree_sitter_rust::LANGUAGE.into()),
7401 )
7402 .with_override_query("(string_literal) @string")
7403 .unwrap(),
7404 );
7405
7406 cx.language_registry().add(rust_language.clone());
7407 cx.update_buffer(|buffer, cx| {
7408 buffer.set_language(Some(rust_language), cx);
7409 });
7410
7411 cx.set_state(
7412 &r#"
7413 let x = ˇ
7414 "#
7415 .unindent(),
7416 );
7417
7418 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
7419 cx.update_editor(|editor, window, cx| {
7420 editor.handle_input("\"", window, cx);
7421 });
7422 cx.assert_editor_state(
7423 &r#"
7424 let x = "ˇ"
7425 "#
7426 .unindent(),
7427 );
7428
7429 // Inserting another quotation mark. The cursor moves across the existing
7430 // automatically-inserted quotation mark.
7431 cx.update_editor(|editor, window, cx| {
7432 editor.handle_input("\"", window, cx);
7433 });
7434 cx.assert_editor_state(
7435 &r#"
7436 let x = ""ˇ
7437 "#
7438 .unindent(),
7439 );
7440
7441 // Reset
7442 cx.set_state(
7443 &r#"
7444 let x = ˇ
7445 "#
7446 .unindent(),
7447 );
7448
7449 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
7450 cx.update_editor(|editor, window, cx| {
7451 editor.handle_input("\"", window, cx);
7452 editor.handle_input(" ", window, cx);
7453 editor.move_left(&Default::default(), window, cx);
7454 editor.handle_input("\\", window, cx);
7455 editor.handle_input("\"", window, cx);
7456 });
7457 cx.assert_editor_state(
7458 &r#"
7459 let x = "\"ˇ "
7460 "#
7461 .unindent(),
7462 );
7463
7464 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
7465 // mark. Nothing is inserted.
7466 cx.update_editor(|editor, window, cx| {
7467 editor.move_right(&Default::default(), window, cx);
7468 editor.handle_input("\"", window, cx);
7469 });
7470 cx.assert_editor_state(
7471 &r#"
7472 let x = "\" "ˇ
7473 "#
7474 .unindent(),
7475 );
7476}
7477
7478#[gpui::test]
7479async fn test_surround_with_pair(cx: &mut TestAppContext) {
7480 init_test(cx, |_| {});
7481
7482 let language = Arc::new(Language::new(
7483 LanguageConfig {
7484 brackets: BracketPairConfig {
7485 pairs: vec![
7486 BracketPair {
7487 start: "{".to_string(),
7488 end: "}".to_string(),
7489 close: true,
7490 surround: true,
7491 newline: true,
7492 },
7493 BracketPair {
7494 start: "/* ".to_string(),
7495 end: "*/".to_string(),
7496 close: true,
7497 surround: true,
7498 ..Default::default()
7499 },
7500 ],
7501 ..Default::default()
7502 },
7503 ..Default::default()
7504 },
7505 Some(tree_sitter_rust::LANGUAGE.into()),
7506 ));
7507
7508 let text = r#"
7509 a
7510 b
7511 c
7512 "#
7513 .unindent();
7514
7515 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7516 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7517 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7518 editor
7519 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7520 .await;
7521
7522 editor.update_in(cx, |editor, window, cx| {
7523 editor.change_selections(None, window, cx, |s| {
7524 s.select_display_ranges([
7525 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7526 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7527 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
7528 ])
7529 });
7530
7531 editor.handle_input("{", window, cx);
7532 editor.handle_input("{", window, cx);
7533 editor.handle_input("{", window, cx);
7534 assert_eq!(
7535 editor.text(cx),
7536 "
7537 {{{a}}}
7538 {{{b}}}
7539 {{{c}}}
7540 "
7541 .unindent()
7542 );
7543 assert_eq!(
7544 editor.selections.display_ranges(cx),
7545 [
7546 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
7547 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
7548 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
7549 ]
7550 );
7551
7552 editor.undo(&Undo, window, cx);
7553 editor.undo(&Undo, window, cx);
7554 editor.undo(&Undo, window, cx);
7555 assert_eq!(
7556 editor.text(cx),
7557 "
7558 a
7559 b
7560 c
7561 "
7562 .unindent()
7563 );
7564 assert_eq!(
7565 editor.selections.display_ranges(cx),
7566 [
7567 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7568 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7569 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7570 ]
7571 );
7572
7573 // Ensure inserting the first character of a multi-byte bracket pair
7574 // doesn't surround the selections with the bracket.
7575 editor.handle_input("/", window, cx);
7576 assert_eq!(
7577 editor.text(cx),
7578 "
7579 /
7580 /
7581 /
7582 "
7583 .unindent()
7584 );
7585 assert_eq!(
7586 editor.selections.display_ranges(cx),
7587 [
7588 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7589 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7590 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7591 ]
7592 );
7593
7594 editor.undo(&Undo, window, cx);
7595 assert_eq!(
7596 editor.text(cx),
7597 "
7598 a
7599 b
7600 c
7601 "
7602 .unindent()
7603 );
7604 assert_eq!(
7605 editor.selections.display_ranges(cx),
7606 [
7607 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7608 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7609 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7610 ]
7611 );
7612
7613 // Ensure inserting the last character of a multi-byte bracket pair
7614 // doesn't surround the selections with the bracket.
7615 editor.handle_input("*", window, cx);
7616 assert_eq!(
7617 editor.text(cx),
7618 "
7619 *
7620 *
7621 *
7622 "
7623 .unindent()
7624 );
7625 assert_eq!(
7626 editor.selections.display_ranges(cx),
7627 [
7628 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7629 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7630 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7631 ]
7632 );
7633 });
7634}
7635
7636#[gpui::test]
7637async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
7638 init_test(cx, |_| {});
7639
7640 let language = Arc::new(Language::new(
7641 LanguageConfig {
7642 brackets: BracketPairConfig {
7643 pairs: vec![BracketPair {
7644 start: "{".to_string(),
7645 end: "}".to_string(),
7646 close: true,
7647 surround: true,
7648 newline: true,
7649 }],
7650 ..Default::default()
7651 },
7652 autoclose_before: "}".to_string(),
7653 ..Default::default()
7654 },
7655 Some(tree_sitter_rust::LANGUAGE.into()),
7656 ));
7657
7658 let text = r#"
7659 a
7660 b
7661 c
7662 "#
7663 .unindent();
7664
7665 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7666 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7667 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7668 editor
7669 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7670 .await;
7671
7672 editor.update_in(cx, |editor, window, cx| {
7673 editor.change_selections(None, window, cx, |s| {
7674 s.select_ranges([
7675 Point::new(0, 1)..Point::new(0, 1),
7676 Point::new(1, 1)..Point::new(1, 1),
7677 Point::new(2, 1)..Point::new(2, 1),
7678 ])
7679 });
7680
7681 editor.handle_input("{", window, cx);
7682 editor.handle_input("{", window, cx);
7683 editor.handle_input("_", window, cx);
7684 assert_eq!(
7685 editor.text(cx),
7686 "
7687 a{{_}}
7688 b{{_}}
7689 c{{_}}
7690 "
7691 .unindent()
7692 );
7693 assert_eq!(
7694 editor.selections.ranges::<Point>(cx),
7695 [
7696 Point::new(0, 4)..Point::new(0, 4),
7697 Point::new(1, 4)..Point::new(1, 4),
7698 Point::new(2, 4)..Point::new(2, 4)
7699 ]
7700 );
7701
7702 editor.backspace(&Default::default(), window, cx);
7703 editor.backspace(&Default::default(), window, cx);
7704 assert_eq!(
7705 editor.text(cx),
7706 "
7707 a{}
7708 b{}
7709 c{}
7710 "
7711 .unindent()
7712 );
7713 assert_eq!(
7714 editor.selections.ranges::<Point>(cx),
7715 [
7716 Point::new(0, 2)..Point::new(0, 2),
7717 Point::new(1, 2)..Point::new(1, 2),
7718 Point::new(2, 2)..Point::new(2, 2)
7719 ]
7720 );
7721
7722 editor.delete_to_previous_word_start(&Default::default(), window, cx);
7723 assert_eq!(
7724 editor.text(cx),
7725 "
7726 a
7727 b
7728 c
7729 "
7730 .unindent()
7731 );
7732 assert_eq!(
7733 editor.selections.ranges::<Point>(cx),
7734 [
7735 Point::new(0, 1)..Point::new(0, 1),
7736 Point::new(1, 1)..Point::new(1, 1),
7737 Point::new(2, 1)..Point::new(2, 1)
7738 ]
7739 );
7740 });
7741}
7742
7743#[gpui::test]
7744async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
7745 init_test(cx, |settings| {
7746 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7747 });
7748
7749 let mut cx = EditorTestContext::new(cx).await;
7750
7751 let language = Arc::new(Language::new(
7752 LanguageConfig {
7753 brackets: BracketPairConfig {
7754 pairs: vec![
7755 BracketPair {
7756 start: "{".to_string(),
7757 end: "}".to_string(),
7758 close: true,
7759 surround: true,
7760 newline: true,
7761 },
7762 BracketPair {
7763 start: "(".to_string(),
7764 end: ")".to_string(),
7765 close: true,
7766 surround: true,
7767 newline: true,
7768 },
7769 BracketPair {
7770 start: "[".to_string(),
7771 end: "]".to_string(),
7772 close: false,
7773 surround: true,
7774 newline: true,
7775 },
7776 ],
7777 ..Default::default()
7778 },
7779 autoclose_before: "})]".to_string(),
7780 ..Default::default()
7781 },
7782 Some(tree_sitter_rust::LANGUAGE.into()),
7783 ));
7784
7785 cx.language_registry().add(language.clone());
7786 cx.update_buffer(|buffer, cx| {
7787 buffer.set_language(Some(language), cx);
7788 });
7789
7790 cx.set_state(
7791 &"
7792 {(ˇ)}
7793 [[ˇ]]
7794 {(ˇ)}
7795 "
7796 .unindent(),
7797 );
7798
7799 cx.update_editor(|editor, window, cx| {
7800 editor.backspace(&Default::default(), window, cx);
7801 editor.backspace(&Default::default(), window, cx);
7802 });
7803
7804 cx.assert_editor_state(
7805 &"
7806 ˇ
7807 ˇ]]
7808 ˇ
7809 "
7810 .unindent(),
7811 );
7812
7813 cx.update_editor(|editor, window, cx| {
7814 editor.handle_input("{", window, cx);
7815 editor.handle_input("{", window, cx);
7816 editor.move_right(&MoveRight, window, cx);
7817 editor.move_right(&MoveRight, window, cx);
7818 editor.move_left(&MoveLeft, window, cx);
7819 editor.move_left(&MoveLeft, window, cx);
7820 editor.backspace(&Default::default(), window, cx);
7821 });
7822
7823 cx.assert_editor_state(
7824 &"
7825 {ˇ}
7826 {ˇ}]]
7827 {ˇ}
7828 "
7829 .unindent(),
7830 );
7831
7832 cx.update_editor(|editor, window, cx| {
7833 editor.backspace(&Default::default(), window, cx);
7834 });
7835
7836 cx.assert_editor_state(
7837 &"
7838 ˇ
7839 ˇ]]
7840 ˇ
7841 "
7842 .unindent(),
7843 );
7844}
7845
7846#[gpui::test]
7847async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
7848 init_test(cx, |_| {});
7849
7850 let language = Arc::new(Language::new(
7851 LanguageConfig::default(),
7852 Some(tree_sitter_rust::LANGUAGE.into()),
7853 ));
7854
7855 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
7856 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7857 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7858 editor
7859 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7860 .await;
7861
7862 editor.update_in(cx, |editor, window, cx| {
7863 editor.set_auto_replace_emoji_shortcode(true);
7864
7865 editor.handle_input("Hello ", window, cx);
7866 editor.handle_input(":wave", window, cx);
7867 assert_eq!(editor.text(cx), "Hello :wave".unindent());
7868
7869 editor.handle_input(":", window, cx);
7870 assert_eq!(editor.text(cx), "Hello 👋".unindent());
7871
7872 editor.handle_input(" :smile", window, cx);
7873 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
7874
7875 editor.handle_input(":", window, cx);
7876 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
7877
7878 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
7879 editor.handle_input(":wave", window, cx);
7880 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
7881
7882 editor.handle_input(":", window, cx);
7883 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
7884
7885 editor.handle_input(":1", window, cx);
7886 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
7887
7888 editor.handle_input(":", window, cx);
7889 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
7890
7891 // Ensure shortcode does not get replaced when it is part of a word
7892 editor.handle_input(" Test:wave", window, cx);
7893 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
7894
7895 editor.handle_input(":", window, cx);
7896 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
7897
7898 editor.set_auto_replace_emoji_shortcode(false);
7899
7900 // Ensure shortcode does not get replaced when auto replace is off
7901 editor.handle_input(" :wave", window, cx);
7902 assert_eq!(
7903 editor.text(cx),
7904 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
7905 );
7906
7907 editor.handle_input(":", window, cx);
7908 assert_eq!(
7909 editor.text(cx),
7910 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
7911 );
7912 });
7913}
7914
7915#[gpui::test]
7916async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
7917 init_test(cx, |_| {});
7918
7919 let (text, insertion_ranges) = marked_text_ranges(
7920 indoc! {"
7921 ˇ
7922 "},
7923 false,
7924 );
7925
7926 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7927 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7928
7929 _ = editor.update_in(cx, |editor, window, cx| {
7930 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
7931
7932 editor
7933 .insert_snippet(&insertion_ranges, snippet, window, cx)
7934 .unwrap();
7935
7936 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7937 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7938 assert_eq!(editor.text(cx), expected_text);
7939 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7940 }
7941
7942 assert(
7943 editor,
7944 cx,
7945 indoc! {"
7946 type «» =•
7947 "},
7948 );
7949
7950 assert!(editor.context_menu_visible(), "There should be a matches");
7951 });
7952}
7953
7954#[gpui::test]
7955async fn test_snippets(cx: &mut TestAppContext) {
7956 init_test(cx, |_| {});
7957
7958 let (text, insertion_ranges) = marked_text_ranges(
7959 indoc! {"
7960 a.ˇ b
7961 a.ˇ b
7962 a.ˇ b
7963 "},
7964 false,
7965 );
7966
7967 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7968 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7969
7970 editor.update_in(cx, |editor, window, cx| {
7971 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
7972
7973 editor
7974 .insert_snippet(&insertion_ranges, snippet, window, cx)
7975 .unwrap();
7976
7977 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7978 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7979 assert_eq!(editor.text(cx), expected_text);
7980 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7981 }
7982
7983 assert(
7984 editor,
7985 cx,
7986 indoc! {"
7987 a.f(«one», two, «three») b
7988 a.f(«one», two, «three») b
7989 a.f(«one», two, «three») b
7990 "},
7991 );
7992
7993 // Can't move earlier than the first tab stop
7994 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
7995 assert(
7996 editor,
7997 cx,
7998 indoc! {"
7999 a.f(«one», two, «three») b
8000 a.f(«one», two, «three») b
8001 a.f(«one», two, «three») b
8002 "},
8003 );
8004
8005 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8006 assert(
8007 editor,
8008 cx,
8009 indoc! {"
8010 a.f(one, «two», three) b
8011 a.f(one, «two», three) b
8012 a.f(one, «two», three) b
8013 "},
8014 );
8015
8016 editor.move_to_prev_snippet_tabstop(window, cx);
8017 assert(
8018 editor,
8019 cx,
8020 indoc! {"
8021 a.f(«one», two, «three») b
8022 a.f(«one», two, «three») b
8023 a.f(«one», two, «three») b
8024 "},
8025 );
8026
8027 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8028 assert(
8029 editor,
8030 cx,
8031 indoc! {"
8032 a.f(one, «two», three) b
8033 a.f(one, «two», three) b
8034 a.f(one, «two», three) b
8035 "},
8036 );
8037 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8038 assert(
8039 editor,
8040 cx,
8041 indoc! {"
8042 a.f(one, two, three)ˇ b
8043 a.f(one, two, three)ˇ b
8044 a.f(one, two, three)ˇ b
8045 "},
8046 );
8047
8048 // As soon as the last tab stop is reached, snippet state is gone
8049 editor.move_to_prev_snippet_tabstop(window, cx);
8050 assert(
8051 editor,
8052 cx,
8053 indoc! {"
8054 a.f(one, two, three)ˇ b
8055 a.f(one, two, three)ˇ b
8056 a.f(one, two, three)ˇ b
8057 "},
8058 );
8059 });
8060}
8061
8062#[gpui::test]
8063async fn test_document_format_during_save(cx: &mut TestAppContext) {
8064 init_test(cx, |_| {});
8065
8066 let fs = FakeFs::new(cx.executor());
8067 fs.insert_file(path!("/file.rs"), Default::default()).await;
8068
8069 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
8070
8071 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8072 language_registry.add(rust_lang());
8073 let mut fake_servers = language_registry.register_fake_lsp(
8074 "Rust",
8075 FakeLspAdapter {
8076 capabilities: lsp::ServerCapabilities {
8077 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8078 ..Default::default()
8079 },
8080 ..Default::default()
8081 },
8082 );
8083
8084 let buffer = project
8085 .update(cx, |project, cx| {
8086 project.open_local_buffer(path!("/file.rs"), cx)
8087 })
8088 .await
8089 .unwrap();
8090
8091 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8092 let (editor, cx) = cx.add_window_view(|window, cx| {
8093 build_editor_with_project(project.clone(), buffer, window, cx)
8094 });
8095 editor.update_in(cx, |editor, window, cx| {
8096 editor.set_text("one\ntwo\nthree\n", window, cx)
8097 });
8098 assert!(cx.read(|cx| editor.is_dirty(cx)));
8099
8100 cx.executor().start_waiting();
8101 let fake_server = fake_servers.next().await.unwrap();
8102
8103 {
8104 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8105 move |params, _| async move {
8106 assert_eq!(
8107 params.text_document.uri,
8108 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8109 );
8110 assert_eq!(params.options.tab_size, 4);
8111 Ok(Some(vec![lsp::TextEdit::new(
8112 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8113 ", ".to_string(),
8114 )]))
8115 },
8116 );
8117 let save = editor
8118 .update_in(cx, |editor, window, cx| {
8119 editor.save(true, project.clone(), window, cx)
8120 })
8121 .unwrap();
8122 cx.executor().start_waiting();
8123 save.await;
8124
8125 assert_eq!(
8126 editor.update(cx, |editor, cx| editor.text(cx)),
8127 "one, two\nthree\n"
8128 );
8129 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8130 }
8131
8132 {
8133 editor.update_in(cx, |editor, window, cx| {
8134 editor.set_text("one\ntwo\nthree\n", window, cx)
8135 });
8136 assert!(cx.read(|cx| editor.is_dirty(cx)));
8137
8138 // Ensure we can still save even if formatting hangs.
8139 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8140 move |params, _| async move {
8141 assert_eq!(
8142 params.text_document.uri,
8143 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8144 );
8145 futures::future::pending::<()>().await;
8146 unreachable!()
8147 },
8148 );
8149 let save = editor
8150 .update_in(cx, |editor, window, cx| {
8151 editor.save(true, project.clone(), window, cx)
8152 })
8153 .unwrap();
8154 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8155 cx.executor().start_waiting();
8156 save.await;
8157 assert_eq!(
8158 editor.update(cx, |editor, cx| editor.text(cx)),
8159 "one\ntwo\nthree\n"
8160 );
8161 }
8162
8163 // For non-dirty buffer, no formatting request should be sent
8164 {
8165 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8166
8167 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8168 panic!("Should not be invoked on non-dirty buffer");
8169 });
8170 let save = editor
8171 .update_in(cx, |editor, window, cx| {
8172 editor.save(true, project.clone(), window, cx)
8173 })
8174 .unwrap();
8175 cx.executor().start_waiting();
8176 save.await;
8177 }
8178
8179 // Set rust language override and assert overridden tabsize is sent to language server
8180 update_test_language_settings(cx, |settings| {
8181 settings.languages.insert(
8182 "Rust".into(),
8183 LanguageSettingsContent {
8184 tab_size: NonZeroU32::new(8),
8185 ..Default::default()
8186 },
8187 );
8188 });
8189
8190 {
8191 editor.update_in(cx, |editor, window, cx| {
8192 editor.set_text("somehting_new\n", window, cx)
8193 });
8194 assert!(cx.read(|cx| editor.is_dirty(cx)));
8195 let _formatting_request_signal = fake_server
8196 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8197 assert_eq!(
8198 params.text_document.uri,
8199 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8200 );
8201 assert_eq!(params.options.tab_size, 8);
8202 Ok(Some(vec![]))
8203 });
8204 let save = editor
8205 .update_in(cx, |editor, window, cx| {
8206 editor.save(true, project.clone(), window, cx)
8207 })
8208 .unwrap();
8209 cx.executor().start_waiting();
8210 save.await;
8211 }
8212}
8213
8214#[gpui::test]
8215async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
8216 init_test(cx, |_| {});
8217
8218 let cols = 4;
8219 let rows = 10;
8220 let sample_text_1 = sample_text(rows, cols, 'a');
8221 assert_eq!(
8222 sample_text_1,
8223 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
8224 );
8225 let sample_text_2 = sample_text(rows, cols, 'l');
8226 assert_eq!(
8227 sample_text_2,
8228 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
8229 );
8230 let sample_text_3 = sample_text(rows, cols, 'v');
8231 assert_eq!(
8232 sample_text_3,
8233 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
8234 );
8235
8236 let fs = FakeFs::new(cx.executor());
8237 fs.insert_tree(
8238 path!("/a"),
8239 json!({
8240 "main.rs": sample_text_1,
8241 "other.rs": sample_text_2,
8242 "lib.rs": sample_text_3,
8243 }),
8244 )
8245 .await;
8246
8247 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
8248 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
8249 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
8250
8251 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8252 language_registry.add(rust_lang());
8253 let mut fake_servers = language_registry.register_fake_lsp(
8254 "Rust",
8255 FakeLspAdapter {
8256 capabilities: lsp::ServerCapabilities {
8257 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8258 ..Default::default()
8259 },
8260 ..Default::default()
8261 },
8262 );
8263
8264 let worktree = project.update(cx, |project, cx| {
8265 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
8266 assert_eq!(worktrees.len(), 1);
8267 worktrees.pop().unwrap()
8268 });
8269 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
8270
8271 let buffer_1 = project
8272 .update(cx, |project, cx| {
8273 project.open_buffer((worktree_id, "main.rs"), cx)
8274 })
8275 .await
8276 .unwrap();
8277 let buffer_2 = project
8278 .update(cx, |project, cx| {
8279 project.open_buffer((worktree_id, "other.rs"), cx)
8280 })
8281 .await
8282 .unwrap();
8283 let buffer_3 = project
8284 .update(cx, |project, cx| {
8285 project.open_buffer((worktree_id, "lib.rs"), cx)
8286 })
8287 .await
8288 .unwrap();
8289
8290 let multi_buffer = cx.new(|cx| {
8291 let mut multi_buffer = MultiBuffer::new(ReadWrite);
8292 multi_buffer.push_excerpts(
8293 buffer_1.clone(),
8294 [
8295 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8296 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8297 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8298 ],
8299 cx,
8300 );
8301 multi_buffer.push_excerpts(
8302 buffer_2.clone(),
8303 [
8304 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8305 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8306 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8307 ],
8308 cx,
8309 );
8310 multi_buffer.push_excerpts(
8311 buffer_3.clone(),
8312 [
8313 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8314 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8315 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8316 ],
8317 cx,
8318 );
8319 multi_buffer
8320 });
8321 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
8322 Editor::new(
8323 EditorMode::full(),
8324 multi_buffer,
8325 Some(project.clone()),
8326 window,
8327 cx,
8328 )
8329 });
8330
8331 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8332 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8333 s.select_ranges(Some(1..2))
8334 });
8335 editor.insert("|one|two|three|", window, cx);
8336 });
8337 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8338 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8339 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8340 s.select_ranges(Some(60..70))
8341 });
8342 editor.insert("|four|five|six|", window, cx);
8343 });
8344 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8345
8346 // First two buffers should be edited, but not the third one.
8347 assert_eq!(
8348 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8349 "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}",
8350 );
8351 buffer_1.update(cx, |buffer, _| {
8352 assert!(buffer.is_dirty());
8353 assert_eq!(
8354 buffer.text(),
8355 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
8356 )
8357 });
8358 buffer_2.update(cx, |buffer, _| {
8359 assert!(buffer.is_dirty());
8360 assert_eq!(
8361 buffer.text(),
8362 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
8363 )
8364 });
8365 buffer_3.update(cx, |buffer, _| {
8366 assert!(!buffer.is_dirty());
8367 assert_eq!(buffer.text(), sample_text_3,)
8368 });
8369 cx.executor().run_until_parked();
8370
8371 cx.executor().start_waiting();
8372 let save = multi_buffer_editor
8373 .update_in(cx, |editor, window, cx| {
8374 editor.save(true, project.clone(), window, cx)
8375 })
8376 .unwrap();
8377
8378 let fake_server = fake_servers.next().await.unwrap();
8379 fake_server
8380 .server
8381 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
8382 Ok(Some(vec![lsp::TextEdit::new(
8383 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8384 format!("[{} formatted]", params.text_document.uri),
8385 )]))
8386 })
8387 .detach();
8388 save.await;
8389
8390 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
8391 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
8392 assert_eq!(
8393 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8394 uri!(
8395 "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}"
8396 ),
8397 );
8398 buffer_1.update(cx, |buffer, _| {
8399 assert!(!buffer.is_dirty());
8400 assert_eq!(
8401 buffer.text(),
8402 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
8403 )
8404 });
8405 buffer_2.update(cx, |buffer, _| {
8406 assert!(!buffer.is_dirty());
8407 assert_eq!(
8408 buffer.text(),
8409 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
8410 )
8411 });
8412 buffer_3.update(cx, |buffer, _| {
8413 assert!(!buffer.is_dirty());
8414 assert_eq!(buffer.text(), sample_text_3,)
8415 });
8416}
8417
8418#[gpui::test]
8419async fn test_range_format_during_save(cx: &mut TestAppContext) {
8420 init_test(cx, |_| {});
8421
8422 let fs = FakeFs::new(cx.executor());
8423 fs.insert_file(path!("/file.rs"), Default::default()).await;
8424
8425 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8426
8427 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8428 language_registry.add(rust_lang());
8429 let mut fake_servers = language_registry.register_fake_lsp(
8430 "Rust",
8431 FakeLspAdapter {
8432 capabilities: lsp::ServerCapabilities {
8433 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
8434 ..Default::default()
8435 },
8436 ..Default::default()
8437 },
8438 );
8439
8440 let buffer = project
8441 .update(cx, |project, cx| {
8442 project.open_local_buffer(path!("/file.rs"), cx)
8443 })
8444 .await
8445 .unwrap();
8446
8447 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8448 let (editor, cx) = cx.add_window_view(|window, cx| {
8449 build_editor_with_project(project.clone(), buffer, window, cx)
8450 });
8451 editor.update_in(cx, |editor, window, cx| {
8452 editor.set_text("one\ntwo\nthree\n", window, cx)
8453 });
8454 assert!(cx.read(|cx| editor.is_dirty(cx)));
8455
8456 cx.executor().start_waiting();
8457 let fake_server = fake_servers.next().await.unwrap();
8458
8459 let save = editor
8460 .update_in(cx, |editor, window, cx| {
8461 editor.save(true, project.clone(), window, cx)
8462 })
8463 .unwrap();
8464 fake_server
8465 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8466 assert_eq!(
8467 params.text_document.uri,
8468 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8469 );
8470 assert_eq!(params.options.tab_size, 4);
8471 Ok(Some(vec![lsp::TextEdit::new(
8472 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8473 ", ".to_string(),
8474 )]))
8475 })
8476 .next()
8477 .await;
8478 cx.executor().start_waiting();
8479 save.await;
8480 assert_eq!(
8481 editor.update(cx, |editor, cx| editor.text(cx)),
8482 "one, two\nthree\n"
8483 );
8484 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8485
8486 editor.update_in(cx, |editor, window, cx| {
8487 editor.set_text("one\ntwo\nthree\n", window, cx)
8488 });
8489 assert!(cx.read(|cx| editor.is_dirty(cx)));
8490
8491 // Ensure we can still save even if formatting hangs.
8492 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
8493 move |params, _| async move {
8494 assert_eq!(
8495 params.text_document.uri,
8496 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8497 );
8498 futures::future::pending::<()>().await;
8499 unreachable!()
8500 },
8501 );
8502 let save = editor
8503 .update_in(cx, |editor, window, cx| {
8504 editor.save(true, project.clone(), window, cx)
8505 })
8506 .unwrap();
8507 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8508 cx.executor().start_waiting();
8509 save.await;
8510 assert_eq!(
8511 editor.update(cx, |editor, cx| editor.text(cx)),
8512 "one\ntwo\nthree\n"
8513 );
8514 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8515
8516 // For non-dirty buffer, no formatting request should be sent
8517 let save = editor
8518 .update_in(cx, |editor, window, cx| {
8519 editor.save(true, project.clone(), window, cx)
8520 })
8521 .unwrap();
8522 let _pending_format_request = fake_server
8523 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
8524 panic!("Should not be invoked on non-dirty buffer");
8525 })
8526 .next();
8527 cx.executor().start_waiting();
8528 save.await;
8529
8530 // Set Rust language override and assert overridden tabsize is sent to language server
8531 update_test_language_settings(cx, |settings| {
8532 settings.languages.insert(
8533 "Rust".into(),
8534 LanguageSettingsContent {
8535 tab_size: NonZeroU32::new(8),
8536 ..Default::default()
8537 },
8538 );
8539 });
8540
8541 editor.update_in(cx, |editor, window, cx| {
8542 editor.set_text("somehting_new\n", window, cx)
8543 });
8544 assert!(cx.read(|cx| editor.is_dirty(cx)));
8545 let save = editor
8546 .update_in(cx, |editor, window, cx| {
8547 editor.save(true, project.clone(), window, cx)
8548 })
8549 .unwrap();
8550 fake_server
8551 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8552 assert_eq!(
8553 params.text_document.uri,
8554 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8555 );
8556 assert_eq!(params.options.tab_size, 8);
8557 Ok(Some(vec![]))
8558 })
8559 .next()
8560 .await;
8561 cx.executor().start_waiting();
8562 save.await;
8563}
8564
8565#[gpui::test]
8566async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
8567 init_test(cx, |settings| {
8568 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8569 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8570 ))
8571 });
8572
8573 let fs = FakeFs::new(cx.executor());
8574 fs.insert_file(path!("/file.rs"), Default::default()).await;
8575
8576 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8577
8578 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8579 language_registry.add(Arc::new(Language::new(
8580 LanguageConfig {
8581 name: "Rust".into(),
8582 matcher: LanguageMatcher {
8583 path_suffixes: vec!["rs".to_string()],
8584 ..Default::default()
8585 },
8586 ..LanguageConfig::default()
8587 },
8588 Some(tree_sitter_rust::LANGUAGE.into()),
8589 )));
8590 update_test_language_settings(cx, |settings| {
8591 // Enable Prettier formatting for the same buffer, and ensure
8592 // LSP is called instead of Prettier.
8593 settings.defaults.prettier = Some(PrettierSettings {
8594 allowed: true,
8595 ..PrettierSettings::default()
8596 });
8597 });
8598 let mut fake_servers = language_registry.register_fake_lsp(
8599 "Rust",
8600 FakeLspAdapter {
8601 capabilities: lsp::ServerCapabilities {
8602 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8603 ..Default::default()
8604 },
8605 ..Default::default()
8606 },
8607 );
8608
8609 let buffer = project
8610 .update(cx, |project, cx| {
8611 project.open_local_buffer(path!("/file.rs"), cx)
8612 })
8613 .await
8614 .unwrap();
8615
8616 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8617 let (editor, cx) = cx.add_window_view(|window, cx| {
8618 build_editor_with_project(project.clone(), buffer, window, cx)
8619 });
8620 editor.update_in(cx, |editor, window, cx| {
8621 editor.set_text("one\ntwo\nthree\n", window, cx)
8622 });
8623
8624 cx.executor().start_waiting();
8625 let fake_server = fake_servers.next().await.unwrap();
8626
8627 let format = editor
8628 .update_in(cx, |editor, window, cx| {
8629 editor.perform_format(
8630 project.clone(),
8631 FormatTrigger::Manual,
8632 FormatTarget::Buffers,
8633 window,
8634 cx,
8635 )
8636 })
8637 .unwrap();
8638 fake_server
8639 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8640 assert_eq!(
8641 params.text_document.uri,
8642 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8643 );
8644 assert_eq!(params.options.tab_size, 4);
8645 Ok(Some(vec![lsp::TextEdit::new(
8646 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8647 ", ".to_string(),
8648 )]))
8649 })
8650 .next()
8651 .await;
8652 cx.executor().start_waiting();
8653 format.await;
8654 assert_eq!(
8655 editor.update(cx, |editor, cx| editor.text(cx)),
8656 "one, two\nthree\n"
8657 );
8658
8659 editor.update_in(cx, |editor, window, cx| {
8660 editor.set_text("one\ntwo\nthree\n", window, cx)
8661 });
8662 // Ensure we don't lock if formatting hangs.
8663 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8664 move |params, _| async move {
8665 assert_eq!(
8666 params.text_document.uri,
8667 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8668 );
8669 futures::future::pending::<()>().await;
8670 unreachable!()
8671 },
8672 );
8673 let format = editor
8674 .update_in(cx, |editor, window, cx| {
8675 editor.perform_format(
8676 project,
8677 FormatTrigger::Manual,
8678 FormatTarget::Buffers,
8679 window,
8680 cx,
8681 )
8682 })
8683 .unwrap();
8684 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8685 cx.executor().start_waiting();
8686 format.await;
8687 assert_eq!(
8688 editor.update(cx, |editor, cx| editor.text(cx)),
8689 "one\ntwo\nthree\n"
8690 );
8691}
8692
8693#[gpui::test]
8694async fn test_multiple_formatters(cx: &mut TestAppContext) {
8695 init_test(cx, |settings| {
8696 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
8697 settings.defaults.formatter =
8698 Some(language_settings::SelectedFormatter::List(FormatterList(
8699 vec![
8700 Formatter::LanguageServer { name: None },
8701 Formatter::CodeActions(
8702 [
8703 ("code-action-1".into(), true),
8704 ("code-action-2".into(), true),
8705 ]
8706 .into_iter()
8707 .collect(),
8708 ),
8709 ]
8710 .into(),
8711 )))
8712 });
8713
8714 let fs = FakeFs::new(cx.executor());
8715 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
8716 .await;
8717
8718 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8719 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8720 language_registry.add(rust_lang());
8721
8722 let mut fake_servers = language_registry.register_fake_lsp(
8723 "Rust",
8724 FakeLspAdapter {
8725 capabilities: lsp::ServerCapabilities {
8726 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8727 execute_command_provider: Some(lsp::ExecuteCommandOptions {
8728 commands: vec!["the-command-for-code-action-1".into()],
8729 ..Default::default()
8730 }),
8731 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
8732 ..Default::default()
8733 },
8734 ..Default::default()
8735 },
8736 );
8737
8738 let buffer = project
8739 .update(cx, |project, cx| {
8740 project.open_local_buffer(path!("/file.rs"), cx)
8741 })
8742 .await
8743 .unwrap();
8744
8745 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8746 let (editor, cx) = cx.add_window_view(|window, cx| {
8747 build_editor_with_project(project.clone(), buffer, window, cx)
8748 });
8749
8750 cx.executor().start_waiting();
8751
8752 let fake_server = fake_servers.next().await.unwrap();
8753 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8754 move |_params, _| async move {
8755 Ok(Some(vec![lsp::TextEdit::new(
8756 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8757 "applied-formatting\n".to_string(),
8758 )]))
8759 },
8760 );
8761 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
8762 move |params, _| async move {
8763 assert_eq!(
8764 params.context.only,
8765 Some(vec!["code-action-1".into(), "code-action-2".into()])
8766 );
8767 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
8768 Ok(Some(vec![
8769 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
8770 kind: Some("code-action-1".into()),
8771 edit: Some(lsp::WorkspaceEdit::new(
8772 [(
8773 uri.clone(),
8774 vec![lsp::TextEdit::new(
8775 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8776 "applied-code-action-1-edit\n".to_string(),
8777 )],
8778 )]
8779 .into_iter()
8780 .collect(),
8781 )),
8782 command: Some(lsp::Command {
8783 command: "the-command-for-code-action-1".into(),
8784 ..Default::default()
8785 }),
8786 ..Default::default()
8787 }),
8788 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
8789 kind: Some("code-action-2".into()),
8790 edit: Some(lsp::WorkspaceEdit::new(
8791 [(
8792 uri.clone(),
8793 vec![lsp::TextEdit::new(
8794 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8795 "applied-code-action-2-edit\n".to_string(),
8796 )],
8797 )]
8798 .into_iter()
8799 .collect(),
8800 )),
8801 ..Default::default()
8802 }),
8803 ]))
8804 },
8805 );
8806
8807 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
8808 move |params, _| async move { Ok(params) }
8809 });
8810
8811 let command_lock = Arc::new(futures::lock::Mutex::new(()));
8812 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
8813 let fake = fake_server.clone();
8814 let lock = command_lock.clone();
8815 move |params, _| {
8816 assert_eq!(params.command, "the-command-for-code-action-1");
8817 let fake = fake.clone();
8818 let lock = lock.clone();
8819 async move {
8820 lock.lock().await;
8821 fake.server
8822 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
8823 label: None,
8824 edit: lsp::WorkspaceEdit {
8825 changes: Some(
8826 [(
8827 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
8828 vec![lsp::TextEdit {
8829 range: lsp::Range::new(
8830 lsp::Position::new(0, 0),
8831 lsp::Position::new(0, 0),
8832 ),
8833 new_text: "applied-code-action-1-command\n".into(),
8834 }],
8835 )]
8836 .into_iter()
8837 .collect(),
8838 ),
8839 ..Default::default()
8840 },
8841 })
8842 .await
8843 .unwrap();
8844 Ok(Some(json!(null)))
8845 }
8846 }
8847 });
8848
8849 cx.executor().start_waiting();
8850 editor
8851 .update_in(cx, |editor, window, cx| {
8852 editor.perform_format(
8853 project.clone(),
8854 FormatTrigger::Manual,
8855 FormatTarget::Buffers,
8856 window,
8857 cx,
8858 )
8859 })
8860 .unwrap()
8861 .await;
8862 editor.update(cx, |editor, cx| {
8863 assert_eq!(
8864 editor.text(cx),
8865 r#"
8866 applied-code-action-2-edit
8867 applied-code-action-1-command
8868 applied-code-action-1-edit
8869 applied-formatting
8870 one
8871 two
8872 three
8873 "#
8874 .unindent()
8875 );
8876 });
8877
8878 editor.update_in(cx, |editor, window, cx| {
8879 editor.undo(&Default::default(), window, cx);
8880 assert_eq!(editor.text(cx), "one \ntwo \nthree");
8881 });
8882
8883 // Perform a manual edit while waiting for an LSP command
8884 // that's being run as part of a formatting code action.
8885 let lock_guard = command_lock.lock().await;
8886 let format = editor
8887 .update_in(cx, |editor, window, cx| {
8888 editor.perform_format(
8889 project.clone(),
8890 FormatTrigger::Manual,
8891 FormatTarget::Buffers,
8892 window,
8893 cx,
8894 )
8895 })
8896 .unwrap();
8897 cx.run_until_parked();
8898 editor.update(cx, |editor, cx| {
8899 assert_eq!(
8900 editor.text(cx),
8901 r#"
8902 applied-code-action-1-edit
8903 applied-formatting
8904 one
8905 two
8906 three
8907 "#
8908 .unindent()
8909 );
8910
8911 editor.buffer.update(cx, |buffer, cx| {
8912 let ix = buffer.len(cx);
8913 buffer.edit([(ix..ix, "edited\n")], None, cx);
8914 });
8915 });
8916
8917 // Allow the LSP command to proceed. Because the buffer was edited,
8918 // the second code action will not be run.
8919 drop(lock_guard);
8920 format.await;
8921 editor.update_in(cx, |editor, window, cx| {
8922 assert_eq!(
8923 editor.text(cx),
8924 r#"
8925 applied-code-action-1-command
8926 applied-code-action-1-edit
8927 applied-formatting
8928 one
8929 two
8930 three
8931 edited
8932 "#
8933 .unindent()
8934 );
8935
8936 // The manual edit is undone first, because it is the last thing the user did
8937 // (even though the command completed afterwards).
8938 editor.undo(&Default::default(), window, cx);
8939 assert_eq!(
8940 editor.text(cx),
8941 r#"
8942 applied-code-action-1-command
8943 applied-code-action-1-edit
8944 applied-formatting
8945 one
8946 two
8947 three
8948 "#
8949 .unindent()
8950 );
8951
8952 // All the formatting (including the command, which completed after the manual edit)
8953 // is undone together.
8954 editor.undo(&Default::default(), window, cx);
8955 assert_eq!(editor.text(cx), "one \ntwo \nthree");
8956 });
8957}
8958
8959#[gpui::test]
8960async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
8961 init_test(cx, |settings| {
8962 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8963 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8964 ))
8965 });
8966
8967 let fs = FakeFs::new(cx.executor());
8968 fs.insert_file(path!("/file.ts"), Default::default()).await;
8969
8970 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8971
8972 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8973 language_registry.add(Arc::new(Language::new(
8974 LanguageConfig {
8975 name: "TypeScript".into(),
8976 matcher: LanguageMatcher {
8977 path_suffixes: vec!["ts".to_string()],
8978 ..Default::default()
8979 },
8980 ..LanguageConfig::default()
8981 },
8982 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
8983 )));
8984 update_test_language_settings(cx, |settings| {
8985 settings.defaults.prettier = Some(PrettierSettings {
8986 allowed: true,
8987 ..PrettierSettings::default()
8988 });
8989 });
8990 let mut fake_servers = language_registry.register_fake_lsp(
8991 "TypeScript",
8992 FakeLspAdapter {
8993 capabilities: lsp::ServerCapabilities {
8994 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
8995 ..Default::default()
8996 },
8997 ..Default::default()
8998 },
8999 );
9000
9001 let buffer = project
9002 .update(cx, |project, cx| {
9003 project.open_local_buffer(path!("/file.ts"), cx)
9004 })
9005 .await
9006 .unwrap();
9007
9008 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9009 let (editor, cx) = cx.add_window_view(|window, cx| {
9010 build_editor_with_project(project.clone(), buffer, window, cx)
9011 });
9012 editor.update_in(cx, |editor, window, cx| {
9013 editor.set_text(
9014 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9015 window,
9016 cx,
9017 )
9018 });
9019
9020 cx.executor().start_waiting();
9021 let fake_server = fake_servers.next().await.unwrap();
9022
9023 let format = editor
9024 .update_in(cx, |editor, window, cx| {
9025 editor.perform_code_action_kind(
9026 project.clone(),
9027 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9028 window,
9029 cx,
9030 )
9031 })
9032 .unwrap();
9033 fake_server
9034 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
9035 assert_eq!(
9036 params.text_document.uri,
9037 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9038 );
9039 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
9040 lsp::CodeAction {
9041 title: "Organize Imports".to_string(),
9042 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
9043 edit: Some(lsp::WorkspaceEdit {
9044 changes: Some(
9045 [(
9046 params.text_document.uri.clone(),
9047 vec![lsp::TextEdit::new(
9048 lsp::Range::new(
9049 lsp::Position::new(1, 0),
9050 lsp::Position::new(2, 0),
9051 ),
9052 "".to_string(),
9053 )],
9054 )]
9055 .into_iter()
9056 .collect(),
9057 ),
9058 ..Default::default()
9059 }),
9060 ..Default::default()
9061 },
9062 )]))
9063 })
9064 .next()
9065 .await;
9066 cx.executor().start_waiting();
9067 format.await;
9068 assert_eq!(
9069 editor.update(cx, |editor, cx| editor.text(cx)),
9070 "import { a } from 'module';\n\nconst x = a;\n"
9071 );
9072
9073 editor.update_in(cx, |editor, window, cx| {
9074 editor.set_text(
9075 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9076 window,
9077 cx,
9078 )
9079 });
9080 // Ensure we don't lock if code action hangs.
9081 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9082 move |params, _| async move {
9083 assert_eq!(
9084 params.text_document.uri,
9085 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9086 );
9087 futures::future::pending::<()>().await;
9088 unreachable!()
9089 },
9090 );
9091 let format = editor
9092 .update_in(cx, |editor, window, cx| {
9093 editor.perform_code_action_kind(
9094 project,
9095 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9096 window,
9097 cx,
9098 )
9099 })
9100 .unwrap();
9101 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
9102 cx.executor().start_waiting();
9103 format.await;
9104 assert_eq!(
9105 editor.update(cx, |editor, cx| editor.text(cx)),
9106 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
9107 );
9108}
9109
9110#[gpui::test]
9111async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
9112 init_test(cx, |_| {});
9113
9114 let mut cx = EditorLspTestContext::new_rust(
9115 lsp::ServerCapabilities {
9116 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9117 ..Default::default()
9118 },
9119 cx,
9120 )
9121 .await;
9122
9123 cx.set_state(indoc! {"
9124 one.twoˇ
9125 "});
9126
9127 // The format request takes a long time. When it completes, it inserts
9128 // a newline and an indent before the `.`
9129 cx.lsp
9130 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
9131 let executor = cx.background_executor().clone();
9132 async move {
9133 executor.timer(Duration::from_millis(100)).await;
9134 Ok(Some(vec![lsp::TextEdit {
9135 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
9136 new_text: "\n ".into(),
9137 }]))
9138 }
9139 });
9140
9141 // Submit a format request.
9142 let format_1 = cx
9143 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9144 .unwrap();
9145 cx.executor().run_until_parked();
9146
9147 // Submit a second format request.
9148 let format_2 = cx
9149 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9150 .unwrap();
9151 cx.executor().run_until_parked();
9152
9153 // Wait for both format requests to complete
9154 cx.executor().advance_clock(Duration::from_millis(200));
9155 cx.executor().start_waiting();
9156 format_1.await.unwrap();
9157 cx.executor().start_waiting();
9158 format_2.await.unwrap();
9159
9160 // The formatting edits only happens once.
9161 cx.assert_editor_state(indoc! {"
9162 one
9163 .twoˇ
9164 "});
9165}
9166
9167#[gpui::test]
9168async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
9169 init_test(cx, |settings| {
9170 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
9171 });
9172
9173 let mut cx = EditorLspTestContext::new_rust(
9174 lsp::ServerCapabilities {
9175 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9176 ..Default::default()
9177 },
9178 cx,
9179 )
9180 .await;
9181
9182 // Set up a buffer white some trailing whitespace and no trailing newline.
9183 cx.set_state(
9184 &[
9185 "one ", //
9186 "twoˇ", //
9187 "three ", //
9188 "four", //
9189 ]
9190 .join("\n"),
9191 );
9192
9193 // Submit a format request.
9194 let format = cx
9195 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9196 .unwrap();
9197
9198 // Record which buffer changes have been sent to the language server
9199 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
9200 cx.lsp
9201 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
9202 let buffer_changes = buffer_changes.clone();
9203 move |params, _| {
9204 buffer_changes.lock().extend(
9205 params
9206 .content_changes
9207 .into_iter()
9208 .map(|e| (e.range.unwrap(), e.text)),
9209 );
9210 }
9211 });
9212
9213 // Handle formatting requests to the language server.
9214 cx.lsp
9215 .set_request_handler::<lsp::request::Formatting, _, _>({
9216 let buffer_changes = buffer_changes.clone();
9217 move |_, _| {
9218 // When formatting is requested, trailing whitespace has already been stripped,
9219 // and the trailing newline has already been added.
9220 assert_eq!(
9221 &buffer_changes.lock()[1..],
9222 &[
9223 (
9224 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
9225 "".into()
9226 ),
9227 (
9228 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
9229 "".into()
9230 ),
9231 (
9232 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
9233 "\n".into()
9234 ),
9235 ]
9236 );
9237
9238 // Insert blank lines between each line of the buffer.
9239 async move {
9240 Ok(Some(vec![
9241 lsp::TextEdit {
9242 range: lsp::Range::new(
9243 lsp::Position::new(1, 0),
9244 lsp::Position::new(1, 0),
9245 ),
9246 new_text: "\n".into(),
9247 },
9248 lsp::TextEdit {
9249 range: lsp::Range::new(
9250 lsp::Position::new(2, 0),
9251 lsp::Position::new(2, 0),
9252 ),
9253 new_text: "\n".into(),
9254 },
9255 ]))
9256 }
9257 }
9258 });
9259
9260 // After formatting the buffer, the trailing whitespace is stripped,
9261 // a newline is appended, and the edits provided by the language server
9262 // have been applied.
9263 format.await.unwrap();
9264 cx.assert_editor_state(
9265 &[
9266 "one", //
9267 "", //
9268 "twoˇ", //
9269 "", //
9270 "three", //
9271 "four", //
9272 "", //
9273 ]
9274 .join("\n"),
9275 );
9276
9277 // Undoing the formatting undoes the trailing whitespace removal, the
9278 // trailing newline, and the LSP edits.
9279 cx.update_buffer(|buffer, cx| buffer.undo(cx));
9280 cx.assert_editor_state(
9281 &[
9282 "one ", //
9283 "twoˇ", //
9284 "three ", //
9285 "four", //
9286 ]
9287 .join("\n"),
9288 );
9289}
9290
9291#[gpui::test]
9292async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
9293 cx: &mut TestAppContext,
9294) {
9295 init_test(cx, |_| {});
9296
9297 cx.update(|cx| {
9298 cx.update_global::<SettingsStore, _>(|settings, cx| {
9299 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9300 settings.auto_signature_help = Some(true);
9301 });
9302 });
9303 });
9304
9305 let mut cx = EditorLspTestContext::new_rust(
9306 lsp::ServerCapabilities {
9307 signature_help_provider: Some(lsp::SignatureHelpOptions {
9308 ..Default::default()
9309 }),
9310 ..Default::default()
9311 },
9312 cx,
9313 )
9314 .await;
9315
9316 let language = Language::new(
9317 LanguageConfig {
9318 name: "Rust".into(),
9319 brackets: BracketPairConfig {
9320 pairs: vec![
9321 BracketPair {
9322 start: "{".to_string(),
9323 end: "}".to_string(),
9324 close: true,
9325 surround: true,
9326 newline: true,
9327 },
9328 BracketPair {
9329 start: "(".to_string(),
9330 end: ")".to_string(),
9331 close: true,
9332 surround: true,
9333 newline: true,
9334 },
9335 BracketPair {
9336 start: "/*".to_string(),
9337 end: " */".to_string(),
9338 close: true,
9339 surround: true,
9340 newline: true,
9341 },
9342 BracketPair {
9343 start: "[".to_string(),
9344 end: "]".to_string(),
9345 close: false,
9346 surround: false,
9347 newline: true,
9348 },
9349 BracketPair {
9350 start: "\"".to_string(),
9351 end: "\"".to_string(),
9352 close: true,
9353 surround: true,
9354 newline: false,
9355 },
9356 BracketPair {
9357 start: "<".to_string(),
9358 end: ">".to_string(),
9359 close: false,
9360 surround: true,
9361 newline: true,
9362 },
9363 ],
9364 ..Default::default()
9365 },
9366 autoclose_before: "})]".to_string(),
9367 ..Default::default()
9368 },
9369 Some(tree_sitter_rust::LANGUAGE.into()),
9370 );
9371 let language = Arc::new(language);
9372
9373 cx.language_registry().add(language.clone());
9374 cx.update_buffer(|buffer, cx| {
9375 buffer.set_language(Some(language), cx);
9376 });
9377
9378 cx.set_state(
9379 &r#"
9380 fn main() {
9381 sampleˇ
9382 }
9383 "#
9384 .unindent(),
9385 );
9386
9387 cx.update_editor(|editor, window, cx| {
9388 editor.handle_input("(", window, cx);
9389 });
9390 cx.assert_editor_state(
9391 &"
9392 fn main() {
9393 sample(ˇ)
9394 }
9395 "
9396 .unindent(),
9397 );
9398
9399 let mocked_response = lsp::SignatureHelp {
9400 signatures: vec![lsp::SignatureInformation {
9401 label: "fn sample(param1: u8, param2: u8)".to_string(),
9402 documentation: None,
9403 parameters: Some(vec![
9404 lsp::ParameterInformation {
9405 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9406 documentation: None,
9407 },
9408 lsp::ParameterInformation {
9409 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9410 documentation: None,
9411 },
9412 ]),
9413 active_parameter: None,
9414 }],
9415 active_signature: Some(0),
9416 active_parameter: Some(0),
9417 };
9418 handle_signature_help_request(&mut cx, mocked_response).await;
9419
9420 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9421 .await;
9422
9423 cx.editor(|editor, _, _| {
9424 let signature_help_state = editor.signature_help_state.popover().cloned();
9425 assert_eq!(
9426 signature_help_state.unwrap().label,
9427 "param1: u8, param2: u8"
9428 );
9429 });
9430}
9431
9432#[gpui::test]
9433async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
9434 init_test(cx, |_| {});
9435
9436 cx.update(|cx| {
9437 cx.update_global::<SettingsStore, _>(|settings, cx| {
9438 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9439 settings.auto_signature_help = Some(false);
9440 settings.show_signature_help_after_edits = Some(false);
9441 });
9442 });
9443 });
9444
9445 let mut cx = EditorLspTestContext::new_rust(
9446 lsp::ServerCapabilities {
9447 signature_help_provider: Some(lsp::SignatureHelpOptions {
9448 ..Default::default()
9449 }),
9450 ..Default::default()
9451 },
9452 cx,
9453 )
9454 .await;
9455
9456 let language = Language::new(
9457 LanguageConfig {
9458 name: "Rust".into(),
9459 brackets: BracketPairConfig {
9460 pairs: vec![
9461 BracketPair {
9462 start: "{".to_string(),
9463 end: "}".to_string(),
9464 close: true,
9465 surround: true,
9466 newline: true,
9467 },
9468 BracketPair {
9469 start: "(".to_string(),
9470 end: ")".to_string(),
9471 close: true,
9472 surround: true,
9473 newline: true,
9474 },
9475 BracketPair {
9476 start: "/*".to_string(),
9477 end: " */".to_string(),
9478 close: true,
9479 surround: true,
9480 newline: true,
9481 },
9482 BracketPair {
9483 start: "[".to_string(),
9484 end: "]".to_string(),
9485 close: false,
9486 surround: false,
9487 newline: true,
9488 },
9489 BracketPair {
9490 start: "\"".to_string(),
9491 end: "\"".to_string(),
9492 close: true,
9493 surround: true,
9494 newline: false,
9495 },
9496 BracketPair {
9497 start: "<".to_string(),
9498 end: ">".to_string(),
9499 close: false,
9500 surround: true,
9501 newline: true,
9502 },
9503 ],
9504 ..Default::default()
9505 },
9506 autoclose_before: "})]".to_string(),
9507 ..Default::default()
9508 },
9509 Some(tree_sitter_rust::LANGUAGE.into()),
9510 );
9511 let language = Arc::new(language);
9512
9513 cx.language_registry().add(language.clone());
9514 cx.update_buffer(|buffer, cx| {
9515 buffer.set_language(Some(language), cx);
9516 });
9517
9518 // Ensure that signature_help is not called when no signature help is enabled.
9519 cx.set_state(
9520 &r#"
9521 fn main() {
9522 sampleˇ
9523 }
9524 "#
9525 .unindent(),
9526 );
9527 cx.update_editor(|editor, window, cx| {
9528 editor.handle_input("(", window, cx);
9529 });
9530 cx.assert_editor_state(
9531 &"
9532 fn main() {
9533 sample(ˇ)
9534 }
9535 "
9536 .unindent(),
9537 );
9538 cx.editor(|editor, _, _| {
9539 assert!(editor.signature_help_state.task().is_none());
9540 });
9541
9542 let mocked_response = lsp::SignatureHelp {
9543 signatures: vec![lsp::SignatureInformation {
9544 label: "fn sample(param1: u8, param2: u8)".to_string(),
9545 documentation: None,
9546 parameters: Some(vec![
9547 lsp::ParameterInformation {
9548 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9549 documentation: None,
9550 },
9551 lsp::ParameterInformation {
9552 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9553 documentation: None,
9554 },
9555 ]),
9556 active_parameter: None,
9557 }],
9558 active_signature: Some(0),
9559 active_parameter: Some(0),
9560 };
9561
9562 // Ensure that signature_help is called when enabled afte edits
9563 cx.update(|_, cx| {
9564 cx.update_global::<SettingsStore, _>(|settings, cx| {
9565 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9566 settings.auto_signature_help = Some(false);
9567 settings.show_signature_help_after_edits = Some(true);
9568 });
9569 });
9570 });
9571 cx.set_state(
9572 &r#"
9573 fn main() {
9574 sampleˇ
9575 }
9576 "#
9577 .unindent(),
9578 );
9579 cx.update_editor(|editor, window, cx| {
9580 editor.handle_input("(", window, cx);
9581 });
9582 cx.assert_editor_state(
9583 &"
9584 fn main() {
9585 sample(ˇ)
9586 }
9587 "
9588 .unindent(),
9589 );
9590 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9591 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9592 .await;
9593 cx.update_editor(|editor, _, _| {
9594 let signature_help_state = editor.signature_help_state.popover().cloned();
9595 assert!(signature_help_state.is_some());
9596 assert_eq!(
9597 signature_help_state.unwrap().label,
9598 "param1: u8, param2: u8"
9599 );
9600 editor.signature_help_state = SignatureHelpState::default();
9601 });
9602
9603 // Ensure that signature_help is called when auto signature help override is enabled
9604 cx.update(|_, cx| {
9605 cx.update_global::<SettingsStore, _>(|settings, cx| {
9606 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9607 settings.auto_signature_help = Some(true);
9608 settings.show_signature_help_after_edits = Some(false);
9609 });
9610 });
9611 });
9612 cx.set_state(
9613 &r#"
9614 fn main() {
9615 sampleˇ
9616 }
9617 "#
9618 .unindent(),
9619 );
9620 cx.update_editor(|editor, window, cx| {
9621 editor.handle_input("(", window, cx);
9622 });
9623 cx.assert_editor_state(
9624 &"
9625 fn main() {
9626 sample(ˇ)
9627 }
9628 "
9629 .unindent(),
9630 );
9631 handle_signature_help_request(&mut cx, mocked_response).await;
9632 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9633 .await;
9634 cx.editor(|editor, _, _| {
9635 let signature_help_state = editor.signature_help_state.popover().cloned();
9636 assert!(signature_help_state.is_some());
9637 assert_eq!(
9638 signature_help_state.unwrap().label,
9639 "param1: u8, param2: u8"
9640 );
9641 });
9642}
9643
9644#[gpui::test]
9645async fn test_signature_help(cx: &mut TestAppContext) {
9646 init_test(cx, |_| {});
9647 cx.update(|cx| {
9648 cx.update_global::<SettingsStore, _>(|settings, cx| {
9649 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9650 settings.auto_signature_help = Some(true);
9651 });
9652 });
9653 });
9654
9655 let mut cx = EditorLspTestContext::new_rust(
9656 lsp::ServerCapabilities {
9657 signature_help_provider: Some(lsp::SignatureHelpOptions {
9658 ..Default::default()
9659 }),
9660 ..Default::default()
9661 },
9662 cx,
9663 )
9664 .await;
9665
9666 // A test that directly calls `show_signature_help`
9667 cx.update_editor(|editor, window, cx| {
9668 editor.show_signature_help(&ShowSignatureHelp, window, cx);
9669 });
9670
9671 let mocked_response = lsp::SignatureHelp {
9672 signatures: vec![lsp::SignatureInformation {
9673 label: "fn sample(param1: u8, param2: u8)".to_string(),
9674 documentation: None,
9675 parameters: Some(vec![
9676 lsp::ParameterInformation {
9677 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9678 documentation: None,
9679 },
9680 lsp::ParameterInformation {
9681 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9682 documentation: None,
9683 },
9684 ]),
9685 active_parameter: None,
9686 }],
9687 active_signature: Some(0),
9688 active_parameter: Some(0),
9689 };
9690 handle_signature_help_request(&mut cx, mocked_response).await;
9691
9692 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9693 .await;
9694
9695 cx.editor(|editor, _, _| {
9696 let signature_help_state = editor.signature_help_state.popover().cloned();
9697 assert!(signature_help_state.is_some());
9698 assert_eq!(
9699 signature_help_state.unwrap().label,
9700 "param1: u8, param2: u8"
9701 );
9702 });
9703
9704 // When exiting outside from inside the brackets, `signature_help` is closed.
9705 cx.set_state(indoc! {"
9706 fn main() {
9707 sample(ˇ);
9708 }
9709
9710 fn sample(param1: u8, param2: u8) {}
9711 "});
9712
9713 cx.update_editor(|editor, window, cx| {
9714 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
9715 });
9716
9717 let mocked_response = lsp::SignatureHelp {
9718 signatures: Vec::new(),
9719 active_signature: None,
9720 active_parameter: None,
9721 };
9722 handle_signature_help_request(&mut cx, mocked_response).await;
9723
9724 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
9725 .await;
9726
9727 cx.editor(|editor, _, _| {
9728 assert!(!editor.signature_help_state.is_shown());
9729 });
9730
9731 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
9732 cx.set_state(indoc! {"
9733 fn main() {
9734 sample(ˇ);
9735 }
9736
9737 fn sample(param1: u8, param2: u8) {}
9738 "});
9739
9740 let mocked_response = lsp::SignatureHelp {
9741 signatures: vec![lsp::SignatureInformation {
9742 label: "fn sample(param1: u8, param2: u8)".to_string(),
9743 documentation: None,
9744 parameters: Some(vec![
9745 lsp::ParameterInformation {
9746 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9747 documentation: None,
9748 },
9749 lsp::ParameterInformation {
9750 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9751 documentation: None,
9752 },
9753 ]),
9754 active_parameter: None,
9755 }],
9756 active_signature: Some(0),
9757 active_parameter: Some(0),
9758 };
9759 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9760 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9761 .await;
9762 cx.editor(|editor, _, _| {
9763 assert!(editor.signature_help_state.is_shown());
9764 });
9765
9766 // Restore the popover with more parameter input
9767 cx.set_state(indoc! {"
9768 fn main() {
9769 sample(param1, param2ˇ);
9770 }
9771
9772 fn sample(param1: u8, param2: u8) {}
9773 "});
9774
9775 let mocked_response = lsp::SignatureHelp {
9776 signatures: vec![lsp::SignatureInformation {
9777 label: "fn sample(param1: u8, param2: u8)".to_string(),
9778 documentation: None,
9779 parameters: Some(vec![
9780 lsp::ParameterInformation {
9781 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9782 documentation: None,
9783 },
9784 lsp::ParameterInformation {
9785 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9786 documentation: None,
9787 },
9788 ]),
9789 active_parameter: None,
9790 }],
9791 active_signature: Some(0),
9792 active_parameter: Some(1),
9793 };
9794 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9795 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9796 .await;
9797
9798 // When selecting a range, the popover is gone.
9799 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
9800 cx.update_editor(|editor, window, cx| {
9801 editor.change_selections(None, window, cx, |s| {
9802 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
9803 })
9804 });
9805 cx.assert_editor_state(indoc! {"
9806 fn main() {
9807 sample(param1, «ˇparam2»);
9808 }
9809
9810 fn sample(param1: u8, param2: u8) {}
9811 "});
9812 cx.editor(|editor, _, _| {
9813 assert!(!editor.signature_help_state.is_shown());
9814 });
9815
9816 // When unselecting again, the popover is back if within the brackets.
9817 cx.update_editor(|editor, window, cx| {
9818 editor.change_selections(None, window, cx, |s| {
9819 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9820 })
9821 });
9822 cx.assert_editor_state(indoc! {"
9823 fn main() {
9824 sample(param1, ˇparam2);
9825 }
9826
9827 fn sample(param1: u8, param2: u8) {}
9828 "});
9829 handle_signature_help_request(&mut cx, mocked_response).await;
9830 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9831 .await;
9832 cx.editor(|editor, _, _| {
9833 assert!(editor.signature_help_state.is_shown());
9834 });
9835
9836 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
9837 cx.update_editor(|editor, window, cx| {
9838 editor.change_selections(None, window, cx, |s| {
9839 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
9840 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9841 })
9842 });
9843 cx.assert_editor_state(indoc! {"
9844 fn main() {
9845 sample(param1, ˇparam2);
9846 }
9847
9848 fn sample(param1: u8, param2: u8) {}
9849 "});
9850
9851 let mocked_response = lsp::SignatureHelp {
9852 signatures: vec![lsp::SignatureInformation {
9853 label: "fn sample(param1: u8, param2: u8)".to_string(),
9854 documentation: None,
9855 parameters: Some(vec![
9856 lsp::ParameterInformation {
9857 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9858 documentation: None,
9859 },
9860 lsp::ParameterInformation {
9861 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9862 documentation: None,
9863 },
9864 ]),
9865 active_parameter: None,
9866 }],
9867 active_signature: Some(0),
9868 active_parameter: Some(1),
9869 };
9870 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9871 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9872 .await;
9873 cx.update_editor(|editor, _, cx| {
9874 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
9875 });
9876 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
9877 .await;
9878 cx.update_editor(|editor, window, cx| {
9879 editor.change_selections(None, window, cx, |s| {
9880 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
9881 })
9882 });
9883 cx.assert_editor_state(indoc! {"
9884 fn main() {
9885 sample(param1, «ˇparam2»);
9886 }
9887
9888 fn sample(param1: u8, param2: u8) {}
9889 "});
9890 cx.update_editor(|editor, window, cx| {
9891 editor.change_selections(None, window, cx, |s| {
9892 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9893 })
9894 });
9895 cx.assert_editor_state(indoc! {"
9896 fn main() {
9897 sample(param1, ˇparam2);
9898 }
9899
9900 fn sample(param1: u8, param2: u8) {}
9901 "});
9902 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
9903 .await;
9904}
9905
9906#[gpui::test]
9907async fn test_completion_mode(cx: &mut TestAppContext) {
9908 init_test(cx, |_| {});
9909 let mut cx = EditorLspTestContext::new_rust(
9910 lsp::ServerCapabilities {
9911 completion_provider: Some(lsp::CompletionOptions {
9912 resolve_provider: Some(true),
9913 ..Default::default()
9914 }),
9915 ..Default::default()
9916 },
9917 cx,
9918 )
9919 .await;
9920
9921 struct Run {
9922 run_description: &'static str,
9923 initial_state: String,
9924 buffer_marked_text: String,
9925 completion_text: &'static str,
9926 expected_with_insert_mode: String,
9927 expected_with_replace_mode: String,
9928 expected_with_replace_subsequence_mode: String,
9929 expected_with_replace_suffix_mode: String,
9930 }
9931
9932 let runs = [
9933 Run {
9934 run_description: "Start of word matches completion text",
9935 initial_state: "before ediˇ after".into(),
9936 buffer_marked_text: "before <edi|> after".into(),
9937 completion_text: "editor",
9938 expected_with_insert_mode: "before editorˇ after".into(),
9939 expected_with_replace_mode: "before editorˇ after".into(),
9940 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
9941 expected_with_replace_suffix_mode: "before editorˇ after".into(),
9942 },
9943 Run {
9944 run_description: "Accept same text at the middle of the word",
9945 initial_state: "before ediˇtor after".into(),
9946 buffer_marked_text: "before <edi|tor> after".into(),
9947 completion_text: "editor",
9948 expected_with_insert_mode: "before editorˇtor after".into(),
9949 expected_with_replace_mode: "before editorˇ after".into(),
9950 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
9951 expected_with_replace_suffix_mode: "before editorˇ after".into(),
9952 },
9953 Run {
9954 run_description: "End of word matches completion text -- cursor at end",
9955 initial_state: "before torˇ after".into(),
9956 buffer_marked_text: "before <tor|> after".into(),
9957 completion_text: "editor",
9958 expected_with_insert_mode: "before editorˇ after".into(),
9959 expected_with_replace_mode: "before editorˇ after".into(),
9960 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
9961 expected_with_replace_suffix_mode: "before editorˇ after".into(),
9962 },
9963 Run {
9964 run_description: "End of word matches completion text -- cursor at start",
9965 initial_state: "before ˇtor after".into(),
9966 buffer_marked_text: "before <|tor> after".into(),
9967 completion_text: "editor",
9968 expected_with_insert_mode: "before editorˇtor after".into(),
9969 expected_with_replace_mode: "before editorˇ after".into(),
9970 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
9971 expected_with_replace_suffix_mode: "before editorˇ after".into(),
9972 },
9973 Run {
9974 run_description: "Prepend text containing whitespace",
9975 initial_state: "pˇfield: bool".into(),
9976 buffer_marked_text: "<p|field>: bool".into(),
9977 completion_text: "pub ",
9978 expected_with_insert_mode: "pub ˇfield: bool".into(),
9979 expected_with_replace_mode: "pub ˇ: bool".into(),
9980 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
9981 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
9982 },
9983 Run {
9984 run_description: "Add element to start of list",
9985 initial_state: "[element_ˇelement_2]".into(),
9986 buffer_marked_text: "[<element_|element_2>]".into(),
9987 completion_text: "element_1",
9988 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
9989 expected_with_replace_mode: "[element_1ˇ]".into(),
9990 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
9991 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
9992 },
9993 Run {
9994 run_description: "Add element to start of list -- first and second elements are equal",
9995 initial_state: "[elˇelement]".into(),
9996 buffer_marked_text: "[<el|element>]".into(),
9997 completion_text: "element",
9998 expected_with_insert_mode: "[elementˇelement]".into(),
9999 expected_with_replace_mode: "[elementˇ]".into(),
10000 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
10001 expected_with_replace_suffix_mode: "[elementˇ]".into(),
10002 },
10003 Run {
10004 run_description: "Ends with matching suffix",
10005 initial_state: "SubˇError".into(),
10006 buffer_marked_text: "<Sub|Error>".into(),
10007 completion_text: "SubscriptionError",
10008 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
10009 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10010 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10011 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
10012 },
10013 Run {
10014 run_description: "Suffix is a subsequence -- contiguous",
10015 initial_state: "SubˇErr".into(),
10016 buffer_marked_text: "<Sub|Err>".into(),
10017 completion_text: "SubscriptionError",
10018 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
10019 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10020 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10021 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
10022 },
10023 Run {
10024 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
10025 initial_state: "Suˇscrirr".into(),
10026 buffer_marked_text: "<Su|scrirr>".into(),
10027 completion_text: "SubscriptionError",
10028 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
10029 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10030 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10031 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
10032 },
10033 Run {
10034 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
10035 initial_state: "foo(indˇix)".into(),
10036 buffer_marked_text: "foo(<ind|ix>)".into(),
10037 completion_text: "node_index",
10038 expected_with_insert_mode: "foo(node_indexˇix)".into(),
10039 expected_with_replace_mode: "foo(node_indexˇ)".into(),
10040 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
10041 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
10042 },
10043 ];
10044
10045 for run in runs {
10046 let run_variations = [
10047 (LspInsertMode::Insert, run.expected_with_insert_mode),
10048 (LspInsertMode::Replace, run.expected_with_replace_mode),
10049 (
10050 LspInsertMode::ReplaceSubsequence,
10051 run.expected_with_replace_subsequence_mode,
10052 ),
10053 (
10054 LspInsertMode::ReplaceSuffix,
10055 run.expected_with_replace_suffix_mode,
10056 ),
10057 ];
10058
10059 for (lsp_insert_mode, expected_text) in run_variations {
10060 eprintln!(
10061 "run = {:?}, mode = {lsp_insert_mode:.?}",
10062 run.run_description,
10063 );
10064
10065 update_test_language_settings(&mut cx, |settings| {
10066 settings.defaults.completions = Some(CompletionSettings {
10067 lsp_insert_mode,
10068 words: WordsCompletionMode::Disabled,
10069 lsp: true,
10070 lsp_fetch_timeout_ms: 0,
10071 });
10072 });
10073
10074 cx.set_state(&run.initial_state);
10075 cx.update_editor(|editor, window, cx| {
10076 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10077 });
10078
10079 let counter = Arc::new(AtomicUsize::new(0));
10080 handle_completion_request_with_insert_and_replace(
10081 &mut cx,
10082 &run.buffer_marked_text,
10083 vec![run.completion_text],
10084 counter.clone(),
10085 )
10086 .await;
10087 cx.condition(|editor, _| editor.context_menu_visible())
10088 .await;
10089 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10090
10091 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10092 editor
10093 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10094 .unwrap()
10095 });
10096 cx.assert_editor_state(&expected_text);
10097 handle_resolve_completion_request(&mut cx, None).await;
10098 apply_additional_edits.await.unwrap();
10099 }
10100 }
10101}
10102
10103#[gpui::test]
10104async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
10105 init_test(cx, |_| {});
10106 let mut cx = EditorLspTestContext::new_rust(
10107 lsp::ServerCapabilities {
10108 completion_provider: Some(lsp::CompletionOptions {
10109 resolve_provider: Some(true),
10110 ..Default::default()
10111 }),
10112 ..Default::default()
10113 },
10114 cx,
10115 )
10116 .await;
10117
10118 let initial_state = "SubˇError";
10119 let buffer_marked_text = "<Sub|Error>";
10120 let completion_text = "SubscriptionError";
10121 let expected_with_insert_mode = "SubscriptionErrorˇError";
10122 let expected_with_replace_mode = "SubscriptionErrorˇ";
10123
10124 update_test_language_settings(&mut cx, |settings| {
10125 settings.defaults.completions = Some(CompletionSettings {
10126 words: WordsCompletionMode::Disabled,
10127 // set the opposite here to ensure that the action is overriding the default behavior
10128 lsp_insert_mode: LspInsertMode::Insert,
10129 lsp: true,
10130 lsp_fetch_timeout_ms: 0,
10131 });
10132 });
10133
10134 cx.set_state(initial_state);
10135 cx.update_editor(|editor, window, cx| {
10136 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10137 });
10138
10139 let counter = Arc::new(AtomicUsize::new(0));
10140 handle_completion_request_with_insert_and_replace(
10141 &mut cx,
10142 &buffer_marked_text,
10143 vec![completion_text],
10144 counter.clone(),
10145 )
10146 .await;
10147 cx.condition(|editor, _| editor.context_menu_visible())
10148 .await;
10149 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10150
10151 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10152 editor
10153 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10154 .unwrap()
10155 });
10156 cx.assert_editor_state(&expected_with_replace_mode);
10157 handle_resolve_completion_request(&mut cx, None).await;
10158 apply_additional_edits.await.unwrap();
10159
10160 update_test_language_settings(&mut cx, |settings| {
10161 settings.defaults.completions = Some(CompletionSettings {
10162 words: WordsCompletionMode::Disabled,
10163 // set the opposite here to ensure that the action is overriding the default behavior
10164 lsp_insert_mode: LspInsertMode::Replace,
10165 lsp: true,
10166 lsp_fetch_timeout_ms: 0,
10167 });
10168 });
10169
10170 cx.set_state(initial_state);
10171 cx.update_editor(|editor, window, cx| {
10172 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10173 });
10174 handle_completion_request_with_insert_and_replace(
10175 &mut cx,
10176 &buffer_marked_text,
10177 vec![completion_text],
10178 counter.clone(),
10179 )
10180 .await;
10181 cx.condition(|editor, _| editor.context_menu_visible())
10182 .await;
10183 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
10184
10185 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10186 editor
10187 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
10188 .unwrap()
10189 });
10190 cx.assert_editor_state(&expected_with_insert_mode);
10191 handle_resolve_completion_request(&mut cx, None).await;
10192 apply_additional_edits.await.unwrap();
10193}
10194
10195#[gpui::test]
10196async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
10197 init_test(cx, |_| {});
10198 let mut cx = EditorLspTestContext::new_rust(
10199 lsp::ServerCapabilities {
10200 completion_provider: Some(lsp::CompletionOptions {
10201 resolve_provider: Some(true),
10202 ..Default::default()
10203 }),
10204 ..Default::default()
10205 },
10206 cx,
10207 )
10208 .await;
10209
10210 // scenario: surrounding text matches completion text
10211 let completion_text = "to_offset";
10212 let initial_state = indoc! {"
10213 1. buf.to_offˇsuffix
10214 2. buf.to_offˇsuf
10215 3. buf.to_offˇfix
10216 4. buf.to_offˇ
10217 5. into_offˇensive
10218 6. ˇsuffix
10219 7. let ˇ //
10220 8. aaˇzz
10221 9. buf.to_off«zzzzzˇ»suffix
10222 10. buf.«ˇzzzzz»suffix
10223 11. to_off«ˇzzzzz»
10224
10225 buf.to_offˇsuffix // newest cursor
10226 "};
10227 let completion_marked_buffer = indoc! {"
10228 1. buf.to_offsuffix
10229 2. buf.to_offsuf
10230 3. buf.to_offfix
10231 4. buf.to_off
10232 5. into_offensive
10233 6. suffix
10234 7. let //
10235 8. aazz
10236 9. buf.to_offzzzzzsuffix
10237 10. buf.zzzzzsuffix
10238 11. to_offzzzzz
10239
10240 buf.<to_off|suffix> // newest cursor
10241 "};
10242 let expected = indoc! {"
10243 1. buf.to_offsetˇ
10244 2. buf.to_offsetˇsuf
10245 3. buf.to_offsetˇfix
10246 4. buf.to_offsetˇ
10247 5. into_offsetˇensive
10248 6. to_offsetˇsuffix
10249 7. let to_offsetˇ //
10250 8. aato_offsetˇzz
10251 9. buf.to_offsetˇ
10252 10. buf.to_offsetˇsuffix
10253 11. to_offsetˇ
10254
10255 buf.to_offsetˇ // newest cursor
10256 "};
10257 cx.set_state(initial_state);
10258 cx.update_editor(|editor, window, cx| {
10259 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10260 });
10261 handle_completion_request_with_insert_and_replace(
10262 &mut cx,
10263 completion_marked_buffer,
10264 vec![completion_text],
10265 Arc::new(AtomicUsize::new(0)),
10266 )
10267 .await;
10268 cx.condition(|editor, _| editor.context_menu_visible())
10269 .await;
10270 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10271 editor
10272 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10273 .unwrap()
10274 });
10275 cx.assert_editor_state(expected);
10276 handle_resolve_completion_request(&mut cx, None).await;
10277 apply_additional_edits.await.unwrap();
10278
10279 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
10280 let completion_text = "foo_and_bar";
10281 let initial_state = indoc! {"
10282 1. ooanbˇ
10283 2. zooanbˇ
10284 3. ooanbˇz
10285 4. zooanbˇz
10286 5. ooanˇ
10287 6. oanbˇ
10288
10289 ooanbˇ
10290 "};
10291 let completion_marked_buffer = indoc! {"
10292 1. ooanb
10293 2. zooanb
10294 3. ooanbz
10295 4. zooanbz
10296 5. ooan
10297 6. oanb
10298
10299 <ooanb|>
10300 "};
10301 let expected = indoc! {"
10302 1. foo_and_barˇ
10303 2. zfoo_and_barˇ
10304 3. foo_and_barˇz
10305 4. zfoo_and_barˇz
10306 5. ooanfoo_and_barˇ
10307 6. oanbfoo_and_barˇ
10308
10309 foo_and_barˇ
10310 "};
10311 cx.set_state(initial_state);
10312 cx.update_editor(|editor, window, cx| {
10313 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10314 });
10315 handle_completion_request_with_insert_and_replace(
10316 &mut cx,
10317 completion_marked_buffer,
10318 vec![completion_text],
10319 Arc::new(AtomicUsize::new(0)),
10320 )
10321 .await;
10322 cx.condition(|editor, _| editor.context_menu_visible())
10323 .await;
10324 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10325 editor
10326 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10327 .unwrap()
10328 });
10329 cx.assert_editor_state(expected);
10330 handle_resolve_completion_request(&mut cx, None).await;
10331 apply_additional_edits.await.unwrap();
10332
10333 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
10334 // (expects the same as if it was inserted at the end)
10335 let completion_text = "foo_and_bar";
10336 let initial_state = indoc! {"
10337 1. ooˇanb
10338 2. zooˇanb
10339 3. ooˇanbz
10340 4. zooˇanbz
10341
10342 ooˇanb
10343 "};
10344 let completion_marked_buffer = indoc! {"
10345 1. ooanb
10346 2. zooanb
10347 3. ooanbz
10348 4. zooanbz
10349
10350 <oo|anb>
10351 "};
10352 let expected = indoc! {"
10353 1. foo_and_barˇ
10354 2. zfoo_and_barˇ
10355 3. foo_and_barˇz
10356 4. zfoo_and_barˇz
10357
10358 foo_and_barˇ
10359 "};
10360 cx.set_state(initial_state);
10361 cx.update_editor(|editor, window, cx| {
10362 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10363 });
10364 handle_completion_request_with_insert_and_replace(
10365 &mut cx,
10366 completion_marked_buffer,
10367 vec![completion_text],
10368 Arc::new(AtomicUsize::new(0)),
10369 )
10370 .await;
10371 cx.condition(|editor, _| editor.context_menu_visible())
10372 .await;
10373 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10374 editor
10375 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10376 .unwrap()
10377 });
10378 cx.assert_editor_state(expected);
10379 handle_resolve_completion_request(&mut cx, None).await;
10380 apply_additional_edits.await.unwrap();
10381}
10382
10383// This used to crash
10384#[gpui::test]
10385async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
10386 init_test(cx, |_| {});
10387
10388 let buffer_text = indoc! {"
10389 fn main() {
10390 10.satu;
10391
10392 //
10393 // separate cursors so they open in different excerpts (manually reproducible)
10394 //
10395
10396 10.satu20;
10397 }
10398 "};
10399 let multibuffer_text_with_selections = indoc! {"
10400 fn main() {
10401 10.satuˇ;
10402
10403 //
10404
10405 //
10406
10407 10.satuˇ20;
10408 }
10409 "};
10410 let expected_multibuffer = indoc! {"
10411 fn main() {
10412 10.saturating_sub()ˇ;
10413
10414 //
10415
10416 //
10417
10418 10.saturating_sub()ˇ;
10419 }
10420 "};
10421
10422 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
10423 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
10424
10425 let fs = FakeFs::new(cx.executor());
10426 fs.insert_tree(
10427 path!("/a"),
10428 json!({
10429 "main.rs": buffer_text,
10430 }),
10431 )
10432 .await;
10433
10434 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
10435 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10436 language_registry.add(rust_lang());
10437 let mut fake_servers = language_registry.register_fake_lsp(
10438 "Rust",
10439 FakeLspAdapter {
10440 capabilities: lsp::ServerCapabilities {
10441 completion_provider: Some(lsp::CompletionOptions {
10442 resolve_provider: None,
10443 ..lsp::CompletionOptions::default()
10444 }),
10445 ..lsp::ServerCapabilities::default()
10446 },
10447 ..FakeLspAdapter::default()
10448 },
10449 );
10450 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10451 let cx = &mut VisualTestContext::from_window(*workspace, cx);
10452 let buffer = project
10453 .update(cx, |project, cx| {
10454 project.open_local_buffer(path!("/a/main.rs"), cx)
10455 })
10456 .await
10457 .unwrap();
10458
10459 let multi_buffer = cx.new(|cx| {
10460 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
10461 multi_buffer.push_excerpts(
10462 buffer.clone(),
10463 [ExcerptRange::new(0..first_excerpt_end)],
10464 cx,
10465 );
10466 multi_buffer.push_excerpts(
10467 buffer.clone(),
10468 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
10469 cx,
10470 );
10471 multi_buffer
10472 });
10473
10474 let editor = workspace
10475 .update(cx, |_, window, cx| {
10476 cx.new(|cx| {
10477 Editor::new(
10478 EditorMode::Full {
10479 scale_ui_elements_with_buffer_font_size: false,
10480 show_active_line_background: false,
10481 sized_by_content: false,
10482 },
10483 multi_buffer.clone(),
10484 Some(project.clone()),
10485 window,
10486 cx,
10487 )
10488 })
10489 })
10490 .unwrap();
10491
10492 let pane = workspace
10493 .update(cx, |workspace, _, _| workspace.active_pane().clone())
10494 .unwrap();
10495 pane.update_in(cx, |pane, window, cx| {
10496 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
10497 });
10498
10499 let fake_server = fake_servers.next().await.unwrap();
10500
10501 editor.update_in(cx, |editor, window, cx| {
10502 editor.change_selections(None, window, cx, |s| {
10503 s.select_ranges([
10504 Point::new(1, 11)..Point::new(1, 11),
10505 Point::new(7, 11)..Point::new(7, 11),
10506 ])
10507 });
10508
10509 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
10510 });
10511
10512 editor.update_in(cx, |editor, window, cx| {
10513 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10514 });
10515
10516 fake_server
10517 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
10518 let completion_item = lsp::CompletionItem {
10519 label: "saturating_sub()".into(),
10520 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
10521 lsp::InsertReplaceEdit {
10522 new_text: "saturating_sub()".to_owned(),
10523 insert: lsp::Range::new(
10524 lsp::Position::new(7, 7),
10525 lsp::Position::new(7, 11),
10526 ),
10527 replace: lsp::Range::new(
10528 lsp::Position::new(7, 7),
10529 lsp::Position::new(7, 13),
10530 ),
10531 },
10532 )),
10533 ..lsp::CompletionItem::default()
10534 };
10535
10536 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
10537 })
10538 .next()
10539 .await
10540 .unwrap();
10541
10542 cx.condition(&editor, |editor, _| editor.context_menu_visible())
10543 .await;
10544
10545 editor
10546 .update_in(cx, |editor, window, cx| {
10547 editor
10548 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10549 .unwrap()
10550 })
10551 .await
10552 .unwrap();
10553
10554 editor.update(cx, |editor, cx| {
10555 assert_text_with_selections(editor, expected_multibuffer, cx);
10556 })
10557}
10558
10559#[gpui::test]
10560async fn test_completion(cx: &mut TestAppContext) {
10561 init_test(cx, |_| {});
10562
10563 let mut cx = EditorLspTestContext::new_rust(
10564 lsp::ServerCapabilities {
10565 completion_provider: Some(lsp::CompletionOptions {
10566 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10567 resolve_provider: Some(true),
10568 ..Default::default()
10569 }),
10570 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10571 ..Default::default()
10572 },
10573 cx,
10574 )
10575 .await;
10576 let counter = Arc::new(AtomicUsize::new(0));
10577
10578 cx.set_state(indoc! {"
10579 oneˇ
10580 two
10581 three
10582 "});
10583 cx.simulate_keystroke(".");
10584 handle_completion_request(
10585 &mut cx,
10586 indoc! {"
10587 one.|<>
10588 two
10589 three
10590 "},
10591 vec!["first_completion", "second_completion"],
10592 counter.clone(),
10593 )
10594 .await;
10595 cx.condition(|editor, _| editor.context_menu_visible())
10596 .await;
10597 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10598
10599 let _handler = handle_signature_help_request(
10600 &mut cx,
10601 lsp::SignatureHelp {
10602 signatures: vec![lsp::SignatureInformation {
10603 label: "test signature".to_string(),
10604 documentation: None,
10605 parameters: Some(vec![lsp::ParameterInformation {
10606 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
10607 documentation: None,
10608 }]),
10609 active_parameter: None,
10610 }],
10611 active_signature: None,
10612 active_parameter: None,
10613 },
10614 );
10615 cx.update_editor(|editor, window, cx| {
10616 assert!(
10617 !editor.signature_help_state.is_shown(),
10618 "No signature help was called for"
10619 );
10620 editor.show_signature_help(&ShowSignatureHelp, window, cx);
10621 });
10622 cx.run_until_parked();
10623 cx.update_editor(|editor, _, _| {
10624 assert!(
10625 !editor.signature_help_state.is_shown(),
10626 "No signature help should be shown when completions menu is open"
10627 );
10628 });
10629
10630 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10631 editor.context_menu_next(&Default::default(), window, cx);
10632 editor
10633 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10634 .unwrap()
10635 });
10636 cx.assert_editor_state(indoc! {"
10637 one.second_completionˇ
10638 two
10639 three
10640 "});
10641
10642 handle_resolve_completion_request(
10643 &mut cx,
10644 Some(vec![
10645 (
10646 //This overlaps with the primary completion edit which is
10647 //misbehavior from the LSP spec, test that we filter it out
10648 indoc! {"
10649 one.second_ˇcompletion
10650 two
10651 threeˇ
10652 "},
10653 "overlapping additional edit",
10654 ),
10655 (
10656 indoc! {"
10657 one.second_completion
10658 two
10659 threeˇ
10660 "},
10661 "\nadditional edit",
10662 ),
10663 ]),
10664 )
10665 .await;
10666 apply_additional_edits.await.unwrap();
10667 cx.assert_editor_state(indoc! {"
10668 one.second_completionˇ
10669 two
10670 three
10671 additional edit
10672 "});
10673
10674 cx.set_state(indoc! {"
10675 one.second_completion
10676 twoˇ
10677 threeˇ
10678 additional edit
10679 "});
10680 cx.simulate_keystroke(" ");
10681 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
10682 cx.simulate_keystroke("s");
10683 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
10684
10685 cx.assert_editor_state(indoc! {"
10686 one.second_completion
10687 two sˇ
10688 three sˇ
10689 additional edit
10690 "});
10691 handle_completion_request(
10692 &mut cx,
10693 indoc! {"
10694 one.second_completion
10695 two s
10696 three <s|>
10697 additional edit
10698 "},
10699 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
10700 counter.clone(),
10701 )
10702 .await;
10703 cx.condition(|editor, _| editor.context_menu_visible())
10704 .await;
10705 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
10706
10707 cx.simulate_keystroke("i");
10708
10709 handle_completion_request(
10710 &mut cx,
10711 indoc! {"
10712 one.second_completion
10713 two si
10714 three <si|>
10715 additional edit
10716 "},
10717 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
10718 counter.clone(),
10719 )
10720 .await;
10721 cx.condition(|editor, _| editor.context_menu_visible())
10722 .await;
10723 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
10724
10725 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10726 editor
10727 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10728 .unwrap()
10729 });
10730 cx.assert_editor_state(indoc! {"
10731 one.second_completion
10732 two sixth_completionˇ
10733 three sixth_completionˇ
10734 additional edit
10735 "});
10736
10737 apply_additional_edits.await.unwrap();
10738
10739 update_test_language_settings(&mut cx, |settings| {
10740 settings.defaults.show_completions_on_input = Some(false);
10741 });
10742 cx.set_state("editorˇ");
10743 cx.simulate_keystroke(".");
10744 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
10745 cx.simulate_keystrokes("c l o");
10746 cx.assert_editor_state("editor.cloˇ");
10747 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
10748 cx.update_editor(|editor, window, cx| {
10749 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10750 });
10751 handle_completion_request(
10752 &mut cx,
10753 "editor.<clo|>",
10754 vec!["close", "clobber"],
10755 counter.clone(),
10756 )
10757 .await;
10758 cx.condition(|editor, _| editor.context_menu_visible())
10759 .await;
10760 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
10761
10762 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10763 editor
10764 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10765 .unwrap()
10766 });
10767 cx.assert_editor_state("editor.closeˇ");
10768 handle_resolve_completion_request(&mut cx, None).await;
10769 apply_additional_edits.await.unwrap();
10770}
10771
10772#[gpui::test]
10773async fn test_word_completion(cx: &mut TestAppContext) {
10774 let lsp_fetch_timeout_ms = 10;
10775 init_test(cx, |language_settings| {
10776 language_settings.defaults.completions = Some(CompletionSettings {
10777 words: WordsCompletionMode::Fallback,
10778 lsp: true,
10779 lsp_fetch_timeout_ms: 10,
10780 lsp_insert_mode: LspInsertMode::Insert,
10781 });
10782 });
10783
10784 let mut cx = EditorLspTestContext::new_rust(
10785 lsp::ServerCapabilities {
10786 completion_provider: Some(lsp::CompletionOptions {
10787 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10788 ..lsp::CompletionOptions::default()
10789 }),
10790 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10791 ..lsp::ServerCapabilities::default()
10792 },
10793 cx,
10794 )
10795 .await;
10796
10797 let throttle_completions = Arc::new(AtomicBool::new(false));
10798
10799 let lsp_throttle_completions = throttle_completions.clone();
10800 let _completion_requests_handler =
10801 cx.lsp
10802 .server
10803 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
10804 let lsp_throttle_completions = lsp_throttle_completions.clone();
10805 let cx = cx.clone();
10806 async move {
10807 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
10808 cx.background_executor()
10809 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
10810 .await;
10811 }
10812 Ok(Some(lsp::CompletionResponse::Array(vec![
10813 lsp::CompletionItem {
10814 label: "first".into(),
10815 ..lsp::CompletionItem::default()
10816 },
10817 lsp::CompletionItem {
10818 label: "last".into(),
10819 ..lsp::CompletionItem::default()
10820 },
10821 ])))
10822 }
10823 });
10824
10825 cx.set_state(indoc! {"
10826 oneˇ
10827 two
10828 three
10829 "});
10830 cx.simulate_keystroke(".");
10831 cx.executor().run_until_parked();
10832 cx.condition(|editor, _| editor.context_menu_visible())
10833 .await;
10834 cx.update_editor(|editor, window, cx| {
10835 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10836 {
10837 assert_eq!(
10838 completion_menu_entries(&menu),
10839 &["first", "last"],
10840 "When LSP server is fast to reply, no fallback word completions are used"
10841 );
10842 } else {
10843 panic!("expected completion menu to be open");
10844 }
10845 editor.cancel(&Cancel, window, cx);
10846 });
10847 cx.executor().run_until_parked();
10848 cx.condition(|editor, _| !editor.context_menu_visible())
10849 .await;
10850
10851 throttle_completions.store(true, atomic::Ordering::Release);
10852 cx.simulate_keystroke(".");
10853 cx.executor()
10854 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
10855 cx.executor().run_until_parked();
10856 cx.condition(|editor, _| editor.context_menu_visible())
10857 .await;
10858 cx.update_editor(|editor, _, _| {
10859 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10860 {
10861 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
10862 "When LSP server is slow, document words can be shown instead, if configured accordingly");
10863 } else {
10864 panic!("expected completion menu to be open");
10865 }
10866 });
10867}
10868
10869#[gpui::test]
10870async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
10871 init_test(cx, |language_settings| {
10872 language_settings.defaults.completions = Some(CompletionSettings {
10873 words: WordsCompletionMode::Enabled,
10874 lsp: true,
10875 lsp_fetch_timeout_ms: 0,
10876 lsp_insert_mode: LspInsertMode::Insert,
10877 });
10878 });
10879
10880 let mut cx = EditorLspTestContext::new_rust(
10881 lsp::ServerCapabilities {
10882 completion_provider: Some(lsp::CompletionOptions {
10883 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10884 ..lsp::CompletionOptions::default()
10885 }),
10886 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10887 ..lsp::ServerCapabilities::default()
10888 },
10889 cx,
10890 )
10891 .await;
10892
10893 let _completion_requests_handler =
10894 cx.lsp
10895 .server
10896 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
10897 Ok(Some(lsp::CompletionResponse::Array(vec![
10898 lsp::CompletionItem {
10899 label: "first".into(),
10900 ..lsp::CompletionItem::default()
10901 },
10902 lsp::CompletionItem {
10903 label: "last".into(),
10904 ..lsp::CompletionItem::default()
10905 },
10906 ])))
10907 });
10908
10909 cx.set_state(indoc! {"ˇ
10910 first
10911 last
10912 second
10913 "});
10914 cx.simulate_keystroke(".");
10915 cx.executor().run_until_parked();
10916 cx.condition(|editor, _| editor.context_menu_visible())
10917 .await;
10918 cx.update_editor(|editor, _, _| {
10919 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10920 {
10921 assert_eq!(
10922 completion_menu_entries(&menu),
10923 &["first", "last", "second"],
10924 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
10925 );
10926 } else {
10927 panic!("expected completion menu to be open");
10928 }
10929 });
10930}
10931
10932#[gpui::test]
10933async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
10934 init_test(cx, |language_settings| {
10935 language_settings.defaults.completions = Some(CompletionSettings {
10936 words: WordsCompletionMode::Disabled,
10937 lsp: true,
10938 lsp_fetch_timeout_ms: 0,
10939 lsp_insert_mode: LspInsertMode::Insert,
10940 });
10941 });
10942
10943 let mut cx = EditorLspTestContext::new_rust(
10944 lsp::ServerCapabilities {
10945 completion_provider: Some(lsp::CompletionOptions {
10946 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10947 ..lsp::CompletionOptions::default()
10948 }),
10949 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10950 ..lsp::ServerCapabilities::default()
10951 },
10952 cx,
10953 )
10954 .await;
10955
10956 let _completion_requests_handler =
10957 cx.lsp
10958 .server
10959 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
10960 panic!("LSP completions should not be queried when dealing with word completions")
10961 });
10962
10963 cx.set_state(indoc! {"ˇ
10964 first
10965 last
10966 second
10967 "});
10968 cx.update_editor(|editor, window, cx| {
10969 editor.show_word_completions(&ShowWordCompletions, window, cx);
10970 });
10971 cx.executor().run_until_parked();
10972 cx.condition(|editor, _| editor.context_menu_visible())
10973 .await;
10974 cx.update_editor(|editor, _, _| {
10975 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10976 {
10977 assert_eq!(
10978 completion_menu_entries(&menu),
10979 &["first", "last", "second"],
10980 "`ShowWordCompletions` action should show word completions"
10981 );
10982 } else {
10983 panic!("expected completion menu to be open");
10984 }
10985 });
10986
10987 cx.simulate_keystroke("l");
10988 cx.executor().run_until_parked();
10989 cx.condition(|editor, _| editor.context_menu_visible())
10990 .await;
10991 cx.update_editor(|editor, _, _| {
10992 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10993 {
10994 assert_eq!(
10995 completion_menu_entries(&menu),
10996 &["last"],
10997 "After showing word completions, further editing should filter them and not query the LSP"
10998 );
10999 } else {
11000 panic!("expected completion menu to be open");
11001 }
11002 });
11003}
11004
11005#[gpui::test]
11006async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
11007 init_test(cx, |language_settings| {
11008 language_settings.defaults.completions = Some(CompletionSettings {
11009 words: WordsCompletionMode::Fallback,
11010 lsp: false,
11011 lsp_fetch_timeout_ms: 0,
11012 lsp_insert_mode: LspInsertMode::Insert,
11013 });
11014 });
11015
11016 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
11017
11018 cx.set_state(indoc! {"ˇ
11019 0_usize
11020 let
11021 33
11022 4.5f32
11023 "});
11024 cx.update_editor(|editor, window, cx| {
11025 editor.show_completions(&ShowCompletions::default(), window, cx);
11026 });
11027 cx.executor().run_until_parked();
11028 cx.condition(|editor, _| editor.context_menu_visible())
11029 .await;
11030 cx.update_editor(|editor, window, cx| {
11031 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11032 {
11033 assert_eq!(
11034 completion_menu_entries(&menu),
11035 &["let"],
11036 "With no digits in the completion query, no digits should be in the word completions"
11037 );
11038 } else {
11039 panic!("expected completion menu to be open");
11040 }
11041 editor.cancel(&Cancel, window, cx);
11042 });
11043
11044 cx.set_state(indoc! {"3ˇ
11045 0_usize
11046 let
11047 3
11048 33.35f32
11049 "});
11050 cx.update_editor(|editor, window, cx| {
11051 editor.show_completions(&ShowCompletions::default(), window, cx);
11052 });
11053 cx.executor().run_until_parked();
11054 cx.condition(|editor, _| editor.context_menu_visible())
11055 .await;
11056 cx.update_editor(|editor, _, _| {
11057 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11058 {
11059 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
11060 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
11061 } else {
11062 panic!("expected completion menu to be open");
11063 }
11064 });
11065}
11066
11067fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
11068 let position = || lsp::Position {
11069 line: params.text_document_position.position.line,
11070 character: params.text_document_position.position.character,
11071 };
11072 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11073 range: lsp::Range {
11074 start: position(),
11075 end: position(),
11076 },
11077 new_text: text.to_string(),
11078 }))
11079}
11080
11081#[gpui::test]
11082async fn test_multiline_completion(cx: &mut TestAppContext) {
11083 init_test(cx, |_| {});
11084
11085 let fs = FakeFs::new(cx.executor());
11086 fs.insert_tree(
11087 path!("/a"),
11088 json!({
11089 "main.ts": "a",
11090 }),
11091 )
11092 .await;
11093
11094 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11095 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11096 let typescript_language = Arc::new(Language::new(
11097 LanguageConfig {
11098 name: "TypeScript".into(),
11099 matcher: LanguageMatcher {
11100 path_suffixes: vec!["ts".to_string()],
11101 ..LanguageMatcher::default()
11102 },
11103 line_comments: vec!["// ".into()],
11104 ..LanguageConfig::default()
11105 },
11106 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
11107 ));
11108 language_registry.add(typescript_language.clone());
11109 let mut fake_servers = language_registry.register_fake_lsp(
11110 "TypeScript",
11111 FakeLspAdapter {
11112 capabilities: lsp::ServerCapabilities {
11113 completion_provider: Some(lsp::CompletionOptions {
11114 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11115 ..lsp::CompletionOptions::default()
11116 }),
11117 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11118 ..lsp::ServerCapabilities::default()
11119 },
11120 // Emulate vtsls label generation
11121 label_for_completion: Some(Box::new(|item, _| {
11122 let text = if let Some(description) = item
11123 .label_details
11124 .as_ref()
11125 .and_then(|label_details| label_details.description.as_ref())
11126 {
11127 format!("{} {}", item.label, description)
11128 } else if let Some(detail) = &item.detail {
11129 format!("{} {}", item.label, detail)
11130 } else {
11131 item.label.clone()
11132 };
11133 let len = text.len();
11134 Some(language::CodeLabel {
11135 text,
11136 runs: Vec::new(),
11137 filter_range: 0..len,
11138 })
11139 })),
11140 ..FakeLspAdapter::default()
11141 },
11142 );
11143 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11144 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11145 let worktree_id = workspace
11146 .update(cx, |workspace, _window, cx| {
11147 workspace.project().update(cx, |project, cx| {
11148 project.worktrees(cx).next().unwrap().read(cx).id()
11149 })
11150 })
11151 .unwrap();
11152 let _buffer = project
11153 .update(cx, |project, cx| {
11154 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
11155 })
11156 .await
11157 .unwrap();
11158 let editor = workspace
11159 .update(cx, |workspace, window, cx| {
11160 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
11161 })
11162 .unwrap()
11163 .await
11164 .unwrap()
11165 .downcast::<Editor>()
11166 .unwrap();
11167 let fake_server = fake_servers.next().await.unwrap();
11168
11169 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
11170 let multiline_label_2 = "a\nb\nc\n";
11171 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
11172 let multiline_description = "d\ne\nf\n";
11173 let multiline_detail_2 = "g\nh\ni\n";
11174
11175 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
11176 move |params, _| async move {
11177 Ok(Some(lsp::CompletionResponse::Array(vec![
11178 lsp::CompletionItem {
11179 label: multiline_label.to_string(),
11180 text_edit: gen_text_edit(¶ms, "new_text_1"),
11181 ..lsp::CompletionItem::default()
11182 },
11183 lsp::CompletionItem {
11184 label: "single line label 1".to_string(),
11185 detail: Some(multiline_detail.to_string()),
11186 text_edit: gen_text_edit(¶ms, "new_text_2"),
11187 ..lsp::CompletionItem::default()
11188 },
11189 lsp::CompletionItem {
11190 label: "single line label 2".to_string(),
11191 label_details: Some(lsp::CompletionItemLabelDetails {
11192 description: Some(multiline_description.to_string()),
11193 detail: None,
11194 }),
11195 text_edit: gen_text_edit(¶ms, "new_text_2"),
11196 ..lsp::CompletionItem::default()
11197 },
11198 lsp::CompletionItem {
11199 label: multiline_label_2.to_string(),
11200 detail: Some(multiline_detail_2.to_string()),
11201 text_edit: gen_text_edit(¶ms, "new_text_3"),
11202 ..lsp::CompletionItem::default()
11203 },
11204 lsp::CompletionItem {
11205 label: "Label with many spaces and \t but without newlines".to_string(),
11206 detail: Some(
11207 "Details with many spaces and \t but without newlines".to_string(),
11208 ),
11209 text_edit: gen_text_edit(¶ms, "new_text_4"),
11210 ..lsp::CompletionItem::default()
11211 },
11212 ])))
11213 },
11214 );
11215
11216 editor.update_in(cx, |editor, window, cx| {
11217 cx.focus_self(window);
11218 editor.move_to_end(&MoveToEnd, window, cx);
11219 editor.handle_input(".", window, cx);
11220 });
11221 cx.run_until_parked();
11222 completion_handle.next().await.unwrap();
11223
11224 editor.update(cx, |editor, _| {
11225 assert!(editor.context_menu_visible());
11226 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11227 {
11228 let completion_labels = menu
11229 .completions
11230 .borrow()
11231 .iter()
11232 .map(|c| c.label.text.clone())
11233 .collect::<Vec<_>>();
11234 assert_eq!(
11235 completion_labels,
11236 &[
11237 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
11238 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
11239 "single line label 2 d e f ",
11240 "a b c g h i ",
11241 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
11242 ],
11243 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
11244 );
11245
11246 for completion in menu
11247 .completions
11248 .borrow()
11249 .iter() {
11250 assert_eq!(
11251 completion.label.filter_range,
11252 0..completion.label.text.len(),
11253 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
11254 );
11255 }
11256 } else {
11257 panic!("expected completion menu to be open");
11258 }
11259 });
11260}
11261
11262#[gpui::test]
11263async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
11264 init_test(cx, |_| {});
11265 let mut cx = EditorLspTestContext::new_rust(
11266 lsp::ServerCapabilities {
11267 completion_provider: Some(lsp::CompletionOptions {
11268 trigger_characters: Some(vec![".".to_string()]),
11269 ..Default::default()
11270 }),
11271 ..Default::default()
11272 },
11273 cx,
11274 )
11275 .await;
11276 cx.lsp
11277 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11278 Ok(Some(lsp::CompletionResponse::Array(vec![
11279 lsp::CompletionItem {
11280 label: "first".into(),
11281 ..Default::default()
11282 },
11283 lsp::CompletionItem {
11284 label: "last".into(),
11285 ..Default::default()
11286 },
11287 ])))
11288 });
11289 cx.set_state("variableˇ");
11290 cx.simulate_keystroke(".");
11291 cx.executor().run_until_parked();
11292
11293 cx.update_editor(|editor, _, _| {
11294 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11295 {
11296 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
11297 } else {
11298 panic!("expected completion menu to be open");
11299 }
11300 });
11301
11302 cx.update_editor(|editor, window, cx| {
11303 editor.move_page_down(&MovePageDown::default(), window, cx);
11304 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11305 {
11306 assert!(
11307 menu.selected_item == 1,
11308 "expected PageDown to select the last item from the context menu"
11309 );
11310 } else {
11311 panic!("expected completion menu to stay open after PageDown");
11312 }
11313 });
11314
11315 cx.update_editor(|editor, window, cx| {
11316 editor.move_page_up(&MovePageUp::default(), window, cx);
11317 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11318 {
11319 assert!(
11320 menu.selected_item == 0,
11321 "expected PageUp to select the first item from the context menu"
11322 );
11323 } else {
11324 panic!("expected completion menu to stay open after PageUp");
11325 }
11326 });
11327}
11328
11329#[gpui::test]
11330async fn test_as_is_completions(cx: &mut TestAppContext) {
11331 init_test(cx, |_| {});
11332 let mut cx = EditorLspTestContext::new_rust(
11333 lsp::ServerCapabilities {
11334 completion_provider: Some(lsp::CompletionOptions {
11335 ..Default::default()
11336 }),
11337 ..Default::default()
11338 },
11339 cx,
11340 )
11341 .await;
11342 cx.lsp
11343 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11344 Ok(Some(lsp::CompletionResponse::Array(vec![
11345 lsp::CompletionItem {
11346 label: "unsafe".into(),
11347 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11348 range: lsp::Range {
11349 start: lsp::Position {
11350 line: 1,
11351 character: 2,
11352 },
11353 end: lsp::Position {
11354 line: 1,
11355 character: 3,
11356 },
11357 },
11358 new_text: "unsafe".to_string(),
11359 })),
11360 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
11361 ..Default::default()
11362 },
11363 ])))
11364 });
11365 cx.set_state("fn a() {}\n nˇ");
11366 cx.executor().run_until_parked();
11367 cx.update_editor(|editor, window, cx| {
11368 editor.show_completions(
11369 &ShowCompletions {
11370 trigger: Some("\n".into()),
11371 },
11372 window,
11373 cx,
11374 );
11375 });
11376 cx.executor().run_until_parked();
11377
11378 cx.update_editor(|editor, window, cx| {
11379 editor.confirm_completion(&Default::default(), window, cx)
11380 });
11381 cx.executor().run_until_parked();
11382 cx.assert_editor_state("fn a() {}\n unsafeˇ");
11383}
11384
11385#[gpui::test]
11386async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
11387 init_test(cx, |_| {});
11388
11389 let mut cx = EditorLspTestContext::new_rust(
11390 lsp::ServerCapabilities {
11391 completion_provider: Some(lsp::CompletionOptions {
11392 trigger_characters: Some(vec![".".to_string()]),
11393 resolve_provider: Some(true),
11394 ..Default::default()
11395 }),
11396 ..Default::default()
11397 },
11398 cx,
11399 )
11400 .await;
11401
11402 cx.set_state("fn main() { let a = 2ˇ; }");
11403 cx.simulate_keystroke(".");
11404 let completion_item = lsp::CompletionItem {
11405 label: "Some".into(),
11406 kind: Some(lsp::CompletionItemKind::SNIPPET),
11407 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
11408 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
11409 kind: lsp::MarkupKind::Markdown,
11410 value: "```rust\nSome(2)\n```".to_string(),
11411 })),
11412 deprecated: Some(false),
11413 sort_text: Some("Some".to_string()),
11414 filter_text: Some("Some".to_string()),
11415 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
11416 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11417 range: lsp::Range {
11418 start: lsp::Position {
11419 line: 0,
11420 character: 22,
11421 },
11422 end: lsp::Position {
11423 line: 0,
11424 character: 22,
11425 },
11426 },
11427 new_text: "Some(2)".to_string(),
11428 })),
11429 additional_text_edits: Some(vec![lsp::TextEdit {
11430 range: lsp::Range {
11431 start: lsp::Position {
11432 line: 0,
11433 character: 20,
11434 },
11435 end: lsp::Position {
11436 line: 0,
11437 character: 22,
11438 },
11439 },
11440 new_text: "".to_string(),
11441 }]),
11442 ..Default::default()
11443 };
11444
11445 let closure_completion_item = completion_item.clone();
11446 let counter = Arc::new(AtomicUsize::new(0));
11447 let counter_clone = counter.clone();
11448 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
11449 let task_completion_item = closure_completion_item.clone();
11450 counter_clone.fetch_add(1, atomic::Ordering::Release);
11451 async move {
11452 Ok(Some(lsp::CompletionResponse::Array(vec![
11453 task_completion_item,
11454 ])))
11455 }
11456 });
11457
11458 cx.condition(|editor, _| editor.context_menu_visible())
11459 .await;
11460 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
11461 assert!(request.next().await.is_some());
11462 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11463
11464 cx.simulate_keystrokes("S o m");
11465 cx.condition(|editor, _| editor.context_menu_visible())
11466 .await;
11467 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
11468 assert!(request.next().await.is_some());
11469 assert!(request.next().await.is_some());
11470 assert!(request.next().await.is_some());
11471 request.close();
11472 assert!(request.next().await.is_none());
11473 assert_eq!(
11474 counter.load(atomic::Ordering::Acquire),
11475 4,
11476 "With the completions menu open, only one LSP request should happen per input"
11477 );
11478}
11479
11480#[gpui::test]
11481async fn test_toggle_comment(cx: &mut TestAppContext) {
11482 init_test(cx, |_| {});
11483 let mut cx = EditorTestContext::new(cx).await;
11484 let language = Arc::new(Language::new(
11485 LanguageConfig {
11486 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
11487 ..Default::default()
11488 },
11489 Some(tree_sitter_rust::LANGUAGE.into()),
11490 ));
11491 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
11492
11493 // If multiple selections intersect a line, the line is only toggled once.
11494 cx.set_state(indoc! {"
11495 fn a() {
11496 «//b();
11497 ˇ»// «c();
11498 //ˇ» d();
11499 }
11500 "});
11501
11502 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11503
11504 cx.assert_editor_state(indoc! {"
11505 fn a() {
11506 «b();
11507 c();
11508 ˇ» d();
11509 }
11510 "});
11511
11512 // The comment prefix is inserted at the same column for every line in a
11513 // selection.
11514 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11515
11516 cx.assert_editor_state(indoc! {"
11517 fn a() {
11518 // «b();
11519 // c();
11520 ˇ»// d();
11521 }
11522 "});
11523
11524 // If a selection ends at the beginning of a line, that line is not toggled.
11525 cx.set_selections_state(indoc! {"
11526 fn a() {
11527 // b();
11528 «// c();
11529 ˇ» // d();
11530 }
11531 "});
11532
11533 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11534
11535 cx.assert_editor_state(indoc! {"
11536 fn a() {
11537 // b();
11538 «c();
11539 ˇ» // d();
11540 }
11541 "});
11542
11543 // If a selection span a single line and is empty, the line is toggled.
11544 cx.set_state(indoc! {"
11545 fn a() {
11546 a();
11547 b();
11548 ˇ
11549 }
11550 "});
11551
11552 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11553
11554 cx.assert_editor_state(indoc! {"
11555 fn a() {
11556 a();
11557 b();
11558 //•ˇ
11559 }
11560 "});
11561
11562 // If a selection span multiple lines, empty lines are not toggled.
11563 cx.set_state(indoc! {"
11564 fn a() {
11565 «a();
11566
11567 c();ˇ»
11568 }
11569 "});
11570
11571 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11572
11573 cx.assert_editor_state(indoc! {"
11574 fn a() {
11575 // «a();
11576
11577 // c();ˇ»
11578 }
11579 "});
11580
11581 // If a selection includes multiple comment prefixes, all lines are uncommented.
11582 cx.set_state(indoc! {"
11583 fn a() {
11584 «// a();
11585 /// b();
11586 //! c();ˇ»
11587 }
11588 "});
11589
11590 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11591
11592 cx.assert_editor_state(indoc! {"
11593 fn a() {
11594 «a();
11595 b();
11596 c();ˇ»
11597 }
11598 "});
11599}
11600
11601#[gpui::test]
11602async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
11603 init_test(cx, |_| {});
11604 let mut cx = EditorTestContext::new(cx).await;
11605 let language = Arc::new(Language::new(
11606 LanguageConfig {
11607 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
11608 ..Default::default()
11609 },
11610 Some(tree_sitter_rust::LANGUAGE.into()),
11611 ));
11612 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
11613
11614 let toggle_comments = &ToggleComments {
11615 advance_downwards: false,
11616 ignore_indent: true,
11617 };
11618
11619 // If multiple selections intersect a line, the line is only toggled once.
11620 cx.set_state(indoc! {"
11621 fn a() {
11622 // «b();
11623 // c();
11624 // ˇ» d();
11625 }
11626 "});
11627
11628 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11629
11630 cx.assert_editor_state(indoc! {"
11631 fn a() {
11632 «b();
11633 c();
11634 ˇ» d();
11635 }
11636 "});
11637
11638 // The comment prefix is inserted at the beginning of each line
11639 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11640
11641 cx.assert_editor_state(indoc! {"
11642 fn a() {
11643 // «b();
11644 // c();
11645 // ˇ» d();
11646 }
11647 "});
11648
11649 // If a selection ends at the beginning of a line, that line is not toggled.
11650 cx.set_selections_state(indoc! {"
11651 fn a() {
11652 // b();
11653 // «c();
11654 ˇ»// d();
11655 }
11656 "});
11657
11658 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11659
11660 cx.assert_editor_state(indoc! {"
11661 fn a() {
11662 // b();
11663 «c();
11664 ˇ»// d();
11665 }
11666 "});
11667
11668 // If a selection span a single line and is empty, the line is toggled.
11669 cx.set_state(indoc! {"
11670 fn a() {
11671 a();
11672 b();
11673 ˇ
11674 }
11675 "});
11676
11677 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11678
11679 cx.assert_editor_state(indoc! {"
11680 fn a() {
11681 a();
11682 b();
11683 //ˇ
11684 }
11685 "});
11686
11687 // If a selection span multiple lines, empty lines are not toggled.
11688 cx.set_state(indoc! {"
11689 fn a() {
11690 «a();
11691
11692 c();ˇ»
11693 }
11694 "});
11695
11696 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11697
11698 cx.assert_editor_state(indoc! {"
11699 fn a() {
11700 // «a();
11701
11702 // c();ˇ»
11703 }
11704 "});
11705
11706 // If a selection includes multiple comment prefixes, all lines are uncommented.
11707 cx.set_state(indoc! {"
11708 fn a() {
11709 // «a();
11710 /// b();
11711 //! c();ˇ»
11712 }
11713 "});
11714
11715 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11716
11717 cx.assert_editor_state(indoc! {"
11718 fn a() {
11719 «a();
11720 b();
11721 c();ˇ»
11722 }
11723 "});
11724}
11725
11726#[gpui::test]
11727async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
11728 init_test(cx, |_| {});
11729
11730 let language = Arc::new(Language::new(
11731 LanguageConfig {
11732 line_comments: vec!["// ".into()],
11733 ..Default::default()
11734 },
11735 Some(tree_sitter_rust::LANGUAGE.into()),
11736 ));
11737
11738 let mut cx = EditorTestContext::new(cx).await;
11739
11740 cx.language_registry().add(language.clone());
11741 cx.update_buffer(|buffer, cx| {
11742 buffer.set_language(Some(language), cx);
11743 });
11744
11745 let toggle_comments = &ToggleComments {
11746 advance_downwards: true,
11747 ignore_indent: false,
11748 };
11749
11750 // Single cursor on one line -> advance
11751 // Cursor moves horizontally 3 characters as well on non-blank line
11752 cx.set_state(indoc!(
11753 "fn a() {
11754 ˇdog();
11755 cat();
11756 }"
11757 ));
11758 cx.update_editor(|editor, window, cx| {
11759 editor.toggle_comments(toggle_comments, window, cx);
11760 });
11761 cx.assert_editor_state(indoc!(
11762 "fn a() {
11763 // dog();
11764 catˇ();
11765 }"
11766 ));
11767
11768 // Single selection on one line -> don't advance
11769 cx.set_state(indoc!(
11770 "fn a() {
11771 «dog()ˇ»;
11772 cat();
11773 }"
11774 ));
11775 cx.update_editor(|editor, window, cx| {
11776 editor.toggle_comments(toggle_comments, window, cx);
11777 });
11778 cx.assert_editor_state(indoc!(
11779 "fn a() {
11780 // «dog()ˇ»;
11781 cat();
11782 }"
11783 ));
11784
11785 // Multiple cursors on one line -> advance
11786 cx.set_state(indoc!(
11787 "fn a() {
11788 ˇdˇog();
11789 cat();
11790 }"
11791 ));
11792 cx.update_editor(|editor, window, cx| {
11793 editor.toggle_comments(toggle_comments, window, cx);
11794 });
11795 cx.assert_editor_state(indoc!(
11796 "fn a() {
11797 // dog();
11798 catˇ(ˇ);
11799 }"
11800 ));
11801
11802 // Multiple cursors on one line, with selection -> don't advance
11803 cx.set_state(indoc!(
11804 "fn a() {
11805 ˇdˇog«()ˇ»;
11806 cat();
11807 }"
11808 ));
11809 cx.update_editor(|editor, window, cx| {
11810 editor.toggle_comments(toggle_comments, window, cx);
11811 });
11812 cx.assert_editor_state(indoc!(
11813 "fn a() {
11814 // ˇdˇog«()ˇ»;
11815 cat();
11816 }"
11817 ));
11818
11819 // Single cursor on one line -> advance
11820 // Cursor moves to column 0 on blank line
11821 cx.set_state(indoc!(
11822 "fn a() {
11823 ˇdog();
11824
11825 cat();
11826 }"
11827 ));
11828 cx.update_editor(|editor, window, cx| {
11829 editor.toggle_comments(toggle_comments, window, cx);
11830 });
11831 cx.assert_editor_state(indoc!(
11832 "fn a() {
11833 // dog();
11834 ˇ
11835 cat();
11836 }"
11837 ));
11838
11839 // Single cursor on one line -> advance
11840 // Cursor starts and ends at column 0
11841 cx.set_state(indoc!(
11842 "fn a() {
11843 ˇ dog();
11844 cat();
11845 }"
11846 ));
11847 cx.update_editor(|editor, window, cx| {
11848 editor.toggle_comments(toggle_comments, window, cx);
11849 });
11850 cx.assert_editor_state(indoc!(
11851 "fn a() {
11852 // dog();
11853 ˇ cat();
11854 }"
11855 ));
11856}
11857
11858#[gpui::test]
11859async fn test_toggle_block_comment(cx: &mut TestAppContext) {
11860 init_test(cx, |_| {});
11861
11862 let mut cx = EditorTestContext::new(cx).await;
11863
11864 let html_language = Arc::new(
11865 Language::new(
11866 LanguageConfig {
11867 name: "HTML".into(),
11868 block_comment: Some(("<!-- ".into(), " -->".into())),
11869 ..Default::default()
11870 },
11871 Some(tree_sitter_html::LANGUAGE.into()),
11872 )
11873 .with_injection_query(
11874 r#"
11875 (script_element
11876 (raw_text) @injection.content
11877 (#set! injection.language "javascript"))
11878 "#,
11879 )
11880 .unwrap(),
11881 );
11882
11883 let javascript_language = Arc::new(Language::new(
11884 LanguageConfig {
11885 name: "JavaScript".into(),
11886 line_comments: vec!["// ".into()],
11887 ..Default::default()
11888 },
11889 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
11890 ));
11891
11892 cx.language_registry().add(html_language.clone());
11893 cx.language_registry().add(javascript_language.clone());
11894 cx.update_buffer(|buffer, cx| {
11895 buffer.set_language(Some(html_language), cx);
11896 });
11897
11898 // Toggle comments for empty selections
11899 cx.set_state(
11900 &r#"
11901 <p>A</p>ˇ
11902 <p>B</p>ˇ
11903 <p>C</p>ˇ
11904 "#
11905 .unindent(),
11906 );
11907 cx.update_editor(|editor, window, cx| {
11908 editor.toggle_comments(&ToggleComments::default(), window, cx)
11909 });
11910 cx.assert_editor_state(
11911 &r#"
11912 <!-- <p>A</p>ˇ -->
11913 <!-- <p>B</p>ˇ -->
11914 <!-- <p>C</p>ˇ -->
11915 "#
11916 .unindent(),
11917 );
11918 cx.update_editor(|editor, window, cx| {
11919 editor.toggle_comments(&ToggleComments::default(), window, cx)
11920 });
11921 cx.assert_editor_state(
11922 &r#"
11923 <p>A</p>ˇ
11924 <p>B</p>ˇ
11925 <p>C</p>ˇ
11926 "#
11927 .unindent(),
11928 );
11929
11930 // Toggle comments for mixture of empty and non-empty selections, where
11931 // multiple selections occupy a given line.
11932 cx.set_state(
11933 &r#"
11934 <p>A«</p>
11935 <p>ˇ»B</p>ˇ
11936 <p>C«</p>
11937 <p>ˇ»D</p>ˇ
11938 "#
11939 .unindent(),
11940 );
11941
11942 cx.update_editor(|editor, window, cx| {
11943 editor.toggle_comments(&ToggleComments::default(), window, cx)
11944 });
11945 cx.assert_editor_state(
11946 &r#"
11947 <!-- <p>A«</p>
11948 <p>ˇ»B</p>ˇ -->
11949 <!-- <p>C«</p>
11950 <p>ˇ»D</p>ˇ -->
11951 "#
11952 .unindent(),
11953 );
11954 cx.update_editor(|editor, window, cx| {
11955 editor.toggle_comments(&ToggleComments::default(), window, cx)
11956 });
11957 cx.assert_editor_state(
11958 &r#"
11959 <p>A«</p>
11960 <p>ˇ»B</p>ˇ
11961 <p>C«</p>
11962 <p>ˇ»D</p>ˇ
11963 "#
11964 .unindent(),
11965 );
11966
11967 // Toggle comments when different languages are active for different
11968 // selections.
11969 cx.set_state(
11970 &r#"
11971 ˇ<script>
11972 ˇvar x = new Y();
11973 ˇ</script>
11974 "#
11975 .unindent(),
11976 );
11977 cx.executor().run_until_parked();
11978 cx.update_editor(|editor, window, cx| {
11979 editor.toggle_comments(&ToggleComments::default(), window, cx)
11980 });
11981 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
11982 // Uncommenting and commenting from this position brings in even more wrong artifacts.
11983 cx.assert_editor_state(
11984 &r#"
11985 <!-- ˇ<script> -->
11986 // ˇvar x = new Y();
11987 <!-- ˇ</script> -->
11988 "#
11989 .unindent(),
11990 );
11991}
11992
11993#[gpui::test]
11994fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
11995 init_test(cx, |_| {});
11996
11997 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
11998 let multibuffer = cx.new(|cx| {
11999 let mut multibuffer = MultiBuffer::new(ReadWrite);
12000 multibuffer.push_excerpts(
12001 buffer.clone(),
12002 [
12003 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
12004 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
12005 ],
12006 cx,
12007 );
12008 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
12009 multibuffer
12010 });
12011
12012 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12013 editor.update_in(cx, |editor, window, cx| {
12014 assert_eq!(editor.text(cx), "aaaa\nbbbb");
12015 editor.change_selections(None, window, cx, |s| {
12016 s.select_ranges([
12017 Point::new(0, 0)..Point::new(0, 0),
12018 Point::new(1, 0)..Point::new(1, 0),
12019 ])
12020 });
12021
12022 editor.handle_input("X", window, cx);
12023 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
12024 assert_eq!(
12025 editor.selections.ranges(cx),
12026 [
12027 Point::new(0, 1)..Point::new(0, 1),
12028 Point::new(1, 1)..Point::new(1, 1),
12029 ]
12030 );
12031
12032 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
12033 editor.change_selections(None, window, cx, |s| {
12034 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
12035 });
12036 editor.backspace(&Default::default(), window, cx);
12037 assert_eq!(editor.text(cx), "Xa\nbbb");
12038 assert_eq!(
12039 editor.selections.ranges(cx),
12040 [Point::new(1, 0)..Point::new(1, 0)]
12041 );
12042
12043 editor.change_selections(None, window, cx, |s| {
12044 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
12045 });
12046 editor.backspace(&Default::default(), window, cx);
12047 assert_eq!(editor.text(cx), "X\nbb");
12048 assert_eq!(
12049 editor.selections.ranges(cx),
12050 [Point::new(0, 1)..Point::new(0, 1)]
12051 );
12052 });
12053}
12054
12055#[gpui::test]
12056fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
12057 init_test(cx, |_| {});
12058
12059 let markers = vec![('[', ']').into(), ('(', ')').into()];
12060 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
12061 indoc! {"
12062 [aaaa
12063 (bbbb]
12064 cccc)",
12065 },
12066 markers.clone(),
12067 );
12068 let excerpt_ranges = markers.into_iter().map(|marker| {
12069 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
12070 ExcerptRange::new(context.clone())
12071 });
12072 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
12073 let multibuffer = cx.new(|cx| {
12074 let mut multibuffer = MultiBuffer::new(ReadWrite);
12075 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
12076 multibuffer
12077 });
12078
12079 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12080 editor.update_in(cx, |editor, window, cx| {
12081 let (expected_text, selection_ranges) = marked_text_ranges(
12082 indoc! {"
12083 aaaa
12084 bˇbbb
12085 bˇbbˇb
12086 cccc"
12087 },
12088 true,
12089 );
12090 assert_eq!(editor.text(cx), expected_text);
12091 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
12092
12093 editor.handle_input("X", window, cx);
12094
12095 let (expected_text, expected_selections) = marked_text_ranges(
12096 indoc! {"
12097 aaaa
12098 bXˇbbXb
12099 bXˇbbXˇb
12100 cccc"
12101 },
12102 false,
12103 );
12104 assert_eq!(editor.text(cx), expected_text);
12105 assert_eq!(editor.selections.ranges(cx), expected_selections);
12106
12107 editor.newline(&Newline, window, cx);
12108 let (expected_text, expected_selections) = marked_text_ranges(
12109 indoc! {"
12110 aaaa
12111 bX
12112 ˇbbX
12113 b
12114 bX
12115 ˇbbX
12116 ˇb
12117 cccc"
12118 },
12119 false,
12120 );
12121 assert_eq!(editor.text(cx), expected_text);
12122 assert_eq!(editor.selections.ranges(cx), expected_selections);
12123 });
12124}
12125
12126#[gpui::test]
12127fn test_refresh_selections(cx: &mut TestAppContext) {
12128 init_test(cx, |_| {});
12129
12130 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12131 let mut excerpt1_id = None;
12132 let multibuffer = cx.new(|cx| {
12133 let mut multibuffer = MultiBuffer::new(ReadWrite);
12134 excerpt1_id = multibuffer
12135 .push_excerpts(
12136 buffer.clone(),
12137 [
12138 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12139 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12140 ],
12141 cx,
12142 )
12143 .into_iter()
12144 .next();
12145 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12146 multibuffer
12147 });
12148
12149 let editor = cx.add_window(|window, cx| {
12150 let mut editor = build_editor(multibuffer.clone(), window, cx);
12151 let snapshot = editor.snapshot(window, cx);
12152 editor.change_selections(None, window, cx, |s| {
12153 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
12154 });
12155 editor.begin_selection(
12156 Point::new(2, 1).to_display_point(&snapshot),
12157 true,
12158 1,
12159 window,
12160 cx,
12161 );
12162 assert_eq!(
12163 editor.selections.ranges(cx),
12164 [
12165 Point::new(1, 3)..Point::new(1, 3),
12166 Point::new(2, 1)..Point::new(2, 1),
12167 ]
12168 );
12169 editor
12170 });
12171
12172 // Refreshing selections is a no-op when excerpts haven't changed.
12173 _ = editor.update(cx, |editor, window, cx| {
12174 editor.change_selections(None, window, cx, |s| s.refresh());
12175 assert_eq!(
12176 editor.selections.ranges(cx),
12177 [
12178 Point::new(1, 3)..Point::new(1, 3),
12179 Point::new(2, 1)..Point::new(2, 1),
12180 ]
12181 );
12182 });
12183
12184 multibuffer.update(cx, |multibuffer, cx| {
12185 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12186 });
12187 _ = editor.update(cx, |editor, window, cx| {
12188 // Removing an excerpt causes the first selection to become degenerate.
12189 assert_eq!(
12190 editor.selections.ranges(cx),
12191 [
12192 Point::new(0, 0)..Point::new(0, 0),
12193 Point::new(0, 1)..Point::new(0, 1)
12194 ]
12195 );
12196
12197 // Refreshing selections will relocate the first selection to the original buffer
12198 // location.
12199 editor.change_selections(None, window, cx, |s| s.refresh());
12200 assert_eq!(
12201 editor.selections.ranges(cx),
12202 [
12203 Point::new(0, 1)..Point::new(0, 1),
12204 Point::new(0, 3)..Point::new(0, 3)
12205 ]
12206 );
12207 assert!(editor.selections.pending_anchor().is_some());
12208 });
12209}
12210
12211#[gpui::test]
12212fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
12213 init_test(cx, |_| {});
12214
12215 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12216 let mut excerpt1_id = None;
12217 let multibuffer = cx.new(|cx| {
12218 let mut multibuffer = MultiBuffer::new(ReadWrite);
12219 excerpt1_id = multibuffer
12220 .push_excerpts(
12221 buffer.clone(),
12222 [
12223 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12224 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12225 ],
12226 cx,
12227 )
12228 .into_iter()
12229 .next();
12230 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12231 multibuffer
12232 });
12233
12234 let editor = cx.add_window(|window, cx| {
12235 let mut editor = build_editor(multibuffer.clone(), window, cx);
12236 let snapshot = editor.snapshot(window, cx);
12237 editor.begin_selection(
12238 Point::new(1, 3).to_display_point(&snapshot),
12239 false,
12240 1,
12241 window,
12242 cx,
12243 );
12244 assert_eq!(
12245 editor.selections.ranges(cx),
12246 [Point::new(1, 3)..Point::new(1, 3)]
12247 );
12248 editor
12249 });
12250
12251 multibuffer.update(cx, |multibuffer, cx| {
12252 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12253 });
12254 _ = editor.update(cx, |editor, window, cx| {
12255 assert_eq!(
12256 editor.selections.ranges(cx),
12257 [Point::new(0, 0)..Point::new(0, 0)]
12258 );
12259
12260 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
12261 editor.change_selections(None, window, cx, |s| s.refresh());
12262 assert_eq!(
12263 editor.selections.ranges(cx),
12264 [Point::new(0, 3)..Point::new(0, 3)]
12265 );
12266 assert!(editor.selections.pending_anchor().is_some());
12267 });
12268}
12269
12270#[gpui::test]
12271async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
12272 init_test(cx, |_| {});
12273
12274 let language = Arc::new(
12275 Language::new(
12276 LanguageConfig {
12277 brackets: BracketPairConfig {
12278 pairs: vec![
12279 BracketPair {
12280 start: "{".to_string(),
12281 end: "}".to_string(),
12282 close: true,
12283 surround: true,
12284 newline: true,
12285 },
12286 BracketPair {
12287 start: "/* ".to_string(),
12288 end: " */".to_string(),
12289 close: true,
12290 surround: true,
12291 newline: true,
12292 },
12293 ],
12294 ..Default::default()
12295 },
12296 ..Default::default()
12297 },
12298 Some(tree_sitter_rust::LANGUAGE.into()),
12299 )
12300 .with_indents_query("")
12301 .unwrap(),
12302 );
12303
12304 let text = concat!(
12305 "{ }\n", //
12306 " x\n", //
12307 " /* */\n", //
12308 "x\n", //
12309 "{{} }\n", //
12310 );
12311
12312 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
12313 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12314 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
12315 editor
12316 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
12317 .await;
12318
12319 editor.update_in(cx, |editor, window, cx| {
12320 editor.change_selections(None, window, cx, |s| {
12321 s.select_display_ranges([
12322 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
12323 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
12324 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
12325 ])
12326 });
12327 editor.newline(&Newline, window, cx);
12328
12329 assert_eq!(
12330 editor.buffer().read(cx).read(cx).text(),
12331 concat!(
12332 "{ \n", // Suppress rustfmt
12333 "\n", //
12334 "}\n", //
12335 " x\n", //
12336 " /* \n", //
12337 " \n", //
12338 " */\n", //
12339 "x\n", //
12340 "{{} \n", //
12341 "}\n", //
12342 )
12343 );
12344 });
12345}
12346
12347#[gpui::test]
12348fn test_highlighted_ranges(cx: &mut TestAppContext) {
12349 init_test(cx, |_| {});
12350
12351 let editor = cx.add_window(|window, cx| {
12352 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
12353 build_editor(buffer.clone(), window, cx)
12354 });
12355
12356 _ = editor.update(cx, |editor, window, cx| {
12357 struct Type1;
12358 struct Type2;
12359
12360 let buffer = editor.buffer.read(cx).snapshot(cx);
12361
12362 let anchor_range =
12363 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
12364
12365 editor.highlight_background::<Type1>(
12366 &[
12367 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
12368 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
12369 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
12370 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
12371 ],
12372 |_| Hsla::red(),
12373 cx,
12374 );
12375 editor.highlight_background::<Type2>(
12376 &[
12377 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
12378 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
12379 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
12380 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
12381 ],
12382 |_| Hsla::green(),
12383 cx,
12384 );
12385
12386 let snapshot = editor.snapshot(window, cx);
12387 let mut highlighted_ranges = editor.background_highlights_in_range(
12388 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
12389 &snapshot,
12390 cx.theme().colors(),
12391 );
12392 // Enforce a consistent ordering based on color without relying on the ordering of the
12393 // highlight's `TypeId` which is non-executor.
12394 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
12395 assert_eq!(
12396 highlighted_ranges,
12397 &[
12398 (
12399 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
12400 Hsla::red(),
12401 ),
12402 (
12403 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
12404 Hsla::red(),
12405 ),
12406 (
12407 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
12408 Hsla::green(),
12409 ),
12410 (
12411 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
12412 Hsla::green(),
12413 ),
12414 ]
12415 );
12416 assert_eq!(
12417 editor.background_highlights_in_range(
12418 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
12419 &snapshot,
12420 cx.theme().colors(),
12421 ),
12422 &[(
12423 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
12424 Hsla::red(),
12425 )]
12426 );
12427 });
12428}
12429
12430#[gpui::test]
12431async fn test_following(cx: &mut TestAppContext) {
12432 init_test(cx, |_| {});
12433
12434 let fs = FakeFs::new(cx.executor());
12435 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
12436
12437 let buffer = project.update(cx, |project, cx| {
12438 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
12439 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
12440 });
12441 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
12442 let follower = cx.update(|cx| {
12443 cx.open_window(
12444 WindowOptions {
12445 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
12446 gpui::Point::new(px(0.), px(0.)),
12447 gpui::Point::new(px(10.), px(80.)),
12448 ))),
12449 ..Default::default()
12450 },
12451 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
12452 )
12453 .unwrap()
12454 });
12455
12456 let is_still_following = Rc::new(RefCell::new(true));
12457 let follower_edit_event_count = Rc::new(RefCell::new(0));
12458 let pending_update = Rc::new(RefCell::new(None));
12459 let leader_entity = leader.root(cx).unwrap();
12460 let follower_entity = follower.root(cx).unwrap();
12461 _ = follower.update(cx, {
12462 let update = pending_update.clone();
12463 let is_still_following = is_still_following.clone();
12464 let follower_edit_event_count = follower_edit_event_count.clone();
12465 |_, window, cx| {
12466 cx.subscribe_in(
12467 &leader_entity,
12468 window,
12469 move |_, leader, event, window, cx| {
12470 leader.read(cx).add_event_to_update_proto(
12471 event,
12472 &mut update.borrow_mut(),
12473 window,
12474 cx,
12475 );
12476 },
12477 )
12478 .detach();
12479
12480 cx.subscribe_in(
12481 &follower_entity,
12482 window,
12483 move |_, _, event: &EditorEvent, _window, _cx| {
12484 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
12485 *is_still_following.borrow_mut() = false;
12486 }
12487
12488 if let EditorEvent::BufferEdited = event {
12489 *follower_edit_event_count.borrow_mut() += 1;
12490 }
12491 },
12492 )
12493 .detach();
12494 }
12495 });
12496
12497 // Update the selections only
12498 _ = leader.update(cx, |leader, window, cx| {
12499 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
12500 });
12501 follower
12502 .update(cx, |follower, window, cx| {
12503 follower.apply_update_proto(
12504 &project,
12505 pending_update.borrow_mut().take().unwrap(),
12506 window,
12507 cx,
12508 )
12509 })
12510 .unwrap()
12511 .await
12512 .unwrap();
12513 _ = follower.update(cx, |follower, _, cx| {
12514 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
12515 });
12516 assert!(*is_still_following.borrow());
12517 assert_eq!(*follower_edit_event_count.borrow(), 0);
12518
12519 // Update the scroll position only
12520 _ = leader.update(cx, |leader, window, cx| {
12521 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
12522 });
12523 follower
12524 .update(cx, |follower, window, cx| {
12525 follower.apply_update_proto(
12526 &project,
12527 pending_update.borrow_mut().take().unwrap(),
12528 window,
12529 cx,
12530 )
12531 })
12532 .unwrap()
12533 .await
12534 .unwrap();
12535 assert_eq!(
12536 follower
12537 .update(cx, |follower, _, cx| follower.scroll_position(cx))
12538 .unwrap(),
12539 gpui::Point::new(1.5, 3.5)
12540 );
12541 assert!(*is_still_following.borrow());
12542 assert_eq!(*follower_edit_event_count.borrow(), 0);
12543
12544 // Update the selections and scroll position. The follower's scroll position is updated
12545 // via autoscroll, not via the leader's exact scroll position.
12546 _ = leader.update(cx, |leader, window, cx| {
12547 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
12548 leader.request_autoscroll(Autoscroll::newest(), cx);
12549 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
12550 });
12551 follower
12552 .update(cx, |follower, window, cx| {
12553 follower.apply_update_proto(
12554 &project,
12555 pending_update.borrow_mut().take().unwrap(),
12556 window,
12557 cx,
12558 )
12559 })
12560 .unwrap()
12561 .await
12562 .unwrap();
12563 _ = follower.update(cx, |follower, _, cx| {
12564 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
12565 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
12566 });
12567 assert!(*is_still_following.borrow());
12568
12569 // Creating a pending selection that precedes another selection
12570 _ = leader.update(cx, |leader, window, cx| {
12571 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
12572 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
12573 });
12574 follower
12575 .update(cx, |follower, window, cx| {
12576 follower.apply_update_proto(
12577 &project,
12578 pending_update.borrow_mut().take().unwrap(),
12579 window,
12580 cx,
12581 )
12582 })
12583 .unwrap()
12584 .await
12585 .unwrap();
12586 _ = follower.update(cx, |follower, _, cx| {
12587 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
12588 });
12589 assert!(*is_still_following.borrow());
12590
12591 // Extend the pending selection so that it surrounds another selection
12592 _ = leader.update(cx, |leader, window, cx| {
12593 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
12594 });
12595 follower
12596 .update(cx, |follower, window, cx| {
12597 follower.apply_update_proto(
12598 &project,
12599 pending_update.borrow_mut().take().unwrap(),
12600 window,
12601 cx,
12602 )
12603 })
12604 .unwrap()
12605 .await
12606 .unwrap();
12607 _ = follower.update(cx, |follower, _, cx| {
12608 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
12609 });
12610
12611 // Scrolling locally breaks the follow
12612 _ = follower.update(cx, |follower, window, cx| {
12613 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
12614 follower.set_scroll_anchor(
12615 ScrollAnchor {
12616 anchor: top_anchor,
12617 offset: gpui::Point::new(0.0, 0.5),
12618 },
12619 window,
12620 cx,
12621 );
12622 });
12623 assert!(!(*is_still_following.borrow()));
12624}
12625
12626#[gpui::test]
12627async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
12628 init_test(cx, |_| {});
12629
12630 let fs = FakeFs::new(cx.executor());
12631 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
12632 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12633 let pane = workspace
12634 .update(cx, |workspace, _, _| workspace.active_pane().clone())
12635 .unwrap();
12636
12637 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12638
12639 let leader = pane.update_in(cx, |_, window, cx| {
12640 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
12641 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
12642 });
12643
12644 // Start following the editor when it has no excerpts.
12645 let mut state_message =
12646 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
12647 let workspace_entity = workspace.root(cx).unwrap();
12648 let follower_1 = cx
12649 .update_window(*workspace.deref(), |_, window, cx| {
12650 Editor::from_state_proto(
12651 workspace_entity,
12652 ViewId {
12653 creator: CollaboratorId::PeerId(PeerId::default()),
12654 id: 0,
12655 },
12656 &mut state_message,
12657 window,
12658 cx,
12659 )
12660 })
12661 .unwrap()
12662 .unwrap()
12663 .await
12664 .unwrap();
12665
12666 let update_message = Rc::new(RefCell::new(None));
12667 follower_1.update_in(cx, {
12668 let update = update_message.clone();
12669 |_, window, cx| {
12670 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
12671 leader.read(cx).add_event_to_update_proto(
12672 event,
12673 &mut update.borrow_mut(),
12674 window,
12675 cx,
12676 );
12677 })
12678 .detach();
12679 }
12680 });
12681
12682 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
12683 (
12684 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
12685 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
12686 )
12687 });
12688
12689 // Insert some excerpts.
12690 leader.update(cx, |leader, cx| {
12691 leader.buffer.update(cx, |multibuffer, cx| {
12692 multibuffer.set_excerpts_for_path(
12693 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
12694 buffer_1.clone(),
12695 vec![
12696 Point::row_range(0..3),
12697 Point::row_range(1..6),
12698 Point::row_range(12..15),
12699 ],
12700 0,
12701 cx,
12702 );
12703 multibuffer.set_excerpts_for_path(
12704 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
12705 buffer_2.clone(),
12706 vec![Point::row_range(0..6), Point::row_range(8..12)],
12707 0,
12708 cx,
12709 );
12710 });
12711 });
12712
12713 // Apply the update of adding the excerpts.
12714 follower_1
12715 .update_in(cx, |follower, window, cx| {
12716 follower.apply_update_proto(
12717 &project,
12718 update_message.borrow().clone().unwrap(),
12719 window,
12720 cx,
12721 )
12722 })
12723 .await
12724 .unwrap();
12725 assert_eq!(
12726 follower_1.update(cx, |editor, cx| editor.text(cx)),
12727 leader.update(cx, |editor, cx| editor.text(cx))
12728 );
12729 update_message.borrow_mut().take();
12730
12731 // Start following separately after it already has excerpts.
12732 let mut state_message =
12733 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
12734 let workspace_entity = workspace.root(cx).unwrap();
12735 let follower_2 = cx
12736 .update_window(*workspace.deref(), |_, window, cx| {
12737 Editor::from_state_proto(
12738 workspace_entity,
12739 ViewId {
12740 creator: CollaboratorId::PeerId(PeerId::default()),
12741 id: 0,
12742 },
12743 &mut state_message,
12744 window,
12745 cx,
12746 )
12747 })
12748 .unwrap()
12749 .unwrap()
12750 .await
12751 .unwrap();
12752 assert_eq!(
12753 follower_2.update(cx, |editor, cx| editor.text(cx)),
12754 leader.update(cx, |editor, cx| editor.text(cx))
12755 );
12756
12757 // Remove some excerpts.
12758 leader.update(cx, |leader, cx| {
12759 leader.buffer.update(cx, |multibuffer, cx| {
12760 let excerpt_ids = multibuffer.excerpt_ids();
12761 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
12762 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
12763 });
12764 });
12765
12766 // Apply the update of removing the excerpts.
12767 follower_1
12768 .update_in(cx, |follower, window, cx| {
12769 follower.apply_update_proto(
12770 &project,
12771 update_message.borrow().clone().unwrap(),
12772 window,
12773 cx,
12774 )
12775 })
12776 .await
12777 .unwrap();
12778 follower_2
12779 .update_in(cx, |follower, window, cx| {
12780 follower.apply_update_proto(
12781 &project,
12782 update_message.borrow().clone().unwrap(),
12783 window,
12784 cx,
12785 )
12786 })
12787 .await
12788 .unwrap();
12789 update_message.borrow_mut().take();
12790 assert_eq!(
12791 follower_1.update(cx, |editor, cx| editor.text(cx)),
12792 leader.update(cx, |editor, cx| editor.text(cx))
12793 );
12794}
12795
12796#[gpui::test]
12797async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
12798 init_test(cx, |_| {});
12799
12800 let mut cx = EditorTestContext::new(cx).await;
12801 let lsp_store =
12802 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
12803
12804 cx.set_state(indoc! {"
12805 ˇfn func(abc def: i32) -> u32 {
12806 }
12807 "});
12808
12809 cx.update(|_, cx| {
12810 lsp_store.update(cx, |lsp_store, cx| {
12811 lsp_store
12812 .update_diagnostics(
12813 LanguageServerId(0),
12814 lsp::PublishDiagnosticsParams {
12815 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
12816 version: None,
12817 diagnostics: vec![
12818 lsp::Diagnostic {
12819 range: lsp::Range::new(
12820 lsp::Position::new(0, 11),
12821 lsp::Position::new(0, 12),
12822 ),
12823 severity: Some(lsp::DiagnosticSeverity::ERROR),
12824 ..Default::default()
12825 },
12826 lsp::Diagnostic {
12827 range: lsp::Range::new(
12828 lsp::Position::new(0, 12),
12829 lsp::Position::new(0, 15),
12830 ),
12831 severity: Some(lsp::DiagnosticSeverity::ERROR),
12832 ..Default::default()
12833 },
12834 lsp::Diagnostic {
12835 range: lsp::Range::new(
12836 lsp::Position::new(0, 25),
12837 lsp::Position::new(0, 28),
12838 ),
12839 severity: Some(lsp::DiagnosticSeverity::ERROR),
12840 ..Default::default()
12841 },
12842 ],
12843 },
12844 &[],
12845 cx,
12846 )
12847 .unwrap()
12848 });
12849 });
12850
12851 executor.run_until_parked();
12852
12853 cx.update_editor(|editor, window, cx| {
12854 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12855 });
12856
12857 cx.assert_editor_state(indoc! {"
12858 fn func(abc def: i32) -> ˇu32 {
12859 }
12860 "});
12861
12862 cx.update_editor(|editor, window, cx| {
12863 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12864 });
12865
12866 cx.assert_editor_state(indoc! {"
12867 fn func(abc ˇdef: i32) -> u32 {
12868 }
12869 "});
12870
12871 cx.update_editor(|editor, window, cx| {
12872 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12873 });
12874
12875 cx.assert_editor_state(indoc! {"
12876 fn func(abcˇ def: i32) -> u32 {
12877 }
12878 "});
12879
12880 cx.update_editor(|editor, window, cx| {
12881 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12882 });
12883
12884 cx.assert_editor_state(indoc! {"
12885 fn func(abc def: i32) -> ˇu32 {
12886 }
12887 "});
12888}
12889
12890#[gpui::test]
12891async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
12892 init_test(cx, |_| {});
12893
12894 let mut cx = EditorTestContext::new(cx).await;
12895
12896 let diff_base = r#"
12897 use some::mod;
12898
12899 const A: u32 = 42;
12900
12901 fn main() {
12902 println!("hello");
12903
12904 println!("world");
12905 }
12906 "#
12907 .unindent();
12908
12909 // Edits are modified, removed, modified, added
12910 cx.set_state(
12911 &r#"
12912 use some::modified;
12913
12914 ˇ
12915 fn main() {
12916 println!("hello there");
12917
12918 println!("around the");
12919 println!("world");
12920 }
12921 "#
12922 .unindent(),
12923 );
12924
12925 cx.set_head_text(&diff_base);
12926 executor.run_until_parked();
12927
12928 cx.update_editor(|editor, window, cx| {
12929 //Wrap around the bottom of the buffer
12930 for _ in 0..3 {
12931 editor.go_to_next_hunk(&GoToHunk, window, cx);
12932 }
12933 });
12934
12935 cx.assert_editor_state(
12936 &r#"
12937 ˇuse some::modified;
12938
12939
12940 fn main() {
12941 println!("hello there");
12942
12943 println!("around the");
12944 println!("world");
12945 }
12946 "#
12947 .unindent(),
12948 );
12949
12950 cx.update_editor(|editor, window, cx| {
12951 //Wrap around the top of the buffer
12952 for _ in 0..2 {
12953 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12954 }
12955 });
12956
12957 cx.assert_editor_state(
12958 &r#"
12959 use some::modified;
12960
12961
12962 fn main() {
12963 ˇ println!("hello there");
12964
12965 println!("around the");
12966 println!("world");
12967 }
12968 "#
12969 .unindent(),
12970 );
12971
12972 cx.update_editor(|editor, window, cx| {
12973 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12974 });
12975
12976 cx.assert_editor_state(
12977 &r#"
12978 use some::modified;
12979
12980 ˇ
12981 fn main() {
12982 println!("hello there");
12983
12984 println!("around the");
12985 println!("world");
12986 }
12987 "#
12988 .unindent(),
12989 );
12990
12991 cx.update_editor(|editor, window, cx| {
12992 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12993 });
12994
12995 cx.assert_editor_state(
12996 &r#"
12997 ˇuse some::modified;
12998
12999
13000 fn main() {
13001 println!("hello there");
13002
13003 println!("around the");
13004 println!("world");
13005 }
13006 "#
13007 .unindent(),
13008 );
13009
13010 cx.update_editor(|editor, window, cx| {
13011 for _ in 0..2 {
13012 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13013 }
13014 });
13015
13016 cx.assert_editor_state(
13017 &r#"
13018 use some::modified;
13019
13020
13021 fn main() {
13022 ˇ println!("hello there");
13023
13024 println!("around the");
13025 println!("world");
13026 }
13027 "#
13028 .unindent(),
13029 );
13030
13031 cx.update_editor(|editor, window, cx| {
13032 editor.fold(&Fold, window, cx);
13033 });
13034
13035 cx.update_editor(|editor, window, cx| {
13036 editor.go_to_next_hunk(&GoToHunk, window, cx);
13037 });
13038
13039 cx.assert_editor_state(
13040 &r#"
13041 ˇuse some::modified;
13042
13043
13044 fn main() {
13045 println!("hello there");
13046
13047 println!("around the");
13048 println!("world");
13049 }
13050 "#
13051 .unindent(),
13052 );
13053}
13054
13055#[test]
13056fn test_split_words() {
13057 fn split(text: &str) -> Vec<&str> {
13058 split_words(text).collect()
13059 }
13060
13061 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
13062 assert_eq!(split("hello_world"), &["hello_", "world"]);
13063 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
13064 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
13065 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
13066 assert_eq!(split("helloworld"), &["helloworld"]);
13067
13068 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
13069}
13070
13071#[gpui::test]
13072async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
13073 init_test(cx, |_| {});
13074
13075 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
13076 let mut assert = |before, after| {
13077 let _state_context = cx.set_state(before);
13078 cx.run_until_parked();
13079 cx.update_editor(|editor, window, cx| {
13080 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
13081 });
13082 cx.run_until_parked();
13083 cx.assert_editor_state(after);
13084 };
13085
13086 // Outside bracket jumps to outside of matching bracket
13087 assert("console.logˇ(var);", "console.log(var)ˇ;");
13088 assert("console.log(var)ˇ;", "console.logˇ(var);");
13089
13090 // Inside bracket jumps to inside of matching bracket
13091 assert("console.log(ˇvar);", "console.log(varˇ);");
13092 assert("console.log(varˇ);", "console.log(ˇvar);");
13093
13094 // When outside a bracket and inside, favor jumping to the inside bracket
13095 assert(
13096 "console.log('foo', [1, 2, 3]ˇ);",
13097 "console.log(ˇ'foo', [1, 2, 3]);",
13098 );
13099 assert(
13100 "console.log(ˇ'foo', [1, 2, 3]);",
13101 "console.log('foo', [1, 2, 3]ˇ);",
13102 );
13103
13104 // Bias forward if two options are equally likely
13105 assert(
13106 "let result = curried_fun()ˇ();",
13107 "let result = curried_fun()()ˇ;",
13108 );
13109
13110 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
13111 assert(
13112 indoc! {"
13113 function test() {
13114 console.log('test')ˇ
13115 }"},
13116 indoc! {"
13117 function test() {
13118 console.logˇ('test')
13119 }"},
13120 );
13121}
13122
13123#[gpui::test]
13124async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
13125 init_test(cx, |_| {});
13126
13127 let fs = FakeFs::new(cx.executor());
13128 fs.insert_tree(
13129 path!("/a"),
13130 json!({
13131 "main.rs": "fn main() { let a = 5; }",
13132 "other.rs": "// Test file",
13133 }),
13134 )
13135 .await;
13136 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13137
13138 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13139 language_registry.add(Arc::new(Language::new(
13140 LanguageConfig {
13141 name: "Rust".into(),
13142 matcher: LanguageMatcher {
13143 path_suffixes: vec!["rs".to_string()],
13144 ..Default::default()
13145 },
13146 brackets: BracketPairConfig {
13147 pairs: vec![BracketPair {
13148 start: "{".to_string(),
13149 end: "}".to_string(),
13150 close: true,
13151 surround: true,
13152 newline: true,
13153 }],
13154 disabled_scopes_by_bracket_ix: Vec::new(),
13155 },
13156 ..Default::default()
13157 },
13158 Some(tree_sitter_rust::LANGUAGE.into()),
13159 )));
13160 let mut fake_servers = language_registry.register_fake_lsp(
13161 "Rust",
13162 FakeLspAdapter {
13163 capabilities: lsp::ServerCapabilities {
13164 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
13165 first_trigger_character: "{".to_string(),
13166 more_trigger_character: None,
13167 }),
13168 ..Default::default()
13169 },
13170 ..Default::default()
13171 },
13172 );
13173
13174 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13175
13176 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13177
13178 let worktree_id = workspace
13179 .update(cx, |workspace, _, cx| {
13180 workspace.project().update(cx, |project, cx| {
13181 project.worktrees(cx).next().unwrap().read(cx).id()
13182 })
13183 })
13184 .unwrap();
13185
13186 let buffer = project
13187 .update(cx, |project, cx| {
13188 project.open_local_buffer(path!("/a/main.rs"), cx)
13189 })
13190 .await
13191 .unwrap();
13192 let editor_handle = workspace
13193 .update(cx, |workspace, window, cx| {
13194 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
13195 })
13196 .unwrap()
13197 .await
13198 .unwrap()
13199 .downcast::<Editor>()
13200 .unwrap();
13201
13202 cx.executor().start_waiting();
13203 let fake_server = fake_servers.next().await.unwrap();
13204
13205 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
13206 |params, _| async move {
13207 assert_eq!(
13208 params.text_document_position.text_document.uri,
13209 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
13210 );
13211 assert_eq!(
13212 params.text_document_position.position,
13213 lsp::Position::new(0, 21),
13214 );
13215
13216 Ok(Some(vec![lsp::TextEdit {
13217 new_text: "]".to_string(),
13218 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13219 }]))
13220 },
13221 );
13222
13223 editor_handle.update_in(cx, |editor, window, cx| {
13224 window.focus(&editor.focus_handle(cx));
13225 editor.change_selections(None, window, cx, |s| {
13226 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
13227 });
13228 editor.handle_input("{", window, cx);
13229 });
13230
13231 cx.executor().run_until_parked();
13232
13233 buffer.update(cx, |buffer, _| {
13234 assert_eq!(
13235 buffer.text(),
13236 "fn main() { let a = {5}; }",
13237 "No extra braces from on type formatting should appear in the buffer"
13238 )
13239 });
13240}
13241
13242#[gpui::test]
13243async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
13244 init_test(cx, |_| {});
13245
13246 let fs = FakeFs::new(cx.executor());
13247 fs.insert_tree(
13248 path!("/a"),
13249 json!({
13250 "main.rs": "fn main() { let a = 5; }",
13251 "other.rs": "// Test file",
13252 }),
13253 )
13254 .await;
13255
13256 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13257
13258 let server_restarts = Arc::new(AtomicUsize::new(0));
13259 let closure_restarts = Arc::clone(&server_restarts);
13260 let language_server_name = "test language server";
13261 let language_name: LanguageName = "Rust".into();
13262
13263 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13264 language_registry.add(Arc::new(Language::new(
13265 LanguageConfig {
13266 name: language_name.clone(),
13267 matcher: LanguageMatcher {
13268 path_suffixes: vec!["rs".to_string()],
13269 ..Default::default()
13270 },
13271 ..Default::default()
13272 },
13273 Some(tree_sitter_rust::LANGUAGE.into()),
13274 )));
13275 let mut fake_servers = language_registry.register_fake_lsp(
13276 "Rust",
13277 FakeLspAdapter {
13278 name: language_server_name,
13279 initialization_options: Some(json!({
13280 "testOptionValue": true
13281 })),
13282 initializer: Some(Box::new(move |fake_server| {
13283 let task_restarts = Arc::clone(&closure_restarts);
13284 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
13285 task_restarts.fetch_add(1, atomic::Ordering::Release);
13286 futures::future::ready(Ok(()))
13287 });
13288 })),
13289 ..Default::default()
13290 },
13291 );
13292
13293 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13294 let _buffer = project
13295 .update(cx, |project, cx| {
13296 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
13297 })
13298 .await
13299 .unwrap();
13300 let _fake_server = fake_servers.next().await.unwrap();
13301 update_test_language_settings(cx, |language_settings| {
13302 language_settings.languages.insert(
13303 language_name.clone(),
13304 LanguageSettingsContent {
13305 tab_size: NonZeroU32::new(8),
13306 ..Default::default()
13307 },
13308 );
13309 });
13310 cx.executor().run_until_parked();
13311 assert_eq!(
13312 server_restarts.load(atomic::Ordering::Acquire),
13313 0,
13314 "Should not restart LSP server on an unrelated change"
13315 );
13316
13317 update_test_project_settings(cx, |project_settings| {
13318 project_settings.lsp.insert(
13319 "Some other server name".into(),
13320 LspSettings {
13321 binary: None,
13322 settings: None,
13323 initialization_options: Some(json!({
13324 "some other init value": false
13325 })),
13326 enable_lsp_tasks: false,
13327 },
13328 );
13329 });
13330 cx.executor().run_until_parked();
13331 assert_eq!(
13332 server_restarts.load(atomic::Ordering::Acquire),
13333 0,
13334 "Should not restart LSP server on an unrelated LSP settings change"
13335 );
13336
13337 update_test_project_settings(cx, |project_settings| {
13338 project_settings.lsp.insert(
13339 language_server_name.into(),
13340 LspSettings {
13341 binary: None,
13342 settings: None,
13343 initialization_options: Some(json!({
13344 "anotherInitValue": false
13345 })),
13346 enable_lsp_tasks: false,
13347 },
13348 );
13349 });
13350 cx.executor().run_until_parked();
13351 assert_eq!(
13352 server_restarts.load(atomic::Ordering::Acquire),
13353 1,
13354 "Should restart LSP server on a related LSP settings change"
13355 );
13356
13357 update_test_project_settings(cx, |project_settings| {
13358 project_settings.lsp.insert(
13359 language_server_name.into(),
13360 LspSettings {
13361 binary: None,
13362 settings: None,
13363 initialization_options: Some(json!({
13364 "anotherInitValue": false
13365 })),
13366 enable_lsp_tasks: false,
13367 },
13368 );
13369 });
13370 cx.executor().run_until_parked();
13371 assert_eq!(
13372 server_restarts.load(atomic::Ordering::Acquire),
13373 1,
13374 "Should not restart LSP server on a related LSP settings change that is the same"
13375 );
13376
13377 update_test_project_settings(cx, |project_settings| {
13378 project_settings.lsp.insert(
13379 language_server_name.into(),
13380 LspSettings {
13381 binary: None,
13382 settings: None,
13383 initialization_options: None,
13384 enable_lsp_tasks: false,
13385 },
13386 );
13387 });
13388 cx.executor().run_until_parked();
13389 assert_eq!(
13390 server_restarts.load(atomic::Ordering::Acquire),
13391 2,
13392 "Should restart LSP server on another related LSP settings change"
13393 );
13394}
13395
13396#[gpui::test]
13397async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
13398 init_test(cx, |_| {});
13399
13400 let mut cx = EditorLspTestContext::new_rust(
13401 lsp::ServerCapabilities {
13402 completion_provider: Some(lsp::CompletionOptions {
13403 trigger_characters: Some(vec![".".to_string()]),
13404 resolve_provider: Some(true),
13405 ..Default::default()
13406 }),
13407 ..Default::default()
13408 },
13409 cx,
13410 )
13411 .await;
13412
13413 cx.set_state("fn main() { let a = 2ˇ; }");
13414 cx.simulate_keystroke(".");
13415 let completion_item = lsp::CompletionItem {
13416 label: "some".into(),
13417 kind: Some(lsp::CompletionItemKind::SNIPPET),
13418 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13419 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13420 kind: lsp::MarkupKind::Markdown,
13421 value: "```rust\nSome(2)\n```".to_string(),
13422 })),
13423 deprecated: Some(false),
13424 sort_text: Some("fffffff2".to_string()),
13425 filter_text: Some("some".to_string()),
13426 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13427 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13428 range: lsp::Range {
13429 start: lsp::Position {
13430 line: 0,
13431 character: 22,
13432 },
13433 end: lsp::Position {
13434 line: 0,
13435 character: 22,
13436 },
13437 },
13438 new_text: "Some(2)".to_string(),
13439 })),
13440 additional_text_edits: Some(vec![lsp::TextEdit {
13441 range: lsp::Range {
13442 start: lsp::Position {
13443 line: 0,
13444 character: 20,
13445 },
13446 end: lsp::Position {
13447 line: 0,
13448 character: 22,
13449 },
13450 },
13451 new_text: "".to_string(),
13452 }]),
13453 ..Default::default()
13454 };
13455
13456 let closure_completion_item = completion_item.clone();
13457 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13458 let task_completion_item = closure_completion_item.clone();
13459 async move {
13460 Ok(Some(lsp::CompletionResponse::Array(vec![
13461 task_completion_item,
13462 ])))
13463 }
13464 });
13465
13466 request.next().await;
13467
13468 cx.condition(|editor, _| editor.context_menu_visible())
13469 .await;
13470 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13471 editor
13472 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13473 .unwrap()
13474 });
13475 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
13476
13477 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
13478 let task_completion_item = completion_item.clone();
13479 async move { Ok(task_completion_item) }
13480 })
13481 .next()
13482 .await
13483 .unwrap();
13484 apply_additional_edits.await.unwrap();
13485 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
13486}
13487
13488#[gpui::test]
13489async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
13490 init_test(cx, |_| {});
13491
13492 let mut cx = EditorLspTestContext::new_rust(
13493 lsp::ServerCapabilities {
13494 completion_provider: Some(lsp::CompletionOptions {
13495 trigger_characters: Some(vec![".".to_string()]),
13496 resolve_provider: Some(true),
13497 ..Default::default()
13498 }),
13499 ..Default::default()
13500 },
13501 cx,
13502 )
13503 .await;
13504
13505 cx.set_state("fn main() { let a = 2ˇ; }");
13506 cx.simulate_keystroke(".");
13507
13508 let item1 = lsp::CompletionItem {
13509 label: "method id()".to_string(),
13510 filter_text: Some("id".to_string()),
13511 detail: None,
13512 documentation: None,
13513 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13514 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13515 new_text: ".id".to_string(),
13516 })),
13517 ..lsp::CompletionItem::default()
13518 };
13519
13520 let item2 = lsp::CompletionItem {
13521 label: "other".to_string(),
13522 filter_text: Some("other".to_string()),
13523 detail: None,
13524 documentation: None,
13525 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13526 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13527 new_text: ".other".to_string(),
13528 })),
13529 ..lsp::CompletionItem::default()
13530 };
13531
13532 let item1 = item1.clone();
13533 cx.set_request_handler::<lsp::request::Completion, _, _>({
13534 let item1 = item1.clone();
13535 move |_, _, _| {
13536 let item1 = item1.clone();
13537 let item2 = item2.clone();
13538 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
13539 }
13540 })
13541 .next()
13542 .await;
13543
13544 cx.condition(|editor, _| editor.context_menu_visible())
13545 .await;
13546 cx.update_editor(|editor, _, _| {
13547 let context_menu = editor.context_menu.borrow_mut();
13548 let context_menu = context_menu
13549 .as_ref()
13550 .expect("Should have the context menu deployed");
13551 match context_menu {
13552 CodeContextMenu::Completions(completions_menu) => {
13553 let completions = completions_menu.completions.borrow_mut();
13554 assert_eq!(
13555 completions
13556 .iter()
13557 .map(|completion| &completion.label.text)
13558 .collect::<Vec<_>>(),
13559 vec!["method id()", "other"]
13560 )
13561 }
13562 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13563 }
13564 });
13565
13566 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
13567 let item1 = item1.clone();
13568 move |_, item_to_resolve, _| {
13569 let item1 = item1.clone();
13570 async move {
13571 if item1 == item_to_resolve {
13572 Ok(lsp::CompletionItem {
13573 label: "method id()".to_string(),
13574 filter_text: Some("id".to_string()),
13575 detail: Some("Now resolved!".to_string()),
13576 documentation: Some(lsp::Documentation::String("Docs".to_string())),
13577 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13578 range: lsp::Range::new(
13579 lsp::Position::new(0, 22),
13580 lsp::Position::new(0, 22),
13581 ),
13582 new_text: ".id".to_string(),
13583 })),
13584 ..lsp::CompletionItem::default()
13585 })
13586 } else {
13587 Ok(item_to_resolve)
13588 }
13589 }
13590 }
13591 })
13592 .next()
13593 .await
13594 .unwrap();
13595 cx.run_until_parked();
13596
13597 cx.update_editor(|editor, window, cx| {
13598 editor.context_menu_next(&Default::default(), window, cx);
13599 });
13600
13601 cx.update_editor(|editor, _, _| {
13602 let context_menu = editor.context_menu.borrow_mut();
13603 let context_menu = context_menu
13604 .as_ref()
13605 .expect("Should have the context menu deployed");
13606 match context_menu {
13607 CodeContextMenu::Completions(completions_menu) => {
13608 let completions = completions_menu.completions.borrow_mut();
13609 assert_eq!(
13610 completions
13611 .iter()
13612 .map(|completion| &completion.label.text)
13613 .collect::<Vec<_>>(),
13614 vec!["method id() Now resolved!", "other"],
13615 "Should update first completion label, but not second as the filter text did not match."
13616 );
13617 }
13618 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13619 }
13620 });
13621}
13622
13623#[gpui::test]
13624async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
13625 init_test(cx, |_| {});
13626
13627 let mut cx = EditorLspTestContext::new_rust(
13628 lsp::ServerCapabilities {
13629 completion_provider: Some(lsp::CompletionOptions {
13630 trigger_characters: Some(vec![".".to_string()]),
13631 resolve_provider: Some(true),
13632 ..Default::default()
13633 }),
13634 ..Default::default()
13635 },
13636 cx,
13637 )
13638 .await;
13639
13640 cx.set_state("fn main() { let a = 2ˇ; }");
13641 cx.simulate_keystroke(".");
13642
13643 let unresolved_item_1 = lsp::CompletionItem {
13644 label: "id".to_string(),
13645 filter_text: Some("id".to_string()),
13646 detail: None,
13647 documentation: None,
13648 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13649 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13650 new_text: ".id".to_string(),
13651 })),
13652 ..lsp::CompletionItem::default()
13653 };
13654 let resolved_item_1 = lsp::CompletionItem {
13655 additional_text_edits: Some(vec![lsp::TextEdit {
13656 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
13657 new_text: "!!".to_string(),
13658 }]),
13659 ..unresolved_item_1.clone()
13660 };
13661 let unresolved_item_2 = lsp::CompletionItem {
13662 label: "other".to_string(),
13663 filter_text: Some("other".to_string()),
13664 detail: None,
13665 documentation: None,
13666 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13667 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13668 new_text: ".other".to_string(),
13669 })),
13670 ..lsp::CompletionItem::default()
13671 };
13672 let resolved_item_2 = lsp::CompletionItem {
13673 additional_text_edits: Some(vec![lsp::TextEdit {
13674 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
13675 new_text: "??".to_string(),
13676 }]),
13677 ..unresolved_item_2.clone()
13678 };
13679
13680 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
13681 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
13682 cx.lsp
13683 .server
13684 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
13685 let unresolved_item_1 = unresolved_item_1.clone();
13686 let resolved_item_1 = resolved_item_1.clone();
13687 let unresolved_item_2 = unresolved_item_2.clone();
13688 let resolved_item_2 = resolved_item_2.clone();
13689 let resolve_requests_1 = resolve_requests_1.clone();
13690 let resolve_requests_2 = resolve_requests_2.clone();
13691 move |unresolved_request, _| {
13692 let unresolved_item_1 = unresolved_item_1.clone();
13693 let resolved_item_1 = resolved_item_1.clone();
13694 let unresolved_item_2 = unresolved_item_2.clone();
13695 let resolved_item_2 = resolved_item_2.clone();
13696 let resolve_requests_1 = resolve_requests_1.clone();
13697 let resolve_requests_2 = resolve_requests_2.clone();
13698 async move {
13699 if unresolved_request == unresolved_item_1 {
13700 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
13701 Ok(resolved_item_1.clone())
13702 } else if unresolved_request == unresolved_item_2 {
13703 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
13704 Ok(resolved_item_2.clone())
13705 } else {
13706 panic!("Unexpected completion item {unresolved_request:?}")
13707 }
13708 }
13709 }
13710 })
13711 .detach();
13712
13713 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13714 let unresolved_item_1 = unresolved_item_1.clone();
13715 let unresolved_item_2 = unresolved_item_2.clone();
13716 async move {
13717 Ok(Some(lsp::CompletionResponse::Array(vec![
13718 unresolved_item_1,
13719 unresolved_item_2,
13720 ])))
13721 }
13722 })
13723 .next()
13724 .await;
13725
13726 cx.condition(|editor, _| editor.context_menu_visible())
13727 .await;
13728 cx.update_editor(|editor, _, _| {
13729 let context_menu = editor.context_menu.borrow_mut();
13730 let context_menu = context_menu
13731 .as_ref()
13732 .expect("Should have the context menu deployed");
13733 match context_menu {
13734 CodeContextMenu::Completions(completions_menu) => {
13735 let completions = completions_menu.completions.borrow_mut();
13736 assert_eq!(
13737 completions
13738 .iter()
13739 .map(|completion| &completion.label.text)
13740 .collect::<Vec<_>>(),
13741 vec!["id", "other"]
13742 )
13743 }
13744 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13745 }
13746 });
13747 cx.run_until_parked();
13748
13749 cx.update_editor(|editor, window, cx| {
13750 editor.context_menu_next(&ContextMenuNext, window, cx);
13751 });
13752 cx.run_until_parked();
13753 cx.update_editor(|editor, window, cx| {
13754 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
13755 });
13756 cx.run_until_parked();
13757 cx.update_editor(|editor, window, cx| {
13758 editor.context_menu_next(&ContextMenuNext, window, cx);
13759 });
13760 cx.run_until_parked();
13761 cx.update_editor(|editor, window, cx| {
13762 editor
13763 .compose_completion(&ComposeCompletion::default(), window, cx)
13764 .expect("No task returned")
13765 })
13766 .await
13767 .expect("Completion failed");
13768 cx.run_until_parked();
13769
13770 cx.update_editor(|editor, _, cx| {
13771 assert_eq!(
13772 resolve_requests_1.load(atomic::Ordering::Acquire),
13773 1,
13774 "Should always resolve once despite multiple selections"
13775 );
13776 assert_eq!(
13777 resolve_requests_2.load(atomic::Ordering::Acquire),
13778 1,
13779 "Should always resolve once after multiple selections and applying the completion"
13780 );
13781 assert_eq!(
13782 editor.text(cx),
13783 "fn main() { let a = ??.other; }",
13784 "Should use resolved data when applying the completion"
13785 );
13786 });
13787}
13788
13789#[gpui::test]
13790async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
13791 init_test(cx, |_| {});
13792
13793 let item_0 = lsp::CompletionItem {
13794 label: "abs".into(),
13795 insert_text: Some("abs".into()),
13796 data: Some(json!({ "very": "special"})),
13797 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
13798 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
13799 lsp::InsertReplaceEdit {
13800 new_text: "abs".to_string(),
13801 insert: lsp::Range::default(),
13802 replace: lsp::Range::default(),
13803 },
13804 )),
13805 ..lsp::CompletionItem::default()
13806 };
13807 let items = iter::once(item_0.clone())
13808 .chain((11..51).map(|i| lsp::CompletionItem {
13809 label: format!("item_{}", i),
13810 insert_text: Some(format!("item_{}", i)),
13811 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
13812 ..lsp::CompletionItem::default()
13813 }))
13814 .collect::<Vec<_>>();
13815
13816 let default_commit_characters = vec!["?".to_string()];
13817 let default_data = json!({ "default": "data"});
13818 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
13819 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
13820 let default_edit_range = lsp::Range {
13821 start: lsp::Position {
13822 line: 0,
13823 character: 5,
13824 },
13825 end: lsp::Position {
13826 line: 0,
13827 character: 5,
13828 },
13829 };
13830
13831 let mut cx = EditorLspTestContext::new_rust(
13832 lsp::ServerCapabilities {
13833 completion_provider: Some(lsp::CompletionOptions {
13834 trigger_characters: Some(vec![".".to_string()]),
13835 resolve_provider: Some(true),
13836 ..Default::default()
13837 }),
13838 ..Default::default()
13839 },
13840 cx,
13841 )
13842 .await;
13843
13844 cx.set_state("fn main() { let a = 2ˇ; }");
13845 cx.simulate_keystroke(".");
13846
13847 let completion_data = default_data.clone();
13848 let completion_characters = default_commit_characters.clone();
13849 let completion_items = items.clone();
13850 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13851 let default_data = completion_data.clone();
13852 let default_commit_characters = completion_characters.clone();
13853 let items = completion_items.clone();
13854 async move {
13855 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13856 items,
13857 item_defaults: Some(lsp::CompletionListItemDefaults {
13858 data: Some(default_data.clone()),
13859 commit_characters: Some(default_commit_characters.clone()),
13860 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
13861 default_edit_range,
13862 )),
13863 insert_text_format: Some(default_insert_text_format),
13864 insert_text_mode: Some(default_insert_text_mode),
13865 }),
13866 ..lsp::CompletionList::default()
13867 })))
13868 }
13869 })
13870 .next()
13871 .await;
13872
13873 let resolved_items = Arc::new(Mutex::new(Vec::new()));
13874 cx.lsp
13875 .server
13876 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
13877 let closure_resolved_items = resolved_items.clone();
13878 move |item_to_resolve, _| {
13879 let closure_resolved_items = closure_resolved_items.clone();
13880 async move {
13881 closure_resolved_items.lock().push(item_to_resolve.clone());
13882 Ok(item_to_resolve)
13883 }
13884 }
13885 })
13886 .detach();
13887
13888 cx.condition(|editor, _| editor.context_menu_visible())
13889 .await;
13890 cx.run_until_parked();
13891 cx.update_editor(|editor, _, _| {
13892 let menu = editor.context_menu.borrow_mut();
13893 match menu.as_ref().expect("should have the completions menu") {
13894 CodeContextMenu::Completions(completions_menu) => {
13895 assert_eq!(
13896 completions_menu
13897 .entries
13898 .borrow()
13899 .iter()
13900 .map(|mat| mat.string.clone())
13901 .collect::<Vec<String>>(),
13902 items
13903 .iter()
13904 .map(|completion| completion.label.clone())
13905 .collect::<Vec<String>>()
13906 );
13907 }
13908 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
13909 }
13910 });
13911 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
13912 // with 4 from the end.
13913 assert_eq!(
13914 *resolved_items.lock(),
13915 [&items[0..16], &items[items.len() - 4..items.len()]]
13916 .concat()
13917 .iter()
13918 .cloned()
13919 .map(|mut item| {
13920 if item.data.is_none() {
13921 item.data = Some(default_data.clone());
13922 }
13923 item
13924 })
13925 .collect::<Vec<lsp::CompletionItem>>(),
13926 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
13927 );
13928 resolved_items.lock().clear();
13929
13930 cx.update_editor(|editor, window, cx| {
13931 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
13932 });
13933 cx.run_until_parked();
13934 // Completions that have already been resolved are skipped.
13935 assert_eq!(
13936 *resolved_items.lock(),
13937 items[items.len() - 16..items.len() - 4]
13938 .iter()
13939 .cloned()
13940 .map(|mut item| {
13941 if item.data.is_none() {
13942 item.data = Some(default_data.clone());
13943 }
13944 item
13945 })
13946 .collect::<Vec<lsp::CompletionItem>>()
13947 );
13948 resolved_items.lock().clear();
13949}
13950
13951#[gpui::test]
13952async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
13953 init_test(cx, |_| {});
13954
13955 let mut cx = EditorLspTestContext::new(
13956 Language::new(
13957 LanguageConfig {
13958 matcher: LanguageMatcher {
13959 path_suffixes: vec!["jsx".into()],
13960 ..Default::default()
13961 },
13962 overrides: [(
13963 "element".into(),
13964 LanguageConfigOverride {
13965 completion_query_characters: Override::Set(['-'].into_iter().collect()),
13966 ..Default::default()
13967 },
13968 )]
13969 .into_iter()
13970 .collect(),
13971 ..Default::default()
13972 },
13973 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
13974 )
13975 .with_override_query("(jsx_self_closing_element) @element")
13976 .unwrap(),
13977 lsp::ServerCapabilities {
13978 completion_provider: Some(lsp::CompletionOptions {
13979 trigger_characters: Some(vec![":".to_string()]),
13980 ..Default::default()
13981 }),
13982 ..Default::default()
13983 },
13984 cx,
13985 )
13986 .await;
13987
13988 cx.lsp
13989 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13990 Ok(Some(lsp::CompletionResponse::Array(vec![
13991 lsp::CompletionItem {
13992 label: "bg-blue".into(),
13993 ..Default::default()
13994 },
13995 lsp::CompletionItem {
13996 label: "bg-red".into(),
13997 ..Default::default()
13998 },
13999 lsp::CompletionItem {
14000 label: "bg-yellow".into(),
14001 ..Default::default()
14002 },
14003 ])))
14004 });
14005
14006 cx.set_state(r#"<p class="bgˇ" />"#);
14007
14008 // Trigger completion when typing a dash, because the dash is an extra
14009 // word character in the 'element' scope, which contains the cursor.
14010 cx.simulate_keystroke("-");
14011 cx.executor().run_until_parked();
14012 cx.update_editor(|editor, _, _| {
14013 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14014 {
14015 assert_eq!(
14016 completion_menu_entries(&menu),
14017 &["bg-red", "bg-blue", "bg-yellow"]
14018 );
14019 } else {
14020 panic!("expected completion menu to be open");
14021 }
14022 });
14023
14024 cx.simulate_keystroke("l");
14025 cx.executor().run_until_parked();
14026 cx.update_editor(|editor, _, _| {
14027 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14028 {
14029 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
14030 } else {
14031 panic!("expected completion menu to be open");
14032 }
14033 });
14034
14035 // When filtering completions, consider the character after the '-' to
14036 // be the start of a subword.
14037 cx.set_state(r#"<p class="yelˇ" />"#);
14038 cx.simulate_keystroke("l");
14039 cx.executor().run_until_parked();
14040 cx.update_editor(|editor, _, _| {
14041 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14042 {
14043 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
14044 } else {
14045 panic!("expected completion menu to be open");
14046 }
14047 });
14048}
14049
14050fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
14051 let entries = menu.entries.borrow();
14052 entries.iter().map(|mat| mat.string.clone()).collect()
14053}
14054
14055#[gpui::test]
14056async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
14057 init_test(cx, |settings| {
14058 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
14059 FormatterList(vec![Formatter::Prettier].into()),
14060 ))
14061 });
14062
14063 let fs = FakeFs::new(cx.executor());
14064 fs.insert_file(path!("/file.ts"), Default::default()).await;
14065
14066 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
14067 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14068
14069 language_registry.add(Arc::new(Language::new(
14070 LanguageConfig {
14071 name: "TypeScript".into(),
14072 matcher: LanguageMatcher {
14073 path_suffixes: vec!["ts".to_string()],
14074 ..Default::default()
14075 },
14076 ..Default::default()
14077 },
14078 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14079 )));
14080 update_test_language_settings(cx, |settings| {
14081 settings.defaults.prettier = Some(PrettierSettings {
14082 allowed: true,
14083 ..PrettierSettings::default()
14084 });
14085 });
14086
14087 let test_plugin = "test_plugin";
14088 let _ = language_registry.register_fake_lsp(
14089 "TypeScript",
14090 FakeLspAdapter {
14091 prettier_plugins: vec![test_plugin],
14092 ..Default::default()
14093 },
14094 );
14095
14096 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
14097 let buffer = project
14098 .update(cx, |project, cx| {
14099 project.open_local_buffer(path!("/file.ts"), cx)
14100 })
14101 .await
14102 .unwrap();
14103
14104 let buffer_text = "one\ntwo\nthree\n";
14105 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14106 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14107 editor.update_in(cx, |editor, window, cx| {
14108 editor.set_text(buffer_text, window, cx)
14109 });
14110
14111 editor
14112 .update_in(cx, |editor, window, cx| {
14113 editor.perform_format(
14114 project.clone(),
14115 FormatTrigger::Manual,
14116 FormatTarget::Buffers,
14117 window,
14118 cx,
14119 )
14120 })
14121 .unwrap()
14122 .await;
14123 assert_eq!(
14124 editor.update(cx, |editor, cx| editor.text(cx)),
14125 buffer_text.to_string() + prettier_format_suffix,
14126 "Test prettier formatting was not applied to the original buffer text",
14127 );
14128
14129 update_test_language_settings(cx, |settings| {
14130 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
14131 });
14132 let format = editor.update_in(cx, |editor, window, cx| {
14133 editor.perform_format(
14134 project.clone(),
14135 FormatTrigger::Manual,
14136 FormatTarget::Buffers,
14137 window,
14138 cx,
14139 )
14140 });
14141 format.await.unwrap();
14142 assert_eq!(
14143 editor.update(cx, |editor, cx| editor.text(cx)),
14144 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
14145 "Autoformatting (via test prettier) was not applied to the original buffer text",
14146 );
14147}
14148
14149#[gpui::test]
14150async fn test_addition_reverts(cx: &mut TestAppContext) {
14151 init_test(cx, |_| {});
14152 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14153 let base_text = indoc! {r#"
14154 struct Row;
14155 struct Row1;
14156 struct Row2;
14157
14158 struct Row4;
14159 struct Row5;
14160 struct Row6;
14161
14162 struct Row8;
14163 struct Row9;
14164 struct Row10;"#};
14165
14166 // When addition hunks are not adjacent to carets, no hunk revert is performed
14167 assert_hunk_revert(
14168 indoc! {r#"struct Row;
14169 struct Row1;
14170 struct Row1.1;
14171 struct Row1.2;
14172 struct Row2;ˇ
14173
14174 struct Row4;
14175 struct Row5;
14176 struct Row6;
14177
14178 struct Row8;
14179 ˇstruct Row9;
14180 struct Row9.1;
14181 struct Row9.2;
14182 struct Row9.3;
14183 struct Row10;"#},
14184 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
14185 indoc! {r#"struct Row;
14186 struct Row1;
14187 struct Row1.1;
14188 struct Row1.2;
14189 struct Row2;ˇ
14190
14191 struct Row4;
14192 struct Row5;
14193 struct Row6;
14194
14195 struct Row8;
14196 ˇstruct Row9;
14197 struct Row9.1;
14198 struct Row9.2;
14199 struct Row9.3;
14200 struct Row10;"#},
14201 base_text,
14202 &mut cx,
14203 );
14204 // Same for selections
14205 assert_hunk_revert(
14206 indoc! {r#"struct Row;
14207 struct Row1;
14208 struct Row2;
14209 struct Row2.1;
14210 struct Row2.2;
14211 «ˇ
14212 struct Row4;
14213 struct» Row5;
14214 «struct Row6;
14215 ˇ»
14216 struct Row9.1;
14217 struct Row9.2;
14218 struct Row9.3;
14219 struct Row8;
14220 struct Row9;
14221 struct Row10;"#},
14222 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
14223 indoc! {r#"struct Row;
14224 struct Row1;
14225 struct Row2;
14226 struct Row2.1;
14227 struct Row2.2;
14228 «ˇ
14229 struct Row4;
14230 struct» Row5;
14231 «struct Row6;
14232 ˇ»
14233 struct Row9.1;
14234 struct Row9.2;
14235 struct Row9.3;
14236 struct Row8;
14237 struct Row9;
14238 struct Row10;"#},
14239 base_text,
14240 &mut cx,
14241 );
14242
14243 // When carets and selections intersect the addition hunks, those are reverted.
14244 // Adjacent carets got merged.
14245 assert_hunk_revert(
14246 indoc! {r#"struct Row;
14247 ˇ// something on the top
14248 struct Row1;
14249 struct Row2;
14250 struct Roˇw3.1;
14251 struct Row2.2;
14252 struct Row2.3;ˇ
14253
14254 struct Row4;
14255 struct ˇRow5.1;
14256 struct Row5.2;
14257 struct «Rowˇ»5.3;
14258 struct Row5;
14259 struct Row6;
14260 ˇ
14261 struct Row9.1;
14262 struct «Rowˇ»9.2;
14263 struct «ˇRow»9.3;
14264 struct Row8;
14265 struct Row9;
14266 «ˇ// something on bottom»
14267 struct Row10;"#},
14268 vec![
14269 DiffHunkStatusKind::Added,
14270 DiffHunkStatusKind::Added,
14271 DiffHunkStatusKind::Added,
14272 DiffHunkStatusKind::Added,
14273 DiffHunkStatusKind::Added,
14274 ],
14275 indoc! {r#"struct Row;
14276 ˇstruct Row1;
14277 struct Row2;
14278 ˇ
14279 struct Row4;
14280 ˇstruct Row5;
14281 struct Row6;
14282 ˇ
14283 ˇstruct Row8;
14284 struct Row9;
14285 ˇstruct Row10;"#},
14286 base_text,
14287 &mut cx,
14288 );
14289}
14290
14291#[gpui::test]
14292async fn test_modification_reverts(cx: &mut TestAppContext) {
14293 init_test(cx, |_| {});
14294 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14295 let base_text = indoc! {r#"
14296 struct Row;
14297 struct Row1;
14298 struct Row2;
14299
14300 struct Row4;
14301 struct Row5;
14302 struct Row6;
14303
14304 struct Row8;
14305 struct Row9;
14306 struct Row10;"#};
14307
14308 // Modification hunks behave the same as the addition ones.
14309 assert_hunk_revert(
14310 indoc! {r#"struct Row;
14311 struct Row1;
14312 struct Row33;
14313 ˇ
14314 struct Row4;
14315 struct Row5;
14316 struct Row6;
14317 ˇ
14318 struct Row99;
14319 struct Row9;
14320 struct Row10;"#},
14321 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
14322 indoc! {r#"struct Row;
14323 struct Row1;
14324 struct Row33;
14325 ˇ
14326 struct Row4;
14327 struct Row5;
14328 struct Row6;
14329 ˇ
14330 struct Row99;
14331 struct Row9;
14332 struct Row10;"#},
14333 base_text,
14334 &mut cx,
14335 );
14336 assert_hunk_revert(
14337 indoc! {r#"struct Row;
14338 struct Row1;
14339 struct Row33;
14340 «ˇ
14341 struct Row4;
14342 struct» Row5;
14343 «struct Row6;
14344 ˇ»
14345 struct Row99;
14346 struct Row9;
14347 struct Row10;"#},
14348 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
14349 indoc! {r#"struct Row;
14350 struct Row1;
14351 struct Row33;
14352 «ˇ
14353 struct Row4;
14354 struct» Row5;
14355 «struct Row6;
14356 ˇ»
14357 struct Row99;
14358 struct Row9;
14359 struct Row10;"#},
14360 base_text,
14361 &mut cx,
14362 );
14363
14364 assert_hunk_revert(
14365 indoc! {r#"ˇstruct Row1.1;
14366 struct Row1;
14367 «ˇstr»uct Row22;
14368
14369 struct ˇRow44;
14370 struct Row5;
14371 struct «Rˇ»ow66;ˇ
14372
14373 «struˇ»ct Row88;
14374 struct Row9;
14375 struct Row1011;ˇ"#},
14376 vec![
14377 DiffHunkStatusKind::Modified,
14378 DiffHunkStatusKind::Modified,
14379 DiffHunkStatusKind::Modified,
14380 DiffHunkStatusKind::Modified,
14381 DiffHunkStatusKind::Modified,
14382 DiffHunkStatusKind::Modified,
14383 ],
14384 indoc! {r#"struct Row;
14385 ˇstruct Row1;
14386 struct Row2;
14387 ˇ
14388 struct Row4;
14389 ˇstruct Row5;
14390 struct Row6;
14391 ˇ
14392 struct Row8;
14393 ˇstruct Row9;
14394 struct Row10;ˇ"#},
14395 base_text,
14396 &mut cx,
14397 );
14398}
14399
14400#[gpui::test]
14401async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
14402 init_test(cx, |_| {});
14403 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14404 let base_text = indoc! {r#"
14405 one
14406
14407 two
14408 three
14409 "#};
14410
14411 cx.set_head_text(base_text);
14412 cx.set_state("\nˇ\n");
14413 cx.executor().run_until_parked();
14414 cx.update_editor(|editor, _window, cx| {
14415 editor.expand_selected_diff_hunks(cx);
14416 });
14417 cx.executor().run_until_parked();
14418 cx.update_editor(|editor, window, cx| {
14419 editor.backspace(&Default::default(), window, cx);
14420 });
14421 cx.run_until_parked();
14422 cx.assert_state_with_diff(
14423 indoc! {r#"
14424
14425 - two
14426 - threeˇ
14427 +
14428 "#}
14429 .to_string(),
14430 );
14431}
14432
14433#[gpui::test]
14434async fn test_deletion_reverts(cx: &mut TestAppContext) {
14435 init_test(cx, |_| {});
14436 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14437 let base_text = indoc! {r#"struct Row;
14438struct Row1;
14439struct Row2;
14440
14441struct Row4;
14442struct Row5;
14443struct Row6;
14444
14445struct Row8;
14446struct Row9;
14447struct Row10;"#};
14448
14449 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
14450 assert_hunk_revert(
14451 indoc! {r#"struct Row;
14452 struct Row2;
14453
14454 ˇstruct Row4;
14455 struct Row5;
14456 struct Row6;
14457 ˇ
14458 struct Row8;
14459 struct Row10;"#},
14460 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14461 indoc! {r#"struct Row;
14462 struct Row2;
14463
14464 ˇstruct Row4;
14465 struct Row5;
14466 struct Row6;
14467 ˇ
14468 struct Row8;
14469 struct Row10;"#},
14470 base_text,
14471 &mut cx,
14472 );
14473 assert_hunk_revert(
14474 indoc! {r#"struct Row;
14475 struct Row2;
14476
14477 «ˇstruct Row4;
14478 struct» Row5;
14479 «struct Row6;
14480 ˇ»
14481 struct Row8;
14482 struct Row10;"#},
14483 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14484 indoc! {r#"struct Row;
14485 struct Row2;
14486
14487 «ˇstruct Row4;
14488 struct» Row5;
14489 «struct Row6;
14490 ˇ»
14491 struct Row8;
14492 struct Row10;"#},
14493 base_text,
14494 &mut cx,
14495 );
14496
14497 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
14498 assert_hunk_revert(
14499 indoc! {r#"struct Row;
14500 ˇstruct Row2;
14501
14502 struct Row4;
14503 struct Row5;
14504 struct Row6;
14505
14506 struct Row8;ˇ
14507 struct Row10;"#},
14508 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14509 indoc! {r#"struct Row;
14510 struct Row1;
14511 ˇstruct Row2;
14512
14513 struct Row4;
14514 struct Row5;
14515 struct Row6;
14516
14517 struct Row8;ˇ
14518 struct Row9;
14519 struct Row10;"#},
14520 base_text,
14521 &mut cx,
14522 );
14523 assert_hunk_revert(
14524 indoc! {r#"struct Row;
14525 struct Row2«ˇ;
14526 struct Row4;
14527 struct» Row5;
14528 «struct Row6;
14529
14530 struct Row8;ˇ»
14531 struct Row10;"#},
14532 vec![
14533 DiffHunkStatusKind::Deleted,
14534 DiffHunkStatusKind::Deleted,
14535 DiffHunkStatusKind::Deleted,
14536 ],
14537 indoc! {r#"struct Row;
14538 struct Row1;
14539 struct Row2«ˇ;
14540
14541 struct Row4;
14542 struct» Row5;
14543 «struct Row6;
14544
14545 struct Row8;ˇ»
14546 struct Row9;
14547 struct Row10;"#},
14548 base_text,
14549 &mut cx,
14550 );
14551}
14552
14553#[gpui::test]
14554async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
14555 init_test(cx, |_| {});
14556
14557 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
14558 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
14559 let base_text_3 =
14560 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
14561
14562 let text_1 = edit_first_char_of_every_line(base_text_1);
14563 let text_2 = edit_first_char_of_every_line(base_text_2);
14564 let text_3 = edit_first_char_of_every_line(base_text_3);
14565
14566 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
14567 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
14568 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
14569
14570 let multibuffer = cx.new(|cx| {
14571 let mut multibuffer = MultiBuffer::new(ReadWrite);
14572 multibuffer.push_excerpts(
14573 buffer_1.clone(),
14574 [
14575 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14576 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14577 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14578 ],
14579 cx,
14580 );
14581 multibuffer.push_excerpts(
14582 buffer_2.clone(),
14583 [
14584 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14585 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14586 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14587 ],
14588 cx,
14589 );
14590 multibuffer.push_excerpts(
14591 buffer_3.clone(),
14592 [
14593 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14594 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14595 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14596 ],
14597 cx,
14598 );
14599 multibuffer
14600 });
14601
14602 let fs = FakeFs::new(cx.executor());
14603 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
14604 let (editor, cx) = cx
14605 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
14606 editor.update_in(cx, |editor, _window, cx| {
14607 for (buffer, diff_base) in [
14608 (buffer_1.clone(), base_text_1),
14609 (buffer_2.clone(), base_text_2),
14610 (buffer_3.clone(), base_text_3),
14611 ] {
14612 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
14613 editor
14614 .buffer
14615 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
14616 }
14617 });
14618 cx.executor().run_until_parked();
14619
14620 editor.update_in(cx, |editor, window, cx| {
14621 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}");
14622 editor.select_all(&SelectAll, window, cx);
14623 editor.git_restore(&Default::default(), window, cx);
14624 });
14625 cx.executor().run_until_parked();
14626
14627 // When all ranges are selected, all buffer hunks are reverted.
14628 editor.update(cx, |editor, cx| {
14629 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");
14630 });
14631 buffer_1.update(cx, |buffer, _| {
14632 assert_eq!(buffer.text(), base_text_1);
14633 });
14634 buffer_2.update(cx, |buffer, _| {
14635 assert_eq!(buffer.text(), base_text_2);
14636 });
14637 buffer_3.update(cx, |buffer, _| {
14638 assert_eq!(buffer.text(), base_text_3);
14639 });
14640
14641 editor.update_in(cx, |editor, window, cx| {
14642 editor.undo(&Default::default(), window, cx);
14643 });
14644
14645 editor.update_in(cx, |editor, window, cx| {
14646 editor.change_selections(None, window, cx, |s| {
14647 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
14648 });
14649 editor.git_restore(&Default::default(), window, cx);
14650 });
14651
14652 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
14653 // but not affect buffer_2 and its related excerpts.
14654 editor.update(cx, |editor, cx| {
14655 assert_eq!(
14656 editor.text(cx),
14657 "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}"
14658 );
14659 });
14660 buffer_1.update(cx, |buffer, _| {
14661 assert_eq!(buffer.text(), base_text_1);
14662 });
14663 buffer_2.update(cx, |buffer, _| {
14664 assert_eq!(
14665 buffer.text(),
14666 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
14667 );
14668 });
14669 buffer_3.update(cx, |buffer, _| {
14670 assert_eq!(
14671 buffer.text(),
14672 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
14673 );
14674 });
14675
14676 fn edit_first_char_of_every_line(text: &str) -> String {
14677 text.split('\n')
14678 .map(|line| format!("X{}", &line[1..]))
14679 .collect::<Vec<_>>()
14680 .join("\n")
14681 }
14682}
14683
14684#[gpui::test]
14685async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
14686 init_test(cx, |_| {});
14687
14688 let cols = 4;
14689 let rows = 10;
14690 let sample_text_1 = sample_text(rows, cols, 'a');
14691 assert_eq!(
14692 sample_text_1,
14693 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
14694 );
14695 let sample_text_2 = sample_text(rows, cols, 'l');
14696 assert_eq!(
14697 sample_text_2,
14698 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
14699 );
14700 let sample_text_3 = sample_text(rows, cols, 'v');
14701 assert_eq!(
14702 sample_text_3,
14703 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
14704 );
14705
14706 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
14707 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
14708 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
14709
14710 let multi_buffer = cx.new(|cx| {
14711 let mut multibuffer = MultiBuffer::new(ReadWrite);
14712 multibuffer.push_excerpts(
14713 buffer_1.clone(),
14714 [
14715 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14716 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14717 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14718 ],
14719 cx,
14720 );
14721 multibuffer.push_excerpts(
14722 buffer_2.clone(),
14723 [
14724 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14725 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14726 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14727 ],
14728 cx,
14729 );
14730 multibuffer.push_excerpts(
14731 buffer_3.clone(),
14732 [
14733 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14734 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14735 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14736 ],
14737 cx,
14738 );
14739 multibuffer
14740 });
14741
14742 let fs = FakeFs::new(cx.executor());
14743 fs.insert_tree(
14744 "/a",
14745 json!({
14746 "main.rs": sample_text_1,
14747 "other.rs": sample_text_2,
14748 "lib.rs": sample_text_3,
14749 }),
14750 )
14751 .await;
14752 let project = Project::test(fs, ["/a".as_ref()], cx).await;
14753 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14754 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14755 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
14756 Editor::new(
14757 EditorMode::full(),
14758 multi_buffer,
14759 Some(project.clone()),
14760 window,
14761 cx,
14762 )
14763 });
14764 let multibuffer_item_id = workspace
14765 .update(cx, |workspace, window, cx| {
14766 assert!(
14767 workspace.active_item(cx).is_none(),
14768 "active item should be None before the first item is added"
14769 );
14770 workspace.add_item_to_active_pane(
14771 Box::new(multi_buffer_editor.clone()),
14772 None,
14773 true,
14774 window,
14775 cx,
14776 );
14777 let active_item = workspace
14778 .active_item(cx)
14779 .expect("should have an active item after adding the multi buffer");
14780 assert!(
14781 !active_item.is_singleton(cx),
14782 "A multi buffer was expected to active after adding"
14783 );
14784 active_item.item_id()
14785 })
14786 .unwrap();
14787 cx.executor().run_until_parked();
14788
14789 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14790 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14791 s.select_ranges(Some(1..2))
14792 });
14793 editor.open_excerpts(&OpenExcerpts, window, cx);
14794 });
14795 cx.executor().run_until_parked();
14796 let first_item_id = workspace
14797 .update(cx, |workspace, window, cx| {
14798 let active_item = workspace
14799 .active_item(cx)
14800 .expect("should have an active item after navigating into the 1st buffer");
14801 let first_item_id = active_item.item_id();
14802 assert_ne!(
14803 first_item_id, multibuffer_item_id,
14804 "Should navigate into the 1st buffer and activate it"
14805 );
14806 assert!(
14807 active_item.is_singleton(cx),
14808 "New active item should be a singleton buffer"
14809 );
14810 assert_eq!(
14811 active_item
14812 .act_as::<Editor>(cx)
14813 .expect("should have navigated into an editor for the 1st buffer")
14814 .read(cx)
14815 .text(cx),
14816 sample_text_1
14817 );
14818
14819 workspace
14820 .go_back(workspace.active_pane().downgrade(), window, cx)
14821 .detach_and_log_err(cx);
14822
14823 first_item_id
14824 })
14825 .unwrap();
14826 cx.executor().run_until_parked();
14827 workspace
14828 .update(cx, |workspace, _, cx| {
14829 let active_item = workspace
14830 .active_item(cx)
14831 .expect("should have an active item after navigating back");
14832 assert_eq!(
14833 active_item.item_id(),
14834 multibuffer_item_id,
14835 "Should navigate back to the multi buffer"
14836 );
14837 assert!(!active_item.is_singleton(cx));
14838 })
14839 .unwrap();
14840
14841 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14842 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14843 s.select_ranges(Some(39..40))
14844 });
14845 editor.open_excerpts(&OpenExcerpts, window, cx);
14846 });
14847 cx.executor().run_until_parked();
14848 let second_item_id = workspace
14849 .update(cx, |workspace, window, cx| {
14850 let active_item = workspace
14851 .active_item(cx)
14852 .expect("should have an active item after navigating into the 2nd buffer");
14853 let second_item_id = active_item.item_id();
14854 assert_ne!(
14855 second_item_id, multibuffer_item_id,
14856 "Should navigate away from the multibuffer"
14857 );
14858 assert_ne!(
14859 second_item_id, first_item_id,
14860 "Should navigate into the 2nd buffer and activate it"
14861 );
14862 assert!(
14863 active_item.is_singleton(cx),
14864 "New active item should be a singleton buffer"
14865 );
14866 assert_eq!(
14867 active_item
14868 .act_as::<Editor>(cx)
14869 .expect("should have navigated into an editor")
14870 .read(cx)
14871 .text(cx),
14872 sample_text_2
14873 );
14874
14875 workspace
14876 .go_back(workspace.active_pane().downgrade(), window, cx)
14877 .detach_and_log_err(cx);
14878
14879 second_item_id
14880 })
14881 .unwrap();
14882 cx.executor().run_until_parked();
14883 workspace
14884 .update(cx, |workspace, _, cx| {
14885 let active_item = workspace
14886 .active_item(cx)
14887 .expect("should have an active item after navigating back from the 2nd buffer");
14888 assert_eq!(
14889 active_item.item_id(),
14890 multibuffer_item_id,
14891 "Should navigate back from the 2nd buffer to the multi buffer"
14892 );
14893 assert!(!active_item.is_singleton(cx));
14894 })
14895 .unwrap();
14896
14897 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14898 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14899 s.select_ranges(Some(70..70))
14900 });
14901 editor.open_excerpts(&OpenExcerpts, window, cx);
14902 });
14903 cx.executor().run_until_parked();
14904 workspace
14905 .update(cx, |workspace, window, cx| {
14906 let active_item = workspace
14907 .active_item(cx)
14908 .expect("should have an active item after navigating into the 3rd buffer");
14909 let third_item_id = active_item.item_id();
14910 assert_ne!(
14911 third_item_id, multibuffer_item_id,
14912 "Should navigate into the 3rd buffer and activate it"
14913 );
14914 assert_ne!(third_item_id, first_item_id);
14915 assert_ne!(third_item_id, second_item_id);
14916 assert!(
14917 active_item.is_singleton(cx),
14918 "New active item should be a singleton buffer"
14919 );
14920 assert_eq!(
14921 active_item
14922 .act_as::<Editor>(cx)
14923 .expect("should have navigated into an editor")
14924 .read(cx)
14925 .text(cx),
14926 sample_text_3
14927 );
14928
14929 workspace
14930 .go_back(workspace.active_pane().downgrade(), window, cx)
14931 .detach_and_log_err(cx);
14932 })
14933 .unwrap();
14934 cx.executor().run_until_parked();
14935 workspace
14936 .update(cx, |workspace, _, cx| {
14937 let active_item = workspace
14938 .active_item(cx)
14939 .expect("should have an active item after navigating back from the 3rd buffer");
14940 assert_eq!(
14941 active_item.item_id(),
14942 multibuffer_item_id,
14943 "Should navigate back from the 3rd buffer to the multi buffer"
14944 );
14945 assert!(!active_item.is_singleton(cx));
14946 })
14947 .unwrap();
14948}
14949
14950#[gpui::test]
14951async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14952 init_test(cx, |_| {});
14953
14954 let mut cx = EditorTestContext::new(cx).await;
14955
14956 let diff_base = r#"
14957 use some::mod;
14958
14959 const A: u32 = 42;
14960
14961 fn main() {
14962 println!("hello");
14963
14964 println!("world");
14965 }
14966 "#
14967 .unindent();
14968
14969 cx.set_state(
14970 &r#"
14971 use some::modified;
14972
14973 ˇ
14974 fn main() {
14975 println!("hello there");
14976
14977 println!("around the");
14978 println!("world");
14979 }
14980 "#
14981 .unindent(),
14982 );
14983
14984 cx.set_head_text(&diff_base);
14985 executor.run_until_parked();
14986
14987 cx.update_editor(|editor, window, cx| {
14988 editor.go_to_next_hunk(&GoToHunk, window, cx);
14989 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14990 });
14991 executor.run_until_parked();
14992 cx.assert_state_with_diff(
14993 r#"
14994 use some::modified;
14995
14996
14997 fn main() {
14998 - println!("hello");
14999 + ˇ println!("hello there");
15000
15001 println!("around the");
15002 println!("world");
15003 }
15004 "#
15005 .unindent(),
15006 );
15007
15008 cx.update_editor(|editor, window, cx| {
15009 for _ in 0..2 {
15010 editor.go_to_next_hunk(&GoToHunk, window, cx);
15011 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15012 }
15013 });
15014 executor.run_until_parked();
15015 cx.assert_state_with_diff(
15016 r#"
15017 - use some::mod;
15018 + ˇuse some::modified;
15019
15020
15021 fn main() {
15022 - println!("hello");
15023 + println!("hello there");
15024
15025 + println!("around the");
15026 println!("world");
15027 }
15028 "#
15029 .unindent(),
15030 );
15031
15032 cx.update_editor(|editor, window, cx| {
15033 editor.go_to_next_hunk(&GoToHunk, window, cx);
15034 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15035 });
15036 executor.run_until_parked();
15037 cx.assert_state_with_diff(
15038 r#"
15039 - use some::mod;
15040 + use some::modified;
15041
15042 - const A: u32 = 42;
15043 ˇ
15044 fn main() {
15045 - println!("hello");
15046 + println!("hello there");
15047
15048 + println!("around the");
15049 println!("world");
15050 }
15051 "#
15052 .unindent(),
15053 );
15054
15055 cx.update_editor(|editor, window, cx| {
15056 editor.cancel(&Cancel, window, cx);
15057 });
15058
15059 cx.assert_state_with_diff(
15060 r#"
15061 use some::modified;
15062
15063 ˇ
15064 fn main() {
15065 println!("hello there");
15066
15067 println!("around the");
15068 println!("world");
15069 }
15070 "#
15071 .unindent(),
15072 );
15073}
15074
15075#[gpui::test]
15076async fn test_diff_base_change_with_expanded_diff_hunks(
15077 executor: BackgroundExecutor,
15078 cx: &mut TestAppContext,
15079) {
15080 init_test(cx, |_| {});
15081
15082 let mut cx = EditorTestContext::new(cx).await;
15083
15084 let diff_base = r#"
15085 use some::mod1;
15086 use some::mod2;
15087
15088 const A: u32 = 42;
15089 const B: u32 = 42;
15090 const C: u32 = 42;
15091
15092 fn main() {
15093 println!("hello");
15094
15095 println!("world");
15096 }
15097 "#
15098 .unindent();
15099
15100 cx.set_state(
15101 &r#"
15102 use some::mod2;
15103
15104 const A: u32 = 42;
15105 const C: u32 = 42;
15106
15107 fn main(ˇ) {
15108 //println!("hello");
15109
15110 println!("world");
15111 //
15112 //
15113 }
15114 "#
15115 .unindent(),
15116 );
15117
15118 cx.set_head_text(&diff_base);
15119 executor.run_until_parked();
15120
15121 cx.update_editor(|editor, window, cx| {
15122 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15123 });
15124 executor.run_until_parked();
15125 cx.assert_state_with_diff(
15126 r#"
15127 - use some::mod1;
15128 use some::mod2;
15129
15130 const A: u32 = 42;
15131 - const B: u32 = 42;
15132 const C: u32 = 42;
15133
15134 fn main(ˇ) {
15135 - println!("hello");
15136 + //println!("hello");
15137
15138 println!("world");
15139 + //
15140 + //
15141 }
15142 "#
15143 .unindent(),
15144 );
15145
15146 cx.set_head_text("new diff base!");
15147 executor.run_until_parked();
15148 cx.assert_state_with_diff(
15149 r#"
15150 - new diff base!
15151 + use some::mod2;
15152 +
15153 + const A: u32 = 42;
15154 + const C: u32 = 42;
15155 +
15156 + fn main(ˇ) {
15157 + //println!("hello");
15158 +
15159 + println!("world");
15160 + //
15161 + //
15162 + }
15163 "#
15164 .unindent(),
15165 );
15166}
15167
15168#[gpui::test]
15169async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
15170 init_test(cx, |_| {});
15171
15172 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
15173 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
15174 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
15175 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
15176 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
15177 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
15178
15179 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
15180 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
15181 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
15182
15183 let multi_buffer = cx.new(|cx| {
15184 let mut multibuffer = MultiBuffer::new(ReadWrite);
15185 multibuffer.push_excerpts(
15186 buffer_1.clone(),
15187 [
15188 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15189 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15190 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15191 ],
15192 cx,
15193 );
15194 multibuffer.push_excerpts(
15195 buffer_2.clone(),
15196 [
15197 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15198 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15199 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15200 ],
15201 cx,
15202 );
15203 multibuffer.push_excerpts(
15204 buffer_3.clone(),
15205 [
15206 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15207 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15208 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15209 ],
15210 cx,
15211 );
15212 multibuffer
15213 });
15214
15215 let editor =
15216 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
15217 editor
15218 .update(cx, |editor, _window, cx| {
15219 for (buffer, diff_base) in [
15220 (buffer_1.clone(), file_1_old),
15221 (buffer_2.clone(), file_2_old),
15222 (buffer_3.clone(), file_3_old),
15223 ] {
15224 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
15225 editor
15226 .buffer
15227 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
15228 }
15229 })
15230 .unwrap();
15231
15232 let mut cx = EditorTestContext::for_editor(editor, cx).await;
15233 cx.run_until_parked();
15234
15235 cx.assert_editor_state(
15236 &"
15237 ˇaaa
15238 ccc
15239 ddd
15240
15241 ggg
15242 hhh
15243
15244
15245 lll
15246 mmm
15247 NNN
15248
15249 qqq
15250 rrr
15251
15252 uuu
15253 111
15254 222
15255 333
15256
15257 666
15258 777
15259
15260 000
15261 !!!"
15262 .unindent(),
15263 );
15264
15265 cx.update_editor(|editor, window, cx| {
15266 editor.select_all(&SelectAll, window, cx);
15267 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15268 });
15269 cx.executor().run_until_parked();
15270
15271 cx.assert_state_with_diff(
15272 "
15273 «aaa
15274 - bbb
15275 ccc
15276 ddd
15277
15278 ggg
15279 hhh
15280
15281
15282 lll
15283 mmm
15284 - nnn
15285 + NNN
15286
15287 qqq
15288 rrr
15289
15290 uuu
15291 111
15292 222
15293 333
15294
15295 + 666
15296 777
15297
15298 000
15299 !!!ˇ»"
15300 .unindent(),
15301 );
15302}
15303
15304#[gpui::test]
15305async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
15306 init_test(cx, |_| {});
15307
15308 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
15309 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
15310
15311 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
15312 let multi_buffer = cx.new(|cx| {
15313 let mut multibuffer = MultiBuffer::new(ReadWrite);
15314 multibuffer.push_excerpts(
15315 buffer.clone(),
15316 [
15317 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
15318 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
15319 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
15320 ],
15321 cx,
15322 );
15323 multibuffer
15324 });
15325
15326 let editor =
15327 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
15328 editor
15329 .update(cx, |editor, _window, cx| {
15330 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
15331 editor
15332 .buffer
15333 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
15334 })
15335 .unwrap();
15336
15337 let mut cx = EditorTestContext::for_editor(editor, cx).await;
15338 cx.run_until_parked();
15339
15340 cx.update_editor(|editor, window, cx| {
15341 editor.expand_all_diff_hunks(&Default::default(), window, cx)
15342 });
15343 cx.executor().run_until_parked();
15344
15345 // When the start of a hunk coincides with the start of its excerpt,
15346 // the hunk is expanded. When the start of a a hunk is earlier than
15347 // the start of its excerpt, the hunk is not expanded.
15348 cx.assert_state_with_diff(
15349 "
15350 ˇaaa
15351 - bbb
15352 + BBB
15353
15354 - ddd
15355 - eee
15356 + DDD
15357 + EEE
15358 fff
15359
15360 iii
15361 "
15362 .unindent(),
15363 );
15364}
15365
15366#[gpui::test]
15367async fn test_edits_around_expanded_insertion_hunks(
15368 executor: BackgroundExecutor,
15369 cx: &mut TestAppContext,
15370) {
15371 init_test(cx, |_| {});
15372
15373 let mut cx = EditorTestContext::new(cx).await;
15374
15375 let diff_base = r#"
15376 use some::mod1;
15377 use some::mod2;
15378
15379 const A: u32 = 42;
15380
15381 fn main() {
15382 println!("hello");
15383
15384 println!("world");
15385 }
15386 "#
15387 .unindent();
15388 executor.run_until_parked();
15389 cx.set_state(
15390 &r#"
15391 use some::mod1;
15392 use some::mod2;
15393
15394 const A: u32 = 42;
15395 const B: u32 = 42;
15396 const C: u32 = 42;
15397 ˇ
15398
15399 fn main() {
15400 println!("hello");
15401
15402 println!("world");
15403 }
15404 "#
15405 .unindent(),
15406 );
15407
15408 cx.set_head_text(&diff_base);
15409 executor.run_until_parked();
15410
15411 cx.update_editor(|editor, window, cx| {
15412 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15413 });
15414 executor.run_until_parked();
15415
15416 cx.assert_state_with_diff(
15417 r#"
15418 use some::mod1;
15419 use some::mod2;
15420
15421 const A: u32 = 42;
15422 + const B: u32 = 42;
15423 + const C: u32 = 42;
15424 + ˇ
15425
15426 fn main() {
15427 println!("hello");
15428
15429 println!("world");
15430 }
15431 "#
15432 .unindent(),
15433 );
15434
15435 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
15436 executor.run_until_parked();
15437
15438 cx.assert_state_with_diff(
15439 r#"
15440 use some::mod1;
15441 use some::mod2;
15442
15443 const A: u32 = 42;
15444 + const B: u32 = 42;
15445 + const C: u32 = 42;
15446 + const D: u32 = 42;
15447 + ˇ
15448
15449 fn main() {
15450 println!("hello");
15451
15452 println!("world");
15453 }
15454 "#
15455 .unindent(),
15456 );
15457
15458 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
15459 executor.run_until_parked();
15460
15461 cx.assert_state_with_diff(
15462 r#"
15463 use some::mod1;
15464 use some::mod2;
15465
15466 const A: u32 = 42;
15467 + const B: u32 = 42;
15468 + const C: u32 = 42;
15469 + const D: u32 = 42;
15470 + const E: u32 = 42;
15471 + ˇ
15472
15473 fn main() {
15474 println!("hello");
15475
15476 println!("world");
15477 }
15478 "#
15479 .unindent(),
15480 );
15481
15482 cx.update_editor(|editor, window, cx| {
15483 editor.delete_line(&DeleteLine, window, cx);
15484 });
15485 executor.run_until_parked();
15486
15487 cx.assert_state_with_diff(
15488 r#"
15489 use some::mod1;
15490 use some::mod2;
15491
15492 const A: u32 = 42;
15493 + const B: u32 = 42;
15494 + const C: u32 = 42;
15495 + const D: u32 = 42;
15496 + const E: u32 = 42;
15497 ˇ
15498 fn main() {
15499 println!("hello");
15500
15501 println!("world");
15502 }
15503 "#
15504 .unindent(),
15505 );
15506
15507 cx.update_editor(|editor, window, cx| {
15508 editor.move_up(&MoveUp, window, cx);
15509 editor.delete_line(&DeleteLine, window, cx);
15510 editor.move_up(&MoveUp, window, cx);
15511 editor.delete_line(&DeleteLine, window, cx);
15512 editor.move_up(&MoveUp, window, cx);
15513 editor.delete_line(&DeleteLine, window, cx);
15514 });
15515 executor.run_until_parked();
15516 cx.assert_state_with_diff(
15517 r#"
15518 use some::mod1;
15519 use some::mod2;
15520
15521 const A: u32 = 42;
15522 + const B: u32 = 42;
15523 ˇ
15524 fn main() {
15525 println!("hello");
15526
15527 println!("world");
15528 }
15529 "#
15530 .unindent(),
15531 );
15532
15533 cx.update_editor(|editor, window, cx| {
15534 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
15535 editor.delete_line(&DeleteLine, window, cx);
15536 });
15537 executor.run_until_parked();
15538 cx.assert_state_with_diff(
15539 r#"
15540 ˇ
15541 fn main() {
15542 println!("hello");
15543
15544 println!("world");
15545 }
15546 "#
15547 .unindent(),
15548 );
15549}
15550
15551#[gpui::test]
15552async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
15553 init_test(cx, |_| {});
15554
15555 let mut cx = EditorTestContext::new(cx).await;
15556 cx.set_head_text(indoc! { "
15557 one
15558 two
15559 three
15560 four
15561 five
15562 "
15563 });
15564 cx.set_state(indoc! { "
15565 one
15566 ˇthree
15567 five
15568 "});
15569 cx.run_until_parked();
15570 cx.update_editor(|editor, window, cx| {
15571 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15572 });
15573 cx.assert_state_with_diff(
15574 indoc! { "
15575 one
15576 - two
15577 ˇthree
15578 - four
15579 five
15580 "}
15581 .to_string(),
15582 );
15583 cx.update_editor(|editor, window, cx| {
15584 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15585 });
15586
15587 cx.assert_state_with_diff(
15588 indoc! { "
15589 one
15590 ˇthree
15591 five
15592 "}
15593 .to_string(),
15594 );
15595
15596 cx.set_state(indoc! { "
15597 one
15598 ˇTWO
15599 three
15600 four
15601 five
15602 "});
15603 cx.run_until_parked();
15604 cx.update_editor(|editor, window, cx| {
15605 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15606 });
15607
15608 cx.assert_state_with_diff(
15609 indoc! { "
15610 one
15611 - two
15612 + ˇTWO
15613 three
15614 four
15615 five
15616 "}
15617 .to_string(),
15618 );
15619 cx.update_editor(|editor, window, cx| {
15620 editor.move_up(&Default::default(), window, cx);
15621 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15622 });
15623 cx.assert_state_with_diff(
15624 indoc! { "
15625 one
15626 ˇTWO
15627 three
15628 four
15629 five
15630 "}
15631 .to_string(),
15632 );
15633}
15634
15635#[gpui::test]
15636async fn test_edits_around_expanded_deletion_hunks(
15637 executor: BackgroundExecutor,
15638 cx: &mut TestAppContext,
15639) {
15640 init_test(cx, |_| {});
15641
15642 let mut cx = EditorTestContext::new(cx).await;
15643
15644 let diff_base = r#"
15645 use some::mod1;
15646 use some::mod2;
15647
15648 const A: u32 = 42;
15649 const B: u32 = 42;
15650 const C: u32 = 42;
15651
15652
15653 fn main() {
15654 println!("hello");
15655
15656 println!("world");
15657 }
15658 "#
15659 .unindent();
15660 executor.run_until_parked();
15661 cx.set_state(
15662 &r#"
15663 use some::mod1;
15664 use some::mod2;
15665
15666 ˇconst B: u32 = 42;
15667 const C: u32 = 42;
15668
15669
15670 fn main() {
15671 println!("hello");
15672
15673 println!("world");
15674 }
15675 "#
15676 .unindent(),
15677 );
15678
15679 cx.set_head_text(&diff_base);
15680 executor.run_until_parked();
15681
15682 cx.update_editor(|editor, window, cx| {
15683 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15684 });
15685 executor.run_until_parked();
15686
15687 cx.assert_state_with_diff(
15688 r#"
15689 use some::mod1;
15690 use some::mod2;
15691
15692 - const A: u32 = 42;
15693 ˇconst B: u32 = 42;
15694 const C: u32 = 42;
15695
15696
15697 fn main() {
15698 println!("hello");
15699
15700 println!("world");
15701 }
15702 "#
15703 .unindent(),
15704 );
15705
15706 cx.update_editor(|editor, window, cx| {
15707 editor.delete_line(&DeleteLine, window, cx);
15708 });
15709 executor.run_until_parked();
15710 cx.assert_state_with_diff(
15711 r#"
15712 use some::mod1;
15713 use some::mod2;
15714
15715 - const A: u32 = 42;
15716 - const B: u32 = 42;
15717 ˇconst C: u32 = 42;
15718
15719
15720 fn main() {
15721 println!("hello");
15722
15723 println!("world");
15724 }
15725 "#
15726 .unindent(),
15727 );
15728
15729 cx.update_editor(|editor, window, cx| {
15730 editor.delete_line(&DeleteLine, window, cx);
15731 });
15732 executor.run_until_parked();
15733 cx.assert_state_with_diff(
15734 r#"
15735 use some::mod1;
15736 use some::mod2;
15737
15738 - const A: u32 = 42;
15739 - const B: u32 = 42;
15740 - const C: u32 = 42;
15741 ˇ
15742
15743 fn main() {
15744 println!("hello");
15745
15746 println!("world");
15747 }
15748 "#
15749 .unindent(),
15750 );
15751
15752 cx.update_editor(|editor, window, cx| {
15753 editor.handle_input("replacement", window, cx);
15754 });
15755 executor.run_until_parked();
15756 cx.assert_state_with_diff(
15757 r#"
15758 use some::mod1;
15759 use some::mod2;
15760
15761 - const A: u32 = 42;
15762 - const B: u32 = 42;
15763 - const C: u32 = 42;
15764 -
15765 + replacementˇ
15766
15767 fn main() {
15768 println!("hello");
15769
15770 println!("world");
15771 }
15772 "#
15773 .unindent(),
15774 );
15775}
15776
15777#[gpui::test]
15778async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15779 init_test(cx, |_| {});
15780
15781 let mut cx = EditorTestContext::new(cx).await;
15782
15783 let base_text = r#"
15784 one
15785 two
15786 three
15787 four
15788 five
15789 "#
15790 .unindent();
15791 executor.run_until_parked();
15792 cx.set_state(
15793 &r#"
15794 one
15795 two
15796 fˇour
15797 five
15798 "#
15799 .unindent(),
15800 );
15801
15802 cx.set_head_text(&base_text);
15803 executor.run_until_parked();
15804
15805 cx.update_editor(|editor, window, cx| {
15806 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15807 });
15808 executor.run_until_parked();
15809
15810 cx.assert_state_with_diff(
15811 r#"
15812 one
15813 two
15814 - three
15815 fˇour
15816 five
15817 "#
15818 .unindent(),
15819 );
15820
15821 cx.update_editor(|editor, window, cx| {
15822 editor.backspace(&Backspace, window, cx);
15823 editor.backspace(&Backspace, window, cx);
15824 });
15825 executor.run_until_parked();
15826 cx.assert_state_with_diff(
15827 r#"
15828 one
15829 two
15830 - threeˇ
15831 - four
15832 + our
15833 five
15834 "#
15835 .unindent(),
15836 );
15837}
15838
15839#[gpui::test]
15840async fn test_edit_after_expanded_modification_hunk(
15841 executor: BackgroundExecutor,
15842 cx: &mut TestAppContext,
15843) {
15844 init_test(cx, |_| {});
15845
15846 let mut cx = EditorTestContext::new(cx).await;
15847
15848 let diff_base = r#"
15849 use some::mod1;
15850 use some::mod2;
15851
15852 const A: u32 = 42;
15853 const B: u32 = 42;
15854 const C: u32 = 42;
15855 const D: u32 = 42;
15856
15857
15858 fn main() {
15859 println!("hello");
15860
15861 println!("world");
15862 }"#
15863 .unindent();
15864
15865 cx.set_state(
15866 &r#"
15867 use some::mod1;
15868 use some::mod2;
15869
15870 const A: u32 = 42;
15871 const B: u32 = 42;
15872 const C: u32 = 43ˇ
15873 const D: u32 = 42;
15874
15875
15876 fn main() {
15877 println!("hello");
15878
15879 println!("world");
15880 }"#
15881 .unindent(),
15882 );
15883
15884 cx.set_head_text(&diff_base);
15885 executor.run_until_parked();
15886 cx.update_editor(|editor, window, cx| {
15887 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15888 });
15889 executor.run_until_parked();
15890
15891 cx.assert_state_with_diff(
15892 r#"
15893 use some::mod1;
15894 use some::mod2;
15895
15896 const A: u32 = 42;
15897 const B: u32 = 42;
15898 - const C: u32 = 42;
15899 + const C: u32 = 43ˇ
15900 const D: u32 = 42;
15901
15902
15903 fn main() {
15904 println!("hello");
15905
15906 println!("world");
15907 }"#
15908 .unindent(),
15909 );
15910
15911 cx.update_editor(|editor, window, cx| {
15912 editor.handle_input("\nnew_line\n", window, cx);
15913 });
15914 executor.run_until_parked();
15915
15916 cx.assert_state_with_diff(
15917 r#"
15918 use some::mod1;
15919 use some::mod2;
15920
15921 const A: u32 = 42;
15922 const B: u32 = 42;
15923 - const C: u32 = 42;
15924 + const C: u32 = 43
15925 + new_line
15926 + ˇ
15927 const D: u32 = 42;
15928
15929
15930 fn main() {
15931 println!("hello");
15932
15933 println!("world");
15934 }"#
15935 .unindent(),
15936 );
15937}
15938
15939#[gpui::test]
15940async fn test_stage_and_unstage_added_file_hunk(
15941 executor: BackgroundExecutor,
15942 cx: &mut TestAppContext,
15943) {
15944 init_test(cx, |_| {});
15945
15946 let mut cx = EditorTestContext::new(cx).await;
15947 cx.update_editor(|editor, _, cx| {
15948 editor.set_expand_all_diff_hunks(cx);
15949 });
15950
15951 let working_copy = r#"
15952 ˇfn main() {
15953 println!("hello, world!");
15954 }
15955 "#
15956 .unindent();
15957
15958 cx.set_state(&working_copy);
15959 executor.run_until_parked();
15960
15961 cx.assert_state_with_diff(
15962 r#"
15963 + ˇfn main() {
15964 + println!("hello, world!");
15965 + }
15966 "#
15967 .unindent(),
15968 );
15969 cx.assert_index_text(None);
15970
15971 cx.update_editor(|editor, window, cx| {
15972 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
15973 });
15974 executor.run_until_parked();
15975 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
15976 cx.assert_state_with_diff(
15977 r#"
15978 + ˇfn main() {
15979 + println!("hello, world!");
15980 + }
15981 "#
15982 .unindent(),
15983 );
15984
15985 cx.update_editor(|editor, window, cx| {
15986 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
15987 });
15988 executor.run_until_parked();
15989 cx.assert_index_text(None);
15990}
15991
15992async fn setup_indent_guides_editor(
15993 text: &str,
15994 cx: &mut TestAppContext,
15995) -> (BufferId, EditorTestContext) {
15996 init_test(cx, |_| {});
15997
15998 let mut cx = EditorTestContext::new(cx).await;
15999
16000 let buffer_id = cx.update_editor(|editor, window, cx| {
16001 editor.set_text(text, window, cx);
16002 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
16003
16004 buffer_ids[0]
16005 });
16006
16007 (buffer_id, cx)
16008}
16009
16010fn assert_indent_guides(
16011 range: Range<u32>,
16012 expected: Vec<IndentGuide>,
16013 active_indices: Option<Vec<usize>>,
16014 cx: &mut EditorTestContext,
16015) {
16016 let indent_guides = cx.update_editor(|editor, window, cx| {
16017 let snapshot = editor.snapshot(window, cx).display_snapshot;
16018 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
16019 editor,
16020 MultiBufferRow(range.start)..MultiBufferRow(range.end),
16021 true,
16022 &snapshot,
16023 cx,
16024 );
16025
16026 indent_guides.sort_by(|a, b| {
16027 a.depth.cmp(&b.depth).then(
16028 a.start_row
16029 .cmp(&b.start_row)
16030 .then(a.end_row.cmp(&b.end_row)),
16031 )
16032 });
16033 indent_guides
16034 });
16035
16036 if let Some(expected) = active_indices {
16037 let active_indices = cx.update_editor(|editor, window, cx| {
16038 let snapshot = editor.snapshot(window, cx).display_snapshot;
16039 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
16040 });
16041
16042 assert_eq!(
16043 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
16044 expected,
16045 "Active indent guide indices do not match"
16046 );
16047 }
16048
16049 assert_eq!(indent_guides, expected, "Indent guides do not match");
16050}
16051
16052fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
16053 IndentGuide {
16054 buffer_id,
16055 start_row: MultiBufferRow(start_row),
16056 end_row: MultiBufferRow(end_row),
16057 depth,
16058 tab_size: 4,
16059 settings: IndentGuideSettings {
16060 enabled: true,
16061 line_width: 1,
16062 active_line_width: 1,
16063 ..Default::default()
16064 },
16065 }
16066}
16067
16068#[gpui::test]
16069async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
16070 let (buffer_id, mut cx) = setup_indent_guides_editor(
16071 &"
16072 fn main() {
16073 let a = 1;
16074 }"
16075 .unindent(),
16076 cx,
16077 )
16078 .await;
16079
16080 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
16081}
16082
16083#[gpui::test]
16084async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
16085 let (buffer_id, mut cx) = setup_indent_guides_editor(
16086 &"
16087 fn main() {
16088 let a = 1;
16089 let b = 2;
16090 }"
16091 .unindent(),
16092 cx,
16093 )
16094 .await;
16095
16096 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
16097}
16098
16099#[gpui::test]
16100async fn test_indent_guide_nested(cx: &mut TestAppContext) {
16101 let (buffer_id, mut cx) = setup_indent_guides_editor(
16102 &"
16103 fn main() {
16104 let a = 1;
16105 if a == 3 {
16106 let b = 2;
16107 } else {
16108 let c = 3;
16109 }
16110 }"
16111 .unindent(),
16112 cx,
16113 )
16114 .await;
16115
16116 assert_indent_guides(
16117 0..8,
16118 vec![
16119 indent_guide(buffer_id, 1, 6, 0),
16120 indent_guide(buffer_id, 3, 3, 1),
16121 indent_guide(buffer_id, 5, 5, 1),
16122 ],
16123 None,
16124 &mut cx,
16125 );
16126}
16127
16128#[gpui::test]
16129async fn test_indent_guide_tab(cx: &mut TestAppContext) {
16130 let (buffer_id, mut cx) = setup_indent_guides_editor(
16131 &"
16132 fn main() {
16133 let a = 1;
16134 let b = 2;
16135 let c = 3;
16136 }"
16137 .unindent(),
16138 cx,
16139 )
16140 .await;
16141
16142 assert_indent_guides(
16143 0..5,
16144 vec![
16145 indent_guide(buffer_id, 1, 3, 0),
16146 indent_guide(buffer_id, 2, 2, 1),
16147 ],
16148 None,
16149 &mut cx,
16150 );
16151}
16152
16153#[gpui::test]
16154async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
16155 let (buffer_id, mut cx) = setup_indent_guides_editor(
16156 &"
16157 fn main() {
16158 let a = 1;
16159
16160 let c = 3;
16161 }"
16162 .unindent(),
16163 cx,
16164 )
16165 .await;
16166
16167 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
16168}
16169
16170#[gpui::test]
16171async fn test_indent_guide_complex(cx: &mut TestAppContext) {
16172 let (buffer_id, mut cx) = setup_indent_guides_editor(
16173 &"
16174 fn main() {
16175 let a = 1;
16176
16177 let c = 3;
16178
16179 if a == 3 {
16180 let b = 2;
16181 } else {
16182 let c = 3;
16183 }
16184 }"
16185 .unindent(),
16186 cx,
16187 )
16188 .await;
16189
16190 assert_indent_guides(
16191 0..11,
16192 vec![
16193 indent_guide(buffer_id, 1, 9, 0),
16194 indent_guide(buffer_id, 6, 6, 1),
16195 indent_guide(buffer_id, 8, 8, 1),
16196 ],
16197 None,
16198 &mut cx,
16199 );
16200}
16201
16202#[gpui::test]
16203async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
16204 let (buffer_id, mut cx) = setup_indent_guides_editor(
16205 &"
16206 fn main() {
16207 let a = 1;
16208
16209 let c = 3;
16210
16211 if a == 3 {
16212 let b = 2;
16213 } else {
16214 let c = 3;
16215 }
16216 }"
16217 .unindent(),
16218 cx,
16219 )
16220 .await;
16221
16222 assert_indent_guides(
16223 1..11,
16224 vec![
16225 indent_guide(buffer_id, 1, 9, 0),
16226 indent_guide(buffer_id, 6, 6, 1),
16227 indent_guide(buffer_id, 8, 8, 1),
16228 ],
16229 None,
16230 &mut cx,
16231 );
16232}
16233
16234#[gpui::test]
16235async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
16236 let (buffer_id, mut cx) = setup_indent_guides_editor(
16237 &"
16238 fn main() {
16239 let a = 1;
16240
16241 let c = 3;
16242
16243 if a == 3 {
16244 let b = 2;
16245 } else {
16246 let c = 3;
16247 }
16248 }"
16249 .unindent(),
16250 cx,
16251 )
16252 .await;
16253
16254 assert_indent_guides(
16255 1..10,
16256 vec![
16257 indent_guide(buffer_id, 1, 9, 0),
16258 indent_guide(buffer_id, 6, 6, 1),
16259 indent_guide(buffer_id, 8, 8, 1),
16260 ],
16261 None,
16262 &mut cx,
16263 );
16264}
16265
16266#[gpui::test]
16267async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
16268 let (buffer_id, mut cx) = setup_indent_guides_editor(
16269 &"
16270 block1
16271 block2
16272 block3
16273 block4
16274 block2
16275 block1
16276 block1"
16277 .unindent(),
16278 cx,
16279 )
16280 .await;
16281
16282 assert_indent_guides(
16283 1..10,
16284 vec![
16285 indent_guide(buffer_id, 1, 4, 0),
16286 indent_guide(buffer_id, 2, 3, 1),
16287 indent_guide(buffer_id, 3, 3, 2),
16288 ],
16289 None,
16290 &mut cx,
16291 );
16292}
16293
16294#[gpui::test]
16295async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
16296 let (buffer_id, mut cx) = setup_indent_guides_editor(
16297 &"
16298 block1
16299 block2
16300 block3
16301
16302 block1
16303 block1"
16304 .unindent(),
16305 cx,
16306 )
16307 .await;
16308
16309 assert_indent_guides(
16310 0..6,
16311 vec![
16312 indent_guide(buffer_id, 1, 2, 0),
16313 indent_guide(buffer_id, 2, 2, 1),
16314 ],
16315 None,
16316 &mut cx,
16317 );
16318}
16319
16320#[gpui::test]
16321async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
16322 let (buffer_id, mut cx) = setup_indent_guides_editor(
16323 &"
16324 block1
16325
16326
16327
16328 block2
16329 "
16330 .unindent(),
16331 cx,
16332 )
16333 .await;
16334
16335 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
16336}
16337
16338#[gpui::test]
16339async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
16340 let (buffer_id, mut cx) = setup_indent_guides_editor(
16341 &"
16342 def a:
16343 \tb = 3
16344 \tif True:
16345 \t\tc = 4
16346 \t\td = 5
16347 \tprint(b)
16348 "
16349 .unindent(),
16350 cx,
16351 )
16352 .await;
16353
16354 assert_indent_guides(
16355 0..6,
16356 vec![
16357 indent_guide(buffer_id, 1, 6, 0),
16358 indent_guide(buffer_id, 3, 4, 1),
16359 ],
16360 None,
16361 &mut cx,
16362 );
16363}
16364
16365#[gpui::test]
16366async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
16367 let (buffer_id, mut cx) = setup_indent_guides_editor(
16368 &"
16369 fn main() {
16370 let a = 1;
16371 }"
16372 .unindent(),
16373 cx,
16374 )
16375 .await;
16376
16377 cx.update_editor(|editor, window, cx| {
16378 editor.change_selections(None, window, cx, |s| {
16379 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16380 });
16381 });
16382
16383 assert_indent_guides(
16384 0..3,
16385 vec![indent_guide(buffer_id, 1, 1, 0)],
16386 Some(vec![0]),
16387 &mut cx,
16388 );
16389}
16390
16391#[gpui::test]
16392async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
16393 let (buffer_id, mut cx) = setup_indent_guides_editor(
16394 &"
16395 fn main() {
16396 if 1 == 2 {
16397 let a = 1;
16398 }
16399 }"
16400 .unindent(),
16401 cx,
16402 )
16403 .await;
16404
16405 cx.update_editor(|editor, window, cx| {
16406 editor.change_selections(None, window, cx, |s| {
16407 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16408 });
16409 });
16410
16411 assert_indent_guides(
16412 0..4,
16413 vec![
16414 indent_guide(buffer_id, 1, 3, 0),
16415 indent_guide(buffer_id, 2, 2, 1),
16416 ],
16417 Some(vec![1]),
16418 &mut cx,
16419 );
16420
16421 cx.update_editor(|editor, window, cx| {
16422 editor.change_selections(None, window, cx, |s| {
16423 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
16424 });
16425 });
16426
16427 assert_indent_guides(
16428 0..4,
16429 vec![
16430 indent_guide(buffer_id, 1, 3, 0),
16431 indent_guide(buffer_id, 2, 2, 1),
16432 ],
16433 Some(vec![1]),
16434 &mut cx,
16435 );
16436
16437 cx.update_editor(|editor, window, cx| {
16438 editor.change_selections(None, window, cx, |s| {
16439 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
16440 });
16441 });
16442
16443 assert_indent_guides(
16444 0..4,
16445 vec![
16446 indent_guide(buffer_id, 1, 3, 0),
16447 indent_guide(buffer_id, 2, 2, 1),
16448 ],
16449 Some(vec![0]),
16450 &mut cx,
16451 );
16452}
16453
16454#[gpui::test]
16455async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
16456 let (buffer_id, mut cx) = setup_indent_guides_editor(
16457 &"
16458 fn main() {
16459 let a = 1;
16460
16461 let b = 2;
16462 }"
16463 .unindent(),
16464 cx,
16465 )
16466 .await;
16467
16468 cx.update_editor(|editor, window, cx| {
16469 editor.change_selections(None, window, cx, |s| {
16470 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
16471 });
16472 });
16473
16474 assert_indent_guides(
16475 0..5,
16476 vec![indent_guide(buffer_id, 1, 3, 0)],
16477 Some(vec![0]),
16478 &mut cx,
16479 );
16480}
16481
16482#[gpui::test]
16483async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
16484 let (buffer_id, mut cx) = setup_indent_guides_editor(
16485 &"
16486 def m:
16487 a = 1
16488 pass"
16489 .unindent(),
16490 cx,
16491 )
16492 .await;
16493
16494 cx.update_editor(|editor, window, cx| {
16495 editor.change_selections(None, window, cx, |s| {
16496 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16497 });
16498 });
16499
16500 assert_indent_guides(
16501 0..3,
16502 vec![indent_guide(buffer_id, 1, 2, 0)],
16503 Some(vec![0]),
16504 &mut cx,
16505 );
16506}
16507
16508#[gpui::test]
16509async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
16510 init_test(cx, |_| {});
16511 let mut cx = EditorTestContext::new(cx).await;
16512 let text = indoc! {
16513 "
16514 impl A {
16515 fn b() {
16516 0;
16517 3;
16518 5;
16519 6;
16520 7;
16521 }
16522 }
16523 "
16524 };
16525 let base_text = indoc! {
16526 "
16527 impl A {
16528 fn b() {
16529 0;
16530 1;
16531 2;
16532 3;
16533 4;
16534 }
16535 fn c() {
16536 5;
16537 6;
16538 7;
16539 }
16540 }
16541 "
16542 };
16543
16544 cx.update_editor(|editor, window, cx| {
16545 editor.set_text(text, window, cx);
16546
16547 editor.buffer().update(cx, |multibuffer, cx| {
16548 let buffer = multibuffer.as_singleton().unwrap();
16549 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
16550
16551 multibuffer.set_all_diff_hunks_expanded(cx);
16552 multibuffer.add_diff(diff, cx);
16553
16554 buffer.read(cx).remote_id()
16555 })
16556 });
16557 cx.run_until_parked();
16558
16559 cx.assert_state_with_diff(
16560 indoc! { "
16561 impl A {
16562 fn b() {
16563 0;
16564 - 1;
16565 - 2;
16566 3;
16567 - 4;
16568 - }
16569 - fn c() {
16570 5;
16571 6;
16572 7;
16573 }
16574 }
16575 ˇ"
16576 }
16577 .to_string(),
16578 );
16579
16580 let mut actual_guides = cx.update_editor(|editor, window, cx| {
16581 editor
16582 .snapshot(window, cx)
16583 .buffer_snapshot
16584 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
16585 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
16586 .collect::<Vec<_>>()
16587 });
16588 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
16589 assert_eq!(
16590 actual_guides,
16591 vec![
16592 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
16593 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
16594 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
16595 ]
16596 );
16597}
16598
16599#[gpui::test]
16600async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16601 init_test(cx, |_| {});
16602 let mut cx = EditorTestContext::new(cx).await;
16603
16604 let diff_base = r#"
16605 a
16606 b
16607 c
16608 "#
16609 .unindent();
16610
16611 cx.set_state(
16612 &r#"
16613 ˇA
16614 b
16615 C
16616 "#
16617 .unindent(),
16618 );
16619 cx.set_head_text(&diff_base);
16620 cx.update_editor(|editor, window, cx| {
16621 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16622 });
16623 executor.run_until_parked();
16624
16625 let both_hunks_expanded = r#"
16626 - a
16627 + ˇA
16628 b
16629 - c
16630 + C
16631 "#
16632 .unindent();
16633
16634 cx.assert_state_with_diff(both_hunks_expanded.clone());
16635
16636 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16637 let snapshot = editor.snapshot(window, cx);
16638 let hunks = editor
16639 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16640 .collect::<Vec<_>>();
16641 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16642 let buffer_id = hunks[0].buffer_id;
16643 hunks
16644 .into_iter()
16645 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16646 .collect::<Vec<_>>()
16647 });
16648 assert_eq!(hunk_ranges.len(), 2);
16649
16650 cx.update_editor(|editor, _, cx| {
16651 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16652 });
16653 executor.run_until_parked();
16654
16655 let second_hunk_expanded = r#"
16656 ˇA
16657 b
16658 - c
16659 + C
16660 "#
16661 .unindent();
16662
16663 cx.assert_state_with_diff(second_hunk_expanded);
16664
16665 cx.update_editor(|editor, _, cx| {
16666 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16667 });
16668 executor.run_until_parked();
16669
16670 cx.assert_state_with_diff(both_hunks_expanded.clone());
16671
16672 cx.update_editor(|editor, _, cx| {
16673 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
16674 });
16675 executor.run_until_parked();
16676
16677 let first_hunk_expanded = r#"
16678 - a
16679 + ˇA
16680 b
16681 C
16682 "#
16683 .unindent();
16684
16685 cx.assert_state_with_diff(first_hunk_expanded);
16686
16687 cx.update_editor(|editor, _, cx| {
16688 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
16689 });
16690 executor.run_until_parked();
16691
16692 cx.assert_state_with_diff(both_hunks_expanded);
16693
16694 cx.set_state(
16695 &r#"
16696 ˇA
16697 b
16698 "#
16699 .unindent(),
16700 );
16701 cx.run_until_parked();
16702
16703 // TODO this cursor position seems bad
16704 cx.assert_state_with_diff(
16705 r#"
16706 - ˇa
16707 + A
16708 b
16709 "#
16710 .unindent(),
16711 );
16712
16713 cx.update_editor(|editor, window, cx| {
16714 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16715 });
16716
16717 cx.assert_state_with_diff(
16718 r#"
16719 - ˇa
16720 + A
16721 b
16722 - c
16723 "#
16724 .unindent(),
16725 );
16726
16727 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16728 let snapshot = editor.snapshot(window, cx);
16729 let hunks = editor
16730 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16731 .collect::<Vec<_>>();
16732 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16733 let buffer_id = hunks[0].buffer_id;
16734 hunks
16735 .into_iter()
16736 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16737 .collect::<Vec<_>>()
16738 });
16739 assert_eq!(hunk_ranges.len(), 2);
16740
16741 cx.update_editor(|editor, _, cx| {
16742 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
16743 });
16744 executor.run_until_parked();
16745
16746 cx.assert_state_with_diff(
16747 r#"
16748 - ˇa
16749 + A
16750 b
16751 "#
16752 .unindent(),
16753 );
16754}
16755
16756#[gpui::test]
16757async fn test_toggle_deletion_hunk_at_start_of_file(
16758 executor: BackgroundExecutor,
16759 cx: &mut TestAppContext,
16760) {
16761 init_test(cx, |_| {});
16762 let mut cx = EditorTestContext::new(cx).await;
16763
16764 let diff_base = r#"
16765 a
16766 b
16767 c
16768 "#
16769 .unindent();
16770
16771 cx.set_state(
16772 &r#"
16773 ˇb
16774 c
16775 "#
16776 .unindent(),
16777 );
16778 cx.set_head_text(&diff_base);
16779 cx.update_editor(|editor, window, cx| {
16780 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16781 });
16782 executor.run_until_parked();
16783
16784 let hunk_expanded = r#"
16785 - a
16786 ˇb
16787 c
16788 "#
16789 .unindent();
16790
16791 cx.assert_state_with_diff(hunk_expanded.clone());
16792
16793 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16794 let snapshot = editor.snapshot(window, cx);
16795 let hunks = editor
16796 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16797 .collect::<Vec<_>>();
16798 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16799 let buffer_id = hunks[0].buffer_id;
16800 hunks
16801 .into_iter()
16802 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16803 .collect::<Vec<_>>()
16804 });
16805 assert_eq!(hunk_ranges.len(), 1);
16806
16807 cx.update_editor(|editor, _, cx| {
16808 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16809 });
16810 executor.run_until_parked();
16811
16812 let hunk_collapsed = r#"
16813 ˇb
16814 c
16815 "#
16816 .unindent();
16817
16818 cx.assert_state_with_diff(hunk_collapsed);
16819
16820 cx.update_editor(|editor, _, cx| {
16821 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16822 });
16823 executor.run_until_parked();
16824
16825 cx.assert_state_with_diff(hunk_expanded.clone());
16826}
16827
16828#[gpui::test]
16829async fn test_display_diff_hunks(cx: &mut TestAppContext) {
16830 init_test(cx, |_| {});
16831
16832 let fs = FakeFs::new(cx.executor());
16833 fs.insert_tree(
16834 path!("/test"),
16835 json!({
16836 ".git": {},
16837 "file-1": "ONE\n",
16838 "file-2": "TWO\n",
16839 "file-3": "THREE\n",
16840 }),
16841 )
16842 .await;
16843
16844 fs.set_head_for_repo(
16845 path!("/test/.git").as_ref(),
16846 &[
16847 ("file-1".into(), "one\n".into()),
16848 ("file-2".into(), "two\n".into()),
16849 ("file-3".into(), "three\n".into()),
16850 ],
16851 );
16852
16853 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
16854 let mut buffers = vec![];
16855 for i in 1..=3 {
16856 let buffer = project
16857 .update(cx, |project, cx| {
16858 let path = format!(path!("/test/file-{}"), i);
16859 project.open_local_buffer(path, cx)
16860 })
16861 .await
16862 .unwrap();
16863 buffers.push(buffer);
16864 }
16865
16866 let multibuffer = cx.new(|cx| {
16867 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
16868 multibuffer.set_all_diff_hunks_expanded(cx);
16869 for buffer in &buffers {
16870 let snapshot = buffer.read(cx).snapshot();
16871 multibuffer.set_excerpts_for_path(
16872 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
16873 buffer.clone(),
16874 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
16875 DEFAULT_MULTIBUFFER_CONTEXT,
16876 cx,
16877 );
16878 }
16879 multibuffer
16880 });
16881
16882 let editor = cx.add_window(|window, cx| {
16883 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
16884 });
16885 cx.run_until_parked();
16886
16887 let snapshot = editor
16888 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16889 .unwrap();
16890 let hunks = snapshot
16891 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
16892 .map(|hunk| match hunk {
16893 DisplayDiffHunk::Unfolded {
16894 display_row_range, ..
16895 } => display_row_range,
16896 DisplayDiffHunk::Folded { .. } => unreachable!(),
16897 })
16898 .collect::<Vec<_>>();
16899 assert_eq!(
16900 hunks,
16901 [
16902 DisplayRow(2)..DisplayRow(4),
16903 DisplayRow(7)..DisplayRow(9),
16904 DisplayRow(12)..DisplayRow(14),
16905 ]
16906 );
16907}
16908
16909#[gpui::test]
16910async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
16911 init_test(cx, |_| {});
16912
16913 let mut cx = EditorTestContext::new(cx).await;
16914 cx.set_head_text(indoc! { "
16915 one
16916 two
16917 three
16918 four
16919 five
16920 "
16921 });
16922 cx.set_index_text(indoc! { "
16923 one
16924 two
16925 three
16926 four
16927 five
16928 "
16929 });
16930 cx.set_state(indoc! {"
16931 one
16932 TWO
16933 ˇTHREE
16934 FOUR
16935 five
16936 "});
16937 cx.run_until_parked();
16938 cx.update_editor(|editor, window, cx| {
16939 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16940 });
16941 cx.run_until_parked();
16942 cx.assert_index_text(Some(indoc! {"
16943 one
16944 TWO
16945 THREE
16946 FOUR
16947 five
16948 "}));
16949 cx.set_state(indoc! { "
16950 one
16951 TWO
16952 ˇTHREE-HUNDRED
16953 FOUR
16954 five
16955 "});
16956 cx.run_until_parked();
16957 cx.update_editor(|editor, window, cx| {
16958 let snapshot = editor.snapshot(window, cx);
16959 let hunks = editor
16960 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16961 .collect::<Vec<_>>();
16962 assert_eq!(hunks.len(), 1);
16963 assert_eq!(
16964 hunks[0].status(),
16965 DiffHunkStatus {
16966 kind: DiffHunkStatusKind::Modified,
16967 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
16968 }
16969 );
16970
16971 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16972 });
16973 cx.run_until_parked();
16974 cx.assert_index_text(Some(indoc! {"
16975 one
16976 TWO
16977 THREE-HUNDRED
16978 FOUR
16979 five
16980 "}));
16981}
16982
16983#[gpui::test]
16984fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
16985 init_test(cx, |_| {});
16986
16987 let editor = cx.add_window(|window, cx| {
16988 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
16989 build_editor(buffer, window, cx)
16990 });
16991
16992 let render_args = Arc::new(Mutex::new(None));
16993 let snapshot = editor
16994 .update(cx, |editor, window, cx| {
16995 let snapshot = editor.buffer().read(cx).snapshot(cx);
16996 let range =
16997 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
16998
16999 struct RenderArgs {
17000 row: MultiBufferRow,
17001 folded: bool,
17002 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
17003 }
17004
17005 let crease = Crease::inline(
17006 range,
17007 FoldPlaceholder::test(),
17008 {
17009 let toggle_callback = render_args.clone();
17010 move |row, folded, callback, _window, _cx| {
17011 *toggle_callback.lock() = Some(RenderArgs {
17012 row,
17013 folded,
17014 callback,
17015 });
17016 div()
17017 }
17018 },
17019 |_row, _folded, _window, _cx| div(),
17020 );
17021
17022 editor.insert_creases(Some(crease), cx);
17023 let snapshot = editor.snapshot(window, cx);
17024 let _div = snapshot.render_crease_toggle(
17025 MultiBufferRow(1),
17026 false,
17027 cx.entity().clone(),
17028 window,
17029 cx,
17030 );
17031 snapshot
17032 })
17033 .unwrap();
17034
17035 let render_args = render_args.lock().take().unwrap();
17036 assert_eq!(render_args.row, MultiBufferRow(1));
17037 assert!(!render_args.folded);
17038 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
17039
17040 cx.update_window(*editor, |_, window, cx| {
17041 (render_args.callback)(true, window, cx)
17042 })
17043 .unwrap();
17044 let snapshot = editor
17045 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17046 .unwrap();
17047 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
17048
17049 cx.update_window(*editor, |_, window, cx| {
17050 (render_args.callback)(false, window, cx)
17051 })
17052 .unwrap();
17053 let snapshot = editor
17054 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17055 .unwrap();
17056 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
17057}
17058
17059#[gpui::test]
17060async fn test_input_text(cx: &mut TestAppContext) {
17061 init_test(cx, |_| {});
17062 let mut cx = EditorTestContext::new(cx).await;
17063
17064 cx.set_state(
17065 &r#"ˇone
17066 two
17067
17068 three
17069 fourˇ
17070 five
17071
17072 siˇx"#
17073 .unindent(),
17074 );
17075
17076 cx.dispatch_action(HandleInput(String::new()));
17077 cx.assert_editor_state(
17078 &r#"ˇone
17079 two
17080
17081 three
17082 fourˇ
17083 five
17084
17085 siˇx"#
17086 .unindent(),
17087 );
17088
17089 cx.dispatch_action(HandleInput("AAAA".to_string()));
17090 cx.assert_editor_state(
17091 &r#"AAAAˇone
17092 two
17093
17094 three
17095 fourAAAAˇ
17096 five
17097
17098 siAAAAˇx"#
17099 .unindent(),
17100 );
17101}
17102
17103#[gpui::test]
17104async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
17105 init_test(cx, |_| {});
17106
17107 let mut cx = EditorTestContext::new(cx).await;
17108 cx.set_state(
17109 r#"let foo = 1;
17110let foo = 2;
17111let foo = 3;
17112let fooˇ = 4;
17113let foo = 5;
17114let foo = 6;
17115let foo = 7;
17116let foo = 8;
17117let foo = 9;
17118let foo = 10;
17119let foo = 11;
17120let foo = 12;
17121let foo = 13;
17122let foo = 14;
17123let foo = 15;"#,
17124 );
17125
17126 cx.update_editor(|e, window, cx| {
17127 assert_eq!(
17128 e.next_scroll_position,
17129 NextScrollCursorCenterTopBottom::Center,
17130 "Default next scroll direction is center",
17131 );
17132
17133 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17134 assert_eq!(
17135 e.next_scroll_position,
17136 NextScrollCursorCenterTopBottom::Top,
17137 "After center, next scroll direction should be top",
17138 );
17139
17140 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17141 assert_eq!(
17142 e.next_scroll_position,
17143 NextScrollCursorCenterTopBottom::Bottom,
17144 "After top, next scroll direction should be bottom",
17145 );
17146
17147 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17148 assert_eq!(
17149 e.next_scroll_position,
17150 NextScrollCursorCenterTopBottom::Center,
17151 "After bottom, scrolling should start over",
17152 );
17153
17154 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17155 assert_eq!(
17156 e.next_scroll_position,
17157 NextScrollCursorCenterTopBottom::Top,
17158 "Scrolling continues if retriggered fast enough"
17159 );
17160 });
17161
17162 cx.executor()
17163 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
17164 cx.executor().run_until_parked();
17165 cx.update_editor(|e, _, _| {
17166 assert_eq!(
17167 e.next_scroll_position,
17168 NextScrollCursorCenterTopBottom::Center,
17169 "If scrolling is not triggered fast enough, it should reset"
17170 );
17171 });
17172}
17173
17174#[gpui::test]
17175async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
17176 init_test(cx, |_| {});
17177 let mut cx = EditorLspTestContext::new_rust(
17178 lsp::ServerCapabilities {
17179 definition_provider: Some(lsp::OneOf::Left(true)),
17180 references_provider: Some(lsp::OneOf::Left(true)),
17181 ..lsp::ServerCapabilities::default()
17182 },
17183 cx,
17184 )
17185 .await;
17186
17187 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
17188 let go_to_definition = cx
17189 .lsp
17190 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
17191 move |params, _| async move {
17192 if empty_go_to_definition {
17193 Ok(None)
17194 } else {
17195 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
17196 uri: params.text_document_position_params.text_document.uri,
17197 range: lsp::Range::new(
17198 lsp::Position::new(4, 3),
17199 lsp::Position::new(4, 6),
17200 ),
17201 })))
17202 }
17203 },
17204 );
17205 let references = cx
17206 .lsp
17207 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
17208 Ok(Some(vec![lsp::Location {
17209 uri: params.text_document_position.text_document.uri,
17210 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
17211 }]))
17212 });
17213 (go_to_definition, references)
17214 };
17215
17216 cx.set_state(
17217 &r#"fn one() {
17218 let mut a = ˇtwo();
17219 }
17220
17221 fn two() {}"#
17222 .unindent(),
17223 );
17224 set_up_lsp_handlers(false, &mut cx);
17225 let navigated = cx
17226 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17227 .await
17228 .expect("Failed to navigate to definition");
17229 assert_eq!(
17230 navigated,
17231 Navigated::Yes,
17232 "Should have navigated to definition from the GetDefinition response"
17233 );
17234 cx.assert_editor_state(
17235 &r#"fn one() {
17236 let mut a = two();
17237 }
17238
17239 fn «twoˇ»() {}"#
17240 .unindent(),
17241 );
17242
17243 let editors = cx.update_workspace(|workspace, _, cx| {
17244 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17245 });
17246 cx.update_editor(|_, _, test_editor_cx| {
17247 assert_eq!(
17248 editors.len(),
17249 1,
17250 "Initially, only one, test, editor should be open in the workspace"
17251 );
17252 assert_eq!(
17253 test_editor_cx.entity(),
17254 editors.last().expect("Asserted len is 1").clone()
17255 );
17256 });
17257
17258 set_up_lsp_handlers(true, &mut cx);
17259 let navigated = cx
17260 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17261 .await
17262 .expect("Failed to navigate to lookup references");
17263 assert_eq!(
17264 navigated,
17265 Navigated::Yes,
17266 "Should have navigated to references as a fallback after empty GoToDefinition response"
17267 );
17268 // We should not change the selections in the existing file,
17269 // if opening another milti buffer with the references
17270 cx.assert_editor_state(
17271 &r#"fn one() {
17272 let mut a = two();
17273 }
17274
17275 fn «twoˇ»() {}"#
17276 .unindent(),
17277 );
17278 let editors = cx.update_workspace(|workspace, _, cx| {
17279 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17280 });
17281 cx.update_editor(|_, _, test_editor_cx| {
17282 assert_eq!(
17283 editors.len(),
17284 2,
17285 "After falling back to references search, we open a new editor with the results"
17286 );
17287 let references_fallback_text = editors
17288 .into_iter()
17289 .find(|new_editor| *new_editor != test_editor_cx.entity())
17290 .expect("Should have one non-test editor now")
17291 .read(test_editor_cx)
17292 .text(test_editor_cx);
17293 assert_eq!(
17294 references_fallback_text, "fn one() {\n let mut a = two();\n}",
17295 "Should use the range from the references response and not the GoToDefinition one"
17296 );
17297 });
17298}
17299
17300#[gpui::test]
17301async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
17302 init_test(cx, |_| {});
17303 cx.update(|cx| {
17304 let mut editor_settings = EditorSettings::get_global(cx).clone();
17305 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
17306 EditorSettings::override_global(editor_settings, cx);
17307 });
17308 let mut cx = EditorLspTestContext::new_rust(
17309 lsp::ServerCapabilities {
17310 definition_provider: Some(lsp::OneOf::Left(true)),
17311 references_provider: Some(lsp::OneOf::Left(true)),
17312 ..lsp::ServerCapabilities::default()
17313 },
17314 cx,
17315 )
17316 .await;
17317 let original_state = r#"fn one() {
17318 let mut a = ˇtwo();
17319 }
17320
17321 fn two() {}"#
17322 .unindent();
17323 cx.set_state(&original_state);
17324
17325 let mut go_to_definition = cx
17326 .lsp
17327 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
17328 move |_, _| async move { Ok(None) },
17329 );
17330 let _references = cx
17331 .lsp
17332 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
17333 panic!("Should not call for references with no go to definition fallback")
17334 });
17335
17336 let navigated = cx
17337 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17338 .await
17339 .expect("Failed to navigate to lookup references");
17340 go_to_definition
17341 .next()
17342 .await
17343 .expect("Should have called the go_to_definition handler");
17344
17345 assert_eq!(
17346 navigated,
17347 Navigated::No,
17348 "Should have navigated to references as a fallback after empty GoToDefinition response"
17349 );
17350 cx.assert_editor_state(&original_state);
17351 let editors = cx.update_workspace(|workspace, _, cx| {
17352 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17353 });
17354 cx.update_editor(|_, _, _| {
17355 assert_eq!(
17356 editors.len(),
17357 1,
17358 "After unsuccessful fallback, no other editor should have been opened"
17359 );
17360 });
17361}
17362
17363#[gpui::test]
17364async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
17365 init_test(cx, |_| {});
17366
17367 let language = Arc::new(Language::new(
17368 LanguageConfig::default(),
17369 Some(tree_sitter_rust::LANGUAGE.into()),
17370 ));
17371
17372 let text = r#"
17373 #[cfg(test)]
17374 mod tests() {
17375 #[test]
17376 fn runnable_1() {
17377 let a = 1;
17378 }
17379
17380 #[test]
17381 fn runnable_2() {
17382 let a = 1;
17383 let b = 2;
17384 }
17385 }
17386 "#
17387 .unindent();
17388
17389 let fs = FakeFs::new(cx.executor());
17390 fs.insert_file("/file.rs", Default::default()).await;
17391
17392 let project = Project::test(fs, ["/a".as_ref()], cx).await;
17393 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17394 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17395 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
17396 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
17397
17398 let editor = cx.new_window_entity(|window, cx| {
17399 Editor::new(
17400 EditorMode::full(),
17401 multi_buffer,
17402 Some(project.clone()),
17403 window,
17404 cx,
17405 )
17406 });
17407
17408 editor.update_in(cx, |editor, window, cx| {
17409 let snapshot = editor.buffer().read(cx).snapshot(cx);
17410 editor.tasks.insert(
17411 (buffer.read(cx).remote_id(), 3),
17412 RunnableTasks {
17413 templates: vec![],
17414 offset: snapshot.anchor_before(43),
17415 column: 0,
17416 extra_variables: HashMap::default(),
17417 context_range: BufferOffset(43)..BufferOffset(85),
17418 },
17419 );
17420 editor.tasks.insert(
17421 (buffer.read(cx).remote_id(), 8),
17422 RunnableTasks {
17423 templates: vec![],
17424 offset: snapshot.anchor_before(86),
17425 column: 0,
17426 extra_variables: HashMap::default(),
17427 context_range: BufferOffset(86)..BufferOffset(191),
17428 },
17429 );
17430
17431 // Test finding task when cursor is inside function body
17432 editor.change_selections(None, window, cx, |s| {
17433 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
17434 });
17435 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
17436 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
17437
17438 // Test finding task when cursor is on function name
17439 editor.change_selections(None, window, cx, |s| {
17440 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
17441 });
17442 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
17443 assert_eq!(row, 8, "Should find task when cursor is on function name");
17444 });
17445}
17446
17447#[gpui::test]
17448async fn test_folding_buffers(cx: &mut TestAppContext) {
17449 init_test(cx, |_| {});
17450
17451 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
17452 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
17453 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
17454
17455 let fs = FakeFs::new(cx.executor());
17456 fs.insert_tree(
17457 path!("/a"),
17458 json!({
17459 "first.rs": sample_text_1,
17460 "second.rs": sample_text_2,
17461 "third.rs": sample_text_3,
17462 }),
17463 )
17464 .await;
17465 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17466 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17467 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17468 let worktree = project.update(cx, |project, cx| {
17469 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17470 assert_eq!(worktrees.len(), 1);
17471 worktrees.pop().unwrap()
17472 });
17473 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17474
17475 let buffer_1 = project
17476 .update(cx, |project, cx| {
17477 project.open_buffer((worktree_id, "first.rs"), cx)
17478 })
17479 .await
17480 .unwrap();
17481 let buffer_2 = project
17482 .update(cx, |project, cx| {
17483 project.open_buffer((worktree_id, "second.rs"), cx)
17484 })
17485 .await
17486 .unwrap();
17487 let buffer_3 = project
17488 .update(cx, |project, cx| {
17489 project.open_buffer((worktree_id, "third.rs"), cx)
17490 })
17491 .await
17492 .unwrap();
17493
17494 let multi_buffer = cx.new(|cx| {
17495 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17496 multi_buffer.push_excerpts(
17497 buffer_1.clone(),
17498 [
17499 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17500 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17501 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17502 ],
17503 cx,
17504 );
17505 multi_buffer.push_excerpts(
17506 buffer_2.clone(),
17507 [
17508 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17509 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17510 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17511 ],
17512 cx,
17513 );
17514 multi_buffer.push_excerpts(
17515 buffer_3.clone(),
17516 [
17517 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17518 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17519 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17520 ],
17521 cx,
17522 );
17523 multi_buffer
17524 });
17525 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17526 Editor::new(
17527 EditorMode::full(),
17528 multi_buffer.clone(),
17529 Some(project.clone()),
17530 window,
17531 cx,
17532 )
17533 });
17534
17535 assert_eq!(
17536 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17537 "\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",
17538 );
17539
17540 multi_buffer_editor.update(cx, |editor, cx| {
17541 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
17542 });
17543 assert_eq!(
17544 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17545 "\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",
17546 "After folding the first buffer, its text should not be displayed"
17547 );
17548
17549 multi_buffer_editor.update(cx, |editor, cx| {
17550 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
17551 });
17552 assert_eq!(
17553 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17554 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
17555 "After folding the second buffer, its text should not be displayed"
17556 );
17557
17558 multi_buffer_editor.update(cx, |editor, cx| {
17559 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
17560 });
17561 assert_eq!(
17562 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17563 "\n\n\n\n\n",
17564 "After folding the third buffer, its text should not be displayed"
17565 );
17566
17567 // Emulate selection inside the fold logic, that should work
17568 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17569 editor
17570 .snapshot(window, cx)
17571 .next_line_boundary(Point::new(0, 4));
17572 });
17573
17574 multi_buffer_editor.update(cx, |editor, cx| {
17575 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
17576 });
17577 assert_eq!(
17578 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17579 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
17580 "After unfolding the second buffer, its text should be displayed"
17581 );
17582
17583 // Typing inside of buffer 1 causes that buffer to be unfolded.
17584 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17585 assert_eq!(
17586 multi_buffer
17587 .read(cx)
17588 .snapshot(cx)
17589 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
17590 .collect::<String>(),
17591 "bbbb"
17592 );
17593 editor.change_selections(None, window, cx, |selections| {
17594 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
17595 });
17596 editor.handle_input("B", window, cx);
17597 });
17598
17599 assert_eq!(
17600 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17601 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
17602 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
17603 );
17604
17605 multi_buffer_editor.update(cx, |editor, cx| {
17606 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
17607 });
17608 assert_eq!(
17609 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17610 "\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",
17611 "After unfolding the all buffers, all original text should be displayed"
17612 );
17613}
17614
17615#[gpui::test]
17616async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
17617 init_test(cx, |_| {});
17618
17619 let sample_text_1 = "1111\n2222\n3333".to_string();
17620 let sample_text_2 = "4444\n5555\n6666".to_string();
17621 let sample_text_3 = "7777\n8888\n9999".to_string();
17622
17623 let fs = FakeFs::new(cx.executor());
17624 fs.insert_tree(
17625 path!("/a"),
17626 json!({
17627 "first.rs": sample_text_1,
17628 "second.rs": sample_text_2,
17629 "third.rs": sample_text_3,
17630 }),
17631 )
17632 .await;
17633 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17634 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17635 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17636 let worktree = project.update(cx, |project, cx| {
17637 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17638 assert_eq!(worktrees.len(), 1);
17639 worktrees.pop().unwrap()
17640 });
17641 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17642
17643 let buffer_1 = project
17644 .update(cx, |project, cx| {
17645 project.open_buffer((worktree_id, "first.rs"), cx)
17646 })
17647 .await
17648 .unwrap();
17649 let buffer_2 = project
17650 .update(cx, |project, cx| {
17651 project.open_buffer((worktree_id, "second.rs"), cx)
17652 })
17653 .await
17654 .unwrap();
17655 let buffer_3 = project
17656 .update(cx, |project, cx| {
17657 project.open_buffer((worktree_id, "third.rs"), cx)
17658 })
17659 .await
17660 .unwrap();
17661
17662 let multi_buffer = cx.new(|cx| {
17663 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17664 multi_buffer.push_excerpts(
17665 buffer_1.clone(),
17666 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
17667 cx,
17668 );
17669 multi_buffer.push_excerpts(
17670 buffer_2.clone(),
17671 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
17672 cx,
17673 );
17674 multi_buffer.push_excerpts(
17675 buffer_3.clone(),
17676 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
17677 cx,
17678 );
17679 multi_buffer
17680 });
17681
17682 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17683 Editor::new(
17684 EditorMode::full(),
17685 multi_buffer,
17686 Some(project.clone()),
17687 window,
17688 cx,
17689 )
17690 });
17691
17692 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
17693 assert_eq!(
17694 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17695 full_text,
17696 );
17697
17698 multi_buffer_editor.update(cx, |editor, cx| {
17699 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
17700 });
17701 assert_eq!(
17702 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17703 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
17704 "After folding the first buffer, its text should not be displayed"
17705 );
17706
17707 multi_buffer_editor.update(cx, |editor, cx| {
17708 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
17709 });
17710
17711 assert_eq!(
17712 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17713 "\n\n\n\n\n\n7777\n8888\n9999",
17714 "After folding the second buffer, its text should not be displayed"
17715 );
17716
17717 multi_buffer_editor.update(cx, |editor, cx| {
17718 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
17719 });
17720 assert_eq!(
17721 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17722 "\n\n\n\n\n",
17723 "After folding the third buffer, its text should not be displayed"
17724 );
17725
17726 multi_buffer_editor.update(cx, |editor, cx| {
17727 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
17728 });
17729 assert_eq!(
17730 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17731 "\n\n\n\n4444\n5555\n6666\n\n",
17732 "After unfolding the second buffer, its text should be displayed"
17733 );
17734
17735 multi_buffer_editor.update(cx, |editor, cx| {
17736 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
17737 });
17738 assert_eq!(
17739 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17740 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
17741 "After unfolding the first buffer, its text should be displayed"
17742 );
17743
17744 multi_buffer_editor.update(cx, |editor, cx| {
17745 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
17746 });
17747 assert_eq!(
17748 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17749 full_text,
17750 "After unfolding all buffers, all original text should be displayed"
17751 );
17752}
17753
17754#[gpui::test]
17755async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
17756 init_test(cx, |_| {});
17757
17758 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
17759
17760 let fs = FakeFs::new(cx.executor());
17761 fs.insert_tree(
17762 path!("/a"),
17763 json!({
17764 "main.rs": sample_text,
17765 }),
17766 )
17767 .await;
17768 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17769 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17770 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17771 let worktree = project.update(cx, |project, cx| {
17772 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17773 assert_eq!(worktrees.len(), 1);
17774 worktrees.pop().unwrap()
17775 });
17776 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17777
17778 let buffer_1 = project
17779 .update(cx, |project, cx| {
17780 project.open_buffer((worktree_id, "main.rs"), cx)
17781 })
17782 .await
17783 .unwrap();
17784
17785 let multi_buffer = cx.new(|cx| {
17786 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17787 multi_buffer.push_excerpts(
17788 buffer_1.clone(),
17789 [ExcerptRange::new(
17790 Point::new(0, 0)
17791 ..Point::new(
17792 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
17793 0,
17794 ),
17795 )],
17796 cx,
17797 );
17798 multi_buffer
17799 });
17800 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17801 Editor::new(
17802 EditorMode::full(),
17803 multi_buffer,
17804 Some(project.clone()),
17805 window,
17806 cx,
17807 )
17808 });
17809
17810 let selection_range = Point::new(1, 0)..Point::new(2, 0);
17811 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17812 enum TestHighlight {}
17813 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
17814 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
17815 editor.highlight_text::<TestHighlight>(
17816 vec![highlight_range.clone()],
17817 HighlightStyle::color(Hsla::green()),
17818 cx,
17819 );
17820 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
17821 });
17822
17823 let full_text = format!("\n\n{sample_text}");
17824 assert_eq!(
17825 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17826 full_text,
17827 );
17828}
17829
17830#[gpui::test]
17831async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
17832 init_test(cx, |_| {});
17833 cx.update(|cx| {
17834 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
17835 "keymaps/default-linux.json",
17836 cx,
17837 )
17838 .unwrap();
17839 cx.bind_keys(default_key_bindings);
17840 });
17841
17842 let (editor, cx) = cx.add_window_view(|window, cx| {
17843 let multi_buffer = MultiBuffer::build_multi(
17844 [
17845 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
17846 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
17847 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
17848 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
17849 ],
17850 cx,
17851 );
17852 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
17853
17854 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
17855 // fold all but the second buffer, so that we test navigating between two
17856 // adjacent folded buffers, as well as folded buffers at the start and
17857 // end the multibuffer
17858 editor.fold_buffer(buffer_ids[0], cx);
17859 editor.fold_buffer(buffer_ids[2], cx);
17860 editor.fold_buffer(buffer_ids[3], cx);
17861
17862 editor
17863 });
17864 cx.simulate_resize(size(px(1000.), px(1000.)));
17865
17866 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
17867 cx.assert_excerpts_with_selections(indoc! {"
17868 [EXCERPT]
17869 ˇ[FOLDED]
17870 [EXCERPT]
17871 a1
17872 b1
17873 [EXCERPT]
17874 [FOLDED]
17875 [EXCERPT]
17876 [FOLDED]
17877 "
17878 });
17879 cx.simulate_keystroke("down");
17880 cx.assert_excerpts_with_selections(indoc! {"
17881 [EXCERPT]
17882 [FOLDED]
17883 [EXCERPT]
17884 ˇa1
17885 b1
17886 [EXCERPT]
17887 [FOLDED]
17888 [EXCERPT]
17889 [FOLDED]
17890 "
17891 });
17892 cx.simulate_keystroke("down");
17893 cx.assert_excerpts_with_selections(indoc! {"
17894 [EXCERPT]
17895 [FOLDED]
17896 [EXCERPT]
17897 a1
17898 ˇb1
17899 [EXCERPT]
17900 [FOLDED]
17901 [EXCERPT]
17902 [FOLDED]
17903 "
17904 });
17905 cx.simulate_keystroke("down");
17906 cx.assert_excerpts_with_selections(indoc! {"
17907 [EXCERPT]
17908 [FOLDED]
17909 [EXCERPT]
17910 a1
17911 b1
17912 ˇ[EXCERPT]
17913 [FOLDED]
17914 [EXCERPT]
17915 [FOLDED]
17916 "
17917 });
17918 cx.simulate_keystroke("down");
17919 cx.assert_excerpts_with_selections(indoc! {"
17920 [EXCERPT]
17921 [FOLDED]
17922 [EXCERPT]
17923 a1
17924 b1
17925 [EXCERPT]
17926 ˇ[FOLDED]
17927 [EXCERPT]
17928 [FOLDED]
17929 "
17930 });
17931 for _ in 0..5 {
17932 cx.simulate_keystroke("down");
17933 cx.assert_excerpts_with_selections(indoc! {"
17934 [EXCERPT]
17935 [FOLDED]
17936 [EXCERPT]
17937 a1
17938 b1
17939 [EXCERPT]
17940 [FOLDED]
17941 [EXCERPT]
17942 ˇ[FOLDED]
17943 "
17944 });
17945 }
17946
17947 cx.simulate_keystroke("up");
17948 cx.assert_excerpts_with_selections(indoc! {"
17949 [EXCERPT]
17950 [FOLDED]
17951 [EXCERPT]
17952 a1
17953 b1
17954 [EXCERPT]
17955 ˇ[FOLDED]
17956 [EXCERPT]
17957 [FOLDED]
17958 "
17959 });
17960 cx.simulate_keystroke("up");
17961 cx.assert_excerpts_with_selections(indoc! {"
17962 [EXCERPT]
17963 [FOLDED]
17964 [EXCERPT]
17965 a1
17966 b1
17967 ˇ[EXCERPT]
17968 [FOLDED]
17969 [EXCERPT]
17970 [FOLDED]
17971 "
17972 });
17973 cx.simulate_keystroke("up");
17974 cx.assert_excerpts_with_selections(indoc! {"
17975 [EXCERPT]
17976 [FOLDED]
17977 [EXCERPT]
17978 a1
17979 ˇb1
17980 [EXCERPT]
17981 [FOLDED]
17982 [EXCERPT]
17983 [FOLDED]
17984 "
17985 });
17986 cx.simulate_keystroke("up");
17987 cx.assert_excerpts_with_selections(indoc! {"
17988 [EXCERPT]
17989 [FOLDED]
17990 [EXCERPT]
17991 ˇa1
17992 b1
17993 [EXCERPT]
17994 [FOLDED]
17995 [EXCERPT]
17996 [FOLDED]
17997 "
17998 });
17999 for _ in 0..5 {
18000 cx.simulate_keystroke("up");
18001 cx.assert_excerpts_with_selections(indoc! {"
18002 [EXCERPT]
18003 ˇ[FOLDED]
18004 [EXCERPT]
18005 a1
18006 b1
18007 [EXCERPT]
18008 [FOLDED]
18009 [EXCERPT]
18010 [FOLDED]
18011 "
18012 });
18013 }
18014}
18015
18016#[gpui::test]
18017async fn test_inline_completion_text(cx: &mut TestAppContext) {
18018 init_test(cx, |_| {});
18019
18020 // Simple insertion
18021 assert_highlighted_edits(
18022 "Hello, world!",
18023 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
18024 true,
18025 cx,
18026 |highlighted_edits, cx| {
18027 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
18028 assert_eq!(highlighted_edits.highlights.len(), 1);
18029 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
18030 assert_eq!(
18031 highlighted_edits.highlights[0].1.background_color,
18032 Some(cx.theme().status().created_background)
18033 );
18034 },
18035 )
18036 .await;
18037
18038 // Replacement
18039 assert_highlighted_edits(
18040 "This is a test.",
18041 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
18042 false,
18043 cx,
18044 |highlighted_edits, cx| {
18045 assert_eq!(highlighted_edits.text, "That is a test.");
18046 assert_eq!(highlighted_edits.highlights.len(), 1);
18047 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
18048 assert_eq!(
18049 highlighted_edits.highlights[0].1.background_color,
18050 Some(cx.theme().status().created_background)
18051 );
18052 },
18053 )
18054 .await;
18055
18056 // Multiple edits
18057 assert_highlighted_edits(
18058 "Hello, world!",
18059 vec![
18060 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
18061 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
18062 ],
18063 false,
18064 cx,
18065 |highlighted_edits, cx| {
18066 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
18067 assert_eq!(highlighted_edits.highlights.len(), 2);
18068 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
18069 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
18070 assert_eq!(
18071 highlighted_edits.highlights[0].1.background_color,
18072 Some(cx.theme().status().created_background)
18073 );
18074 assert_eq!(
18075 highlighted_edits.highlights[1].1.background_color,
18076 Some(cx.theme().status().created_background)
18077 );
18078 },
18079 )
18080 .await;
18081
18082 // Multiple lines with edits
18083 assert_highlighted_edits(
18084 "First line\nSecond line\nThird line\nFourth line",
18085 vec![
18086 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
18087 (
18088 Point::new(2, 0)..Point::new(2, 10),
18089 "New third line".to_string(),
18090 ),
18091 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
18092 ],
18093 false,
18094 cx,
18095 |highlighted_edits, cx| {
18096 assert_eq!(
18097 highlighted_edits.text,
18098 "Second modified\nNew third line\nFourth updated line"
18099 );
18100 assert_eq!(highlighted_edits.highlights.len(), 3);
18101 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
18102 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
18103 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
18104 for highlight in &highlighted_edits.highlights {
18105 assert_eq!(
18106 highlight.1.background_color,
18107 Some(cx.theme().status().created_background)
18108 );
18109 }
18110 },
18111 )
18112 .await;
18113}
18114
18115#[gpui::test]
18116async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
18117 init_test(cx, |_| {});
18118
18119 // Deletion
18120 assert_highlighted_edits(
18121 "Hello, world!",
18122 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
18123 true,
18124 cx,
18125 |highlighted_edits, cx| {
18126 assert_eq!(highlighted_edits.text, "Hello, world!");
18127 assert_eq!(highlighted_edits.highlights.len(), 1);
18128 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
18129 assert_eq!(
18130 highlighted_edits.highlights[0].1.background_color,
18131 Some(cx.theme().status().deleted_background)
18132 );
18133 },
18134 )
18135 .await;
18136
18137 // Insertion
18138 assert_highlighted_edits(
18139 "Hello, world!",
18140 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
18141 true,
18142 cx,
18143 |highlighted_edits, cx| {
18144 assert_eq!(highlighted_edits.highlights.len(), 1);
18145 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
18146 assert_eq!(
18147 highlighted_edits.highlights[0].1.background_color,
18148 Some(cx.theme().status().created_background)
18149 );
18150 },
18151 )
18152 .await;
18153}
18154
18155async fn assert_highlighted_edits(
18156 text: &str,
18157 edits: Vec<(Range<Point>, String)>,
18158 include_deletions: bool,
18159 cx: &mut TestAppContext,
18160 assertion_fn: impl Fn(HighlightedText, &App),
18161) {
18162 let window = cx.add_window(|window, cx| {
18163 let buffer = MultiBuffer::build_simple(text, cx);
18164 Editor::new(EditorMode::full(), buffer, None, window, cx)
18165 });
18166 let cx = &mut VisualTestContext::from_window(*window, cx);
18167
18168 let (buffer, snapshot) = window
18169 .update(cx, |editor, _window, cx| {
18170 (
18171 editor.buffer().clone(),
18172 editor.buffer().read(cx).snapshot(cx),
18173 )
18174 })
18175 .unwrap();
18176
18177 let edits = edits
18178 .into_iter()
18179 .map(|(range, edit)| {
18180 (
18181 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
18182 edit,
18183 )
18184 })
18185 .collect::<Vec<_>>();
18186
18187 let text_anchor_edits = edits
18188 .clone()
18189 .into_iter()
18190 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
18191 .collect::<Vec<_>>();
18192
18193 let edit_preview = window
18194 .update(cx, |_, _window, cx| {
18195 buffer
18196 .read(cx)
18197 .as_singleton()
18198 .unwrap()
18199 .read(cx)
18200 .preview_edits(text_anchor_edits.into(), cx)
18201 })
18202 .unwrap()
18203 .await;
18204
18205 cx.update(|_window, cx| {
18206 let highlighted_edits = inline_completion_edit_text(
18207 &snapshot.as_singleton().unwrap().2,
18208 &edits,
18209 &edit_preview,
18210 include_deletions,
18211 cx,
18212 );
18213 assertion_fn(highlighted_edits, cx)
18214 });
18215}
18216
18217#[track_caller]
18218fn assert_breakpoint(
18219 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
18220 path: &Arc<Path>,
18221 expected: Vec<(u32, Breakpoint)>,
18222) {
18223 if expected.len() == 0usize {
18224 assert!(!breakpoints.contains_key(path), "{}", path.display());
18225 } else {
18226 let mut breakpoint = breakpoints
18227 .get(path)
18228 .unwrap()
18229 .into_iter()
18230 .map(|breakpoint| {
18231 (
18232 breakpoint.row,
18233 Breakpoint {
18234 message: breakpoint.message.clone(),
18235 state: breakpoint.state,
18236 condition: breakpoint.condition.clone(),
18237 hit_condition: breakpoint.hit_condition.clone(),
18238 },
18239 )
18240 })
18241 .collect::<Vec<_>>();
18242
18243 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
18244
18245 assert_eq!(expected, breakpoint);
18246 }
18247}
18248
18249fn add_log_breakpoint_at_cursor(
18250 editor: &mut Editor,
18251 log_message: &str,
18252 window: &mut Window,
18253 cx: &mut Context<Editor>,
18254) {
18255 let (anchor, bp) = editor
18256 .breakpoints_at_cursors(window, cx)
18257 .first()
18258 .and_then(|(anchor, bp)| {
18259 if let Some(bp) = bp {
18260 Some((*anchor, bp.clone()))
18261 } else {
18262 None
18263 }
18264 })
18265 .unwrap_or_else(|| {
18266 let cursor_position: Point = editor.selections.newest(cx).head();
18267
18268 let breakpoint_position = editor
18269 .snapshot(window, cx)
18270 .display_snapshot
18271 .buffer_snapshot
18272 .anchor_before(Point::new(cursor_position.row, 0));
18273
18274 (breakpoint_position, Breakpoint::new_log(&log_message))
18275 });
18276
18277 editor.edit_breakpoint_at_anchor(
18278 anchor,
18279 bp,
18280 BreakpointEditAction::EditLogMessage(log_message.into()),
18281 cx,
18282 );
18283}
18284
18285#[gpui::test]
18286async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
18287 init_test(cx, |_| {});
18288
18289 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18290 let fs = FakeFs::new(cx.executor());
18291 fs.insert_tree(
18292 path!("/a"),
18293 json!({
18294 "main.rs": sample_text,
18295 }),
18296 )
18297 .await;
18298 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18299 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18300 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18301
18302 let fs = FakeFs::new(cx.executor());
18303 fs.insert_tree(
18304 path!("/a"),
18305 json!({
18306 "main.rs": sample_text,
18307 }),
18308 )
18309 .await;
18310 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18311 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18312 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18313 let worktree_id = workspace
18314 .update(cx, |workspace, _window, cx| {
18315 workspace.project().update(cx, |project, cx| {
18316 project.worktrees(cx).next().unwrap().read(cx).id()
18317 })
18318 })
18319 .unwrap();
18320
18321 let buffer = project
18322 .update(cx, |project, cx| {
18323 project.open_buffer((worktree_id, "main.rs"), cx)
18324 })
18325 .await
18326 .unwrap();
18327
18328 let (editor, cx) = cx.add_window_view(|window, cx| {
18329 Editor::new(
18330 EditorMode::full(),
18331 MultiBuffer::build_from_buffer(buffer, cx),
18332 Some(project.clone()),
18333 window,
18334 cx,
18335 )
18336 });
18337
18338 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18339 let abs_path = project.read_with(cx, |project, cx| {
18340 project
18341 .absolute_path(&project_path, cx)
18342 .map(|path_buf| Arc::from(path_buf.to_owned()))
18343 .unwrap()
18344 });
18345
18346 // assert we can add breakpoint on the first line
18347 editor.update_in(cx, |editor, window, cx| {
18348 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18349 editor.move_to_end(&MoveToEnd, window, cx);
18350 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18351 });
18352
18353 let breakpoints = editor.update(cx, |editor, cx| {
18354 editor
18355 .breakpoint_store()
18356 .as_ref()
18357 .unwrap()
18358 .read(cx)
18359 .all_breakpoints(cx)
18360 .clone()
18361 });
18362
18363 assert_eq!(1, breakpoints.len());
18364 assert_breakpoint(
18365 &breakpoints,
18366 &abs_path,
18367 vec![
18368 (0, Breakpoint::new_standard()),
18369 (3, Breakpoint::new_standard()),
18370 ],
18371 );
18372
18373 editor.update_in(cx, |editor, window, cx| {
18374 editor.move_to_beginning(&MoveToBeginning, window, cx);
18375 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18376 });
18377
18378 let breakpoints = editor.update(cx, |editor, cx| {
18379 editor
18380 .breakpoint_store()
18381 .as_ref()
18382 .unwrap()
18383 .read(cx)
18384 .all_breakpoints(cx)
18385 .clone()
18386 });
18387
18388 assert_eq!(1, breakpoints.len());
18389 assert_breakpoint(
18390 &breakpoints,
18391 &abs_path,
18392 vec![(3, Breakpoint::new_standard())],
18393 );
18394
18395 editor.update_in(cx, |editor, window, cx| {
18396 editor.move_to_end(&MoveToEnd, window, cx);
18397 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18398 });
18399
18400 let breakpoints = editor.update(cx, |editor, cx| {
18401 editor
18402 .breakpoint_store()
18403 .as_ref()
18404 .unwrap()
18405 .read(cx)
18406 .all_breakpoints(cx)
18407 .clone()
18408 });
18409
18410 assert_eq!(0, breakpoints.len());
18411 assert_breakpoint(&breakpoints, &abs_path, vec![]);
18412}
18413
18414#[gpui::test]
18415async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
18416 init_test(cx, |_| {});
18417
18418 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18419
18420 let fs = FakeFs::new(cx.executor());
18421 fs.insert_tree(
18422 path!("/a"),
18423 json!({
18424 "main.rs": sample_text,
18425 }),
18426 )
18427 .await;
18428 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18429 let (workspace, cx) =
18430 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
18431
18432 let worktree_id = workspace.update(cx, |workspace, cx| {
18433 workspace.project().update(cx, |project, cx| {
18434 project.worktrees(cx).next().unwrap().read(cx).id()
18435 })
18436 });
18437
18438 let buffer = project
18439 .update(cx, |project, cx| {
18440 project.open_buffer((worktree_id, "main.rs"), cx)
18441 })
18442 .await
18443 .unwrap();
18444
18445 let (editor, cx) = cx.add_window_view(|window, cx| {
18446 Editor::new(
18447 EditorMode::full(),
18448 MultiBuffer::build_from_buffer(buffer, cx),
18449 Some(project.clone()),
18450 window,
18451 cx,
18452 )
18453 });
18454
18455 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18456 let abs_path = project.read_with(cx, |project, cx| {
18457 project
18458 .absolute_path(&project_path, cx)
18459 .map(|path_buf| Arc::from(path_buf.to_owned()))
18460 .unwrap()
18461 });
18462
18463 editor.update_in(cx, |editor, window, cx| {
18464 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
18465 });
18466
18467 let breakpoints = editor.update(cx, |editor, cx| {
18468 editor
18469 .breakpoint_store()
18470 .as_ref()
18471 .unwrap()
18472 .read(cx)
18473 .all_breakpoints(cx)
18474 .clone()
18475 });
18476
18477 assert_breakpoint(
18478 &breakpoints,
18479 &abs_path,
18480 vec![(0, Breakpoint::new_log("hello world"))],
18481 );
18482
18483 // Removing a log message from a log breakpoint should remove it
18484 editor.update_in(cx, |editor, window, cx| {
18485 add_log_breakpoint_at_cursor(editor, "", window, cx);
18486 });
18487
18488 let breakpoints = editor.update(cx, |editor, cx| {
18489 editor
18490 .breakpoint_store()
18491 .as_ref()
18492 .unwrap()
18493 .read(cx)
18494 .all_breakpoints(cx)
18495 .clone()
18496 });
18497
18498 assert_breakpoint(&breakpoints, &abs_path, vec![]);
18499
18500 editor.update_in(cx, |editor, window, cx| {
18501 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18502 editor.move_to_end(&MoveToEnd, window, cx);
18503 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18504 // Not adding a log message to a standard breakpoint shouldn't remove it
18505 add_log_breakpoint_at_cursor(editor, "", window, cx);
18506 });
18507
18508 let breakpoints = editor.update(cx, |editor, cx| {
18509 editor
18510 .breakpoint_store()
18511 .as_ref()
18512 .unwrap()
18513 .read(cx)
18514 .all_breakpoints(cx)
18515 .clone()
18516 });
18517
18518 assert_breakpoint(
18519 &breakpoints,
18520 &abs_path,
18521 vec![
18522 (0, Breakpoint::new_standard()),
18523 (3, Breakpoint::new_standard()),
18524 ],
18525 );
18526
18527 editor.update_in(cx, |editor, window, cx| {
18528 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
18529 });
18530
18531 let breakpoints = editor.update(cx, |editor, cx| {
18532 editor
18533 .breakpoint_store()
18534 .as_ref()
18535 .unwrap()
18536 .read(cx)
18537 .all_breakpoints(cx)
18538 .clone()
18539 });
18540
18541 assert_breakpoint(
18542 &breakpoints,
18543 &abs_path,
18544 vec![
18545 (0, Breakpoint::new_standard()),
18546 (3, Breakpoint::new_log("hello world")),
18547 ],
18548 );
18549
18550 editor.update_in(cx, |editor, window, cx| {
18551 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
18552 });
18553
18554 let breakpoints = editor.update(cx, |editor, cx| {
18555 editor
18556 .breakpoint_store()
18557 .as_ref()
18558 .unwrap()
18559 .read(cx)
18560 .all_breakpoints(cx)
18561 .clone()
18562 });
18563
18564 assert_breakpoint(
18565 &breakpoints,
18566 &abs_path,
18567 vec![
18568 (0, Breakpoint::new_standard()),
18569 (3, Breakpoint::new_log("hello Earth!!")),
18570 ],
18571 );
18572}
18573
18574/// This also tests that Editor::breakpoint_at_cursor_head is working properly
18575/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
18576/// or when breakpoints were placed out of order. This tests for a regression too
18577#[gpui::test]
18578async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
18579 init_test(cx, |_| {});
18580
18581 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18582 let fs = FakeFs::new(cx.executor());
18583 fs.insert_tree(
18584 path!("/a"),
18585 json!({
18586 "main.rs": sample_text,
18587 }),
18588 )
18589 .await;
18590 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18591 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18592 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18593
18594 let fs = FakeFs::new(cx.executor());
18595 fs.insert_tree(
18596 path!("/a"),
18597 json!({
18598 "main.rs": sample_text,
18599 }),
18600 )
18601 .await;
18602 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18603 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18604 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18605 let worktree_id = workspace
18606 .update(cx, |workspace, _window, cx| {
18607 workspace.project().update(cx, |project, cx| {
18608 project.worktrees(cx).next().unwrap().read(cx).id()
18609 })
18610 })
18611 .unwrap();
18612
18613 let buffer = project
18614 .update(cx, |project, cx| {
18615 project.open_buffer((worktree_id, "main.rs"), cx)
18616 })
18617 .await
18618 .unwrap();
18619
18620 let (editor, cx) = cx.add_window_view(|window, cx| {
18621 Editor::new(
18622 EditorMode::full(),
18623 MultiBuffer::build_from_buffer(buffer, cx),
18624 Some(project.clone()),
18625 window,
18626 cx,
18627 )
18628 });
18629
18630 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18631 let abs_path = project.read_with(cx, |project, cx| {
18632 project
18633 .absolute_path(&project_path, cx)
18634 .map(|path_buf| Arc::from(path_buf.to_owned()))
18635 .unwrap()
18636 });
18637
18638 // assert we can add breakpoint on the first line
18639 editor.update_in(cx, |editor, window, cx| {
18640 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18641 editor.move_to_end(&MoveToEnd, window, cx);
18642 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18643 editor.move_up(&MoveUp, window, cx);
18644 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18645 });
18646
18647 let breakpoints = editor.update(cx, |editor, cx| {
18648 editor
18649 .breakpoint_store()
18650 .as_ref()
18651 .unwrap()
18652 .read(cx)
18653 .all_breakpoints(cx)
18654 .clone()
18655 });
18656
18657 assert_eq!(1, breakpoints.len());
18658 assert_breakpoint(
18659 &breakpoints,
18660 &abs_path,
18661 vec![
18662 (0, Breakpoint::new_standard()),
18663 (2, Breakpoint::new_standard()),
18664 (3, Breakpoint::new_standard()),
18665 ],
18666 );
18667
18668 editor.update_in(cx, |editor, window, cx| {
18669 editor.move_to_beginning(&MoveToBeginning, window, cx);
18670 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18671 editor.move_to_end(&MoveToEnd, window, cx);
18672 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18673 // Disabling a breakpoint that doesn't exist should do nothing
18674 editor.move_up(&MoveUp, window, cx);
18675 editor.move_up(&MoveUp, window, cx);
18676 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18677 });
18678
18679 let breakpoints = editor.update(cx, |editor, cx| {
18680 editor
18681 .breakpoint_store()
18682 .as_ref()
18683 .unwrap()
18684 .read(cx)
18685 .all_breakpoints(cx)
18686 .clone()
18687 });
18688
18689 let disable_breakpoint = {
18690 let mut bp = Breakpoint::new_standard();
18691 bp.state = BreakpointState::Disabled;
18692 bp
18693 };
18694
18695 assert_eq!(1, breakpoints.len());
18696 assert_breakpoint(
18697 &breakpoints,
18698 &abs_path,
18699 vec![
18700 (0, disable_breakpoint.clone()),
18701 (2, Breakpoint::new_standard()),
18702 (3, disable_breakpoint.clone()),
18703 ],
18704 );
18705
18706 editor.update_in(cx, |editor, window, cx| {
18707 editor.move_to_beginning(&MoveToBeginning, window, cx);
18708 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
18709 editor.move_to_end(&MoveToEnd, window, cx);
18710 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
18711 editor.move_up(&MoveUp, window, cx);
18712 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18713 });
18714
18715 let breakpoints = editor.update(cx, |editor, cx| {
18716 editor
18717 .breakpoint_store()
18718 .as_ref()
18719 .unwrap()
18720 .read(cx)
18721 .all_breakpoints(cx)
18722 .clone()
18723 });
18724
18725 assert_eq!(1, breakpoints.len());
18726 assert_breakpoint(
18727 &breakpoints,
18728 &abs_path,
18729 vec![
18730 (0, Breakpoint::new_standard()),
18731 (2, disable_breakpoint),
18732 (3, Breakpoint::new_standard()),
18733 ],
18734 );
18735}
18736
18737#[gpui::test]
18738async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
18739 init_test(cx, |_| {});
18740 let capabilities = lsp::ServerCapabilities {
18741 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
18742 prepare_provider: Some(true),
18743 work_done_progress_options: Default::default(),
18744 })),
18745 ..Default::default()
18746 };
18747 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
18748
18749 cx.set_state(indoc! {"
18750 struct Fˇoo {}
18751 "});
18752
18753 cx.update_editor(|editor, _, cx| {
18754 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
18755 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
18756 editor.highlight_background::<DocumentHighlightRead>(
18757 &[highlight_range],
18758 |c| c.editor_document_highlight_read_background,
18759 cx,
18760 );
18761 });
18762
18763 let mut prepare_rename_handler = cx
18764 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
18765 move |_, _, _| async move {
18766 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
18767 start: lsp::Position {
18768 line: 0,
18769 character: 7,
18770 },
18771 end: lsp::Position {
18772 line: 0,
18773 character: 10,
18774 },
18775 })))
18776 },
18777 );
18778 let prepare_rename_task = cx
18779 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
18780 .expect("Prepare rename was not started");
18781 prepare_rename_handler.next().await.unwrap();
18782 prepare_rename_task.await.expect("Prepare rename failed");
18783
18784 let mut rename_handler =
18785 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
18786 let edit = lsp::TextEdit {
18787 range: lsp::Range {
18788 start: lsp::Position {
18789 line: 0,
18790 character: 7,
18791 },
18792 end: lsp::Position {
18793 line: 0,
18794 character: 10,
18795 },
18796 },
18797 new_text: "FooRenamed".to_string(),
18798 };
18799 Ok(Some(lsp::WorkspaceEdit::new(
18800 // Specify the same edit twice
18801 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
18802 )))
18803 });
18804 let rename_task = cx
18805 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
18806 .expect("Confirm rename was not started");
18807 rename_handler.next().await.unwrap();
18808 rename_task.await.expect("Confirm rename failed");
18809 cx.run_until_parked();
18810
18811 // Despite two edits, only one is actually applied as those are identical
18812 cx.assert_editor_state(indoc! {"
18813 struct FooRenamedˇ {}
18814 "});
18815}
18816
18817#[gpui::test]
18818async fn test_rename_without_prepare(cx: &mut TestAppContext) {
18819 init_test(cx, |_| {});
18820 // These capabilities indicate that the server does not support prepare rename.
18821 let capabilities = lsp::ServerCapabilities {
18822 rename_provider: Some(lsp::OneOf::Left(true)),
18823 ..Default::default()
18824 };
18825 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
18826
18827 cx.set_state(indoc! {"
18828 struct Fˇoo {}
18829 "});
18830
18831 cx.update_editor(|editor, _window, cx| {
18832 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
18833 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
18834 editor.highlight_background::<DocumentHighlightRead>(
18835 &[highlight_range],
18836 |c| c.editor_document_highlight_read_background,
18837 cx,
18838 );
18839 });
18840
18841 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
18842 .expect("Prepare rename was not started")
18843 .await
18844 .expect("Prepare rename failed");
18845
18846 let mut rename_handler =
18847 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
18848 let edit = lsp::TextEdit {
18849 range: lsp::Range {
18850 start: lsp::Position {
18851 line: 0,
18852 character: 7,
18853 },
18854 end: lsp::Position {
18855 line: 0,
18856 character: 10,
18857 },
18858 },
18859 new_text: "FooRenamed".to_string(),
18860 };
18861 Ok(Some(lsp::WorkspaceEdit::new(
18862 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
18863 )))
18864 });
18865 let rename_task = cx
18866 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
18867 .expect("Confirm rename was not started");
18868 rename_handler.next().await.unwrap();
18869 rename_task.await.expect("Confirm rename failed");
18870 cx.run_until_parked();
18871
18872 // Correct range is renamed, as `surrounding_word` is used to find it.
18873 cx.assert_editor_state(indoc! {"
18874 struct FooRenamedˇ {}
18875 "});
18876}
18877
18878#[gpui::test]
18879async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
18880 init_test(cx, |_| {});
18881 let mut cx = EditorTestContext::new(cx).await;
18882
18883 let language = Arc::new(
18884 Language::new(
18885 LanguageConfig::default(),
18886 Some(tree_sitter_html::LANGUAGE.into()),
18887 )
18888 .with_brackets_query(
18889 r#"
18890 ("<" @open "/>" @close)
18891 ("</" @open ">" @close)
18892 ("<" @open ">" @close)
18893 ("\"" @open "\"" @close)
18894 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
18895 "#,
18896 )
18897 .unwrap(),
18898 );
18899 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
18900
18901 cx.set_state(indoc! {"
18902 <span>ˇ</span>
18903 "});
18904 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18905 cx.assert_editor_state(indoc! {"
18906 <span>
18907 ˇ
18908 </span>
18909 "});
18910
18911 cx.set_state(indoc! {"
18912 <span><span></span>ˇ</span>
18913 "});
18914 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18915 cx.assert_editor_state(indoc! {"
18916 <span><span></span>
18917 ˇ</span>
18918 "});
18919
18920 cx.set_state(indoc! {"
18921 <span>ˇ
18922 </span>
18923 "});
18924 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18925 cx.assert_editor_state(indoc! {"
18926 <span>
18927 ˇ
18928 </span>
18929 "});
18930}
18931
18932#[gpui::test(iterations = 10)]
18933async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
18934 init_test(cx, |_| {});
18935
18936 let fs = FakeFs::new(cx.executor());
18937 fs.insert_tree(
18938 path!("/dir"),
18939 json!({
18940 "a.ts": "a",
18941 }),
18942 )
18943 .await;
18944
18945 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
18946 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18947 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18948
18949 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18950 language_registry.add(Arc::new(Language::new(
18951 LanguageConfig {
18952 name: "TypeScript".into(),
18953 matcher: LanguageMatcher {
18954 path_suffixes: vec!["ts".to_string()],
18955 ..Default::default()
18956 },
18957 ..Default::default()
18958 },
18959 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18960 )));
18961 let mut fake_language_servers = language_registry.register_fake_lsp(
18962 "TypeScript",
18963 FakeLspAdapter {
18964 capabilities: lsp::ServerCapabilities {
18965 code_lens_provider: Some(lsp::CodeLensOptions {
18966 resolve_provider: Some(true),
18967 }),
18968 execute_command_provider: Some(lsp::ExecuteCommandOptions {
18969 commands: vec!["_the/command".to_string()],
18970 ..lsp::ExecuteCommandOptions::default()
18971 }),
18972 ..lsp::ServerCapabilities::default()
18973 },
18974 ..FakeLspAdapter::default()
18975 },
18976 );
18977
18978 let (buffer, _handle) = project
18979 .update(cx, |p, cx| {
18980 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
18981 })
18982 .await
18983 .unwrap();
18984 cx.executor().run_until_parked();
18985
18986 let fake_server = fake_language_servers.next().await.unwrap();
18987
18988 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
18989 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
18990 drop(buffer_snapshot);
18991 let actions = cx
18992 .update_window(*workspace, |_, window, cx| {
18993 project.code_actions(&buffer, anchor..anchor, window, cx)
18994 })
18995 .unwrap();
18996
18997 fake_server
18998 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
18999 Ok(Some(vec![
19000 lsp::CodeLens {
19001 range: lsp::Range::default(),
19002 command: Some(lsp::Command {
19003 title: "Code lens command".to_owned(),
19004 command: "_the/command".to_owned(),
19005 arguments: None,
19006 }),
19007 data: None,
19008 },
19009 lsp::CodeLens {
19010 range: lsp::Range::default(),
19011 command: Some(lsp::Command {
19012 title: "Command not in capabilities".to_owned(),
19013 command: "not in capabilities".to_owned(),
19014 arguments: None,
19015 }),
19016 data: None,
19017 },
19018 lsp::CodeLens {
19019 range: lsp::Range {
19020 start: lsp::Position {
19021 line: 1,
19022 character: 1,
19023 },
19024 end: lsp::Position {
19025 line: 1,
19026 character: 1,
19027 },
19028 },
19029 command: Some(lsp::Command {
19030 title: "Command not in range".to_owned(),
19031 command: "_the/command".to_owned(),
19032 arguments: None,
19033 }),
19034 data: None,
19035 },
19036 ]))
19037 })
19038 .next()
19039 .await;
19040
19041 let actions = actions.await.unwrap();
19042 assert_eq!(
19043 actions.len(),
19044 1,
19045 "Should have only one valid action for the 0..0 range"
19046 );
19047 let action = actions[0].clone();
19048 let apply = project.update(cx, |project, cx| {
19049 project.apply_code_action(buffer.clone(), action, true, cx)
19050 });
19051
19052 // Resolving the code action does not populate its edits. In absence of
19053 // edits, we must execute the given command.
19054 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
19055 |mut lens, _| async move {
19056 let lens_command = lens.command.as_mut().expect("should have a command");
19057 assert_eq!(lens_command.title, "Code lens command");
19058 lens_command.arguments = Some(vec![json!("the-argument")]);
19059 Ok(lens)
19060 },
19061 );
19062
19063 // While executing the command, the language server sends the editor
19064 // a `workspaceEdit` request.
19065 fake_server
19066 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
19067 let fake = fake_server.clone();
19068 move |params, _| {
19069 assert_eq!(params.command, "_the/command");
19070 let fake = fake.clone();
19071 async move {
19072 fake.server
19073 .request::<lsp::request::ApplyWorkspaceEdit>(
19074 lsp::ApplyWorkspaceEditParams {
19075 label: None,
19076 edit: lsp::WorkspaceEdit {
19077 changes: Some(
19078 [(
19079 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
19080 vec![lsp::TextEdit {
19081 range: lsp::Range::new(
19082 lsp::Position::new(0, 0),
19083 lsp::Position::new(0, 0),
19084 ),
19085 new_text: "X".into(),
19086 }],
19087 )]
19088 .into_iter()
19089 .collect(),
19090 ),
19091 ..Default::default()
19092 },
19093 },
19094 )
19095 .await
19096 .unwrap();
19097 Ok(Some(json!(null)))
19098 }
19099 }
19100 })
19101 .next()
19102 .await;
19103
19104 // Applying the code lens command returns a project transaction containing the edits
19105 // sent by the language server in its `workspaceEdit` request.
19106 let transaction = apply.await.unwrap();
19107 assert!(transaction.0.contains_key(&buffer));
19108 buffer.update(cx, |buffer, cx| {
19109 assert_eq!(buffer.text(), "Xa");
19110 buffer.undo(cx);
19111 assert_eq!(buffer.text(), "a");
19112 });
19113}
19114
19115#[gpui::test]
19116async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
19117 init_test(cx, |_| {});
19118
19119 let fs = FakeFs::new(cx.executor());
19120 let main_text = r#"fn main() {
19121println!("1");
19122println!("2");
19123println!("3");
19124println!("4");
19125println!("5");
19126}"#;
19127 let lib_text = "mod foo {}";
19128 fs.insert_tree(
19129 path!("/a"),
19130 json!({
19131 "lib.rs": lib_text,
19132 "main.rs": main_text,
19133 }),
19134 )
19135 .await;
19136
19137 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19138 let (workspace, cx) =
19139 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19140 let worktree_id = workspace.update(cx, |workspace, cx| {
19141 workspace.project().update(cx, |project, cx| {
19142 project.worktrees(cx).next().unwrap().read(cx).id()
19143 })
19144 });
19145
19146 let expected_ranges = vec![
19147 Point::new(0, 0)..Point::new(0, 0),
19148 Point::new(1, 0)..Point::new(1, 1),
19149 Point::new(2, 0)..Point::new(2, 2),
19150 Point::new(3, 0)..Point::new(3, 3),
19151 ];
19152
19153 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
19154 let editor_1 = workspace
19155 .update_in(cx, |workspace, window, cx| {
19156 workspace.open_path(
19157 (worktree_id, "main.rs"),
19158 Some(pane_1.downgrade()),
19159 true,
19160 window,
19161 cx,
19162 )
19163 })
19164 .unwrap()
19165 .await
19166 .downcast::<Editor>()
19167 .unwrap();
19168 pane_1.update(cx, |pane, cx| {
19169 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19170 open_editor.update(cx, |editor, cx| {
19171 assert_eq!(
19172 editor.display_text(cx),
19173 main_text,
19174 "Original main.rs text on initial open",
19175 );
19176 assert_eq!(
19177 editor
19178 .selections
19179 .all::<Point>(cx)
19180 .into_iter()
19181 .map(|s| s.range())
19182 .collect::<Vec<_>>(),
19183 vec![Point::zero()..Point::zero()],
19184 "Default selections on initial open",
19185 );
19186 })
19187 });
19188 editor_1.update_in(cx, |editor, window, cx| {
19189 editor.change_selections(None, window, cx, |s| {
19190 s.select_ranges(expected_ranges.clone());
19191 });
19192 });
19193
19194 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
19195 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
19196 });
19197 let editor_2 = workspace
19198 .update_in(cx, |workspace, window, cx| {
19199 workspace.open_path(
19200 (worktree_id, "main.rs"),
19201 Some(pane_2.downgrade()),
19202 true,
19203 window,
19204 cx,
19205 )
19206 })
19207 .unwrap()
19208 .await
19209 .downcast::<Editor>()
19210 .unwrap();
19211 pane_2.update(cx, |pane, cx| {
19212 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19213 open_editor.update(cx, |editor, cx| {
19214 assert_eq!(
19215 editor.display_text(cx),
19216 main_text,
19217 "Original main.rs text on initial open in another panel",
19218 );
19219 assert_eq!(
19220 editor
19221 .selections
19222 .all::<Point>(cx)
19223 .into_iter()
19224 .map(|s| s.range())
19225 .collect::<Vec<_>>(),
19226 vec![Point::zero()..Point::zero()],
19227 "Default selections on initial open in another panel",
19228 );
19229 })
19230 });
19231
19232 editor_2.update_in(cx, |editor, window, cx| {
19233 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
19234 });
19235
19236 let _other_editor_1 = workspace
19237 .update_in(cx, |workspace, window, cx| {
19238 workspace.open_path(
19239 (worktree_id, "lib.rs"),
19240 Some(pane_1.downgrade()),
19241 true,
19242 window,
19243 cx,
19244 )
19245 })
19246 .unwrap()
19247 .await
19248 .downcast::<Editor>()
19249 .unwrap();
19250 pane_1
19251 .update_in(cx, |pane, window, cx| {
19252 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
19253 .unwrap()
19254 })
19255 .await
19256 .unwrap();
19257 drop(editor_1);
19258 pane_1.update(cx, |pane, cx| {
19259 pane.active_item()
19260 .unwrap()
19261 .downcast::<Editor>()
19262 .unwrap()
19263 .update(cx, |editor, cx| {
19264 assert_eq!(
19265 editor.display_text(cx),
19266 lib_text,
19267 "Other file should be open and active",
19268 );
19269 });
19270 assert_eq!(pane.items().count(), 1, "No other editors should be open");
19271 });
19272
19273 let _other_editor_2 = workspace
19274 .update_in(cx, |workspace, window, cx| {
19275 workspace.open_path(
19276 (worktree_id, "lib.rs"),
19277 Some(pane_2.downgrade()),
19278 true,
19279 window,
19280 cx,
19281 )
19282 })
19283 .unwrap()
19284 .await
19285 .downcast::<Editor>()
19286 .unwrap();
19287 pane_2
19288 .update_in(cx, |pane, window, cx| {
19289 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
19290 .unwrap()
19291 })
19292 .await
19293 .unwrap();
19294 drop(editor_2);
19295 pane_2.update(cx, |pane, cx| {
19296 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19297 open_editor.update(cx, |editor, cx| {
19298 assert_eq!(
19299 editor.display_text(cx),
19300 lib_text,
19301 "Other file should be open and active in another panel too",
19302 );
19303 });
19304 assert_eq!(
19305 pane.items().count(),
19306 1,
19307 "No other editors should be open in another pane",
19308 );
19309 });
19310
19311 let _editor_1_reopened = workspace
19312 .update_in(cx, |workspace, window, cx| {
19313 workspace.open_path(
19314 (worktree_id, "main.rs"),
19315 Some(pane_1.downgrade()),
19316 true,
19317 window,
19318 cx,
19319 )
19320 })
19321 .unwrap()
19322 .await
19323 .downcast::<Editor>()
19324 .unwrap();
19325 let _editor_2_reopened = workspace
19326 .update_in(cx, |workspace, window, cx| {
19327 workspace.open_path(
19328 (worktree_id, "main.rs"),
19329 Some(pane_2.downgrade()),
19330 true,
19331 window,
19332 cx,
19333 )
19334 })
19335 .unwrap()
19336 .await
19337 .downcast::<Editor>()
19338 .unwrap();
19339 pane_1.update(cx, |pane, cx| {
19340 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19341 open_editor.update(cx, |editor, cx| {
19342 assert_eq!(
19343 editor.display_text(cx),
19344 main_text,
19345 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
19346 );
19347 assert_eq!(
19348 editor
19349 .selections
19350 .all::<Point>(cx)
19351 .into_iter()
19352 .map(|s| s.range())
19353 .collect::<Vec<_>>(),
19354 expected_ranges,
19355 "Previous editor in the 1st panel had selections and should get them restored on reopen",
19356 );
19357 })
19358 });
19359 pane_2.update(cx, |pane, cx| {
19360 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19361 open_editor.update(cx, |editor, cx| {
19362 assert_eq!(
19363 editor.display_text(cx),
19364 r#"fn main() {
19365⋯rintln!("1");
19366⋯intln!("2");
19367⋯ntln!("3");
19368println!("4");
19369println!("5");
19370}"#,
19371 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
19372 );
19373 assert_eq!(
19374 editor
19375 .selections
19376 .all::<Point>(cx)
19377 .into_iter()
19378 .map(|s| s.range())
19379 .collect::<Vec<_>>(),
19380 vec![Point::zero()..Point::zero()],
19381 "Previous editor in the 2nd pane had no selections changed hence should restore none",
19382 );
19383 })
19384 });
19385}
19386
19387#[gpui::test]
19388async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
19389 init_test(cx, |_| {});
19390
19391 let fs = FakeFs::new(cx.executor());
19392 let main_text = r#"fn main() {
19393println!("1");
19394println!("2");
19395println!("3");
19396println!("4");
19397println!("5");
19398}"#;
19399 let lib_text = "mod foo {}";
19400 fs.insert_tree(
19401 path!("/a"),
19402 json!({
19403 "lib.rs": lib_text,
19404 "main.rs": main_text,
19405 }),
19406 )
19407 .await;
19408
19409 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19410 let (workspace, cx) =
19411 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19412 let worktree_id = workspace.update(cx, |workspace, cx| {
19413 workspace.project().update(cx, |project, cx| {
19414 project.worktrees(cx).next().unwrap().read(cx).id()
19415 })
19416 });
19417
19418 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
19419 let editor = workspace
19420 .update_in(cx, |workspace, window, cx| {
19421 workspace.open_path(
19422 (worktree_id, "main.rs"),
19423 Some(pane.downgrade()),
19424 true,
19425 window,
19426 cx,
19427 )
19428 })
19429 .unwrap()
19430 .await
19431 .downcast::<Editor>()
19432 .unwrap();
19433 pane.update(cx, |pane, cx| {
19434 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19435 open_editor.update(cx, |editor, cx| {
19436 assert_eq!(
19437 editor.display_text(cx),
19438 main_text,
19439 "Original main.rs text on initial open",
19440 );
19441 })
19442 });
19443 editor.update_in(cx, |editor, window, cx| {
19444 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
19445 });
19446
19447 cx.update_global(|store: &mut SettingsStore, cx| {
19448 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
19449 s.restore_on_file_reopen = Some(false);
19450 });
19451 });
19452 editor.update_in(cx, |editor, window, cx| {
19453 editor.fold_ranges(
19454 vec![
19455 Point::new(1, 0)..Point::new(1, 1),
19456 Point::new(2, 0)..Point::new(2, 2),
19457 Point::new(3, 0)..Point::new(3, 3),
19458 ],
19459 false,
19460 window,
19461 cx,
19462 );
19463 });
19464 pane.update_in(cx, |pane, window, cx| {
19465 pane.close_all_items(&CloseAllItems::default(), window, cx)
19466 .unwrap()
19467 })
19468 .await
19469 .unwrap();
19470 pane.update(cx, |pane, _| {
19471 assert!(pane.active_item().is_none());
19472 });
19473 cx.update_global(|store: &mut SettingsStore, cx| {
19474 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
19475 s.restore_on_file_reopen = Some(true);
19476 });
19477 });
19478
19479 let _editor_reopened = workspace
19480 .update_in(cx, |workspace, window, cx| {
19481 workspace.open_path(
19482 (worktree_id, "main.rs"),
19483 Some(pane.downgrade()),
19484 true,
19485 window,
19486 cx,
19487 )
19488 })
19489 .unwrap()
19490 .await
19491 .downcast::<Editor>()
19492 .unwrap();
19493 pane.update(cx, |pane, cx| {
19494 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19495 open_editor.update(cx, |editor, cx| {
19496 assert_eq!(
19497 editor.display_text(cx),
19498 main_text,
19499 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
19500 );
19501 })
19502 });
19503}
19504
19505#[gpui::test]
19506async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
19507 struct EmptyModalView {
19508 focus_handle: gpui::FocusHandle,
19509 }
19510 impl EventEmitter<DismissEvent> for EmptyModalView {}
19511 impl Render for EmptyModalView {
19512 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
19513 div()
19514 }
19515 }
19516 impl Focusable for EmptyModalView {
19517 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
19518 self.focus_handle.clone()
19519 }
19520 }
19521 impl workspace::ModalView for EmptyModalView {}
19522 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
19523 EmptyModalView {
19524 focus_handle: cx.focus_handle(),
19525 }
19526 }
19527
19528 init_test(cx, |_| {});
19529
19530 let fs = FakeFs::new(cx.executor());
19531 let project = Project::test(fs, [], cx).await;
19532 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19533 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
19534 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19535 let editor = cx.new_window_entity(|window, cx| {
19536 Editor::new(
19537 EditorMode::full(),
19538 buffer,
19539 Some(project.clone()),
19540 window,
19541 cx,
19542 )
19543 });
19544 workspace
19545 .update(cx, |workspace, window, cx| {
19546 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
19547 })
19548 .unwrap();
19549 editor.update_in(cx, |editor, window, cx| {
19550 editor.open_context_menu(&OpenContextMenu, window, cx);
19551 assert!(editor.mouse_context_menu.is_some());
19552 });
19553 workspace
19554 .update(cx, |workspace, window, cx| {
19555 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
19556 })
19557 .unwrap();
19558 cx.read(|cx| {
19559 assert!(editor.read(cx).mouse_context_menu.is_none());
19560 });
19561}
19562
19563#[gpui::test]
19564async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
19565 init_test(cx, |_| {});
19566
19567 let fs = FakeFs::new(cx.executor());
19568 fs.insert_file(path!("/file.html"), Default::default())
19569 .await;
19570
19571 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
19572
19573 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19574 let html_language = Arc::new(Language::new(
19575 LanguageConfig {
19576 name: "HTML".into(),
19577 matcher: LanguageMatcher {
19578 path_suffixes: vec!["html".to_string()],
19579 ..LanguageMatcher::default()
19580 },
19581 brackets: BracketPairConfig {
19582 pairs: vec![BracketPair {
19583 start: "<".into(),
19584 end: ">".into(),
19585 close: true,
19586 ..Default::default()
19587 }],
19588 ..Default::default()
19589 },
19590 ..Default::default()
19591 },
19592 Some(tree_sitter_html::LANGUAGE.into()),
19593 ));
19594 language_registry.add(html_language);
19595 let mut fake_servers = language_registry.register_fake_lsp(
19596 "HTML",
19597 FakeLspAdapter {
19598 capabilities: lsp::ServerCapabilities {
19599 completion_provider: Some(lsp::CompletionOptions {
19600 resolve_provider: Some(true),
19601 ..Default::default()
19602 }),
19603 ..Default::default()
19604 },
19605 ..Default::default()
19606 },
19607 );
19608
19609 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19610 let cx = &mut VisualTestContext::from_window(*workspace, cx);
19611
19612 let worktree_id = workspace
19613 .update(cx, |workspace, _window, cx| {
19614 workspace.project().update(cx, |project, cx| {
19615 project.worktrees(cx).next().unwrap().read(cx).id()
19616 })
19617 })
19618 .unwrap();
19619 project
19620 .update(cx, |project, cx| {
19621 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
19622 })
19623 .await
19624 .unwrap();
19625 let editor = workspace
19626 .update(cx, |workspace, window, cx| {
19627 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
19628 })
19629 .unwrap()
19630 .await
19631 .unwrap()
19632 .downcast::<Editor>()
19633 .unwrap();
19634
19635 let fake_server = fake_servers.next().await.unwrap();
19636 editor.update_in(cx, |editor, window, cx| {
19637 editor.set_text("<ad></ad>", window, cx);
19638 editor.change_selections(None, window, cx, |selections| {
19639 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
19640 });
19641 let Some((buffer, _)) = editor
19642 .buffer
19643 .read(cx)
19644 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
19645 else {
19646 panic!("Failed to get buffer for selection position");
19647 };
19648 let buffer = buffer.read(cx);
19649 let buffer_id = buffer.remote_id();
19650 let opening_range =
19651 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
19652 let closing_range =
19653 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
19654 let mut linked_ranges = HashMap::default();
19655 linked_ranges.insert(
19656 buffer_id,
19657 vec![(opening_range.clone(), vec![closing_range.clone()])],
19658 );
19659 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
19660 });
19661 let mut completion_handle =
19662 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
19663 Ok(Some(lsp::CompletionResponse::Array(vec![
19664 lsp::CompletionItem {
19665 label: "head".to_string(),
19666 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
19667 lsp::InsertReplaceEdit {
19668 new_text: "head".to_string(),
19669 insert: lsp::Range::new(
19670 lsp::Position::new(0, 1),
19671 lsp::Position::new(0, 3),
19672 ),
19673 replace: lsp::Range::new(
19674 lsp::Position::new(0, 1),
19675 lsp::Position::new(0, 3),
19676 ),
19677 },
19678 )),
19679 ..Default::default()
19680 },
19681 ])))
19682 });
19683 editor.update_in(cx, |editor, window, cx| {
19684 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
19685 });
19686 cx.run_until_parked();
19687 completion_handle.next().await.unwrap();
19688 editor.update(cx, |editor, _| {
19689 assert!(
19690 editor.context_menu_visible(),
19691 "Completion menu should be visible"
19692 );
19693 });
19694 editor.update_in(cx, |editor, window, cx| {
19695 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
19696 });
19697 cx.executor().run_until_parked();
19698 editor.update(cx, |editor, cx| {
19699 assert_eq!(editor.text(cx), "<head></head>");
19700 });
19701}
19702
19703fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
19704 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
19705 point..point
19706}
19707
19708fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
19709 let (text, ranges) = marked_text_ranges(marked_text, true);
19710 assert_eq!(editor.text(cx), text);
19711 assert_eq!(
19712 editor.selections.ranges(cx),
19713 ranges,
19714 "Assert selections are {}",
19715 marked_text
19716 );
19717}
19718
19719pub fn handle_signature_help_request(
19720 cx: &mut EditorLspTestContext,
19721 mocked_response: lsp::SignatureHelp,
19722) -> impl Future<Output = ()> + use<> {
19723 let mut request =
19724 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
19725 let mocked_response = mocked_response.clone();
19726 async move { Ok(Some(mocked_response)) }
19727 });
19728
19729 async move {
19730 request.next().await;
19731 }
19732}
19733
19734/// Handle completion request passing a marked string specifying where the completion
19735/// should be triggered from using '|' character, what range should be replaced, and what completions
19736/// should be returned using '<' and '>' to delimit the range.
19737///
19738/// Also see `handle_completion_request_with_insert_and_replace`.
19739#[track_caller]
19740pub fn handle_completion_request(
19741 cx: &mut EditorLspTestContext,
19742 marked_string: &str,
19743 completions: Vec<&'static str>,
19744 counter: Arc<AtomicUsize>,
19745) -> impl Future<Output = ()> {
19746 let complete_from_marker: TextRangeMarker = '|'.into();
19747 let replace_range_marker: TextRangeMarker = ('<', '>').into();
19748 let (_, mut marked_ranges) = marked_text_ranges_by(
19749 marked_string,
19750 vec![complete_from_marker.clone(), replace_range_marker.clone()],
19751 );
19752
19753 let complete_from_position =
19754 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
19755 let replace_range =
19756 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
19757
19758 let mut request =
19759 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
19760 let completions = completions.clone();
19761 counter.fetch_add(1, atomic::Ordering::Release);
19762 async move {
19763 assert_eq!(params.text_document_position.text_document.uri, url.clone());
19764 assert_eq!(
19765 params.text_document_position.position,
19766 complete_from_position
19767 );
19768 Ok(Some(lsp::CompletionResponse::Array(
19769 completions
19770 .iter()
19771 .map(|completion_text| lsp::CompletionItem {
19772 label: completion_text.to_string(),
19773 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
19774 range: replace_range,
19775 new_text: completion_text.to_string(),
19776 })),
19777 ..Default::default()
19778 })
19779 .collect(),
19780 )))
19781 }
19782 });
19783
19784 async move {
19785 request.next().await;
19786 }
19787}
19788
19789/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
19790/// given instead, which also contains an `insert` range.
19791///
19792/// This function uses the cursor position to mimic what Rust-Analyzer provides as the `insert` range,
19793/// that is, `replace_range.start..cursor_pos`.
19794pub fn handle_completion_request_with_insert_and_replace(
19795 cx: &mut EditorLspTestContext,
19796 marked_string: &str,
19797 completions: Vec<&'static str>,
19798 counter: Arc<AtomicUsize>,
19799) -> impl Future<Output = ()> {
19800 let complete_from_marker: TextRangeMarker = '|'.into();
19801 let replace_range_marker: TextRangeMarker = ('<', '>').into();
19802 let (_, mut marked_ranges) = marked_text_ranges_by(
19803 marked_string,
19804 vec![complete_from_marker.clone(), replace_range_marker.clone()],
19805 );
19806
19807 let complete_from_position =
19808 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
19809 let replace_range =
19810 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
19811
19812 let mut request =
19813 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
19814 let completions = completions.clone();
19815 counter.fetch_add(1, atomic::Ordering::Release);
19816 async move {
19817 assert_eq!(params.text_document_position.text_document.uri, url.clone());
19818 assert_eq!(
19819 params.text_document_position.position, complete_from_position,
19820 "marker `|` position doesn't match",
19821 );
19822 Ok(Some(lsp::CompletionResponse::Array(
19823 completions
19824 .iter()
19825 .map(|completion_text| lsp::CompletionItem {
19826 label: completion_text.to_string(),
19827 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
19828 lsp::InsertReplaceEdit {
19829 insert: lsp::Range {
19830 start: replace_range.start,
19831 end: complete_from_position,
19832 },
19833 replace: replace_range,
19834 new_text: completion_text.to_string(),
19835 },
19836 )),
19837 ..Default::default()
19838 })
19839 .collect(),
19840 )))
19841 }
19842 });
19843
19844 async move {
19845 request.next().await;
19846 }
19847}
19848
19849fn handle_resolve_completion_request(
19850 cx: &mut EditorLspTestContext,
19851 edits: Option<Vec<(&'static str, &'static str)>>,
19852) -> impl Future<Output = ()> {
19853 let edits = edits.map(|edits| {
19854 edits
19855 .iter()
19856 .map(|(marked_string, new_text)| {
19857 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
19858 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
19859 lsp::TextEdit::new(replace_range, new_text.to_string())
19860 })
19861 .collect::<Vec<_>>()
19862 });
19863
19864 let mut request =
19865 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
19866 let edits = edits.clone();
19867 async move {
19868 Ok(lsp::CompletionItem {
19869 additional_text_edits: edits,
19870 ..Default::default()
19871 })
19872 }
19873 });
19874
19875 async move {
19876 request.next().await;
19877 }
19878}
19879
19880pub(crate) fn update_test_language_settings(
19881 cx: &mut TestAppContext,
19882 f: impl Fn(&mut AllLanguageSettingsContent),
19883) {
19884 cx.update(|cx| {
19885 SettingsStore::update_global(cx, |store, cx| {
19886 store.update_user_settings::<AllLanguageSettings>(cx, f);
19887 });
19888 });
19889}
19890
19891pub(crate) fn update_test_project_settings(
19892 cx: &mut TestAppContext,
19893 f: impl Fn(&mut ProjectSettings),
19894) {
19895 cx.update(|cx| {
19896 SettingsStore::update_global(cx, |store, cx| {
19897 store.update_user_settings::<ProjectSettings>(cx, f);
19898 });
19899 });
19900}
19901
19902pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
19903 cx.update(|cx| {
19904 assets::Assets.load_test_fonts(cx);
19905 let store = SettingsStore::test(cx);
19906 cx.set_global(store);
19907 theme::init(theme::LoadThemes::JustBase, cx);
19908 release_channel::init(SemanticVersion::default(), cx);
19909 client::init_settings(cx);
19910 language::init(cx);
19911 Project::init_settings(cx);
19912 workspace::init_settings(cx);
19913 crate::init(cx);
19914 });
19915
19916 update_test_language_settings(cx, f);
19917}
19918
19919#[track_caller]
19920fn assert_hunk_revert(
19921 not_reverted_text_with_selections: &str,
19922 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
19923 expected_reverted_text_with_selections: &str,
19924 base_text: &str,
19925 cx: &mut EditorLspTestContext,
19926) {
19927 cx.set_state(not_reverted_text_with_selections);
19928 cx.set_head_text(base_text);
19929 cx.executor().run_until_parked();
19930
19931 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
19932 let snapshot = editor.snapshot(window, cx);
19933 let reverted_hunk_statuses = snapshot
19934 .buffer_snapshot
19935 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
19936 .map(|hunk| hunk.status().kind)
19937 .collect::<Vec<_>>();
19938
19939 editor.git_restore(&Default::default(), window, cx);
19940 reverted_hunk_statuses
19941 });
19942 cx.executor().run_until_parked();
19943 cx.assert_editor_state(expected_reverted_text_with_selections);
19944 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
19945}