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 render_in_minimap: true,
4331 }],
4332 Some(Autoscroll::fit()),
4333 cx,
4334 );
4335 editor.change_selections(None, window, cx, |s| {
4336 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4337 });
4338 editor.move_line_down(&MoveLineDown, window, cx);
4339 });
4340}
4341
4342#[gpui::test]
4343async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4344 init_test(cx, |_| {});
4345
4346 let mut cx = EditorTestContext::new(cx).await;
4347 cx.set_state(
4348 &"
4349 ˇzero
4350 one
4351 two
4352 three
4353 four
4354 five
4355 "
4356 .unindent(),
4357 );
4358
4359 // Create a four-line block that replaces three lines of text.
4360 cx.update_editor(|editor, window, cx| {
4361 let snapshot = editor.snapshot(window, cx);
4362 let snapshot = &snapshot.buffer_snapshot;
4363 let placement = BlockPlacement::Replace(
4364 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4365 );
4366 editor.insert_blocks(
4367 [BlockProperties {
4368 placement,
4369 height: Some(4),
4370 style: BlockStyle::Sticky,
4371 render: Arc::new(|_| gpui::div().into_any_element()),
4372 priority: 0,
4373 render_in_minimap: true,
4374 }],
4375 None,
4376 cx,
4377 );
4378 });
4379
4380 // Move down so that the cursor touches the block.
4381 cx.update_editor(|editor, window, cx| {
4382 editor.move_down(&Default::default(), window, cx);
4383 });
4384 cx.assert_editor_state(
4385 &"
4386 zero
4387 «one
4388 two
4389 threeˇ»
4390 four
4391 five
4392 "
4393 .unindent(),
4394 );
4395
4396 // Move down past the block.
4397 cx.update_editor(|editor, window, cx| {
4398 editor.move_down(&Default::default(), window, cx);
4399 });
4400 cx.assert_editor_state(
4401 &"
4402 zero
4403 one
4404 two
4405 three
4406 ˇfour
4407 five
4408 "
4409 .unindent(),
4410 );
4411}
4412
4413#[gpui::test]
4414fn test_transpose(cx: &mut TestAppContext) {
4415 init_test(cx, |_| {});
4416
4417 _ = cx.add_window(|window, cx| {
4418 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4419 editor.set_style(EditorStyle::default(), window, cx);
4420 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4421 editor.transpose(&Default::default(), window, cx);
4422 assert_eq!(editor.text(cx), "bac");
4423 assert_eq!(editor.selections.ranges(cx), [2..2]);
4424
4425 editor.transpose(&Default::default(), window, cx);
4426 assert_eq!(editor.text(cx), "bca");
4427 assert_eq!(editor.selections.ranges(cx), [3..3]);
4428
4429 editor.transpose(&Default::default(), window, cx);
4430 assert_eq!(editor.text(cx), "bac");
4431 assert_eq!(editor.selections.ranges(cx), [3..3]);
4432
4433 editor
4434 });
4435
4436 _ = cx.add_window(|window, cx| {
4437 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4438 editor.set_style(EditorStyle::default(), window, cx);
4439 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4440 editor.transpose(&Default::default(), window, cx);
4441 assert_eq!(editor.text(cx), "acb\nde");
4442 assert_eq!(editor.selections.ranges(cx), [3..3]);
4443
4444 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4445 editor.transpose(&Default::default(), window, cx);
4446 assert_eq!(editor.text(cx), "acbd\ne");
4447 assert_eq!(editor.selections.ranges(cx), [5..5]);
4448
4449 editor.transpose(&Default::default(), window, cx);
4450 assert_eq!(editor.text(cx), "acbde\n");
4451 assert_eq!(editor.selections.ranges(cx), [6..6]);
4452
4453 editor.transpose(&Default::default(), window, cx);
4454 assert_eq!(editor.text(cx), "acbd\ne");
4455 assert_eq!(editor.selections.ranges(cx), [6..6]);
4456
4457 editor
4458 });
4459
4460 _ = cx.add_window(|window, cx| {
4461 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4462 editor.set_style(EditorStyle::default(), window, cx);
4463 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4464 editor.transpose(&Default::default(), window, cx);
4465 assert_eq!(editor.text(cx), "bacd\ne");
4466 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4467
4468 editor.transpose(&Default::default(), window, cx);
4469 assert_eq!(editor.text(cx), "bcade\n");
4470 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4471
4472 editor.transpose(&Default::default(), window, cx);
4473 assert_eq!(editor.text(cx), "bcda\ne");
4474 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4475
4476 editor.transpose(&Default::default(), window, cx);
4477 assert_eq!(editor.text(cx), "bcade\n");
4478 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4479
4480 editor.transpose(&Default::default(), window, cx);
4481 assert_eq!(editor.text(cx), "bcaed\n");
4482 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4483
4484 editor
4485 });
4486
4487 _ = cx.add_window(|window, cx| {
4488 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4489 editor.set_style(EditorStyle::default(), window, cx);
4490 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4491 editor.transpose(&Default::default(), window, cx);
4492 assert_eq!(editor.text(cx), "🏀🍐✋");
4493 assert_eq!(editor.selections.ranges(cx), [8..8]);
4494
4495 editor.transpose(&Default::default(), window, cx);
4496 assert_eq!(editor.text(cx), "🏀✋🍐");
4497 assert_eq!(editor.selections.ranges(cx), [11..11]);
4498
4499 editor.transpose(&Default::default(), window, cx);
4500 assert_eq!(editor.text(cx), "🏀🍐✋");
4501 assert_eq!(editor.selections.ranges(cx), [11..11]);
4502
4503 editor
4504 });
4505}
4506
4507#[gpui::test]
4508async fn test_rewrap(cx: &mut TestAppContext) {
4509 init_test(cx, |settings| {
4510 settings.languages.extend([
4511 (
4512 "Markdown".into(),
4513 LanguageSettingsContent {
4514 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4515 ..Default::default()
4516 },
4517 ),
4518 (
4519 "Plain Text".into(),
4520 LanguageSettingsContent {
4521 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4522 ..Default::default()
4523 },
4524 ),
4525 ])
4526 });
4527
4528 let mut cx = EditorTestContext::new(cx).await;
4529
4530 let language_with_c_comments = Arc::new(Language::new(
4531 LanguageConfig {
4532 line_comments: vec!["// ".into()],
4533 ..LanguageConfig::default()
4534 },
4535 None,
4536 ));
4537 let language_with_pound_comments = Arc::new(Language::new(
4538 LanguageConfig {
4539 line_comments: vec!["# ".into()],
4540 ..LanguageConfig::default()
4541 },
4542 None,
4543 ));
4544 let markdown_language = Arc::new(Language::new(
4545 LanguageConfig {
4546 name: "Markdown".into(),
4547 ..LanguageConfig::default()
4548 },
4549 None,
4550 ));
4551 let language_with_doc_comments = Arc::new(Language::new(
4552 LanguageConfig {
4553 line_comments: vec!["// ".into(), "/// ".into()],
4554 ..LanguageConfig::default()
4555 },
4556 Some(tree_sitter_rust::LANGUAGE.into()),
4557 ));
4558
4559 let plaintext_language = Arc::new(Language::new(
4560 LanguageConfig {
4561 name: "Plain Text".into(),
4562 ..LanguageConfig::default()
4563 },
4564 None,
4565 ));
4566
4567 assert_rewrap(
4568 indoc! {"
4569 // ˇ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.
4570 "},
4571 indoc! {"
4572 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4573 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4574 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4575 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4576 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4577 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4578 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4579 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4580 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4581 // porttitor id. Aliquam id accumsan eros.
4582 "},
4583 language_with_c_comments.clone(),
4584 &mut cx,
4585 );
4586
4587 // Test that rewrapping works inside of a selection
4588 assert_rewrap(
4589 indoc! {"
4590 «// 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.ˇ»
4591 "},
4592 indoc! {"
4593 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4594 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4595 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4596 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4597 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4598 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4599 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4600 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4601 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4602 // porttitor id. Aliquam id accumsan eros.ˇ»
4603 "},
4604 language_with_c_comments.clone(),
4605 &mut cx,
4606 );
4607
4608 // Test that cursors that expand to the same region are collapsed.
4609 assert_rewrap(
4610 indoc! {"
4611 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4612 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4613 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4614 // ˇ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.
4615 "},
4616 indoc! {"
4617 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4618 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4619 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
4620 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4621 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
4622 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4623 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4624 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4625 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4626 // porttitor id. Aliquam id accumsan eros.
4627 "},
4628 language_with_c_comments.clone(),
4629 &mut cx,
4630 );
4631
4632 // Test that non-contiguous selections are treated separately.
4633 assert_rewrap(
4634 indoc! {"
4635 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4636 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4637 //
4638 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4639 // ˇ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.
4640 "},
4641 indoc! {"
4642 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4643 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4644 // auctor, eu lacinia sapien scelerisque.
4645 //
4646 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
4647 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4648 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
4649 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
4650 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
4651 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
4652 // vulputate turpis porttitor id. Aliquam id accumsan eros.
4653 "},
4654 language_with_c_comments.clone(),
4655 &mut cx,
4656 );
4657
4658 // Test that different comment prefixes are supported.
4659 assert_rewrap(
4660 indoc! {"
4661 # ˇ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.
4662 "},
4663 indoc! {"
4664 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4665 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4666 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4667 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4668 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
4669 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
4670 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
4671 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
4672 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
4673 # accumsan eros.
4674 "},
4675 language_with_pound_comments.clone(),
4676 &mut cx,
4677 );
4678
4679 // Test that rewrapping is ignored outside of comments in most languages.
4680 assert_rewrap(
4681 indoc! {"
4682 /// Adds two numbers.
4683 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4684 fn add(a: u32, b: u32) -> u32 {
4685 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ˇ
4686 }
4687 "},
4688 indoc! {"
4689 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
4690 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4691 fn add(a: u32, b: u32) -> u32 {
4692 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ˇ
4693 }
4694 "},
4695 language_with_doc_comments.clone(),
4696 &mut cx,
4697 );
4698
4699 // Test that rewrapping works in Markdown and Plain Text languages.
4700 assert_rewrap(
4701 indoc! {"
4702 # Hello
4703
4704 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.
4705 "},
4706 indoc! {"
4707 # Hello
4708
4709 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4710 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4711 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4712 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4713 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4714 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4715 Integer sit amet scelerisque nisi.
4716 "},
4717 markdown_language,
4718 &mut cx,
4719 );
4720
4721 assert_rewrap(
4722 indoc! {"
4723 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.
4724 "},
4725 indoc! {"
4726 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4727 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4728 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4729 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4730 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4731 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4732 Integer sit amet scelerisque nisi.
4733 "},
4734 plaintext_language,
4735 &mut cx,
4736 );
4737
4738 // Test rewrapping unaligned comments in a selection.
4739 assert_rewrap(
4740 indoc! {"
4741 fn foo() {
4742 if true {
4743 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4744 // Praesent semper egestas tellus id dignissim.ˇ»
4745 do_something();
4746 } else {
4747 //
4748 }
4749 }
4750 "},
4751 indoc! {"
4752 fn foo() {
4753 if true {
4754 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4755 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4756 // egestas tellus id dignissim.ˇ»
4757 do_something();
4758 } else {
4759 //
4760 }
4761 }
4762 "},
4763 language_with_doc_comments.clone(),
4764 &mut cx,
4765 );
4766
4767 assert_rewrap(
4768 indoc! {"
4769 fn foo() {
4770 if true {
4771 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4772 // Praesent semper egestas tellus id dignissim.»
4773 do_something();
4774 } else {
4775 //
4776 }
4777
4778 }
4779 "},
4780 indoc! {"
4781 fn foo() {
4782 if true {
4783 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4784 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4785 // egestas tellus id dignissim.»
4786 do_something();
4787 } else {
4788 //
4789 }
4790
4791 }
4792 "},
4793 language_with_doc_comments.clone(),
4794 &mut cx,
4795 );
4796
4797 #[track_caller]
4798 fn assert_rewrap(
4799 unwrapped_text: &str,
4800 wrapped_text: &str,
4801 language: Arc<Language>,
4802 cx: &mut EditorTestContext,
4803 ) {
4804 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4805 cx.set_state(unwrapped_text);
4806 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
4807 cx.assert_editor_state(wrapped_text);
4808 }
4809}
4810
4811#[gpui::test]
4812async fn test_hard_wrap(cx: &mut TestAppContext) {
4813 init_test(cx, |_| {});
4814 let mut cx = EditorTestContext::new(cx).await;
4815
4816 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
4817 cx.update_editor(|editor, _, cx| {
4818 editor.set_hard_wrap(Some(14), cx);
4819 });
4820
4821 cx.set_state(indoc!(
4822 "
4823 one two three ˇ
4824 "
4825 ));
4826 cx.simulate_input("four");
4827 cx.run_until_parked();
4828
4829 cx.assert_editor_state(indoc!(
4830 "
4831 one two three
4832 fourˇ
4833 "
4834 ));
4835
4836 cx.update_editor(|editor, window, cx| {
4837 editor.newline(&Default::default(), window, cx);
4838 });
4839 cx.run_until_parked();
4840 cx.assert_editor_state(indoc!(
4841 "
4842 one two three
4843 four
4844 ˇ
4845 "
4846 ));
4847
4848 cx.simulate_input("five");
4849 cx.run_until_parked();
4850 cx.assert_editor_state(indoc!(
4851 "
4852 one two three
4853 four
4854 fiveˇ
4855 "
4856 ));
4857
4858 cx.update_editor(|editor, window, cx| {
4859 editor.newline(&Default::default(), window, cx);
4860 });
4861 cx.run_until_parked();
4862 cx.simulate_input("# ");
4863 cx.run_until_parked();
4864 cx.assert_editor_state(indoc!(
4865 "
4866 one two three
4867 four
4868 five
4869 # ˇ
4870 "
4871 ));
4872
4873 cx.update_editor(|editor, window, cx| {
4874 editor.newline(&Default::default(), window, cx);
4875 });
4876 cx.run_until_parked();
4877 cx.assert_editor_state(indoc!(
4878 "
4879 one two three
4880 four
4881 five
4882 #\x20
4883 #ˇ
4884 "
4885 ));
4886
4887 cx.simulate_input(" 6");
4888 cx.run_until_parked();
4889 cx.assert_editor_state(indoc!(
4890 "
4891 one two three
4892 four
4893 five
4894 #
4895 # 6ˇ
4896 "
4897 ));
4898}
4899
4900#[gpui::test]
4901async fn test_clipboard(cx: &mut TestAppContext) {
4902 init_test(cx, |_| {});
4903
4904 let mut cx = EditorTestContext::new(cx).await;
4905
4906 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
4907 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4908 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
4909
4910 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
4911 cx.set_state("two ˇfour ˇsix ˇ");
4912 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4913 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
4914
4915 // Paste again but with only two cursors. Since the number of cursors doesn't
4916 // match the number of slices in the clipboard, the entire clipboard text
4917 // is pasted at each cursor.
4918 cx.set_state("ˇtwo one✅ four three six five ˇ");
4919 cx.update_editor(|e, window, cx| {
4920 e.handle_input("( ", window, cx);
4921 e.paste(&Paste, window, cx);
4922 e.handle_input(") ", window, cx);
4923 });
4924 cx.assert_editor_state(
4925 &([
4926 "( one✅ ",
4927 "three ",
4928 "five ) ˇtwo one✅ four three six five ( one✅ ",
4929 "three ",
4930 "five ) ˇ",
4931 ]
4932 .join("\n")),
4933 );
4934
4935 // Cut with three selections, one of which is full-line.
4936 cx.set_state(indoc! {"
4937 1«2ˇ»3
4938 4ˇ567
4939 «8ˇ»9"});
4940 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4941 cx.assert_editor_state(indoc! {"
4942 1ˇ3
4943 ˇ9"});
4944
4945 // Paste with three selections, noticing how the copied selection that was full-line
4946 // gets inserted before the second cursor.
4947 cx.set_state(indoc! {"
4948 1ˇ3
4949 9ˇ
4950 «oˇ»ne"});
4951 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4952 cx.assert_editor_state(indoc! {"
4953 12ˇ3
4954 4567
4955 9ˇ
4956 8ˇne"});
4957
4958 // Copy with a single cursor only, which writes the whole line into the clipboard.
4959 cx.set_state(indoc! {"
4960 The quick brown
4961 fox juˇmps over
4962 the lazy dog"});
4963 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4964 assert_eq!(
4965 cx.read_from_clipboard()
4966 .and_then(|item| item.text().as_deref().map(str::to_string)),
4967 Some("fox jumps over\n".to_string())
4968 );
4969
4970 // Paste with three selections, noticing how the copied full-line selection is inserted
4971 // before the empty selections but replaces the selection that is non-empty.
4972 cx.set_state(indoc! {"
4973 Tˇhe quick brown
4974 «foˇ»x jumps over
4975 tˇhe lazy dog"});
4976 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4977 cx.assert_editor_state(indoc! {"
4978 fox jumps over
4979 Tˇhe quick brown
4980 fox jumps over
4981 ˇx jumps over
4982 fox jumps over
4983 tˇhe lazy dog"});
4984}
4985
4986#[gpui::test]
4987async fn test_copy_trim(cx: &mut TestAppContext) {
4988 init_test(cx, |_| {});
4989
4990 let mut cx = EditorTestContext::new(cx).await;
4991 cx.set_state(
4992 r#" «for selection in selections.iter() {
4993 let mut start = selection.start;
4994 let mut end = selection.end;
4995 let is_entire_line = selection.is_empty();
4996 if is_entire_line {
4997 start = Point::new(start.row, 0);ˇ»
4998 end = cmp::min(max_point, Point::new(end.row + 1, 0));
4999 }
5000 "#,
5001 );
5002 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5003 assert_eq!(
5004 cx.read_from_clipboard()
5005 .and_then(|item| item.text().as_deref().map(str::to_string)),
5006 Some(
5007 "for selection in selections.iter() {
5008 let mut start = selection.start;
5009 let mut end = selection.end;
5010 let is_entire_line = selection.is_empty();
5011 if is_entire_line {
5012 start = Point::new(start.row, 0);"
5013 .to_string()
5014 ),
5015 "Regular copying preserves all indentation selected",
5016 );
5017 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5018 assert_eq!(
5019 cx.read_from_clipboard()
5020 .and_then(|item| item.text().as_deref().map(str::to_string)),
5021 Some(
5022 "for selection in selections.iter() {
5023let mut start = selection.start;
5024let mut end = selection.end;
5025let is_entire_line = selection.is_empty();
5026if is_entire_line {
5027 start = Point::new(start.row, 0);"
5028 .to_string()
5029 ),
5030 "Copying with stripping should strip all leading whitespaces"
5031 );
5032
5033 cx.set_state(
5034 r#" « for selection in selections.iter() {
5035 let mut start = selection.start;
5036 let mut end = selection.end;
5037 let is_entire_line = selection.is_empty();
5038 if is_entire_line {
5039 start = Point::new(start.row, 0);ˇ»
5040 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5041 }
5042 "#,
5043 );
5044 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5045 assert_eq!(
5046 cx.read_from_clipboard()
5047 .and_then(|item| item.text().as_deref().map(str::to_string)),
5048 Some(
5049 " for selection in selections.iter() {
5050 let mut start = selection.start;
5051 let mut end = selection.end;
5052 let is_entire_line = selection.is_empty();
5053 if is_entire_line {
5054 start = Point::new(start.row, 0);"
5055 .to_string()
5056 ),
5057 "Regular copying preserves all indentation selected",
5058 );
5059 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5060 assert_eq!(
5061 cx.read_from_clipboard()
5062 .and_then(|item| item.text().as_deref().map(str::to_string)),
5063 Some(
5064 "for selection in selections.iter() {
5065let mut start = selection.start;
5066let mut end = selection.end;
5067let is_entire_line = selection.is_empty();
5068if is_entire_line {
5069 start = Point::new(start.row, 0);"
5070 .to_string()
5071 ),
5072 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5073 );
5074
5075 cx.set_state(
5076 r#" «ˇ for selection in selections.iter() {
5077 let mut start = selection.start;
5078 let mut end = selection.end;
5079 let is_entire_line = selection.is_empty();
5080 if is_entire_line {
5081 start = Point::new(start.row, 0);»
5082 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5083 }
5084 "#,
5085 );
5086 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5087 assert_eq!(
5088 cx.read_from_clipboard()
5089 .and_then(|item| item.text().as_deref().map(str::to_string)),
5090 Some(
5091 " for selection in selections.iter() {
5092 let mut start = selection.start;
5093 let mut end = selection.end;
5094 let is_entire_line = selection.is_empty();
5095 if is_entire_line {
5096 start = Point::new(start.row, 0);"
5097 .to_string()
5098 ),
5099 "Regular copying for reverse selection works the same",
5100 );
5101 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5102 assert_eq!(
5103 cx.read_from_clipboard()
5104 .and_then(|item| item.text().as_deref().map(str::to_string)),
5105 Some(
5106 "for selection in selections.iter() {
5107let mut start = selection.start;
5108let mut end = selection.end;
5109let is_entire_line = selection.is_empty();
5110if is_entire_line {
5111 start = Point::new(start.row, 0);"
5112 .to_string()
5113 ),
5114 "Copying with stripping for reverse selection works the same"
5115 );
5116
5117 cx.set_state(
5118 r#" for selection «in selections.iter() {
5119 let mut start = selection.start;
5120 let mut end = selection.end;
5121 let is_entire_line = selection.is_empty();
5122 if is_entire_line {
5123 start = Point::new(start.row, 0);ˇ»
5124 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5125 }
5126 "#,
5127 );
5128 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5129 assert_eq!(
5130 cx.read_from_clipboard()
5131 .and_then(|item| item.text().as_deref().map(str::to_string)),
5132 Some(
5133 "in selections.iter() {
5134 let mut start = selection.start;
5135 let mut end = selection.end;
5136 let is_entire_line = selection.is_empty();
5137 if is_entire_line {
5138 start = Point::new(start.row, 0);"
5139 .to_string()
5140 ),
5141 "When selecting past the indent, the copying works as usual",
5142 );
5143 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5144 assert_eq!(
5145 cx.read_from_clipboard()
5146 .and_then(|item| item.text().as_deref().map(str::to_string)),
5147 Some(
5148 "in selections.iter() {
5149 let mut start = selection.start;
5150 let mut end = selection.end;
5151 let is_entire_line = selection.is_empty();
5152 if is_entire_line {
5153 start = Point::new(start.row, 0);"
5154 .to_string()
5155 ),
5156 "When selecting past the indent, nothing is trimmed"
5157 );
5158
5159 cx.set_state(
5160 r#" «for selection in selections.iter() {
5161 let mut start = selection.start;
5162
5163 let mut end = selection.end;
5164 let is_entire_line = selection.is_empty();
5165 if is_entire_line {
5166 start = Point::new(start.row, 0);
5167ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
5168 }
5169 "#,
5170 );
5171 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5172 assert_eq!(
5173 cx.read_from_clipboard()
5174 .and_then(|item| item.text().as_deref().map(str::to_string)),
5175 Some(
5176 "for selection in selections.iter() {
5177let mut start = selection.start;
5178
5179let mut end = selection.end;
5180let is_entire_line = selection.is_empty();
5181if is_entire_line {
5182 start = Point::new(start.row, 0);
5183"
5184 .to_string()
5185 ),
5186 "Copying with stripping should ignore empty lines"
5187 );
5188}
5189
5190#[gpui::test]
5191async fn test_paste_multiline(cx: &mut TestAppContext) {
5192 init_test(cx, |_| {});
5193
5194 let mut cx = EditorTestContext::new(cx).await;
5195 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5196
5197 // Cut an indented block, without the leading whitespace.
5198 cx.set_state(indoc! {"
5199 const a: B = (
5200 c(),
5201 «d(
5202 e,
5203 f
5204 )ˇ»
5205 );
5206 "});
5207 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5208 cx.assert_editor_state(indoc! {"
5209 const a: B = (
5210 c(),
5211 ˇ
5212 );
5213 "});
5214
5215 // Paste it at the same position.
5216 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5217 cx.assert_editor_state(indoc! {"
5218 const a: B = (
5219 c(),
5220 d(
5221 e,
5222 f
5223 )ˇ
5224 );
5225 "});
5226
5227 // Paste it at a line with a lower indent level.
5228 cx.set_state(indoc! {"
5229 ˇ
5230 const a: B = (
5231 c(),
5232 );
5233 "});
5234 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5235 cx.assert_editor_state(indoc! {"
5236 d(
5237 e,
5238 f
5239 )ˇ
5240 const a: B = (
5241 c(),
5242 );
5243 "});
5244
5245 // Cut an indented block, with the leading whitespace.
5246 cx.set_state(indoc! {"
5247 const a: B = (
5248 c(),
5249 « d(
5250 e,
5251 f
5252 )
5253 ˇ»);
5254 "});
5255 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5256 cx.assert_editor_state(indoc! {"
5257 const a: B = (
5258 c(),
5259 ˇ);
5260 "});
5261
5262 // Paste it at the same position.
5263 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5264 cx.assert_editor_state(indoc! {"
5265 const a: B = (
5266 c(),
5267 d(
5268 e,
5269 f
5270 )
5271 ˇ);
5272 "});
5273
5274 // Paste it at a line with a higher indent level.
5275 cx.set_state(indoc! {"
5276 const a: B = (
5277 c(),
5278 d(
5279 e,
5280 fˇ
5281 )
5282 );
5283 "});
5284 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5285 cx.assert_editor_state(indoc! {"
5286 const a: B = (
5287 c(),
5288 d(
5289 e,
5290 f d(
5291 e,
5292 f
5293 )
5294 ˇ
5295 )
5296 );
5297 "});
5298
5299 // Copy an indented block, starting mid-line
5300 cx.set_state(indoc! {"
5301 const a: B = (
5302 c(),
5303 somethin«g(
5304 e,
5305 f
5306 )ˇ»
5307 );
5308 "});
5309 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5310
5311 // Paste it on a line with a lower indent level
5312 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
5313 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5314 cx.assert_editor_state(indoc! {"
5315 const a: B = (
5316 c(),
5317 something(
5318 e,
5319 f
5320 )
5321 );
5322 g(
5323 e,
5324 f
5325 )ˇ"});
5326}
5327
5328#[gpui::test]
5329async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
5330 init_test(cx, |_| {});
5331
5332 cx.write_to_clipboard(ClipboardItem::new_string(
5333 " d(\n e\n );\n".into(),
5334 ));
5335
5336 let mut cx = EditorTestContext::new(cx).await;
5337 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5338
5339 cx.set_state(indoc! {"
5340 fn a() {
5341 b();
5342 if c() {
5343 ˇ
5344 }
5345 }
5346 "});
5347
5348 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5349 cx.assert_editor_state(indoc! {"
5350 fn a() {
5351 b();
5352 if c() {
5353 d(
5354 e
5355 );
5356 ˇ
5357 }
5358 }
5359 "});
5360
5361 cx.set_state(indoc! {"
5362 fn a() {
5363 b();
5364 ˇ
5365 }
5366 "});
5367
5368 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5369 cx.assert_editor_state(indoc! {"
5370 fn a() {
5371 b();
5372 d(
5373 e
5374 );
5375 ˇ
5376 }
5377 "});
5378}
5379
5380#[gpui::test]
5381fn test_select_all(cx: &mut TestAppContext) {
5382 init_test(cx, |_| {});
5383
5384 let editor = cx.add_window(|window, cx| {
5385 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
5386 build_editor(buffer, window, cx)
5387 });
5388 _ = editor.update(cx, |editor, window, cx| {
5389 editor.select_all(&SelectAll, window, cx);
5390 assert_eq!(
5391 editor.selections.display_ranges(cx),
5392 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
5393 );
5394 });
5395}
5396
5397#[gpui::test]
5398fn test_select_line(cx: &mut TestAppContext) {
5399 init_test(cx, |_| {});
5400
5401 let editor = cx.add_window(|window, cx| {
5402 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
5403 build_editor(buffer, window, cx)
5404 });
5405 _ = editor.update(cx, |editor, window, cx| {
5406 editor.change_selections(None, window, cx, |s| {
5407 s.select_display_ranges([
5408 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5409 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5410 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5411 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
5412 ])
5413 });
5414 editor.select_line(&SelectLine, window, cx);
5415 assert_eq!(
5416 editor.selections.display_ranges(cx),
5417 vec![
5418 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
5419 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
5420 ]
5421 );
5422 });
5423
5424 _ = editor.update(cx, |editor, window, cx| {
5425 editor.select_line(&SelectLine, window, cx);
5426 assert_eq!(
5427 editor.selections.display_ranges(cx),
5428 vec![
5429 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
5430 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
5431 ]
5432 );
5433 });
5434
5435 _ = editor.update(cx, |editor, window, cx| {
5436 editor.select_line(&SelectLine, window, cx);
5437 assert_eq!(
5438 editor.selections.display_ranges(cx),
5439 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
5440 );
5441 });
5442}
5443
5444#[gpui::test]
5445async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
5446 init_test(cx, |_| {});
5447 let mut cx = EditorTestContext::new(cx).await;
5448
5449 #[track_caller]
5450 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
5451 cx.set_state(initial_state);
5452 cx.update_editor(|e, window, cx| {
5453 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
5454 });
5455 cx.assert_editor_state(expected_state);
5456 }
5457
5458 // Selection starts and ends at the middle of lines, left-to-right
5459 test(
5460 &mut cx,
5461 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
5462 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5463 );
5464 // Same thing, right-to-left
5465 test(
5466 &mut cx,
5467 "aa\nb«b\ncc\ndd\neˇ»e\nff",
5468 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5469 );
5470
5471 // Whole buffer, left-to-right, last line *doesn't* end with newline
5472 test(
5473 &mut cx,
5474 "«ˇaa\nbb\ncc\ndd\nee\nff»",
5475 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5476 );
5477 // Same thing, right-to-left
5478 test(
5479 &mut cx,
5480 "«aa\nbb\ncc\ndd\nee\nffˇ»",
5481 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5482 );
5483
5484 // Whole buffer, left-to-right, last line ends with newline
5485 test(
5486 &mut cx,
5487 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
5488 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5489 );
5490 // Same thing, right-to-left
5491 test(
5492 &mut cx,
5493 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
5494 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5495 );
5496
5497 // Starts at the end of a line, ends at the start of another
5498 test(
5499 &mut cx,
5500 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
5501 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
5502 );
5503}
5504
5505#[gpui::test]
5506async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
5507 init_test(cx, |_| {});
5508
5509 let editor = cx.add_window(|window, cx| {
5510 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
5511 build_editor(buffer, window, cx)
5512 });
5513
5514 // setup
5515 _ = editor.update(cx, |editor, window, cx| {
5516 editor.fold_creases(
5517 vec![
5518 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5519 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5520 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5521 ],
5522 true,
5523 window,
5524 cx,
5525 );
5526 assert_eq!(
5527 editor.display_text(cx),
5528 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5529 );
5530 });
5531
5532 _ = editor.update(cx, |editor, window, cx| {
5533 editor.change_selections(None, window, cx, |s| {
5534 s.select_display_ranges([
5535 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5536 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5537 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5538 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
5539 ])
5540 });
5541 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5542 assert_eq!(
5543 editor.display_text(cx),
5544 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5545 );
5546 });
5547 EditorTestContext::for_editor(editor, cx)
5548 .await
5549 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
5550
5551 _ = editor.update(cx, |editor, window, cx| {
5552 editor.change_selections(None, window, cx, |s| {
5553 s.select_display_ranges([
5554 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
5555 ])
5556 });
5557 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5558 assert_eq!(
5559 editor.display_text(cx),
5560 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
5561 );
5562 assert_eq!(
5563 editor.selections.display_ranges(cx),
5564 [
5565 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
5566 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
5567 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
5568 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
5569 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
5570 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
5571 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
5572 ]
5573 );
5574 });
5575 EditorTestContext::for_editor(editor, cx)
5576 .await
5577 .assert_editor_state(
5578 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
5579 );
5580}
5581
5582#[gpui::test]
5583async fn test_add_selection_above_below(cx: &mut TestAppContext) {
5584 init_test(cx, |_| {});
5585
5586 let mut cx = EditorTestContext::new(cx).await;
5587
5588 cx.set_state(indoc!(
5589 r#"abc
5590 defˇghi
5591
5592 jk
5593 nlmo
5594 "#
5595 ));
5596
5597 cx.update_editor(|editor, window, cx| {
5598 editor.add_selection_above(&Default::default(), window, cx);
5599 });
5600
5601 cx.assert_editor_state(indoc!(
5602 r#"abcˇ
5603 defˇghi
5604
5605 jk
5606 nlmo
5607 "#
5608 ));
5609
5610 cx.update_editor(|editor, window, cx| {
5611 editor.add_selection_above(&Default::default(), window, cx);
5612 });
5613
5614 cx.assert_editor_state(indoc!(
5615 r#"abcˇ
5616 defˇghi
5617
5618 jk
5619 nlmo
5620 "#
5621 ));
5622
5623 cx.update_editor(|editor, window, cx| {
5624 editor.add_selection_below(&Default::default(), window, cx);
5625 });
5626
5627 cx.assert_editor_state(indoc!(
5628 r#"abc
5629 defˇghi
5630
5631 jk
5632 nlmo
5633 "#
5634 ));
5635
5636 cx.update_editor(|editor, window, cx| {
5637 editor.undo_selection(&Default::default(), window, cx);
5638 });
5639
5640 cx.assert_editor_state(indoc!(
5641 r#"abcˇ
5642 defˇghi
5643
5644 jk
5645 nlmo
5646 "#
5647 ));
5648
5649 cx.update_editor(|editor, window, cx| {
5650 editor.redo_selection(&Default::default(), window, cx);
5651 });
5652
5653 cx.assert_editor_state(indoc!(
5654 r#"abc
5655 defˇghi
5656
5657 jk
5658 nlmo
5659 "#
5660 ));
5661
5662 cx.update_editor(|editor, window, cx| {
5663 editor.add_selection_below(&Default::default(), window, cx);
5664 });
5665
5666 cx.assert_editor_state(indoc!(
5667 r#"abc
5668 defˇghi
5669
5670 jk
5671 nlmˇo
5672 "#
5673 ));
5674
5675 cx.update_editor(|editor, window, cx| {
5676 editor.add_selection_below(&Default::default(), window, cx);
5677 });
5678
5679 cx.assert_editor_state(indoc!(
5680 r#"abc
5681 defˇghi
5682
5683 jk
5684 nlmˇo
5685 "#
5686 ));
5687
5688 // change selections
5689 cx.set_state(indoc!(
5690 r#"abc
5691 def«ˇg»hi
5692
5693 jk
5694 nlmo
5695 "#
5696 ));
5697
5698 cx.update_editor(|editor, window, cx| {
5699 editor.add_selection_below(&Default::default(), window, cx);
5700 });
5701
5702 cx.assert_editor_state(indoc!(
5703 r#"abc
5704 def«ˇg»hi
5705
5706 jk
5707 nlm«ˇo»
5708 "#
5709 ));
5710
5711 cx.update_editor(|editor, window, cx| {
5712 editor.add_selection_below(&Default::default(), window, cx);
5713 });
5714
5715 cx.assert_editor_state(indoc!(
5716 r#"abc
5717 def«ˇg»hi
5718
5719 jk
5720 nlm«ˇo»
5721 "#
5722 ));
5723
5724 cx.update_editor(|editor, window, cx| {
5725 editor.add_selection_above(&Default::default(), window, cx);
5726 });
5727
5728 cx.assert_editor_state(indoc!(
5729 r#"abc
5730 def«ˇg»hi
5731
5732 jk
5733 nlmo
5734 "#
5735 ));
5736
5737 cx.update_editor(|editor, window, cx| {
5738 editor.add_selection_above(&Default::default(), window, cx);
5739 });
5740
5741 cx.assert_editor_state(indoc!(
5742 r#"abc
5743 def«ˇg»hi
5744
5745 jk
5746 nlmo
5747 "#
5748 ));
5749
5750 // Change selections again
5751 cx.set_state(indoc!(
5752 r#"a«bc
5753 defgˇ»hi
5754
5755 jk
5756 nlmo
5757 "#
5758 ));
5759
5760 cx.update_editor(|editor, window, cx| {
5761 editor.add_selection_below(&Default::default(), window, cx);
5762 });
5763
5764 cx.assert_editor_state(indoc!(
5765 r#"a«bcˇ»
5766 d«efgˇ»hi
5767
5768 j«kˇ»
5769 nlmo
5770 "#
5771 ));
5772
5773 cx.update_editor(|editor, window, cx| {
5774 editor.add_selection_below(&Default::default(), window, cx);
5775 });
5776 cx.assert_editor_state(indoc!(
5777 r#"a«bcˇ»
5778 d«efgˇ»hi
5779
5780 j«kˇ»
5781 n«lmoˇ»
5782 "#
5783 ));
5784 cx.update_editor(|editor, window, cx| {
5785 editor.add_selection_above(&Default::default(), window, cx);
5786 });
5787
5788 cx.assert_editor_state(indoc!(
5789 r#"a«bcˇ»
5790 d«efgˇ»hi
5791
5792 j«kˇ»
5793 nlmo
5794 "#
5795 ));
5796
5797 // Change selections again
5798 cx.set_state(indoc!(
5799 r#"abc
5800 d«ˇefghi
5801
5802 jk
5803 nlm»o
5804 "#
5805 ));
5806
5807 cx.update_editor(|editor, window, cx| {
5808 editor.add_selection_above(&Default::default(), window, cx);
5809 });
5810
5811 cx.assert_editor_state(indoc!(
5812 r#"a«ˇbc»
5813 d«ˇef»ghi
5814
5815 j«ˇk»
5816 n«ˇlm»o
5817 "#
5818 ));
5819
5820 cx.update_editor(|editor, window, cx| {
5821 editor.add_selection_below(&Default::default(), window, cx);
5822 });
5823
5824 cx.assert_editor_state(indoc!(
5825 r#"abc
5826 d«ˇef»ghi
5827
5828 j«ˇk»
5829 n«ˇlm»o
5830 "#
5831 ));
5832}
5833
5834#[gpui::test]
5835async fn test_select_next(cx: &mut TestAppContext) {
5836 init_test(cx, |_| {});
5837
5838 let mut cx = EditorTestContext::new(cx).await;
5839 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5840
5841 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5842 .unwrap();
5843 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5844
5845 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5846 .unwrap();
5847 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5848
5849 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5850 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5851
5852 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5853 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5854
5855 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5856 .unwrap();
5857 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5858
5859 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5860 .unwrap();
5861 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5862
5863 // Test selection direction should be preserved
5864 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
5865
5866 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5867 .unwrap();
5868 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
5869}
5870
5871#[gpui::test]
5872async fn test_select_all_matches(cx: &mut TestAppContext) {
5873 init_test(cx, |_| {});
5874
5875 let mut cx = EditorTestContext::new(cx).await;
5876
5877 // Test caret-only selections
5878 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5879 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5880 .unwrap();
5881 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5882
5883 // Test left-to-right selections
5884 cx.set_state("abc\n«abcˇ»\nabc");
5885 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5886 .unwrap();
5887 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
5888
5889 // Test right-to-left selections
5890 cx.set_state("abc\n«ˇabc»\nabc");
5891 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5892 .unwrap();
5893 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
5894
5895 // Test selecting whitespace with caret selection
5896 cx.set_state("abc\nˇ abc\nabc");
5897 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5898 .unwrap();
5899 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
5900
5901 // Test selecting whitespace with left-to-right selection
5902 cx.set_state("abc\n«ˇ »abc\nabc");
5903 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5904 .unwrap();
5905 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
5906
5907 // Test no matches with right-to-left selection
5908 cx.set_state("abc\n« ˇ»abc\nabc");
5909 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5910 .unwrap();
5911 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
5912}
5913
5914#[gpui::test]
5915async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
5916 init_test(cx, |_| {});
5917
5918 let mut cx = EditorTestContext::new(cx).await;
5919
5920 let large_body_1 = "\nd".repeat(200);
5921 let large_body_2 = "\ne".repeat(200);
5922
5923 cx.set_state(&format!(
5924 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
5925 ));
5926 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
5927 let scroll_position = editor.scroll_position(cx);
5928 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
5929 scroll_position
5930 });
5931
5932 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5933 .unwrap();
5934 cx.assert_editor_state(&format!(
5935 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
5936 ));
5937 let scroll_position_after_selection =
5938 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
5939 assert_eq!(
5940 initial_scroll_position, scroll_position_after_selection,
5941 "Scroll position should not change after selecting all matches"
5942 );
5943}
5944
5945#[gpui::test]
5946async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
5947 init_test(cx, |_| {});
5948
5949 let mut cx = EditorLspTestContext::new_rust(
5950 lsp::ServerCapabilities {
5951 document_formatting_provider: Some(lsp::OneOf::Left(true)),
5952 ..Default::default()
5953 },
5954 cx,
5955 )
5956 .await;
5957
5958 cx.set_state(indoc! {"
5959 line 1
5960 line 2
5961 linˇe 3
5962 line 4
5963 line 5
5964 "});
5965
5966 // Make an edit
5967 cx.update_editor(|editor, window, cx| {
5968 editor.handle_input("X", window, cx);
5969 });
5970
5971 // Move cursor to a different position
5972 cx.update_editor(|editor, window, cx| {
5973 editor.change_selections(None, window, cx, |s| {
5974 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
5975 });
5976 });
5977
5978 cx.assert_editor_state(indoc! {"
5979 line 1
5980 line 2
5981 linXe 3
5982 line 4
5983 liˇne 5
5984 "});
5985
5986 cx.lsp
5987 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
5988 Ok(Some(vec![lsp::TextEdit::new(
5989 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
5990 "PREFIX ".to_string(),
5991 )]))
5992 });
5993
5994 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
5995 .unwrap()
5996 .await
5997 .unwrap();
5998
5999 cx.assert_editor_state(indoc! {"
6000 PREFIX line 1
6001 line 2
6002 linXe 3
6003 line 4
6004 liˇne 5
6005 "});
6006
6007 // Undo formatting
6008 cx.update_editor(|editor, window, cx| {
6009 editor.undo(&Default::default(), window, cx);
6010 });
6011
6012 // Verify cursor moved back to position after edit
6013 cx.assert_editor_state(indoc! {"
6014 line 1
6015 line 2
6016 linXˇe 3
6017 line 4
6018 line 5
6019 "});
6020}
6021
6022#[gpui::test]
6023async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
6024 init_test(cx, |_| {});
6025
6026 let mut cx = EditorTestContext::new(cx).await;
6027 cx.set_state(
6028 r#"let foo = 2;
6029lˇet foo = 2;
6030let fooˇ = 2;
6031let foo = 2;
6032let foo = ˇ2;"#,
6033 );
6034
6035 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6036 .unwrap();
6037 cx.assert_editor_state(
6038 r#"let foo = 2;
6039«letˇ» foo = 2;
6040let «fooˇ» = 2;
6041let foo = 2;
6042let foo = «2ˇ»;"#,
6043 );
6044
6045 // noop for multiple selections with different contents
6046 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6047 .unwrap();
6048 cx.assert_editor_state(
6049 r#"let foo = 2;
6050«letˇ» foo = 2;
6051let «fooˇ» = 2;
6052let foo = 2;
6053let foo = «2ˇ»;"#,
6054 );
6055
6056 // Test last selection direction should be preserved
6057 cx.set_state(
6058 r#"let foo = 2;
6059let foo = 2;
6060let «fooˇ» = 2;
6061let «ˇfoo» = 2;
6062let foo = 2;"#,
6063 );
6064
6065 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6066 .unwrap();
6067 cx.assert_editor_state(
6068 r#"let foo = 2;
6069let foo = 2;
6070let «fooˇ» = 2;
6071let «ˇfoo» = 2;
6072let «ˇfoo» = 2;"#,
6073 );
6074}
6075
6076#[gpui::test]
6077async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
6078 init_test(cx, |_| {});
6079
6080 let mut cx =
6081 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
6082
6083 cx.assert_editor_state(indoc! {"
6084 ˇbbb
6085 ccc
6086
6087 bbb
6088 ccc
6089 "});
6090 cx.dispatch_action(SelectPrevious::default());
6091 cx.assert_editor_state(indoc! {"
6092 «bbbˇ»
6093 ccc
6094
6095 bbb
6096 ccc
6097 "});
6098 cx.dispatch_action(SelectPrevious::default());
6099 cx.assert_editor_state(indoc! {"
6100 «bbbˇ»
6101 ccc
6102
6103 «bbbˇ»
6104 ccc
6105 "});
6106}
6107
6108#[gpui::test]
6109async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
6110 init_test(cx, |_| {});
6111
6112 let mut cx = EditorTestContext::new(cx).await;
6113 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6114
6115 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6116 .unwrap();
6117 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6118
6119 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6120 .unwrap();
6121 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6122
6123 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6124 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6125
6126 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6127 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6128
6129 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6130 .unwrap();
6131 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
6132
6133 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6134 .unwrap();
6135 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6136}
6137
6138#[gpui::test]
6139async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
6140 init_test(cx, |_| {});
6141
6142 let mut cx = EditorTestContext::new(cx).await;
6143 cx.set_state("aˇ");
6144
6145 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6146 .unwrap();
6147 cx.assert_editor_state("«aˇ»");
6148 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6149 .unwrap();
6150 cx.assert_editor_state("«aˇ»");
6151}
6152
6153#[gpui::test]
6154async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
6155 init_test(cx, |_| {});
6156
6157 let mut cx = EditorTestContext::new(cx).await;
6158 cx.set_state(
6159 r#"let foo = 2;
6160lˇet foo = 2;
6161let fooˇ = 2;
6162let foo = 2;
6163let foo = ˇ2;"#,
6164 );
6165
6166 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6167 .unwrap();
6168 cx.assert_editor_state(
6169 r#"let foo = 2;
6170«letˇ» foo = 2;
6171let «fooˇ» = 2;
6172let foo = 2;
6173let foo = «2ˇ»;"#,
6174 );
6175
6176 // noop for multiple selections with different contents
6177 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6178 .unwrap();
6179 cx.assert_editor_state(
6180 r#"let foo = 2;
6181«letˇ» foo = 2;
6182let «fooˇ» = 2;
6183let foo = 2;
6184let foo = «2ˇ»;"#,
6185 );
6186}
6187
6188#[gpui::test]
6189async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
6190 init_test(cx, |_| {});
6191
6192 let mut cx = EditorTestContext::new(cx).await;
6193 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6194
6195 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6196 .unwrap();
6197 // selection direction is preserved
6198 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
6199
6200 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6201 .unwrap();
6202 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
6203
6204 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6205 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
6206
6207 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6208 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
6209
6210 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6211 .unwrap();
6212 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
6213
6214 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6215 .unwrap();
6216 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
6217}
6218
6219#[gpui::test]
6220async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
6221 init_test(cx, |_| {});
6222
6223 let language = Arc::new(Language::new(
6224 LanguageConfig::default(),
6225 Some(tree_sitter_rust::LANGUAGE.into()),
6226 ));
6227
6228 let text = r#"
6229 use mod1::mod2::{mod3, mod4};
6230
6231 fn fn_1(param1: bool, param2: &str) {
6232 let var1 = "text";
6233 }
6234 "#
6235 .unindent();
6236
6237 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6238 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6239 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6240
6241 editor
6242 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6243 .await;
6244
6245 editor.update_in(cx, |editor, window, cx| {
6246 editor.change_selections(None, window, cx, |s| {
6247 s.select_display_ranges([
6248 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
6249 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
6250 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
6251 ]);
6252 });
6253 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6254 });
6255 editor.update(cx, |editor, cx| {
6256 assert_text_with_selections(
6257 editor,
6258 indoc! {r#"
6259 use mod1::mod2::{mod3, «mod4ˇ»};
6260
6261 fn fn_1«ˇ(param1: bool, param2: &str)» {
6262 let var1 = "«ˇtext»";
6263 }
6264 "#},
6265 cx,
6266 );
6267 });
6268
6269 editor.update_in(cx, |editor, window, cx| {
6270 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6271 });
6272 editor.update(cx, |editor, cx| {
6273 assert_text_with_selections(
6274 editor,
6275 indoc! {r#"
6276 use mod1::mod2::«{mod3, mod4}ˇ»;
6277
6278 «ˇfn fn_1(param1: bool, param2: &str) {
6279 let var1 = "text";
6280 }»
6281 "#},
6282 cx,
6283 );
6284 });
6285
6286 editor.update_in(cx, |editor, window, cx| {
6287 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6288 });
6289 assert_eq!(
6290 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6291 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6292 );
6293
6294 // Trying to expand the selected syntax node one more time has no effect.
6295 editor.update_in(cx, |editor, window, cx| {
6296 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6297 });
6298 assert_eq!(
6299 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6300 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6301 );
6302
6303 editor.update_in(cx, |editor, window, cx| {
6304 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6305 });
6306 editor.update(cx, |editor, cx| {
6307 assert_text_with_selections(
6308 editor,
6309 indoc! {r#"
6310 use mod1::mod2::«{mod3, mod4}ˇ»;
6311
6312 «ˇfn fn_1(param1: bool, param2: &str) {
6313 let var1 = "text";
6314 }»
6315 "#},
6316 cx,
6317 );
6318 });
6319
6320 editor.update_in(cx, |editor, window, cx| {
6321 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6322 });
6323 editor.update(cx, |editor, cx| {
6324 assert_text_with_selections(
6325 editor,
6326 indoc! {r#"
6327 use mod1::mod2::{mod3, «mod4ˇ»};
6328
6329 fn fn_1«ˇ(param1: bool, param2: &str)» {
6330 let var1 = "«ˇtext»";
6331 }
6332 "#},
6333 cx,
6334 );
6335 });
6336
6337 editor.update_in(cx, |editor, window, cx| {
6338 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6339 });
6340 editor.update(cx, |editor, cx| {
6341 assert_text_with_selections(
6342 editor,
6343 indoc! {r#"
6344 use mod1::mod2::{mod3, mo«ˇ»d4};
6345
6346 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6347 let var1 = "te«ˇ»xt";
6348 }
6349 "#},
6350 cx,
6351 );
6352 });
6353
6354 // Trying to shrink the selected syntax node one more time has no effect.
6355 editor.update_in(cx, |editor, window, cx| {
6356 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6357 });
6358 editor.update_in(cx, |editor, _, cx| {
6359 assert_text_with_selections(
6360 editor,
6361 indoc! {r#"
6362 use mod1::mod2::{mod3, mo«ˇ»d4};
6363
6364 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6365 let var1 = "te«ˇ»xt";
6366 }
6367 "#},
6368 cx,
6369 );
6370 });
6371
6372 // Ensure that we keep expanding the selection if the larger selection starts or ends within
6373 // a fold.
6374 editor.update_in(cx, |editor, window, cx| {
6375 editor.fold_creases(
6376 vec![
6377 Crease::simple(
6378 Point::new(0, 21)..Point::new(0, 24),
6379 FoldPlaceholder::test(),
6380 ),
6381 Crease::simple(
6382 Point::new(3, 20)..Point::new(3, 22),
6383 FoldPlaceholder::test(),
6384 ),
6385 ],
6386 true,
6387 window,
6388 cx,
6389 );
6390 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6391 });
6392 editor.update(cx, |editor, cx| {
6393 assert_text_with_selections(
6394 editor,
6395 indoc! {r#"
6396 use mod1::mod2::«{mod3, mod4}ˇ»;
6397
6398 fn fn_1«ˇ(param1: bool, param2: &str)» {
6399 let var1 = "«ˇtext»";
6400 }
6401 "#},
6402 cx,
6403 );
6404 });
6405}
6406
6407#[gpui::test]
6408async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
6409 init_test(cx, |_| {});
6410
6411 let language = Arc::new(Language::new(
6412 LanguageConfig::default(),
6413 Some(tree_sitter_rust::LANGUAGE.into()),
6414 ));
6415
6416 let text = "let a = 2;";
6417
6418 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6419 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6420 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6421
6422 editor
6423 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6424 .await;
6425
6426 // Test case 1: Cursor at end of word
6427 editor.update_in(cx, |editor, window, cx| {
6428 editor.change_selections(None, window, cx, |s| {
6429 s.select_display_ranges([
6430 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
6431 ]);
6432 });
6433 });
6434 editor.update(cx, |editor, cx| {
6435 assert_text_with_selections(editor, "let aˇ = 2;", cx);
6436 });
6437 editor.update_in(cx, |editor, window, cx| {
6438 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6439 });
6440 editor.update(cx, |editor, cx| {
6441 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
6442 });
6443 editor.update_in(cx, |editor, window, cx| {
6444 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6445 });
6446 editor.update(cx, |editor, cx| {
6447 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
6448 });
6449
6450 // Test case 2: Cursor at end of statement
6451 editor.update_in(cx, |editor, window, cx| {
6452 editor.change_selections(None, window, cx, |s| {
6453 s.select_display_ranges([
6454 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
6455 ]);
6456 });
6457 });
6458 editor.update(cx, |editor, cx| {
6459 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
6460 });
6461 editor.update_in(cx, |editor, window, cx| {
6462 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6463 });
6464 editor.update(cx, |editor, cx| {
6465 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
6466 });
6467}
6468
6469#[gpui::test]
6470async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
6471 init_test(cx, |_| {});
6472
6473 let language = Arc::new(Language::new(
6474 LanguageConfig::default(),
6475 Some(tree_sitter_rust::LANGUAGE.into()),
6476 ));
6477
6478 let text = r#"
6479 use mod1::mod2::{mod3, mod4};
6480
6481 fn fn_1(param1: bool, param2: &str) {
6482 let var1 = "hello world";
6483 }
6484 "#
6485 .unindent();
6486
6487 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6488 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6489 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6490
6491 editor
6492 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6493 .await;
6494
6495 // Test 1: Cursor on a letter of a string word
6496 editor.update_in(cx, |editor, window, cx| {
6497 editor.change_selections(None, window, cx, |s| {
6498 s.select_display_ranges([
6499 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
6500 ]);
6501 });
6502 });
6503 editor.update_in(cx, |editor, window, cx| {
6504 assert_text_with_selections(
6505 editor,
6506 indoc! {r#"
6507 use mod1::mod2::{mod3, mod4};
6508
6509 fn fn_1(param1: bool, param2: &str) {
6510 let var1 = "hˇello world";
6511 }
6512 "#},
6513 cx,
6514 );
6515 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6516 assert_text_with_selections(
6517 editor,
6518 indoc! {r#"
6519 use mod1::mod2::{mod3, mod4};
6520
6521 fn fn_1(param1: bool, param2: &str) {
6522 let var1 = "«ˇhello» world";
6523 }
6524 "#},
6525 cx,
6526 );
6527 });
6528
6529 // Test 2: Partial selection within a word
6530 editor.update_in(cx, |editor, window, cx| {
6531 editor.change_selections(None, window, cx, |s| {
6532 s.select_display_ranges([
6533 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
6534 ]);
6535 });
6536 });
6537 editor.update_in(cx, |editor, window, cx| {
6538 assert_text_with_selections(
6539 editor,
6540 indoc! {r#"
6541 use mod1::mod2::{mod3, mod4};
6542
6543 fn fn_1(param1: bool, param2: &str) {
6544 let var1 = "h«elˇ»lo world";
6545 }
6546 "#},
6547 cx,
6548 );
6549 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6550 assert_text_with_selections(
6551 editor,
6552 indoc! {r#"
6553 use mod1::mod2::{mod3, mod4};
6554
6555 fn fn_1(param1: bool, param2: &str) {
6556 let var1 = "«ˇhello» world";
6557 }
6558 "#},
6559 cx,
6560 );
6561 });
6562
6563 // Test 3: Complete word already selected
6564 editor.update_in(cx, |editor, window, cx| {
6565 editor.change_selections(None, window, cx, |s| {
6566 s.select_display_ranges([
6567 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
6568 ]);
6569 });
6570 });
6571 editor.update_in(cx, |editor, window, cx| {
6572 assert_text_with_selections(
6573 editor,
6574 indoc! {r#"
6575 use mod1::mod2::{mod3, mod4};
6576
6577 fn fn_1(param1: bool, param2: &str) {
6578 let var1 = "«helloˇ» world";
6579 }
6580 "#},
6581 cx,
6582 );
6583 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6584 assert_text_with_selections(
6585 editor,
6586 indoc! {r#"
6587 use mod1::mod2::{mod3, mod4};
6588
6589 fn fn_1(param1: bool, param2: &str) {
6590 let var1 = "«hello worldˇ»";
6591 }
6592 "#},
6593 cx,
6594 );
6595 });
6596
6597 // Test 4: Selection spanning across words
6598 editor.update_in(cx, |editor, window, cx| {
6599 editor.change_selections(None, window, cx, |s| {
6600 s.select_display_ranges([
6601 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
6602 ]);
6603 });
6604 });
6605 editor.update_in(cx, |editor, window, cx| {
6606 assert_text_with_selections(
6607 editor,
6608 indoc! {r#"
6609 use mod1::mod2::{mod3, mod4};
6610
6611 fn fn_1(param1: bool, param2: &str) {
6612 let var1 = "hel«lo woˇ»rld";
6613 }
6614 "#},
6615 cx,
6616 );
6617 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6618 assert_text_with_selections(
6619 editor,
6620 indoc! {r#"
6621 use mod1::mod2::{mod3, mod4};
6622
6623 fn fn_1(param1: bool, param2: &str) {
6624 let var1 = "«ˇhello world»";
6625 }
6626 "#},
6627 cx,
6628 );
6629 });
6630
6631 // Test 5: Expansion beyond string
6632 editor.update_in(cx, |editor, window, cx| {
6633 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6634 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6635 assert_text_with_selections(
6636 editor,
6637 indoc! {r#"
6638 use mod1::mod2::{mod3, mod4};
6639
6640 fn fn_1(param1: bool, param2: &str) {
6641 «ˇlet var1 = "hello world";»
6642 }
6643 "#},
6644 cx,
6645 );
6646 });
6647}
6648
6649#[gpui::test]
6650async fn test_fold_function_bodies(cx: &mut TestAppContext) {
6651 init_test(cx, |_| {});
6652
6653 let base_text = r#"
6654 impl A {
6655 // this is an uncommitted comment
6656
6657 fn b() {
6658 c();
6659 }
6660
6661 // this is another uncommitted comment
6662
6663 fn d() {
6664 // e
6665 // f
6666 }
6667 }
6668
6669 fn g() {
6670 // h
6671 }
6672 "#
6673 .unindent();
6674
6675 let text = r#"
6676 ˇimpl A {
6677
6678 fn b() {
6679 c();
6680 }
6681
6682 fn d() {
6683 // e
6684 // f
6685 }
6686 }
6687
6688 fn g() {
6689 // h
6690 }
6691 "#
6692 .unindent();
6693
6694 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6695 cx.set_state(&text);
6696 cx.set_head_text(&base_text);
6697 cx.update_editor(|editor, window, cx| {
6698 editor.expand_all_diff_hunks(&Default::default(), window, cx);
6699 });
6700
6701 cx.assert_state_with_diff(
6702 "
6703 ˇimpl A {
6704 - // this is an uncommitted comment
6705
6706 fn b() {
6707 c();
6708 }
6709
6710 - // this is another uncommitted comment
6711 -
6712 fn d() {
6713 // e
6714 // f
6715 }
6716 }
6717
6718 fn g() {
6719 // h
6720 }
6721 "
6722 .unindent(),
6723 );
6724
6725 let expected_display_text = "
6726 impl A {
6727 // this is an uncommitted comment
6728
6729 fn b() {
6730 ⋯
6731 }
6732
6733 // this is another uncommitted comment
6734
6735 fn d() {
6736 ⋯
6737 }
6738 }
6739
6740 fn g() {
6741 ⋯
6742 }
6743 "
6744 .unindent();
6745
6746 cx.update_editor(|editor, window, cx| {
6747 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
6748 assert_eq!(editor.display_text(cx), expected_display_text);
6749 });
6750}
6751
6752#[gpui::test]
6753async fn test_autoindent(cx: &mut TestAppContext) {
6754 init_test(cx, |_| {});
6755
6756 let language = Arc::new(
6757 Language::new(
6758 LanguageConfig {
6759 brackets: BracketPairConfig {
6760 pairs: vec![
6761 BracketPair {
6762 start: "{".to_string(),
6763 end: "}".to_string(),
6764 close: false,
6765 surround: false,
6766 newline: true,
6767 },
6768 BracketPair {
6769 start: "(".to_string(),
6770 end: ")".to_string(),
6771 close: false,
6772 surround: false,
6773 newline: true,
6774 },
6775 ],
6776 ..Default::default()
6777 },
6778 ..Default::default()
6779 },
6780 Some(tree_sitter_rust::LANGUAGE.into()),
6781 )
6782 .with_indents_query(
6783 r#"
6784 (_ "(" ")" @end) @indent
6785 (_ "{" "}" @end) @indent
6786 "#,
6787 )
6788 .unwrap(),
6789 );
6790
6791 let text = "fn a() {}";
6792
6793 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6794 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6795 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6796 editor
6797 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6798 .await;
6799
6800 editor.update_in(cx, |editor, window, cx| {
6801 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
6802 editor.newline(&Newline, window, cx);
6803 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
6804 assert_eq!(
6805 editor.selections.ranges(cx),
6806 &[
6807 Point::new(1, 4)..Point::new(1, 4),
6808 Point::new(3, 4)..Point::new(3, 4),
6809 Point::new(5, 0)..Point::new(5, 0)
6810 ]
6811 );
6812 });
6813}
6814
6815#[gpui::test]
6816async fn test_autoindent_selections(cx: &mut TestAppContext) {
6817 init_test(cx, |_| {});
6818
6819 {
6820 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6821 cx.set_state(indoc! {"
6822 impl A {
6823
6824 fn b() {}
6825
6826 «fn c() {
6827
6828 }ˇ»
6829 }
6830 "});
6831
6832 cx.update_editor(|editor, window, cx| {
6833 editor.autoindent(&Default::default(), window, cx);
6834 });
6835
6836 cx.assert_editor_state(indoc! {"
6837 impl A {
6838
6839 fn b() {}
6840
6841 «fn c() {
6842
6843 }ˇ»
6844 }
6845 "});
6846 }
6847
6848 {
6849 let mut cx = EditorTestContext::new_multibuffer(
6850 cx,
6851 [indoc! { "
6852 impl A {
6853 «
6854 // a
6855 fn b(){}
6856 »
6857 «
6858 }
6859 fn c(){}
6860 »
6861 "}],
6862 );
6863
6864 let buffer = cx.update_editor(|editor, _, cx| {
6865 let buffer = editor.buffer().update(cx, |buffer, _| {
6866 buffer.all_buffers().iter().next().unwrap().clone()
6867 });
6868 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6869 buffer
6870 });
6871
6872 cx.run_until_parked();
6873 cx.update_editor(|editor, window, cx| {
6874 editor.select_all(&Default::default(), window, cx);
6875 editor.autoindent(&Default::default(), window, cx)
6876 });
6877 cx.run_until_parked();
6878
6879 cx.update(|_, cx| {
6880 assert_eq!(
6881 buffer.read(cx).text(),
6882 indoc! { "
6883 impl A {
6884
6885 // a
6886 fn b(){}
6887
6888
6889 }
6890 fn c(){}
6891
6892 " }
6893 )
6894 });
6895 }
6896}
6897
6898#[gpui::test]
6899async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
6900 init_test(cx, |_| {});
6901
6902 let mut cx = EditorTestContext::new(cx).await;
6903
6904 let language = Arc::new(Language::new(
6905 LanguageConfig {
6906 brackets: BracketPairConfig {
6907 pairs: vec![
6908 BracketPair {
6909 start: "{".to_string(),
6910 end: "}".to_string(),
6911 close: true,
6912 surround: true,
6913 newline: true,
6914 },
6915 BracketPair {
6916 start: "(".to_string(),
6917 end: ")".to_string(),
6918 close: true,
6919 surround: true,
6920 newline: true,
6921 },
6922 BracketPair {
6923 start: "/*".to_string(),
6924 end: " */".to_string(),
6925 close: true,
6926 surround: true,
6927 newline: true,
6928 },
6929 BracketPair {
6930 start: "[".to_string(),
6931 end: "]".to_string(),
6932 close: false,
6933 surround: false,
6934 newline: true,
6935 },
6936 BracketPair {
6937 start: "\"".to_string(),
6938 end: "\"".to_string(),
6939 close: true,
6940 surround: true,
6941 newline: false,
6942 },
6943 BracketPair {
6944 start: "<".to_string(),
6945 end: ">".to_string(),
6946 close: false,
6947 surround: true,
6948 newline: true,
6949 },
6950 ],
6951 ..Default::default()
6952 },
6953 autoclose_before: "})]".to_string(),
6954 ..Default::default()
6955 },
6956 Some(tree_sitter_rust::LANGUAGE.into()),
6957 ));
6958
6959 cx.language_registry().add(language.clone());
6960 cx.update_buffer(|buffer, cx| {
6961 buffer.set_language(Some(language), cx);
6962 });
6963
6964 cx.set_state(
6965 &r#"
6966 🏀ˇ
6967 εˇ
6968 ❤️ˇ
6969 "#
6970 .unindent(),
6971 );
6972
6973 // autoclose multiple nested brackets at multiple cursors
6974 cx.update_editor(|editor, window, cx| {
6975 editor.handle_input("{", window, cx);
6976 editor.handle_input("{", window, cx);
6977 editor.handle_input("{", window, cx);
6978 });
6979 cx.assert_editor_state(
6980 &"
6981 🏀{{{ˇ}}}
6982 ε{{{ˇ}}}
6983 ❤️{{{ˇ}}}
6984 "
6985 .unindent(),
6986 );
6987
6988 // insert a different closing bracket
6989 cx.update_editor(|editor, window, cx| {
6990 editor.handle_input(")", window, cx);
6991 });
6992 cx.assert_editor_state(
6993 &"
6994 🏀{{{)ˇ}}}
6995 ε{{{)ˇ}}}
6996 ❤️{{{)ˇ}}}
6997 "
6998 .unindent(),
6999 );
7000
7001 // skip over the auto-closed brackets when typing a closing bracket
7002 cx.update_editor(|editor, window, cx| {
7003 editor.move_right(&MoveRight, window, cx);
7004 editor.handle_input("}", window, cx);
7005 editor.handle_input("}", window, cx);
7006 editor.handle_input("}", window, cx);
7007 });
7008 cx.assert_editor_state(
7009 &"
7010 🏀{{{)}}}}ˇ
7011 ε{{{)}}}}ˇ
7012 ❤️{{{)}}}}ˇ
7013 "
7014 .unindent(),
7015 );
7016
7017 // autoclose multi-character pairs
7018 cx.set_state(
7019 &"
7020 ˇ
7021 ˇ
7022 "
7023 .unindent(),
7024 );
7025 cx.update_editor(|editor, window, cx| {
7026 editor.handle_input("/", window, cx);
7027 editor.handle_input("*", window, cx);
7028 });
7029 cx.assert_editor_state(
7030 &"
7031 /*ˇ */
7032 /*ˇ */
7033 "
7034 .unindent(),
7035 );
7036
7037 // one cursor autocloses a multi-character pair, one cursor
7038 // does not autoclose.
7039 cx.set_state(
7040 &"
7041 /ˇ
7042 ˇ
7043 "
7044 .unindent(),
7045 );
7046 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
7047 cx.assert_editor_state(
7048 &"
7049 /*ˇ */
7050 *ˇ
7051 "
7052 .unindent(),
7053 );
7054
7055 // Don't autoclose if the next character isn't whitespace and isn't
7056 // listed in the language's "autoclose_before" section.
7057 cx.set_state("ˇa b");
7058 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7059 cx.assert_editor_state("{ˇa b");
7060
7061 // Don't autoclose if `close` is false for the bracket pair
7062 cx.set_state("ˇ");
7063 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
7064 cx.assert_editor_state("[ˇ");
7065
7066 // Surround with brackets if text is selected
7067 cx.set_state("«aˇ» b");
7068 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7069 cx.assert_editor_state("{«aˇ»} b");
7070
7071 // Autoclose when not immediately after a word character
7072 cx.set_state("a ˇ");
7073 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7074 cx.assert_editor_state("a \"ˇ\"");
7075
7076 // Autoclose pair where the start and end characters are the same
7077 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7078 cx.assert_editor_state("a \"\"ˇ");
7079
7080 // Don't autoclose when immediately after a word character
7081 cx.set_state("aˇ");
7082 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7083 cx.assert_editor_state("a\"ˇ");
7084
7085 // Do autoclose when after a non-word character
7086 cx.set_state("{ˇ");
7087 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7088 cx.assert_editor_state("{\"ˇ\"");
7089
7090 // Non identical pairs autoclose regardless of preceding character
7091 cx.set_state("aˇ");
7092 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7093 cx.assert_editor_state("a{ˇ}");
7094
7095 // Don't autoclose pair if autoclose is disabled
7096 cx.set_state("ˇ");
7097 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7098 cx.assert_editor_state("<ˇ");
7099
7100 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
7101 cx.set_state("«aˇ» b");
7102 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7103 cx.assert_editor_state("<«aˇ»> b");
7104}
7105
7106#[gpui::test]
7107async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
7108 init_test(cx, |settings| {
7109 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7110 });
7111
7112 let mut cx = EditorTestContext::new(cx).await;
7113
7114 let language = Arc::new(Language::new(
7115 LanguageConfig {
7116 brackets: BracketPairConfig {
7117 pairs: vec![
7118 BracketPair {
7119 start: "{".to_string(),
7120 end: "}".to_string(),
7121 close: true,
7122 surround: true,
7123 newline: true,
7124 },
7125 BracketPair {
7126 start: "(".to_string(),
7127 end: ")".to_string(),
7128 close: true,
7129 surround: true,
7130 newline: true,
7131 },
7132 BracketPair {
7133 start: "[".to_string(),
7134 end: "]".to_string(),
7135 close: false,
7136 surround: false,
7137 newline: true,
7138 },
7139 ],
7140 ..Default::default()
7141 },
7142 autoclose_before: "})]".to_string(),
7143 ..Default::default()
7144 },
7145 Some(tree_sitter_rust::LANGUAGE.into()),
7146 ));
7147
7148 cx.language_registry().add(language.clone());
7149 cx.update_buffer(|buffer, cx| {
7150 buffer.set_language(Some(language), cx);
7151 });
7152
7153 cx.set_state(
7154 &"
7155 ˇ
7156 ˇ
7157 ˇ
7158 "
7159 .unindent(),
7160 );
7161
7162 // ensure only matching closing brackets are skipped over
7163 cx.update_editor(|editor, window, cx| {
7164 editor.handle_input("}", window, cx);
7165 editor.move_left(&MoveLeft, window, cx);
7166 editor.handle_input(")", window, cx);
7167 editor.move_left(&MoveLeft, window, cx);
7168 });
7169 cx.assert_editor_state(
7170 &"
7171 ˇ)}
7172 ˇ)}
7173 ˇ)}
7174 "
7175 .unindent(),
7176 );
7177
7178 // skip-over closing brackets at multiple cursors
7179 cx.update_editor(|editor, window, cx| {
7180 editor.handle_input(")", window, cx);
7181 editor.handle_input("}", window, cx);
7182 });
7183 cx.assert_editor_state(
7184 &"
7185 )}ˇ
7186 )}ˇ
7187 )}ˇ
7188 "
7189 .unindent(),
7190 );
7191
7192 // ignore non-close brackets
7193 cx.update_editor(|editor, window, cx| {
7194 editor.handle_input("]", window, cx);
7195 editor.move_left(&MoveLeft, window, cx);
7196 editor.handle_input("]", window, cx);
7197 });
7198 cx.assert_editor_state(
7199 &"
7200 )}]ˇ]
7201 )}]ˇ]
7202 )}]ˇ]
7203 "
7204 .unindent(),
7205 );
7206}
7207
7208#[gpui::test]
7209async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
7210 init_test(cx, |_| {});
7211
7212 let mut cx = EditorTestContext::new(cx).await;
7213
7214 let html_language = Arc::new(
7215 Language::new(
7216 LanguageConfig {
7217 name: "HTML".into(),
7218 brackets: BracketPairConfig {
7219 pairs: vec![
7220 BracketPair {
7221 start: "<".into(),
7222 end: ">".into(),
7223 close: true,
7224 ..Default::default()
7225 },
7226 BracketPair {
7227 start: "{".into(),
7228 end: "}".into(),
7229 close: true,
7230 ..Default::default()
7231 },
7232 BracketPair {
7233 start: "(".into(),
7234 end: ")".into(),
7235 close: true,
7236 ..Default::default()
7237 },
7238 ],
7239 ..Default::default()
7240 },
7241 autoclose_before: "})]>".into(),
7242 ..Default::default()
7243 },
7244 Some(tree_sitter_html::LANGUAGE.into()),
7245 )
7246 .with_injection_query(
7247 r#"
7248 (script_element
7249 (raw_text) @injection.content
7250 (#set! injection.language "javascript"))
7251 "#,
7252 )
7253 .unwrap(),
7254 );
7255
7256 let javascript_language = Arc::new(Language::new(
7257 LanguageConfig {
7258 name: "JavaScript".into(),
7259 brackets: BracketPairConfig {
7260 pairs: vec![
7261 BracketPair {
7262 start: "/*".into(),
7263 end: " */".into(),
7264 close: true,
7265 ..Default::default()
7266 },
7267 BracketPair {
7268 start: "{".into(),
7269 end: "}".into(),
7270 close: true,
7271 ..Default::default()
7272 },
7273 BracketPair {
7274 start: "(".into(),
7275 end: ")".into(),
7276 close: true,
7277 ..Default::default()
7278 },
7279 ],
7280 ..Default::default()
7281 },
7282 autoclose_before: "})]>".into(),
7283 ..Default::default()
7284 },
7285 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
7286 ));
7287
7288 cx.language_registry().add(html_language.clone());
7289 cx.language_registry().add(javascript_language.clone());
7290
7291 cx.update_buffer(|buffer, cx| {
7292 buffer.set_language(Some(html_language), cx);
7293 });
7294
7295 cx.set_state(
7296 &r#"
7297 <body>ˇ
7298 <script>
7299 var x = 1;ˇ
7300 </script>
7301 </body>ˇ
7302 "#
7303 .unindent(),
7304 );
7305
7306 // Precondition: different languages are active at different locations.
7307 cx.update_editor(|editor, window, cx| {
7308 let snapshot = editor.snapshot(window, cx);
7309 let cursors = editor.selections.ranges::<usize>(cx);
7310 let languages = cursors
7311 .iter()
7312 .map(|c| snapshot.language_at(c.start).unwrap().name())
7313 .collect::<Vec<_>>();
7314 assert_eq!(
7315 languages,
7316 &["HTML".into(), "JavaScript".into(), "HTML".into()]
7317 );
7318 });
7319
7320 // Angle brackets autoclose in HTML, but not JavaScript.
7321 cx.update_editor(|editor, window, cx| {
7322 editor.handle_input("<", window, cx);
7323 editor.handle_input("a", window, cx);
7324 });
7325 cx.assert_editor_state(
7326 &r#"
7327 <body><aˇ>
7328 <script>
7329 var x = 1;<aˇ
7330 </script>
7331 </body><aˇ>
7332 "#
7333 .unindent(),
7334 );
7335
7336 // Curly braces and parens autoclose in both HTML and JavaScript.
7337 cx.update_editor(|editor, window, cx| {
7338 editor.handle_input(" b=", window, cx);
7339 editor.handle_input("{", window, cx);
7340 editor.handle_input("c", window, cx);
7341 editor.handle_input("(", window, cx);
7342 });
7343 cx.assert_editor_state(
7344 &r#"
7345 <body><a b={c(ˇ)}>
7346 <script>
7347 var x = 1;<a b={c(ˇ)}
7348 </script>
7349 </body><a b={c(ˇ)}>
7350 "#
7351 .unindent(),
7352 );
7353
7354 // Brackets that were already autoclosed are skipped.
7355 cx.update_editor(|editor, window, cx| {
7356 editor.handle_input(")", window, cx);
7357 editor.handle_input("d", window, cx);
7358 editor.handle_input("}", window, cx);
7359 });
7360 cx.assert_editor_state(
7361 &r#"
7362 <body><a b={c()d}ˇ>
7363 <script>
7364 var x = 1;<a b={c()d}ˇ
7365 </script>
7366 </body><a b={c()d}ˇ>
7367 "#
7368 .unindent(),
7369 );
7370 cx.update_editor(|editor, window, cx| {
7371 editor.handle_input(">", window, cx);
7372 });
7373 cx.assert_editor_state(
7374 &r#"
7375 <body><a b={c()d}>ˇ
7376 <script>
7377 var x = 1;<a b={c()d}>ˇ
7378 </script>
7379 </body><a b={c()d}>ˇ
7380 "#
7381 .unindent(),
7382 );
7383
7384 // Reset
7385 cx.set_state(
7386 &r#"
7387 <body>ˇ
7388 <script>
7389 var x = 1;ˇ
7390 </script>
7391 </body>ˇ
7392 "#
7393 .unindent(),
7394 );
7395
7396 cx.update_editor(|editor, window, cx| {
7397 editor.handle_input("<", window, cx);
7398 });
7399 cx.assert_editor_state(
7400 &r#"
7401 <body><ˇ>
7402 <script>
7403 var x = 1;<ˇ
7404 </script>
7405 </body><ˇ>
7406 "#
7407 .unindent(),
7408 );
7409
7410 // When backspacing, the closing angle brackets are removed.
7411 cx.update_editor(|editor, window, cx| {
7412 editor.backspace(&Backspace, window, cx);
7413 });
7414 cx.assert_editor_state(
7415 &r#"
7416 <body>ˇ
7417 <script>
7418 var x = 1;ˇ
7419 </script>
7420 </body>ˇ
7421 "#
7422 .unindent(),
7423 );
7424
7425 // Block comments autoclose in JavaScript, but not HTML.
7426 cx.update_editor(|editor, window, cx| {
7427 editor.handle_input("/", window, cx);
7428 editor.handle_input("*", window, cx);
7429 });
7430 cx.assert_editor_state(
7431 &r#"
7432 <body>/*ˇ
7433 <script>
7434 var x = 1;/*ˇ */
7435 </script>
7436 </body>/*ˇ
7437 "#
7438 .unindent(),
7439 );
7440}
7441
7442#[gpui::test]
7443async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
7444 init_test(cx, |_| {});
7445
7446 let mut cx = EditorTestContext::new(cx).await;
7447
7448 let rust_language = Arc::new(
7449 Language::new(
7450 LanguageConfig {
7451 name: "Rust".into(),
7452 brackets: serde_json::from_value(json!([
7453 { "start": "{", "end": "}", "close": true, "newline": true },
7454 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
7455 ]))
7456 .unwrap(),
7457 autoclose_before: "})]>".into(),
7458 ..Default::default()
7459 },
7460 Some(tree_sitter_rust::LANGUAGE.into()),
7461 )
7462 .with_override_query("(string_literal) @string")
7463 .unwrap(),
7464 );
7465
7466 cx.language_registry().add(rust_language.clone());
7467 cx.update_buffer(|buffer, cx| {
7468 buffer.set_language(Some(rust_language), cx);
7469 });
7470
7471 cx.set_state(
7472 &r#"
7473 let x = ˇ
7474 "#
7475 .unindent(),
7476 );
7477
7478 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
7479 cx.update_editor(|editor, window, cx| {
7480 editor.handle_input("\"", window, cx);
7481 });
7482 cx.assert_editor_state(
7483 &r#"
7484 let x = "ˇ"
7485 "#
7486 .unindent(),
7487 );
7488
7489 // Inserting another quotation mark. The cursor moves across the existing
7490 // automatically-inserted quotation mark.
7491 cx.update_editor(|editor, window, cx| {
7492 editor.handle_input("\"", window, cx);
7493 });
7494 cx.assert_editor_state(
7495 &r#"
7496 let x = ""ˇ
7497 "#
7498 .unindent(),
7499 );
7500
7501 // Reset
7502 cx.set_state(
7503 &r#"
7504 let x = ˇ
7505 "#
7506 .unindent(),
7507 );
7508
7509 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
7510 cx.update_editor(|editor, window, cx| {
7511 editor.handle_input("\"", window, cx);
7512 editor.handle_input(" ", window, cx);
7513 editor.move_left(&Default::default(), window, cx);
7514 editor.handle_input("\\", window, cx);
7515 editor.handle_input("\"", window, cx);
7516 });
7517 cx.assert_editor_state(
7518 &r#"
7519 let x = "\"ˇ "
7520 "#
7521 .unindent(),
7522 );
7523
7524 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
7525 // mark. Nothing is inserted.
7526 cx.update_editor(|editor, window, cx| {
7527 editor.move_right(&Default::default(), window, cx);
7528 editor.handle_input("\"", window, cx);
7529 });
7530 cx.assert_editor_state(
7531 &r#"
7532 let x = "\" "ˇ
7533 "#
7534 .unindent(),
7535 );
7536}
7537
7538#[gpui::test]
7539async fn test_surround_with_pair(cx: &mut TestAppContext) {
7540 init_test(cx, |_| {});
7541
7542 let language = Arc::new(Language::new(
7543 LanguageConfig {
7544 brackets: BracketPairConfig {
7545 pairs: vec![
7546 BracketPair {
7547 start: "{".to_string(),
7548 end: "}".to_string(),
7549 close: true,
7550 surround: true,
7551 newline: true,
7552 },
7553 BracketPair {
7554 start: "/* ".to_string(),
7555 end: "*/".to_string(),
7556 close: true,
7557 surround: true,
7558 ..Default::default()
7559 },
7560 ],
7561 ..Default::default()
7562 },
7563 ..Default::default()
7564 },
7565 Some(tree_sitter_rust::LANGUAGE.into()),
7566 ));
7567
7568 let text = r#"
7569 a
7570 b
7571 c
7572 "#
7573 .unindent();
7574
7575 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7576 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7577 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7578 editor
7579 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7580 .await;
7581
7582 editor.update_in(cx, |editor, window, cx| {
7583 editor.change_selections(None, window, cx, |s| {
7584 s.select_display_ranges([
7585 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7586 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7587 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
7588 ])
7589 });
7590
7591 editor.handle_input("{", window, cx);
7592 editor.handle_input("{", window, cx);
7593 editor.handle_input("{", window, cx);
7594 assert_eq!(
7595 editor.text(cx),
7596 "
7597 {{{a}}}
7598 {{{b}}}
7599 {{{c}}}
7600 "
7601 .unindent()
7602 );
7603 assert_eq!(
7604 editor.selections.display_ranges(cx),
7605 [
7606 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
7607 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
7608 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
7609 ]
7610 );
7611
7612 editor.undo(&Undo, window, cx);
7613 editor.undo(&Undo, window, cx);
7614 editor.undo(&Undo, window, cx);
7615 assert_eq!(
7616 editor.text(cx),
7617 "
7618 a
7619 b
7620 c
7621 "
7622 .unindent()
7623 );
7624 assert_eq!(
7625 editor.selections.display_ranges(cx),
7626 [
7627 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7628 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7629 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7630 ]
7631 );
7632
7633 // Ensure inserting the first character of a multi-byte bracket pair
7634 // doesn't surround the selections with the bracket.
7635 editor.handle_input("/", window, cx);
7636 assert_eq!(
7637 editor.text(cx),
7638 "
7639 /
7640 /
7641 /
7642 "
7643 .unindent()
7644 );
7645 assert_eq!(
7646 editor.selections.display_ranges(cx),
7647 [
7648 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7649 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7650 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7651 ]
7652 );
7653
7654 editor.undo(&Undo, window, cx);
7655 assert_eq!(
7656 editor.text(cx),
7657 "
7658 a
7659 b
7660 c
7661 "
7662 .unindent()
7663 );
7664 assert_eq!(
7665 editor.selections.display_ranges(cx),
7666 [
7667 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7668 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7669 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7670 ]
7671 );
7672
7673 // Ensure inserting the last character of a multi-byte bracket pair
7674 // doesn't surround the selections with the bracket.
7675 editor.handle_input("*", window, cx);
7676 assert_eq!(
7677 editor.text(cx),
7678 "
7679 *
7680 *
7681 *
7682 "
7683 .unindent()
7684 );
7685 assert_eq!(
7686 editor.selections.display_ranges(cx),
7687 [
7688 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7689 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7690 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7691 ]
7692 );
7693 });
7694}
7695
7696#[gpui::test]
7697async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
7698 init_test(cx, |_| {});
7699
7700 let language = Arc::new(Language::new(
7701 LanguageConfig {
7702 brackets: BracketPairConfig {
7703 pairs: vec![BracketPair {
7704 start: "{".to_string(),
7705 end: "}".to_string(),
7706 close: true,
7707 surround: true,
7708 newline: true,
7709 }],
7710 ..Default::default()
7711 },
7712 autoclose_before: "}".to_string(),
7713 ..Default::default()
7714 },
7715 Some(tree_sitter_rust::LANGUAGE.into()),
7716 ));
7717
7718 let text = r#"
7719 a
7720 b
7721 c
7722 "#
7723 .unindent();
7724
7725 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7726 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7727 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7728 editor
7729 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7730 .await;
7731
7732 editor.update_in(cx, |editor, window, cx| {
7733 editor.change_selections(None, window, cx, |s| {
7734 s.select_ranges([
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 editor.handle_input("{", window, cx);
7742 editor.handle_input("{", window, cx);
7743 editor.handle_input("_", window, cx);
7744 assert_eq!(
7745 editor.text(cx),
7746 "
7747 a{{_}}
7748 b{{_}}
7749 c{{_}}
7750 "
7751 .unindent()
7752 );
7753 assert_eq!(
7754 editor.selections.ranges::<Point>(cx),
7755 [
7756 Point::new(0, 4)..Point::new(0, 4),
7757 Point::new(1, 4)..Point::new(1, 4),
7758 Point::new(2, 4)..Point::new(2, 4)
7759 ]
7760 );
7761
7762 editor.backspace(&Default::default(), window, cx);
7763 editor.backspace(&Default::default(), window, cx);
7764 assert_eq!(
7765 editor.text(cx),
7766 "
7767 a{}
7768 b{}
7769 c{}
7770 "
7771 .unindent()
7772 );
7773 assert_eq!(
7774 editor.selections.ranges::<Point>(cx),
7775 [
7776 Point::new(0, 2)..Point::new(0, 2),
7777 Point::new(1, 2)..Point::new(1, 2),
7778 Point::new(2, 2)..Point::new(2, 2)
7779 ]
7780 );
7781
7782 editor.delete_to_previous_word_start(&Default::default(), window, cx);
7783 assert_eq!(
7784 editor.text(cx),
7785 "
7786 a
7787 b
7788 c
7789 "
7790 .unindent()
7791 );
7792 assert_eq!(
7793 editor.selections.ranges::<Point>(cx),
7794 [
7795 Point::new(0, 1)..Point::new(0, 1),
7796 Point::new(1, 1)..Point::new(1, 1),
7797 Point::new(2, 1)..Point::new(2, 1)
7798 ]
7799 );
7800 });
7801}
7802
7803#[gpui::test]
7804async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
7805 init_test(cx, |settings| {
7806 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7807 });
7808
7809 let mut cx = EditorTestContext::new(cx).await;
7810
7811 let language = Arc::new(Language::new(
7812 LanguageConfig {
7813 brackets: BracketPairConfig {
7814 pairs: vec![
7815 BracketPair {
7816 start: "{".to_string(),
7817 end: "}".to_string(),
7818 close: true,
7819 surround: true,
7820 newline: true,
7821 },
7822 BracketPair {
7823 start: "(".to_string(),
7824 end: ")".to_string(),
7825 close: true,
7826 surround: true,
7827 newline: true,
7828 },
7829 BracketPair {
7830 start: "[".to_string(),
7831 end: "]".to_string(),
7832 close: false,
7833 surround: true,
7834 newline: true,
7835 },
7836 ],
7837 ..Default::default()
7838 },
7839 autoclose_before: "})]".to_string(),
7840 ..Default::default()
7841 },
7842 Some(tree_sitter_rust::LANGUAGE.into()),
7843 ));
7844
7845 cx.language_registry().add(language.clone());
7846 cx.update_buffer(|buffer, cx| {
7847 buffer.set_language(Some(language), cx);
7848 });
7849
7850 cx.set_state(
7851 &"
7852 {(ˇ)}
7853 [[ˇ]]
7854 {(ˇ)}
7855 "
7856 .unindent(),
7857 );
7858
7859 cx.update_editor(|editor, window, cx| {
7860 editor.backspace(&Default::default(), window, cx);
7861 editor.backspace(&Default::default(), window, cx);
7862 });
7863
7864 cx.assert_editor_state(
7865 &"
7866 ˇ
7867 ˇ]]
7868 ˇ
7869 "
7870 .unindent(),
7871 );
7872
7873 cx.update_editor(|editor, window, cx| {
7874 editor.handle_input("{", window, cx);
7875 editor.handle_input("{", window, cx);
7876 editor.move_right(&MoveRight, window, cx);
7877 editor.move_right(&MoveRight, window, cx);
7878 editor.move_left(&MoveLeft, window, cx);
7879 editor.move_left(&MoveLeft, window, cx);
7880 editor.backspace(&Default::default(), window, cx);
7881 });
7882
7883 cx.assert_editor_state(
7884 &"
7885 {ˇ}
7886 {ˇ}]]
7887 {ˇ}
7888 "
7889 .unindent(),
7890 );
7891
7892 cx.update_editor(|editor, window, cx| {
7893 editor.backspace(&Default::default(), window, cx);
7894 });
7895
7896 cx.assert_editor_state(
7897 &"
7898 ˇ
7899 ˇ]]
7900 ˇ
7901 "
7902 .unindent(),
7903 );
7904}
7905
7906#[gpui::test]
7907async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
7908 init_test(cx, |_| {});
7909
7910 let language = Arc::new(Language::new(
7911 LanguageConfig::default(),
7912 Some(tree_sitter_rust::LANGUAGE.into()),
7913 ));
7914
7915 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
7916 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7917 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7918 editor
7919 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7920 .await;
7921
7922 editor.update_in(cx, |editor, window, cx| {
7923 editor.set_auto_replace_emoji_shortcode(true);
7924
7925 editor.handle_input("Hello ", window, cx);
7926 editor.handle_input(":wave", window, cx);
7927 assert_eq!(editor.text(cx), "Hello :wave".unindent());
7928
7929 editor.handle_input(":", window, cx);
7930 assert_eq!(editor.text(cx), "Hello 👋".unindent());
7931
7932 editor.handle_input(" :smile", window, cx);
7933 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
7934
7935 editor.handle_input(":", window, cx);
7936 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
7937
7938 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
7939 editor.handle_input(":wave", window, cx);
7940 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
7941
7942 editor.handle_input(":", window, cx);
7943 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
7944
7945 editor.handle_input(":1", window, cx);
7946 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
7947
7948 editor.handle_input(":", window, cx);
7949 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
7950
7951 // Ensure shortcode does not get replaced when it is part of a word
7952 editor.handle_input(" Test:wave", window, cx);
7953 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
7954
7955 editor.handle_input(":", window, cx);
7956 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
7957
7958 editor.set_auto_replace_emoji_shortcode(false);
7959
7960 // Ensure shortcode does not get replaced when auto replace is off
7961 editor.handle_input(" :wave", window, cx);
7962 assert_eq!(
7963 editor.text(cx),
7964 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
7965 );
7966
7967 editor.handle_input(":", window, cx);
7968 assert_eq!(
7969 editor.text(cx),
7970 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
7971 );
7972 });
7973}
7974
7975#[gpui::test]
7976async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
7977 init_test(cx, |_| {});
7978
7979 let (text, insertion_ranges) = marked_text_ranges(
7980 indoc! {"
7981 ˇ
7982 "},
7983 false,
7984 );
7985
7986 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7987 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7988
7989 _ = editor.update_in(cx, |editor, window, cx| {
7990 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
7991
7992 editor
7993 .insert_snippet(&insertion_ranges, snippet, window, cx)
7994 .unwrap();
7995
7996 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7997 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7998 assert_eq!(editor.text(cx), expected_text);
7999 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
8000 }
8001
8002 assert(
8003 editor,
8004 cx,
8005 indoc! {"
8006 type «» =•
8007 "},
8008 );
8009
8010 assert!(editor.context_menu_visible(), "There should be a matches");
8011 });
8012}
8013
8014#[gpui::test]
8015async fn test_snippets(cx: &mut TestAppContext) {
8016 init_test(cx, |_| {});
8017
8018 let (text, insertion_ranges) = marked_text_ranges(
8019 indoc! {"
8020 a.ˇ b
8021 a.ˇ b
8022 a.ˇ b
8023 "},
8024 false,
8025 );
8026
8027 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
8028 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8029
8030 editor.update_in(cx, |editor, window, cx| {
8031 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
8032
8033 editor
8034 .insert_snippet(&insertion_ranges, snippet, window, cx)
8035 .unwrap();
8036
8037 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
8038 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
8039 assert_eq!(editor.text(cx), expected_text);
8040 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
8041 }
8042
8043 assert(
8044 editor,
8045 cx,
8046 indoc! {"
8047 a.f(«one», two, «three») b
8048 a.f(«one», two, «three») b
8049 a.f(«one», two, «three») b
8050 "},
8051 );
8052
8053 // Can't move earlier than the first tab stop
8054 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
8055 assert(
8056 editor,
8057 cx,
8058 indoc! {"
8059 a.f(«one», two, «three») b
8060 a.f(«one», two, «three») b
8061 a.f(«one», two, «three») b
8062 "},
8063 );
8064
8065 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8066 assert(
8067 editor,
8068 cx,
8069 indoc! {"
8070 a.f(one, «two», three) b
8071 a.f(one, «two», three) b
8072 a.f(one, «two», three) b
8073 "},
8074 );
8075
8076 editor.move_to_prev_snippet_tabstop(window, cx);
8077 assert(
8078 editor,
8079 cx,
8080 indoc! {"
8081 a.f(«one», two, «three») b
8082 a.f(«one», two, «three») b
8083 a.f(«one», two, «three») b
8084 "},
8085 );
8086
8087 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8088 assert(
8089 editor,
8090 cx,
8091 indoc! {"
8092 a.f(one, «two», three) b
8093 a.f(one, «two», three) b
8094 a.f(one, «two», three) b
8095 "},
8096 );
8097 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8098 assert(
8099 editor,
8100 cx,
8101 indoc! {"
8102 a.f(one, two, three)ˇ b
8103 a.f(one, two, three)ˇ b
8104 a.f(one, two, three)ˇ b
8105 "},
8106 );
8107
8108 // As soon as the last tab stop is reached, snippet state is gone
8109 editor.move_to_prev_snippet_tabstop(window, cx);
8110 assert(
8111 editor,
8112 cx,
8113 indoc! {"
8114 a.f(one, two, three)ˇ b
8115 a.f(one, two, three)ˇ b
8116 a.f(one, two, three)ˇ b
8117 "},
8118 );
8119 });
8120}
8121
8122#[gpui::test]
8123async fn test_document_format_during_save(cx: &mut TestAppContext) {
8124 init_test(cx, |_| {});
8125
8126 let fs = FakeFs::new(cx.executor());
8127 fs.insert_file(path!("/file.rs"), Default::default()).await;
8128
8129 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
8130
8131 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8132 language_registry.add(rust_lang());
8133 let mut fake_servers = language_registry.register_fake_lsp(
8134 "Rust",
8135 FakeLspAdapter {
8136 capabilities: lsp::ServerCapabilities {
8137 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8138 ..Default::default()
8139 },
8140 ..Default::default()
8141 },
8142 );
8143
8144 let buffer = project
8145 .update(cx, |project, cx| {
8146 project.open_local_buffer(path!("/file.rs"), cx)
8147 })
8148 .await
8149 .unwrap();
8150
8151 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8152 let (editor, cx) = cx.add_window_view(|window, cx| {
8153 build_editor_with_project(project.clone(), buffer, window, cx)
8154 });
8155 editor.update_in(cx, |editor, window, cx| {
8156 editor.set_text("one\ntwo\nthree\n", window, cx)
8157 });
8158 assert!(cx.read(|cx| editor.is_dirty(cx)));
8159
8160 cx.executor().start_waiting();
8161 let fake_server = fake_servers.next().await.unwrap();
8162
8163 {
8164 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8165 move |params, _| async move {
8166 assert_eq!(
8167 params.text_document.uri,
8168 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8169 );
8170 assert_eq!(params.options.tab_size, 4);
8171 Ok(Some(vec![lsp::TextEdit::new(
8172 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8173 ", ".to_string(),
8174 )]))
8175 },
8176 );
8177 let save = editor
8178 .update_in(cx, |editor, window, cx| {
8179 editor.save(true, project.clone(), window, cx)
8180 })
8181 .unwrap();
8182 cx.executor().start_waiting();
8183 save.await;
8184
8185 assert_eq!(
8186 editor.update(cx, |editor, cx| editor.text(cx)),
8187 "one, two\nthree\n"
8188 );
8189 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8190 }
8191
8192 {
8193 editor.update_in(cx, |editor, window, cx| {
8194 editor.set_text("one\ntwo\nthree\n", window, cx)
8195 });
8196 assert!(cx.read(|cx| editor.is_dirty(cx)));
8197
8198 // Ensure we can still save even if formatting hangs.
8199 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8200 move |params, _| async move {
8201 assert_eq!(
8202 params.text_document.uri,
8203 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8204 );
8205 futures::future::pending::<()>().await;
8206 unreachable!()
8207 },
8208 );
8209 let save = editor
8210 .update_in(cx, |editor, window, cx| {
8211 editor.save(true, project.clone(), window, cx)
8212 })
8213 .unwrap();
8214 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8215 cx.executor().start_waiting();
8216 save.await;
8217 assert_eq!(
8218 editor.update(cx, |editor, cx| editor.text(cx)),
8219 "one\ntwo\nthree\n"
8220 );
8221 }
8222
8223 // For non-dirty buffer, no formatting request should be sent
8224 {
8225 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8226
8227 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8228 panic!("Should not be invoked on non-dirty buffer");
8229 });
8230 let save = editor
8231 .update_in(cx, |editor, window, cx| {
8232 editor.save(true, project.clone(), window, cx)
8233 })
8234 .unwrap();
8235 cx.executor().start_waiting();
8236 save.await;
8237 }
8238
8239 // Set rust language override and assert overridden tabsize is sent to language server
8240 update_test_language_settings(cx, |settings| {
8241 settings.languages.insert(
8242 "Rust".into(),
8243 LanguageSettingsContent {
8244 tab_size: NonZeroU32::new(8),
8245 ..Default::default()
8246 },
8247 );
8248 });
8249
8250 {
8251 editor.update_in(cx, |editor, window, cx| {
8252 editor.set_text("somehting_new\n", window, cx)
8253 });
8254 assert!(cx.read(|cx| editor.is_dirty(cx)));
8255 let _formatting_request_signal = fake_server
8256 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8257 assert_eq!(
8258 params.text_document.uri,
8259 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8260 );
8261 assert_eq!(params.options.tab_size, 8);
8262 Ok(Some(vec![]))
8263 });
8264 let save = editor
8265 .update_in(cx, |editor, window, cx| {
8266 editor.save(true, project.clone(), window, cx)
8267 })
8268 .unwrap();
8269 cx.executor().start_waiting();
8270 save.await;
8271 }
8272}
8273
8274#[gpui::test]
8275async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
8276 init_test(cx, |_| {});
8277
8278 let cols = 4;
8279 let rows = 10;
8280 let sample_text_1 = sample_text(rows, cols, 'a');
8281 assert_eq!(
8282 sample_text_1,
8283 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
8284 );
8285 let sample_text_2 = sample_text(rows, cols, 'l');
8286 assert_eq!(
8287 sample_text_2,
8288 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
8289 );
8290 let sample_text_3 = sample_text(rows, cols, 'v');
8291 assert_eq!(
8292 sample_text_3,
8293 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
8294 );
8295
8296 let fs = FakeFs::new(cx.executor());
8297 fs.insert_tree(
8298 path!("/a"),
8299 json!({
8300 "main.rs": sample_text_1,
8301 "other.rs": sample_text_2,
8302 "lib.rs": sample_text_3,
8303 }),
8304 )
8305 .await;
8306
8307 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
8308 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
8309 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
8310
8311 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8312 language_registry.add(rust_lang());
8313 let mut fake_servers = language_registry.register_fake_lsp(
8314 "Rust",
8315 FakeLspAdapter {
8316 capabilities: lsp::ServerCapabilities {
8317 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8318 ..Default::default()
8319 },
8320 ..Default::default()
8321 },
8322 );
8323
8324 let worktree = project.update(cx, |project, cx| {
8325 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
8326 assert_eq!(worktrees.len(), 1);
8327 worktrees.pop().unwrap()
8328 });
8329 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
8330
8331 let buffer_1 = project
8332 .update(cx, |project, cx| {
8333 project.open_buffer((worktree_id, "main.rs"), cx)
8334 })
8335 .await
8336 .unwrap();
8337 let buffer_2 = project
8338 .update(cx, |project, cx| {
8339 project.open_buffer((worktree_id, "other.rs"), cx)
8340 })
8341 .await
8342 .unwrap();
8343 let buffer_3 = project
8344 .update(cx, |project, cx| {
8345 project.open_buffer((worktree_id, "lib.rs"), cx)
8346 })
8347 .await
8348 .unwrap();
8349
8350 let multi_buffer = cx.new(|cx| {
8351 let mut multi_buffer = MultiBuffer::new(ReadWrite);
8352 multi_buffer.push_excerpts(
8353 buffer_1.clone(),
8354 [
8355 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8356 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8357 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8358 ],
8359 cx,
8360 );
8361 multi_buffer.push_excerpts(
8362 buffer_2.clone(),
8363 [
8364 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8365 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8366 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8367 ],
8368 cx,
8369 );
8370 multi_buffer.push_excerpts(
8371 buffer_3.clone(),
8372 [
8373 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8374 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8375 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8376 ],
8377 cx,
8378 );
8379 multi_buffer
8380 });
8381 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
8382 Editor::new(
8383 EditorMode::full(),
8384 multi_buffer,
8385 Some(project.clone()),
8386 window,
8387 cx,
8388 )
8389 });
8390
8391 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8392 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8393 s.select_ranges(Some(1..2))
8394 });
8395 editor.insert("|one|two|three|", window, cx);
8396 });
8397 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8398 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8399 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8400 s.select_ranges(Some(60..70))
8401 });
8402 editor.insert("|four|five|six|", window, cx);
8403 });
8404 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8405
8406 // First two buffers should be edited, but not the third one.
8407 assert_eq!(
8408 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8409 "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}",
8410 );
8411 buffer_1.update(cx, |buffer, _| {
8412 assert!(buffer.is_dirty());
8413 assert_eq!(
8414 buffer.text(),
8415 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
8416 )
8417 });
8418 buffer_2.update(cx, |buffer, _| {
8419 assert!(buffer.is_dirty());
8420 assert_eq!(
8421 buffer.text(),
8422 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
8423 )
8424 });
8425 buffer_3.update(cx, |buffer, _| {
8426 assert!(!buffer.is_dirty());
8427 assert_eq!(buffer.text(), sample_text_3,)
8428 });
8429 cx.executor().run_until_parked();
8430
8431 cx.executor().start_waiting();
8432 let save = multi_buffer_editor
8433 .update_in(cx, |editor, window, cx| {
8434 editor.save(true, project.clone(), window, cx)
8435 })
8436 .unwrap();
8437
8438 let fake_server = fake_servers.next().await.unwrap();
8439 fake_server
8440 .server
8441 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
8442 Ok(Some(vec![lsp::TextEdit::new(
8443 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8444 format!("[{} formatted]", params.text_document.uri),
8445 )]))
8446 })
8447 .detach();
8448 save.await;
8449
8450 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
8451 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
8452 assert_eq!(
8453 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8454 uri!(
8455 "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}"
8456 ),
8457 );
8458 buffer_1.update(cx, |buffer, _| {
8459 assert!(!buffer.is_dirty());
8460 assert_eq!(
8461 buffer.text(),
8462 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
8463 )
8464 });
8465 buffer_2.update(cx, |buffer, _| {
8466 assert!(!buffer.is_dirty());
8467 assert_eq!(
8468 buffer.text(),
8469 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
8470 )
8471 });
8472 buffer_3.update(cx, |buffer, _| {
8473 assert!(!buffer.is_dirty());
8474 assert_eq!(buffer.text(), sample_text_3,)
8475 });
8476}
8477
8478#[gpui::test]
8479async fn test_range_format_during_save(cx: &mut TestAppContext) {
8480 init_test(cx, |_| {});
8481
8482 let fs = FakeFs::new(cx.executor());
8483 fs.insert_file(path!("/file.rs"), Default::default()).await;
8484
8485 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8486
8487 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8488 language_registry.add(rust_lang());
8489 let mut fake_servers = language_registry.register_fake_lsp(
8490 "Rust",
8491 FakeLspAdapter {
8492 capabilities: lsp::ServerCapabilities {
8493 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
8494 ..Default::default()
8495 },
8496 ..Default::default()
8497 },
8498 );
8499
8500 let buffer = project
8501 .update(cx, |project, cx| {
8502 project.open_local_buffer(path!("/file.rs"), cx)
8503 })
8504 .await
8505 .unwrap();
8506
8507 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8508 let (editor, cx) = cx.add_window_view(|window, cx| {
8509 build_editor_with_project(project.clone(), buffer, window, cx)
8510 });
8511 editor.update_in(cx, |editor, window, cx| {
8512 editor.set_text("one\ntwo\nthree\n", window, cx)
8513 });
8514 assert!(cx.read(|cx| editor.is_dirty(cx)));
8515
8516 cx.executor().start_waiting();
8517 let fake_server = fake_servers.next().await.unwrap();
8518
8519 let save = editor
8520 .update_in(cx, |editor, window, cx| {
8521 editor.save(true, project.clone(), window, cx)
8522 })
8523 .unwrap();
8524 fake_server
8525 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8526 assert_eq!(
8527 params.text_document.uri,
8528 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8529 );
8530 assert_eq!(params.options.tab_size, 4);
8531 Ok(Some(vec![lsp::TextEdit::new(
8532 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8533 ", ".to_string(),
8534 )]))
8535 })
8536 .next()
8537 .await;
8538 cx.executor().start_waiting();
8539 save.await;
8540 assert_eq!(
8541 editor.update(cx, |editor, cx| editor.text(cx)),
8542 "one, two\nthree\n"
8543 );
8544 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8545
8546 editor.update_in(cx, |editor, window, cx| {
8547 editor.set_text("one\ntwo\nthree\n", window, cx)
8548 });
8549 assert!(cx.read(|cx| editor.is_dirty(cx)));
8550
8551 // Ensure we can still save even if formatting hangs.
8552 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
8553 move |params, _| async move {
8554 assert_eq!(
8555 params.text_document.uri,
8556 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8557 );
8558 futures::future::pending::<()>().await;
8559 unreachable!()
8560 },
8561 );
8562 let save = editor
8563 .update_in(cx, |editor, window, cx| {
8564 editor.save(true, project.clone(), window, cx)
8565 })
8566 .unwrap();
8567 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8568 cx.executor().start_waiting();
8569 save.await;
8570 assert_eq!(
8571 editor.update(cx, |editor, cx| editor.text(cx)),
8572 "one\ntwo\nthree\n"
8573 );
8574 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8575
8576 // For non-dirty buffer, no formatting request should be sent
8577 let save = editor
8578 .update_in(cx, |editor, window, cx| {
8579 editor.save(true, project.clone(), window, cx)
8580 })
8581 .unwrap();
8582 let _pending_format_request = fake_server
8583 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
8584 panic!("Should not be invoked on non-dirty buffer");
8585 })
8586 .next();
8587 cx.executor().start_waiting();
8588 save.await;
8589
8590 // Set Rust language override and assert overridden tabsize is sent to language server
8591 update_test_language_settings(cx, |settings| {
8592 settings.languages.insert(
8593 "Rust".into(),
8594 LanguageSettingsContent {
8595 tab_size: NonZeroU32::new(8),
8596 ..Default::default()
8597 },
8598 );
8599 });
8600
8601 editor.update_in(cx, |editor, window, cx| {
8602 editor.set_text("somehting_new\n", window, cx)
8603 });
8604 assert!(cx.read(|cx| editor.is_dirty(cx)));
8605 let save = editor
8606 .update_in(cx, |editor, window, cx| {
8607 editor.save(true, project.clone(), window, cx)
8608 })
8609 .unwrap();
8610 fake_server
8611 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8612 assert_eq!(
8613 params.text_document.uri,
8614 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8615 );
8616 assert_eq!(params.options.tab_size, 8);
8617 Ok(Some(vec![]))
8618 })
8619 .next()
8620 .await;
8621 cx.executor().start_waiting();
8622 save.await;
8623}
8624
8625#[gpui::test]
8626async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
8627 init_test(cx, |settings| {
8628 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8629 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8630 ))
8631 });
8632
8633 let fs = FakeFs::new(cx.executor());
8634 fs.insert_file(path!("/file.rs"), Default::default()).await;
8635
8636 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8637
8638 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8639 language_registry.add(Arc::new(Language::new(
8640 LanguageConfig {
8641 name: "Rust".into(),
8642 matcher: LanguageMatcher {
8643 path_suffixes: vec!["rs".to_string()],
8644 ..Default::default()
8645 },
8646 ..LanguageConfig::default()
8647 },
8648 Some(tree_sitter_rust::LANGUAGE.into()),
8649 )));
8650 update_test_language_settings(cx, |settings| {
8651 // Enable Prettier formatting for the same buffer, and ensure
8652 // LSP is called instead of Prettier.
8653 settings.defaults.prettier = Some(PrettierSettings {
8654 allowed: true,
8655 ..PrettierSettings::default()
8656 });
8657 });
8658 let mut fake_servers = language_registry.register_fake_lsp(
8659 "Rust",
8660 FakeLspAdapter {
8661 capabilities: lsp::ServerCapabilities {
8662 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8663 ..Default::default()
8664 },
8665 ..Default::default()
8666 },
8667 );
8668
8669 let buffer = project
8670 .update(cx, |project, cx| {
8671 project.open_local_buffer(path!("/file.rs"), cx)
8672 })
8673 .await
8674 .unwrap();
8675
8676 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8677 let (editor, cx) = cx.add_window_view(|window, cx| {
8678 build_editor_with_project(project.clone(), buffer, window, cx)
8679 });
8680 editor.update_in(cx, |editor, window, cx| {
8681 editor.set_text("one\ntwo\nthree\n", window, cx)
8682 });
8683
8684 cx.executor().start_waiting();
8685 let fake_server = fake_servers.next().await.unwrap();
8686
8687 let format = editor
8688 .update_in(cx, |editor, window, cx| {
8689 editor.perform_format(
8690 project.clone(),
8691 FormatTrigger::Manual,
8692 FormatTarget::Buffers,
8693 window,
8694 cx,
8695 )
8696 })
8697 .unwrap();
8698 fake_server
8699 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8700 assert_eq!(
8701 params.text_document.uri,
8702 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8703 );
8704 assert_eq!(params.options.tab_size, 4);
8705 Ok(Some(vec![lsp::TextEdit::new(
8706 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8707 ", ".to_string(),
8708 )]))
8709 })
8710 .next()
8711 .await;
8712 cx.executor().start_waiting();
8713 format.await;
8714 assert_eq!(
8715 editor.update(cx, |editor, cx| editor.text(cx)),
8716 "one, two\nthree\n"
8717 );
8718
8719 editor.update_in(cx, |editor, window, cx| {
8720 editor.set_text("one\ntwo\nthree\n", window, cx)
8721 });
8722 // Ensure we don't lock if formatting hangs.
8723 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8724 move |params, _| async move {
8725 assert_eq!(
8726 params.text_document.uri,
8727 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8728 );
8729 futures::future::pending::<()>().await;
8730 unreachable!()
8731 },
8732 );
8733 let format = editor
8734 .update_in(cx, |editor, window, cx| {
8735 editor.perform_format(
8736 project,
8737 FormatTrigger::Manual,
8738 FormatTarget::Buffers,
8739 window,
8740 cx,
8741 )
8742 })
8743 .unwrap();
8744 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8745 cx.executor().start_waiting();
8746 format.await;
8747 assert_eq!(
8748 editor.update(cx, |editor, cx| editor.text(cx)),
8749 "one\ntwo\nthree\n"
8750 );
8751}
8752
8753#[gpui::test]
8754async fn test_multiple_formatters(cx: &mut TestAppContext) {
8755 init_test(cx, |settings| {
8756 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
8757 settings.defaults.formatter =
8758 Some(language_settings::SelectedFormatter::List(FormatterList(
8759 vec![
8760 Formatter::LanguageServer { name: None },
8761 Formatter::CodeActions(
8762 [
8763 ("code-action-1".into(), true),
8764 ("code-action-2".into(), true),
8765 ]
8766 .into_iter()
8767 .collect(),
8768 ),
8769 ]
8770 .into(),
8771 )))
8772 });
8773
8774 let fs = FakeFs::new(cx.executor());
8775 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
8776 .await;
8777
8778 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8779 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8780 language_registry.add(rust_lang());
8781
8782 let mut fake_servers = language_registry.register_fake_lsp(
8783 "Rust",
8784 FakeLspAdapter {
8785 capabilities: lsp::ServerCapabilities {
8786 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8787 execute_command_provider: Some(lsp::ExecuteCommandOptions {
8788 commands: vec!["the-command-for-code-action-1".into()],
8789 ..Default::default()
8790 }),
8791 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
8792 ..Default::default()
8793 },
8794 ..Default::default()
8795 },
8796 );
8797
8798 let buffer = project
8799 .update(cx, |project, cx| {
8800 project.open_local_buffer(path!("/file.rs"), cx)
8801 })
8802 .await
8803 .unwrap();
8804
8805 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8806 let (editor, cx) = cx.add_window_view(|window, cx| {
8807 build_editor_with_project(project.clone(), buffer, window, cx)
8808 });
8809
8810 cx.executor().start_waiting();
8811
8812 let fake_server = fake_servers.next().await.unwrap();
8813 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8814 move |_params, _| async move {
8815 Ok(Some(vec![lsp::TextEdit::new(
8816 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8817 "applied-formatting\n".to_string(),
8818 )]))
8819 },
8820 );
8821 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
8822 move |params, _| async move {
8823 assert_eq!(
8824 params.context.only,
8825 Some(vec!["code-action-1".into(), "code-action-2".into()])
8826 );
8827 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
8828 Ok(Some(vec![
8829 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
8830 kind: Some("code-action-1".into()),
8831 edit: Some(lsp::WorkspaceEdit::new(
8832 [(
8833 uri.clone(),
8834 vec![lsp::TextEdit::new(
8835 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8836 "applied-code-action-1-edit\n".to_string(),
8837 )],
8838 )]
8839 .into_iter()
8840 .collect(),
8841 )),
8842 command: Some(lsp::Command {
8843 command: "the-command-for-code-action-1".into(),
8844 ..Default::default()
8845 }),
8846 ..Default::default()
8847 }),
8848 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
8849 kind: Some("code-action-2".into()),
8850 edit: Some(lsp::WorkspaceEdit::new(
8851 [(
8852 uri.clone(),
8853 vec![lsp::TextEdit::new(
8854 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8855 "applied-code-action-2-edit\n".to_string(),
8856 )],
8857 )]
8858 .into_iter()
8859 .collect(),
8860 )),
8861 ..Default::default()
8862 }),
8863 ]))
8864 },
8865 );
8866
8867 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
8868 move |params, _| async move { Ok(params) }
8869 });
8870
8871 let command_lock = Arc::new(futures::lock::Mutex::new(()));
8872 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
8873 let fake = fake_server.clone();
8874 let lock = command_lock.clone();
8875 move |params, _| {
8876 assert_eq!(params.command, "the-command-for-code-action-1");
8877 let fake = fake.clone();
8878 let lock = lock.clone();
8879 async move {
8880 lock.lock().await;
8881 fake.server
8882 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
8883 label: None,
8884 edit: lsp::WorkspaceEdit {
8885 changes: Some(
8886 [(
8887 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
8888 vec![lsp::TextEdit {
8889 range: lsp::Range::new(
8890 lsp::Position::new(0, 0),
8891 lsp::Position::new(0, 0),
8892 ),
8893 new_text: "applied-code-action-1-command\n".into(),
8894 }],
8895 )]
8896 .into_iter()
8897 .collect(),
8898 ),
8899 ..Default::default()
8900 },
8901 })
8902 .await
8903 .unwrap();
8904 Ok(Some(json!(null)))
8905 }
8906 }
8907 });
8908
8909 cx.executor().start_waiting();
8910 editor
8911 .update_in(cx, |editor, window, cx| {
8912 editor.perform_format(
8913 project.clone(),
8914 FormatTrigger::Manual,
8915 FormatTarget::Buffers,
8916 window,
8917 cx,
8918 )
8919 })
8920 .unwrap()
8921 .await;
8922 editor.update(cx, |editor, cx| {
8923 assert_eq!(
8924 editor.text(cx),
8925 r#"
8926 applied-code-action-2-edit
8927 applied-code-action-1-command
8928 applied-code-action-1-edit
8929 applied-formatting
8930 one
8931 two
8932 three
8933 "#
8934 .unindent()
8935 );
8936 });
8937
8938 editor.update_in(cx, |editor, window, cx| {
8939 editor.undo(&Default::default(), window, cx);
8940 assert_eq!(editor.text(cx), "one \ntwo \nthree");
8941 });
8942
8943 // Perform a manual edit while waiting for an LSP command
8944 // that's being run as part of a formatting code action.
8945 let lock_guard = command_lock.lock().await;
8946 let format = editor
8947 .update_in(cx, |editor, window, cx| {
8948 editor.perform_format(
8949 project.clone(),
8950 FormatTrigger::Manual,
8951 FormatTarget::Buffers,
8952 window,
8953 cx,
8954 )
8955 })
8956 .unwrap();
8957 cx.run_until_parked();
8958 editor.update(cx, |editor, cx| {
8959 assert_eq!(
8960 editor.text(cx),
8961 r#"
8962 applied-code-action-1-edit
8963 applied-formatting
8964 one
8965 two
8966 three
8967 "#
8968 .unindent()
8969 );
8970
8971 editor.buffer.update(cx, |buffer, cx| {
8972 let ix = buffer.len(cx);
8973 buffer.edit([(ix..ix, "edited\n")], None, cx);
8974 });
8975 });
8976
8977 // Allow the LSP command to proceed. Because the buffer was edited,
8978 // the second code action will not be run.
8979 drop(lock_guard);
8980 format.await;
8981 editor.update_in(cx, |editor, window, cx| {
8982 assert_eq!(
8983 editor.text(cx),
8984 r#"
8985 applied-code-action-1-command
8986 applied-code-action-1-edit
8987 applied-formatting
8988 one
8989 two
8990 three
8991 edited
8992 "#
8993 .unindent()
8994 );
8995
8996 // The manual edit is undone first, because it is the last thing the user did
8997 // (even though the command completed afterwards).
8998 editor.undo(&Default::default(), window, cx);
8999 assert_eq!(
9000 editor.text(cx),
9001 r#"
9002 applied-code-action-1-command
9003 applied-code-action-1-edit
9004 applied-formatting
9005 one
9006 two
9007 three
9008 "#
9009 .unindent()
9010 );
9011
9012 // All the formatting (including the command, which completed after the manual edit)
9013 // is undone together.
9014 editor.undo(&Default::default(), window, cx);
9015 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9016 });
9017}
9018
9019#[gpui::test]
9020async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
9021 init_test(cx, |settings| {
9022 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
9023 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
9024 ))
9025 });
9026
9027 let fs = FakeFs::new(cx.executor());
9028 fs.insert_file(path!("/file.ts"), Default::default()).await;
9029
9030 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9031
9032 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9033 language_registry.add(Arc::new(Language::new(
9034 LanguageConfig {
9035 name: "TypeScript".into(),
9036 matcher: LanguageMatcher {
9037 path_suffixes: vec!["ts".to_string()],
9038 ..Default::default()
9039 },
9040 ..LanguageConfig::default()
9041 },
9042 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
9043 )));
9044 update_test_language_settings(cx, |settings| {
9045 settings.defaults.prettier = Some(PrettierSettings {
9046 allowed: true,
9047 ..PrettierSettings::default()
9048 });
9049 });
9050 let mut fake_servers = language_registry.register_fake_lsp(
9051 "TypeScript",
9052 FakeLspAdapter {
9053 capabilities: lsp::ServerCapabilities {
9054 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9055 ..Default::default()
9056 },
9057 ..Default::default()
9058 },
9059 );
9060
9061 let buffer = project
9062 .update(cx, |project, cx| {
9063 project.open_local_buffer(path!("/file.ts"), cx)
9064 })
9065 .await
9066 .unwrap();
9067
9068 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9069 let (editor, cx) = cx.add_window_view(|window, cx| {
9070 build_editor_with_project(project.clone(), buffer, window, cx)
9071 });
9072 editor.update_in(cx, |editor, window, cx| {
9073 editor.set_text(
9074 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9075 window,
9076 cx,
9077 )
9078 });
9079
9080 cx.executor().start_waiting();
9081 let fake_server = fake_servers.next().await.unwrap();
9082
9083 let format = editor
9084 .update_in(cx, |editor, window, cx| {
9085 editor.perform_code_action_kind(
9086 project.clone(),
9087 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9088 window,
9089 cx,
9090 )
9091 })
9092 .unwrap();
9093 fake_server
9094 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
9095 assert_eq!(
9096 params.text_document.uri,
9097 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9098 );
9099 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
9100 lsp::CodeAction {
9101 title: "Organize Imports".to_string(),
9102 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
9103 edit: Some(lsp::WorkspaceEdit {
9104 changes: Some(
9105 [(
9106 params.text_document.uri.clone(),
9107 vec![lsp::TextEdit::new(
9108 lsp::Range::new(
9109 lsp::Position::new(1, 0),
9110 lsp::Position::new(2, 0),
9111 ),
9112 "".to_string(),
9113 )],
9114 )]
9115 .into_iter()
9116 .collect(),
9117 ),
9118 ..Default::default()
9119 }),
9120 ..Default::default()
9121 },
9122 )]))
9123 })
9124 .next()
9125 .await;
9126 cx.executor().start_waiting();
9127 format.await;
9128 assert_eq!(
9129 editor.update(cx, |editor, cx| editor.text(cx)),
9130 "import { a } from 'module';\n\nconst x = a;\n"
9131 );
9132
9133 editor.update_in(cx, |editor, window, cx| {
9134 editor.set_text(
9135 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9136 window,
9137 cx,
9138 )
9139 });
9140 // Ensure we don't lock if code action hangs.
9141 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9142 move |params, _| async move {
9143 assert_eq!(
9144 params.text_document.uri,
9145 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9146 );
9147 futures::future::pending::<()>().await;
9148 unreachable!()
9149 },
9150 );
9151 let format = editor
9152 .update_in(cx, |editor, window, cx| {
9153 editor.perform_code_action_kind(
9154 project,
9155 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9156 window,
9157 cx,
9158 )
9159 })
9160 .unwrap();
9161 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
9162 cx.executor().start_waiting();
9163 format.await;
9164 assert_eq!(
9165 editor.update(cx, |editor, cx| editor.text(cx)),
9166 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
9167 );
9168}
9169
9170#[gpui::test]
9171async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
9172 init_test(cx, |_| {});
9173
9174 let mut cx = EditorLspTestContext::new_rust(
9175 lsp::ServerCapabilities {
9176 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9177 ..Default::default()
9178 },
9179 cx,
9180 )
9181 .await;
9182
9183 cx.set_state(indoc! {"
9184 one.twoˇ
9185 "});
9186
9187 // The format request takes a long time. When it completes, it inserts
9188 // a newline and an indent before the `.`
9189 cx.lsp
9190 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
9191 let executor = cx.background_executor().clone();
9192 async move {
9193 executor.timer(Duration::from_millis(100)).await;
9194 Ok(Some(vec![lsp::TextEdit {
9195 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
9196 new_text: "\n ".into(),
9197 }]))
9198 }
9199 });
9200
9201 // Submit a format request.
9202 let format_1 = cx
9203 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9204 .unwrap();
9205 cx.executor().run_until_parked();
9206
9207 // Submit a second format request.
9208 let format_2 = cx
9209 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9210 .unwrap();
9211 cx.executor().run_until_parked();
9212
9213 // Wait for both format requests to complete
9214 cx.executor().advance_clock(Duration::from_millis(200));
9215 cx.executor().start_waiting();
9216 format_1.await.unwrap();
9217 cx.executor().start_waiting();
9218 format_2.await.unwrap();
9219
9220 // The formatting edits only happens once.
9221 cx.assert_editor_state(indoc! {"
9222 one
9223 .twoˇ
9224 "});
9225}
9226
9227#[gpui::test]
9228async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
9229 init_test(cx, |settings| {
9230 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
9231 });
9232
9233 let mut cx = EditorLspTestContext::new_rust(
9234 lsp::ServerCapabilities {
9235 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9236 ..Default::default()
9237 },
9238 cx,
9239 )
9240 .await;
9241
9242 // Set up a buffer white some trailing whitespace and no trailing newline.
9243 cx.set_state(
9244 &[
9245 "one ", //
9246 "twoˇ", //
9247 "three ", //
9248 "four", //
9249 ]
9250 .join("\n"),
9251 );
9252
9253 // Submit a format request.
9254 let format = cx
9255 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9256 .unwrap();
9257
9258 // Record which buffer changes have been sent to the language server
9259 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
9260 cx.lsp
9261 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
9262 let buffer_changes = buffer_changes.clone();
9263 move |params, _| {
9264 buffer_changes.lock().extend(
9265 params
9266 .content_changes
9267 .into_iter()
9268 .map(|e| (e.range.unwrap(), e.text)),
9269 );
9270 }
9271 });
9272
9273 // Handle formatting requests to the language server.
9274 cx.lsp
9275 .set_request_handler::<lsp::request::Formatting, _, _>({
9276 let buffer_changes = buffer_changes.clone();
9277 move |_, _| {
9278 // When formatting is requested, trailing whitespace has already been stripped,
9279 // and the trailing newline has already been added.
9280 assert_eq!(
9281 &buffer_changes.lock()[1..],
9282 &[
9283 (
9284 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
9285 "".into()
9286 ),
9287 (
9288 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
9289 "".into()
9290 ),
9291 (
9292 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
9293 "\n".into()
9294 ),
9295 ]
9296 );
9297
9298 // Insert blank lines between each line of the buffer.
9299 async move {
9300 Ok(Some(vec![
9301 lsp::TextEdit {
9302 range: lsp::Range::new(
9303 lsp::Position::new(1, 0),
9304 lsp::Position::new(1, 0),
9305 ),
9306 new_text: "\n".into(),
9307 },
9308 lsp::TextEdit {
9309 range: lsp::Range::new(
9310 lsp::Position::new(2, 0),
9311 lsp::Position::new(2, 0),
9312 ),
9313 new_text: "\n".into(),
9314 },
9315 ]))
9316 }
9317 }
9318 });
9319
9320 // After formatting the buffer, the trailing whitespace is stripped,
9321 // a newline is appended, and the edits provided by the language server
9322 // have been applied.
9323 format.await.unwrap();
9324 cx.assert_editor_state(
9325 &[
9326 "one", //
9327 "", //
9328 "twoˇ", //
9329 "", //
9330 "three", //
9331 "four", //
9332 "", //
9333 ]
9334 .join("\n"),
9335 );
9336
9337 // Undoing the formatting undoes the trailing whitespace removal, the
9338 // trailing newline, and the LSP edits.
9339 cx.update_buffer(|buffer, cx| buffer.undo(cx));
9340 cx.assert_editor_state(
9341 &[
9342 "one ", //
9343 "twoˇ", //
9344 "three ", //
9345 "four", //
9346 ]
9347 .join("\n"),
9348 );
9349}
9350
9351#[gpui::test]
9352async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
9353 cx: &mut TestAppContext,
9354) {
9355 init_test(cx, |_| {});
9356
9357 cx.update(|cx| {
9358 cx.update_global::<SettingsStore, _>(|settings, cx| {
9359 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9360 settings.auto_signature_help = Some(true);
9361 });
9362 });
9363 });
9364
9365 let mut cx = EditorLspTestContext::new_rust(
9366 lsp::ServerCapabilities {
9367 signature_help_provider: Some(lsp::SignatureHelpOptions {
9368 ..Default::default()
9369 }),
9370 ..Default::default()
9371 },
9372 cx,
9373 )
9374 .await;
9375
9376 let language = Language::new(
9377 LanguageConfig {
9378 name: "Rust".into(),
9379 brackets: BracketPairConfig {
9380 pairs: vec![
9381 BracketPair {
9382 start: "{".to_string(),
9383 end: "}".to_string(),
9384 close: true,
9385 surround: true,
9386 newline: true,
9387 },
9388 BracketPair {
9389 start: "(".to_string(),
9390 end: ")".to_string(),
9391 close: true,
9392 surround: true,
9393 newline: true,
9394 },
9395 BracketPair {
9396 start: "/*".to_string(),
9397 end: " */".to_string(),
9398 close: true,
9399 surround: true,
9400 newline: true,
9401 },
9402 BracketPair {
9403 start: "[".to_string(),
9404 end: "]".to_string(),
9405 close: false,
9406 surround: false,
9407 newline: true,
9408 },
9409 BracketPair {
9410 start: "\"".to_string(),
9411 end: "\"".to_string(),
9412 close: true,
9413 surround: true,
9414 newline: false,
9415 },
9416 BracketPair {
9417 start: "<".to_string(),
9418 end: ">".to_string(),
9419 close: false,
9420 surround: true,
9421 newline: true,
9422 },
9423 ],
9424 ..Default::default()
9425 },
9426 autoclose_before: "})]".to_string(),
9427 ..Default::default()
9428 },
9429 Some(tree_sitter_rust::LANGUAGE.into()),
9430 );
9431 let language = Arc::new(language);
9432
9433 cx.language_registry().add(language.clone());
9434 cx.update_buffer(|buffer, cx| {
9435 buffer.set_language(Some(language), cx);
9436 });
9437
9438 cx.set_state(
9439 &r#"
9440 fn main() {
9441 sampleˇ
9442 }
9443 "#
9444 .unindent(),
9445 );
9446
9447 cx.update_editor(|editor, window, cx| {
9448 editor.handle_input("(", window, cx);
9449 });
9450 cx.assert_editor_state(
9451 &"
9452 fn main() {
9453 sample(ˇ)
9454 }
9455 "
9456 .unindent(),
9457 );
9458
9459 let mocked_response = lsp::SignatureHelp {
9460 signatures: vec![lsp::SignatureInformation {
9461 label: "fn sample(param1: u8, param2: u8)".to_string(),
9462 documentation: None,
9463 parameters: Some(vec![
9464 lsp::ParameterInformation {
9465 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9466 documentation: None,
9467 },
9468 lsp::ParameterInformation {
9469 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9470 documentation: None,
9471 },
9472 ]),
9473 active_parameter: None,
9474 }],
9475 active_signature: Some(0),
9476 active_parameter: Some(0),
9477 };
9478 handle_signature_help_request(&mut cx, mocked_response).await;
9479
9480 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9481 .await;
9482
9483 cx.editor(|editor, _, _| {
9484 let signature_help_state = editor.signature_help_state.popover().cloned();
9485 assert_eq!(
9486 signature_help_state.unwrap().label,
9487 "param1: u8, param2: u8"
9488 );
9489 });
9490}
9491
9492#[gpui::test]
9493async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
9494 init_test(cx, |_| {});
9495
9496 cx.update(|cx| {
9497 cx.update_global::<SettingsStore, _>(|settings, cx| {
9498 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9499 settings.auto_signature_help = Some(false);
9500 settings.show_signature_help_after_edits = Some(false);
9501 });
9502 });
9503 });
9504
9505 let mut cx = EditorLspTestContext::new_rust(
9506 lsp::ServerCapabilities {
9507 signature_help_provider: Some(lsp::SignatureHelpOptions {
9508 ..Default::default()
9509 }),
9510 ..Default::default()
9511 },
9512 cx,
9513 )
9514 .await;
9515
9516 let language = Language::new(
9517 LanguageConfig {
9518 name: "Rust".into(),
9519 brackets: BracketPairConfig {
9520 pairs: vec![
9521 BracketPair {
9522 start: "{".to_string(),
9523 end: "}".to_string(),
9524 close: true,
9525 surround: true,
9526 newline: true,
9527 },
9528 BracketPair {
9529 start: "(".to_string(),
9530 end: ")".to_string(),
9531 close: true,
9532 surround: true,
9533 newline: true,
9534 },
9535 BracketPair {
9536 start: "/*".to_string(),
9537 end: " */".to_string(),
9538 close: true,
9539 surround: true,
9540 newline: true,
9541 },
9542 BracketPair {
9543 start: "[".to_string(),
9544 end: "]".to_string(),
9545 close: false,
9546 surround: false,
9547 newline: true,
9548 },
9549 BracketPair {
9550 start: "\"".to_string(),
9551 end: "\"".to_string(),
9552 close: true,
9553 surround: true,
9554 newline: false,
9555 },
9556 BracketPair {
9557 start: "<".to_string(),
9558 end: ">".to_string(),
9559 close: false,
9560 surround: true,
9561 newline: true,
9562 },
9563 ],
9564 ..Default::default()
9565 },
9566 autoclose_before: "})]".to_string(),
9567 ..Default::default()
9568 },
9569 Some(tree_sitter_rust::LANGUAGE.into()),
9570 );
9571 let language = Arc::new(language);
9572
9573 cx.language_registry().add(language.clone());
9574 cx.update_buffer(|buffer, cx| {
9575 buffer.set_language(Some(language), cx);
9576 });
9577
9578 // Ensure that signature_help is not called when no signature help is enabled.
9579 cx.set_state(
9580 &r#"
9581 fn main() {
9582 sampleˇ
9583 }
9584 "#
9585 .unindent(),
9586 );
9587 cx.update_editor(|editor, window, cx| {
9588 editor.handle_input("(", window, cx);
9589 });
9590 cx.assert_editor_state(
9591 &"
9592 fn main() {
9593 sample(ˇ)
9594 }
9595 "
9596 .unindent(),
9597 );
9598 cx.editor(|editor, _, _| {
9599 assert!(editor.signature_help_state.task().is_none());
9600 });
9601
9602 let mocked_response = lsp::SignatureHelp {
9603 signatures: vec![lsp::SignatureInformation {
9604 label: "fn sample(param1: u8, param2: u8)".to_string(),
9605 documentation: None,
9606 parameters: Some(vec![
9607 lsp::ParameterInformation {
9608 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9609 documentation: None,
9610 },
9611 lsp::ParameterInformation {
9612 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9613 documentation: None,
9614 },
9615 ]),
9616 active_parameter: None,
9617 }],
9618 active_signature: Some(0),
9619 active_parameter: Some(0),
9620 };
9621
9622 // Ensure that signature_help is called when enabled afte edits
9623 cx.update(|_, cx| {
9624 cx.update_global::<SettingsStore, _>(|settings, cx| {
9625 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9626 settings.auto_signature_help = Some(false);
9627 settings.show_signature_help_after_edits = Some(true);
9628 });
9629 });
9630 });
9631 cx.set_state(
9632 &r#"
9633 fn main() {
9634 sampleˇ
9635 }
9636 "#
9637 .unindent(),
9638 );
9639 cx.update_editor(|editor, window, cx| {
9640 editor.handle_input("(", window, cx);
9641 });
9642 cx.assert_editor_state(
9643 &"
9644 fn main() {
9645 sample(ˇ)
9646 }
9647 "
9648 .unindent(),
9649 );
9650 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9651 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9652 .await;
9653 cx.update_editor(|editor, _, _| {
9654 let signature_help_state = editor.signature_help_state.popover().cloned();
9655 assert!(signature_help_state.is_some());
9656 assert_eq!(
9657 signature_help_state.unwrap().label,
9658 "param1: u8, param2: u8"
9659 );
9660 editor.signature_help_state = SignatureHelpState::default();
9661 });
9662
9663 // Ensure that signature_help is called when auto signature help override is enabled
9664 cx.update(|_, cx| {
9665 cx.update_global::<SettingsStore, _>(|settings, cx| {
9666 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9667 settings.auto_signature_help = Some(true);
9668 settings.show_signature_help_after_edits = Some(false);
9669 });
9670 });
9671 });
9672 cx.set_state(
9673 &r#"
9674 fn main() {
9675 sampleˇ
9676 }
9677 "#
9678 .unindent(),
9679 );
9680 cx.update_editor(|editor, window, cx| {
9681 editor.handle_input("(", window, cx);
9682 });
9683 cx.assert_editor_state(
9684 &"
9685 fn main() {
9686 sample(ˇ)
9687 }
9688 "
9689 .unindent(),
9690 );
9691 handle_signature_help_request(&mut cx, mocked_response).await;
9692 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9693 .await;
9694 cx.editor(|editor, _, _| {
9695 let signature_help_state = editor.signature_help_state.popover().cloned();
9696 assert!(signature_help_state.is_some());
9697 assert_eq!(
9698 signature_help_state.unwrap().label,
9699 "param1: u8, param2: u8"
9700 );
9701 });
9702}
9703
9704#[gpui::test]
9705async fn test_signature_help(cx: &mut TestAppContext) {
9706 init_test(cx, |_| {});
9707 cx.update(|cx| {
9708 cx.update_global::<SettingsStore, _>(|settings, cx| {
9709 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9710 settings.auto_signature_help = Some(true);
9711 });
9712 });
9713 });
9714
9715 let mut cx = EditorLspTestContext::new_rust(
9716 lsp::ServerCapabilities {
9717 signature_help_provider: Some(lsp::SignatureHelpOptions {
9718 ..Default::default()
9719 }),
9720 ..Default::default()
9721 },
9722 cx,
9723 )
9724 .await;
9725
9726 // A test that directly calls `show_signature_help`
9727 cx.update_editor(|editor, window, cx| {
9728 editor.show_signature_help(&ShowSignatureHelp, window, cx);
9729 });
9730
9731 let mocked_response = lsp::SignatureHelp {
9732 signatures: vec![lsp::SignatureInformation {
9733 label: "fn sample(param1: u8, param2: u8)".to_string(),
9734 documentation: None,
9735 parameters: Some(vec![
9736 lsp::ParameterInformation {
9737 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9738 documentation: None,
9739 },
9740 lsp::ParameterInformation {
9741 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9742 documentation: None,
9743 },
9744 ]),
9745 active_parameter: None,
9746 }],
9747 active_signature: Some(0),
9748 active_parameter: Some(0),
9749 };
9750 handle_signature_help_request(&mut cx, mocked_response).await;
9751
9752 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9753 .await;
9754
9755 cx.editor(|editor, _, _| {
9756 let signature_help_state = editor.signature_help_state.popover().cloned();
9757 assert!(signature_help_state.is_some());
9758 assert_eq!(
9759 signature_help_state.unwrap().label,
9760 "param1: u8, param2: u8"
9761 );
9762 });
9763
9764 // When exiting outside from inside the brackets, `signature_help` is closed.
9765 cx.set_state(indoc! {"
9766 fn main() {
9767 sample(ˇ);
9768 }
9769
9770 fn sample(param1: u8, param2: u8) {}
9771 "});
9772
9773 cx.update_editor(|editor, window, cx| {
9774 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
9775 });
9776
9777 let mocked_response = lsp::SignatureHelp {
9778 signatures: Vec::new(),
9779 active_signature: None,
9780 active_parameter: None,
9781 };
9782 handle_signature_help_request(&mut cx, mocked_response).await;
9783
9784 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
9785 .await;
9786
9787 cx.editor(|editor, _, _| {
9788 assert!(!editor.signature_help_state.is_shown());
9789 });
9790
9791 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
9792 cx.set_state(indoc! {"
9793 fn main() {
9794 sample(ˇ);
9795 }
9796
9797 fn sample(param1: u8, param2: u8) {}
9798 "});
9799
9800 let mocked_response = lsp::SignatureHelp {
9801 signatures: vec![lsp::SignatureInformation {
9802 label: "fn sample(param1: u8, param2: u8)".to_string(),
9803 documentation: None,
9804 parameters: Some(vec![
9805 lsp::ParameterInformation {
9806 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9807 documentation: None,
9808 },
9809 lsp::ParameterInformation {
9810 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9811 documentation: None,
9812 },
9813 ]),
9814 active_parameter: None,
9815 }],
9816 active_signature: Some(0),
9817 active_parameter: Some(0),
9818 };
9819 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9820 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9821 .await;
9822 cx.editor(|editor, _, _| {
9823 assert!(editor.signature_help_state.is_shown());
9824 });
9825
9826 // Restore the popover with more parameter input
9827 cx.set_state(indoc! {"
9828 fn main() {
9829 sample(param1, param2ˇ);
9830 }
9831
9832 fn sample(param1: u8, param2: u8) {}
9833 "});
9834
9835 let mocked_response = lsp::SignatureHelp {
9836 signatures: vec![lsp::SignatureInformation {
9837 label: "fn sample(param1: u8, param2: u8)".to_string(),
9838 documentation: None,
9839 parameters: Some(vec![
9840 lsp::ParameterInformation {
9841 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9842 documentation: None,
9843 },
9844 lsp::ParameterInformation {
9845 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9846 documentation: None,
9847 },
9848 ]),
9849 active_parameter: None,
9850 }],
9851 active_signature: Some(0),
9852 active_parameter: Some(1),
9853 };
9854 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9855 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9856 .await;
9857
9858 // When selecting a range, the popover is gone.
9859 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
9860 cx.update_editor(|editor, window, cx| {
9861 editor.change_selections(None, window, cx, |s| {
9862 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
9863 })
9864 });
9865 cx.assert_editor_state(indoc! {"
9866 fn main() {
9867 sample(param1, «ˇparam2»);
9868 }
9869
9870 fn sample(param1: u8, param2: u8) {}
9871 "});
9872 cx.editor(|editor, _, _| {
9873 assert!(!editor.signature_help_state.is_shown());
9874 });
9875
9876 // When unselecting again, the popover is back if within the brackets.
9877 cx.update_editor(|editor, window, cx| {
9878 editor.change_selections(None, window, cx, |s| {
9879 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9880 })
9881 });
9882 cx.assert_editor_state(indoc! {"
9883 fn main() {
9884 sample(param1, ˇparam2);
9885 }
9886
9887 fn sample(param1: u8, param2: u8) {}
9888 "});
9889 handle_signature_help_request(&mut cx, mocked_response).await;
9890 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9891 .await;
9892 cx.editor(|editor, _, _| {
9893 assert!(editor.signature_help_state.is_shown());
9894 });
9895
9896 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
9897 cx.update_editor(|editor, window, cx| {
9898 editor.change_selections(None, window, cx, |s| {
9899 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
9900 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9901 })
9902 });
9903 cx.assert_editor_state(indoc! {"
9904 fn main() {
9905 sample(param1, ˇparam2);
9906 }
9907
9908 fn sample(param1: u8, param2: u8) {}
9909 "});
9910
9911 let mocked_response = lsp::SignatureHelp {
9912 signatures: vec![lsp::SignatureInformation {
9913 label: "fn sample(param1: u8, param2: u8)".to_string(),
9914 documentation: None,
9915 parameters: Some(vec![
9916 lsp::ParameterInformation {
9917 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9918 documentation: None,
9919 },
9920 lsp::ParameterInformation {
9921 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9922 documentation: None,
9923 },
9924 ]),
9925 active_parameter: None,
9926 }],
9927 active_signature: Some(0),
9928 active_parameter: Some(1),
9929 };
9930 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9931 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9932 .await;
9933 cx.update_editor(|editor, _, cx| {
9934 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
9935 });
9936 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
9937 .await;
9938 cx.update_editor(|editor, window, cx| {
9939 editor.change_selections(None, window, cx, |s| {
9940 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
9941 })
9942 });
9943 cx.assert_editor_state(indoc! {"
9944 fn main() {
9945 sample(param1, «ˇparam2»);
9946 }
9947
9948 fn sample(param1: u8, param2: u8) {}
9949 "});
9950 cx.update_editor(|editor, window, cx| {
9951 editor.change_selections(None, window, cx, |s| {
9952 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9953 })
9954 });
9955 cx.assert_editor_state(indoc! {"
9956 fn main() {
9957 sample(param1, ˇparam2);
9958 }
9959
9960 fn sample(param1: u8, param2: u8) {}
9961 "});
9962 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
9963 .await;
9964}
9965
9966#[gpui::test]
9967async fn test_completion_mode(cx: &mut TestAppContext) {
9968 init_test(cx, |_| {});
9969 let mut cx = EditorLspTestContext::new_rust(
9970 lsp::ServerCapabilities {
9971 completion_provider: Some(lsp::CompletionOptions {
9972 resolve_provider: Some(true),
9973 ..Default::default()
9974 }),
9975 ..Default::default()
9976 },
9977 cx,
9978 )
9979 .await;
9980
9981 struct Run {
9982 run_description: &'static str,
9983 initial_state: String,
9984 buffer_marked_text: String,
9985 completion_text: &'static str,
9986 expected_with_insert_mode: String,
9987 expected_with_replace_mode: String,
9988 expected_with_replace_subsequence_mode: String,
9989 expected_with_replace_suffix_mode: String,
9990 }
9991
9992 let runs = [
9993 Run {
9994 run_description: "Start of word matches completion text",
9995 initial_state: "before ediˇ after".into(),
9996 buffer_marked_text: "before <edi|> after".into(),
9997 completion_text: "editor",
9998 expected_with_insert_mode: "before editorˇ after".into(),
9999 expected_with_replace_mode: "before editorˇ after".into(),
10000 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10001 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10002 },
10003 Run {
10004 run_description: "Accept same text at the middle of the word",
10005 initial_state: "before ediˇtor after".into(),
10006 buffer_marked_text: "before <edi|tor> after".into(),
10007 completion_text: "editor",
10008 expected_with_insert_mode: "before editorˇtor after".into(),
10009 expected_with_replace_mode: "before editorˇ after".into(),
10010 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10011 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10012 },
10013 Run {
10014 run_description: "End of word matches completion text -- cursor at end",
10015 initial_state: "before torˇ after".into(),
10016 buffer_marked_text: "before <tor|> after".into(),
10017 completion_text: "editor",
10018 expected_with_insert_mode: "before editorˇ after".into(),
10019 expected_with_replace_mode: "before editorˇ after".into(),
10020 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10021 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10022 },
10023 Run {
10024 run_description: "End of word matches completion text -- cursor at start",
10025 initial_state: "before ˇtor after".into(),
10026 buffer_marked_text: "before <|tor> after".into(),
10027 completion_text: "editor",
10028 expected_with_insert_mode: "before editorˇtor after".into(),
10029 expected_with_replace_mode: "before editorˇ after".into(),
10030 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10031 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10032 },
10033 Run {
10034 run_description: "Prepend text containing whitespace",
10035 initial_state: "pˇfield: bool".into(),
10036 buffer_marked_text: "<p|field>: bool".into(),
10037 completion_text: "pub ",
10038 expected_with_insert_mode: "pub ˇfield: bool".into(),
10039 expected_with_replace_mode: "pub ˇ: bool".into(),
10040 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
10041 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
10042 },
10043 Run {
10044 run_description: "Add element to start of list",
10045 initial_state: "[element_ˇelement_2]".into(),
10046 buffer_marked_text: "[<element_|element_2>]".into(),
10047 completion_text: "element_1",
10048 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
10049 expected_with_replace_mode: "[element_1ˇ]".into(),
10050 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
10051 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
10052 },
10053 Run {
10054 run_description: "Add element to start of list -- first and second elements are equal",
10055 initial_state: "[elˇelement]".into(),
10056 buffer_marked_text: "[<el|element>]".into(),
10057 completion_text: "element",
10058 expected_with_insert_mode: "[elementˇelement]".into(),
10059 expected_with_replace_mode: "[elementˇ]".into(),
10060 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
10061 expected_with_replace_suffix_mode: "[elementˇ]".into(),
10062 },
10063 Run {
10064 run_description: "Ends with matching suffix",
10065 initial_state: "SubˇError".into(),
10066 buffer_marked_text: "<Sub|Error>".into(),
10067 completion_text: "SubscriptionError",
10068 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
10069 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10070 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10071 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
10072 },
10073 Run {
10074 run_description: "Suffix is a subsequence -- contiguous",
10075 initial_state: "SubˇErr".into(),
10076 buffer_marked_text: "<Sub|Err>".into(),
10077 completion_text: "SubscriptionError",
10078 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
10079 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10080 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10081 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
10082 },
10083 Run {
10084 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
10085 initial_state: "Suˇscrirr".into(),
10086 buffer_marked_text: "<Su|scrirr>".into(),
10087 completion_text: "SubscriptionError",
10088 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
10089 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10090 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10091 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
10092 },
10093 Run {
10094 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
10095 initial_state: "foo(indˇix)".into(),
10096 buffer_marked_text: "foo(<ind|ix>)".into(),
10097 completion_text: "node_index",
10098 expected_with_insert_mode: "foo(node_indexˇix)".into(),
10099 expected_with_replace_mode: "foo(node_indexˇ)".into(),
10100 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
10101 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
10102 },
10103 ];
10104
10105 for run in runs {
10106 let run_variations = [
10107 (LspInsertMode::Insert, run.expected_with_insert_mode),
10108 (LspInsertMode::Replace, run.expected_with_replace_mode),
10109 (
10110 LspInsertMode::ReplaceSubsequence,
10111 run.expected_with_replace_subsequence_mode,
10112 ),
10113 (
10114 LspInsertMode::ReplaceSuffix,
10115 run.expected_with_replace_suffix_mode,
10116 ),
10117 ];
10118
10119 for (lsp_insert_mode, expected_text) in run_variations {
10120 eprintln!(
10121 "run = {:?}, mode = {lsp_insert_mode:.?}",
10122 run.run_description,
10123 );
10124
10125 update_test_language_settings(&mut cx, |settings| {
10126 settings.defaults.completions = Some(CompletionSettings {
10127 lsp_insert_mode,
10128 words: WordsCompletionMode::Disabled,
10129 lsp: true,
10130 lsp_fetch_timeout_ms: 0,
10131 });
10132 });
10133
10134 cx.set_state(&run.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 &run.buffer_marked_text,
10143 vec![run.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(&ConfirmCompletion::default(), window, cx)
10154 .unwrap()
10155 });
10156 cx.assert_editor_state(&expected_text);
10157 handle_resolve_completion_request(&mut cx, None).await;
10158 apply_additional_edits.await.unwrap();
10159 }
10160 }
10161}
10162
10163#[gpui::test]
10164async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
10165 init_test(cx, |_| {});
10166 let mut cx = EditorLspTestContext::new_rust(
10167 lsp::ServerCapabilities {
10168 completion_provider: Some(lsp::CompletionOptions {
10169 resolve_provider: Some(true),
10170 ..Default::default()
10171 }),
10172 ..Default::default()
10173 },
10174 cx,
10175 )
10176 .await;
10177
10178 let initial_state = "SubˇError";
10179 let buffer_marked_text = "<Sub|Error>";
10180 let completion_text = "SubscriptionError";
10181 let expected_with_insert_mode = "SubscriptionErrorˇError";
10182 let expected_with_replace_mode = "SubscriptionErrorˇ";
10183
10184 update_test_language_settings(&mut cx, |settings| {
10185 settings.defaults.completions = Some(CompletionSettings {
10186 words: WordsCompletionMode::Disabled,
10187 // set the opposite here to ensure that the action is overriding the default behavior
10188 lsp_insert_mode: LspInsertMode::Insert,
10189 lsp: true,
10190 lsp_fetch_timeout_ms: 0,
10191 });
10192 });
10193
10194 cx.set_state(initial_state);
10195 cx.update_editor(|editor, window, cx| {
10196 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10197 });
10198
10199 let counter = Arc::new(AtomicUsize::new(0));
10200 handle_completion_request_with_insert_and_replace(
10201 &mut cx,
10202 &buffer_marked_text,
10203 vec![completion_text],
10204 counter.clone(),
10205 )
10206 .await;
10207 cx.condition(|editor, _| editor.context_menu_visible())
10208 .await;
10209 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10210
10211 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10212 editor
10213 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10214 .unwrap()
10215 });
10216 cx.assert_editor_state(&expected_with_replace_mode);
10217 handle_resolve_completion_request(&mut cx, None).await;
10218 apply_additional_edits.await.unwrap();
10219
10220 update_test_language_settings(&mut cx, |settings| {
10221 settings.defaults.completions = Some(CompletionSettings {
10222 words: WordsCompletionMode::Disabled,
10223 // set the opposite here to ensure that the action is overriding the default behavior
10224 lsp_insert_mode: LspInsertMode::Replace,
10225 lsp: true,
10226 lsp_fetch_timeout_ms: 0,
10227 });
10228 });
10229
10230 cx.set_state(initial_state);
10231 cx.update_editor(|editor, window, cx| {
10232 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10233 });
10234 handle_completion_request_with_insert_and_replace(
10235 &mut cx,
10236 &buffer_marked_text,
10237 vec![completion_text],
10238 counter.clone(),
10239 )
10240 .await;
10241 cx.condition(|editor, _| editor.context_menu_visible())
10242 .await;
10243 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
10244
10245 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10246 editor
10247 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
10248 .unwrap()
10249 });
10250 cx.assert_editor_state(&expected_with_insert_mode);
10251 handle_resolve_completion_request(&mut cx, None).await;
10252 apply_additional_edits.await.unwrap();
10253}
10254
10255#[gpui::test]
10256async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
10257 init_test(cx, |_| {});
10258 let mut cx = EditorLspTestContext::new_rust(
10259 lsp::ServerCapabilities {
10260 completion_provider: Some(lsp::CompletionOptions {
10261 resolve_provider: Some(true),
10262 ..Default::default()
10263 }),
10264 ..Default::default()
10265 },
10266 cx,
10267 )
10268 .await;
10269
10270 // scenario: surrounding text matches completion text
10271 let completion_text = "to_offset";
10272 let initial_state = indoc! {"
10273 1. buf.to_offˇsuffix
10274 2. buf.to_offˇsuf
10275 3. buf.to_offˇfix
10276 4. buf.to_offˇ
10277 5. into_offˇensive
10278 6. ˇsuffix
10279 7. let ˇ //
10280 8. aaˇzz
10281 9. buf.to_off«zzzzzˇ»suffix
10282 10. buf.«ˇzzzzz»suffix
10283 11. to_off«ˇzzzzz»
10284
10285 buf.to_offˇsuffix // newest cursor
10286 "};
10287 let completion_marked_buffer = indoc! {"
10288 1. buf.to_offsuffix
10289 2. buf.to_offsuf
10290 3. buf.to_offfix
10291 4. buf.to_off
10292 5. into_offensive
10293 6. suffix
10294 7. let //
10295 8. aazz
10296 9. buf.to_offzzzzzsuffix
10297 10. buf.zzzzzsuffix
10298 11. to_offzzzzz
10299
10300 buf.<to_off|suffix> // newest cursor
10301 "};
10302 let expected = indoc! {"
10303 1. buf.to_offsetˇ
10304 2. buf.to_offsetˇsuf
10305 3. buf.to_offsetˇfix
10306 4. buf.to_offsetˇ
10307 5. into_offsetˇensive
10308 6. to_offsetˇsuffix
10309 7. let to_offsetˇ //
10310 8. aato_offsetˇzz
10311 9. buf.to_offsetˇ
10312 10. buf.to_offsetˇsuffix
10313 11. to_offsetˇ
10314
10315 buf.to_offsetˇ // newest cursor
10316 "};
10317 cx.set_state(initial_state);
10318 cx.update_editor(|editor, window, cx| {
10319 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10320 });
10321 handle_completion_request_with_insert_and_replace(
10322 &mut cx,
10323 completion_marked_buffer,
10324 vec![completion_text],
10325 Arc::new(AtomicUsize::new(0)),
10326 )
10327 .await;
10328 cx.condition(|editor, _| editor.context_menu_visible())
10329 .await;
10330 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10331 editor
10332 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10333 .unwrap()
10334 });
10335 cx.assert_editor_state(expected);
10336 handle_resolve_completion_request(&mut cx, None).await;
10337 apply_additional_edits.await.unwrap();
10338
10339 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
10340 let completion_text = "foo_and_bar";
10341 let initial_state = indoc! {"
10342 1. ooanbˇ
10343 2. zooanbˇ
10344 3. ooanbˇz
10345 4. zooanbˇz
10346 5. ooanˇ
10347 6. oanbˇ
10348
10349 ooanbˇ
10350 "};
10351 let completion_marked_buffer = indoc! {"
10352 1. ooanb
10353 2. zooanb
10354 3. ooanbz
10355 4. zooanbz
10356 5. ooan
10357 6. oanb
10358
10359 <ooanb|>
10360 "};
10361 let expected = indoc! {"
10362 1. foo_and_barˇ
10363 2. zfoo_and_barˇ
10364 3. foo_and_barˇz
10365 4. zfoo_and_barˇz
10366 5. ooanfoo_and_barˇ
10367 6. oanbfoo_and_barˇ
10368
10369 foo_and_barˇ
10370 "};
10371 cx.set_state(initial_state);
10372 cx.update_editor(|editor, window, cx| {
10373 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10374 });
10375 handle_completion_request_with_insert_and_replace(
10376 &mut cx,
10377 completion_marked_buffer,
10378 vec![completion_text],
10379 Arc::new(AtomicUsize::new(0)),
10380 )
10381 .await;
10382 cx.condition(|editor, _| editor.context_menu_visible())
10383 .await;
10384 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10385 editor
10386 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10387 .unwrap()
10388 });
10389 cx.assert_editor_state(expected);
10390 handle_resolve_completion_request(&mut cx, None).await;
10391 apply_additional_edits.await.unwrap();
10392
10393 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
10394 // (expects the same as if it was inserted at the end)
10395 let completion_text = "foo_and_bar";
10396 let initial_state = indoc! {"
10397 1. ooˇanb
10398 2. zooˇanb
10399 3. ooˇanbz
10400 4. zooˇanbz
10401
10402 ooˇanb
10403 "};
10404 let completion_marked_buffer = indoc! {"
10405 1. ooanb
10406 2. zooanb
10407 3. ooanbz
10408 4. zooanbz
10409
10410 <oo|anb>
10411 "};
10412 let expected = indoc! {"
10413 1. foo_and_barˇ
10414 2. zfoo_and_barˇ
10415 3. foo_and_barˇz
10416 4. zfoo_and_barˇz
10417
10418 foo_and_barˇ
10419 "};
10420 cx.set_state(initial_state);
10421 cx.update_editor(|editor, window, cx| {
10422 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10423 });
10424 handle_completion_request_with_insert_and_replace(
10425 &mut cx,
10426 completion_marked_buffer,
10427 vec![completion_text],
10428 Arc::new(AtomicUsize::new(0)),
10429 )
10430 .await;
10431 cx.condition(|editor, _| editor.context_menu_visible())
10432 .await;
10433 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10434 editor
10435 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10436 .unwrap()
10437 });
10438 cx.assert_editor_state(expected);
10439 handle_resolve_completion_request(&mut cx, None).await;
10440 apply_additional_edits.await.unwrap();
10441}
10442
10443// This used to crash
10444#[gpui::test]
10445async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
10446 init_test(cx, |_| {});
10447
10448 let buffer_text = indoc! {"
10449 fn main() {
10450 10.satu;
10451
10452 //
10453 // separate cursors so they open in different excerpts (manually reproducible)
10454 //
10455
10456 10.satu20;
10457 }
10458 "};
10459 let multibuffer_text_with_selections = indoc! {"
10460 fn main() {
10461 10.satuˇ;
10462
10463 //
10464
10465 //
10466
10467 10.satuˇ20;
10468 }
10469 "};
10470 let expected_multibuffer = indoc! {"
10471 fn main() {
10472 10.saturating_sub()ˇ;
10473
10474 //
10475
10476 //
10477
10478 10.saturating_sub()ˇ;
10479 }
10480 "};
10481
10482 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
10483 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
10484
10485 let fs = FakeFs::new(cx.executor());
10486 fs.insert_tree(
10487 path!("/a"),
10488 json!({
10489 "main.rs": buffer_text,
10490 }),
10491 )
10492 .await;
10493
10494 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
10495 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10496 language_registry.add(rust_lang());
10497 let mut fake_servers = language_registry.register_fake_lsp(
10498 "Rust",
10499 FakeLspAdapter {
10500 capabilities: lsp::ServerCapabilities {
10501 completion_provider: Some(lsp::CompletionOptions {
10502 resolve_provider: None,
10503 ..lsp::CompletionOptions::default()
10504 }),
10505 ..lsp::ServerCapabilities::default()
10506 },
10507 ..FakeLspAdapter::default()
10508 },
10509 );
10510 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10511 let cx = &mut VisualTestContext::from_window(*workspace, cx);
10512 let buffer = project
10513 .update(cx, |project, cx| {
10514 project.open_local_buffer(path!("/a/main.rs"), cx)
10515 })
10516 .await
10517 .unwrap();
10518
10519 let multi_buffer = cx.new(|cx| {
10520 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
10521 multi_buffer.push_excerpts(
10522 buffer.clone(),
10523 [ExcerptRange::new(0..first_excerpt_end)],
10524 cx,
10525 );
10526 multi_buffer.push_excerpts(
10527 buffer.clone(),
10528 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
10529 cx,
10530 );
10531 multi_buffer
10532 });
10533
10534 let editor = workspace
10535 .update(cx, |_, window, cx| {
10536 cx.new(|cx| {
10537 Editor::new(
10538 EditorMode::Full {
10539 scale_ui_elements_with_buffer_font_size: false,
10540 show_active_line_background: false,
10541 sized_by_content: false,
10542 },
10543 multi_buffer.clone(),
10544 Some(project.clone()),
10545 window,
10546 cx,
10547 )
10548 })
10549 })
10550 .unwrap();
10551
10552 let pane = workspace
10553 .update(cx, |workspace, _, _| workspace.active_pane().clone())
10554 .unwrap();
10555 pane.update_in(cx, |pane, window, cx| {
10556 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
10557 });
10558
10559 let fake_server = fake_servers.next().await.unwrap();
10560
10561 editor.update_in(cx, |editor, window, cx| {
10562 editor.change_selections(None, window, cx, |s| {
10563 s.select_ranges([
10564 Point::new(1, 11)..Point::new(1, 11),
10565 Point::new(7, 11)..Point::new(7, 11),
10566 ])
10567 });
10568
10569 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
10570 });
10571
10572 editor.update_in(cx, |editor, window, cx| {
10573 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10574 });
10575
10576 fake_server
10577 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
10578 let completion_item = lsp::CompletionItem {
10579 label: "saturating_sub()".into(),
10580 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
10581 lsp::InsertReplaceEdit {
10582 new_text: "saturating_sub()".to_owned(),
10583 insert: lsp::Range::new(
10584 lsp::Position::new(7, 7),
10585 lsp::Position::new(7, 11),
10586 ),
10587 replace: lsp::Range::new(
10588 lsp::Position::new(7, 7),
10589 lsp::Position::new(7, 13),
10590 ),
10591 },
10592 )),
10593 ..lsp::CompletionItem::default()
10594 };
10595
10596 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
10597 })
10598 .next()
10599 .await
10600 .unwrap();
10601
10602 cx.condition(&editor, |editor, _| editor.context_menu_visible())
10603 .await;
10604
10605 editor
10606 .update_in(cx, |editor, window, cx| {
10607 editor
10608 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10609 .unwrap()
10610 })
10611 .await
10612 .unwrap();
10613
10614 editor.update(cx, |editor, cx| {
10615 assert_text_with_selections(editor, expected_multibuffer, cx);
10616 })
10617}
10618
10619#[gpui::test]
10620async fn test_completion(cx: &mut TestAppContext) {
10621 init_test(cx, |_| {});
10622
10623 let mut cx = EditorLspTestContext::new_rust(
10624 lsp::ServerCapabilities {
10625 completion_provider: Some(lsp::CompletionOptions {
10626 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10627 resolve_provider: Some(true),
10628 ..Default::default()
10629 }),
10630 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10631 ..Default::default()
10632 },
10633 cx,
10634 )
10635 .await;
10636 let counter = Arc::new(AtomicUsize::new(0));
10637
10638 cx.set_state(indoc! {"
10639 oneˇ
10640 two
10641 three
10642 "});
10643 cx.simulate_keystroke(".");
10644 handle_completion_request(
10645 &mut cx,
10646 indoc! {"
10647 one.|<>
10648 two
10649 three
10650 "},
10651 vec!["first_completion", "second_completion"],
10652 counter.clone(),
10653 )
10654 .await;
10655 cx.condition(|editor, _| editor.context_menu_visible())
10656 .await;
10657 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10658
10659 let _handler = handle_signature_help_request(
10660 &mut cx,
10661 lsp::SignatureHelp {
10662 signatures: vec![lsp::SignatureInformation {
10663 label: "test signature".to_string(),
10664 documentation: None,
10665 parameters: Some(vec![lsp::ParameterInformation {
10666 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
10667 documentation: None,
10668 }]),
10669 active_parameter: None,
10670 }],
10671 active_signature: None,
10672 active_parameter: None,
10673 },
10674 );
10675 cx.update_editor(|editor, window, cx| {
10676 assert!(
10677 !editor.signature_help_state.is_shown(),
10678 "No signature help was called for"
10679 );
10680 editor.show_signature_help(&ShowSignatureHelp, window, cx);
10681 });
10682 cx.run_until_parked();
10683 cx.update_editor(|editor, _, _| {
10684 assert!(
10685 !editor.signature_help_state.is_shown(),
10686 "No signature help should be shown when completions menu is open"
10687 );
10688 });
10689
10690 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10691 editor.context_menu_next(&Default::default(), window, cx);
10692 editor
10693 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10694 .unwrap()
10695 });
10696 cx.assert_editor_state(indoc! {"
10697 one.second_completionˇ
10698 two
10699 three
10700 "});
10701
10702 handle_resolve_completion_request(
10703 &mut cx,
10704 Some(vec![
10705 (
10706 //This overlaps with the primary completion edit which is
10707 //misbehavior from the LSP spec, test that we filter it out
10708 indoc! {"
10709 one.second_ˇcompletion
10710 two
10711 threeˇ
10712 "},
10713 "overlapping additional edit",
10714 ),
10715 (
10716 indoc! {"
10717 one.second_completion
10718 two
10719 threeˇ
10720 "},
10721 "\nadditional edit",
10722 ),
10723 ]),
10724 )
10725 .await;
10726 apply_additional_edits.await.unwrap();
10727 cx.assert_editor_state(indoc! {"
10728 one.second_completionˇ
10729 two
10730 three
10731 additional edit
10732 "});
10733
10734 cx.set_state(indoc! {"
10735 one.second_completion
10736 twoˇ
10737 threeˇ
10738 additional edit
10739 "});
10740 cx.simulate_keystroke(" ");
10741 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
10742 cx.simulate_keystroke("s");
10743 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
10744
10745 cx.assert_editor_state(indoc! {"
10746 one.second_completion
10747 two sˇ
10748 three sˇ
10749 additional edit
10750 "});
10751 handle_completion_request(
10752 &mut cx,
10753 indoc! {"
10754 one.second_completion
10755 two s
10756 three <s|>
10757 additional edit
10758 "},
10759 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
10760 counter.clone(),
10761 )
10762 .await;
10763 cx.condition(|editor, _| editor.context_menu_visible())
10764 .await;
10765 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
10766
10767 cx.simulate_keystroke("i");
10768
10769 handle_completion_request(
10770 &mut cx,
10771 indoc! {"
10772 one.second_completion
10773 two si
10774 three <si|>
10775 additional edit
10776 "},
10777 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
10778 counter.clone(),
10779 )
10780 .await;
10781 cx.condition(|editor, _| editor.context_menu_visible())
10782 .await;
10783 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
10784
10785 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10786 editor
10787 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10788 .unwrap()
10789 });
10790 cx.assert_editor_state(indoc! {"
10791 one.second_completion
10792 two sixth_completionˇ
10793 three sixth_completionˇ
10794 additional edit
10795 "});
10796
10797 apply_additional_edits.await.unwrap();
10798
10799 update_test_language_settings(&mut cx, |settings| {
10800 settings.defaults.show_completions_on_input = Some(false);
10801 });
10802 cx.set_state("editorˇ");
10803 cx.simulate_keystroke(".");
10804 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
10805 cx.simulate_keystrokes("c l o");
10806 cx.assert_editor_state("editor.cloˇ");
10807 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
10808 cx.update_editor(|editor, window, cx| {
10809 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10810 });
10811 handle_completion_request(
10812 &mut cx,
10813 "editor.<clo|>",
10814 vec!["close", "clobber"],
10815 counter.clone(),
10816 )
10817 .await;
10818 cx.condition(|editor, _| editor.context_menu_visible())
10819 .await;
10820 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
10821
10822 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10823 editor
10824 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10825 .unwrap()
10826 });
10827 cx.assert_editor_state("editor.closeˇ");
10828 handle_resolve_completion_request(&mut cx, None).await;
10829 apply_additional_edits.await.unwrap();
10830}
10831
10832#[gpui::test]
10833async fn test_word_completion(cx: &mut TestAppContext) {
10834 let lsp_fetch_timeout_ms = 10;
10835 init_test(cx, |language_settings| {
10836 language_settings.defaults.completions = Some(CompletionSettings {
10837 words: WordsCompletionMode::Fallback,
10838 lsp: true,
10839 lsp_fetch_timeout_ms: 10,
10840 lsp_insert_mode: LspInsertMode::Insert,
10841 });
10842 });
10843
10844 let mut cx = EditorLspTestContext::new_rust(
10845 lsp::ServerCapabilities {
10846 completion_provider: Some(lsp::CompletionOptions {
10847 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10848 ..lsp::CompletionOptions::default()
10849 }),
10850 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10851 ..lsp::ServerCapabilities::default()
10852 },
10853 cx,
10854 )
10855 .await;
10856
10857 let throttle_completions = Arc::new(AtomicBool::new(false));
10858
10859 let lsp_throttle_completions = throttle_completions.clone();
10860 let _completion_requests_handler =
10861 cx.lsp
10862 .server
10863 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
10864 let lsp_throttle_completions = lsp_throttle_completions.clone();
10865 let cx = cx.clone();
10866 async move {
10867 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
10868 cx.background_executor()
10869 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
10870 .await;
10871 }
10872 Ok(Some(lsp::CompletionResponse::Array(vec![
10873 lsp::CompletionItem {
10874 label: "first".into(),
10875 ..lsp::CompletionItem::default()
10876 },
10877 lsp::CompletionItem {
10878 label: "last".into(),
10879 ..lsp::CompletionItem::default()
10880 },
10881 ])))
10882 }
10883 });
10884
10885 cx.set_state(indoc! {"
10886 oneˇ
10887 two
10888 three
10889 "});
10890 cx.simulate_keystroke(".");
10891 cx.executor().run_until_parked();
10892 cx.condition(|editor, _| editor.context_menu_visible())
10893 .await;
10894 cx.update_editor(|editor, window, cx| {
10895 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10896 {
10897 assert_eq!(
10898 completion_menu_entries(&menu),
10899 &["first", "last"],
10900 "When LSP server is fast to reply, no fallback word completions are used"
10901 );
10902 } else {
10903 panic!("expected completion menu to be open");
10904 }
10905 editor.cancel(&Cancel, window, cx);
10906 });
10907 cx.executor().run_until_parked();
10908 cx.condition(|editor, _| !editor.context_menu_visible())
10909 .await;
10910
10911 throttle_completions.store(true, atomic::Ordering::Release);
10912 cx.simulate_keystroke(".");
10913 cx.executor()
10914 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
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!(completion_menu_entries(&menu), &["one", "three", "two"],
10922 "When LSP server is slow, document words can be shown instead, if configured accordingly");
10923 } else {
10924 panic!("expected completion menu to be open");
10925 }
10926 });
10927}
10928
10929#[gpui::test]
10930async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
10931 init_test(cx, |language_settings| {
10932 language_settings.defaults.completions = Some(CompletionSettings {
10933 words: WordsCompletionMode::Enabled,
10934 lsp: true,
10935 lsp_fetch_timeout_ms: 0,
10936 lsp_insert_mode: LspInsertMode::Insert,
10937 });
10938 });
10939
10940 let mut cx = EditorLspTestContext::new_rust(
10941 lsp::ServerCapabilities {
10942 completion_provider: Some(lsp::CompletionOptions {
10943 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10944 ..lsp::CompletionOptions::default()
10945 }),
10946 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10947 ..lsp::ServerCapabilities::default()
10948 },
10949 cx,
10950 )
10951 .await;
10952
10953 let _completion_requests_handler =
10954 cx.lsp
10955 .server
10956 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
10957 Ok(Some(lsp::CompletionResponse::Array(vec![
10958 lsp::CompletionItem {
10959 label: "first".into(),
10960 ..lsp::CompletionItem::default()
10961 },
10962 lsp::CompletionItem {
10963 label: "last".into(),
10964 ..lsp::CompletionItem::default()
10965 },
10966 ])))
10967 });
10968
10969 cx.set_state(indoc! {"ˇ
10970 first
10971 last
10972 second
10973 "});
10974 cx.simulate_keystroke(".");
10975 cx.executor().run_until_parked();
10976 cx.condition(|editor, _| editor.context_menu_visible())
10977 .await;
10978 cx.update_editor(|editor, _, _| {
10979 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10980 {
10981 assert_eq!(
10982 completion_menu_entries(&menu),
10983 &["first", "last", "second"],
10984 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
10985 );
10986 } else {
10987 panic!("expected completion menu to be open");
10988 }
10989 });
10990}
10991
10992#[gpui::test]
10993async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
10994 init_test(cx, |language_settings| {
10995 language_settings.defaults.completions = Some(CompletionSettings {
10996 words: WordsCompletionMode::Disabled,
10997 lsp: true,
10998 lsp_fetch_timeout_ms: 0,
10999 lsp_insert_mode: LspInsertMode::Insert,
11000 });
11001 });
11002
11003 let mut cx = EditorLspTestContext::new_rust(
11004 lsp::ServerCapabilities {
11005 completion_provider: Some(lsp::CompletionOptions {
11006 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11007 ..lsp::CompletionOptions::default()
11008 }),
11009 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11010 ..lsp::ServerCapabilities::default()
11011 },
11012 cx,
11013 )
11014 .await;
11015
11016 let _completion_requests_handler =
11017 cx.lsp
11018 .server
11019 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11020 panic!("LSP completions should not be queried when dealing with word completions")
11021 });
11022
11023 cx.set_state(indoc! {"ˇ
11024 first
11025 last
11026 second
11027 "});
11028 cx.update_editor(|editor, window, cx| {
11029 editor.show_word_completions(&ShowWordCompletions, window, cx);
11030 });
11031 cx.executor().run_until_parked();
11032 cx.condition(|editor, _| editor.context_menu_visible())
11033 .await;
11034 cx.update_editor(|editor, _, _| {
11035 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11036 {
11037 assert_eq!(
11038 completion_menu_entries(&menu),
11039 &["first", "last", "second"],
11040 "`ShowWordCompletions` action should show word completions"
11041 );
11042 } else {
11043 panic!("expected completion menu to be open");
11044 }
11045 });
11046
11047 cx.simulate_keystroke("l");
11048 cx.executor().run_until_parked();
11049 cx.condition(|editor, _| editor.context_menu_visible())
11050 .await;
11051 cx.update_editor(|editor, _, _| {
11052 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11053 {
11054 assert_eq!(
11055 completion_menu_entries(&menu),
11056 &["last"],
11057 "After showing word completions, further editing should filter them and not query the LSP"
11058 );
11059 } else {
11060 panic!("expected completion menu to be open");
11061 }
11062 });
11063}
11064
11065#[gpui::test]
11066async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
11067 init_test(cx, |language_settings| {
11068 language_settings.defaults.completions = Some(CompletionSettings {
11069 words: WordsCompletionMode::Fallback,
11070 lsp: false,
11071 lsp_fetch_timeout_ms: 0,
11072 lsp_insert_mode: LspInsertMode::Insert,
11073 });
11074 });
11075
11076 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
11077
11078 cx.set_state(indoc! {"ˇ
11079 0_usize
11080 let
11081 33
11082 4.5f32
11083 "});
11084 cx.update_editor(|editor, window, cx| {
11085 editor.show_completions(&ShowCompletions::default(), window, cx);
11086 });
11087 cx.executor().run_until_parked();
11088 cx.condition(|editor, _| editor.context_menu_visible())
11089 .await;
11090 cx.update_editor(|editor, window, cx| {
11091 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11092 {
11093 assert_eq!(
11094 completion_menu_entries(&menu),
11095 &["let"],
11096 "With no digits in the completion query, no digits should be in the word completions"
11097 );
11098 } else {
11099 panic!("expected completion menu to be open");
11100 }
11101 editor.cancel(&Cancel, window, cx);
11102 });
11103
11104 cx.set_state(indoc! {"3ˇ
11105 0_usize
11106 let
11107 3
11108 33.35f32
11109 "});
11110 cx.update_editor(|editor, window, cx| {
11111 editor.show_completions(&ShowCompletions::default(), window, cx);
11112 });
11113 cx.executor().run_until_parked();
11114 cx.condition(|editor, _| editor.context_menu_visible())
11115 .await;
11116 cx.update_editor(|editor, _, _| {
11117 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11118 {
11119 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
11120 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
11121 } else {
11122 panic!("expected completion menu to be open");
11123 }
11124 });
11125}
11126
11127fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
11128 let position = || lsp::Position {
11129 line: params.text_document_position.position.line,
11130 character: params.text_document_position.position.character,
11131 };
11132 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11133 range: lsp::Range {
11134 start: position(),
11135 end: position(),
11136 },
11137 new_text: text.to_string(),
11138 }))
11139}
11140
11141#[gpui::test]
11142async fn test_multiline_completion(cx: &mut TestAppContext) {
11143 init_test(cx, |_| {});
11144
11145 let fs = FakeFs::new(cx.executor());
11146 fs.insert_tree(
11147 path!("/a"),
11148 json!({
11149 "main.ts": "a",
11150 }),
11151 )
11152 .await;
11153
11154 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11155 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11156 let typescript_language = Arc::new(Language::new(
11157 LanguageConfig {
11158 name: "TypeScript".into(),
11159 matcher: LanguageMatcher {
11160 path_suffixes: vec!["ts".to_string()],
11161 ..LanguageMatcher::default()
11162 },
11163 line_comments: vec!["// ".into()],
11164 ..LanguageConfig::default()
11165 },
11166 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
11167 ));
11168 language_registry.add(typescript_language.clone());
11169 let mut fake_servers = language_registry.register_fake_lsp(
11170 "TypeScript",
11171 FakeLspAdapter {
11172 capabilities: lsp::ServerCapabilities {
11173 completion_provider: Some(lsp::CompletionOptions {
11174 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11175 ..lsp::CompletionOptions::default()
11176 }),
11177 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11178 ..lsp::ServerCapabilities::default()
11179 },
11180 // Emulate vtsls label generation
11181 label_for_completion: Some(Box::new(|item, _| {
11182 let text = if let Some(description) = item
11183 .label_details
11184 .as_ref()
11185 .and_then(|label_details| label_details.description.as_ref())
11186 {
11187 format!("{} {}", item.label, description)
11188 } else if let Some(detail) = &item.detail {
11189 format!("{} {}", item.label, detail)
11190 } else {
11191 item.label.clone()
11192 };
11193 let len = text.len();
11194 Some(language::CodeLabel {
11195 text,
11196 runs: Vec::new(),
11197 filter_range: 0..len,
11198 })
11199 })),
11200 ..FakeLspAdapter::default()
11201 },
11202 );
11203 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11204 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11205 let worktree_id = workspace
11206 .update(cx, |workspace, _window, cx| {
11207 workspace.project().update(cx, |project, cx| {
11208 project.worktrees(cx).next().unwrap().read(cx).id()
11209 })
11210 })
11211 .unwrap();
11212 let _buffer = project
11213 .update(cx, |project, cx| {
11214 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
11215 })
11216 .await
11217 .unwrap();
11218 let editor = workspace
11219 .update(cx, |workspace, window, cx| {
11220 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
11221 })
11222 .unwrap()
11223 .await
11224 .unwrap()
11225 .downcast::<Editor>()
11226 .unwrap();
11227 let fake_server = fake_servers.next().await.unwrap();
11228
11229 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
11230 let multiline_label_2 = "a\nb\nc\n";
11231 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
11232 let multiline_description = "d\ne\nf\n";
11233 let multiline_detail_2 = "g\nh\ni\n";
11234
11235 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
11236 move |params, _| async move {
11237 Ok(Some(lsp::CompletionResponse::Array(vec![
11238 lsp::CompletionItem {
11239 label: multiline_label.to_string(),
11240 text_edit: gen_text_edit(¶ms, "new_text_1"),
11241 ..lsp::CompletionItem::default()
11242 },
11243 lsp::CompletionItem {
11244 label: "single line label 1".to_string(),
11245 detail: Some(multiline_detail.to_string()),
11246 text_edit: gen_text_edit(¶ms, "new_text_2"),
11247 ..lsp::CompletionItem::default()
11248 },
11249 lsp::CompletionItem {
11250 label: "single line label 2".to_string(),
11251 label_details: Some(lsp::CompletionItemLabelDetails {
11252 description: Some(multiline_description.to_string()),
11253 detail: None,
11254 }),
11255 text_edit: gen_text_edit(¶ms, "new_text_2"),
11256 ..lsp::CompletionItem::default()
11257 },
11258 lsp::CompletionItem {
11259 label: multiline_label_2.to_string(),
11260 detail: Some(multiline_detail_2.to_string()),
11261 text_edit: gen_text_edit(¶ms, "new_text_3"),
11262 ..lsp::CompletionItem::default()
11263 },
11264 lsp::CompletionItem {
11265 label: "Label with many spaces and \t but without newlines".to_string(),
11266 detail: Some(
11267 "Details with many spaces and \t but without newlines".to_string(),
11268 ),
11269 text_edit: gen_text_edit(¶ms, "new_text_4"),
11270 ..lsp::CompletionItem::default()
11271 },
11272 ])))
11273 },
11274 );
11275
11276 editor.update_in(cx, |editor, window, cx| {
11277 cx.focus_self(window);
11278 editor.move_to_end(&MoveToEnd, window, cx);
11279 editor.handle_input(".", window, cx);
11280 });
11281 cx.run_until_parked();
11282 completion_handle.next().await.unwrap();
11283
11284 editor.update(cx, |editor, _| {
11285 assert!(editor.context_menu_visible());
11286 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11287 {
11288 let completion_labels = menu
11289 .completions
11290 .borrow()
11291 .iter()
11292 .map(|c| c.label.text.clone())
11293 .collect::<Vec<_>>();
11294 assert_eq!(
11295 completion_labels,
11296 &[
11297 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
11298 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
11299 "single line label 2 d e f ",
11300 "a b c g h i ",
11301 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
11302 ],
11303 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
11304 );
11305
11306 for completion in menu
11307 .completions
11308 .borrow()
11309 .iter() {
11310 assert_eq!(
11311 completion.label.filter_range,
11312 0..completion.label.text.len(),
11313 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
11314 );
11315 }
11316 } else {
11317 panic!("expected completion menu to be open");
11318 }
11319 });
11320}
11321
11322#[gpui::test]
11323async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
11324 init_test(cx, |_| {});
11325 let mut cx = EditorLspTestContext::new_rust(
11326 lsp::ServerCapabilities {
11327 completion_provider: Some(lsp::CompletionOptions {
11328 trigger_characters: Some(vec![".".to_string()]),
11329 ..Default::default()
11330 }),
11331 ..Default::default()
11332 },
11333 cx,
11334 )
11335 .await;
11336 cx.lsp
11337 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11338 Ok(Some(lsp::CompletionResponse::Array(vec![
11339 lsp::CompletionItem {
11340 label: "first".into(),
11341 ..Default::default()
11342 },
11343 lsp::CompletionItem {
11344 label: "last".into(),
11345 ..Default::default()
11346 },
11347 ])))
11348 });
11349 cx.set_state("variableˇ");
11350 cx.simulate_keystroke(".");
11351 cx.executor().run_until_parked();
11352
11353 cx.update_editor(|editor, _, _| {
11354 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11355 {
11356 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
11357 } else {
11358 panic!("expected completion menu to be open");
11359 }
11360 });
11361
11362 cx.update_editor(|editor, window, cx| {
11363 editor.move_page_down(&MovePageDown::default(), window, cx);
11364 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11365 {
11366 assert!(
11367 menu.selected_item == 1,
11368 "expected PageDown to select the last item from the context menu"
11369 );
11370 } else {
11371 panic!("expected completion menu to stay open after PageDown");
11372 }
11373 });
11374
11375 cx.update_editor(|editor, window, cx| {
11376 editor.move_page_up(&MovePageUp::default(), window, cx);
11377 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11378 {
11379 assert!(
11380 menu.selected_item == 0,
11381 "expected PageUp to select the first item from the context menu"
11382 );
11383 } else {
11384 panic!("expected completion menu to stay open after PageUp");
11385 }
11386 });
11387}
11388
11389#[gpui::test]
11390async fn test_as_is_completions(cx: &mut TestAppContext) {
11391 init_test(cx, |_| {});
11392 let mut cx = EditorLspTestContext::new_rust(
11393 lsp::ServerCapabilities {
11394 completion_provider: Some(lsp::CompletionOptions {
11395 ..Default::default()
11396 }),
11397 ..Default::default()
11398 },
11399 cx,
11400 )
11401 .await;
11402 cx.lsp
11403 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11404 Ok(Some(lsp::CompletionResponse::Array(vec![
11405 lsp::CompletionItem {
11406 label: "unsafe".into(),
11407 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11408 range: lsp::Range {
11409 start: lsp::Position {
11410 line: 1,
11411 character: 2,
11412 },
11413 end: lsp::Position {
11414 line: 1,
11415 character: 3,
11416 },
11417 },
11418 new_text: "unsafe".to_string(),
11419 })),
11420 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
11421 ..Default::default()
11422 },
11423 ])))
11424 });
11425 cx.set_state("fn a() {}\n nˇ");
11426 cx.executor().run_until_parked();
11427 cx.update_editor(|editor, window, cx| {
11428 editor.show_completions(
11429 &ShowCompletions {
11430 trigger: Some("\n".into()),
11431 },
11432 window,
11433 cx,
11434 );
11435 });
11436 cx.executor().run_until_parked();
11437
11438 cx.update_editor(|editor, window, cx| {
11439 editor.confirm_completion(&Default::default(), window, cx)
11440 });
11441 cx.executor().run_until_parked();
11442 cx.assert_editor_state("fn a() {}\n unsafeˇ");
11443}
11444
11445#[gpui::test]
11446async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
11447 init_test(cx, |_| {});
11448
11449 let mut cx = EditorLspTestContext::new_rust(
11450 lsp::ServerCapabilities {
11451 completion_provider: Some(lsp::CompletionOptions {
11452 trigger_characters: Some(vec![".".to_string()]),
11453 resolve_provider: Some(true),
11454 ..Default::default()
11455 }),
11456 ..Default::default()
11457 },
11458 cx,
11459 )
11460 .await;
11461
11462 cx.set_state("fn main() { let a = 2ˇ; }");
11463 cx.simulate_keystroke(".");
11464 let completion_item = lsp::CompletionItem {
11465 label: "Some".into(),
11466 kind: Some(lsp::CompletionItemKind::SNIPPET),
11467 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
11468 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
11469 kind: lsp::MarkupKind::Markdown,
11470 value: "```rust\nSome(2)\n```".to_string(),
11471 })),
11472 deprecated: Some(false),
11473 sort_text: Some("Some".to_string()),
11474 filter_text: Some("Some".to_string()),
11475 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
11476 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11477 range: lsp::Range {
11478 start: lsp::Position {
11479 line: 0,
11480 character: 22,
11481 },
11482 end: lsp::Position {
11483 line: 0,
11484 character: 22,
11485 },
11486 },
11487 new_text: "Some(2)".to_string(),
11488 })),
11489 additional_text_edits: Some(vec![lsp::TextEdit {
11490 range: lsp::Range {
11491 start: lsp::Position {
11492 line: 0,
11493 character: 20,
11494 },
11495 end: lsp::Position {
11496 line: 0,
11497 character: 22,
11498 },
11499 },
11500 new_text: "".to_string(),
11501 }]),
11502 ..Default::default()
11503 };
11504
11505 let closure_completion_item = completion_item.clone();
11506 let counter = Arc::new(AtomicUsize::new(0));
11507 let counter_clone = counter.clone();
11508 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
11509 let task_completion_item = closure_completion_item.clone();
11510 counter_clone.fetch_add(1, atomic::Ordering::Release);
11511 async move {
11512 Ok(Some(lsp::CompletionResponse::Array(vec![
11513 task_completion_item,
11514 ])))
11515 }
11516 });
11517
11518 cx.condition(|editor, _| editor.context_menu_visible())
11519 .await;
11520 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
11521 assert!(request.next().await.is_some());
11522 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11523
11524 cx.simulate_keystrokes("S o m");
11525 cx.condition(|editor, _| editor.context_menu_visible())
11526 .await;
11527 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
11528 assert!(request.next().await.is_some());
11529 assert!(request.next().await.is_some());
11530 assert!(request.next().await.is_some());
11531 request.close();
11532 assert!(request.next().await.is_none());
11533 assert_eq!(
11534 counter.load(atomic::Ordering::Acquire),
11535 4,
11536 "With the completions menu open, only one LSP request should happen per input"
11537 );
11538}
11539
11540#[gpui::test]
11541async fn test_toggle_comment(cx: &mut TestAppContext) {
11542 init_test(cx, |_| {});
11543 let mut cx = EditorTestContext::new(cx).await;
11544 let language = Arc::new(Language::new(
11545 LanguageConfig {
11546 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
11547 ..Default::default()
11548 },
11549 Some(tree_sitter_rust::LANGUAGE.into()),
11550 ));
11551 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
11552
11553 // If multiple selections intersect a line, the line is only toggled once.
11554 cx.set_state(indoc! {"
11555 fn a() {
11556 «//b();
11557 ˇ»// «c();
11558 //ˇ» d();
11559 }
11560 "});
11561
11562 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11563
11564 cx.assert_editor_state(indoc! {"
11565 fn a() {
11566 «b();
11567 c();
11568 ˇ» d();
11569 }
11570 "});
11571
11572 // The comment prefix is inserted at the same column for every line in a
11573 // selection.
11574 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11575
11576 cx.assert_editor_state(indoc! {"
11577 fn a() {
11578 // «b();
11579 // c();
11580 ˇ»// d();
11581 }
11582 "});
11583
11584 // If a selection ends at the beginning of a line, that line is not toggled.
11585 cx.set_selections_state(indoc! {"
11586 fn a() {
11587 // b();
11588 «// c();
11589 ˇ» // d();
11590 }
11591 "});
11592
11593 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11594
11595 cx.assert_editor_state(indoc! {"
11596 fn a() {
11597 // b();
11598 «c();
11599 ˇ» // d();
11600 }
11601 "});
11602
11603 // If a selection span a single line and is empty, the line is toggled.
11604 cx.set_state(indoc! {"
11605 fn a() {
11606 a();
11607 b();
11608 ˇ
11609 }
11610 "});
11611
11612 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11613
11614 cx.assert_editor_state(indoc! {"
11615 fn a() {
11616 a();
11617 b();
11618 //•ˇ
11619 }
11620 "});
11621
11622 // If a selection span multiple lines, empty lines are not toggled.
11623 cx.set_state(indoc! {"
11624 fn a() {
11625 «a();
11626
11627 c();ˇ»
11628 }
11629 "});
11630
11631 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11632
11633 cx.assert_editor_state(indoc! {"
11634 fn a() {
11635 // «a();
11636
11637 // c();ˇ»
11638 }
11639 "});
11640
11641 // If a selection includes multiple comment prefixes, all lines are uncommented.
11642 cx.set_state(indoc! {"
11643 fn a() {
11644 «// a();
11645 /// b();
11646 //! c();ˇ»
11647 }
11648 "});
11649
11650 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11651
11652 cx.assert_editor_state(indoc! {"
11653 fn a() {
11654 «a();
11655 b();
11656 c();ˇ»
11657 }
11658 "});
11659}
11660
11661#[gpui::test]
11662async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
11663 init_test(cx, |_| {});
11664 let mut cx = EditorTestContext::new(cx).await;
11665 let language = Arc::new(Language::new(
11666 LanguageConfig {
11667 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
11668 ..Default::default()
11669 },
11670 Some(tree_sitter_rust::LANGUAGE.into()),
11671 ));
11672 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
11673
11674 let toggle_comments = &ToggleComments {
11675 advance_downwards: false,
11676 ignore_indent: true,
11677 };
11678
11679 // If multiple selections intersect a line, the line is only toggled once.
11680 cx.set_state(indoc! {"
11681 fn a() {
11682 // «b();
11683 // c();
11684 // ˇ» d();
11685 }
11686 "});
11687
11688 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11689
11690 cx.assert_editor_state(indoc! {"
11691 fn a() {
11692 «b();
11693 c();
11694 ˇ» d();
11695 }
11696 "});
11697
11698 // The comment prefix is inserted at the beginning of each line
11699 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11700
11701 cx.assert_editor_state(indoc! {"
11702 fn a() {
11703 // «b();
11704 // c();
11705 // ˇ» d();
11706 }
11707 "});
11708
11709 // If a selection ends at the beginning of a line, that line is not toggled.
11710 cx.set_selections_state(indoc! {"
11711 fn a() {
11712 // b();
11713 // «c();
11714 ˇ»// d();
11715 }
11716 "});
11717
11718 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11719
11720 cx.assert_editor_state(indoc! {"
11721 fn a() {
11722 // b();
11723 «c();
11724 ˇ»// d();
11725 }
11726 "});
11727
11728 // If a selection span a single line and is empty, the line is toggled.
11729 cx.set_state(indoc! {"
11730 fn a() {
11731 a();
11732 b();
11733 ˇ
11734 }
11735 "});
11736
11737 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11738
11739 cx.assert_editor_state(indoc! {"
11740 fn a() {
11741 a();
11742 b();
11743 //ˇ
11744 }
11745 "});
11746
11747 // If a selection span multiple lines, empty lines are not toggled.
11748 cx.set_state(indoc! {"
11749 fn a() {
11750 «a();
11751
11752 c();ˇ»
11753 }
11754 "});
11755
11756 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11757
11758 cx.assert_editor_state(indoc! {"
11759 fn a() {
11760 // «a();
11761
11762 // c();ˇ»
11763 }
11764 "});
11765
11766 // If a selection includes multiple comment prefixes, all lines are uncommented.
11767 cx.set_state(indoc! {"
11768 fn a() {
11769 // «a();
11770 /// b();
11771 //! c();ˇ»
11772 }
11773 "});
11774
11775 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11776
11777 cx.assert_editor_state(indoc! {"
11778 fn a() {
11779 «a();
11780 b();
11781 c();ˇ»
11782 }
11783 "});
11784}
11785
11786#[gpui::test]
11787async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
11788 init_test(cx, |_| {});
11789
11790 let language = Arc::new(Language::new(
11791 LanguageConfig {
11792 line_comments: vec!["// ".into()],
11793 ..Default::default()
11794 },
11795 Some(tree_sitter_rust::LANGUAGE.into()),
11796 ));
11797
11798 let mut cx = EditorTestContext::new(cx).await;
11799
11800 cx.language_registry().add(language.clone());
11801 cx.update_buffer(|buffer, cx| {
11802 buffer.set_language(Some(language), cx);
11803 });
11804
11805 let toggle_comments = &ToggleComments {
11806 advance_downwards: true,
11807 ignore_indent: false,
11808 };
11809
11810 // Single cursor on one line -> advance
11811 // Cursor moves horizontally 3 characters as well on non-blank line
11812 cx.set_state(indoc!(
11813 "fn a() {
11814 ˇdog();
11815 cat();
11816 }"
11817 ));
11818 cx.update_editor(|editor, window, cx| {
11819 editor.toggle_comments(toggle_comments, window, cx);
11820 });
11821 cx.assert_editor_state(indoc!(
11822 "fn a() {
11823 // dog();
11824 catˇ();
11825 }"
11826 ));
11827
11828 // Single selection on one line -> don't advance
11829 cx.set_state(indoc!(
11830 "fn a() {
11831 «dog()ˇ»;
11832 cat();
11833 }"
11834 ));
11835 cx.update_editor(|editor, window, cx| {
11836 editor.toggle_comments(toggle_comments, window, cx);
11837 });
11838 cx.assert_editor_state(indoc!(
11839 "fn a() {
11840 // «dog()ˇ»;
11841 cat();
11842 }"
11843 ));
11844
11845 // Multiple cursors on one line -> advance
11846 cx.set_state(indoc!(
11847 "fn a() {
11848 ˇdˇog();
11849 cat();
11850 }"
11851 ));
11852 cx.update_editor(|editor, window, cx| {
11853 editor.toggle_comments(toggle_comments, window, cx);
11854 });
11855 cx.assert_editor_state(indoc!(
11856 "fn a() {
11857 // dog();
11858 catˇ(ˇ);
11859 }"
11860 ));
11861
11862 // Multiple cursors on one line, with selection -> don't advance
11863 cx.set_state(indoc!(
11864 "fn a() {
11865 ˇdˇog«()ˇ»;
11866 cat();
11867 }"
11868 ));
11869 cx.update_editor(|editor, window, cx| {
11870 editor.toggle_comments(toggle_comments, window, cx);
11871 });
11872 cx.assert_editor_state(indoc!(
11873 "fn a() {
11874 // ˇdˇog«()ˇ»;
11875 cat();
11876 }"
11877 ));
11878
11879 // Single cursor on one line -> advance
11880 // Cursor moves to column 0 on blank line
11881 cx.set_state(indoc!(
11882 "fn a() {
11883 ˇdog();
11884
11885 cat();
11886 }"
11887 ));
11888 cx.update_editor(|editor, window, cx| {
11889 editor.toggle_comments(toggle_comments, window, cx);
11890 });
11891 cx.assert_editor_state(indoc!(
11892 "fn a() {
11893 // dog();
11894 ˇ
11895 cat();
11896 }"
11897 ));
11898
11899 // Single cursor on one line -> advance
11900 // Cursor starts and ends at column 0
11901 cx.set_state(indoc!(
11902 "fn a() {
11903 ˇ dog();
11904 cat();
11905 }"
11906 ));
11907 cx.update_editor(|editor, window, cx| {
11908 editor.toggle_comments(toggle_comments, window, cx);
11909 });
11910 cx.assert_editor_state(indoc!(
11911 "fn a() {
11912 // dog();
11913 ˇ cat();
11914 }"
11915 ));
11916}
11917
11918#[gpui::test]
11919async fn test_toggle_block_comment(cx: &mut TestAppContext) {
11920 init_test(cx, |_| {});
11921
11922 let mut cx = EditorTestContext::new(cx).await;
11923
11924 let html_language = Arc::new(
11925 Language::new(
11926 LanguageConfig {
11927 name: "HTML".into(),
11928 block_comment: Some(("<!-- ".into(), " -->".into())),
11929 ..Default::default()
11930 },
11931 Some(tree_sitter_html::LANGUAGE.into()),
11932 )
11933 .with_injection_query(
11934 r#"
11935 (script_element
11936 (raw_text) @injection.content
11937 (#set! injection.language "javascript"))
11938 "#,
11939 )
11940 .unwrap(),
11941 );
11942
11943 let javascript_language = Arc::new(Language::new(
11944 LanguageConfig {
11945 name: "JavaScript".into(),
11946 line_comments: vec!["// ".into()],
11947 ..Default::default()
11948 },
11949 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
11950 ));
11951
11952 cx.language_registry().add(html_language.clone());
11953 cx.language_registry().add(javascript_language.clone());
11954 cx.update_buffer(|buffer, cx| {
11955 buffer.set_language(Some(html_language), cx);
11956 });
11957
11958 // Toggle comments for empty selections
11959 cx.set_state(
11960 &r#"
11961 <p>A</p>ˇ
11962 <p>B</p>ˇ
11963 <p>C</p>ˇ
11964 "#
11965 .unindent(),
11966 );
11967 cx.update_editor(|editor, window, cx| {
11968 editor.toggle_comments(&ToggleComments::default(), window, cx)
11969 });
11970 cx.assert_editor_state(
11971 &r#"
11972 <!-- <p>A</p>ˇ -->
11973 <!-- <p>B</p>ˇ -->
11974 <!-- <p>C</p>ˇ -->
11975 "#
11976 .unindent(),
11977 );
11978 cx.update_editor(|editor, window, cx| {
11979 editor.toggle_comments(&ToggleComments::default(), window, cx)
11980 });
11981 cx.assert_editor_state(
11982 &r#"
11983 <p>A</p>ˇ
11984 <p>B</p>ˇ
11985 <p>C</p>ˇ
11986 "#
11987 .unindent(),
11988 );
11989
11990 // Toggle comments for mixture of empty and non-empty selections, where
11991 // multiple selections occupy a given line.
11992 cx.set_state(
11993 &r#"
11994 <p>A«</p>
11995 <p>ˇ»B</p>ˇ
11996 <p>C«</p>
11997 <p>ˇ»D</p>ˇ
11998 "#
11999 .unindent(),
12000 );
12001
12002 cx.update_editor(|editor, window, cx| {
12003 editor.toggle_comments(&ToggleComments::default(), window, cx)
12004 });
12005 cx.assert_editor_state(
12006 &r#"
12007 <!-- <p>A«</p>
12008 <p>ˇ»B</p>ˇ -->
12009 <!-- <p>C«</p>
12010 <p>ˇ»D</p>ˇ -->
12011 "#
12012 .unindent(),
12013 );
12014 cx.update_editor(|editor, window, cx| {
12015 editor.toggle_comments(&ToggleComments::default(), window, cx)
12016 });
12017 cx.assert_editor_state(
12018 &r#"
12019 <p>A«</p>
12020 <p>ˇ»B</p>ˇ
12021 <p>C«</p>
12022 <p>ˇ»D</p>ˇ
12023 "#
12024 .unindent(),
12025 );
12026
12027 // Toggle comments when different languages are active for different
12028 // selections.
12029 cx.set_state(
12030 &r#"
12031 ˇ<script>
12032 ˇvar x = new Y();
12033 ˇ</script>
12034 "#
12035 .unindent(),
12036 );
12037 cx.executor().run_until_parked();
12038 cx.update_editor(|editor, window, cx| {
12039 editor.toggle_comments(&ToggleComments::default(), window, cx)
12040 });
12041 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
12042 // Uncommenting and commenting from this position brings in even more wrong artifacts.
12043 cx.assert_editor_state(
12044 &r#"
12045 <!-- ˇ<script> -->
12046 // ˇvar x = new Y();
12047 <!-- ˇ</script> -->
12048 "#
12049 .unindent(),
12050 );
12051}
12052
12053#[gpui::test]
12054fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
12055 init_test(cx, |_| {});
12056
12057 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12058 let multibuffer = cx.new(|cx| {
12059 let mut multibuffer = MultiBuffer::new(ReadWrite);
12060 multibuffer.push_excerpts(
12061 buffer.clone(),
12062 [
12063 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
12064 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
12065 ],
12066 cx,
12067 );
12068 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
12069 multibuffer
12070 });
12071
12072 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12073 editor.update_in(cx, |editor, window, cx| {
12074 assert_eq!(editor.text(cx), "aaaa\nbbbb");
12075 editor.change_selections(None, window, cx, |s| {
12076 s.select_ranges([
12077 Point::new(0, 0)..Point::new(0, 0),
12078 Point::new(1, 0)..Point::new(1, 0),
12079 ])
12080 });
12081
12082 editor.handle_input("X", window, cx);
12083 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
12084 assert_eq!(
12085 editor.selections.ranges(cx),
12086 [
12087 Point::new(0, 1)..Point::new(0, 1),
12088 Point::new(1, 1)..Point::new(1, 1),
12089 ]
12090 );
12091
12092 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
12093 editor.change_selections(None, window, cx, |s| {
12094 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
12095 });
12096 editor.backspace(&Default::default(), window, cx);
12097 assert_eq!(editor.text(cx), "Xa\nbbb");
12098 assert_eq!(
12099 editor.selections.ranges(cx),
12100 [Point::new(1, 0)..Point::new(1, 0)]
12101 );
12102
12103 editor.change_selections(None, window, cx, |s| {
12104 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
12105 });
12106 editor.backspace(&Default::default(), window, cx);
12107 assert_eq!(editor.text(cx), "X\nbb");
12108 assert_eq!(
12109 editor.selections.ranges(cx),
12110 [Point::new(0, 1)..Point::new(0, 1)]
12111 );
12112 });
12113}
12114
12115#[gpui::test]
12116fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
12117 init_test(cx, |_| {});
12118
12119 let markers = vec![('[', ']').into(), ('(', ')').into()];
12120 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
12121 indoc! {"
12122 [aaaa
12123 (bbbb]
12124 cccc)",
12125 },
12126 markers.clone(),
12127 );
12128 let excerpt_ranges = markers.into_iter().map(|marker| {
12129 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
12130 ExcerptRange::new(context.clone())
12131 });
12132 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
12133 let multibuffer = cx.new(|cx| {
12134 let mut multibuffer = MultiBuffer::new(ReadWrite);
12135 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
12136 multibuffer
12137 });
12138
12139 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12140 editor.update_in(cx, |editor, window, cx| {
12141 let (expected_text, selection_ranges) = marked_text_ranges(
12142 indoc! {"
12143 aaaa
12144 bˇbbb
12145 bˇbbˇb
12146 cccc"
12147 },
12148 true,
12149 );
12150 assert_eq!(editor.text(cx), expected_text);
12151 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
12152
12153 editor.handle_input("X", window, cx);
12154
12155 let (expected_text, expected_selections) = marked_text_ranges(
12156 indoc! {"
12157 aaaa
12158 bXˇbbXb
12159 bXˇbbXˇb
12160 cccc"
12161 },
12162 false,
12163 );
12164 assert_eq!(editor.text(cx), expected_text);
12165 assert_eq!(editor.selections.ranges(cx), expected_selections);
12166
12167 editor.newline(&Newline, window, cx);
12168 let (expected_text, expected_selections) = marked_text_ranges(
12169 indoc! {"
12170 aaaa
12171 bX
12172 ˇbbX
12173 b
12174 bX
12175 ˇbbX
12176 ˇb
12177 cccc"
12178 },
12179 false,
12180 );
12181 assert_eq!(editor.text(cx), expected_text);
12182 assert_eq!(editor.selections.ranges(cx), expected_selections);
12183 });
12184}
12185
12186#[gpui::test]
12187fn test_refresh_selections(cx: &mut TestAppContext) {
12188 init_test(cx, |_| {});
12189
12190 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12191 let mut excerpt1_id = None;
12192 let multibuffer = cx.new(|cx| {
12193 let mut multibuffer = MultiBuffer::new(ReadWrite);
12194 excerpt1_id = multibuffer
12195 .push_excerpts(
12196 buffer.clone(),
12197 [
12198 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12199 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12200 ],
12201 cx,
12202 )
12203 .into_iter()
12204 .next();
12205 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12206 multibuffer
12207 });
12208
12209 let editor = cx.add_window(|window, cx| {
12210 let mut editor = build_editor(multibuffer.clone(), window, cx);
12211 let snapshot = editor.snapshot(window, cx);
12212 editor.change_selections(None, window, cx, |s| {
12213 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
12214 });
12215 editor.begin_selection(
12216 Point::new(2, 1).to_display_point(&snapshot),
12217 true,
12218 1,
12219 window,
12220 cx,
12221 );
12222 assert_eq!(
12223 editor.selections.ranges(cx),
12224 [
12225 Point::new(1, 3)..Point::new(1, 3),
12226 Point::new(2, 1)..Point::new(2, 1),
12227 ]
12228 );
12229 editor
12230 });
12231
12232 // Refreshing selections is a no-op when excerpts haven't changed.
12233 _ = editor.update(cx, |editor, window, cx| {
12234 editor.change_selections(None, window, cx, |s| s.refresh());
12235 assert_eq!(
12236 editor.selections.ranges(cx),
12237 [
12238 Point::new(1, 3)..Point::new(1, 3),
12239 Point::new(2, 1)..Point::new(2, 1),
12240 ]
12241 );
12242 });
12243
12244 multibuffer.update(cx, |multibuffer, cx| {
12245 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12246 });
12247 _ = editor.update(cx, |editor, window, cx| {
12248 // Removing an excerpt causes the first selection to become degenerate.
12249 assert_eq!(
12250 editor.selections.ranges(cx),
12251 [
12252 Point::new(0, 0)..Point::new(0, 0),
12253 Point::new(0, 1)..Point::new(0, 1)
12254 ]
12255 );
12256
12257 // Refreshing selections will relocate the first selection to the original buffer
12258 // location.
12259 editor.change_selections(None, window, cx, |s| s.refresh());
12260 assert_eq!(
12261 editor.selections.ranges(cx),
12262 [
12263 Point::new(0, 1)..Point::new(0, 1),
12264 Point::new(0, 3)..Point::new(0, 3)
12265 ]
12266 );
12267 assert!(editor.selections.pending_anchor().is_some());
12268 });
12269}
12270
12271#[gpui::test]
12272fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
12273 init_test(cx, |_| {});
12274
12275 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12276 let mut excerpt1_id = None;
12277 let multibuffer = cx.new(|cx| {
12278 let mut multibuffer = MultiBuffer::new(ReadWrite);
12279 excerpt1_id = multibuffer
12280 .push_excerpts(
12281 buffer.clone(),
12282 [
12283 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12284 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12285 ],
12286 cx,
12287 )
12288 .into_iter()
12289 .next();
12290 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12291 multibuffer
12292 });
12293
12294 let editor = cx.add_window(|window, cx| {
12295 let mut editor = build_editor(multibuffer.clone(), window, cx);
12296 let snapshot = editor.snapshot(window, cx);
12297 editor.begin_selection(
12298 Point::new(1, 3).to_display_point(&snapshot),
12299 false,
12300 1,
12301 window,
12302 cx,
12303 );
12304 assert_eq!(
12305 editor.selections.ranges(cx),
12306 [Point::new(1, 3)..Point::new(1, 3)]
12307 );
12308 editor
12309 });
12310
12311 multibuffer.update(cx, |multibuffer, cx| {
12312 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12313 });
12314 _ = editor.update(cx, |editor, window, cx| {
12315 assert_eq!(
12316 editor.selections.ranges(cx),
12317 [Point::new(0, 0)..Point::new(0, 0)]
12318 );
12319
12320 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
12321 editor.change_selections(None, window, cx, |s| s.refresh());
12322 assert_eq!(
12323 editor.selections.ranges(cx),
12324 [Point::new(0, 3)..Point::new(0, 3)]
12325 );
12326 assert!(editor.selections.pending_anchor().is_some());
12327 });
12328}
12329
12330#[gpui::test]
12331async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
12332 init_test(cx, |_| {});
12333
12334 let language = Arc::new(
12335 Language::new(
12336 LanguageConfig {
12337 brackets: BracketPairConfig {
12338 pairs: vec![
12339 BracketPair {
12340 start: "{".to_string(),
12341 end: "}".to_string(),
12342 close: true,
12343 surround: true,
12344 newline: true,
12345 },
12346 BracketPair {
12347 start: "/* ".to_string(),
12348 end: " */".to_string(),
12349 close: true,
12350 surround: true,
12351 newline: true,
12352 },
12353 ],
12354 ..Default::default()
12355 },
12356 ..Default::default()
12357 },
12358 Some(tree_sitter_rust::LANGUAGE.into()),
12359 )
12360 .with_indents_query("")
12361 .unwrap(),
12362 );
12363
12364 let text = concat!(
12365 "{ }\n", //
12366 " x\n", //
12367 " /* */\n", //
12368 "x\n", //
12369 "{{} }\n", //
12370 );
12371
12372 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
12373 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12374 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
12375 editor
12376 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
12377 .await;
12378
12379 editor.update_in(cx, |editor, window, cx| {
12380 editor.change_selections(None, window, cx, |s| {
12381 s.select_display_ranges([
12382 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
12383 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
12384 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
12385 ])
12386 });
12387 editor.newline(&Newline, window, cx);
12388
12389 assert_eq!(
12390 editor.buffer().read(cx).read(cx).text(),
12391 concat!(
12392 "{ \n", // Suppress rustfmt
12393 "\n", //
12394 "}\n", //
12395 " x\n", //
12396 " /* \n", //
12397 " \n", //
12398 " */\n", //
12399 "x\n", //
12400 "{{} \n", //
12401 "}\n", //
12402 )
12403 );
12404 });
12405}
12406
12407#[gpui::test]
12408fn test_highlighted_ranges(cx: &mut TestAppContext) {
12409 init_test(cx, |_| {});
12410
12411 let editor = cx.add_window(|window, cx| {
12412 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
12413 build_editor(buffer.clone(), window, cx)
12414 });
12415
12416 _ = editor.update(cx, |editor, window, cx| {
12417 struct Type1;
12418 struct Type2;
12419
12420 let buffer = editor.buffer.read(cx).snapshot(cx);
12421
12422 let anchor_range =
12423 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
12424
12425 editor.highlight_background::<Type1>(
12426 &[
12427 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
12428 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
12429 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
12430 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
12431 ],
12432 |_| Hsla::red(),
12433 cx,
12434 );
12435 editor.highlight_background::<Type2>(
12436 &[
12437 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
12438 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
12439 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
12440 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
12441 ],
12442 |_| Hsla::green(),
12443 cx,
12444 );
12445
12446 let snapshot = editor.snapshot(window, cx);
12447 let mut highlighted_ranges = editor.background_highlights_in_range(
12448 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
12449 &snapshot,
12450 cx.theme().colors(),
12451 );
12452 // Enforce a consistent ordering based on color without relying on the ordering of the
12453 // highlight's `TypeId` which is non-executor.
12454 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
12455 assert_eq!(
12456 highlighted_ranges,
12457 &[
12458 (
12459 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
12460 Hsla::red(),
12461 ),
12462 (
12463 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
12464 Hsla::red(),
12465 ),
12466 (
12467 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
12468 Hsla::green(),
12469 ),
12470 (
12471 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
12472 Hsla::green(),
12473 ),
12474 ]
12475 );
12476 assert_eq!(
12477 editor.background_highlights_in_range(
12478 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
12479 &snapshot,
12480 cx.theme().colors(),
12481 ),
12482 &[(
12483 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
12484 Hsla::red(),
12485 )]
12486 );
12487 });
12488}
12489
12490#[gpui::test]
12491async fn test_following(cx: &mut TestAppContext) {
12492 init_test(cx, |_| {});
12493
12494 let fs = FakeFs::new(cx.executor());
12495 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
12496
12497 let buffer = project.update(cx, |project, cx| {
12498 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
12499 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
12500 });
12501 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
12502 let follower = cx.update(|cx| {
12503 cx.open_window(
12504 WindowOptions {
12505 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
12506 gpui::Point::new(px(0.), px(0.)),
12507 gpui::Point::new(px(10.), px(80.)),
12508 ))),
12509 ..Default::default()
12510 },
12511 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
12512 )
12513 .unwrap()
12514 });
12515
12516 let is_still_following = Rc::new(RefCell::new(true));
12517 let follower_edit_event_count = Rc::new(RefCell::new(0));
12518 let pending_update = Rc::new(RefCell::new(None));
12519 let leader_entity = leader.root(cx).unwrap();
12520 let follower_entity = follower.root(cx).unwrap();
12521 _ = follower.update(cx, {
12522 let update = pending_update.clone();
12523 let is_still_following = is_still_following.clone();
12524 let follower_edit_event_count = follower_edit_event_count.clone();
12525 |_, window, cx| {
12526 cx.subscribe_in(
12527 &leader_entity,
12528 window,
12529 move |_, leader, event, window, cx| {
12530 leader.read(cx).add_event_to_update_proto(
12531 event,
12532 &mut update.borrow_mut(),
12533 window,
12534 cx,
12535 );
12536 },
12537 )
12538 .detach();
12539
12540 cx.subscribe_in(
12541 &follower_entity,
12542 window,
12543 move |_, _, event: &EditorEvent, _window, _cx| {
12544 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
12545 *is_still_following.borrow_mut() = false;
12546 }
12547
12548 if let EditorEvent::BufferEdited = event {
12549 *follower_edit_event_count.borrow_mut() += 1;
12550 }
12551 },
12552 )
12553 .detach();
12554 }
12555 });
12556
12557 // Update the selections only
12558 _ = leader.update(cx, |leader, window, cx| {
12559 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
12560 });
12561 follower
12562 .update(cx, |follower, window, cx| {
12563 follower.apply_update_proto(
12564 &project,
12565 pending_update.borrow_mut().take().unwrap(),
12566 window,
12567 cx,
12568 )
12569 })
12570 .unwrap()
12571 .await
12572 .unwrap();
12573 _ = follower.update(cx, |follower, _, cx| {
12574 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
12575 });
12576 assert!(*is_still_following.borrow());
12577 assert_eq!(*follower_edit_event_count.borrow(), 0);
12578
12579 // Update the scroll position only
12580 _ = leader.update(cx, |leader, window, cx| {
12581 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
12582 });
12583 follower
12584 .update(cx, |follower, window, cx| {
12585 follower.apply_update_proto(
12586 &project,
12587 pending_update.borrow_mut().take().unwrap(),
12588 window,
12589 cx,
12590 )
12591 })
12592 .unwrap()
12593 .await
12594 .unwrap();
12595 assert_eq!(
12596 follower
12597 .update(cx, |follower, _, cx| follower.scroll_position(cx))
12598 .unwrap(),
12599 gpui::Point::new(1.5, 3.5)
12600 );
12601 assert!(*is_still_following.borrow());
12602 assert_eq!(*follower_edit_event_count.borrow(), 0);
12603
12604 // Update the selections and scroll position. The follower's scroll position is updated
12605 // via autoscroll, not via the leader's exact scroll position.
12606 _ = leader.update(cx, |leader, window, cx| {
12607 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
12608 leader.request_autoscroll(Autoscroll::newest(), cx);
12609 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
12610 });
12611 follower
12612 .update(cx, |follower, window, cx| {
12613 follower.apply_update_proto(
12614 &project,
12615 pending_update.borrow_mut().take().unwrap(),
12616 window,
12617 cx,
12618 )
12619 })
12620 .unwrap()
12621 .await
12622 .unwrap();
12623 _ = follower.update(cx, |follower, _, cx| {
12624 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
12625 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
12626 });
12627 assert!(*is_still_following.borrow());
12628
12629 // Creating a pending selection that precedes another selection
12630 _ = leader.update(cx, |leader, window, cx| {
12631 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
12632 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
12633 });
12634 follower
12635 .update(cx, |follower, window, cx| {
12636 follower.apply_update_proto(
12637 &project,
12638 pending_update.borrow_mut().take().unwrap(),
12639 window,
12640 cx,
12641 )
12642 })
12643 .unwrap()
12644 .await
12645 .unwrap();
12646 _ = follower.update(cx, |follower, _, cx| {
12647 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
12648 });
12649 assert!(*is_still_following.borrow());
12650
12651 // Extend the pending selection so that it surrounds another selection
12652 _ = leader.update(cx, |leader, window, cx| {
12653 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
12654 });
12655 follower
12656 .update(cx, |follower, window, cx| {
12657 follower.apply_update_proto(
12658 &project,
12659 pending_update.borrow_mut().take().unwrap(),
12660 window,
12661 cx,
12662 )
12663 })
12664 .unwrap()
12665 .await
12666 .unwrap();
12667 _ = follower.update(cx, |follower, _, cx| {
12668 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
12669 });
12670
12671 // Scrolling locally breaks the follow
12672 _ = follower.update(cx, |follower, window, cx| {
12673 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
12674 follower.set_scroll_anchor(
12675 ScrollAnchor {
12676 anchor: top_anchor,
12677 offset: gpui::Point::new(0.0, 0.5),
12678 },
12679 window,
12680 cx,
12681 );
12682 });
12683 assert!(!(*is_still_following.borrow()));
12684}
12685
12686#[gpui::test]
12687async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
12688 init_test(cx, |_| {});
12689
12690 let fs = FakeFs::new(cx.executor());
12691 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
12692 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12693 let pane = workspace
12694 .update(cx, |workspace, _, _| workspace.active_pane().clone())
12695 .unwrap();
12696
12697 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12698
12699 let leader = pane.update_in(cx, |_, window, cx| {
12700 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
12701 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
12702 });
12703
12704 // Start following the editor when it has no excerpts.
12705 let mut state_message =
12706 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
12707 let workspace_entity = workspace.root(cx).unwrap();
12708 let follower_1 = cx
12709 .update_window(*workspace.deref(), |_, window, cx| {
12710 Editor::from_state_proto(
12711 workspace_entity,
12712 ViewId {
12713 creator: CollaboratorId::PeerId(PeerId::default()),
12714 id: 0,
12715 },
12716 &mut state_message,
12717 window,
12718 cx,
12719 )
12720 })
12721 .unwrap()
12722 .unwrap()
12723 .await
12724 .unwrap();
12725
12726 let update_message = Rc::new(RefCell::new(None));
12727 follower_1.update_in(cx, {
12728 let update = update_message.clone();
12729 |_, window, cx| {
12730 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
12731 leader.read(cx).add_event_to_update_proto(
12732 event,
12733 &mut update.borrow_mut(),
12734 window,
12735 cx,
12736 );
12737 })
12738 .detach();
12739 }
12740 });
12741
12742 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
12743 (
12744 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
12745 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
12746 )
12747 });
12748
12749 // Insert some excerpts.
12750 leader.update(cx, |leader, cx| {
12751 leader.buffer.update(cx, |multibuffer, cx| {
12752 multibuffer.set_excerpts_for_path(
12753 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
12754 buffer_1.clone(),
12755 vec![
12756 Point::row_range(0..3),
12757 Point::row_range(1..6),
12758 Point::row_range(12..15),
12759 ],
12760 0,
12761 cx,
12762 );
12763 multibuffer.set_excerpts_for_path(
12764 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
12765 buffer_2.clone(),
12766 vec![Point::row_range(0..6), Point::row_range(8..12)],
12767 0,
12768 cx,
12769 );
12770 });
12771 });
12772
12773 // Apply the update of adding the excerpts.
12774 follower_1
12775 .update_in(cx, |follower, window, cx| {
12776 follower.apply_update_proto(
12777 &project,
12778 update_message.borrow().clone().unwrap(),
12779 window,
12780 cx,
12781 )
12782 })
12783 .await
12784 .unwrap();
12785 assert_eq!(
12786 follower_1.update(cx, |editor, cx| editor.text(cx)),
12787 leader.update(cx, |editor, cx| editor.text(cx))
12788 );
12789 update_message.borrow_mut().take();
12790
12791 // Start following separately after it already has excerpts.
12792 let mut state_message =
12793 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
12794 let workspace_entity = workspace.root(cx).unwrap();
12795 let follower_2 = cx
12796 .update_window(*workspace.deref(), |_, window, cx| {
12797 Editor::from_state_proto(
12798 workspace_entity,
12799 ViewId {
12800 creator: CollaboratorId::PeerId(PeerId::default()),
12801 id: 0,
12802 },
12803 &mut state_message,
12804 window,
12805 cx,
12806 )
12807 })
12808 .unwrap()
12809 .unwrap()
12810 .await
12811 .unwrap();
12812 assert_eq!(
12813 follower_2.update(cx, |editor, cx| editor.text(cx)),
12814 leader.update(cx, |editor, cx| editor.text(cx))
12815 );
12816
12817 // Remove some excerpts.
12818 leader.update(cx, |leader, cx| {
12819 leader.buffer.update(cx, |multibuffer, cx| {
12820 let excerpt_ids = multibuffer.excerpt_ids();
12821 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
12822 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
12823 });
12824 });
12825
12826 // Apply the update of removing the excerpts.
12827 follower_1
12828 .update_in(cx, |follower, window, cx| {
12829 follower.apply_update_proto(
12830 &project,
12831 update_message.borrow().clone().unwrap(),
12832 window,
12833 cx,
12834 )
12835 })
12836 .await
12837 .unwrap();
12838 follower_2
12839 .update_in(cx, |follower, window, cx| {
12840 follower.apply_update_proto(
12841 &project,
12842 update_message.borrow().clone().unwrap(),
12843 window,
12844 cx,
12845 )
12846 })
12847 .await
12848 .unwrap();
12849 update_message.borrow_mut().take();
12850 assert_eq!(
12851 follower_1.update(cx, |editor, cx| editor.text(cx)),
12852 leader.update(cx, |editor, cx| editor.text(cx))
12853 );
12854}
12855
12856#[gpui::test]
12857async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
12858 init_test(cx, |_| {});
12859
12860 let mut cx = EditorTestContext::new(cx).await;
12861 let lsp_store =
12862 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
12863
12864 cx.set_state(indoc! {"
12865 ˇfn func(abc def: i32) -> u32 {
12866 }
12867 "});
12868
12869 cx.update(|_, cx| {
12870 lsp_store.update(cx, |lsp_store, cx| {
12871 lsp_store
12872 .update_diagnostics(
12873 LanguageServerId(0),
12874 lsp::PublishDiagnosticsParams {
12875 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
12876 version: None,
12877 diagnostics: vec![
12878 lsp::Diagnostic {
12879 range: lsp::Range::new(
12880 lsp::Position::new(0, 11),
12881 lsp::Position::new(0, 12),
12882 ),
12883 severity: Some(lsp::DiagnosticSeverity::ERROR),
12884 ..Default::default()
12885 },
12886 lsp::Diagnostic {
12887 range: lsp::Range::new(
12888 lsp::Position::new(0, 12),
12889 lsp::Position::new(0, 15),
12890 ),
12891 severity: Some(lsp::DiagnosticSeverity::ERROR),
12892 ..Default::default()
12893 },
12894 lsp::Diagnostic {
12895 range: lsp::Range::new(
12896 lsp::Position::new(0, 25),
12897 lsp::Position::new(0, 28),
12898 ),
12899 severity: Some(lsp::DiagnosticSeverity::ERROR),
12900 ..Default::default()
12901 },
12902 ],
12903 },
12904 &[],
12905 cx,
12906 )
12907 .unwrap()
12908 });
12909 });
12910
12911 executor.run_until_parked();
12912
12913 cx.update_editor(|editor, window, cx| {
12914 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12915 });
12916
12917 cx.assert_editor_state(indoc! {"
12918 fn func(abc def: i32) -> ˇu32 {
12919 }
12920 "});
12921
12922 cx.update_editor(|editor, window, cx| {
12923 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12924 });
12925
12926 cx.assert_editor_state(indoc! {"
12927 fn func(abc ˇdef: i32) -> u32 {
12928 }
12929 "});
12930
12931 cx.update_editor(|editor, window, cx| {
12932 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12933 });
12934
12935 cx.assert_editor_state(indoc! {"
12936 fn func(abcˇ def: i32) -> u32 {
12937 }
12938 "});
12939
12940 cx.update_editor(|editor, window, cx| {
12941 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12942 });
12943
12944 cx.assert_editor_state(indoc! {"
12945 fn func(abc def: i32) -> ˇu32 {
12946 }
12947 "});
12948}
12949
12950#[gpui::test]
12951async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
12952 init_test(cx, |_| {});
12953
12954 let mut cx = EditorTestContext::new(cx).await;
12955
12956 let diff_base = r#"
12957 use some::mod;
12958
12959 const A: u32 = 42;
12960
12961 fn main() {
12962 println!("hello");
12963
12964 println!("world");
12965 }
12966 "#
12967 .unindent();
12968
12969 // Edits are modified, removed, modified, added
12970 cx.set_state(
12971 &r#"
12972 use some::modified;
12973
12974 ˇ
12975 fn main() {
12976 println!("hello there");
12977
12978 println!("around the");
12979 println!("world");
12980 }
12981 "#
12982 .unindent(),
12983 );
12984
12985 cx.set_head_text(&diff_base);
12986 executor.run_until_parked();
12987
12988 cx.update_editor(|editor, window, cx| {
12989 //Wrap around the bottom of the buffer
12990 for _ in 0..3 {
12991 editor.go_to_next_hunk(&GoToHunk, window, cx);
12992 }
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 //Wrap around the top of the buffer
13012 for _ in 0..2 {
13013 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13014 }
13015 });
13016
13017 cx.assert_editor_state(
13018 &r#"
13019 use some::modified;
13020
13021
13022 fn main() {
13023 ˇ println!("hello there");
13024
13025 println!("around the");
13026 println!("world");
13027 }
13028 "#
13029 .unindent(),
13030 );
13031
13032 cx.update_editor(|editor, window, cx| {
13033 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13034 });
13035
13036 cx.assert_editor_state(
13037 &r#"
13038 use some::modified;
13039
13040 ˇ
13041 fn main() {
13042 println!("hello there");
13043
13044 println!("around the");
13045 println!("world");
13046 }
13047 "#
13048 .unindent(),
13049 );
13050
13051 cx.update_editor(|editor, window, cx| {
13052 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13053 });
13054
13055 cx.assert_editor_state(
13056 &r#"
13057 ˇuse some::modified;
13058
13059
13060 fn main() {
13061 println!("hello there");
13062
13063 println!("around the");
13064 println!("world");
13065 }
13066 "#
13067 .unindent(),
13068 );
13069
13070 cx.update_editor(|editor, window, cx| {
13071 for _ in 0..2 {
13072 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13073 }
13074 });
13075
13076 cx.assert_editor_state(
13077 &r#"
13078 use some::modified;
13079
13080
13081 fn main() {
13082 ˇ println!("hello there");
13083
13084 println!("around the");
13085 println!("world");
13086 }
13087 "#
13088 .unindent(),
13089 );
13090
13091 cx.update_editor(|editor, window, cx| {
13092 editor.fold(&Fold, window, cx);
13093 });
13094
13095 cx.update_editor(|editor, window, cx| {
13096 editor.go_to_next_hunk(&GoToHunk, window, cx);
13097 });
13098
13099 cx.assert_editor_state(
13100 &r#"
13101 ˇuse some::modified;
13102
13103
13104 fn main() {
13105 println!("hello there");
13106
13107 println!("around the");
13108 println!("world");
13109 }
13110 "#
13111 .unindent(),
13112 );
13113}
13114
13115#[test]
13116fn test_split_words() {
13117 fn split(text: &str) -> Vec<&str> {
13118 split_words(text).collect()
13119 }
13120
13121 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
13122 assert_eq!(split("hello_world"), &["hello_", "world"]);
13123 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
13124 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
13125 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
13126 assert_eq!(split("helloworld"), &["helloworld"]);
13127
13128 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
13129}
13130
13131#[gpui::test]
13132async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
13133 init_test(cx, |_| {});
13134
13135 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
13136 let mut assert = |before, after| {
13137 let _state_context = cx.set_state(before);
13138 cx.run_until_parked();
13139 cx.update_editor(|editor, window, cx| {
13140 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
13141 });
13142 cx.run_until_parked();
13143 cx.assert_editor_state(after);
13144 };
13145
13146 // Outside bracket jumps to outside of matching bracket
13147 assert("console.logˇ(var);", "console.log(var)ˇ;");
13148 assert("console.log(var)ˇ;", "console.logˇ(var);");
13149
13150 // Inside bracket jumps to inside of matching bracket
13151 assert("console.log(ˇvar);", "console.log(varˇ);");
13152 assert("console.log(varˇ);", "console.log(ˇvar);");
13153
13154 // When outside a bracket and inside, favor jumping to the inside bracket
13155 assert(
13156 "console.log('foo', [1, 2, 3]ˇ);",
13157 "console.log(ˇ'foo', [1, 2, 3]);",
13158 );
13159 assert(
13160 "console.log(ˇ'foo', [1, 2, 3]);",
13161 "console.log('foo', [1, 2, 3]ˇ);",
13162 );
13163
13164 // Bias forward if two options are equally likely
13165 assert(
13166 "let result = curried_fun()ˇ();",
13167 "let result = curried_fun()()ˇ;",
13168 );
13169
13170 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
13171 assert(
13172 indoc! {"
13173 function test() {
13174 console.log('test')ˇ
13175 }"},
13176 indoc! {"
13177 function test() {
13178 console.logˇ('test')
13179 }"},
13180 );
13181}
13182
13183#[gpui::test]
13184async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
13185 init_test(cx, |_| {});
13186
13187 let fs = FakeFs::new(cx.executor());
13188 fs.insert_tree(
13189 path!("/a"),
13190 json!({
13191 "main.rs": "fn main() { let a = 5; }",
13192 "other.rs": "// Test file",
13193 }),
13194 )
13195 .await;
13196 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13197
13198 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13199 language_registry.add(Arc::new(Language::new(
13200 LanguageConfig {
13201 name: "Rust".into(),
13202 matcher: LanguageMatcher {
13203 path_suffixes: vec!["rs".to_string()],
13204 ..Default::default()
13205 },
13206 brackets: BracketPairConfig {
13207 pairs: vec![BracketPair {
13208 start: "{".to_string(),
13209 end: "}".to_string(),
13210 close: true,
13211 surround: true,
13212 newline: true,
13213 }],
13214 disabled_scopes_by_bracket_ix: Vec::new(),
13215 },
13216 ..Default::default()
13217 },
13218 Some(tree_sitter_rust::LANGUAGE.into()),
13219 )));
13220 let mut fake_servers = language_registry.register_fake_lsp(
13221 "Rust",
13222 FakeLspAdapter {
13223 capabilities: lsp::ServerCapabilities {
13224 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
13225 first_trigger_character: "{".to_string(),
13226 more_trigger_character: None,
13227 }),
13228 ..Default::default()
13229 },
13230 ..Default::default()
13231 },
13232 );
13233
13234 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13235
13236 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13237
13238 let worktree_id = workspace
13239 .update(cx, |workspace, _, cx| {
13240 workspace.project().update(cx, |project, cx| {
13241 project.worktrees(cx).next().unwrap().read(cx).id()
13242 })
13243 })
13244 .unwrap();
13245
13246 let buffer = project
13247 .update(cx, |project, cx| {
13248 project.open_local_buffer(path!("/a/main.rs"), cx)
13249 })
13250 .await
13251 .unwrap();
13252 let editor_handle = workspace
13253 .update(cx, |workspace, window, cx| {
13254 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
13255 })
13256 .unwrap()
13257 .await
13258 .unwrap()
13259 .downcast::<Editor>()
13260 .unwrap();
13261
13262 cx.executor().start_waiting();
13263 let fake_server = fake_servers.next().await.unwrap();
13264
13265 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
13266 |params, _| async move {
13267 assert_eq!(
13268 params.text_document_position.text_document.uri,
13269 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
13270 );
13271 assert_eq!(
13272 params.text_document_position.position,
13273 lsp::Position::new(0, 21),
13274 );
13275
13276 Ok(Some(vec![lsp::TextEdit {
13277 new_text: "]".to_string(),
13278 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13279 }]))
13280 },
13281 );
13282
13283 editor_handle.update_in(cx, |editor, window, cx| {
13284 window.focus(&editor.focus_handle(cx));
13285 editor.change_selections(None, window, cx, |s| {
13286 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
13287 });
13288 editor.handle_input("{", window, cx);
13289 });
13290
13291 cx.executor().run_until_parked();
13292
13293 buffer.update(cx, |buffer, _| {
13294 assert_eq!(
13295 buffer.text(),
13296 "fn main() { let a = {5}; }",
13297 "No extra braces from on type formatting should appear in the buffer"
13298 )
13299 });
13300}
13301
13302#[gpui::test]
13303async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
13304 init_test(cx, |_| {});
13305
13306 let fs = FakeFs::new(cx.executor());
13307 fs.insert_tree(
13308 path!("/a"),
13309 json!({
13310 "main.rs": "fn main() { let a = 5; }",
13311 "other.rs": "// Test file",
13312 }),
13313 )
13314 .await;
13315
13316 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13317
13318 let server_restarts = Arc::new(AtomicUsize::new(0));
13319 let closure_restarts = Arc::clone(&server_restarts);
13320 let language_server_name = "test language server";
13321 let language_name: LanguageName = "Rust".into();
13322
13323 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13324 language_registry.add(Arc::new(Language::new(
13325 LanguageConfig {
13326 name: language_name.clone(),
13327 matcher: LanguageMatcher {
13328 path_suffixes: vec!["rs".to_string()],
13329 ..Default::default()
13330 },
13331 ..Default::default()
13332 },
13333 Some(tree_sitter_rust::LANGUAGE.into()),
13334 )));
13335 let mut fake_servers = language_registry.register_fake_lsp(
13336 "Rust",
13337 FakeLspAdapter {
13338 name: language_server_name,
13339 initialization_options: Some(json!({
13340 "testOptionValue": true
13341 })),
13342 initializer: Some(Box::new(move |fake_server| {
13343 let task_restarts = Arc::clone(&closure_restarts);
13344 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
13345 task_restarts.fetch_add(1, atomic::Ordering::Release);
13346 futures::future::ready(Ok(()))
13347 });
13348 })),
13349 ..Default::default()
13350 },
13351 );
13352
13353 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13354 let _buffer = project
13355 .update(cx, |project, cx| {
13356 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
13357 })
13358 .await
13359 .unwrap();
13360 let _fake_server = fake_servers.next().await.unwrap();
13361 update_test_language_settings(cx, |language_settings| {
13362 language_settings.languages.insert(
13363 language_name.clone(),
13364 LanguageSettingsContent {
13365 tab_size: NonZeroU32::new(8),
13366 ..Default::default()
13367 },
13368 );
13369 });
13370 cx.executor().run_until_parked();
13371 assert_eq!(
13372 server_restarts.load(atomic::Ordering::Acquire),
13373 0,
13374 "Should not restart LSP server on an unrelated change"
13375 );
13376
13377 update_test_project_settings(cx, |project_settings| {
13378 project_settings.lsp.insert(
13379 "Some other server name".into(),
13380 LspSettings {
13381 binary: None,
13382 settings: None,
13383 initialization_options: Some(json!({
13384 "some other init value": false
13385 })),
13386 enable_lsp_tasks: false,
13387 },
13388 );
13389 });
13390 cx.executor().run_until_parked();
13391 assert_eq!(
13392 server_restarts.load(atomic::Ordering::Acquire),
13393 0,
13394 "Should not restart LSP server on an unrelated LSP settings change"
13395 );
13396
13397 update_test_project_settings(cx, |project_settings| {
13398 project_settings.lsp.insert(
13399 language_server_name.into(),
13400 LspSettings {
13401 binary: None,
13402 settings: None,
13403 initialization_options: Some(json!({
13404 "anotherInitValue": false
13405 })),
13406 enable_lsp_tasks: false,
13407 },
13408 );
13409 });
13410 cx.executor().run_until_parked();
13411 assert_eq!(
13412 server_restarts.load(atomic::Ordering::Acquire),
13413 1,
13414 "Should restart LSP server on a related LSP settings change"
13415 );
13416
13417 update_test_project_settings(cx, |project_settings| {
13418 project_settings.lsp.insert(
13419 language_server_name.into(),
13420 LspSettings {
13421 binary: None,
13422 settings: None,
13423 initialization_options: Some(json!({
13424 "anotherInitValue": false
13425 })),
13426 enable_lsp_tasks: false,
13427 },
13428 );
13429 });
13430 cx.executor().run_until_parked();
13431 assert_eq!(
13432 server_restarts.load(atomic::Ordering::Acquire),
13433 1,
13434 "Should not restart LSP server on a related LSP settings change that is the same"
13435 );
13436
13437 update_test_project_settings(cx, |project_settings| {
13438 project_settings.lsp.insert(
13439 language_server_name.into(),
13440 LspSettings {
13441 binary: None,
13442 settings: None,
13443 initialization_options: None,
13444 enable_lsp_tasks: false,
13445 },
13446 );
13447 });
13448 cx.executor().run_until_parked();
13449 assert_eq!(
13450 server_restarts.load(atomic::Ordering::Acquire),
13451 2,
13452 "Should restart LSP server on another related LSP settings change"
13453 );
13454}
13455
13456#[gpui::test]
13457async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
13458 init_test(cx, |_| {});
13459
13460 let mut cx = EditorLspTestContext::new_rust(
13461 lsp::ServerCapabilities {
13462 completion_provider: Some(lsp::CompletionOptions {
13463 trigger_characters: Some(vec![".".to_string()]),
13464 resolve_provider: Some(true),
13465 ..Default::default()
13466 }),
13467 ..Default::default()
13468 },
13469 cx,
13470 )
13471 .await;
13472
13473 cx.set_state("fn main() { let a = 2ˇ; }");
13474 cx.simulate_keystroke(".");
13475 let completion_item = lsp::CompletionItem {
13476 label: "some".into(),
13477 kind: Some(lsp::CompletionItemKind::SNIPPET),
13478 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13479 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13480 kind: lsp::MarkupKind::Markdown,
13481 value: "```rust\nSome(2)\n```".to_string(),
13482 })),
13483 deprecated: Some(false),
13484 sort_text: Some("fffffff2".to_string()),
13485 filter_text: Some("some".to_string()),
13486 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13487 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13488 range: lsp::Range {
13489 start: lsp::Position {
13490 line: 0,
13491 character: 22,
13492 },
13493 end: lsp::Position {
13494 line: 0,
13495 character: 22,
13496 },
13497 },
13498 new_text: "Some(2)".to_string(),
13499 })),
13500 additional_text_edits: Some(vec![lsp::TextEdit {
13501 range: lsp::Range {
13502 start: lsp::Position {
13503 line: 0,
13504 character: 20,
13505 },
13506 end: lsp::Position {
13507 line: 0,
13508 character: 22,
13509 },
13510 },
13511 new_text: "".to_string(),
13512 }]),
13513 ..Default::default()
13514 };
13515
13516 let closure_completion_item = completion_item.clone();
13517 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13518 let task_completion_item = closure_completion_item.clone();
13519 async move {
13520 Ok(Some(lsp::CompletionResponse::Array(vec![
13521 task_completion_item,
13522 ])))
13523 }
13524 });
13525
13526 request.next().await;
13527
13528 cx.condition(|editor, _| editor.context_menu_visible())
13529 .await;
13530 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13531 editor
13532 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13533 .unwrap()
13534 });
13535 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
13536
13537 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
13538 let task_completion_item = completion_item.clone();
13539 async move { Ok(task_completion_item) }
13540 })
13541 .next()
13542 .await
13543 .unwrap();
13544 apply_additional_edits.await.unwrap();
13545 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
13546}
13547
13548#[gpui::test]
13549async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
13550 init_test(cx, |_| {});
13551
13552 let mut cx = EditorLspTestContext::new_rust(
13553 lsp::ServerCapabilities {
13554 completion_provider: Some(lsp::CompletionOptions {
13555 trigger_characters: Some(vec![".".to_string()]),
13556 resolve_provider: Some(true),
13557 ..Default::default()
13558 }),
13559 ..Default::default()
13560 },
13561 cx,
13562 )
13563 .await;
13564
13565 cx.set_state("fn main() { let a = 2ˇ; }");
13566 cx.simulate_keystroke(".");
13567
13568 let item1 = lsp::CompletionItem {
13569 label: "method id()".to_string(),
13570 filter_text: Some("id".to_string()),
13571 detail: None,
13572 documentation: None,
13573 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13574 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13575 new_text: ".id".to_string(),
13576 })),
13577 ..lsp::CompletionItem::default()
13578 };
13579
13580 let item2 = lsp::CompletionItem {
13581 label: "other".to_string(),
13582 filter_text: Some("other".to_string()),
13583 detail: None,
13584 documentation: None,
13585 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13586 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13587 new_text: ".other".to_string(),
13588 })),
13589 ..lsp::CompletionItem::default()
13590 };
13591
13592 let item1 = item1.clone();
13593 cx.set_request_handler::<lsp::request::Completion, _, _>({
13594 let item1 = item1.clone();
13595 move |_, _, _| {
13596 let item1 = item1.clone();
13597 let item2 = item2.clone();
13598 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
13599 }
13600 })
13601 .next()
13602 .await;
13603
13604 cx.condition(|editor, _| editor.context_menu_visible())
13605 .await;
13606 cx.update_editor(|editor, _, _| {
13607 let context_menu = editor.context_menu.borrow_mut();
13608 let context_menu = context_menu
13609 .as_ref()
13610 .expect("Should have the context menu deployed");
13611 match context_menu {
13612 CodeContextMenu::Completions(completions_menu) => {
13613 let completions = completions_menu.completions.borrow_mut();
13614 assert_eq!(
13615 completions
13616 .iter()
13617 .map(|completion| &completion.label.text)
13618 .collect::<Vec<_>>(),
13619 vec!["method id()", "other"]
13620 )
13621 }
13622 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13623 }
13624 });
13625
13626 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
13627 let item1 = item1.clone();
13628 move |_, item_to_resolve, _| {
13629 let item1 = item1.clone();
13630 async move {
13631 if item1 == item_to_resolve {
13632 Ok(lsp::CompletionItem {
13633 label: "method id()".to_string(),
13634 filter_text: Some("id".to_string()),
13635 detail: Some("Now resolved!".to_string()),
13636 documentation: Some(lsp::Documentation::String("Docs".to_string())),
13637 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13638 range: lsp::Range::new(
13639 lsp::Position::new(0, 22),
13640 lsp::Position::new(0, 22),
13641 ),
13642 new_text: ".id".to_string(),
13643 })),
13644 ..lsp::CompletionItem::default()
13645 })
13646 } else {
13647 Ok(item_to_resolve)
13648 }
13649 }
13650 }
13651 })
13652 .next()
13653 .await
13654 .unwrap();
13655 cx.run_until_parked();
13656
13657 cx.update_editor(|editor, window, cx| {
13658 editor.context_menu_next(&Default::default(), window, cx);
13659 });
13660
13661 cx.update_editor(|editor, _, _| {
13662 let context_menu = editor.context_menu.borrow_mut();
13663 let context_menu = context_menu
13664 .as_ref()
13665 .expect("Should have the context menu deployed");
13666 match context_menu {
13667 CodeContextMenu::Completions(completions_menu) => {
13668 let completions = completions_menu.completions.borrow_mut();
13669 assert_eq!(
13670 completions
13671 .iter()
13672 .map(|completion| &completion.label.text)
13673 .collect::<Vec<_>>(),
13674 vec!["method id() Now resolved!", "other"],
13675 "Should update first completion label, but not second as the filter text did not match."
13676 );
13677 }
13678 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13679 }
13680 });
13681}
13682
13683#[gpui::test]
13684async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
13685 init_test(cx, |_| {});
13686
13687 let mut cx = EditorLspTestContext::new_rust(
13688 lsp::ServerCapabilities {
13689 completion_provider: Some(lsp::CompletionOptions {
13690 trigger_characters: Some(vec![".".to_string()]),
13691 resolve_provider: Some(true),
13692 ..Default::default()
13693 }),
13694 ..Default::default()
13695 },
13696 cx,
13697 )
13698 .await;
13699
13700 cx.set_state("fn main() { let a = 2ˇ; }");
13701 cx.simulate_keystroke(".");
13702
13703 let unresolved_item_1 = lsp::CompletionItem {
13704 label: "id".to_string(),
13705 filter_text: Some("id".to_string()),
13706 detail: None,
13707 documentation: None,
13708 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13709 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13710 new_text: ".id".to_string(),
13711 })),
13712 ..lsp::CompletionItem::default()
13713 };
13714 let resolved_item_1 = lsp::CompletionItem {
13715 additional_text_edits: Some(vec![lsp::TextEdit {
13716 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
13717 new_text: "!!".to_string(),
13718 }]),
13719 ..unresolved_item_1.clone()
13720 };
13721 let unresolved_item_2 = lsp::CompletionItem {
13722 label: "other".to_string(),
13723 filter_text: Some("other".to_string()),
13724 detail: None,
13725 documentation: None,
13726 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13727 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13728 new_text: ".other".to_string(),
13729 })),
13730 ..lsp::CompletionItem::default()
13731 };
13732 let resolved_item_2 = lsp::CompletionItem {
13733 additional_text_edits: Some(vec![lsp::TextEdit {
13734 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
13735 new_text: "??".to_string(),
13736 }]),
13737 ..unresolved_item_2.clone()
13738 };
13739
13740 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
13741 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
13742 cx.lsp
13743 .server
13744 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
13745 let unresolved_item_1 = unresolved_item_1.clone();
13746 let resolved_item_1 = resolved_item_1.clone();
13747 let unresolved_item_2 = unresolved_item_2.clone();
13748 let resolved_item_2 = resolved_item_2.clone();
13749 let resolve_requests_1 = resolve_requests_1.clone();
13750 let resolve_requests_2 = resolve_requests_2.clone();
13751 move |unresolved_request, _| {
13752 let unresolved_item_1 = unresolved_item_1.clone();
13753 let resolved_item_1 = resolved_item_1.clone();
13754 let unresolved_item_2 = unresolved_item_2.clone();
13755 let resolved_item_2 = resolved_item_2.clone();
13756 let resolve_requests_1 = resolve_requests_1.clone();
13757 let resolve_requests_2 = resolve_requests_2.clone();
13758 async move {
13759 if unresolved_request == unresolved_item_1 {
13760 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
13761 Ok(resolved_item_1.clone())
13762 } else if unresolved_request == unresolved_item_2 {
13763 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
13764 Ok(resolved_item_2.clone())
13765 } else {
13766 panic!("Unexpected completion item {unresolved_request:?}")
13767 }
13768 }
13769 }
13770 })
13771 .detach();
13772
13773 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13774 let unresolved_item_1 = unresolved_item_1.clone();
13775 let unresolved_item_2 = unresolved_item_2.clone();
13776 async move {
13777 Ok(Some(lsp::CompletionResponse::Array(vec![
13778 unresolved_item_1,
13779 unresolved_item_2,
13780 ])))
13781 }
13782 })
13783 .next()
13784 .await;
13785
13786 cx.condition(|editor, _| editor.context_menu_visible())
13787 .await;
13788 cx.update_editor(|editor, _, _| {
13789 let context_menu = editor.context_menu.borrow_mut();
13790 let context_menu = context_menu
13791 .as_ref()
13792 .expect("Should have the context menu deployed");
13793 match context_menu {
13794 CodeContextMenu::Completions(completions_menu) => {
13795 let completions = completions_menu.completions.borrow_mut();
13796 assert_eq!(
13797 completions
13798 .iter()
13799 .map(|completion| &completion.label.text)
13800 .collect::<Vec<_>>(),
13801 vec!["id", "other"]
13802 )
13803 }
13804 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13805 }
13806 });
13807 cx.run_until_parked();
13808
13809 cx.update_editor(|editor, window, cx| {
13810 editor.context_menu_next(&ContextMenuNext, window, cx);
13811 });
13812 cx.run_until_parked();
13813 cx.update_editor(|editor, window, cx| {
13814 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
13815 });
13816 cx.run_until_parked();
13817 cx.update_editor(|editor, window, cx| {
13818 editor.context_menu_next(&ContextMenuNext, window, cx);
13819 });
13820 cx.run_until_parked();
13821 cx.update_editor(|editor, window, cx| {
13822 editor
13823 .compose_completion(&ComposeCompletion::default(), window, cx)
13824 .expect("No task returned")
13825 })
13826 .await
13827 .expect("Completion failed");
13828 cx.run_until_parked();
13829
13830 cx.update_editor(|editor, _, cx| {
13831 assert_eq!(
13832 resolve_requests_1.load(atomic::Ordering::Acquire),
13833 1,
13834 "Should always resolve once despite multiple selections"
13835 );
13836 assert_eq!(
13837 resolve_requests_2.load(atomic::Ordering::Acquire),
13838 1,
13839 "Should always resolve once after multiple selections and applying the completion"
13840 );
13841 assert_eq!(
13842 editor.text(cx),
13843 "fn main() { let a = ??.other; }",
13844 "Should use resolved data when applying the completion"
13845 );
13846 });
13847}
13848
13849#[gpui::test]
13850async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
13851 init_test(cx, |_| {});
13852
13853 let item_0 = lsp::CompletionItem {
13854 label: "abs".into(),
13855 insert_text: Some("abs".into()),
13856 data: Some(json!({ "very": "special"})),
13857 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
13858 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
13859 lsp::InsertReplaceEdit {
13860 new_text: "abs".to_string(),
13861 insert: lsp::Range::default(),
13862 replace: lsp::Range::default(),
13863 },
13864 )),
13865 ..lsp::CompletionItem::default()
13866 };
13867 let items = iter::once(item_0.clone())
13868 .chain((11..51).map(|i| lsp::CompletionItem {
13869 label: format!("item_{}", i),
13870 insert_text: Some(format!("item_{}", i)),
13871 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
13872 ..lsp::CompletionItem::default()
13873 }))
13874 .collect::<Vec<_>>();
13875
13876 let default_commit_characters = vec!["?".to_string()];
13877 let default_data = json!({ "default": "data"});
13878 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
13879 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
13880 let default_edit_range = lsp::Range {
13881 start: lsp::Position {
13882 line: 0,
13883 character: 5,
13884 },
13885 end: lsp::Position {
13886 line: 0,
13887 character: 5,
13888 },
13889 };
13890
13891 let mut cx = EditorLspTestContext::new_rust(
13892 lsp::ServerCapabilities {
13893 completion_provider: Some(lsp::CompletionOptions {
13894 trigger_characters: Some(vec![".".to_string()]),
13895 resolve_provider: Some(true),
13896 ..Default::default()
13897 }),
13898 ..Default::default()
13899 },
13900 cx,
13901 )
13902 .await;
13903
13904 cx.set_state("fn main() { let a = 2ˇ; }");
13905 cx.simulate_keystroke(".");
13906
13907 let completion_data = default_data.clone();
13908 let completion_characters = default_commit_characters.clone();
13909 let completion_items = items.clone();
13910 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13911 let default_data = completion_data.clone();
13912 let default_commit_characters = completion_characters.clone();
13913 let items = completion_items.clone();
13914 async move {
13915 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13916 items,
13917 item_defaults: Some(lsp::CompletionListItemDefaults {
13918 data: Some(default_data.clone()),
13919 commit_characters: Some(default_commit_characters.clone()),
13920 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
13921 default_edit_range,
13922 )),
13923 insert_text_format: Some(default_insert_text_format),
13924 insert_text_mode: Some(default_insert_text_mode),
13925 }),
13926 ..lsp::CompletionList::default()
13927 })))
13928 }
13929 })
13930 .next()
13931 .await;
13932
13933 let resolved_items = Arc::new(Mutex::new(Vec::new()));
13934 cx.lsp
13935 .server
13936 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
13937 let closure_resolved_items = resolved_items.clone();
13938 move |item_to_resolve, _| {
13939 let closure_resolved_items = closure_resolved_items.clone();
13940 async move {
13941 closure_resolved_items.lock().push(item_to_resolve.clone());
13942 Ok(item_to_resolve)
13943 }
13944 }
13945 })
13946 .detach();
13947
13948 cx.condition(|editor, _| editor.context_menu_visible())
13949 .await;
13950 cx.run_until_parked();
13951 cx.update_editor(|editor, _, _| {
13952 let menu = editor.context_menu.borrow_mut();
13953 match menu.as_ref().expect("should have the completions menu") {
13954 CodeContextMenu::Completions(completions_menu) => {
13955 assert_eq!(
13956 completions_menu
13957 .entries
13958 .borrow()
13959 .iter()
13960 .map(|mat| mat.string.clone())
13961 .collect::<Vec<String>>(),
13962 items
13963 .iter()
13964 .map(|completion| completion.label.clone())
13965 .collect::<Vec<String>>()
13966 );
13967 }
13968 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
13969 }
13970 });
13971 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
13972 // with 4 from the end.
13973 assert_eq!(
13974 *resolved_items.lock(),
13975 [&items[0..16], &items[items.len() - 4..items.len()]]
13976 .concat()
13977 .iter()
13978 .cloned()
13979 .map(|mut item| {
13980 if item.data.is_none() {
13981 item.data = Some(default_data.clone());
13982 }
13983 item
13984 })
13985 .collect::<Vec<lsp::CompletionItem>>(),
13986 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
13987 );
13988 resolved_items.lock().clear();
13989
13990 cx.update_editor(|editor, window, cx| {
13991 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
13992 });
13993 cx.run_until_parked();
13994 // Completions that have already been resolved are skipped.
13995 assert_eq!(
13996 *resolved_items.lock(),
13997 items[items.len() - 16..items.len() - 4]
13998 .iter()
13999 .cloned()
14000 .map(|mut item| {
14001 if item.data.is_none() {
14002 item.data = Some(default_data.clone());
14003 }
14004 item
14005 })
14006 .collect::<Vec<lsp::CompletionItem>>()
14007 );
14008 resolved_items.lock().clear();
14009}
14010
14011#[gpui::test]
14012async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
14013 init_test(cx, |_| {});
14014
14015 let mut cx = EditorLspTestContext::new(
14016 Language::new(
14017 LanguageConfig {
14018 matcher: LanguageMatcher {
14019 path_suffixes: vec!["jsx".into()],
14020 ..Default::default()
14021 },
14022 overrides: [(
14023 "element".into(),
14024 LanguageConfigOverride {
14025 completion_query_characters: Override::Set(['-'].into_iter().collect()),
14026 ..Default::default()
14027 },
14028 )]
14029 .into_iter()
14030 .collect(),
14031 ..Default::default()
14032 },
14033 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
14034 )
14035 .with_override_query("(jsx_self_closing_element) @element")
14036 .unwrap(),
14037 lsp::ServerCapabilities {
14038 completion_provider: Some(lsp::CompletionOptions {
14039 trigger_characters: Some(vec![":".to_string()]),
14040 ..Default::default()
14041 }),
14042 ..Default::default()
14043 },
14044 cx,
14045 )
14046 .await;
14047
14048 cx.lsp
14049 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14050 Ok(Some(lsp::CompletionResponse::Array(vec![
14051 lsp::CompletionItem {
14052 label: "bg-blue".into(),
14053 ..Default::default()
14054 },
14055 lsp::CompletionItem {
14056 label: "bg-red".into(),
14057 ..Default::default()
14058 },
14059 lsp::CompletionItem {
14060 label: "bg-yellow".into(),
14061 ..Default::default()
14062 },
14063 ])))
14064 });
14065
14066 cx.set_state(r#"<p class="bgˇ" />"#);
14067
14068 // Trigger completion when typing a dash, because the dash is an extra
14069 // word character in the 'element' scope, which contains the cursor.
14070 cx.simulate_keystroke("-");
14071 cx.executor().run_until_parked();
14072 cx.update_editor(|editor, _, _| {
14073 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14074 {
14075 assert_eq!(
14076 completion_menu_entries(&menu),
14077 &["bg-red", "bg-blue", "bg-yellow"]
14078 );
14079 } else {
14080 panic!("expected completion menu to be open");
14081 }
14082 });
14083
14084 cx.simulate_keystroke("l");
14085 cx.executor().run_until_parked();
14086 cx.update_editor(|editor, _, _| {
14087 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14088 {
14089 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
14090 } else {
14091 panic!("expected completion menu to be open");
14092 }
14093 });
14094
14095 // When filtering completions, consider the character after the '-' to
14096 // be the start of a subword.
14097 cx.set_state(r#"<p class="yelˇ" />"#);
14098 cx.simulate_keystroke("l");
14099 cx.executor().run_until_parked();
14100 cx.update_editor(|editor, _, _| {
14101 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14102 {
14103 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
14104 } else {
14105 panic!("expected completion menu to be open");
14106 }
14107 });
14108}
14109
14110fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
14111 let entries = menu.entries.borrow();
14112 entries.iter().map(|mat| mat.string.clone()).collect()
14113}
14114
14115#[gpui::test]
14116async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
14117 init_test(cx, |settings| {
14118 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
14119 FormatterList(vec![Formatter::Prettier].into()),
14120 ))
14121 });
14122
14123 let fs = FakeFs::new(cx.executor());
14124 fs.insert_file(path!("/file.ts"), Default::default()).await;
14125
14126 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
14127 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14128
14129 language_registry.add(Arc::new(Language::new(
14130 LanguageConfig {
14131 name: "TypeScript".into(),
14132 matcher: LanguageMatcher {
14133 path_suffixes: vec!["ts".to_string()],
14134 ..Default::default()
14135 },
14136 ..Default::default()
14137 },
14138 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14139 )));
14140 update_test_language_settings(cx, |settings| {
14141 settings.defaults.prettier = Some(PrettierSettings {
14142 allowed: true,
14143 ..PrettierSettings::default()
14144 });
14145 });
14146
14147 let test_plugin = "test_plugin";
14148 let _ = language_registry.register_fake_lsp(
14149 "TypeScript",
14150 FakeLspAdapter {
14151 prettier_plugins: vec![test_plugin],
14152 ..Default::default()
14153 },
14154 );
14155
14156 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
14157 let buffer = project
14158 .update(cx, |project, cx| {
14159 project.open_local_buffer(path!("/file.ts"), cx)
14160 })
14161 .await
14162 .unwrap();
14163
14164 let buffer_text = "one\ntwo\nthree\n";
14165 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14166 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14167 editor.update_in(cx, |editor, window, cx| {
14168 editor.set_text(buffer_text, window, cx)
14169 });
14170
14171 editor
14172 .update_in(cx, |editor, window, cx| {
14173 editor.perform_format(
14174 project.clone(),
14175 FormatTrigger::Manual,
14176 FormatTarget::Buffers,
14177 window,
14178 cx,
14179 )
14180 })
14181 .unwrap()
14182 .await;
14183 assert_eq!(
14184 editor.update(cx, |editor, cx| editor.text(cx)),
14185 buffer_text.to_string() + prettier_format_suffix,
14186 "Test prettier formatting was not applied to the original buffer text",
14187 );
14188
14189 update_test_language_settings(cx, |settings| {
14190 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
14191 });
14192 let format = editor.update_in(cx, |editor, window, cx| {
14193 editor.perform_format(
14194 project.clone(),
14195 FormatTrigger::Manual,
14196 FormatTarget::Buffers,
14197 window,
14198 cx,
14199 )
14200 });
14201 format.await.unwrap();
14202 assert_eq!(
14203 editor.update(cx, |editor, cx| editor.text(cx)),
14204 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
14205 "Autoformatting (via test prettier) was not applied to the original buffer text",
14206 );
14207}
14208
14209#[gpui::test]
14210async fn test_addition_reverts(cx: &mut TestAppContext) {
14211 init_test(cx, |_| {});
14212 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14213 let base_text = indoc! {r#"
14214 struct Row;
14215 struct Row1;
14216 struct Row2;
14217
14218 struct Row4;
14219 struct Row5;
14220 struct Row6;
14221
14222 struct Row8;
14223 struct Row9;
14224 struct Row10;"#};
14225
14226 // When addition hunks are not adjacent to carets, no hunk revert is performed
14227 assert_hunk_revert(
14228 indoc! {r#"struct Row;
14229 struct Row1;
14230 struct Row1.1;
14231 struct Row1.2;
14232 struct Row2;ˇ
14233
14234 struct Row4;
14235 struct Row5;
14236 struct Row6;
14237
14238 struct Row8;
14239 ˇstruct Row9;
14240 struct Row9.1;
14241 struct Row9.2;
14242 struct Row9.3;
14243 struct Row10;"#},
14244 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
14245 indoc! {r#"struct Row;
14246 struct Row1;
14247 struct Row1.1;
14248 struct Row1.2;
14249 struct Row2;ˇ
14250
14251 struct Row4;
14252 struct Row5;
14253 struct Row6;
14254
14255 struct Row8;
14256 ˇstruct Row9;
14257 struct Row9.1;
14258 struct Row9.2;
14259 struct Row9.3;
14260 struct Row10;"#},
14261 base_text,
14262 &mut cx,
14263 );
14264 // Same for selections
14265 assert_hunk_revert(
14266 indoc! {r#"struct Row;
14267 struct Row1;
14268 struct Row2;
14269 struct Row2.1;
14270 struct Row2.2;
14271 «ˇ
14272 struct Row4;
14273 struct» Row5;
14274 «struct Row6;
14275 ˇ»
14276 struct Row9.1;
14277 struct Row9.2;
14278 struct Row9.3;
14279 struct Row8;
14280 struct Row9;
14281 struct Row10;"#},
14282 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
14283 indoc! {r#"struct Row;
14284 struct Row1;
14285 struct Row2;
14286 struct Row2.1;
14287 struct Row2.2;
14288 «ˇ
14289 struct Row4;
14290 struct» Row5;
14291 «struct Row6;
14292 ˇ»
14293 struct Row9.1;
14294 struct Row9.2;
14295 struct Row9.3;
14296 struct Row8;
14297 struct Row9;
14298 struct Row10;"#},
14299 base_text,
14300 &mut cx,
14301 );
14302
14303 // When carets and selections intersect the addition hunks, those are reverted.
14304 // Adjacent carets got merged.
14305 assert_hunk_revert(
14306 indoc! {r#"struct Row;
14307 ˇ// something on the top
14308 struct Row1;
14309 struct Row2;
14310 struct Roˇw3.1;
14311 struct Row2.2;
14312 struct Row2.3;ˇ
14313
14314 struct Row4;
14315 struct ˇRow5.1;
14316 struct Row5.2;
14317 struct «Rowˇ»5.3;
14318 struct Row5;
14319 struct Row6;
14320 ˇ
14321 struct Row9.1;
14322 struct «Rowˇ»9.2;
14323 struct «ˇRow»9.3;
14324 struct Row8;
14325 struct Row9;
14326 «ˇ// something on bottom»
14327 struct Row10;"#},
14328 vec![
14329 DiffHunkStatusKind::Added,
14330 DiffHunkStatusKind::Added,
14331 DiffHunkStatusKind::Added,
14332 DiffHunkStatusKind::Added,
14333 DiffHunkStatusKind::Added,
14334 ],
14335 indoc! {r#"struct Row;
14336 ˇstruct Row1;
14337 struct Row2;
14338 ˇ
14339 struct Row4;
14340 ˇstruct Row5;
14341 struct Row6;
14342 ˇ
14343 ˇstruct Row8;
14344 struct Row9;
14345 ˇstruct Row10;"#},
14346 base_text,
14347 &mut cx,
14348 );
14349}
14350
14351#[gpui::test]
14352async fn test_modification_reverts(cx: &mut TestAppContext) {
14353 init_test(cx, |_| {});
14354 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14355 let base_text = indoc! {r#"
14356 struct Row;
14357 struct Row1;
14358 struct Row2;
14359
14360 struct Row4;
14361 struct Row5;
14362 struct Row6;
14363
14364 struct Row8;
14365 struct Row9;
14366 struct Row10;"#};
14367
14368 // Modification hunks behave the same as the addition ones.
14369 assert_hunk_revert(
14370 indoc! {r#"struct Row;
14371 struct Row1;
14372 struct Row33;
14373 ˇ
14374 struct Row4;
14375 struct Row5;
14376 struct Row6;
14377 ˇ
14378 struct Row99;
14379 struct Row9;
14380 struct Row10;"#},
14381 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
14382 indoc! {r#"struct Row;
14383 struct Row1;
14384 struct Row33;
14385 ˇ
14386 struct Row4;
14387 struct Row5;
14388 struct Row6;
14389 ˇ
14390 struct Row99;
14391 struct Row9;
14392 struct Row10;"#},
14393 base_text,
14394 &mut cx,
14395 );
14396 assert_hunk_revert(
14397 indoc! {r#"struct Row;
14398 struct Row1;
14399 struct Row33;
14400 «ˇ
14401 struct Row4;
14402 struct» Row5;
14403 «struct Row6;
14404 ˇ»
14405 struct Row99;
14406 struct Row9;
14407 struct Row10;"#},
14408 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
14409 indoc! {r#"struct Row;
14410 struct Row1;
14411 struct Row33;
14412 «ˇ
14413 struct Row4;
14414 struct» Row5;
14415 «struct Row6;
14416 ˇ»
14417 struct Row99;
14418 struct Row9;
14419 struct Row10;"#},
14420 base_text,
14421 &mut cx,
14422 );
14423
14424 assert_hunk_revert(
14425 indoc! {r#"ˇstruct Row1.1;
14426 struct Row1;
14427 «ˇstr»uct Row22;
14428
14429 struct ˇRow44;
14430 struct Row5;
14431 struct «Rˇ»ow66;ˇ
14432
14433 «struˇ»ct Row88;
14434 struct Row9;
14435 struct Row1011;ˇ"#},
14436 vec![
14437 DiffHunkStatusKind::Modified,
14438 DiffHunkStatusKind::Modified,
14439 DiffHunkStatusKind::Modified,
14440 DiffHunkStatusKind::Modified,
14441 DiffHunkStatusKind::Modified,
14442 DiffHunkStatusKind::Modified,
14443 ],
14444 indoc! {r#"struct Row;
14445 ˇstruct Row1;
14446 struct Row2;
14447 ˇ
14448 struct Row4;
14449 ˇstruct Row5;
14450 struct Row6;
14451 ˇ
14452 struct Row8;
14453 ˇstruct Row9;
14454 struct Row10;ˇ"#},
14455 base_text,
14456 &mut cx,
14457 );
14458}
14459
14460#[gpui::test]
14461async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
14462 init_test(cx, |_| {});
14463 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14464 let base_text = indoc! {r#"
14465 one
14466
14467 two
14468 three
14469 "#};
14470
14471 cx.set_head_text(base_text);
14472 cx.set_state("\nˇ\n");
14473 cx.executor().run_until_parked();
14474 cx.update_editor(|editor, _window, cx| {
14475 editor.expand_selected_diff_hunks(cx);
14476 });
14477 cx.executor().run_until_parked();
14478 cx.update_editor(|editor, window, cx| {
14479 editor.backspace(&Default::default(), window, cx);
14480 });
14481 cx.run_until_parked();
14482 cx.assert_state_with_diff(
14483 indoc! {r#"
14484
14485 - two
14486 - threeˇ
14487 +
14488 "#}
14489 .to_string(),
14490 );
14491}
14492
14493#[gpui::test]
14494async fn test_deletion_reverts(cx: &mut TestAppContext) {
14495 init_test(cx, |_| {});
14496 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14497 let base_text = indoc! {r#"struct Row;
14498struct Row1;
14499struct Row2;
14500
14501struct Row4;
14502struct Row5;
14503struct Row6;
14504
14505struct Row8;
14506struct Row9;
14507struct Row10;"#};
14508
14509 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
14510 assert_hunk_revert(
14511 indoc! {r#"struct Row;
14512 struct Row2;
14513
14514 ˇstruct Row4;
14515 struct Row5;
14516 struct Row6;
14517 ˇ
14518 struct Row8;
14519 struct Row10;"#},
14520 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14521 indoc! {r#"struct Row;
14522 struct Row2;
14523
14524 ˇstruct Row4;
14525 struct Row5;
14526 struct Row6;
14527 ˇ
14528 struct Row8;
14529 struct Row10;"#},
14530 base_text,
14531 &mut cx,
14532 );
14533 assert_hunk_revert(
14534 indoc! {r#"struct Row;
14535 struct Row2;
14536
14537 «ˇstruct Row4;
14538 struct» Row5;
14539 «struct Row6;
14540 ˇ»
14541 struct Row8;
14542 struct Row10;"#},
14543 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14544 indoc! {r#"struct Row;
14545 struct Row2;
14546
14547 «ˇstruct Row4;
14548 struct» Row5;
14549 «struct Row6;
14550 ˇ»
14551 struct Row8;
14552 struct Row10;"#},
14553 base_text,
14554 &mut cx,
14555 );
14556
14557 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
14558 assert_hunk_revert(
14559 indoc! {r#"struct Row;
14560 ˇstruct Row2;
14561
14562 struct Row4;
14563 struct Row5;
14564 struct Row6;
14565
14566 struct Row8;ˇ
14567 struct Row10;"#},
14568 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14569 indoc! {r#"struct Row;
14570 struct Row1;
14571 ˇstruct Row2;
14572
14573 struct Row4;
14574 struct Row5;
14575 struct Row6;
14576
14577 struct Row8;ˇ
14578 struct Row9;
14579 struct Row10;"#},
14580 base_text,
14581 &mut cx,
14582 );
14583 assert_hunk_revert(
14584 indoc! {r#"struct Row;
14585 struct Row2«ˇ;
14586 struct Row4;
14587 struct» Row5;
14588 «struct Row6;
14589
14590 struct Row8;ˇ»
14591 struct Row10;"#},
14592 vec![
14593 DiffHunkStatusKind::Deleted,
14594 DiffHunkStatusKind::Deleted,
14595 DiffHunkStatusKind::Deleted,
14596 ],
14597 indoc! {r#"struct Row;
14598 struct Row1;
14599 struct Row2«ˇ;
14600
14601 struct Row4;
14602 struct» Row5;
14603 «struct Row6;
14604
14605 struct Row8;ˇ»
14606 struct Row9;
14607 struct Row10;"#},
14608 base_text,
14609 &mut cx,
14610 );
14611}
14612
14613#[gpui::test]
14614async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
14615 init_test(cx, |_| {});
14616
14617 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
14618 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
14619 let base_text_3 =
14620 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
14621
14622 let text_1 = edit_first_char_of_every_line(base_text_1);
14623 let text_2 = edit_first_char_of_every_line(base_text_2);
14624 let text_3 = edit_first_char_of_every_line(base_text_3);
14625
14626 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
14627 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
14628 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
14629
14630 let multibuffer = cx.new(|cx| {
14631 let mut multibuffer = MultiBuffer::new(ReadWrite);
14632 multibuffer.push_excerpts(
14633 buffer_1.clone(),
14634 [
14635 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14636 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14637 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14638 ],
14639 cx,
14640 );
14641 multibuffer.push_excerpts(
14642 buffer_2.clone(),
14643 [
14644 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14645 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14646 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14647 ],
14648 cx,
14649 );
14650 multibuffer.push_excerpts(
14651 buffer_3.clone(),
14652 [
14653 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14654 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14655 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14656 ],
14657 cx,
14658 );
14659 multibuffer
14660 });
14661
14662 let fs = FakeFs::new(cx.executor());
14663 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
14664 let (editor, cx) = cx
14665 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
14666 editor.update_in(cx, |editor, _window, cx| {
14667 for (buffer, diff_base) in [
14668 (buffer_1.clone(), base_text_1),
14669 (buffer_2.clone(), base_text_2),
14670 (buffer_3.clone(), base_text_3),
14671 ] {
14672 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
14673 editor
14674 .buffer
14675 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
14676 }
14677 });
14678 cx.executor().run_until_parked();
14679
14680 editor.update_in(cx, |editor, window, cx| {
14681 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}");
14682 editor.select_all(&SelectAll, window, cx);
14683 editor.git_restore(&Default::default(), window, cx);
14684 });
14685 cx.executor().run_until_parked();
14686
14687 // When all ranges are selected, all buffer hunks are reverted.
14688 editor.update(cx, |editor, cx| {
14689 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");
14690 });
14691 buffer_1.update(cx, |buffer, _| {
14692 assert_eq!(buffer.text(), base_text_1);
14693 });
14694 buffer_2.update(cx, |buffer, _| {
14695 assert_eq!(buffer.text(), base_text_2);
14696 });
14697 buffer_3.update(cx, |buffer, _| {
14698 assert_eq!(buffer.text(), base_text_3);
14699 });
14700
14701 editor.update_in(cx, |editor, window, cx| {
14702 editor.undo(&Default::default(), window, cx);
14703 });
14704
14705 editor.update_in(cx, |editor, window, cx| {
14706 editor.change_selections(None, window, cx, |s| {
14707 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
14708 });
14709 editor.git_restore(&Default::default(), window, cx);
14710 });
14711
14712 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
14713 // but not affect buffer_2 and its related excerpts.
14714 editor.update(cx, |editor, cx| {
14715 assert_eq!(
14716 editor.text(cx),
14717 "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}"
14718 );
14719 });
14720 buffer_1.update(cx, |buffer, _| {
14721 assert_eq!(buffer.text(), base_text_1);
14722 });
14723 buffer_2.update(cx, |buffer, _| {
14724 assert_eq!(
14725 buffer.text(),
14726 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
14727 );
14728 });
14729 buffer_3.update(cx, |buffer, _| {
14730 assert_eq!(
14731 buffer.text(),
14732 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
14733 );
14734 });
14735
14736 fn edit_first_char_of_every_line(text: &str) -> String {
14737 text.split('\n')
14738 .map(|line| format!("X{}", &line[1..]))
14739 .collect::<Vec<_>>()
14740 .join("\n")
14741 }
14742}
14743
14744#[gpui::test]
14745async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
14746 init_test(cx, |_| {});
14747
14748 let cols = 4;
14749 let rows = 10;
14750 let sample_text_1 = sample_text(rows, cols, 'a');
14751 assert_eq!(
14752 sample_text_1,
14753 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
14754 );
14755 let sample_text_2 = sample_text(rows, cols, 'l');
14756 assert_eq!(
14757 sample_text_2,
14758 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
14759 );
14760 let sample_text_3 = sample_text(rows, cols, 'v');
14761 assert_eq!(
14762 sample_text_3,
14763 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
14764 );
14765
14766 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
14767 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
14768 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
14769
14770 let multi_buffer = cx.new(|cx| {
14771 let mut multibuffer = MultiBuffer::new(ReadWrite);
14772 multibuffer.push_excerpts(
14773 buffer_1.clone(),
14774 [
14775 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14776 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14777 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14778 ],
14779 cx,
14780 );
14781 multibuffer.push_excerpts(
14782 buffer_2.clone(),
14783 [
14784 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14785 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14786 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14787 ],
14788 cx,
14789 );
14790 multibuffer.push_excerpts(
14791 buffer_3.clone(),
14792 [
14793 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14794 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14795 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14796 ],
14797 cx,
14798 );
14799 multibuffer
14800 });
14801
14802 let fs = FakeFs::new(cx.executor());
14803 fs.insert_tree(
14804 "/a",
14805 json!({
14806 "main.rs": sample_text_1,
14807 "other.rs": sample_text_2,
14808 "lib.rs": sample_text_3,
14809 }),
14810 )
14811 .await;
14812 let project = Project::test(fs, ["/a".as_ref()], cx).await;
14813 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14814 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14815 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
14816 Editor::new(
14817 EditorMode::full(),
14818 multi_buffer,
14819 Some(project.clone()),
14820 window,
14821 cx,
14822 )
14823 });
14824 let multibuffer_item_id = workspace
14825 .update(cx, |workspace, window, cx| {
14826 assert!(
14827 workspace.active_item(cx).is_none(),
14828 "active item should be None before the first item is added"
14829 );
14830 workspace.add_item_to_active_pane(
14831 Box::new(multi_buffer_editor.clone()),
14832 None,
14833 true,
14834 window,
14835 cx,
14836 );
14837 let active_item = workspace
14838 .active_item(cx)
14839 .expect("should have an active item after adding the multi buffer");
14840 assert!(
14841 !active_item.is_singleton(cx),
14842 "A multi buffer was expected to active after adding"
14843 );
14844 active_item.item_id()
14845 })
14846 .unwrap();
14847 cx.executor().run_until_parked();
14848
14849 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14850 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14851 s.select_ranges(Some(1..2))
14852 });
14853 editor.open_excerpts(&OpenExcerpts, window, cx);
14854 });
14855 cx.executor().run_until_parked();
14856 let first_item_id = workspace
14857 .update(cx, |workspace, window, cx| {
14858 let active_item = workspace
14859 .active_item(cx)
14860 .expect("should have an active item after navigating into the 1st buffer");
14861 let first_item_id = active_item.item_id();
14862 assert_ne!(
14863 first_item_id, multibuffer_item_id,
14864 "Should navigate into the 1st buffer and activate it"
14865 );
14866 assert!(
14867 active_item.is_singleton(cx),
14868 "New active item should be a singleton buffer"
14869 );
14870 assert_eq!(
14871 active_item
14872 .act_as::<Editor>(cx)
14873 .expect("should have navigated into an editor for the 1st buffer")
14874 .read(cx)
14875 .text(cx),
14876 sample_text_1
14877 );
14878
14879 workspace
14880 .go_back(workspace.active_pane().downgrade(), window, cx)
14881 .detach_and_log_err(cx);
14882
14883 first_item_id
14884 })
14885 .unwrap();
14886 cx.executor().run_until_parked();
14887 workspace
14888 .update(cx, |workspace, _, cx| {
14889 let active_item = workspace
14890 .active_item(cx)
14891 .expect("should have an active item after navigating back");
14892 assert_eq!(
14893 active_item.item_id(),
14894 multibuffer_item_id,
14895 "Should navigate back to the multi buffer"
14896 );
14897 assert!(!active_item.is_singleton(cx));
14898 })
14899 .unwrap();
14900
14901 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14902 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14903 s.select_ranges(Some(39..40))
14904 });
14905 editor.open_excerpts(&OpenExcerpts, window, cx);
14906 });
14907 cx.executor().run_until_parked();
14908 let second_item_id = workspace
14909 .update(cx, |workspace, window, cx| {
14910 let active_item = workspace
14911 .active_item(cx)
14912 .expect("should have an active item after navigating into the 2nd buffer");
14913 let second_item_id = active_item.item_id();
14914 assert_ne!(
14915 second_item_id, multibuffer_item_id,
14916 "Should navigate away from the multibuffer"
14917 );
14918 assert_ne!(
14919 second_item_id, first_item_id,
14920 "Should navigate into the 2nd buffer and activate it"
14921 );
14922 assert!(
14923 active_item.is_singleton(cx),
14924 "New active item should be a singleton buffer"
14925 );
14926 assert_eq!(
14927 active_item
14928 .act_as::<Editor>(cx)
14929 .expect("should have navigated into an editor")
14930 .read(cx)
14931 .text(cx),
14932 sample_text_2
14933 );
14934
14935 workspace
14936 .go_back(workspace.active_pane().downgrade(), window, cx)
14937 .detach_and_log_err(cx);
14938
14939 second_item_id
14940 })
14941 .unwrap();
14942 cx.executor().run_until_parked();
14943 workspace
14944 .update(cx, |workspace, _, cx| {
14945 let active_item = workspace
14946 .active_item(cx)
14947 .expect("should have an active item after navigating back from the 2nd buffer");
14948 assert_eq!(
14949 active_item.item_id(),
14950 multibuffer_item_id,
14951 "Should navigate back from the 2nd buffer to the multi buffer"
14952 );
14953 assert!(!active_item.is_singleton(cx));
14954 })
14955 .unwrap();
14956
14957 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14958 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14959 s.select_ranges(Some(70..70))
14960 });
14961 editor.open_excerpts(&OpenExcerpts, window, cx);
14962 });
14963 cx.executor().run_until_parked();
14964 workspace
14965 .update(cx, |workspace, window, cx| {
14966 let active_item = workspace
14967 .active_item(cx)
14968 .expect("should have an active item after navigating into the 3rd buffer");
14969 let third_item_id = active_item.item_id();
14970 assert_ne!(
14971 third_item_id, multibuffer_item_id,
14972 "Should navigate into the 3rd buffer and activate it"
14973 );
14974 assert_ne!(third_item_id, first_item_id);
14975 assert_ne!(third_item_id, second_item_id);
14976 assert!(
14977 active_item.is_singleton(cx),
14978 "New active item should be a singleton buffer"
14979 );
14980 assert_eq!(
14981 active_item
14982 .act_as::<Editor>(cx)
14983 .expect("should have navigated into an editor")
14984 .read(cx)
14985 .text(cx),
14986 sample_text_3
14987 );
14988
14989 workspace
14990 .go_back(workspace.active_pane().downgrade(), window, cx)
14991 .detach_and_log_err(cx);
14992 })
14993 .unwrap();
14994 cx.executor().run_until_parked();
14995 workspace
14996 .update(cx, |workspace, _, cx| {
14997 let active_item = workspace
14998 .active_item(cx)
14999 .expect("should have an active item after navigating back from the 3rd buffer");
15000 assert_eq!(
15001 active_item.item_id(),
15002 multibuffer_item_id,
15003 "Should navigate back from the 3rd buffer to the multi buffer"
15004 );
15005 assert!(!active_item.is_singleton(cx));
15006 })
15007 .unwrap();
15008}
15009
15010#[gpui::test]
15011async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15012 init_test(cx, |_| {});
15013
15014 let mut cx = EditorTestContext::new(cx).await;
15015
15016 let diff_base = r#"
15017 use some::mod;
15018
15019 const A: u32 = 42;
15020
15021 fn main() {
15022 println!("hello");
15023
15024 println!("world");
15025 }
15026 "#
15027 .unindent();
15028
15029 cx.set_state(
15030 &r#"
15031 use some::modified;
15032
15033 ˇ
15034 fn main() {
15035 println!("hello there");
15036
15037 println!("around the");
15038 println!("world");
15039 }
15040 "#
15041 .unindent(),
15042 );
15043
15044 cx.set_head_text(&diff_base);
15045 executor.run_until_parked();
15046
15047 cx.update_editor(|editor, window, cx| {
15048 editor.go_to_next_hunk(&GoToHunk, window, cx);
15049 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15050 });
15051 executor.run_until_parked();
15052 cx.assert_state_with_diff(
15053 r#"
15054 use some::modified;
15055
15056
15057 fn main() {
15058 - println!("hello");
15059 + ˇ println!("hello there");
15060
15061 println!("around the");
15062 println!("world");
15063 }
15064 "#
15065 .unindent(),
15066 );
15067
15068 cx.update_editor(|editor, window, cx| {
15069 for _ in 0..2 {
15070 editor.go_to_next_hunk(&GoToHunk, window, cx);
15071 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15072 }
15073 });
15074 executor.run_until_parked();
15075 cx.assert_state_with_diff(
15076 r#"
15077 - use some::mod;
15078 + ˇuse some::modified;
15079
15080
15081 fn main() {
15082 - println!("hello");
15083 + println!("hello there");
15084
15085 + println!("around the");
15086 println!("world");
15087 }
15088 "#
15089 .unindent(),
15090 );
15091
15092 cx.update_editor(|editor, window, cx| {
15093 editor.go_to_next_hunk(&GoToHunk, window, cx);
15094 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15095 });
15096 executor.run_until_parked();
15097 cx.assert_state_with_diff(
15098 r#"
15099 - use some::mod;
15100 + use some::modified;
15101
15102 - const A: u32 = 42;
15103 ˇ
15104 fn main() {
15105 - println!("hello");
15106 + println!("hello there");
15107
15108 + println!("around the");
15109 println!("world");
15110 }
15111 "#
15112 .unindent(),
15113 );
15114
15115 cx.update_editor(|editor, window, cx| {
15116 editor.cancel(&Cancel, window, cx);
15117 });
15118
15119 cx.assert_state_with_diff(
15120 r#"
15121 use some::modified;
15122
15123 ˇ
15124 fn main() {
15125 println!("hello there");
15126
15127 println!("around the");
15128 println!("world");
15129 }
15130 "#
15131 .unindent(),
15132 );
15133}
15134
15135#[gpui::test]
15136async fn test_diff_base_change_with_expanded_diff_hunks(
15137 executor: BackgroundExecutor,
15138 cx: &mut TestAppContext,
15139) {
15140 init_test(cx, |_| {});
15141
15142 let mut cx = EditorTestContext::new(cx).await;
15143
15144 let diff_base = r#"
15145 use some::mod1;
15146 use some::mod2;
15147
15148 const A: u32 = 42;
15149 const B: u32 = 42;
15150 const C: u32 = 42;
15151
15152 fn main() {
15153 println!("hello");
15154
15155 println!("world");
15156 }
15157 "#
15158 .unindent();
15159
15160 cx.set_state(
15161 &r#"
15162 use some::mod2;
15163
15164 const A: u32 = 42;
15165 const C: u32 = 42;
15166
15167 fn main(ˇ) {
15168 //println!("hello");
15169
15170 println!("world");
15171 //
15172 //
15173 }
15174 "#
15175 .unindent(),
15176 );
15177
15178 cx.set_head_text(&diff_base);
15179 executor.run_until_parked();
15180
15181 cx.update_editor(|editor, window, cx| {
15182 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15183 });
15184 executor.run_until_parked();
15185 cx.assert_state_with_diff(
15186 r#"
15187 - use some::mod1;
15188 use some::mod2;
15189
15190 const A: u32 = 42;
15191 - const B: u32 = 42;
15192 const C: u32 = 42;
15193
15194 fn main(ˇ) {
15195 - println!("hello");
15196 + //println!("hello");
15197
15198 println!("world");
15199 + //
15200 + //
15201 }
15202 "#
15203 .unindent(),
15204 );
15205
15206 cx.set_head_text("new diff base!");
15207 executor.run_until_parked();
15208 cx.assert_state_with_diff(
15209 r#"
15210 - new diff base!
15211 + use some::mod2;
15212 +
15213 + const A: u32 = 42;
15214 + const C: u32 = 42;
15215 +
15216 + fn main(ˇ) {
15217 + //println!("hello");
15218 +
15219 + println!("world");
15220 + //
15221 + //
15222 + }
15223 "#
15224 .unindent(),
15225 );
15226}
15227
15228#[gpui::test]
15229async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
15230 init_test(cx, |_| {});
15231
15232 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
15233 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
15234 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
15235 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
15236 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
15237 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
15238
15239 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
15240 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
15241 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
15242
15243 let multi_buffer = cx.new(|cx| {
15244 let mut multibuffer = MultiBuffer::new(ReadWrite);
15245 multibuffer.push_excerpts(
15246 buffer_1.clone(),
15247 [
15248 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15249 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15250 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15251 ],
15252 cx,
15253 );
15254 multibuffer.push_excerpts(
15255 buffer_2.clone(),
15256 [
15257 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15258 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15259 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15260 ],
15261 cx,
15262 );
15263 multibuffer.push_excerpts(
15264 buffer_3.clone(),
15265 [
15266 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15267 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15268 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15269 ],
15270 cx,
15271 );
15272 multibuffer
15273 });
15274
15275 let editor =
15276 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
15277 editor
15278 .update(cx, |editor, _window, cx| {
15279 for (buffer, diff_base) in [
15280 (buffer_1.clone(), file_1_old),
15281 (buffer_2.clone(), file_2_old),
15282 (buffer_3.clone(), file_3_old),
15283 ] {
15284 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
15285 editor
15286 .buffer
15287 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
15288 }
15289 })
15290 .unwrap();
15291
15292 let mut cx = EditorTestContext::for_editor(editor, cx).await;
15293 cx.run_until_parked();
15294
15295 cx.assert_editor_state(
15296 &"
15297 ˇaaa
15298 ccc
15299 ddd
15300
15301 ggg
15302 hhh
15303
15304
15305 lll
15306 mmm
15307 NNN
15308
15309 qqq
15310 rrr
15311
15312 uuu
15313 111
15314 222
15315 333
15316
15317 666
15318 777
15319
15320 000
15321 !!!"
15322 .unindent(),
15323 );
15324
15325 cx.update_editor(|editor, window, cx| {
15326 editor.select_all(&SelectAll, window, cx);
15327 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15328 });
15329 cx.executor().run_until_parked();
15330
15331 cx.assert_state_with_diff(
15332 "
15333 «aaa
15334 - bbb
15335 ccc
15336 ddd
15337
15338 ggg
15339 hhh
15340
15341
15342 lll
15343 mmm
15344 - nnn
15345 + NNN
15346
15347 qqq
15348 rrr
15349
15350 uuu
15351 111
15352 222
15353 333
15354
15355 + 666
15356 777
15357
15358 000
15359 !!!ˇ»"
15360 .unindent(),
15361 );
15362}
15363
15364#[gpui::test]
15365async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
15366 init_test(cx, |_| {});
15367
15368 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
15369 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
15370
15371 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
15372 let multi_buffer = cx.new(|cx| {
15373 let mut multibuffer = MultiBuffer::new(ReadWrite);
15374 multibuffer.push_excerpts(
15375 buffer.clone(),
15376 [
15377 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
15378 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
15379 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
15380 ],
15381 cx,
15382 );
15383 multibuffer
15384 });
15385
15386 let editor =
15387 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
15388 editor
15389 .update(cx, |editor, _window, cx| {
15390 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
15391 editor
15392 .buffer
15393 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
15394 })
15395 .unwrap();
15396
15397 let mut cx = EditorTestContext::for_editor(editor, cx).await;
15398 cx.run_until_parked();
15399
15400 cx.update_editor(|editor, window, cx| {
15401 editor.expand_all_diff_hunks(&Default::default(), window, cx)
15402 });
15403 cx.executor().run_until_parked();
15404
15405 // When the start of a hunk coincides with the start of its excerpt,
15406 // the hunk is expanded. When the start of a a hunk is earlier than
15407 // the start of its excerpt, the hunk is not expanded.
15408 cx.assert_state_with_diff(
15409 "
15410 ˇaaa
15411 - bbb
15412 + BBB
15413
15414 - ddd
15415 - eee
15416 + DDD
15417 + EEE
15418 fff
15419
15420 iii
15421 "
15422 .unindent(),
15423 );
15424}
15425
15426#[gpui::test]
15427async fn test_edits_around_expanded_insertion_hunks(
15428 executor: BackgroundExecutor,
15429 cx: &mut TestAppContext,
15430) {
15431 init_test(cx, |_| {});
15432
15433 let mut cx = EditorTestContext::new(cx).await;
15434
15435 let diff_base = r#"
15436 use some::mod1;
15437 use some::mod2;
15438
15439 const A: u32 = 42;
15440
15441 fn main() {
15442 println!("hello");
15443
15444 println!("world");
15445 }
15446 "#
15447 .unindent();
15448 executor.run_until_parked();
15449 cx.set_state(
15450 &r#"
15451 use some::mod1;
15452 use some::mod2;
15453
15454 const A: u32 = 42;
15455 const B: u32 = 42;
15456 const C: u32 = 42;
15457 ˇ
15458
15459 fn main() {
15460 println!("hello");
15461
15462 println!("world");
15463 }
15464 "#
15465 .unindent(),
15466 );
15467
15468 cx.set_head_text(&diff_base);
15469 executor.run_until_parked();
15470
15471 cx.update_editor(|editor, window, cx| {
15472 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15473 });
15474 executor.run_until_parked();
15475
15476 cx.assert_state_with_diff(
15477 r#"
15478 use some::mod1;
15479 use some::mod2;
15480
15481 const A: u32 = 42;
15482 + const B: u32 = 42;
15483 + const C: u32 = 42;
15484 + ˇ
15485
15486 fn main() {
15487 println!("hello");
15488
15489 println!("world");
15490 }
15491 "#
15492 .unindent(),
15493 );
15494
15495 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
15496 executor.run_until_parked();
15497
15498 cx.assert_state_with_diff(
15499 r#"
15500 use some::mod1;
15501 use some::mod2;
15502
15503 const A: u32 = 42;
15504 + const B: u32 = 42;
15505 + const C: u32 = 42;
15506 + const D: u32 = 42;
15507 + ˇ
15508
15509 fn main() {
15510 println!("hello");
15511
15512 println!("world");
15513 }
15514 "#
15515 .unindent(),
15516 );
15517
15518 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
15519 executor.run_until_parked();
15520
15521 cx.assert_state_with_diff(
15522 r#"
15523 use some::mod1;
15524 use some::mod2;
15525
15526 const A: u32 = 42;
15527 + const B: u32 = 42;
15528 + const C: u32 = 42;
15529 + const D: u32 = 42;
15530 + const E: u32 = 42;
15531 + ˇ
15532
15533 fn main() {
15534 println!("hello");
15535
15536 println!("world");
15537 }
15538 "#
15539 .unindent(),
15540 );
15541
15542 cx.update_editor(|editor, window, cx| {
15543 editor.delete_line(&DeleteLine, window, cx);
15544 });
15545 executor.run_until_parked();
15546
15547 cx.assert_state_with_diff(
15548 r#"
15549 use some::mod1;
15550 use some::mod2;
15551
15552 const A: u32 = 42;
15553 + const B: u32 = 42;
15554 + const C: u32 = 42;
15555 + const D: u32 = 42;
15556 + const E: u32 = 42;
15557 ˇ
15558 fn main() {
15559 println!("hello");
15560
15561 println!("world");
15562 }
15563 "#
15564 .unindent(),
15565 );
15566
15567 cx.update_editor(|editor, window, cx| {
15568 editor.move_up(&MoveUp, window, cx);
15569 editor.delete_line(&DeleteLine, window, cx);
15570 editor.move_up(&MoveUp, window, cx);
15571 editor.delete_line(&DeleteLine, window, cx);
15572 editor.move_up(&MoveUp, window, cx);
15573 editor.delete_line(&DeleteLine, window, cx);
15574 });
15575 executor.run_until_parked();
15576 cx.assert_state_with_diff(
15577 r#"
15578 use some::mod1;
15579 use some::mod2;
15580
15581 const A: u32 = 42;
15582 + const B: u32 = 42;
15583 ˇ
15584 fn main() {
15585 println!("hello");
15586
15587 println!("world");
15588 }
15589 "#
15590 .unindent(),
15591 );
15592
15593 cx.update_editor(|editor, window, cx| {
15594 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
15595 editor.delete_line(&DeleteLine, window, cx);
15596 });
15597 executor.run_until_parked();
15598 cx.assert_state_with_diff(
15599 r#"
15600 ˇ
15601 fn main() {
15602 println!("hello");
15603
15604 println!("world");
15605 }
15606 "#
15607 .unindent(),
15608 );
15609}
15610
15611#[gpui::test]
15612async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
15613 init_test(cx, |_| {});
15614
15615 let mut cx = EditorTestContext::new(cx).await;
15616 cx.set_head_text(indoc! { "
15617 one
15618 two
15619 three
15620 four
15621 five
15622 "
15623 });
15624 cx.set_state(indoc! { "
15625 one
15626 ˇthree
15627 five
15628 "});
15629 cx.run_until_parked();
15630 cx.update_editor(|editor, window, cx| {
15631 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15632 });
15633 cx.assert_state_with_diff(
15634 indoc! { "
15635 one
15636 - two
15637 ˇthree
15638 - four
15639 five
15640 "}
15641 .to_string(),
15642 );
15643 cx.update_editor(|editor, window, cx| {
15644 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15645 });
15646
15647 cx.assert_state_with_diff(
15648 indoc! { "
15649 one
15650 ˇthree
15651 five
15652 "}
15653 .to_string(),
15654 );
15655
15656 cx.set_state(indoc! { "
15657 one
15658 ˇTWO
15659 three
15660 four
15661 five
15662 "});
15663 cx.run_until_parked();
15664 cx.update_editor(|editor, window, cx| {
15665 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15666 });
15667
15668 cx.assert_state_with_diff(
15669 indoc! { "
15670 one
15671 - two
15672 + ˇTWO
15673 three
15674 four
15675 five
15676 "}
15677 .to_string(),
15678 );
15679 cx.update_editor(|editor, window, cx| {
15680 editor.move_up(&Default::default(), window, cx);
15681 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15682 });
15683 cx.assert_state_with_diff(
15684 indoc! { "
15685 one
15686 ˇTWO
15687 three
15688 four
15689 five
15690 "}
15691 .to_string(),
15692 );
15693}
15694
15695#[gpui::test]
15696async fn test_edits_around_expanded_deletion_hunks(
15697 executor: BackgroundExecutor,
15698 cx: &mut TestAppContext,
15699) {
15700 init_test(cx, |_| {});
15701
15702 let mut cx = EditorTestContext::new(cx).await;
15703
15704 let diff_base = r#"
15705 use some::mod1;
15706 use some::mod2;
15707
15708 const A: u32 = 42;
15709 const B: u32 = 42;
15710 const C: u32 = 42;
15711
15712
15713 fn main() {
15714 println!("hello");
15715
15716 println!("world");
15717 }
15718 "#
15719 .unindent();
15720 executor.run_until_parked();
15721 cx.set_state(
15722 &r#"
15723 use some::mod1;
15724 use some::mod2;
15725
15726 ˇconst B: u32 = 42;
15727 const C: u32 = 42;
15728
15729
15730 fn main() {
15731 println!("hello");
15732
15733 println!("world");
15734 }
15735 "#
15736 .unindent(),
15737 );
15738
15739 cx.set_head_text(&diff_base);
15740 executor.run_until_parked();
15741
15742 cx.update_editor(|editor, window, cx| {
15743 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15744 });
15745 executor.run_until_parked();
15746
15747 cx.assert_state_with_diff(
15748 r#"
15749 use some::mod1;
15750 use some::mod2;
15751
15752 - const A: u32 = 42;
15753 ˇconst B: u32 = 42;
15754 const C: u32 = 42;
15755
15756
15757 fn main() {
15758 println!("hello");
15759
15760 println!("world");
15761 }
15762 "#
15763 .unindent(),
15764 );
15765
15766 cx.update_editor(|editor, window, cx| {
15767 editor.delete_line(&DeleteLine, window, cx);
15768 });
15769 executor.run_until_parked();
15770 cx.assert_state_with_diff(
15771 r#"
15772 use some::mod1;
15773 use some::mod2;
15774
15775 - const A: u32 = 42;
15776 - const B: u32 = 42;
15777 ˇconst C: u32 = 42;
15778
15779
15780 fn main() {
15781 println!("hello");
15782
15783 println!("world");
15784 }
15785 "#
15786 .unindent(),
15787 );
15788
15789 cx.update_editor(|editor, window, cx| {
15790 editor.delete_line(&DeleteLine, window, cx);
15791 });
15792 executor.run_until_parked();
15793 cx.assert_state_with_diff(
15794 r#"
15795 use some::mod1;
15796 use some::mod2;
15797
15798 - const A: u32 = 42;
15799 - const B: u32 = 42;
15800 - const C: u32 = 42;
15801 ˇ
15802
15803 fn main() {
15804 println!("hello");
15805
15806 println!("world");
15807 }
15808 "#
15809 .unindent(),
15810 );
15811
15812 cx.update_editor(|editor, window, cx| {
15813 editor.handle_input("replacement", window, cx);
15814 });
15815 executor.run_until_parked();
15816 cx.assert_state_with_diff(
15817 r#"
15818 use some::mod1;
15819 use some::mod2;
15820
15821 - const A: u32 = 42;
15822 - const B: u32 = 42;
15823 - const C: u32 = 42;
15824 -
15825 + replacementˇ
15826
15827 fn main() {
15828 println!("hello");
15829
15830 println!("world");
15831 }
15832 "#
15833 .unindent(),
15834 );
15835}
15836
15837#[gpui::test]
15838async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15839 init_test(cx, |_| {});
15840
15841 let mut cx = EditorTestContext::new(cx).await;
15842
15843 let base_text = r#"
15844 one
15845 two
15846 three
15847 four
15848 five
15849 "#
15850 .unindent();
15851 executor.run_until_parked();
15852 cx.set_state(
15853 &r#"
15854 one
15855 two
15856 fˇour
15857 five
15858 "#
15859 .unindent(),
15860 );
15861
15862 cx.set_head_text(&base_text);
15863 executor.run_until_parked();
15864
15865 cx.update_editor(|editor, window, cx| {
15866 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15867 });
15868 executor.run_until_parked();
15869
15870 cx.assert_state_with_diff(
15871 r#"
15872 one
15873 two
15874 - three
15875 fˇour
15876 five
15877 "#
15878 .unindent(),
15879 );
15880
15881 cx.update_editor(|editor, window, cx| {
15882 editor.backspace(&Backspace, window, cx);
15883 editor.backspace(&Backspace, window, cx);
15884 });
15885 executor.run_until_parked();
15886 cx.assert_state_with_diff(
15887 r#"
15888 one
15889 two
15890 - threeˇ
15891 - four
15892 + our
15893 five
15894 "#
15895 .unindent(),
15896 );
15897}
15898
15899#[gpui::test]
15900async fn test_edit_after_expanded_modification_hunk(
15901 executor: BackgroundExecutor,
15902 cx: &mut TestAppContext,
15903) {
15904 init_test(cx, |_| {});
15905
15906 let mut cx = EditorTestContext::new(cx).await;
15907
15908 let diff_base = r#"
15909 use some::mod1;
15910 use some::mod2;
15911
15912 const A: u32 = 42;
15913 const B: u32 = 42;
15914 const C: u32 = 42;
15915 const D: u32 = 42;
15916
15917
15918 fn main() {
15919 println!("hello");
15920
15921 println!("world");
15922 }"#
15923 .unindent();
15924
15925 cx.set_state(
15926 &r#"
15927 use some::mod1;
15928 use some::mod2;
15929
15930 const A: u32 = 42;
15931 const B: u32 = 42;
15932 const C: u32 = 43ˇ
15933 const D: u32 = 42;
15934
15935
15936 fn main() {
15937 println!("hello");
15938
15939 println!("world");
15940 }"#
15941 .unindent(),
15942 );
15943
15944 cx.set_head_text(&diff_base);
15945 executor.run_until_parked();
15946 cx.update_editor(|editor, window, cx| {
15947 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15948 });
15949 executor.run_until_parked();
15950
15951 cx.assert_state_with_diff(
15952 r#"
15953 use some::mod1;
15954 use some::mod2;
15955
15956 const A: u32 = 42;
15957 const B: u32 = 42;
15958 - const C: u32 = 42;
15959 + const C: u32 = 43ˇ
15960 const D: u32 = 42;
15961
15962
15963 fn main() {
15964 println!("hello");
15965
15966 println!("world");
15967 }"#
15968 .unindent(),
15969 );
15970
15971 cx.update_editor(|editor, window, cx| {
15972 editor.handle_input("\nnew_line\n", window, cx);
15973 });
15974 executor.run_until_parked();
15975
15976 cx.assert_state_with_diff(
15977 r#"
15978 use some::mod1;
15979 use some::mod2;
15980
15981 const A: u32 = 42;
15982 const B: u32 = 42;
15983 - const C: u32 = 42;
15984 + const C: u32 = 43
15985 + new_line
15986 + ˇ
15987 const D: u32 = 42;
15988
15989
15990 fn main() {
15991 println!("hello");
15992
15993 println!("world");
15994 }"#
15995 .unindent(),
15996 );
15997}
15998
15999#[gpui::test]
16000async fn test_stage_and_unstage_added_file_hunk(
16001 executor: BackgroundExecutor,
16002 cx: &mut TestAppContext,
16003) {
16004 init_test(cx, |_| {});
16005
16006 let mut cx = EditorTestContext::new(cx).await;
16007 cx.update_editor(|editor, _, cx| {
16008 editor.set_expand_all_diff_hunks(cx);
16009 });
16010
16011 let working_copy = r#"
16012 ˇfn main() {
16013 println!("hello, world!");
16014 }
16015 "#
16016 .unindent();
16017
16018 cx.set_state(&working_copy);
16019 executor.run_until_parked();
16020
16021 cx.assert_state_with_diff(
16022 r#"
16023 + ˇfn main() {
16024 + println!("hello, world!");
16025 + }
16026 "#
16027 .unindent(),
16028 );
16029 cx.assert_index_text(None);
16030
16031 cx.update_editor(|editor, window, cx| {
16032 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16033 });
16034 executor.run_until_parked();
16035 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
16036 cx.assert_state_with_diff(
16037 r#"
16038 + ˇfn main() {
16039 + println!("hello, world!");
16040 + }
16041 "#
16042 .unindent(),
16043 );
16044
16045 cx.update_editor(|editor, window, cx| {
16046 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16047 });
16048 executor.run_until_parked();
16049 cx.assert_index_text(None);
16050}
16051
16052async fn setup_indent_guides_editor(
16053 text: &str,
16054 cx: &mut TestAppContext,
16055) -> (BufferId, EditorTestContext) {
16056 init_test(cx, |_| {});
16057
16058 let mut cx = EditorTestContext::new(cx).await;
16059
16060 let buffer_id = cx.update_editor(|editor, window, cx| {
16061 editor.set_text(text, window, cx);
16062 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
16063
16064 buffer_ids[0]
16065 });
16066
16067 (buffer_id, cx)
16068}
16069
16070fn assert_indent_guides(
16071 range: Range<u32>,
16072 expected: Vec<IndentGuide>,
16073 active_indices: Option<Vec<usize>>,
16074 cx: &mut EditorTestContext,
16075) {
16076 let indent_guides = cx.update_editor(|editor, window, cx| {
16077 let snapshot = editor.snapshot(window, cx).display_snapshot;
16078 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
16079 editor,
16080 MultiBufferRow(range.start)..MultiBufferRow(range.end),
16081 true,
16082 &snapshot,
16083 cx,
16084 );
16085
16086 indent_guides.sort_by(|a, b| {
16087 a.depth.cmp(&b.depth).then(
16088 a.start_row
16089 .cmp(&b.start_row)
16090 .then(a.end_row.cmp(&b.end_row)),
16091 )
16092 });
16093 indent_guides
16094 });
16095
16096 if let Some(expected) = active_indices {
16097 let active_indices = cx.update_editor(|editor, window, cx| {
16098 let snapshot = editor.snapshot(window, cx).display_snapshot;
16099 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
16100 });
16101
16102 assert_eq!(
16103 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
16104 expected,
16105 "Active indent guide indices do not match"
16106 );
16107 }
16108
16109 assert_eq!(indent_guides, expected, "Indent guides do not match");
16110}
16111
16112fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
16113 IndentGuide {
16114 buffer_id,
16115 start_row: MultiBufferRow(start_row),
16116 end_row: MultiBufferRow(end_row),
16117 depth,
16118 tab_size: 4,
16119 settings: IndentGuideSettings {
16120 enabled: true,
16121 line_width: 1,
16122 active_line_width: 1,
16123 ..Default::default()
16124 },
16125 }
16126}
16127
16128#[gpui::test]
16129async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
16130 let (buffer_id, mut cx) = setup_indent_guides_editor(
16131 &"
16132 fn main() {
16133 let a = 1;
16134 }"
16135 .unindent(),
16136 cx,
16137 )
16138 .await;
16139
16140 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
16141}
16142
16143#[gpui::test]
16144async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
16145 let (buffer_id, mut cx) = setup_indent_guides_editor(
16146 &"
16147 fn main() {
16148 let a = 1;
16149 let b = 2;
16150 }"
16151 .unindent(),
16152 cx,
16153 )
16154 .await;
16155
16156 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
16157}
16158
16159#[gpui::test]
16160async fn test_indent_guide_nested(cx: &mut TestAppContext) {
16161 let (buffer_id, mut cx) = setup_indent_guides_editor(
16162 &"
16163 fn main() {
16164 let a = 1;
16165 if a == 3 {
16166 let b = 2;
16167 } else {
16168 let c = 3;
16169 }
16170 }"
16171 .unindent(),
16172 cx,
16173 )
16174 .await;
16175
16176 assert_indent_guides(
16177 0..8,
16178 vec![
16179 indent_guide(buffer_id, 1, 6, 0),
16180 indent_guide(buffer_id, 3, 3, 1),
16181 indent_guide(buffer_id, 5, 5, 1),
16182 ],
16183 None,
16184 &mut cx,
16185 );
16186}
16187
16188#[gpui::test]
16189async fn test_indent_guide_tab(cx: &mut TestAppContext) {
16190 let (buffer_id, mut cx) = setup_indent_guides_editor(
16191 &"
16192 fn main() {
16193 let a = 1;
16194 let b = 2;
16195 let c = 3;
16196 }"
16197 .unindent(),
16198 cx,
16199 )
16200 .await;
16201
16202 assert_indent_guides(
16203 0..5,
16204 vec![
16205 indent_guide(buffer_id, 1, 3, 0),
16206 indent_guide(buffer_id, 2, 2, 1),
16207 ],
16208 None,
16209 &mut cx,
16210 );
16211}
16212
16213#[gpui::test]
16214async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
16215 let (buffer_id, mut cx) = setup_indent_guides_editor(
16216 &"
16217 fn main() {
16218 let a = 1;
16219
16220 let c = 3;
16221 }"
16222 .unindent(),
16223 cx,
16224 )
16225 .await;
16226
16227 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
16228}
16229
16230#[gpui::test]
16231async fn test_indent_guide_complex(cx: &mut TestAppContext) {
16232 let (buffer_id, mut cx) = setup_indent_guides_editor(
16233 &"
16234 fn main() {
16235 let a = 1;
16236
16237 let c = 3;
16238
16239 if a == 3 {
16240 let b = 2;
16241 } else {
16242 let c = 3;
16243 }
16244 }"
16245 .unindent(),
16246 cx,
16247 )
16248 .await;
16249
16250 assert_indent_guides(
16251 0..11,
16252 vec![
16253 indent_guide(buffer_id, 1, 9, 0),
16254 indent_guide(buffer_id, 6, 6, 1),
16255 indent_guide(buffer_id, 8, 8, 1),
16256 ],
16257 None,
16258 &mut cx,
16259 );
16260}
16261
16262#[gpui::test]
16263async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
16264 let (buffer_id, mut cx) = setup_indent_guides_editor(
16265 &"
16266 fn main() {
16267 let a = 1;
16268
16269 let c = 3;
16270
16271 if a == 3 {
16272 let b = 2;
16273 } else {
16274 let c = 3;
16275 }
16276 }"
16277 .unindent(),
16278 cx,
16279 )
16280 .await;
16281
16282 assert_indent_guides(
16283 1..11,
16284 vec![
16285 indent_guide(buffer_id, 1, 9, 0),
16286 indent_guide(buffer_id, 6, 6, 1),
16287 indent_guide(buffer_id, 8, 8, 1),
16288 ],
16289 None,
16290 &mut cx,
16291 );
16292}
16293
16294#[gpui::test]
16295async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
16296 let (buffer_id, mut cx) = setup_indent_guides_editor(
16297 &"
16298 fn main() {
16299 let a = 1;
16300
16301 let c = 3;
16302
16303 if a == 3 {
16304 let b = 2;
16305 } else {
16306 let c = 3;
16307 }
16308 }"
16309 .unindent(),
16310 cx,
16311 )
16312 .await;
16313
16314 assert_indent_guides(
16315 1..10,
16316 vec![
16317 indent_guide(buffer_id, 1, 9, 0),
16318 indent_guide(buffer_id, 6, 6, 1),
16319 indent_guide(buffer_id, 8, 8, 1),
16320 ],
16321 None,
16322 &mut cx,
16323 );
16324}
16325
16326#[gpui::test]
16327async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
16328 let (buffer_id, mut cx) = setup_indent_guides_editor(
16329 &"
16330 block1
16331 block2
16332 block3
16333 block4
16334 block2
16335 block1
16336 block1"
16337 .unindent(),
16338 cx,
16339 )
16340 .await;
16341
16342 assert_indent_guides(
16343 1..10,
16344 vec![
16345 indent_guide(buffer_id, 1, 4, 0),
16346 indent_guide(buffer_id, 2, 3, 1),
16347 indent_guide(buffer_id, 3, 3, 2),
16348 ],
16349 None,
16350 &mut cx,
16351 );
16352}
16353
16354#[gpui::test]
16355async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
16356 let (buffer_id, mut cx) = setup_indent_guides_editor(
16357 &"
16358 block1
16359 block2
16360 block3
16361
16362 block1
16363 block1"
16364 .unindent(),
16365 cx,
16366 )
16367 .await;
16368
16369 assert_indent_guides(
16370 0..6,
16371 vec![
16372 indent_guide(buffer_id, 1, 2, 0),
16373 indent_guide(buffer_id, 2, 2, 1),
16374 ],
16375 None,
16376 &mut cx,
16377 );
16378}
16379
16380#[gpui::test]
16381async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
16382 let (buffer_id, mut cx) = setup_indent_guides_editor(
16383 &"
16384 block1
16385
16386
16387
16388 block2
16389 "
16390 .unindent(),
16391 cx,
16392 )
16393 .await;
16394
16395 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
16396}
16397
16398#[gpui::test]
16399async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
16400 let (buffer_id, mut cx) = setup_indent_guides_editor(
16401 &"
16402 def a:
16403 \tb = 3
16404 \tif True:
16405 \t\tc = 4
16406 \t\td = 5
16407 \tprint(b)
16408 "
16409 .unindent(),
16410 cx,
16411 )
16412 .await;
16413
16414 assert_indent_guides(
16415 0..6,
16416 vec![
16417 indent_guide(buffer_id, 1, 6, 0),
16418 indent_guide(buffer_id, 3, 4, 1),
16419 ],
16420 None,
16421 &mut cx,
16422 );
16423}
16424
16425#[gpui::test]
16426async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
16427 let (buffer_id, mut cx) = setup_indent_guides_editor(
16428 &"
16429 fn main() {
16430 let a = 1;
16431 }"
16432 .unindent(),
16433 cx,
16434 )
16435 .await;
16436
16437 cx.update_editor(|editor, window, cx| {
16438 editor.change_selections(None, window, cx, |s| {
16439 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16440 });
16441 });
16442
16443 assert_indent_guides(
16444 0..3,
16445 vec![indent_guide(buffer_id, 1, 1, 0)],
16446 Some(vec![0]),
16447 &mut cx,
16448 );
16449}
16450
16451#[gpui::test]
16452async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
16453 let (buffer_id, mut cx) = setup_indent_guides_editor(
16454 &"
16455 fn main() {
16456 if 1 == 2 {
16457 let a = 1;
16458 }
16459 }"
16460 .unindent(),
16461 cx,
16462 )
16463 .await;
16464
16465 cx.update_editor(|editor, window, cx| {
16466 editor.change_selections(None, window, cx, |s| {
16467 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16468 });
16469 });
16470
16471 assert_indent_guides(
16472 0..4,
16473 vec![
16474 indent_guide(buffer_id, 1, 3, 0),
16475 indent_guide(buffer_id, 2, 2, 1),
16476 ],
16477 Some(vec![1]),
16478 &mut cx,
16479 );
16480
16481 cx.update_editor(|editor, window, cx| {
16482 editor.change_selections(None, window, cx, |s| {
16483 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
16484 });
16485 });
16486
16487 assert_indent_guides(
16488 0..4,
16489 vec![
16490 indent_guide(buffer_id, 1, 3, 0),
16491 indent_guide(buffer_id, 2, 2, 1),
16492 ],
16493 Some(vec![1]),
16494 &mut cx,
16495 );
16496
16497 cx.update_editor(|editor, window, cx| {
16498 editor.change_selections(None, window, cx, |s| {
16499 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
16500 });
16501 });
16502
16503 assert_indent_guides(
16504 0..4,
16505 vec![
16506 indent_guide(buffer_id, 1, 3, 0),
16507 indent_guide(buffer_id, 2, 2, 1),
16508 ],
16509 Some(vec![0]),
16510 &mut cx,
16511 );
16512}
16513
16514#[gpui::test]
16515async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
16516 let (buffer_id, mut cx) = setup_indent_guides_editor(
16517 &"
16518 fn main() {
16519 let a = 1;
16520
16521 let b = 2;
16522 }"
16523 .unindent(),
16524 cx,
16525 )
16526 .await;
16527
16528 cx.update_editor(|editor, window, cx| {
16529 editor.change_selections(None, window, cx, |s| {
16530 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
16531 });
16532 });
16533
16534 assert_indent_guides(
16535 0..5,
16536 vec![indent_guide(buffer_id, 1, 3, 0)],
16537 Some(vec![0]),
16538 &mut cx,
16539 );
16540}
16541
16542#[gpui::test]
16543async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
16544 let (buffer_id, mut cx) = setup_indent_guides_editor(
16545 &"
16546 def m:
16547 a = 1
16548 pass"
16549 .unindent(),
16550 cx,
16551 )
16552 .await;
16553
16554 cx.update_editor(|editor, window, cx| {
16555 editor.change_selections(None, window, cx, |s| {
16556 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16557 });
16558 });
16559
16560 assert_indent_guides(
16561 0..3,
16562 vec![indent_guide(buffer_id, 1, 2, 0)],
16563 Some(vec![0]),
16564 &mut cx,
16565 );
16566}
16567
16568#[gpui::test]
16569async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
16570 init_test(cx, |_| {});
16571 let mut cx = EditorTestContext::new(cx).await;
16572 let text = indoc! {
16573 "
16574 impl A {
16575 fn b() {
16576 0;
16577 3;
16578 5;
16579 6;
16580 7;
16581 }
16582 }
16583 "
16584 };
16585 let base_text = indoc! {
16586 "
16587 impl A {
16588 fn b() {
16589 0;
16590 1;
16591 2;
16592 3;
16593 4;
16594 }
16595 fn c() {
16596 5;
16597 6;
16598 7;
16599 }
16600 }
16601 "
16602 };
16603
16604 cx.update_editor(|editor, window, cx| {
16605 editor.set_text(text, window, cx);
16606
16607 editor.buffer().update(cx, |multibuffer, cx| {
16608 let buffer = multibuffer.as_singleton().unwrap();
16609 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
16610
16611 multibuffer.set_all_diff_hunks_expanded(cx);
16612 multibuffer.add_diff(diff, cx);
16613
16614 buffer.read(cx).remote_id()
16615 })
16616 });
16617 cx.run_until_parked();
16618
16619 cx.assert_state_with_diff(
16620 indoc! { "
16621 impl A {
16622 fn b() {
16623 0;
16624 - 1;
16625 - 2;
16626 3;
16627 - 4;
16628 - }
16629 - fn c() {
16630 5;
16631 6;
16632 7;
16633 }
16634 }
16635 ˇ"
16636 }
16637 .to_string(),
16638 );
16639
16640 let mut actual_guides = cx.update_editor(|editor, window, cx| {
16641 editor
16642 .snapshot(window, cx)
16643 .buffer_snapshot
16644 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
16645 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
16646 .collect::<Vec<_>>()
16647 });
16648 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
16649 assert_eq!(
16650 actual_guides,
16651 vec![
16652 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
16653 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
16654 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
16655 ]
16656 );
16657}
16658
16659#[gpui::test]
16660async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16661 init_test(cx, |_| {});
16662 let mut cx = EditorTestContext::new(cx).await;
16663
16664 let diff_base = r#"
16665 a
16666 b
16667 c
16668 "#
16669 .unindent();
16670
16671 cx.set_state(
16672 &r#"
16673 ˇA
16674 b
16675 C
16676 "#
16677 .unindent(),
16678 );
16679 cx.set_head_text(&diff_base);
16680 cx.update_editor(|editor, window, cx| {
16681 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16682 });
16683 executor.run_until_parked();
16684
16685 let both_hunks_expanded = r#"
16686 - a
16687 + ˇA
16688 b
16689 - c
16690 + C
16691 "#
16692 .unindent();
16693
16694 cx.assert_state_with_diff(both_hunks_expanded.clone());
16695
16696 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16697 let snapshot = editor.snapshot(window, cx);
16698 let hunks = editor
16699 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16700 .collect::<Vec<_>>();
16701 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16702 let buffer_id = hunks[0].buffer_id;
16703 hunks
16704 .into_iter()
16705 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16706 .collect::<Vec<_>>()
16707 });
16708 assert_eq!(hunk_ranges.len(), 2);
16709
16710 cx.update_editor(|editor, _, cx| {
16711 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16712 });
16713 executor.run_until_parked();
16714
16715 let second_hunk_expanded = r#"
16716 ˇA
16717 b
16718 - c
16719 + C
16720 "#
16721 .unindent();
16722
16723 cx.assert_state_with_diff(second_hunk_expanded);
16724
16725 cx.update_editor(|editor, _, cx| {
16726 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16727 });
16728 executor.run_until_parked();
16729
16730 cx.assert_state_with_diff(both_hunks_expanded.clone());
16731
16732 cx.update_editor(|editor, _, cx| {
16733 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
16734 });
16735 executor.run_until_parked();
16736
16737 let first_hunk_expanded = r#"
16738 - a
16739 + ˇA
16740 b
16741 C
16742 "#
16743 .unindent();
16744
16745 cx.assert_state_with_diff(first_hunk_expanded);
16746
16747 cx.update_editor(|editor, _, cx| {
16748 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
16749 });
16750 executor.run_until_parked();
16751
16752 cx.assert_state_with_diff(both_hunks_expanded);
16753
16754 cx.set_state(
16755 &r#"
16756 ˇA
16757 b
16758 "#
16759 .unindent(),
16760 );
16761 cx.run_until_parked();
16762
16763 // TODO this cursor position seems bad
16764 cx.assert_state_with_diff(
16765 r#"
16766 - ˇa
16767 + A
16768 b
16769 "#
16770 .unindent(),
16771 );
16772
16773 cx.update_editor(|editor, window, cx| {
16774 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16775 });
16776
16777 cx.assert_state_with_diff(
16778 r#"
16779 - ˇa
16780 + A
16781 b
16782 - c
16783 "#
16784 .unindent(),
16785 );
16786
16787 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16788 let snapshot = editor.snapshot(window, cx);
16789 let hunks = editor
16790 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16791 .collect::<Vec<_>>();
16792 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16793 let buffer_id = hunks[0].buffer_id;
16794 hunks
16795 .into_iter()
16796 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16797 .collect::<Vec<_>>()
16798 });
16799 assert_eq!(hunk_ranges.len(), 2);
16800
16801 cx.update_editor(|editor, _, cx| {
16802 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
16803 });
16804 executor.run_until_parked();
16805
16806 cx.assert_state_with_diff(
16807 r#"
16808 - ˇa
16809 + A
16810 b
16811 "#
16812 .unindent(),
16813 );
16814}
16815
16816#[gpui::test]
16817async fn test_toggle_deletion_hunk_at_start_of_file(
16818 executor: BackgroundExecutor,
16819 cx: &mut TestAppContext,
16820) {
16821 init_test(cx, |_| {});
16822 let mut cx = EditorTestContext::new(cx).await;
16823
16824 let diff_base = r#"
16825 a
16826 b
16827 c
16828 "#
16829 .unindent();
16830
16831 cx.set_state(
16832 &r#"
16833 ˇb
16834 c
16835 "#
16836 .unindent(),
16837 );
16838 cx.set_head_text(&diff_base);
16839 cx.update_editor(|editor, window, cx| {
16840 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16841 });
16842 executor.run_until_parked();
16843
16844 let hunk_expanded = r#"
16845 - a
16846 ˇb
16847 c
16848 "#
16849 .unindent();
16850
16851 cx.assert_state_with_diff(hunk_expanded.clone());
16852
16853 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16854 let snapshot = editor.snapshot(window, cx);
16855 let hunks = editor
16856 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16857 .collect::<Vec<_>>();
16858 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16859 let buffer_id = hunks[0].buffer_id;
16860 hunks
16861 .into_iter()
16862 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16863 .collect::<Vec<_>>()
16864 });
16865 assert_eq!(hunk_ranges.len(), 1);
16866
16867 cx.update_editor(|editor, _, cx| {
16868 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16869 });
16870 executor.run_until_parked();
16871
16872 let hunk_collapsed = r#"
16873 ˇb
16874 c
16875 "#
16876 .unindent();
16877
16878 cx.assert_state_with_diff(hunk_collapsed);
16879
16880 cx.update_editor(|editor, _, cx| {
16881 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16882 });
16883 executor.run_until_parked();
16884
16885 cx.assert_state_with_diff(hunk_expanded.clone());
16886}
16887
16888#[gpui::test]
16889async fn test_display_diff_hunks(cx: &mut TestAppContext) {
16890 init_test(cx, |_| {});
16891
16892 let fs = FakeFs::new(cx.executor());
16893 fs.insert_tree(
16894 path!("/test"),
16895 json!({
16896 ".git": {},
16897 "file-1": "ONE\n",
16898 "file-2": "TWO\n",
16899 "file-3": "THREE\n",
16900 }),
16901 )
16902 .await;
16903
16904 fs.set_head_for_repo(
16905 path!("/test/.git").as_ref(),
16906 &[
16907 ("file-1".into(), "one\n".into()),
16908 ("file-2".into(), "two\n".into()),
16909 ("file-3".into(), "three\n".into()),
16910 ],
16911 );
16912
16913 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
16914 let mut buffers = vec![];
16915 for i in 1..=3 {
16916 let buffer = project
16917 .update(cx, |project, cx| {
16918 let path = format!(path!("/test/file-{}"), i);
16919 project.open_local_buffer(path, cx)
16920 })
16921 .await
16922 .unwrap();
16923 buffers.push(buffer);
16924 }
16925
16926 let multibuffer = cx.new(|cx| {
16927 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
16928 multibuffer.set_all_diff_hunks_expanded(cx);
16929 for buffer in &buffers {
16930 let snapshot = buffer.read(cx).snapshot();
16931 multibuffer.set_excerpts_for_path(
16932 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
16933 buffer.clone(),
16934 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
16935 DEFAULT_MULTIBUFFER_CONTEXT,
16936 cx,
16937 );
16938 }
16939 multibuffer
16940 });
16941
16942 let editor = cx.add_window(|window, cx| {
16943 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
16944 });
16945 cx.run_until_parked();
16946
16947 let snapshot = editor
16948 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16949 .unwrap();
16950 let hunks = snapshot
16951 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
16952 .map(|hunk| match hunk {
16953 DisplayDiffHunk::Unfolded {
16954 display_row_range, ..
16955 } => display_row_range,
16956 DisplayDiffHunk::Folded { .. } => unreachable!(),
16957 })
16958 .collect::<Vec<_>>();
16959 assert_eq!(
16960 hunks,
16961 [
16962 DisplayRow(2)..DisplayRow(4),
16963 DisplayRow(7)..DisplayRow(9),
16964 DisplayRow(12)..DisplayRow(14),
16965 ]
16966 );
16967}
16968
16969#[gpui::test]
16970async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
16971 init_test(cx, |_| {});
16972
16973 let mut cx = EditorTestContext::new(cx).await;
16974 cx.set_head_text(indoc! { "
16975 one
16976 two
16977 three
16978 four
16979 five
16980 "
16981 });
16982 cx.set_index_text(indoc! { "
16983 one
16984 two
16985 three
16986 four
16987 five
16988 "
16989 });
16990 cx.set_state(indoc! {"
16991 one
16992 TWO
16993 ˇTHREE
16994 FOUR
16995 five
16996 "});
16997 cx.run_until_parked();
16998 cx.update_editor(|editor, window, cx| {
16999 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17000 });
17001 cx.run_until_parked();
17002 cx.assert_index_text(Some(indoc! {"
17003 one
17004 TWO
17005 THREE
17006 FOUR
17007 five
17008 "}));
17009 cx.set_state(indoc! { "
17010 one
17011 TWO
17012 ˇTHREE-HUNDRED
17013 FOUR
17014 five
17015 "});
17016 cx.run_until_parked();
17017 cx.update_editor(|editor, window, cx| {
17018 let snapshot = editor.snapshot(window, cx);
17019 let hunks = editor
17020 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17021 .collect::<Vec<_>>();
17022 assert_eq!(hunks.len(), 1);
17023 assert_eq!(
17024 hunks[0].status(),
17025 DiffHunkStatus {
17026 kind: DiffHunkStatusKind::Modified,
17027 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
17028 }
17029 );
17030
17031 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17032 });
17033 cx.run_until_parked();
17034 cx.assert_index_text(Some(indoc! {"
17035 one
17036 TWO
17037 THREE-HUNDRED
17038 FOUR
17039 five
17040 "}));
17041}
17042
17043#[gpui::test]
17044fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
17045 init_test(cx, |_| {});
17046
17047 let editor = cx.add_window(|window, cx| {
17048 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
17049 build_editor(buffer, window, cx)
17050 });
17051
17052 let render_args = Arc::new(Mutex::new(None));
17053 let snapshot = editor
17054 .update(cx, |editor, window, cx| {
17055 let snapshot = editor.buffer().read(cx).snapshot(cx);
17056 let range =
17057 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
17058
17059 struct RenderArgs {
17060 row: MultiBufferRow,
17061 folded: bool,
17062 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
17063 }
17064
17065 let crease = Crease::inline(
17066 range,
17067 FoldPlaceholder::test(),
17068 {
17069 let toggle_callback = render_args.clone();
17070 move |row, folded, callback, _window, _cx| {
17071 *toggle_callback.lock() = Some(RenderArgs {
17072 row,
17073 folded,
17074 callback,
17075 });
17076 div()
17077 }
17078 },
17079 |_row, _folded, _window, _cx| div(),
17080 );
17081
17082 editor.insert_creases(Some(crease), cx);
17083 let snapshot = editor.snapshot(window, cx);
17084 let _div = snapshot.render_crease_toggle(
17085 MultiBufferRow(1),
17086 false,
17087 cx.entity().clone(),
17088 window,
17089 cx,
17090 );
17091 snapshot
17092 })
17093 .unwrap();
17094
17095 let render_args = render_args.lock().take().unwrap();
17096 assert_eq!(render_args.row, MultiBufferRow(1));
17097 assert!(!render_args.folded);
17098 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
17099
17100 cx.update_window(*editor, |_, window, cx| {
17101 (render_args.callback)(true, window, cx)
17102 })
17103 .unwrap();
17104 let snapshot = editor
17105 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17106 .unwrap();
17107 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
17108
17109 cx.update_window(*editor, |_, window, cx| {
17110 (render_args.callback)(false, window, cx)
17111 })
17112 .unwrap();
17113 let snapshot = editor
17114 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17115 .unwrap();
17116 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
17117}
17118
17119#[gpui::test]
17120async fn test_input_text(cx: &mut TestAppContext) {
17121 init_test(cx, |_| {});
17122 let mut cx = EditorTestContext::new(cx).await;
17123
17124 cx.set_state(
17125 &r#"ˇone
17126 two
17127
17128 three
17129 fourˇ
17130 five
17131
17132 siˇx"#
17133 .unindent(),
17134 );
17135
17136 cx.dispatch_action(HandleInput(String::new()));
17137 cx.assert_editor_state(
17138 &r#"ˇone
17139 two
17140
17141 three
17142 fourˇ
17143 five
17144
17145 siˇx"#
17146 .unindent(),
17147 );
17148
17149 cx.dispatch_action(HandleInput("AAAA".to_string()));
17150 cx.assert_editor_state(
17151 &r#"AAAAˇone
17152 two
17153
17154 three
17155 fourAAAAˇ
17156 five
17157
17158 siAAAAˇx"#
17159 .unindent(),
17160 );
17161}
17162
17163#[gpui::test]
17164async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
17165 init_test(cx, |_| {});
17166
17167 let mut cx = EditorTestContext::new(cx).await;
17168 cx.set_state(
17169 r#"let foo = 1;
17170let foo = 2;
17171let foo = 3;
17172let fooˇ = 4;
17173let foo = 5;
17174let foo = 6;
17175let foo = 7;
17176let foo = 8;
17177let foo = 9;
17178let foo = 10;
17179let foo = 11;
17180let foo = 12;
17181let foo = 13;
17182let foo = 14;
17183let foo = 15;"#,
17184 );
17185
17186 cx.update_editor(|e, window, cx| {
17187 assert_eq!(
17188 e.next_scroll_position,
17189 NextScrollCursorCenterTopBottom::Center,
17190 "Default next scroll direction is center",
17191 );
17192
17193 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17194 assert_eq!(
17195 e.next_scroll_position,
17196 NextScrollCursorCenterTopBottom::Top,
17197 "After center, next scroll direction should be top",
17198 );
17199
17200 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17201 assert_eq!(
17202 e.next_scroll_position,
17203 NextScrollCursorCenterTopBottom::Bottom,
17204 "After top, next scroll direction should be bottom",
17205 );
17206
17207 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17208 assert_eq!(
17209 e.next_scroll_position,
17210 NextScrollCursorCenterTopBottom::Center,
17211 "After bottom, scrolling should start over",
17212 );
17213
17214 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17215 assert_eq!(
17216 e.next_scroll_position,
17217 NextScrollCursorCenterTopBottom::Top,
17218 "Scrolling continues if retriggered fast enough"
17219 );
17220 });
17221
17222 cx.executor()
17223 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
17224 cx.executor().run_until_parked();
17225 cx.update_editor(|e, _, _| {
17226 assert_eq!(
17227 e.next_scroll_position,
17228 NextScrollCursorCenterTopBottom::Center,
17229 "If scrolling is not triggered fast enough, it should reset"
17230 );
17231 });
17232}
17233
17234#[gpui::test]
17235async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
17236 init_test(cx, |_| {});
17237 let mut cx = EditorLspTestContext::new_rust(
17238 lsp::ServerCapabilities {
17239 definition_provider: Some(lsp::OneOf::Left(true)),
17240 references_provider: Some(lsp::OneOf::Left(true)),
17241 ..lsp::ServerCapabilities::default()
17242 },
17243 cx,
17244 )
17245 .await;
17246
17247 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
17248 let go_to_definition = cx
17249 .lsp
17250 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
17251 move |params, _| async move {
17252 if empty_go_to_definition {
17253 Ok(None)
17254 } else {
17255 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
17256 uri: params.text_document_position_params.text_document.uri,
17257 range: lsp::Range::new(
17258 lsp::Position::new(4, 3),
17259 lsp::Position::new(4, 6),
17260 ),
17261 })))
17262 }
17263 },
17264 );
17265 let references = cx
17266 .lsp
17267 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
17268 Ok(Some(vec![lsp::Location {
17269 uri: params.text_document_position.text_document.uri,
17270 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
17271 }]))
17272 });
17273 (go_to_definition, references)
17274 };
17275
17276 cx.set_state(
17277 &r#"fn one() {
17278 let mut a = ˇtwo();
17279 }
17280
17281 fn two() {}"#
17282 .unindent(),
17283 );
17284 set_up_lsp_handlers(false, &mut cx);
17285 let navigated = cx
17286 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17287 .await
17288 .expect("Failed to navigate to definition");
17289 assert_eq!(
17290 navigated,
17291 Navigated::Yes,
17292 "Should have navigated to definition from the GetDefinition response"
17293 );
17294 cx.assert_editor_state(
17295 &r#"fn one() {
17296 let mut a = two();
17297 }
17298
17299 fn «twoˇ»() {}"#
17300 .unindent(),
17301 );
17302
17303 let editors = cx.update_workspace(|workspace, _, cx| {
17304 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17305 });
17306 cx.update_editor(|_, _, test_editor_cx| {
17307 assert_eq!(
17308 editors.len(),
17309 1,
17310 "Initially, only one, test, editor should be open in the workspace"
17311 );
17312 assert_eq!(
17313 test_editor_cx.entity(),
17314 editors.last().expect("Asserted len is 1").clone()
17315 );
17316 });
17317
17318 set_up_lsp_handlers(true, &mut cx);
17319 let navigated = cx
17320 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17321 .await
17322 .expect("Failed to navigate to lookup references");
17323 assert_eq!(
17324 navigated,
17325 Navigated::Yes,
17326 "Should have navigated to references as a fallback after empty GoToDefinition response"
17327 );
17328 // We should not change the selections in the existing file,
17329 // if opening another milti buffer with the references
17330 cx.assert_editor_state(
17331 &r#"fn one() {
17332 let mut a = two();
17333 }
17334
17335 fn «twoˇ»() {}"#
17336 .unindent(),
17337 );
17338 let editors = cx.update_workspace(|workspace, _, cx| {
17339 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17340 });
17341 cx.update_editor(|_, _, test_editor_cx| {
17342 assert_eq!(
17343 editors.len(),
17344 2,
17345 "After falling back to references search, we open a new editor with the results"
17346 );
17347 let references_fallback_text = editors
17348 .into_iter()
17349 .find(|new_editor| *new_editor != test_editor_cx.entity())
17350 .expect("Should have one non-test editor now")
17351 .read(test_editor_cx)
17352 .text(test_editor_cx);
17353 assert_eq!(
17354 references_fallback_text, "fn one() {\n let mut a = two();\n}",
17355 "Should use the range from the references response and not the GoToDefinition one"
17356 );
17357 });
17358}
17359
17360#[gpui::test]
17361async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
17362 init_test(cx, |_| {});
17363 cx.update(|cx| {
17364 let mut editor_settings = EditorSettings::get_global(cx).clone();
17365 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
17366 EditorSettings::override_global(editor_settings, cx);
17367 });
17368 let mut cx = EditorLspTestContext::new_rust(
17369 lsp::ServerCapabilities {
17370 definition_provider: Some(lsp::OneOf::Left(true)),
17371 references_provider: Some(lsp::OneOf::Left(true)),
17372 ..lsp::ServerCapabilities::default()
17373 },
17374 cx,
17375 )
17376 .await;
17377 let original_state = r#"fn one() {
17378 let mut a = ˇtwo();
17379 }
17380
17381 fn two() {}"#
17382 .unindent();
17383 cx.set_state(&original_state);
17384
17385 let mut go_to_definition = cx
17386 .lsp
17387 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
17388 move |_, _| async move { Ok(None) },
17389 );
17390 let _references = cx
17391 .lsp
17392 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
17393 panic!("Should not call for references with no go to definition fallback")
17394 });
17395
17396 let navigated = cx
17397 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17398 .await
17399 .expect("Failed to navigate to lookup references");
17400 go_to_definition
17401 .next()
17402 .await
17403 .expect("Should have called the go_to_definition handler");
17404
17405 assert_eq!(
17406 navigated,
17407 Navigated::No,
17408 "Should have navigated to references as a fallback after empty GoToDefinition response"
17409 );
17410 cx.assert_editor_state(&original_state);
17411 let editors = cx.update_workspace(|workspace, _, cx| {
17412 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17413 });
17414 cx.update_editor(|_, _, _| {
17415 assert_eq!(
17416 editors.len(),
17417 1,
17418 "After unsuccessful fallback, no other editor should have been opened"
17419 );
17420 });
17421}
17422
17423#[gpui::test]
17424async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
17425 init_test(cx, |_| {});
17426
17427 let language = Arc::new(Language::new(
17428 LanguageConfig::default(),
17429 Some(tree_sitter_rust::LANGUAGE.into()),
17430 ));
17431
17432 let text = r#"
17433 #[cfg(test)]
17434 mod tests() {
17435 #[test]
17436 fn runnable_1() {
17437 let a = 1;
17438 }
17439
17440 #[test]
17441 fn runnable_2() {
17442 let a = 1;
17443 let b = 2;
17444 }
17445 }
17446 "#
17447 .unindent();
17448
17449 let fs = FakeFs::new(cx.executor());
17450 fs.insert_file("/file.rs", Default::default()).await;
17451
17452 let project = Project::test(fs, ["/a".as_ref()], cx).await;
17453 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17454 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17455 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
17456 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
17457
17458 let editor = cx.new_window_entity(|window, cx| {
17459 Editor::new(
17460 EditorMode::full(),
17461 multi_buffer,
17462 Some(project.clone()),
17463 window,
17464 cx,
17465 )
17466 });
17467
17468 editor.update_in(cx, |editor, window, cx| {
17469 let snapshot = editor.buffer().read(cx).snapshot(cx);
17470 editor.tasks.insert(
17471 (buffer.read(cx).remote_id(), 3),
17472 RunnableTasks {
17473 templates: vec![],
17474 offset: snapshot.anchor_before(43),
17475 column: 0,
17476 extra_variables: HashMap::default(),
17477 context_range: BufferOffset(43)..BufferOffset(85),
17478 },
17479 );
17480 editor.tasks.insert(
17481 (buffer.read(cx).remote_id(), 8),
17482 RunnableTasks {
17483 templates: vec![],
17484 offset: snapshot.anchor_before(86),
17485 column: 0,
17486 extra_variables: HashMap::default(),
17487 context_range: BufferOffset(86)..BufferOffset(191),
17488 },
17489 );
17490
17491 // Test finding task when cursor is inside function body
17492 editor.change_selections(None, window, cx, |s| {
17493 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
17494 });
17495 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
17496 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
17497
17498 // Test finding task when cursor is on function name
17499 editor.change_selections(None, window, cx, |s| {
17500 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
17501 });
17502 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
17503 assert_eq!(row, 8, "Should find task when cursor is on function name");
17504 });
17505}
17506
17507#[gpui::test]
17508async fn test_folding_buffers(cx: &mut TestAppContext) {
17509 init_test(cx, |_| {});
17510
17511 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
17512 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
17513 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
17514
17515 let fs = FakeFs::new(cx.executor());
17516 fs.insert_tree(
17517 path!("/a"),
17518 json!({
17519 "first.rs": sample_text_1,
17520 "second.rs": sample_text_2,
17521 "third.rs": sample_text_3,
17522 }),
17523 )
17524 .await;
17525 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17526 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17527 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17528 let worktree = project.update(cx, |project, cx| {
17529 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17530 assert_eq!(worktrees.len(), 1);
17531 worktrees.pop().unwrap()
17532 });
17533 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17534
17535 let buffer_1 = project
17536 .update(cx, |project, cx| {
17537 project.open_buffer((worktree_id, "first.rs"), cx)
17538 })
17539 .await
17540 .unwrap();
17541 let buffer_2 = project
17542 .update(cx, |project, cx| {
17543 project.open_buffer((worktree_id, "second.rs"), cx)
17544 })
17545 .await
17546 .unwrap();
17547 let buffer_3 = project
17548 .update(cx, |project, cx| {
17549 project.open_buffer((worktree_id, "third.rs"), cx)
17550 })
17551 .await
17552 .unwrap();
17553
17554 let multi_buffer = cx.new(|cx| {
17555 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17556 multi_buffer.push_excerpts(
17557 buffer_1.clone(),
17558 [
17559 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17560 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17561 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17562 ],
17563 cx,
17564 );
17565 multi_buffer.push_excerpts(
17566 buffer_2.clone(),
17567 [
17568 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17569 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17570 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17571 ],
17572 cx,
17573 );
17574 multi_buffer.push_excerpts(
17575 buffer_3.clone(),
17576 [
17577 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17578 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17579 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17580 ],
17581 cx,
17582 );
17583 multi_buffer
17584 });
17585 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17586 Editor::new(
17587 EditorMode::full(),
17588 multi_buffer.clone(),
17589 Some(project.clone()),
17590 window,
17591 cx,
17592 )
17593 });
17594
17595 assert_eq!(
17596 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17597 "\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",
17598 );
17599
17600 multi_buffer_editor.update(cx, |editor, cx| {
17601 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
17602 });
17603 assert_eq!(
17604 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17605 "\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",
17606 "After folding the first buffer, its text should not be displayed"
17607 );
17608
17609 multi_buffer_editor.update(cx, |editor, cx| {
17610 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
17611 });
17612 assert_eq!(
17613 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17614 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
17615 "After folding the second buffer, its text should not be displayed"
17616 );
17617
17618 multi_buffer_editor.update(cx, |editor, cx| {
17619 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
17620 });
17621 assert_eq!(
17622 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17623 "\n\n\n\n\n",
17624 "After folding the third buffer, its text should not be displayed"
17625 );
17626
17627 // Emulate selection inside the fold logic, that should work
17628 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17629 editor
17630 .snapshot(window, cx)
17631 .next_line_boundary(Point::new(0, 4));
17632 });
17633
17634 multi_buffer_editor.update(cx, |editor, cx| {
17635 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
17636 });
17637 assert_eq!(
17638 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17639 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
17640 "After unfolding the second buffer, its text should be displayed"
17641 );
17642
17643 // Typing inside of buffer 1 causes that buffer to be unfolded.
17644 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17645 assert_eq!(
17646 multi_buffer
17647 .read(cx)
17648 .snapshot(cx)
17649 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
17650 .collect::<String>(),
17651 "bbbb"
17652 );
17653 editor.change_selections(None, window, cx, |selections| {
17654 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
17655 });
17656 editor.handle_input("B", window, cx);
17657 });
17658
17659 assert_eq!(
17660 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17661 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
17662 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
17663 );
17664
17665 multi_buffer_editor.update(cx, |editor, cx| {
17666 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
17667 });
17668 assert_eq!(
17669 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17670 "\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",
17671 "After unfolding the all buffers, all original text should be displayed"
17672 );
17673}
17674
17675#[gpui::test]
17676async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
17677 init_test(cx, |_| {});
17678
17679 let sample_text_1 = "1111\n2222\n3333".to_string();
17680 let sample_text_2 = "4444\n5555\n6666".to_string();
17681 let sample_text_3 = "7777\n8888\n9999".to_string();
17682
17683 let fs = FakeFs::new(cx.executor());
17684 fs.insert_tree(
17685 path!("/a"),
17686 json!({
17687 "first.rs": sample_text_1,
17688 "second.rs": sample_text_2,
17689 "third.rs": sample_text_3,
17690 }),
17691 )
17692 .await;
17693 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17694 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17695 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17696 let worktree = project.update(cx, |project, cx| {
17697 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17698 assert_eq!(worktrees.len(), 1);
17699 worktrees.pop().unwrap()
17700 });
17701 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17702
17703 let buffer_1 = project
17704 .update(cx, |project, cx| {
17705 project.open_buffer((worktree_id, "first.rs"), cx)
17706 })
17707 .await
17708 .unwrap();
17709 let buffer_2 = project
17710 .update(cx, |project, cx| {
17711 project.open_buffer((worktree_id, "second.rs"), cx)
17712 })
17713 .await
17714 .unwrap();
17715 let buffer_3 = project
17716 .update(cx, |project, cx| {
17717 project.open_buffer((worktree_id, "third.rs"), cx)
17718 })
17719 .await
17720 .unwrap();
17721
17722 let multi_buffer = cx.new(|cx| {
17723 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17724 multi_buffer.push_excerpts(
17725 buffer_1.clone(),
17726 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
17727 cx,
17728 );
17729 multi_buffer.push_excerpts(
17730 buffer_2.clone(),
17731 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
17732 cx,
17733 );
17734 multi_buffer.push_excerpts(
17735 buffer_3.clone(),
17736 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
17737 cx,
17738 );
17739 multi_buffer
17740 });
17741
17742 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17743 Editor::new(
17744 EditorMode::full(),
17745 multi_buffer,
17746 Some(project.clone()),
17747 window,
17748 cx,
17749 )
17750 });
17751
17752 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
17753 assert_eq!(
17754 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17755 full_text,
17756 );
17757
17758 multi_buffer_editor.update(cx, |editor, cx| {
17759 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
17760 });
17761 assert_eq!(
17762 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17763 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
17764 "After folding the first buffer, its text should not be displayed"
17765 );
17766
17767 multi_buffer_editor.update(cx, |editor, cx| {
17768 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
17769 });
17770
17771 assert_eq!(
17772 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17773 "\n\n\n\n\n\n7777\n8888\n9999",
17774 "After folding the second buffer, its text should not be displayed"
17775 );
17776
17777 multi_buffer_editor.update(cx, |editor, cx| {
17778 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
17779 });
17780 assert_eq!(
17781 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17782 "\n\n\n\n\n",
17783 "After folding the third buffer, its text should not be displayed"
17784 );
17785
17786 multi_buffer_editor.update(cx, |editor, cx| {
17787 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
17788 });
17789 assert_eq!(
17790 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17791 "\n\n\n\n4444\n5555\n6666\n\n",
17792 "After unfolding the second buffer, its text should be displayed"
17793 );
17794
17795 multi_buffer_editor.update(cx, |editor, cx| {
17796 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
17797 });
17798 assert_eq!(
17799 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17800 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
17801 "After unfolding the first buffer, its text should be displayed"
17802 );
17803
17804 multi_buffer_editor.update(cx, |editor, cx| {
17805 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
17806 });
17807 assert_eq!(
17808 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17809 full_text,
17810 "After unfolding all buffers, all original text should be displayed"
17811 );
17812}
17813
17814#[gpui::test]
17815async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
17816 init_test(cx, |_| {});
17817
17818 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
17819
17820 let fs = FakeFs::new(cx.executor());
17821 fs.insert_tree(
17822 path!("/a"),
17823 json!({
17824 "main.rs": sample_text,
17825 }),
17826 )
17827 .await;
17828 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17829 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17830 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17831 let worktree = project.update(cx, |project, cx| {
17832 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17833 assert_eq!(worktrees.len(), 1);
17834 worktrees.pop().unwrap()
17835 });
17836 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17837
17838 let buffer_1 = project
17839 .update(cx, |project, cx| {
17840 project.open_buffer((worktree_id, "main.rs"), cx)
17841 })
17842 .await
17843 .unwrap();
17844
17845 let multi_buffer = cx.new(|cx| {
17846 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17847 multi_buffer.push_excerpts(
17848 buffer_1.clone(),
17849 [ExcerptRange::new(
17850 Point::new(0, 0)
17851 ..Point::new(
17852 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
17853 0,
17854 ),
17855 )],
17856 cx,
17857 );
17858 multi_buffer
17859 });
17860 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17861 Editor::new(
17862 EditorMode::full(),
17863 multi_buffer,
17864 Some(project.clone()),
17865 window,
17866 cx,
17867 )
17868 });
17869
17870 let selection_range = Point::new(1, 0)..Point::new(2, 0);
17871 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17872 enum TestHighlight {}
17873 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
17874 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
17875 editor.highlight_text::<TestHighlight>(
17876 vec![highlight_range.clone()],
17877 HighlightStyle::color(Hsla::green()),
17878 cx,
17879 );
17880 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
17881 });
17882
17883 let full_text = format!("\n\n{sample_text}");
17884 assert_eq!(
17885 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17886 full_text,
17887 );
17888}
17889
17890#[gpui::test]
17891async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
17892 init_test(cx, |_| {});
17893 cx.update(|cx| {
17894 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
17895 "keymaps/default-linux.json",
17896 cx,
17897 )
17898 .unwrap();
17899 cx.bind_keys(default_key_bindings);
17900 });
17901
17902 let (editor, cx) = cx.add_window_view(|window, cx| {
17903 let multi_buffer = MultiBuffer::build_multi(
17904 [
17905 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
17906 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
17907 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
17908 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
17909 ],
17910 cx,
17911 );
17912 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
17913
17914 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
17915 // fold all but the second buffer, so that we test navigating between two
17916 // adjacent folded buffers, as well as folded buffers at the start and
17917 // end the multibuffer
17918 editor.fold_buffer(buffer_ids[0], cx);
17919 editor.fold_buffer(buffer_ids[2], cx);
17920 editor.fold_buffer(buffer_ids[3], cx);
17921
17922 editor
17923 });
17924 cx.simulate_resize(size(px(1000.), px(1000.)));
17925
17926 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
17927 cx.assert_excerpts_with_selections(indoc! {"
17928 [EXCERPT]
17929 ˇ[FOLDED]
17930 [EXCERPT]
17931 a1
17932 b1
17933 [EXCERPT]
17934 [FOLDED]
17935 [EXCERPT]
17936 [FOLDED]
17937 "
17938 });
17939 cx.simulate_keystroke("down");
17940 cx.assert_excerpts_with_selections(indoc! {"
17941 [EXCERPT]
17942 [FOLDED]
17943 [EXCERPT]
17944 ˇa1
17945 b1
17946 [EXCERPT]
17947 [FOLDED]
17948 [EXCERPT]
17949 [FOLDED]
17950 "
17951 });
17952 cx.simulate_keystroke("down");
17953 cx.assert_excerpts_with_selections(indoc! {"
17954 [EXCERPT]
17955 [FOLDED]
17956 [EXCERPT]
17957 a1
17958 ˇb1
17959 [EXCERPT]
17960 [FOLDED]
17961 [EXCERPT]
17962 [FOLDED]
17963 "
17964 });
17965 cx.simulate_keystroke("down");
17966 cx.assert_excerpts_with_selections(indoc! {"
17967 [EXCERPT]
17968 [FOLDED]
17969 [EXCERPT]
17970 a1
17971 b1
17972 ˇ[EXCERPT]
17973 [FOLDED]
17974 [EXCERPT]
17975 [FOLDED]
17976 "
17977 });
17978 cx.simulate_keystroke("down");
17979 cx.assert_excerpts_with_selections(indoc! {"
17980 [EXCERPT]
17981 [FOLDED]
17982 [EXCERPT]
17983 a1
17984 b1
17985 [EXCERPT]
17986 ˇ[FOLDED]
17987 [EXCERPT]
17988 [FOLDED]
17989 "
17990 });
17991 for _ in 0..5 {
17992 cx.simulate_keystroke("down");
17993 cx.assert_excerpts_with_selections(indoc! {"
17994 [EXCERPT]
17995 [FOLDED]
17996 [EXCERPT]
17997 a1
17998 b1
17999 [EXCERPT]
18000 [FOLDED]
18001 [EXCERPT]
18002 ˇ[FOLDED]
18003 "
18004 });
18005 }
18006
18007 cx.simulate_keystroke("up");
18008 cx.assert_excerpts_with_selections(indoc! {"
18009 [EXCERPT]
18010 [FOLDED]
18011 [EXCERPT]
18012 a1
18013 b1
18014 [EXCERPT]
18015 ˇ[FOLDED]
18016 [EXCERPT]
18017 [FOLDED]
18018 "
18019 });
18020 cx.simulate_keystroke("up");
18021 cx.assert_excerpts_with_selections(indoc! {"
18022 [EXCERPT]
18023 [FOLDED]
18024 [EXCERPT]
18025 a1
18026 b1
18027 ˇ[EXCERPT]
18028 [FOLDED]
18029 [EXCERPT]
18030 [FOLDED]
18031 "
18032 });
18033 cx.simulate_keystroke("up");
18034 cx.assert_excerpts_with_selections(indoc! {"
18035 [EXCERPT]
18036 [FOLDED]
18037 [EXCERPT]
18038 a1
18039 ˇb1
18040 [EXCERPT]
18041 [FOLDED]
18042 [EXCERPT]
18043 [FOLDED]
18044 "
18045 });
18046 cx.simulate_keystroke("up");
18047 cx.assert_excerpts_with_selections(indoc! {"
18048 [EXCERPT]
18049 [FOLDED]
18050 [EXCERPT]
18051 ˇa1
18052 b1
18053 [EXCERPT]
18054 [FOLDED]
18055 [EXCERPT]
18056 [FOLDED]
18057 "
18058 });
18059 for _ in 0..5 {
18060 cx.simulate_keystroke("up");
18061 cx.assert_excerpts_with_selections(indoc! {"
18062 [EXCERPT]
18063 ˇ[FOLDED]
18064 [EXCERPT]
18065 a1
18066 b1
18067 [EXCERPT]
18068 [FOLDED]
18069 [EXCERPT]
18070 [FOLDED]
18071 "
18072 });
18073 }
18074}
18075
18076#[gpui::test]
18077async fn test_inline_completion_text(cx: &mut TestAppContext) {
18078 init_test(cx, |_| {});
18079
18080 // Simple insertion
18081 assert_highlighted_edits(
18082 "Hello, world!",
18083 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
18084 true,
18085 cx,
18086 |highlighted_edits, cx| {
18087 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
18088 assert_eq!(highlighted_edits.highlights.len(), 1);
18089 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
18090 assert_eq!(
18091 highlighted_edits.highlights[0].1.background_color,
18092 Some(cx.theme().status().created_background)
18093 );
18094 },
18095 )
18096 .await;
18097
18098 // Replacement
18099 assert_highlighted_edits(
18100 "This is a test.",
18101 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
18102 false,
18103 cx,
18104 |highlighted_edits, cx| {
18105 assert_eq!(highlighted_edits.text, "That is a test.");
18106 assert_eq!(highlighted_edits.highlights.len(), 1);
18107 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
18108 assert_eq!(
18109 highlighted_edits.highlights[0].1.background_color,
18110 Some(cx.theme().status().created_background)
18111 );
18112 },
18113 )
18114 .await;
18115
18116 // Multiple edits
18117 assert_highlighted_edits(
18118 "Hello, world!",
18119 vec![
18120 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
18121 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
18122 ],
18123 false,
18124 cx,
18125 |highlighted_edits, cx| {
18126 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
18127 assert_eq!(highlighted_edits.highlights.len(), 2);
18128 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
18129 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
18130 assert_eq!(
18131 highlighted_edits.highlights[0].1.background_color,
18132 Some(cx.theme().status().created_background)
18133 );
18134 assert_eq!(
18135 highlighted_edits.highlights[1].1.background_color,
18136 Some(cx.theme().status().created_background)
18137 );
18138 },
18139 )
18140 .await;
18141
18142 // Multiple lines with edits
18143 assert_highlighted_edits(
18144 "First line\nSecond line\nThird line\nFourth line",
18145 vec![
18146 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
18147 (
18148 Point::new(2, 0)..Point::new(2, 10),
18149 "New third line".to_string(),
18150 ),
18151 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
18152 ],
18153 false,
18154 cx,
18155 |highlighted_edits, cx| {
18156 assert_eq!(
18157 highlighted_edits.text,
18158 "Second modified\nNew third line\nFourth updated line"
18159 );
18160 assert_eq!(highlighted_edits.highlights.len(), 3);
18161 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
18162 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
18163 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
18164 for highlight in &highlighted_edits.highlights {
18165 assert_eq!(
18166 highlight.1.background_color,
18167 Some(cx.theme().status().created_background)
18168 );
18169 }
18170 },
18171 )
18172 .await;
18173}
18174
18175#[gpui::test]
18176async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
18177 init_test(cx, |_| {});
18178
18179 // Deletion
18180 assert_highlighted_edits(
18181 "Hello, world!",
18182 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
18183 true,
18184 cx,
18185 |highlighted_edits, cx| {
18186 assert_eq!(highlighted_edits.text, "Hello, world!");
18187 assert_eq!(highlighted_edits.highlights.len(), 1);
18188 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
18189 assert_eq!(
18190 highlighted_edits.highlights[0].1.background_color,
18191 Some(cx.theme().status().deleted_background)
18192 );
18193 },
18194 )
18195 .await;
18196
18197 // Insertion
18198 assert_highlighted_edits(
18199 "Hello, world!",
18200 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
18201 true,
18202 cx,
18203 |highlighted_edits, cx| {
18204 assert_eq!(highlighted_edits.highlights.len(), 1);
18205 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
18206 assert_eq!(
18207 highlighted_edits.highlights[0].1.background_color,
18208 Some(cx.theme().status().created_background)
18209 );
18210 },
18211 )
18212 .await;
18213}
18214
18215async fn assert_highlighted_edits(
18216 text: &str,
18217 edits: Vec<(Range<Point>, String)>,
18218 include_deletions: bool,
18219 cx: &mut TestAppContext,
18220 assertion_fn: impl Fn(HighlightedText, &App),
18221) {
18222 let window = cx.add_window(|window, cx| {
18223 let buffer = MultiBuffer::build_simple(text, cx);
18224 Editor::new(EditorMode::full(), buffer, None, window, cx)
18225 });
18226 let cx = &mut VisualTestContext::from_window(*window, cx);
18227
18228 let (buffer, snapshot) = window
18229 .update(cx, |editor, _window, cx| {
18230 (
18231 editor.buffer().clone(),
18232 editor.buffer().read(cx).snapshot(cx),
18233 )
18234 })
18235 .unwrap();
18236
18237 let edits = edits
18238 .into_iter()
18239 .map(|(range, edit)| {
18240 (
18241 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
18242 edit,
18243 )
18244 })
18245 .collect::<Vec<_>>();
18246
18247 let text_anchor_edits = edits
18248 .clone()
18249 .into_iter()
18250 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
18251 .collect::<Vec<_>>();
18252
18253 let edit_preview = window
18254 .update(cx, |_, _window, cx| {
18255 buffer
18256 .read(cx)
18257 .as_singleton()
18258 .unwrap()
18259 .read(cx)
18260 .preview_edits(text_anchor_edits.into(), cx)
18261 })
18262 .unwrap()
18263 .await;
18264
18265 cx.update(|_window, cx| {
18266 let highlighted_edits = inline_completion_edit_text(
18267 &snapshot.as_singleton().unwrap().2,
18268 &edits,
18269 &edit_preview,
18270 include_deletions,
18271 cx,
18272 );
18273 assertion_fn(highlighted_edits, cx)
18274 });
18275}
18276
18277#[track_caller]
18278fn assert_breakpoint(
18279 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
18280 path: &Arc<Path>,
18281 expected: Vec<(u32, Breakpoint)>,
18282) {
18283 if expected.len() == 0usize {
18284 assert!(!breakpoints.contains_key(path), "{}", path.display());
18285 } else {
18286 let mut breakpoint = breakpoints
18287 .get(path)
18288 .unwrap()
18289 .into_iter()
18290 .map(|breakpoint| {
18291 (
18292 breakpoint.row,
18293 Breakpoint {
18294 message: breakpoint.message.clone(),
18295 state: breakpoint.state,
18296 condition: breakpoint.condition.clone(),
18297 hit_condition: breakpoint.hit_condition.clone(),
18298 },
18299 )
18300 })
18301 .collect::<Vec<_>>();
18302
18303 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
18304
18305 assert_eq!(expected, breakpoint);
18306 }
18307}
18308
18309fn add_log_breakpoint_at_cursor(
18310 editor: &mut Editor,
18311 log_message: &str,
18312 window: &mut Window,
18313 cx: &mut Context<Editor>,
18314) {
18315 let (anchor, bp) = editor
18316 .breakpoints_at_cursors(window, cx)
18317 .first()
18318 .and_then(|(anchor, bp)| {
18319 if let Some(bp) = bp {
18320 Some((*anchor, bp.clone()))
18321 } else {
18322 None
18323 }
18324 })
18325 .unwrap_or_else(|| {
18326 let cursor_position: Point = editor.selections.newest(cx).head();
18327
18328 let breakpoint_position = editor
18329 .snapshot(window, cx)
18330 .display_snapshot
18331 .buffer_snapshot
18332 .anchor_before(Point::new(cursor_position.row, 0));
18333
18334 (breakpoint_position, Breakpoint::new_log(&log_message))
18335 });
18336
18337 editor.edit_breakpoint_at_anchor(
18338 anchor,
18339 bp,
18340 BreakpointEditAction::EditLogMessage(log_message.into()),
18341 cx,
18342 );
18343}
18344
18345#[gpui::test]
18346async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
18347 init_test(cx, |_| {});
18348
18349 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18350 let fs = FakeFs::new(cx.executor());
18351 fs.insert_tree(
18352 path!("/a"),
18353 json!({
18354 "main.rs": sample_text,
18355 }),
18356 )
18357 .await;
18358 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18359 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18360 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18361
18362 let fs = FakeFs::new(cx.executor());
18363 fs.insert_tree(
18364 path!("/a"),
18365 json!({
18366 "main.rs": sample_text,
18367 }),
18368 )
18369 .await;
18370 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18371 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18372 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18373 let worktree_id = workspace
18374 .update(cx, |workspace, _window, cx| {
18375 workspace.project().update(cx, |project, cx| {
18376 project.worktrees(cx).next().unwrap().read(cx).id()
18377 })
18378 })
18379 .unwrap();
18380
18381 let buffer = project
18382 .update(cx, |project, cx| {
18383 project.open_buffer((worktree_id, "main.rs"), cx)
18384 })
18385 .await
18386 .unwrap();
18387
18388 let (editor, cx) = cx.add_window_view(|window, cx| {
18389 Editor::new(
18390 EditorMode::full(),
18391 MultiBuffer::build_from_buffer(buffer, cx),
18392 Some(project.clone()),
18393 window,
18394 cx,
18395 )
18396 });
18397
18398 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18399 let abs_path = project.read_with(cx, |project, cx| {
18400 project
18401 .absolute_path(&project_path, cx)
18402 .map(|path_buf| Arc::from(path_buf.to_owned()))
18403 .unwrap()
18404 });
18405
18406 // assert we can add breakpoint on the first line
18407 editor.update_in(cx, |editor, window, cx| {
18408 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18409 editor.move_to_end(&MoveToEnd, window, cx);
18410 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18411 });
18412
18413 let breakpoints = editor.update(cx, |editor, cx| {
18414 editor
18415 .breakpoint_store()
18416 .as_ref()
18417 .unwrap()
18418 .read(cx)
18419 .all_breakpoints(cx)
18420 .clone()
18421 });
18422
18423 assert_eq!(1, breakpoints.len());
18424 assert_breakpoint(
18425 &breakpoints,
18426 &abs_path,
18427 vec![
18428 (0, Breakpoint::new_standard()),
18429 (3, Breakpoint::new_standard()),
18430 ],
18431 );
18432
18433 editor.update_in(cx, |editor, window, cx| {
18434 editor.move_to_beginning(&MoveToBeginning, window, cx);
18435 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18436 });
18437
18438 let breakpoints = editor.update(cx, |editor, cx| {
18439 editor
18440 .breakpoint_store()
18441 .as_ref()
18442 .unwrap()
18443 .read(cx)
18444 .all_breakpoints(cx)
18445 .clone()
18446 });
18447
18448 assert_eq!(1, breakpoints.len());
18449 assert_breakpoint(
18450 &breakpoints,
18451 &abs_path,
18452 vec![(3, Breakpoint::new_standard())],
18453 );
18454
18455 editor.update_in(cx, |editor, window, cx| {
18456 editor.move_to_end(&MoveToEnd, window, cx);
18457 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18458 });
18459
18460 let breakpoints = editor.update(cx, |editor, cx| {
18461 editor
18462 .breakpoint_store()
18463 .as_ref()
18464 .unwrap()
18465 .read(cx)
18466 .all_breakpoints(cx)
18467 .clone()
18468 });
18469
18470 assert_eq!(0, breakpoints.len());
18471 assert_breakpoint(&breakpoints, &abs_path, vec![]);
18472}
18473
18474#[gpui::test]
18475async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
18476 init_test(cx, |_| {});
18477
18478 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18479
18480 let fs = FakeFs::new(cx.executor());
18481 fs.insert_tree(
18482 path!("/a"),
18483 json!({
18484 "main.rs": sample_text,
18485 }),
18486 )
18487 .await;
18488 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18489 let (workspace, cx) =
18490 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
18491
18492 let worktree_id = workspace.update(cx, |workspace, cx| {
18493 workspace.project().update(cx, |project, cx| {
18494 project.worktrees(cx).next().unwrap().read(cx).id()
18495 })
18496 });
18497
18498 let buffer = project
18499 .update(cx, |project, cx| {
18500 project.open_buffer((worktree_id, "main.rs"), cx)
18501 })
18502 .await
18503 .unwrap();
18504
18505 let (editor, cx) = cx.add_window_view(|window, cx| {
18506 Editor::new(
18507 EditorMode::full(),
18508 MultiBuffer::build_from_buffer(buffer, cx),
18509 Some(project.clone()),
18510 window,
18511 cx,
18512 )
18513 });
18514
18515 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18516 let abs_path = project.read_with(cx, |project, cx| {
18517 project
18518 .absolute_path(&project_path, cx)
18519 .map(|path_buf| Arc::from(path_buf.to_owned()))
18520 .unwrap()
18521 });
18522
18523 editor.update_in(cx, |editor, window, cx| {
18524 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
18525 });
18526
18527 let breakpoints = editor.update(cx, |editor, cx| {
18528 editor
18529 .breakpoint_store()
18530 .as_ref()
18531 .unwrap()
18532 .read(cx)
18533 .all_breakpoints(cx)
18534 .clone()
18535 });
18536
18537 assert_breakpoint(
18538 &breakpoints,
18539 &abs_path,
18540 vec![(0, Breakpoint::new_log("hello world"))],
18541 );
18542
18543 // Removing a log message from a log breakpoint should remove it
18544 editor.update_in(cx, |editor, window, cx| {
18545 add_log_breakpoint_at_cursor(editor, "", window, cx);
18546 });
18547
18548 let breakpoints = editor.update(cx, |editor, cx| {
18549 editor
18550 .breakpoint_store()
18551 .as_ref()
18552 .unwrap()
18553 .read(cx)
18554 .all_breakpoints(cx)
18555 .clone()
18556 });
18557
18558 assert_breakpoint(&breakpoints, &abs_path, vec![]);
18559
18560 editor.update_in(cx, |editor, window, cx| {
18561 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18562 editor.move_to_end(&MoveToEnd, window, cx);
18563 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18564 // Not adding a log message to a standard breakpoint shouldn't remove it
18565 add_log_breakpoint_at_cursor(editor, "", window, cx);
18566 });
18567
18568 let breakpoints = editor.update(cx, |editor, cx| {
18569 editor
18570 .breakpoint_store()
18571 .as_ref()
18572 .unwrap()
18573 .read(cx)
18574 .all_breakpoints(cx)
18575 .clone()
18576 });
18577
18578 assert_breakpoint(
18579 &breakpoints,
18580 &abs_path,
18581 vec![
18582 (0, Breakpoint::new_standard()),
18583 (3, Breakpoint::new_standard()),
18584 ],
18585 );
18586
18587 editor.update_in(cx, |editor, window, cx| {
18588 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
18589 });
18590
18591 let breakpoints = editor.update(cx, |editor, cx| {
18592 editor
18593 .breakpoint_store()
18594 .as_ref()
18595 .unwrap()
18596 .read(cx)
18597 .all_breakpoints(cx)
18598 .clone()
18599 });
18600
18601 assert_breakpoint(
18602 &breakpoints,
18603 &abs_path,
18604 vec![
18605 (0, Breakpoint::new_standard()),
18606 (3, Breakpoint::new_log("hello world")),
18607 ],
18608 );
18609
18610 editor.update_in(cx, |editor, window, cx| {
18611 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
18612 });
18613
18614 let breakpoints = editor.update(cx, |editor, cx| {
18615 editor
18616 .breakpoint_store()
18617 .as_ref()
18618 .unwrap()
18619 .read(cx)
18620 .all_breakpoints(cx)
18621 .clone()
18622 });
18623
18624 assert_breakpoint(
18625 &breakpoints,
18626 &abs_path,
18627 vec![
18628 (0, Breakpoint::new_standard()),
18629 (3, Breakpoint::new_log("hello Earth!!")),
18630 ],
18631 );
18632}
18633
18634/// This also tests that Editor::breakpoint_at_cursor_head is working properly
18635/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
18636/// or when breakpoints were placed out of order. This tests for a regression too
18637#[gpui::test]
18638async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
18639 init_test(cx, |_| {});
18640
18641 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18642 let fs = FakeFs::new(cx.executor());
18643 fs.insert_tree(
18644 path!("/a"),
18645 json!({
18646 "main.rs": sample_text,
18647 }),
18648 )
18649 .await;
18650 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18651 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18652 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18653
18654 let fs = FakeFs::new(cx.executor());
18655 fs.insert_tree(
18656 path!("/a"),
18657 json!({
18658 "main.rs": sample_text,
18659 }),
18660 )
18661 .await;
18662 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18663 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18664 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18665 let worktree_id = workspace
18666 .update(cx, |workspace, _window, cx| {
18667 workspace.project().update(cx, |project, cx| {
18668 project.worktrees(cx).next().unwrap().read(cx).id()
18669 })
18670 })
18671 .unwrap();
18672
18673 let buffer = project
18674 .update(cx, |project, cx| {
18675 project.open_buffer((worktree_id, "main.rs"), cx)
18676 })
18677 .await
18678 .unwrap();
18679
18680 let (editor, cx) = cx.add_window_view(|window, cx| {
18681 Editor::new(
18682 EditorMode::full(),
18683 MultiBuffer::build_from_buffer(buffer, cx),
18684 Some(project.clone()),
18685 window,
18686 cx,
18687 )
18688 });
18689
18690 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18691 let abs_path = project.read_with(cx, |project, cx| {
18692 project
18693 .absolute_path(&project_path, cx)
18694 .map(|path_buf| Arc::from(path_buf.to_owned()))
18695 .unwrap()
18696 });
18697
18698 // assert we can add breakpoint on the first line
18699 editor.update_in(cx, |editor, window, cx| {
18700 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18701 editor.move_to_end(&MoveToEnd, window, cx);
18702 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18703 editor.move_up(&MoveUp, window, cx);
18704 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18705 });
18706
18707 let breakpoints = editor.update(cx, |editor, cx| {
18708 editor
18709 .breakpoint_store()
18710 .as_ref()
18711 .unwrap()
18712 .read(cx)
18713 .all_breakpoints(cx)
18714 .clone()
18715 });
18716
18717 assert_eq!(1, breakpoints.len());
18718 assert_breakpoint(
18719 &breakpoints,
18720 &abs_path,
18721 vec![
18722 (0, Breakpoint::new_standard()),
18723 (2, Breakpoint::new_standard()),
18724 (3, Breakpoint::new_standard()),
18725 ],
18726 );
18727
18728 editor.update_in(cx, |editor, window, cx| {
18729 editor.move_to_beginning(&MoveToBeginning, window, cx);
18730 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18731 editor.move_to_end(&MoveToEnd, window, cx);
18732 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18733 // Disabling a breakpoint that doesn't exist should do nothing
18734 editor.move_up(&MoveUp, window, cx);
18735 editor.move_up(&MoveUp, window, cx);
18736 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18737 });
18738
18739 let breakpoints = editor.update(cx, |editor, cx| {
18740 editor
18741 .breakpoint_store()
18742 .as_ref()
18743 .unwrap()
18744 .read(cx)
18745 .all_breakpoints(cx)
18746 .clone()
18747 });
18748
18749 let disable_breakpoint = {
18750 let mut bp = Breakpoint::new_standard();
18751 bp.state = BreakpointState::Disabled;
18752 bp
18753 };
18754
18755 assert_eq!(1, breakpoints.len());
18756 assert_breakpoint(
18757 &breakpoints,
18758 &abs_path,
18759 vec![
18760 (0, disable_breakpoint.clone()),
18761 (2, Breakpoint::new_standard()),
18762 (3, disable_breakpoint.clone()),
18763 ],
18764 );
18765
18766 editor.update_in(cx, |editor, window, cx| {
18767 editor.move_to_beginning(&MoveToBeginning, window, cx);
18768 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
18769 editor.move_to_end(&MoveToEnd, window, cx);
18770 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
18771 editor.move_up(&MoveUp, window, cx);
18772 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18773 });
18774
18775 let breakpoints = editor.update(cx, |editor, cx| {
18776 editor
18777 .breakpoint_store()
18778 .as_ref()
18779 .unwrap()
18780 .read(cx)
18781 .all_breakpoints(cx)
18782 .clone()
18783 });
18784
18785 assert_eq!(1, breakpoints.len());
18786 assert_breakpoint(
18787 &breakpoints,
18788 &abs_path,
18789 vec![
18790 (0, Breakpoint::new_standard()),
18791 (2, disable_breakpoint),
18792 (3, Breakpoint::new_standard()),
18793 ],
18794 );
18795}
18796
18797#[gpui::test]
18798async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
18799 init_test(cx, |_| {});
18800 let capabilities = lsp::ServerCapabilities {
18801 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
18802 prepare_provider: Some(true),
18803 work_done_progress_options: Default::default(),
18804 })),
18805 ..Default::default()
18806 };
18807 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
18808
18809 cx.set_state(indoc! {"
18810 struct Fˇoo {}
18811 "});
18812
18813 cx.update_editor(|editor, _, cx| {
18814 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
18815 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
18816 editor.highlight_background::<DocumentHighlightRead>(
18817 &[highlight_range],
18818 |c| c.editor_document_highlight_read_background,
18819 cx,
18820 );
18821 });
18822
18823 let mut prepare_rename_handler = cx
18824 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
18825 move |_, _, _| async move {
18826 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
18827 start: lsp::Position {
18828 line: 0,
18829 character: 7,
18830 },
18831 end: lsp::Position {
18832 line: 0,
18833 character: 10,
18834 },
18835 })))
18836 },
18837 );
18838 let prepare_rename_task = cx
18839 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
18840 .expect("Prepare rename was not started");
18841 prepare_rename_handler.next().await.unwrap();
18842 prepare_rename_task.await.expect("Prepare rename failed");
18843
18844 let mut rename_handler =
18845 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
18846 let edit = lsp::TextEdit {
18847 range: lsp::Range {
18848 start: lsp::Position {
18849 line: 0,
18850 character: 7,
18851 },
18852 end: lsp::Position {
18853 line: 0,
18854 character: 10,
18855 },
18856 },
18857 new_text: "FooRenamed".to_string(),
18858 };
18859 Ok(Some(lsp::WorkspaceEdit::new(
18860 // Specify the same edit twice
18861 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
18862 )))
18863 });
18864 let rename_task = cx
18865 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
18866 .expect("Confirm rename was not started");
18867 rename_handler.next().await.unwrap();
18868 rename_task.await.expect("Confirm rename failed");
18869 cx.run_until_parked();
18870
18871 // Despite two edits, only one is actually applied as those are identical
18872 cx.assert_editor_state(indoc! {"
18873 struct FooRenamedˇ {}
18874 "});
18875}
18876
18877#[gpui::test]
18878async fn test_rename_without_prepare(cx: &mut TestAppContext) {
18879 init_test(cx, |_| {});
18880 // These capabilities indicate that the server does not support prepare rename.
18881 let capabilities = lsp::ServerCapabilities {
18882 rename_provider: Some(lsp::OneOf::Left(true)),
18883 ..Default::default()
18884 };
18885 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
18886
18887 cx.set_state(indoc! {"
18888 struct Fˇoo {}
18889 "});
18890
18891 cx.update_editor(|editor, _window, cx| {
18892 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
18893 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
18894 editor.highlight_background::<DocumentHighlightRead>(
18895 &[highlight_range],
18896 |c| c.editor_document_highlight_read_background,
18897 cx,
18898 );
18899 });
18900
18901 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
18902 .expect("Prepare rename was not started")
18903 .await
18904 .expect("Prepare rename failed");
18905
18906 let mut rename_handler =
18907 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
18908 let edit = lsp::TextEdit {
18909 range: lsp::Range {
18910 start: lsp::Position {
18911 line: 0,
18912 character: 7,
18913 },
18914 end: lsp::Position {
18915 line: 0,
18916 character: 10,
18917 },
18918 },
18919 new_text: "FooRenamed".to_string(),
18920 };
18921 Ok(Some(lsp::WorkspaceEdit::new(
18922 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
18923 )))
18924 });
18925 let rename_task = cx
18926 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
18927 .expect("Confirm rename was not started");
18928 rename_handler.next().await.unwrap();
18929 rename_task.await.expect("Confirm rename failed");
18930 cx.run_until_parked();
18931
18932 // Correct range is renamed, as `surrounding_word` is used to find it.
18933 cx.assert_editor_state(indoc! {"
18934 struct FooRenamedˇ {}
18935 "});
18936}
18937
18938#[gpui::test]
18939async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
18940 init_test(cx, |_| {});
18941 let mut cx = EditorTestContext::new(cx).await;
18942
18943 let language = Arc::new(
18944 Language::new(
18945 LanguageConfig::default(),
18946 Some(tree_sitter_html::LANGUAGE.into()),
18947 )
18948 .with_brackets_query(
18949 r#"
18950 ("<" @open "/>" @close)
18951 ("</" @open ">" @close)
18952 ("<" @open ">" @close)
18953 ("\"" @open "\"" @close)
18954 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
18955 "#,
18956 )
18957 .unwrap(),
18958 );
18959 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
18960
18961 cx.set_state(indoc! {"
18962 <span>ˇ</span>
18963 "});
18964 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18965 cx.assert_editor_state(indoc! {"
18966 <span>
18967 ˇ
18968 </span>
18969 "});
18970
18971 cx.set_state(indoc! {"
18972 <span><span></span>ˇ</span>
18973 "});
18974 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18975 cx.assert_editor_state(indoc! {"
18976 <span><span></span>
18977 ˇ</span>
18978 "});
18979
18980 cx.set_state(indoc! {"
18981 <span>ˇ
18982 </span>
18983 "});
18984 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18985 cx.assert_editor_state(indoc! {"
18986 <span>
18987 ˇ
18988 </span>
18989 "});
18990}
18991
18992#[gpui::test(iterations = 10)]
18993async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
18994 init_test(cx, |_| {});
18995
18996 let fs = FakeFs::new(cx.executor());
18997 fs.insert_tree(
18998 path!("/dir"),
18999 json!({
19000 "a.ts": "a",
19001 }),
19002 )
19003 .await;
19004
19005 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
19006 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19007 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19008
19009 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19010 language_registry.add(Arc::new(Language::new(
19011 LanguageConfig {
19012 name: "TypeScript".into(),
19013 matcher: LanguageMatcher {
19014 path_suffixes: vec!["ts".to_string()],
19015 ..Default::default()
19016 },
19017 ..Default::default()
19018 },
19019 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19020 )));
19021 let mut fake_language_servers = language_registry.register_fake_lsp(
19022 "TypeScript",
19023 FakeLspAdapter {
19024 capabilities: lsp::ServerCapabilities {
19025 code_lens_provider: Some(lsp::CodeLensOptions {
19026 resolve_provider: Some(true),
19027 }),
19028 execute_command_provider: Some(lsp::ExecuteCommandOptions {
19029 commands: vec!["_the/command".to_string()],
19030 ..lsp::ExecuteCommandOptions::default()
19031 }),
19032 ..lsp::ServerCapabilities::default()
19033 },
19034 ..FakeLspAdapter::default()
19035 },
19036 );
19037
19038 let (buffer, _handle) = project
19039 .update(cx, |p, cx| {
19040 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
19041 })
19042 .await
19043 .unwrap();
19044 cx.executor().run_until_parked();
19045
19046 let fake_server = fake_language_servers.next().await.unwrap();
19047
19048 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
19049 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
19050 drop(buffer_snapshot);
19051 let actions = cx
19052 .update_window(*workspace, |_, window, cx| {
19053 project.code_actions(&buffer, anchor..anchor, window, cx)
19054 })
19055 .unwrap();
19056
19057 fake_server
19058 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
19059 Ok(Some(vec![
19060 lsp::CodeLens {
19061 range: lsp::Range::default(),
19062 command: Some(lsp::Command {
19063 title: "Code lens command".to_owned(),
19064 command: "_the/command".to_owned(),
19065 arguments: None,
19066 }),
19067 data: None,
19068 },
19069 lsp::CodeLens {
19070 range: lsp::Range::default(),
19071 command: Some(lsp::Command {
19072 title: "Command not in capabilities".to_owned(),
19073 command: "not in capabilities".to_owned(),
19074 arguments: None,
19075 }),
19076 data: None,
19077 },
19078 lsp::CodeLens {
19079 range: lsp::Range {
19080 start: lsp::Position {
19081 line: 1,
19082 character: 1,
19083 },
19084 end: lsp::Position {
19085 line: 1,
19086 character: 1,
19087 },
19088 },
19089 command: Some(lsp::Command {
19090 title: "Command not in range".to_owned(),
19091 command: "_the/command".to_owned(),
19092 arguments: None,
19093 }),
19094 data: None,
19095 },
19096 ]))
19097 })
19098 .next()
19099 .await;
19100
19101 let actions = actions.await.unwrap();
19102 assert_eq!(
19103 actions.len(),
19104 1,
19105 "Should have only one valid action for the 0..0 range"
19106 );
19107 let action = actions[0].clone();
19108 let apply = project.update(cx, |project, cx| {
19109 project.apply_code_action(buffer.clone(), action, true, cx)
19110 });
19111
19112 // Resolving the code action does not populate its edits. In absence of
19113 // edits, we must execute the given command.
19114 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
19115 |mut lens, _| async move {
19116 let lens_command = lens.command.as_mut().expect("should have a command");
19117 assert_eq!(lens_command.title, "Code lens command");
19118 lens_command.arguments = Some(vec![json!("the-argument")]);
19119 Ok(lens)
19120 },
19121 );
19122
19123 // While executing the command, the language server sends the editor
19124 // a `workspaceEdit` request.
19125 fake_server
19126 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
19127 let fake = fake_server.clone();
19128 move |params, _| {
19129 assert_eq!(params.command, "_the/command");
19130 let fake = fake.clone();
19131 async move {
19132 fake.server
19133 .request::<lsp::request::ApplyWorkspaceEdit>(
19134 lsp::ApplyWorkspaceEditParams {
19135 label: None,
19136 edit: lsp::WorkspaceEdit {
19137 changes: Some(
19138 [(
19139 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
19140 vec![lsp::TextEdit {
19141 range: lsp::Range::new(
19142 lsp::Position::new(0, 0),
19143 lsp::Position::new(0, 0),
19144 ),
19145 new_text: "X".into(),
19146 }],
19147 )]
19148 .into_iter()
19149 .collect(),
19150 ),
19151 ..Default::default()
19152 },
19153 },
19154 )
19155 .await
19156 .unwrap();
19157 Ok(Some(json!(null)))
19158 }
19159 }
19160 })
19161 .next()
19162 .await;
19163
19164 // Applying the code lens command returns a project transaction containing the edits
19165 // sent by the language server in its `workspaceEdit` request.
19166 let transaction = apply.await.unwrap();
19167 assert!(transaction.0.contains_key(&buffer));
19168 buffer.update(cx, |buffer, cx| {
19169 assert_eq!(buffer.text(), "Xa");
19170 buffer.undo(cx);
19171 assert_eq!(buffer.text(), "a");
19172 });
19173}
19174
19175#[gpui::test]
19176async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
19177 init_test(cx, |_| {});
19178
19179 let fs = FakeFs::new(cx.executor());
19180 let main_text = r#"fn main() {
19181println!("1");
19182println!("2");
19183println!("3");
19184println!("4");
19185println!("5");
19186}"#;
19187 let lib_text = "mod foo {}";
19188 fs.insert_tree(
19189 path!("/a"),
19190 json!({
19191 "lib.rs": lib_text,
19192 "main.rs": main_text,
19193 }),
19194 )
19195 .await;
19196
19197 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19198 let (workspace, cx) =
19199 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19200 let worktree_id = workspace.update(cx, |workspace, cx| {
19201 workspace.project().update(cx, |project, cx| {
19202 project.worktrees(cx).next().unwrap().read(cx).id()
19203 })
19204 });
19205
19206 let expected_ranges = vec![
19207 Point::new(0, 0)..Point::new(0, 0),
19208 Point::new(1, 0)..Point::new(1, 1),
19209 Point::new(2, 0)..Point::new(2, 2),
19210 Point::new(3, 0)..Point::new(3, 3),
19211 ];
19212
19213 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
19214 let editor_1 = workspace
19215 .update_in(cx, |workspace, window, cx| {
19216 workspace.open_path(
19217 (worktree_id, "main.rs"),
19218 Some(pane_1.downgrade()),
19219 true,
19220 window,
19221 cx,
19222 )
19223 })
19224 .unwrap()
19225 .await
19226 .downcast::<Editor>()
19227 .unwrap();
19228 pane_1.update(cx, |pane, cx| {
19229 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19230 open_editor.update(cx, |editor, cx| {
19231 assert_eq!(
19232 editor.display_text(cx),
19233 main_text,
19234 "Original main.rs text on initial open",
19235 );
19236 assert_eq!(
19237 editor
19238 .selections
19239 .all::<Point>(cx)
19240 .into_iter()
19241 .map(|s| s.range())
19242 .collect::<Vec<_>>(),
19243 vec![Point::zero()..Point::zero()],
19244 "Default selections on initial open",
19245 );
19246 })
19247 });
19248 editor_1.update_in(cx, |editor, window, cx| {
19249 editor.change_selections(None, window, cx, |s| {
19250 s.select_ranges(expected_ranges.clone());
19251 });
19252 });
19253
19254 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
19255 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
19256 });
19257 let editor_2 = workspace
19258 .update_in(cx, |workspace, window, cx| {
19259 workspace.open_path(
19260 (worktree_id, "main.rs"),
19261 Some(pane_2.downgrade()),
19262 true,
19263 window,
19264 cx,
19265 )
19266 })
19267 .unwrap()
19268 .await
19269 .downcast::<Editor>()
19270 .unwrap();
19271 pane_2.update(cx, |pane, cx| {
19272 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19273 open_editor.update(cx, |editor, cx| {
19274 assert_eq!(
19275 editor.display_text(cx),
19276 main_text,
19277 "Original main.rs text on initial open in another panel",
19278 );
19279 assert_eq!(
19280 editor
19281 .selections
19282 .all::<Point>(cx)
19283 .into_iter()
19284 .map(|s| s.range())
19285 .collect::<Vec<_>>(),
19286 vec![Point::zero()..Point::zero()],
19287 "Default selections on initial open in another panel",
19288 );
19289 })
19290 });
19291
19292 editor_2.update_in(cx, |editor, window, cx| {
19293 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
19294 });
19295
19296 let _other_editor_1 = workspace
19297 .update_in(cx, |workspace, window, cx| {
19298 workspace.open_path(
19299 (worktree_id, "lib.rs"),
19300 Some(pane_1.downgrade()),
19301 true,
19302 window,
19303 cx,
19304 )
19305 })
19306 .unwrap()
19307 .await
19308 .downcast::<Editor>()
19309 .unwrap();
19310 pane_1
19311 .update_in(cx, |pane, window, cx| {
19312 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
19313 .unwrap()
19314 })
19315 .await
19316 .unwrap();
19317 drop(editor_1);
19318 pane_1.update(cx, |pane, cx| {
19319 pane.active_item()
19320 .unwrap()
19321 .downcast::<Editor>()
19322 .unwrap()
19323 .update(cx, |editor, cx| {
19324 assert_eq!(
19325 editor.display_text(cx),
19326 lib_text,
19327 "Other file should be open and active",
19328 );
19329 });
19330 assert_eq!(pane.items().count(), 1, "No other editors should be open");
19331 });
19332
19333 let _other_editor_2 = workspace
19334 .update_in(cx, |workspace, window, cx| {
19335 workspace.open_path(
19336 (worktree_id, "lib.rs"),
19337 Some(pane_2.downgrade()),
19338 true,
19339 window,
19340 cx,
19341 )
19342 })
19343 .unwrap()
19344 .await
19345 .downcast::<Editor>()
19346 .unwrap();
19347 pane_2
19348 .update_in(cx, |pane, window, cx| {
19349 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
19350 .unwrap()
19351 })
19352 .await
19353 .unwrap();
19354 drop(editor_2);
19355 pane_2.update(cx, |pane, cx| {
19356 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19357 open_editor.update(cx, |editor, cx| {
19358 assert_eq!(
19359 editor.display_text(cx),
19360 lib_text,
19361 "Other file should be open and active in another panel too",
19362 );
19363 });
19364 assert_eq!(
19365 pane.items().count(),
19366 1,
19367 "No other editors should be open in another pane",
19368 );
19369 });
19370
19371 let _editor_1_reopened = workspace
19372 .update_in(cx, |workspace, window, cx| {
19373 workspace.open_path(
19374 (worktree_id, "main.rs"),
19375 Some(pane_1.downgrade()),
19376 true,
19377 window,
19378 cx,
19379 )
19380 })
19381 .unwrap()
19382 .await
19383 .downcast::<Editor>()
19384 .unwrap();
19385 let _editor_2_reopened = workspace
19386 .update_in(cx, |workspace, window, cx| {
19387 workspace.open_path(
19388 (worktree_id, "main.rs"),
19389 Some(pane_2.downgrade()),
19390 true,
19391 window,
19392 cx,
19393 )
19394 })
19395 .unwrap()
19396 .await
19397 .downcast::<Editor>()
19398 .unwrap();
19399 pane_1.update(cx, |pane, cx| {
19400 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19401 open_editor.update(cx, |editor, cx| {
19402 assert_eq!(
19403 editor.display_text(cx),
19404 main_text,
19405 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
19406 );
19407 assert_eq!(
19408 editor
19409 .selections
19410 .all::<Point>(cx)
19411 .into_iter()
19412 .map(|s| s.range())
19413 .collect::<Vec<_>>(),
19414 expected_ranges,
19415 "Previous editor in the 1st panel had selections and should get them restored on reopen",
19416 );
19417 })
19418 });
19419 pane_2.update(cx, |pane, cx| {
19420 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19421 open_editor.update(cx, |editor, cx| {
19422 assert_eq!(
19423 editor.display_text(cx),
19424 r#"fn main() {
19425⋯rintln!("1");
19426⋯intln!("2");
19427⋯ntln!("3");
19428println!("4");
19429println!("5");
19430}"#,
19431 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
19432 );
19433 assert_eq!(
19434 editor
19435 .selections
19436 .all::<Point>(cx)
19437 .into_iter()
19438 .map(|s| s.range())
19439 .collect::<Vec<_>>(),
19440 vec![Point::zero()..Point::zero()],
19441 "Previous editor in the 2nd pane had no selections changed hence should restore none",
19442 );
19443 })
19444 });
19445}
19446
19447#[gpui::test]
19448async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
19449 init_test(cx, |_| {});
19450
19451 let fs = FakeFs::new(cx.executor());
19452 let main_text = r#"fn main() {
19453println!("1");
19454println!("2");
19455println!("3");
19456println!("4");
19457println!("5");
19458}"#;
19459 let lib_text = "mod foo {}";
19460 fs.insert_tree(
19461 path!("/a"),
19462 json!({
19463 "lib.rs": lib_text,
19464 "main.rs": main_text,
19465 }),
19466 )
19467 .await;
19468
19469 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19470 let (workspace, cx) =
19471 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19472 let worktree_id = workspace.update(cx, |workspace, cx| {
19473 workspace.project().update(cx, |project, cx| {
19474 project.worktrees(cx).next().unwrap().read(cx).id()
19475 })
19476 });
19477
19478 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
19479 let editor = 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 "Original main.rs text on initial open",
19500 );
19501 })
19502 });
19503 editor.update_in(cx, |editor, window, cx| {
19504 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
19505 });
19506
19507 cx.update_global(|store: &mut SettingsStore, cx| {
19508 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
19509 s.restore_on_file_reopen = Some(false);
19510 });
19511 });
19512 editor.update_in(cx, |editor, window, cx| {
19513 editor.fold_ranges(
19514 vec![
19515 Point::new(1, 0)..Point::new(1, 1),
19516 Point::new(2, 0)..Point::new(2, 2),
19517 Point::new(3, 0)..Point::new(3, 3),
19518 ],
19519 false,
19520 window,
19521 cx,
19522 );
19523 });
19524 pane.update_in(cx, |pane, window, cx| {
19525 pane.close_all_items(&CloseAllItems::default(), window, cx)
19526 .unwrap()
19527 })
19528 .await
19529 .unwrap();
19530 pane.update(cx, |pane, _| {
19531 assert!(pane.active_item().is_none());
19532 });
19533 cx.update_global(|store: &mut SettingsStore, cx| {
19534 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
19535 s.restore_on_file_reopen = Some(true);
19536 });
19537 });
19538
19539 let _editor_reopened = workspace
19540 .update_in(cx, |workspace, window, cx| {
19541 workspace.open_path(
19542 (worktree_id, "main.rs"),
19543 Some(pane.downgrade()),
19544 true,
19545 window,
19546 cx,
19547 )
19548 })
19549 .unwrap()
19550 .await
19551 .downcast::<Editor>()
19552 .unwrap();
19553 pane.update(cx, |pane, cx| {
19554 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19555 open_editor.update(cx, |editor, cx| {
19556 assert_eq!(
19557 editor.display_text(cx),
19558 main_text,
19559 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
19560 );
19561 })
19562 });
19563}
19564
19565#[gpui::test]
19566async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
19567 struct EmptyModalView {
19568 focus_handle: gpui::FocusHandle,
19569 }
19570 impl EventEmitter<DismissEvent> for EmptyModalView {}
19571 impl Render for EmptyModalView {
19572 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
19573 div()
19574 }
19575 }
19576 impl Focusable for EmptyModalView {
19577 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
19578 self.focus_handle.clone()
19579 }
19580 }
19581 impl workspace::ModalView for EmptyModalView {}
19582 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
19583 EmptyModalView {
19584 focus_handle: cx.focus_handle(),
19585 }
19586 }
19587
19588 init_test(cx, |_| {});
19589
19590 let fs = FakeFs::new(cx.executor());
19591 let project = Project::test(fs, [], cx).await;
19592 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19593 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
19594 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19595 let editor = cx.new_window_entity(|window, cx| {
19596 Editor::new(
19597 EditorMode::full(),
19598 buffer,
19599 Some(project.clone()),
19600 window,
19601 cx,
19602 )
19603 });
19604 workspace
19605 .update(cx, |workspace, window, cx| {
19606 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
19607 })
19608 .unwrap();
19609 editor.update_in(cx, |editor, window, cx| {
19610 editor.open_context_menu(&OpenContextMenu, window, cx);
19611 assert!(editor.mouse_context_menu.is_some());
19612 });
19613 workspace
19614 .update(cx, |workspace, window, cx| {
19615 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
19616 })
19617 .unwrap();
19618 cx.read(|cx| {
19619 assert!(editor.read(cx).mouse_context_menu.is_none());
19620 });
19621}
19622
19623#[gpui::test]
19624async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
19625 init_test(cx, |_| {});
19626
19627 let fs = FakeFs::new(cx.executor());
19628 fs.insert_file(path!("/file.html"), Default::default())
19629 .await;
19630
19631 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
19632
19633 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19634 let html_language = Arc::new(Language::new(
19635 LanguageConfig {
19636 name: "HTML".into(),
19637 matcher: LanguageMatcher {
19638 path_suffixes: vec!["html".to_string()],
19639 ..LanguageMatcher::default()
19640 },
19641 brackets: BracketPairConfig {
19642 pairs: vec![BracketPair {
19643 start: "<".into(),
19644 end: ">".into(),
19645 close: true,
19646 ..Default::default()
19647 }],
19648 ..Default::default()
19649 },
19650 ..Default::default()
19651 },
19652 Some(tree_sitter_html::LANGUAGE.into()),
19653 ));
19654 language_registry.add(html_language);
19655 let mut fake_servers = language_registry.register_fake_lsp(
19656 "HTML",
19657 FakeLspAdapter {
19658 capabilities: lsp::ServerCapabilities {
19659 completion_provider: Some(lsp::CompletionOptions {
19660 resolve_provider: Some(true),
19661 ..Default::default()
19662 }),
19663 ..Default::default()
19664 },
19665 ..Default::default()
19666 },
19667 );
19668
19669 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19670 let cx = &mut VisualTestContext::from_window(*workspace, cx);
19671
19672 let worktree_id = workspace
19673 .update(cx, |workspace, _window, cx| {
19674 workspace.project().update(cx, |project, cx| {
19675 project.worktrees(cx).next().unwrap().read(cx).id()
19676 })
19677 })
19678 .unwrap();
19679 project
19680 .update(cx, |project, cx| {
19681 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
19682 })
19683 .await
19684 .unwrap();
19685 let editor = workspace
19686 .update(cx, |workspace, window, cx| {
19687 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
19688 })
19689 .unwrap()
19690 .await
19691 .unwrap()
19692 .downcast::<Editor>()
19693 .unwrap();
19694
19695 let fake_server = fake_servers.next().await.unwrap();
19696 editor.update_in(cx, |editor, window, cx| {
19697 editor.set_text("<ad></ad>", window, cx);
19698 editor.change_selections(None, window, cx, |selections| {
19699 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
19700 });
19701 let Some((buffer, _)) = editor
19702 .buffer
19703 .read(cx)
19704 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
19705 else {
19706 panic!("Failed to get buffer for selection position");
19707 };
19708 let buffer = buffer.read(cx);
19709 let buffer_id = buffer.remote_id();
19710 let opening_range =
19711 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
19712 let closing_range =
19713 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
19714 let mut linked_ranges = HashMap::default();
19715 linked_ranges.insert(
19716 buffer_id,
19717 vec![(opening_range.clone(), vec![closing_range.clone()])],
19718 );
19719 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
19720 });
19721 let mut completion_handle =
19722 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
19723 Ok(Some(lsp::CompletionResponse::Array(vec![
19724 lsp::CompletionItem {
19725 label: "head".to_string(),
19726 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
19727 lsp::InsertReplaceEdit {
19728 new_text: "head".to_string(),
19729 insert: lsp::Range::new(
19730 lsp::Position::new(0, 1),
19731 lsp::Position::new(0, 3),
19732 ),
19733 replace: lsp::Range::new(
19734 lsp::Position::new(0, 1),
19735 lsp::Position::new(0, 3),
19736 ),
19737 },
19738 )),
19739 ..Default::default()
19740 },
19741 ])))
19742 });
19743 editor.update_in(cx, |editor, window, cx| {
19744 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
19745 });
19746 cx.run_until_parked();
19747 completion_handle.next().await.unwrap();
19748 editor.update(cx, |editor, _| {
19749 assert!(
19750 editor.context_menu_visible(),
19751 "Completion menu should be visible"
19752 );
19753 });
19754 editor.update_in(cx, |editor, window, cx| {
19755 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
19756 });
19757 cx.executor().run_until_parked();
19758 editor.update(cx, |editor, cx| {
19759 assert_eq!(editor.text(cx), "<head></head>");
19760 });
19761}
19762
19763fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
19764 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
19765 point..point
19766}
19767
19768fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
19769 let (text, ranges) = marked_text_ranges(marked_text, true);
19770 assert_eq!(editor.text(cx), text);
19771 assert_eq!(
19772 editor.selections.ranges(cx),
19773 ranges,
19774 "Assert selections are {}",
19775 marked_text
19776 );
19777}
19778
19779pub fn handle_signature_help_request(
19780 cx: &mut EditorLspTestContext,
19781 mocked_response: lsp::SignatureHelp,
19782) -> impl Future<Output = ()> + use<> {
19783 let mut request =
19784 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
19785 let mocked_response = mocked_response.clone();
19786 async move { Ok(Some(mocked_response)) }
19787 });
19788
19789 async move {
19790 request.next().await;
19791 }
19792}
19793
19794/// Handle completion request passing a marked string specifying where the completion
19795/// should be triggered from using '|' character, what range should be replaced, and what completions
19796/// should be returned using '<' and '>' to delimit the range.
19797///
19798/// Also see `handle_completion_request_with_insert_and_replace`.
19799#[track_caller]
19800pub fn handle_completion_request(
19801 cx: &mut EditorLspTestContext,
19802 marked_string: &str,
19803 completions: Vec<&'static str>,
19804 counter: Arc<AtomicUsize>,
19805) -> impl Future<Output = ()> {
19806 let complete_from_marker: TextRangeMarker = '|'.into();
19807 let replace_range_marker: TextRangeMarker = ('<', '>').into();
19808 let (_, mut marked_ranges) = marked_text_ranges_by(
19809 marked_string,
19810 vec![complete_from_marker.clone(), replace_range_marker.clone()],
19811 );
19812
19813 let complete_from_position =
19814 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
19815 let replace_range =
19816 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
19817
19818 let mut request =
19819 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
19820 let completions = completions.clone();
19821 counter.fetch_add(1, atomic::Ordering::Release);
19822 async move {
19823 assert_eq!(params.text_document_position.text_document.uri, url.clone());
19824 assert_eq!(
19825 params.text_document_position.position,
19826 complete_from_position
19827 );
19828 Ok(Some(lsp::CompletionResponse::Array(
19829 completions
19830 .iter()
19831 .map(|completion_text| lsp::CompletionItem {
19832 label: completion_text.to_string(),
19833 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
19834 range: replace_range,
19835 new_text: completion_text.to_string(),
19836 })),
19837 ..Default::default()
19838 })
19839 .collect(),
19840 )))
19841 }
19842 });
19843
19844 async move {
19845 request.next().await;
19846 }
19847}
19848
19849/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
19850/// given instead, which also contains an `insert` range.
19851///
19852/// This function uses the cursor position to mimic what Rust-Analyzer provides as the `insert` range,
19853/// that is, `replace_range.start..cursor_pos`.
19854pub fn handle_completion_request_with_insert_and_replace(
19855 cx: &mut EditorLspTestContext,
19856 marked_string: &str,
19857 completions: Vec<&'static str>,
19858 counter: Arc<AtomicUsize>,
19859) -> impl Future<Output = ()> {
19860 let complete_from_marker: TextRangeMarker = '|'.into();
19861 let replace_range_marker: TextRangeMarker = ('<', '>').into();
19862 let (_, mut marked_ranges) = marked_text_ranges_by(
19863 marked_string,
19864 vec![complete_from_marker.clone(), replace_range_marker.clone()],
19865 );
19866
19867 let complete_from_position =
19868 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
19869 let replace_range =
19870 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
19871
19872 let mut request =
19873 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
19874 let completions = completions.clone();
19875 counter.fetch_add(1, atomic::Ordering::Release);
19876 async move {
19877 assert_eq!(params.text_document_position.text_document.uri, url.clone());
19878 assert_eq!(
19879 params.text_document_position.position, complete_from_position,
19880 "marker `|` position doesn't match",
19881 );
19882 Ok(Some(lsp::CompletionResponse::Array(
19883 completions
19884 .iter()
19885 .map(|completion_text| lsp::CompletionItem {
19886 label: completion_text.to_string(),
19887 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
19888 lsp::InsertReplaceEdit {
19889 insert: lsp::Range {
19890 start: replace_range.start,
19891 end: complete_from_position,
19892 },
19893 replace: replace_range,
19894 new_text: completion_text.to_string(),
19895 },
19896 )),
19897 ..Default::default()
19898 })
19899 .collect(),
19900 )))
19901 }
19902 });
19903
19904 async move {
19905 request.next().await;
19906 }
19907}
19908
19909fn handle_resolve_completion_request(
19910 cx: &mut EditorLspTestContext,
19911 edits: Option<Vec<(&'static str, &'static str)>>,
19912) -> impl Future<Output = ()> {
19913 let edits = edits.map(|edits| {
19914 edits
19915 .iter()
19916 .map(|(marked_string, new_text)| {
19917 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
19918 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
19919 lsp::TextEdit::new(replace_range, new_text.to_string())
19920 })
19921 .collect::<Vec<_>>()
19922 });
19923
19924 let mut request =
19925 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
19926 let edits = edits.clone();
19927 async move {
19928 Ok(lsp::CompletionItem {
19929 additional_text_edits: edits,
19930 ..Default::default()
19931 })
19932 }
19933 });
19934
19935 async move {
19936 request.next().await;
19937 }
19938}
19939
19940pub(crate) fn update_test_language_settings(
19941 cx: &mut TestAppContext,
19942 f: impl Fn(&mut AllLanguageSettingsContent),
19943) {
19944 cx.update(|cx| {
19945 SettingsStore::update_global(cx, |store, cx| {
19946 store.update_user_settings::<AllLanguageSettings>(cx, f);
19947 });
19948 });
19949}
19950
19951pub(crate) fn update_test_project_settings(
19952 cx: &mut TestAppContext,
19953 f: impl Fn(&mut ProjectSettings),
19954) {
19955 cx.update(|cx| {
19956 SettingsStore::update_global(cx, |store, cx| {
19957 store.update_user_settings::<ProjectSettings>(cx, f);
19958 });
19959 });
19960}
19961
19962pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
19963 cx.update(|cx| {
19964 assets::Assets.load_test_fonts(cx);
19965 let store = SettingsStore::test(cx);
19966 cx.set_global(store);
19967 theme::init(theme::LoadThemes::JustBase, cx);
19968 release_channel::init(SemanticVersion::default(), cx);
19969 client::init_settings(cx);
19970 language::init(cx);
19971 Project::init_settings(cx);
19972 workspace::init_settings(cx);
19973 crate::init(cx);
19974 });
19975
19976 update_test_language_settings(cx, f);
19977}
19978
19979#[track_caller]
19980fn assert_hunk_revert(
19981 not_reverted_text_with_selections: &str,
19982 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
19983 expected_reverted_text_with_selections: &str,
19984 base_text: &str,
19985 cx: &mut EditorLspTestContext,
19986) {
19987 cx.set_state(not_reverted_text_with_selections);
19988 cx.set_head_text(base_text);
19989 cx.executor().run_until_parked();
19990
19991 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
19992 let snapshot = editor.snapshot(window, cx);
19993 let reverted_hunk_statuses = snapshot
19994 .buffer_snapshot
19995 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
19996 .map(|hunk| hunk.status().kind)
19997 .collect::<Vec<_>>();
19998
19999 editor.git_restore(&Default::default(), window, cx);
20000 reverted_hunk_statuses
20001 });
20002 cx.executor().run_until_parked();
20003 cx.assert_editor_state(expected_reverted_text_with_selections);
20004 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
20005}