1use super::*;
2use crate::{
3 JoinLines,
4 linked_editing_ranges::LinkedEditingRanges,
5 scroll::scroll_amount::ScrollAmount,
6 test::{
7 assert_text_with_selections, build_editor,
8 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
9 editor_test_context::EditorTestContext,
10 select_ranges,
11 },
12};
13use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
14use futures::StreamExt;
15use gpui::{
16 BackgroundExecutor, DismissEvent, SemanticVersion, TestAppContext, UpdateGlobal,
17 VisualTestContext, WindowBounds, WindowOptions, div,
18};
19use indoc::indoc;
20use language::{
21 BracketPairConfig,
22 Capability::ReadWrite,
23 FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName,
24 Override, Point,
25 language_settings::{
26 AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
27 LanguageSettingsContent, LspInsertMode, PrettierSettings,
28 },
29};
30use language_settings::{Formatter, FormatterList, IndentGuideSettings};
31use lsp::CompletionParams;
32use multi_buffer::{IndentGuide, PathKey};
33use parking_lot::Mutex;
34use pretty_assertions::{assert_eq, assert_ne};
35use project::{
36 FakeFs,
37 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
38 project_settings::{LspSettings, ProjectSettings},
39};
40use serde_json::{self, json};
41use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
42use std::{
43 iter,
44 sync::atomic::{self, AtomicUsize},
45};
46use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
47use text::ToPoint as _;
48use unindent::Unindent;
49use util::{
50 assert_set_eq, path,
51 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
52 uri,
53};
54use workspace::{
55 CloseAllItems, CloseInactiveItems, NavigationEntry, ViewId,
56 item::{FollowEvent, FollowableItem, Item, ItemHandle},
57};
58
59#[gpui::test]
60fn test_edit_events(cx: &mut TestAppContext) {
61 init_test(cx, |_| {});
62
63 let buffer = cx.new(|cx| {
64 let mut buffer = language::Buffer::local("123456", cx);
65 buffer.set_group_interval(Duration::from_secs(1));
66 buffer
67 });
68
69 let events = Rc::new(RefCell::new(Vec::new()));
70 let editor1 = cx.add_window({
71 let events = events.clone();
72 |window, cx| {
73 let entity = cx.entity().clone();
74 cx.subscribe_in(
75 &entity,
76 window,
77 move |_, _, event: &EditorEvent, _, _| match event {
78 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
79 EditorEvent::BufferEdited => {
80 events.borrow_mut().push(("editor1", "buffer edited"))
81 }
82 _ => {}
83 },
84 )
85 .detach();
86 Editor::for_buffer(buffer.clone(), None, window, cx)
87 }
88 });
89
90 let editor2 = cx.add_window({
91 let events = events.clone();
92 |window, cx| {
93 cx.subscribe_in(
94 &cx.entity().clone(),
95 window,
96 move |_, _, event: &EditorEvent, _, _| match event {
97 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
98 EditorEvent::BufferEdited => {
99 events.borrow_mut().push(("editor2", "buffer edited"))
100 }
101 _ => {}
102 },
103 )
104 .detach();
105 Editor::for_buffer(buffer.clone(), None, window, cx)
106 }
107 });
108
109 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
110
111 // Mutating editor 1 will emit an `Edited` event only for that editor.
112 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
113 assert_eq!(
114 mem::take(&mut *events.borrow_mut()),
115 [
116 ("editor1", "edited"),
117 ("editor1", "buffer edited"),
118 ("editor2", "buffer edited"),
119 ]
120 );
121
122 // Mutating editor 2 will emit an `Edited` event only for that editor.
123 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
124 assert_eq!(
125 mem::take(&mut *events.borrow_mut()),
126 [
127 ("editor2", "edited"),
128 ("editor1", "buffer edited"),
129 ("editor2", "buffer edited"),
130 ]
131 );
132
133 // Undoing on editor 1 will emit an `Edited` event only for that editor.
134 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
135 assert_eq!(
136 mem::take(&mut *events.borrow_mut()),
137 [
138 ("editor1", "edited"),
139 ("editor1", "buffer edited"),
140 ("editor2", "buffer edited"),
141 ]
142 );
143
144 // Redoing on editor 1 will emit an `Edited` event only for that editor.
145 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
146 assert_eq!(
147 mem::take(&mut *events.borrow_mut()),
148 [
149 ("editor1", "edited"),
150 ("editor1", "buffer edited"),
151 ("editor2", "buffer edited"),
152 ]
153 );
154
155 // Undoing on editor 2 will emit an `Edited` event only for that editor.
156 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
157 assert_eq!(
158 mem::take(&mut *events.borrow_mut()),
159 [
160 ("editor2", "edited"),
161 ("editor1", "buffer edited"),
162 ("editor2", "buffer edited"),
163 ]
164 );
165
166 // Redoing on editor 2 will emit an `Edited` event only for that editor.
167 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
168 assert_eq!(
169 mem::take(&mut *events.borrow_mut()),
170 [
171 ("editor2", "edited"),
172 ("editor1", "buffer edited"),
173 ("editor2", "buffer edited"),
174 ]
175 );
176
177 // No event is emitted when the mutation is a no-op.
178 _ = editor2.update(cx, |editor, window, cx| {
179 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
180
181 editor.backspace(&Backspace, window, cx);
182 });
183 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
184}
185
186#[gpui::test]
187fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
188 init_test(cx, |_| {});
189
190 let mut now = Instant::now();
191 let group_interval = Duration::from_millis(1);
192 let buffer = cx.new(|cx| {
193 let mut buf = language::Buffer::local("123456", cx);
194 buf.set_group_interval(group_interval);
195 buf
196 });
197 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
198 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
199
200 _ = editor.update(cx, |editor, window, cx| {
201 editor.start_transaction_at(now, window, cx);
202 editor.change_selections(None, window, cx, |s| s.select_ranges([2..4]));
203
204 editor.insert("cd", window, cx);
205 editor.end_transaction_at(now, cx);
206 assert_eq!(editor.text(cx), "12cd56");
207 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
208
209 editor.start_transaction_at(now, window, cx);
210 editor.change_selections(None, window, cx, |s| s.select_ranges([4..5]));
211 editor.insert("e", window, cx);
212 editor.end_transaction_at(now, cx);
213 assert_eq!(editor.text(cx), "12cde6");
214 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
215
216 now += group_interval + Duration::from_millis(1);
217 editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]));
218
219 // Simulate an edit in another editor
220 buffer.update(cx, |buffer, cx| {
221 buffer.start_transaction_at(now, cx);
222 buffer.edit([(0..1, "a")], None, cx);
223 buffer.edit([(1..1, "b")], None, cx);
224 buffer.end_transaction_at(now, cx);
225 });
226
227 assert_eq!(editor.text(cx), "ab2cde6");
228 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
229
230 // Last transaction happened past the group interval in a different editor.
231 // Undo it individually and don't restore selections.
232 editor.undo(&Undo, window, cx);
233 assert_eq!(editor.text(cx), "12cde6");
234 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
235
236 // First two transactions happened within the group interval in this editor.
237 // Undo them together and restore selections.
238 editor.undo(&Undo, window, cx);
239 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
240 assert_eq!(editor.text(cx), "123456");
241 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
242
243 // Redo the first two transactions together.
244 editor.redo(&Redo, window, cx);
245 assert_eq!(editor.text(cx), "12cde6");
246 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
247
248 // Redo the last transaction on its own.
249 editor.redo(&Redo, window, cx);
250 assert_eq!(editor.text(cx), "ab2cde6");
251 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
252
253 // Test empty transactions.
254 editor.start_transaction_at(now, window, cx);
255 editor.end_transaction_at(now, cx);
256 editor.undo(&Undo, window, cx);
257 assert_eq!(editor.text(cx), "12cde6");
258 });
259}
260
261#[gpui::test]
262fn test_ime_composition(cx: &mut TestAppContext) {
263 init_test(cx, |_| {});
264
265 let buffer = cx.new(|cx| {
266 let mut buffer = language::Buffer::local("abcde", cx);
267 // Ensure automatic grouping doesn't occur.
268 buffer.set_group_interval(Duration::ZERO);
269 buffer
270 });
271
272 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
273 cx.add_window(|window, cx| {
274 let mut editor = build_editor(buffer.clone(), window, cx);
275
276 // Start a new IME composition.
277 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
278 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
279 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
280 assert_eq!(editor.text(cx), "äbcde");
281 assert_eq!(
282 editor.marked_text_ranges(cx),
283 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
284 );
285
286 // Finalize IME composition.
287 editor.replace_text_in_range(None, "ā", window, cx);
288 assert_eq!(editor.text(cx), "ābcde");
289 assert_eq!(editor.marked_text_ranges(cx), None);
290
291 // IME composition edits are grouped and are undone/redone at once.
292 editor.undo(&Default::default(), window, cx);
293 assert_eq!(editor.text(cx), "abcde");
294 assert_eq!(editor.marked_text_ranges(cx), None);
295 editor.redo(&Default::default(), window, cx);
296 assert_eq!(editor.text(cx), "ābcde");
297 assert_eq!(editor.marked_text_ranges(cx), None);
298
299 // Start a new IME composition.
300 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
301 assert_eq!(
302 editor.marked_text_ranges(cx),
303 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
304 );
305
306 // Undoing during an IME composition cancels it.
307 editor.undo(&Default::default(), window, cx);
308 assert_eq!(editor.text(cx), "ābcde");
309 assert_eq!(editor.marked_text_ranges(cx), None);
310
311 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
312 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
313 assert_eq!(editor.text(cx), "ābcdè");
314 assert_eq!(
315 editor.marked_text_ranges(cx),
316 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
317 );
318
319 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
320 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
321 assert_eq!(editor.text(cx), "ābcdę");
322 assert_eq!(editor.marked_text_ranges(cx), None);
323
324 // Start a new IME composition with multiple cursors.
325 editor.change_selections(None, window, cx, |s| {
326 s.select_ranges([
327 OffsetUtf16(1)..OffsetUtf16(1),
328 OffsetUtf16(3)..OffsetUtf16(3),
329 OffsetUtf16(5)..OffsetUtf16(5),
330 ])
331 });
332 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
333 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
334 assert_eq!(
335 editor.marked_text_ranges(cx),
336 Some(vec![
337 OffsetUtf16(0)..OffsetUtf16(3),
338 OffsetUtf16(4)..OffsetUtf16(7),
339 OffsetUtf16(8)..OffsetUtf16(11)
340 ])
341 );
342
343 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
344 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
345 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
346 assert_eq!(
347 editor.marked_text_ranges(cx),
348 Some(vec![
349 OffsetUtf16(1)..OffsetUtf16(2),
350 OffsetUtf16(5)..OffsetUtf16(6),
351 OffsetUtf16(9)..OffsetUtf16(10)
352 ])
353 );
354
355 // Finalize IME composition with multiple cursors.
356 editor.replace_text_in_range(Some(9..10), "2", window, cx);
357 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
358 assert_eq!(editor.marked_text_ranges(cx), None);
359
360 editor
361 });
362}
363
364#[gpui::test]
365fn test_selection_with_mouse(cx: &mut TestAppContext) {
366 init_test(cx, |_| {});
367
368 let editor = cx.add_window(|window, cx| {
369 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
370 build_editor(buffer, window, cx)
371 });
372
373 _ = editor.update(cx, |editor, window, cx| {
374 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
375 });
376 assert_eq!(
377 editor
378 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
379 .unwrap(),
380 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
381 );
382
383 _ = editor.update(cx, |editor, window, cx| {
384 editor.update_selection(
385 DisplayPoint::new(DisplayRow(3), 3),
386 0,
387 gpui::Point::<f32>::default(),
388 window,
389 cx,
390 );
391 });
392
393 assert_eq!(
394 editor
395 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
396 .unwrap(),
397 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
398 );
399
400 _ = editor.update(cx, |editor, window, cx| {
401 editor.update_selection(
402 DisplayPoint::new(DisplayRow(1), 1),
403 0,
404 gpui::Point::<f32>::default(),
405 window,
406 cx,
407 );
408 });
409
410 assert_eq!(
411 editor
412 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
413 .unwrap(),
414 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
415 );
416
417 _ = editor.update(cx, |editor, window, cx| {
418 editor.end_selection(window, cx);
419 editor.update_selection(
420 DisplayPoint::new(DisplayRow(3), 3),
421 0,
422 gpui::Point::<f32>::default(),
423 window,
424 cx,
425 );
426 });
427
428 assert_eq!(
429 editor
430 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
431 .unwrap(),
432 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
433 );
434
435 _ = editor.update(cx, |editor, window, cx| {
436 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
437 editor.update_selection(
438 DisplayPoint::new(DisplayRow(0), 0),
439 0,
440 gpui::Point::<f32>::default(),
441 window,
442 cx,
443 );
444 });
445
446 assert_eq!(
447 editor
448 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
449 .unwrap(),
450 [
451 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
452 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
453 ]
454 );
455
456 _ = editor.update(cx, |editor, window, cx| {
457 editor.end_selection(window, cx);
458 });
459
460 assert_eq!(
461 editor
462 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
463 .unwrap(),
464 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
465 );
466}
467
468#[gpui::test]
469fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
470 init_test(cx, |_| {});
471
472 let editor = cx.add_window(|window, cx| {
473 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
474 build_editor(buffer, window, cx)
475 });
476
477 _ = editor.update(cx, |editor, window, cx| {
478 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
479 });
480
481 _ = editor.update(cx, |editor, window, cx| {
482 editor.end_selection(window, cx);
483 });
484
485 _ = editor.update(cx, |editor, window, cx| {
486 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
487 });
488
489 _ = editor.update(cx, |editor, window, cx| {
490 editor.end_selection(window, cx);
491 });
492
493 assert_eq!(
494 editor
495 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
496 .unwrap(),
497 [
498 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
499 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
500 ]
501 );
502
503 _ = editor.update(cx, |editor, window, cx| {
504 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
505 });
506
507 _ = editor.update(cx, |editor, window, cx| {
508 editor.end_selection(window, cx);
509 });
510
511 assert_eq!(
512 editor
513 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
514 .unwrap(),
515 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
516 );
517}
518
519#[gpui::test]
520fn test_canceling_pending_selection(cx: &mut TestAppContext) {
521 init_test(cx, |_| {});
522
523 let editor = cx.add_window(|window, cx| {
524 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
525 build_editor(buffer, window, cx)
526 });
527
528 _ = editor.update(cx, |editor, window, cx| {
529 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
530 assert_eq!(
531 editor.selections.display_ranges(cx),
532 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
533 );
534 });
535
536 _ = editor.update(cx, |editor, window, cx| {
537 editor.update_selection(
538 DisplayPoint::new(DisplayRow(3), 3),
539 0,
540 gpui::Point::<f32>::default(),
541 window,
542 cx,
543 );
544 assert_eq!(
545 editor.selections.display_ranges(cx),
546 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
547 );
548 });
549
550 _ = editor.update(cx, |editor, window, cx| {
551 editor.cancel(&Cancel, window, cx);
552 editor.update_selection(
553 DisplayPoint::new(DisplayRow(1), 1),
554 0,
555 gpui::Point::<f32>::default(),
556 window,
557 cx,
558 );
559 assert_eq!(
560 editor.selections.display_ranges(cx),
561 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
562 );
563 });
564}
565
566#[gpui::test]
567fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
568 init_test(cx, |_| {});
569
570 let editor = cx.add_window(|window, cx| {
571 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
572 build_editor(buffer, window, cx)
573 });
574
575 _ = editor.update(cx, |editor, window, cx| {
576 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
577 assert_eq!(
578 editor.selections.display_ranges(cx),
579 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
580 );
581
582 editor.move_down(&Default::default(), window, cx);
583 assert_eq!(
584 editor.selections.display_ranges(cx),
585 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
586 );
587
588 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
589 assert_eq!(
590 editor.selections.display_ranges(cx),
591 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
592 );
593
594 editor.move_up(&Default::default(), window, cx);
595 assert_eq!(
596 editor.selections.display_ranges(cx),
597 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
598 );
599 });
600}
601
602#[gpui::test]
603fn test_clone(cx: &mut TestAppContext) {
604 init_test(cx, |_| {});
605
606 let (text, selection_ranges) = marked_text_ranges(
607 indoc! {"
608 one
609 two
610 threeˇ
611 four
612 fiveˇ
613 "},
614 true,
615 );
616
617 let editor = cx.add_window(|window, cx| {
618 let buffer = MultiBuffer::build_simple(&text, cx);
619 build_editor(buffer, window, cx)
620 });
621
622 _ = editor.update(cx, |editor, window, cx| {
623 editor.change_selections(None, window, cx, |s| {
624 s.select_ranges(selection_ranges.clone())
625 });
626 editor.fold_creases(
627 vec![
628 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
629 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
630 ],
631 true,
632 window,
633 cx,
634 );
635 });
636
637 let cloned_editor = editor
638 .update(cx, |editor, _, cx| {
639 cx.open_window(Default::default(), |window, cx| {
640 cx.new(|cx| editor.clone(window, cx))
641 })
642 })
643 .unwrap()
644 .unwrap();
645
646 let snapshot = editor
647 .update(cx, |e, window, cx| e.snapshot(window, cx))
648 .unwrap();
649 let cloned_snapshot = cloned_editor
650 .update(cx, |e, window, cx| e.snapshot(window, cx))
651 .unwrap();
652
653 assert_eq!(
654 cloned_editor
655 .update(cx, |e, _, cx| e.display_text(cx))
656 .unwrap(),
657 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
658 );
659 assert_eq!(
660 cloned_snapshot
661 .folds_in_range(0..text.len())
662 .collect::<Vec<_>>(),
663 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
664 );
665 assert_set_eq!(
666 cloned_editor
667 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
668 .unwrap(),
669 editor
670 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
671 .unwrap()
672 );
673 assert_set_eq!(
674 cloned_editor
675 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
676 .unwrap(),
677 editor
678 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
679 .unwrap()
680 );
681}
682
683#[gpui::test]
684async fn test_navigation_history(cx: &mut TestAppContext) {
685 init_test(cx, |_| {});
686
687 use workspace::item::Item;
688
689 let fs = FakeFs::new(cx.executor());
690 let project = Project::test(fs, [], cx).await;
691 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
692 let pane = workspace
693 .update(cx, |workspace, _, _| workspace.active_pane().clone())
694 .unwrap();
695
696 _ = workspace.update(cx, |_v, window, cx| {
697 cx.new(|cx| {
698 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
699 let mut editor = build_editor(buffer.clone(), window, cx);
700 let handle = cx.entity();
701 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
702
703 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
704 editor.nav_history.as_mut().unwrap().pop_backward(cx)
705 }
706
707 // Move the cursor a small distance.
708 // Nothing is added to the navigation history.
709 editor.change_selections(None, window, cx, |s| {
710 s.select_display_ranges([
711 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
712 ])
713 });
714 editor.change_selections(None, window, cx, |s| {
715 s.select_display_ranges([
716 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
717 ])
718 });
719 assert!(pop_history(&mut editor, cx).is_none());
720
721 // Move the cursor a large distance.
722 // The history can jump back to the previous position.
723 editor.change_selections(None, window, cx, |s| {
724 s.select_display_ranges([
725 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
726 ])
727 });
728 let nav_entry = pop_history(&mut editor, cx).unwrap();
729 editor.navigate(nav_entry.data.unwrap(), window, cx);
730 assert_eq!(nav_entry.item.id(), cx.entity_id());
731 assert_eq!(
732 editor.selections.display_ranges(cx),
733 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
734 );
735 assert!(pop_history(&mut editor, cx).is_none());
736
737 // Move the cursor a small distance via the mouse.
738 // Nothing is added to the navigation history.
739 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
740 editor.end_selection(window, cx);
741 assert_eq!(
742 editor.selections.display_ranges(cx),
743 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
744 );
745 assert!(pop_history(&mut editor, cx).is_none());
746
747 // Move the cursor a large distance via the mouse.
748 // The history can jump back to the previous position.
749 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
750 editor.end_selection(window, cx);
751 assert_eq!(
752 editor.selections.display_ranges(cx),
753 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
754 );
755 let nav_entry = pop_history(&mut editor, cx).unwrap();
756 editor.navigate(nav_entry.data.unwrap(), window, cx);
757 assert_eq!(nav_entry.item.id(), cx.entity_id());
758 assert_eq!(
759 editor.selections.display_ranges(cx),
760 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
761 );
762 assert!(pop_history(&mut editor, cx).is_none());
763
764 // Set scroll position to check later
765 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
766 let original_scroll_position = editor.scroll_manager.anchor();
767
768 // Jump to the end of the document and adjust scroll
769 editor.move_to_end(&MoveToEnd, window, cx);
770 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
771 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
772
773 let nav_entry = pop_history(&mut editor, cx).unwrap();
774 editor.navigate(nav_entry.data.unwrap(), window, cx);
775 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
776
777 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
778 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
779 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
780 let invalid_point = Point::new(9999, 0);
781 editor.navigate(
782 Box::new(NavigationData {
783 cursor_anchor: invalid_anchor,
784 cursor_position: invalid_point,
785 scroll_anchor: ScrollAnchor {
786 anchor: invalid_anchor,
787 offset: Default::default(),
788 },
789 scroll_top_row: invalid_point.row,
790 }),
791 window,
792 cx,
793 );
794 assert_eq!(
795 editor.selections.display_ranges(cx),
796 &[editor.max_point(cx)..editor.max_point(cx)]
797 );
798 assert_eq!(
799 editor.scroll_position(cx),
800 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
801 );
802
803 editor
804 })
805 });
806}
807
808#[gpui::test]
809fn test_cancel(cx: &mut TestAppContext) {
810 init_test(cx, |_| {});
811
812 let editor = cx.add_window(|window, cx| {
813 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
814 build_editor(buffer, window, cx)
815 });
816
817 _ = editor.update(cx, |editor, window, cx| {
818 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
819 editor.update_selection(
820 DisplayPoint::new(DisplayRow(1), 1),
821 0,
822 gpui::Point::<f32>::default(),
823 window,
824 cx,
825 );
826 editor.end_selection(window, cx);
827
828 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
829 editor.update_selection(
830 DisplayPoint::new(DisplayRow(0), 3),
831 0,
832 gpui::Point::<f32>::default(),
833 window,
834 cx,
835 );
836 editor.end_selection(window, cx);
837 assert_eq!(
838 editor.selections.display_ranges(cx),
839 [
840 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
841 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
842 ]
843 );
844 });
845
846 _ = editor.update(cx, |editor, window, cx| {
847 editor.cancel(&Cancel, window, cx);
848 assert_eq!(
849 editor.selections.display_ranges(cx),
850 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
851 );
852 });
853
854 _ = editor.update(cx, |editor, window, cx| {
855 editor.cancel(&Cancel, window, cx);
856 assert_eq!(
857 editor.selections.display_ranges(cx),
858 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
859 );
860 });
861}
862
863#[gpui::test]
864fn test_fold_action(cx: &mut TestAppContext) {
865 init_test(cx, |_| {});
866
867 let editor = cx.add_window(|window, cx| {
868 let buffer = MultiBuffer::build_simple(
869 &"
870 impl Foo {
871 // Hello!
872
873 fn a() {
874 1
875 }
876
877 fn b() {
878 2
879 }
880
881 fn c() {
882 3
883 }
884 }
885 "
886 .unindent(),
887 cx,
888 );
889 build_editor(buffer.clone(), window, cx)
890 });
891
892 _ = editor.update(cx, |editor, window, cx| {
893 editor.change_selections(None, window, cx, |s| {
894 s.select_display_ranges([
895 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
896 ]);
897 });
898 editor.fold(&Fold, window, cx);
899 assert_eq!(
900 editor.display_text(cx),
901 "
902 impl Foo {
903 // Hello!
904
905 fn a() {
906 1
907 }
908
909 fn b() {⋯
910 }
911
912 fn c() {⋯
913 }
914 }
915 "
916 .unindent(),
917 );
918
919 editor.fold(&Fold, window, cx);
920 assert_eq!(
921 editor.display_text(cx),
922 "
923 impl Foo {⋯
924 }
925 "
926 .unindent(),
927 );
928
929 editor.unfold_lines(&UnfoldLines, window, cx);
930 assert_eq!(
931 editor.display_text(cx),
932 "
933 impl Foo {
934 // Hello!
935
936 fn a() {
937 1
938 }
939
940 fn b() {⋯
941 }
942
943 fn c() {⋯
944 }
945 }
946 "
947 .unindent(),
948 );
949
950 editor.unfold_lines(&UnfoldLines, window, cx);
951 assert_eq!(
952 editor.display_text(cx),
953 editor.buffer.read(cx).read(cx).text()
954 );
955 });
956}
957
958#[gpui::test]
959fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
960 init_test(cx, |_| {});
961
962 let editor = cx.add_window(|window, cx| {
963 let buffer = MultiBuffer::build_simple(
964 &"
965 class Foo:
966 # Hello!
967
968 def a():
969 print(1)
970
971 def b():
972 print(2)
973
974 def c():
975 print(3)
976 "
977 .unindent(),
978 cx,
979 );
980 build_editor(buffer.clone(), window, cx)
981 });
982
983 _ = editor.update(cx, |editor, window, cx| {
984 editor.change_selections(None, window, cx, |s| {
985 s.select_display_ranges([
986 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
987 ]);
988 });
989 editor.fold(&Fold, window, cx);
990 assert_eq!(
991 editor.display_text(cx),
992 "
993 class Foo:
994 # Hello!
995
996 def a():
997 print(1)
998
999 def b():⋯
1000
1001 def c():⋯
1002 "
1003 .unindent(),
1004 );
1005
1006 editor.fold(&Fold, window, cx);
1007 assert_eq!(
1008 editor.display_text(cx),
1009 "
1010 class Foo:⋯
1011 "
1012 .unindent(),
1013 );
1014
1015 editor.unfold_lines(&UnfoldLines, window, cx);
1016 assert_eq!(
1017 editor.display_text(cx),
1018 "
1019 class Foo:
1020 # Hello!
1021
1022 def a():
1023 print(1)
1024
1025 def b():⋯
1026
1027 def c():⋯
1028 "
1029 .unindent(),
1030 );
1031
1032 editor.unfold_lines(&UnfoldLines, window, cx);
1033 assert_eq!(
1034 editor.display_text(cx),
1035 editor.buffer.read(cx).read(cx).text()
1036 );
1037 });
1038}
1039
1040#[gpui::test]
1041fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1042 init_test(cx, |_| {});
1043
1044 let editor = cx.add_window(|window, cx| {
1045 let buffer = MultiBuffer::build_simple(
1046 &"
1047 class Foo:
1048 # Hello!
1049
1050 def a():
1051 print(1)
1052
1053 def b():
1054 print(2)
1055
1056
1057 def c():
1058 print(3)
1059
1060
1061 "
1062 .unindent(),
1063 cx,
1064 );
1065 build_editor(buffer.clone(), window, cx)
1066 });
1067
1068 _ = editor.update(cx, |editor, window, cx| {
1069 editor.change_selections(None, window, cx, |s| {
1070 s.select_display_ranges([
1071 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1072 ]);
1073 });
1074 editor.fold(&Fold, window, cx);
1075 assert_eq!(
1076 editor.display_text(cx),
1077 "
1078 class Foo:
1079 # Hello!
1080
1081 def a():
1082 print(1)
1083
1084 def b():⋯
1085
1086
1087 def c():⋯
1088
1089
1090 "
1091 .unindent(),
1092 );
1093
1094 editor.fold(&Fold, window, cx);
1095 assert_eq!(
1096 editor.display_text(cx),
1097 "
1098 class Foo:⋯
1099
1100
1101 "
1102 .unindent(),
1103 );
1104
1105 editor.unfold_lines(&UnfoldLines, window, cx);
1106 assert_eq!(
1107 editor.display_text(cx),
1108 "
1109 class Foo:
1110 # Hello!
1111
1112 def a():
1113 print(1)
1114
1115 def b():⋯
1116
1117
1118 def c():⋯
1119
1120
1121 "
1122 .unindent(),
1123 );
1124
1125 editor.unfold_lines(&UnfoldLines, window, cx);
1126 assert_eq!(
1127 editor.display_text(cx),
1128 editor.buffer.read(cx).read(cx).text()
1129 );
1130 });
1131}
1132
1133#[gpui::test]
1134fn test_fold_at_level(cx: &mut TestAppContext) {
1135 init_test(cx, |_| {});
1136
1137 let editor = cx.add_window(|window, cx| {
1138 let buffer = MultiBuffer::build_simple(
1139 &"
1140 class Foo:
1141 # Hello!
1142
1143 def a():
1144 print(1)
1145
1146 def b():
1147 print(2)
1148
1149
1150 class Bar:
1151 # World!
1152
1153 def a():
1154 print(1)
1155
1156 def b():
1157 print(2)
1158
1159
1160 "
1161 .unindent(),
1162 cx,
1163 );
1164 build_editor(buffer.clone(), window, cx)
1165 });
1166
1167 _ = editor.update(cx, |editor, window, cx| {
1168 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1169 assert_eq!(
1170 editor.display_text(cx),
1171 "
1172 class Foo:
1173 # Hello!
1174
1175 def a():⋯
1176
1177 def b():⋯
1178
1179
1180 class Bar:
1181 # World!
1182
1183 def a():⋯
1184
1185 def b():⋯
1186
1187
1188 "
1189 .unindent(),
1190 );
1191
1192 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1193 assert_eq!(
1194 editor.display_text(cx),
1195 "
1196 class Foo:⋯
1197
1198
1199 class Bar:⋯
1200
1201
1202 "
1203 .unindent(),
1204 );
1205
1206 editor.unfold_all(&UnfoldAll, window, cx);
1207 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1208 assert_eq!(
1209 editor.display_text(cx),
1210 "
1211 class Foo:
1212 # Hello!
1213
1214 def a():
1215 print(1)
1216
1217 def b():
1218 print(2)
1219
1220
1221 class Bar:
1222 # World!
1223
1224 def a():
1225 print(1)
1226
1227 def b():
1228 print(2)
1229
1230
1231 "
1232 .unindent(),
1233 );
1234
1235 assert_eq!(
1236 editor.display_text(cx),
1237 editor.buffer.read(cx).read(cx).text()
1238 );
1239 });
1240}
1241
1242#[gpui::test]
1243fn test_move_cursor(cx: &mut TestAppContext) {
1244 init_test(cx, |_| {});
1245
1246 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1247 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1248
1249 buffer.update(cx, |buffer, cx| {
1250 buffer.edit(
1251 vec![
1252 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1253 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1254 ],
1255 None,
1256 cx,
1257 );
1258 });
1259 _ = editor.update(cx, |editor, window, cx| {
1260 assert_eq!(
1261 editor.selections.display_ranges(cx),
1262 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1263 );
1264
1265 editor.move_down(&MoveDown, window, cx);
1266 assert_eq!(
1267 editor.selections.display_ranges(cx),
1268 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1269 );
1270
1271 editor.move_right(&MoveRight, window, cx);
1272 assert_eq!(
1273 editor.selections.display_ranges(cx),
1274 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1275 );
1276
1277 editor.move_left(&MoveLeft, window, cx);
1278 assert_eq!(
1279 editor.selections.display_ranges(cx),
1280 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1281 );
1282
1283 editor.move_up(&MoveUp, window, cx);
1284 assert_eq!(
1285 editor.selections.display_ranges(cx),
1286 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1287 );
1288
1289 editor.move_to_end(&MoveToEnd, window, cx);
1290 assert_eq!(
1291 editor.selections.display_ranges(cx),
1292 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1293 );
1294
1295 editor.move_to_beginning(&MoveToBeginning, window, cx);
1296 assert_eq!(
1297 editor.selections.display_ranges(cx),
1298 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1299 );
1300
1301 editor.change_selections(None, window, cx, |s| {
1302 s.select_display_ranges([
1303 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1304 ]);
1305 });
1306 editor.select_to_beginning(&SelectToBeginning, window, cx);
1307 assert_eq!(
1308 editor.selections.display_ranges(cx),
1309 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1310 );
1311
1312 editor.select_to_end(&SelectToEnd, window, cx);
1313 assert_eq!(
1314 editor.selections.display_ranges(cx),
1315 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1316 );
1317 });
1318}
1319
1320#[gpui::test]
1321fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1322 init_test(cx, |_| {});
1323
1324 let editor = cx.add_window(|window, cx| {
1325 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1326 build_editor(buffer.clone(), window, cx)
1327 });
1328
1329 assert_eq!('🟥'.len_utf8(), 4);
1330 assert_eq!('α'.len_utf8(), 2);
1331
1332 _ = editor.update(cx, |editor, window, cx| {
1333 editor.fold_creases(
1334 vec![
1335 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1336 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1337 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1338 ],
1339 true,
1340 window,
1341 cx,
1342 );
1343 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1344
1345 editor.move_right(&MoveRight, window, cx);
1346 assert_eq!(
1347 editor.selections.display_ranges(cx),
1348 &[empty_range(0, "🟥".len())]
1349 );
1350 editor.move_right(&MoveRight, window, cx);
1351 assert_eq!(
1352 editor.selections.display_ranges(cx),
1353 &[empty_range(0, "🟥🟧".len())]
1354 );
1355 editor.move_right(&MoveRight, window, cx);
1356 assert_eq!(
1357 editor.selections.display_ranges(cx),
1358 &[empty_range(0, "🟥🟧⋯".len())]
1359 );
1360
1361 editor.move_down(&MoveDown, window, cx);
1362 assert_eq!(
1363 editor.selections.display_ranges(cx),
1364 &[empty_range(1, "ab⋯e".len())]
1365 );
1366 editor.move_left(&MoveLeft, window, cx);
1367 assert_eq!(
1368 editor.selections.display_ranges(cx),
1369 &[empty_range(1, "ab⋯".len())]
1370 );
1371 editor.move_left(&MoveLeft, window, cx);
1372 assert_eq!(
1373 editor.selections.display_ranges(cx),
1374 &[empty_range(1, "ab".len())]
1375 );
1376 editor.move_left(&MoveLeft, window, cx);
1377 assert_eq!(
1378 editor.selections.display_ranges(cx),
1379 &[empty_range(1, "a".len())]
1380 );
1381
1382 editor.move_down(&MoveDown, window, cx);
1383 assert_eq!(
1384 editor.selections.display_ranges(cx),
1385 &[empty_range(2, "α".len())]
1386 );
1387 editor.move_right(&MoveRight, window, cx);
1388 assert_eq!(
1389 editor.selections.display_ranges(cx),
1390 &[empty_range(2, "αβ".len())]
1391 );
1392 editor.move_right(&MoveRight, window, cx);
1393 assert_eq!(
1394 editor.selections.display_ranges(cx),
1395 &[empty_range(2, "αβ⋯".len())]
1396 );
1397 editor.move_right(&MoveRight, window, cx);
1398 assert_eq!(
1399 editor.selections.display_ranges(cx),
1400 &[empty_range(2, "αβ⋯ε".len())]
1401 );
1402
1403 editor.move_up(&MoveUp, window, cx);
1404 assert_eq!(
1405 editor.selections.display_ranges(cx),
1406 &[empty_range(1, "ab⋯e".len())]
1407 );
1408 editor.move_down(&MoveDown, window, cx);
1409 assert_eq!(
1410 editor.selections.display_ranges(cx),
1411 &[empty_range(2, "αβ⋯ε".len())]
1412 );
1413 editor.move_up(&MoveUp, window, cx);
1414 assert_eq!(
1415 editor.selections.display_ranges(cx),
1416 &[empty_range(1, "ab⋯e".len())]
1417 );
1418
1419 editor.move_up(&MoveUp, window, cx);
1420 assert_eq!(
1421 editor.selections.display_ranges(cx),
1422 &[empty_range(0, "🟥🟧".len())]
1423 );
1424 editor.move_left(&MoveLeft, window, cx);
1425 assert_eq!(
1426 editor.selections.display_ranges(cx),
1427 &[empty_range(0, "🟥".len())]
1428 );
1429 editor.move_left(&MoveLeft, window, cx);
1430 assert_eq!(
1431 editor.selections.display_ranges(cx),
1432 &[empty_range(0, "".len())]
1433 );
1434 });
1435}
1436
1437#[gpui::test]
1438fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1439 init_test(cx, |_| {});
1440
1441 let editor = cx.add_window(|window, cx| {
1442 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1443 build_editor(buffer.clone(), window, cx)
1444 });
1445 _ = editor.update(cx, |editor, window, cx| {
1446 editor.change_selections(None, window, cx, |s| {
1447 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1448 });
1449
1450 // moving above start of document should move selection to start of document,
1451 // but the next move down should still be at the original goal_x
1452 editor.move_up(&MoveUp, window, cx);
1453 assert_eq!(
1454 editor.selections.display_ranges(cx),
1455 &[empty_range(0, "".len())]
1456 );
1457
1458 editor.move_down(&MoveDown, window, cx);
1459 assert_eq!(
1460 editor.selections.display_ranges(cx),
1461 &[empty_range(1, "abcd".len())]
1462 );
1463
1464 editor.move_down(&MoveDown, window, cx);
1465 assert_eq!(
1466 editor.selections.display_ranges(cx),
1467 &[empty_range(2, "αβγ".len())]
1468 );
1469
1470 editor.move_down(&MoveDown, window, cx);
1471 assert_eq!(
1472 editor.selections.display_ranges(cx),
1473 &[empty_range(3, "abcd".len())]
1474 );
1475
1476 editor.move_down(&MoveDown, window, cx);
1477 assert_eq!(
1478 editor.selections.display_ranges(cx),
1479 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1480 );
1481
1482 // moving past end of document should not change goal_x
1483 editor.move_down(&MoveDown, window, cx);
1484 assert_eq!(
1485 editor.selections.display_ranges(cx),
1486 &[empty_range(5, "".len())]
1487 );
1488
1489 editor.move_down(&MoveDown, window, cx);
1490 assert_eq!(
1491 editor.selections.display_ranges(cx),
1492 &[empty_range(5, "".len())]
1493 );
1494
1495 editor.move_up(&MoveUp, window, cx);
1496 assert_eq!(
1497 editor.selections.display_ranges(cx),
1498 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1499 );
1500
1501 editor.move_up(&MoveUp, window, cx);
1502 assert_eq!(
1503 editor.selections.display_ranges(cx),
1504 &[empty_range(3, "abcd".len())]
1505 );
1506
1507 editor.move_up(&MoveUp, window, cx);
1508 assert_eq!(
1509 editor.selections.display_ranges(cx),
1510 &[empty_range(2, "αβγ".len())]
1511 );
1512 });
1513}
1514
1515#[gpui::test]
1516fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1517 init_test(cx, |_| {});
1518 let move_to_beg = MoveToBeginningOfLine {
1519 stop_at_soft_wraps: true,
1520 stop_at_indent: true,
1521 };
1522
1523 let delete_to_beg = DeleteToBeginningOfLine {
1524 stop_at_indent: false,
1525 };
1526
1527 let move_to_end = MoveToEndOfLine {
1528 stop_at_soft_wraps: true,
1529 };
1530
1531 let editor = cx.add_window(|window, cx| {
1532 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1533 build_editor(buffer, window, cx)
1534 });
1535 _ = editor.update(cx, |editor, window, cx| {
1536 editor.change_selections(None, window, cx, |s| {
1537 s.select_display_ranges([
1538 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1539 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1540 ]);
1541 });
1542 });
1543
1544 _ = editor.update(cx, |editor, window, cx| {
1545 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1546 assert_eq!(
1547 editor.selections.display_ranges(cx),
1548 &[
1549 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1550 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1551 ]
1552 );
1553 });
1554
1555 _ = editor.update(cx, |editor, window, cx| {
1556 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1557 assert_eq!(
1558 editor.selections.display_ranges(cx),
1559 &[
1560 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1561 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1562 ]
1563 );
1564 });
1565
1566 _ = editor.update(cx, |editor, window, cx| {
1567 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1568 assert_eq!(
1569 editor.selections.display_ranges(cx),
1570 &[
1571 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1572 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1573 ]
1574 );
1575 });
1576
1577 _ = editor.update(cx, |editor, window, cx| {
1578 editor.move_to_end_of_line(&move_to_end, window, cx);
1579 assert_eq!(
1580 editor.selections.display_ranges(cx),
1581 &[
1582 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1583 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1584 ]
1585 );
1586 });
1587
1588 // Moving to the end of line again is a no-op.
1589 _ = editor.update(cx, |editor, window, cx| {
1590 editor.move_to_end_of_line(&move_to_end, window, cx);
1591 assert_eq!(
1592 editor.selections.display_ranges(cx),
1593 &[
1594 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1595 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1596 ]
1597 );
1598 });
1599
1600 _ = editor.update(cx, |editor, window, cx| {
1601 editor.move_left(&MoveLeft, window, cx);
1602 editor.select_to_beginning_of_line(
1603 &SelectToBeginningOfLine {
1604 stop_at_soft_wraps: true,
1605 stop_at_indent: true,
1606 },
1607 window,
1608 cx,
1609 );
1610 assert_eq!(
1611 editor.selections.display_ranges(cx),
1612 &[
1613 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1614 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1615 ]
1616 );
1617 });
1618
1619 _ = editor.update(cx, |editor, window, cx| {
1620 editor.select_to_beginning_of_line(
1621 &SelectToBeginningOfLine {
1622 stop_at_soft_wraps: true,
1623 stop_at_indent: true,
1624 },
1625 window,
1626 cx,
1627 );
1628 assert_eq!(
1629 editor.selections.display_ranges(cx),
1630 &[
1631 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1632 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1633 ]
1634 );
1635 });
1636
1637 _ = editor.update(cx, |editor, window, cx| {
1638 editor.select_to_beginning_of_line(
1639 &SelectToBeginningOfLine {
1640 stop_at_soft_wraps: true,
1641 stop_at_indent: true,
1642 },
1643 window,
1644 cx,
1645 );
1646 assert_eq!(
1647 editor.selections.display_ranges(cx),
1648 &[
1649 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1650 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1651 ]
1652 );
1653 });
1654
1655 _ = editor.update(cx, |editor, window, cx| {
1656 editor.select_to_end_of_line(
1657 &SelectToEndOfLine {
1658 stop_at_soft_wraps: true,
1659 },
1660 window,
1661 cx,
1662 );
1663 assert_eq!(
1664 editor.selections.display_ranges(cx),
1665 &[
1666 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1667 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1668 ]
1669 );
1670 });
1671
1672 _ = editor.update(cx, |editor, window, cx| {
1673 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1674 assert_eq!(editor.display_text(cx), "ab\n de");
1675 assert_eq!(
1676 editor.selections.display_ranges(cx),
1677 &[
1678 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1679 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1680 ]
1681 );
1682 });
1683
1684 _ = editor.update(cx, |editor, window, cx| {
1685 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1686 assert_eq!(editor.display_text(cx), "\n");
1687 assert_eq!(
1688 editor.selections.display_ranges(cx),
1689 &[
1690 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1691 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1692 ]
1693 );
1694 });
1695}
1696
1697#[gpui::test]
1698fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1699 init_test(cx, |_| {});
1700 let move_to_beg = MoveToBeginningOfLine {
1701 stop_at_soft_wraps: false,
1702 stop_at_indent: false,
1703 };
1704
1705 let move_to_end = MoveToEndOfLine {
1706 stop_at_soft_wraps: false,
1707 };
1708
1709 let editor = cx.add_window(|window, cx| {
1710 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1711 build_editor(buffer, window, cx)
1712 });
1713
1714 _ = editor.update(cx, |editor, window, cx| {
1715 editor.set_wrap_width(Some(140.0.into()), cx);
1716
1717 // We expect the following lines after wrapping
1718 // ```
1719 // thequickbrownfox
1720 // jumpedoverthelazydo
1721 // gs
1722 // ```
1723 // The final `gs` was soft-wrapped onto a new line.
1724 assert_eq!(
1725 "thequickbrownfox\njumpedoverthelaz\nydogs",
1726 editor.display_text(cx),
1727 );
1728
1729 // First, let's assert behavior on the first line, that was not soft-wrapped.
1730 // Start the cursor at the `k` on the first line
1731 editor.change_selections(None, window, cx, |s| {
1732 s.select_display_ranges([
1733 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1734 ]);
1735 });
1736
1737 // Moving to the beginning of the line should put us at the beginning of the line.
1738 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1739 assert_eq!(
1740 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1741 editor.selections.display_ranges(cx)
1742 );
1743
1744 // Moving to the end of the line should put us at the end of the line.
1745 editor.move_to_end_of_line(&move_to_end, window, cx);
1746 assert_eq!(
1747 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1748 editor.selections.display_ranges(cx)
1749 );
1750
1751 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1752 // Start the cursor at the last line (`y` that was wrapped to a new line)
1753 editor.change_selections(None, window, cx, |s| {
1754 s.select_display_ranges([
1755 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1756 ]);
1757 });
1758
1759 // Moving to the beginning of the line should put us at the start of the second line of
1760 // display text, i.e., the `j`.
1761 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1762 assert_eq!(
1763 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1764 editor.selections.display_ranges(cx)
1765 );
1766
1767 // Moving to the beginning of the line again should be a no-op.
1768 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1769 assert_eq!(
1770 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1771 editor.selections.display_ranges(cx)
1772 );
1773
1774 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1775 // next display line.
1776 editor.move_to_end_of_line(&move_to_end, window, cx);
1777 assert_eq!(
1778 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1779 editor.selections.display_ranges(cx)
1780 );
1781
1782 // Moving to the end of the line again should be a no-op.
1783 editor.move_to_end_of_line(&move_to_end, window, cx);
1784 assert_eq!(
1785 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1786 editor.selections.display_ranges(cx)
1787 );
1788 });
1789}
1790
1791#[gpui::test]
1792fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1793 init_test(cx, |_| {});
1794
1795 let move_to_beg = MoveToBeginningOfLine {
1796 stop_at_soft_wraps: true,
1797 stop_at_indent: true,
1798 };
1799
1800 let select_to_beg = SelectToBeginningOfLine {
1801 stop_at_soft_wraps: true,
1802 stop_at_indent: true,
1803 };
1804
1805 let delete_to_beg = DeleteToBeginningOfLine {
1806 stop_at_indent: true,
1807 };
1808
1809 let move_to_end = MoveToEndOfLine {
1810 stop_at_soft_wraps: false,
1811 };
1812
1813 let editor = cx.add_window(|window, cx| {
1814 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1815 build_editor(buffer, window, cx)
1816 });
1817
1818 _ = editor.update(cx, |editor, window, cx| {
1819 editor.change_selections(None, window, cx, |s| {
1820 s.select_display_ranges([
1821 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1822 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1823 ]);
1824 });
1825
1826 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1827 // and the second cursor at the first non-whitespace character in the line.
1828 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1829 assert_eq!(
1830 editor.selections.display_ranges(cx),
1831 &[
1832 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1833 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1834 ]
1835 );
1836
1837 // Moving to the beginning of the line again should be a no-op for the first cursor,
1838 // and should move the second cursor to the beginning of the line.
1839 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1840 assert_eq!(
1841 editor.selections.display_ranges(cx),
1842 &[
1843 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1844 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1845 ]
1846 );
1847
1848 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1849 // and should move the second cursor back to the first non-whitespace character in the line.
1850 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1851 assert_eq!(
1852 editor.selections.display_ranges(cx),
1853 &[
1854 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1855 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1856 ]
1857 );
1858
1859 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1860 // and to the first non-whitespace character in the line for the second cursor.
1861 editor.move_to_end_of_line(&move_to_end, window, cx);
1862 editor.move_left(&MoveLeft, window, cx);
1863 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1864 assert_eq!(
1865 editor.selections.display_ranges(cx),
1866 &[
1867 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1868 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1869 ]
1870 );
1871
1872 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1873 // and should select to the beginning of the line for the second cursor.
1874 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1875 assert_eq!(
1876 editor.selections.display_ranges(cx),
1877 &[
1878 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1879 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1880 ]
1881 );
1882
1883 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1884 // and should delete to the first non-whitespace character in the line for the second cursor.
1885 editor.move_to_end_of_line(&move_to_end, window, cx);
1886 editor.move_left(&MoveLeft, window, cx);
1887 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1888 assert_eq!(editor.text(cx), "c\n f");
1889 });
1890}
1891
1892#[gpui::test]
1893fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1894 init_test(cx, |_| {});
1895
1896 let editor = cx.add_window(|window, cx| {
1897 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1898 build_editor(buffer, window, cx)
1899 });
1900 _ = editor.update(cx, |editor, window, cx| {
1901 editor.change_selections(None, window, cx, |s| {
1902 s.select_display_ranges([
1903 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1904 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1905 ])
1906 });
1907
1908 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1909 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1910
1911 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1912 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", editor, cx);
1913
1914 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1915 assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1916
1917 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1918 assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1919
1920 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1921 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1922
1923 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1924 assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1925
1926 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1927 assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1928
1929 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1930 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1931
1932 editor.move_right(&MoveRight, window, cx);
1933 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1934 assert_selection_ranges(
1935 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1936 editor,
1937 cx,
1938 );
1939
1940 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1941 assert_selection_ranges(
1942 "use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}",
1943 editor,
1944 cx,
1945 );
1946
1947 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1948 assert_selection_ranges(
1949 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1950 editor,
1951 cx,
1952 );
1953 });
1954}
1955
1956#[gpui::test]
1957fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1958 init_test(cx, |_| {});
1959
1960 let editor = cx.add_window(|window, cx| {
1961 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1962 build_editor(buffer, window, cx)
1963 });
1964
1965 _ = editor.update(cx, |editor, window, cx| {
1966 editor.set_wrap_width(Some(140.0.into()), cx);
1967 assert_eq!(
1968 editor.display_text(cx),
1969 "use one::{\n two::three::\n four::five\n};"
1970 );
1971
1972 editor.change_selections(None, window, cx, |s| {
1973 s.select_display_ranges([
1974 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1975 ]);
1976 });
1977
1978 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1979 assert_eq!(
1980 editor.selections.display_ranges(cx),
1981 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1982 );
1983
1984 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1985 assert_eq!(
1986 editor.selections.display_ranges(cx),
1987 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1988 );
1989
1990 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1991 assert_eq!(
1992 editor.selections.display_ranges(cx),
1993 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1994 );
1995
1996 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1997 assert_eq!(
1998 editor.selections.display_ranges(cx),
1999 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2000 );
2001
2002 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2003 assert_eq!(
2004 editor.selections.display_ranges(cx),
2005 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2006 );
2007
2008 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2009 assert_eq!(
2010 editor.selections.display_ranges(cx),
2011 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2012 );
2013 });
2014}
2015
2016#[gpui::test]
2017async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2018 init_test(cx, |_| {});
2019 let mut cx = EditorTestContext::new(cx).await;
2020
2021 let line_height = cx.editor(|editor, window, _| {
2022 editor
2023 .style()
2024 .unwrap()
2025 .text
2026 .line_height_in_pixels(window.rem_size())
2027 });
2028 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2029
2030 cx.set_state(
2031 &r#"ˇone
2032 two
2033
2034 three
2035 fourˇ
2036 five
2037
2038 six"#
2039 .unindent(),
2040 );
2041
2042 cx.update_editor(|editor, window, cx| {
2043 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2044 });
2045 cx.assert_editor_state(
2046 &r#"one
2047 two
2048 ˇ
2049 three
2050 four
2051 five
2052 ˇ
2053 six"#
2054 .unindent(),
2055 );
2056
2057 cx.update_editor(|editor, window, cx| {
2058 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2059 });
2060 cx.assert_editor_state(
2061 &r#"one
2062 two
2063
2064 three
2065 four
2066 five
2067 ˇ
2068 sixˇ"#
2069 .unindent(),
2070 );
2071
2072 cx.update_editor(|editor, window, cx| {
2073 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2074 });
2075 cx.assert_editor_state(
2076 &r#"one
2077 two
2078
2079 three
2080 four
2081 five
2082
2083 sixˇ"#
2084 .unindent(),
2085 );
2086
2087 cx.update_editor(|editor, window, cx| {
2088 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2089 });
2090 cx.assert_editor_state(
2091 &r#"one
2092 two
2093
2094 three
2095 four
2096 five
2097 ˇ
2098 six"#
2099 .unindent(),
2100 );
2101
2102 cx.update_editor(|editor, window, cx| {
2103 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2104 });
2105 cx.assert_editor_state(
2106 &r#"one
2107 two
2108 ˇ
2109 three
2110 four
2111 five
2112
2113 six"#
2114 .unindent(),
2115 );
2116
2117 cx.update_editor(|editor, window, cx| {
2118 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2119 });
2120 cx.assert_editor_state(
2121 &r#"ˇone
2122 two
2123
2124 three
2125 four
2126 five
2127
2128 six"#
2129 .unindent(),
2130 );
2131}
2132
2133#[gpui::test]
2134async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2135 init_test(cx, |_| {});
2136 let mut cx = EditorTestContext::new(cx).await;
2137 let line_height = cx.editor(|editor, window, _| {
2138 editor
2139 .style()
2140 .unwrap()
2141 .text
2142 .line_height_in_pixels(window.rem_size())
2143 });
2144 let window = cx.window;
2145 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2146
2147 cx.set_state(
2148 r#"ˇone
2149 two
2150 three
2151 four
2152 five
2153 six
2154 seven
2155 eight
2156 nine
2157 ten
2158 "#,
2159 );
2160
2161 cx.update_editor(|editor, window, cx| {
2162 assert_eq!(
2163 editor.snapshot(window, cx).scroll_position(),
2164 gpui::Point::new(0., 0.)
2165 );
2166 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2167 assert_eq!(
2168 editor.snapshot(window, cx).scroll_position(),
2169 gpui::Point::new(0., 3.)
2170 );
2171 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2172 assert_eq!(
2173 editor.snapshot(window, cx).scroll_position(),
2174 gpui::Point::new(0., 6.)
2175 );
2176 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2177 assert_eq!(
2178 editor.snapshot(window, cx).scroll_position(),
2179 gpui::Point::new(0., 3.)
2180 );
2181
2182 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2183 assert_eq!(
2184 editor.snapshot(window, cx).scroll_position(),
2185 gpui::Point::new(0., 1.)
2186 );
2187 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2188 assert_eq!(
2189 editor.snapshot(window, cx).scroll_position(),
2190 gpui::Point::new(0., 3.)
2191 );
2192 });
2193}
2194
2195#[gpui::test]
2196async fn test_autoscroll(cx: &mut TestAppContext) {
2197 init_test(cx, |_| {});
2198 let mut cx = EditorTestContext::new(cx).await;
2199
2200 let line_height = cx.update_editor(|editor, window, cx| {
2201 editor.set_vertical_scroll_margin(2, cx);
2202 editor
2203 .style()
2204 .unwrap()
2205 .text
2206 .line_height_in_pixels(window.rem_size())
2207 });
2208 let window = cx.window;
2209 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2210
2211 cx.set_state(
2212 r#"ˇone
2213 two
2214 three
2215 four
2216 five
2217 six
2218 seven
2219 eight
2220 nine
2221 ten
2222 "#,
2223 );
2224 cx.update_editor(|editor, window, cx| {
2225 assert_eq!(
2226 editor.snapshot(window, cx).scroll_position(),
2227 gpui::Point::new(0., 0.0)
2228 );
2229 });
2230
2231 // Add a cursor below the visible area. Since both cursors cannot fit
2232 // on screen, the editor autoscrolls to reveal the newest cursor, and
2233 // allows the vertical scroll margin below that cursor.
2234 cx.update_editor(|editor, window, cx| {
2235 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2236 selections.select_ranges([
2237 Point::new(0, 0)..Point::new(0, 0),
2238 Point::new(6, 0)..Point::new(6, 0),
2239 ]);
2240 })
2241 });
2242 cx.update_editor(|editor, window, cx| {
2243 assert_eq!(
2244 editor.snapshot(window, cx).scroll_position(),
2245 gpui::Point::new(0., 3.0)
2246 );
2247 });
2248
2249 // Move down. The editor cursor scrolls down to track the newest cursor.
2250 cx.update_editor(|editor, window, cx| {
2251 editor.move_down(&Default::default(), window, cx);
2252 });
2253 cx.update_editor(|editor, window, cx| {
2254 assert_eq!(
2255 editor.snapshot(window, cx).scroll_position(),
2256 gpui::Point::new(0., 4.0)
2257 );
2258 });
2259
2260 // Add a cursor above the visible area. Since both cursors fit on screen,
2261 // the editor scrolls to show both.
2262 cx.update_editor(|editor, window, cx| {
2263 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2264 selections.select_ranges([
2265 Point::new(1, 0)..Point::new(1, 0),
2266 Point::new(6, 0)..Point::new(6, 0),
2267 ]);
2268 })
2269 });
2270 cx.update_editor(|editor, window, cx| {
2271 assert_eq!(
2272 editor.snapshot(window, cx).scroll_position(),
2273 gpui::Point::new(0., 1.0)
2274 );
2275 });
2276}
2277
2278#[gpui::test]
2279async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2280 init_test(cx, |_| {});
2281 let mut cx = EditorTestContext::new(cx).await;
2282
2283 let line_height = cx.editor(|editor, window, _cx| {
2284 editor
2285 .style()
2286 .unwrap()
2287 .text
2288 .line_height_in_pixels(window.rem_size())
2289 });
2290 let window = cx.window;
2291 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2292 cx.set_state(
2293 &r#"
2294 ˇone
2295 two
2296 threeˇ
2297 four
2298 five
2299 six
2300 seven
2301 eight
2302 nine
2303 ten
2304 "#
2305 .unindent(),
2306 );
2307
2308 cx.update_editor(|editor, window, cx| {
2309 editor.move_page_down(&MovePageDown::default(), window, cx)
2310 });
2311 cx.assert_editor_state(
2312 &r#"
2313 one
2314 two
2315 three
2316 ˇfour
2317 five
2318 sixˇ
2319 seven
2320 eight
2321 nine
2322 ten
2323 "#
2324 .unindent(),
2325 );
2326
2327 cx.update_editor(|editor, window, cx| {
2328 editor.move_page_down(&MovePageDown::default(), window, cx)
2329 });
2330 cx.assert_editor_state(
2331 &r#"
2332 one
2333 two
2334 three
2335 four
2336 five
2337 six
2338 ˇseven
2339 eight
2340 nineˇ
2341 ten
2342 "#
2343 .unindent(),
2344 );
2345
2346 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2347 cx.assert_editor_state(
2348 &r#"
2349 one
2350 two
2351 three
2352 ˇfour
2353 five
2354 sixˇ
2355 seven
2356 eight
2357 nine
2358 ten
2359 "#
2360 .unindent(),
2361 );
2362
2363 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2364 cx.assert_editor_state(
2365 &r#"
2366 ˇone
2367 two
2368 threeˇ
2369 four
2370 five
2371 six
2372 seven
2373 eight
2374 nine
2375 ten
2376 "#
2377 .unindent(),
2378 );
2379
2380 // Test select collapsing
2381 cx.update_editor(|editor, window, cx| {
2382 editor.move_page_down(&MovePageDown::default(), window, cx);
2383 editor.move_page_down(&MovePageDown::default(), window, cx);
2384 editor.move_page_down(&MovePageDown::default(), window, cx);
2385 });
2386 cx.assert_editor_state(
2387 &r#"
2388 one
2389 two
2390 three
2391 four
2392 five
2393 six
2394 seven
2395 eight
2396 nine
2397 ˇten
2398 ˇ"#
2399 .unindent(),
2400 );
2401}
2402
2403#[gpui::test]
2404async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2405 init_test(cx, |_| {});
2406 let mut cx = EditorTestContext::new(cx).await;
2407 cx.set_state("one «two threeˇ» four");
2408 cx.update_editor(|editor, window, cx| {
2409 editor.delete_to_beginning_of_line(
2410 &DeleteToBeginningOfLine {
2411 stop_at_indent: false,
2412 },
2413 window,
2414 cx,
2415 );
2416 assert_eq!(editor.text(cx), " four");
2417 });
2418}
2419
2420#[gpui::test]
2421fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2422 init_test(cx, |_| {});
2423
2424 let editor = cx.add_window(|window, cx| {
2425 let buffer = MultiBuffer::build_simple("one two three four", cx);
2426 build_editor(buffer.clone(), window, cx)
2427 });
2428
2429 _ = editor.update(cx, |editor, window, cx| {
2430 editor.change_selections(None, window, cx, |s| {
2431 s.select_display_ranges([
2432 // an empty selection - the preceding word fragment is deleted
2433 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2434 // characters selected - they are deleted
2435 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2436 ])
2437 });
2438 editor.delete_to_previous_word_start(
2439 &DeleteToPreviousWordStart {
2440 ignore_newlines: false,
2441 },
2442 window,
2443 cx,
2444 );
2445 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2446 });
2447
2448 _ = editor.update(cx, |editor, window, cx| {
2449 editor.change_selections(None, window, cx, |s| {
2450 s.select_display_ranges([
2451 // an empty selection - the following word fragment is deleted
2452 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2453 // characters selected - they are deleted
2454 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2455 ])
2456 });
2457 editor.delete_to_next_word_end(
2458 &DeleteToNextWordEnd {
2459 ignore_newlines: false,
2460 },
2461 window,
2462 cx,
2463 );
2464 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2465 });
2466}
2467
2468#[gpui::test]
2469fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2470 init_test(cx, |_| {});
2471
2472 let editor = cx.add_window(|window, cx| {
2473 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2474 build_editor(buffer.clone(), window, cx)
2475 });
2476 let del_to_prev_word_start = DeleteToPreviousWordStart {
2477 ignore_newlines: false,
2478 };
2479 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2480 ignore_newlines: true,
2481 };
2482
2483 _ = editor.update(cx, |editor, window, cx| {
2484 editor.change_selections(None, window, cx, |s| {
2485 s.select_display_ranges([
2486 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2487 ])
2488 });
2489 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2490 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2491 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2492 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2493 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2494 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2495 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2496 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2497 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2498 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2499 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2500 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2501 });
2502}
2503
2504#[gpui::test]
2505fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2506 init_test(cx, |_| {});
2507
2508 let editor = cx.add_window(|window, cx| {
2509 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2510 build_editor(buffer.clone(), window, cx)
2511 });
2512 let del_to_next_word_end = DeleteToNextWordEnd {
2513 ignore_newlines: false,
2514 };
2515 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2516 ignore_newlines: true,
2517 };
2518
2519 _ = editor.update(cx, |editor, window, cx| {
2520 editor.change_selections(None, window, cx, |s| {
2521 s.select_display_ranges([
2522 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2523 ])
2524 });
2525 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2526 assert_eq!(
2527 editor.buffer.read(cx).read(cx).text(),
2528 "one\n two\nthree\n four"
2529 );
2530 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2531 assert_eq!(
2532 editor.buffer.read(cx).read(cx).text(),
2533 "\n two\nthree\n four"
2534 );
2535 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2536 assert_eq!(
2537 editor.buffer.read(cx).read(cx).text(),
2538 "two\nthree\n four"
2539 );
2540 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2541 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2542 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2543 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2544 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2545 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2546 });
2547}
2548
2549#[gpui::test]
2550fn test_newline(cx: &mut TestAppContext) {
2551 init_test(cx, |_| {});
2552
2553 let editor = cx.add_window(|window, cx| {
2554 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2555 build_editor(buffer.clone(), window, cx)
2556 });
2557
2558 _ = editor.update(cx, |editor, window, cx| {
2559 editor.change_selections(None, window, cx, |s| {
2560 s.select_display_ranges([
2561 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2562 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2563 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2564 ])
2565 });
2566
2567 editor.newline(&Newline, window, cx);
2568 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2569 });
2570}
2571
2572#[gpui::test]
2573fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2574 init_test(cx, |_| {});
2575
2576 let editor = cx.add_window(|window, cx| {
2577 let buffer = MultiBuffer::build_simple(
2578 "
2579 a
2580 b(
2581 X
2582 )
2583 c(
2584 X
2585 )
2586 "
2587 .unindent()
2588 .as_str(),
2589 cx,
2590 );
2591 let mut editor = build_editor(buffer.clone(), window, cx);
2592 editor.change_selections(None, window, cx, |s| {
2593 s.select_ranges([
2594 Point::new(2, 4)..Point::new(2, 5),
2595 Point::new(5, 4)..Point::new(5, 5),
2596 ])
2597 });
2598 editor
2599 });
2600
2601 _ = editor.update(cx, |editor, window, cx| {
2602 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2603 editor.buffer.update(cx, |buffer, cx| {
2604 buffer.edit(
2605 [
2606 (Point::new(1, 2)..Point::new(3, 0), ""),
2607 (Point::new(4, 2)..Point::new(6, 0), ""),
2608 ],
2609 None,
2610 cx,
2611 );
2612 assert_eq!(
2613 buffer.read(cx).text(),
2614 "
2615 a
2616 b()
2617 c()
2618 "
2619 .unindent()
2620 );
2621 });
2622 assert_eq!(
2623 editor.selections.ranges(cx),
2624 &[
2625 Point::new(1, 2)..Point::new(1, 2),
2626 Point::new(2, 2)..Point::new(2, 2),
2627 ],
2628 );
2629
2630 editor.newline(&Newline, window, cx);
2631 assert_eq!(
2632 editor.text(cx),
2633 "
2634 a
2635 b(
2636 )
2637 c(
2638 )
2639 "
2640 .unindent()
2641 );
2642
2643 // The selections are moved after the inserted newlines
2644 assert_eq!(
2645 editor.selections.ranges(cx),
2646 &[
2647 Point::new(2, 0)..Point::new(2, 0),
2648 Point::new(4, 0)..Point::new(4, 0),
2649 ],
2650 );
2651 });
2652}
2653
2654#[gpui::test]
2655async fn test_newline_above(cx: &mut TestAppContext) {
2656 init_test(cx, |settings| {
2657 settings.defaults.tab_size = NonZeroU32::new(4)
2658 });
2659
2660 let language = Arc::new(
2661 Language::new(
2662 LanguageConfig::default(),
2663 Some(tree_sitter_rust::LANGUAGE.into()),
2664 )
2665 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2666 .unwrap(),
2667 );
2668
2669 let mut cx = EditorTestContext::new(cx).await;
2670 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2671 cx.set_state(indoc! {"
2672 const a: ˇA = (
2673 (ˇ
2674 «const_functionˇ»(ˇ),
2675 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2676 )ˇ
2677 ˇ);ˇ
2678 "});
2679
2680 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2681 cx.assert_editor_state(indoc! {"
2682 ˇ
2683 const a: A = (
2684 ˇ
2685 (
2686 ˇ
2687 ˇ
2688 const_function(),
2689 ˇ
2690 ˇ
2691 ˇ
2692 ˇ
2693 something_else,
2694 ˇ
2695 )
2696 ˇ
2697 ˇ
2698 );
2699 "});
2700}
2701
2702#[gpui::test]
2703async fn test_newline_below(cx: &mut TestAppContext) {
2704 init_test(cx, |settings| {
2705 settings.defaults.tab_size = NonZeroU32::new(4)
2706 });
2707
2708 let language = Arc::new(
2709 Language::new(
2710 LanguageConfig::default(),
2711 Some(tree_sitter_rust::LANGUAGE.into()),
2712 )
2713 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2714 .unwrap(),
2715 );
2716
2717 let mut cx = EditorTestContext::new(cx).await;
2718 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2719 cx.set_state(indoc! {"
2720 const a: ˇA = (
2721 (ˇ
2722 «const_functionˇ»(ˇ),
2723 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2724 )ˇ
2725 ˇ);ˇ
2726 "});
2727
2728 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2729 cx.assert_editor_state(indoc! {"
2730 const a: A = (
2731 ˇ
2732 (
2733 ˇ
2734 const_function(),
2735 ˇ
2736 ˇ
2737 something_else,
2738 ˇ
2739 ˇ
2740 ˇ
2741 ˇ
2742 )
2743 ˇ
2744 );
2745 ˇ
2746 ˇ
2747 "});
2748}
2749
2750#[gpui::test]
2751async fn test_newline_comments(cx: &mut TestAppContext) {
2752 init_test(cx, |settings| {
2753 settings.defaults.tab_size = NonZeroU32::new(4)
2754 });
2755
2756 let language = Arc::new(Language::new(
2757 LanguageConfig {
2758 line_comments: vec!["//".into()],
2759 ..LanguageConfig::default()
2760 },
2761 None,
2762 ));
2763 {
2764 let mut cx = EditorTestContext::new(cx).await;
2765 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2766 cx.set_state(indoc! {"
2767 // Fooˇ
2768 "});
2769
2770 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2771 cx.assert_editor_state(indoc! {"
2772 // Foo
2773 //ˇ
2774 "});
2775 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2776 cx.set_state(indoc! {"
2777 ˇ// Foo
2778 "});
2779 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2780 cx.assert_editor_state(indoc! {"
2781
2782 ˇ// Foo
2783 "});
2784 }
2785 // Ensure that comment continuations can be disabled.
2786 update_test_language_settings(cx, |settings| {
2787 settings.defaults.extend_comment_on_newline = Some(false);
2788 });
2789 let mut cx = EditorTestContext::new(cx).await;
2790 cx.set_state(indoc! {"
2791 // Fooˇ
2792 "});
2793 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2794 cx.assert_editor_state(indoc! {"
2795 // Foo
2796 ˇ
2797 "});
2798}
2799
2800#[gpui::test]
2801fn test_insert_with_old_selections(cx: &mut TestAppContext) {
2802 init_test(cx, |_| {});
2803
2804 let editor = cx.add_window(|window, cx| {
2805 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
2806 let mut editor = build_editor(buffer.clone(), window, cx);
2807 editor.change_selections(None, window, cx, |s| {
2808 s.select_ranges([3..4, 11..12, 19..20])
2809 });
2810 editor
2811 });
2812
2813 _ = editor.update(cx, |editor, window, cx| {
2814 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2815 editor.buffer.update(cx, |buffer, cx| {
2816 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
2817 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
2818 });
2819 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
2820
2821 editor.insert("Z", window, cx);
2822 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
2823
2824 // The selections are moved after the inserted characters
2825 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
2826 });
2827}
2828
2829#[gpui::test]
2830async fn test_tab(cx: &mut TestAppContext) {
2831 init_test(cx, |settings| {
2832 settings.defaults.tab_size = NonZeroU32::new(3)
2833 });
2834
2835 let mut cx = EditorTestContext::new(cx).await;
2836 cx.set_state(indoc! {"
2837 ˇabˇc
2838 ˇ🏀ˇ🏀ˇefg
2839 dˇ
2840 "});
2841 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2842 cx.assert_editor_state(indoc! {"
2843 ˇab ˇc
2844 ˇ🏀 ˇ🏀 ˇefg
2845 d ˇ
2846 "});
2847
2848 cx.set_state(indoc! {"
2849 a
2850 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2851 "});
2852 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2853 cx.assert_editor_state(indoc! {"
2854 a
2855 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2856 "});
2857}
2858
2859#[gpui::test]
2860async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
2861 init_test(cx, |_| {});
2862
2863 let mut cx = EditorTestContext::new(cx).await;
2864 let language = Arc::new(
2865 Language::new(
2866 LanguageConfig::default(),
2867 Some(tree_sitter_rust::LANGUAGE.into()),
2868 )
2869 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2870 .unwrap(),
2871 );
2872 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2873
2874 // when all cursors are to the left of the suggested indent, then auto-indent all.
2875 cx.set_state(indoc! {"
2876 const a: B = (
2877 c(
2878 ˇ
2879 ˇ )
2880 );
2881 "});
2882 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2883 cx.assert_editor_state(indoc! {"
2884 const a: B = (
2885 c(
2886 ˇ
2887 ˇ)
2888 );
2889 "});
2890
2891 // cursors that are already at the suggested indent level do not move
2892 // until other cursors that are to the left of the suggested indent
2893 // auto-indent.
2894 cx.set_state(indoc! {"
2895 ˇ
2896 const a: B = (
2897 c(
2898 d(
2899 ˇ
2900 )
2901 ˇ
2902 ˇ )
2903 );
2904 "});
2905 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2906 cx.assert_editor_state(indoc! {"
2907 ˇ
2908 const a: B = (
2909 c(
2910 d(
2911 ˇ
2912 )
2913 ˇ
2914 ˇ)
2915 );
2916 "});
2917 // once all multi-cursors are at the suggested
2918 // indent level, they all insert a soft tab together.
2919 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2920 cx.assert_editor_state(indoc! {"
2921 ˇ
2922 const a: B = (
2923 c(
2924 d(
2925 ˇ
2926 )
2927 ˇ
2928 ˇ)
2929 );
2930 "});
2931
2932 // handle auto-indent when there are multiple cursors on the same line
2933 cx.set_state(indoc! {"
2934 const a: B = (
2935 c(
2936 ˇ ˇ
2937 ˇ )
2938 );
2939 "});
2940 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2941 cx.assert_editor_state(indoc! {"
2942 const a: B = (
2943 c(
2944 ˇ
2945 ˇ)
2946 );
2947 "});
2948}
2949
2950#[gpui::test]
2951async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
2952 init_test(cx, |settings| {
2953 settings.defaults.tab_size = NonZeroU32::new(3)
2954 });
2955
2956 let mut cx = EditorTestContext::new(cx).await;
2957 cx.set_state(indoc! {"
2958 ˇ
2959 \t ˇ
2960 \t ˇ
2961 \t ˇ
2962 \t \t\t \t \t\t \t\t \t \t ˇ
2963 "});
2964
2965 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2966 cx.assert_editor_state(indoc! {"
2967 ˇ
2968 \t ˇ
2969 \t ˇ
2970 \t ˇ
2971 \t \t\t \t \t\t \t\t \t \t ˇ
2972 "});
2973}
2974
2975#[gpui::test]
2976async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
2977 init_test(cx, |settings| {
2978 settings.defaults.tab_size = NonZeroU32::new(4)
2979 });
2980
2981 let language = Arc::new(
2982 Language::new(
2983 LanguageConfig::default(),
2984 Some(tree_sitter_rust::LANGUAGE.into()),
2985 )
2986 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
2987 .unwrap(),
2988 );
2989
2990 let mut cx = EditorTestContext::new(cx).await;
2991 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2992 cx.set_state(indoc! {"
2993 fn a() {
2994 if b {
2995 \t ˇc
2996 }
2997 }
2998 "});
2999
3000 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3001 cx.assert_editor_state(indoc! {"
3002 fn a() {
3003 if b {
3004 ˇc
3005 }
3006 }
3007 "});
3008}
3009
3010#[gpui::test]
3011async fn test_indent_outdent(cx: &mut TestAppContext) {
3012 init_test(cx, |settings| {
3013 settings.defaults.tab_size = NonZeroU32::new(4);
3014 });
3015
3016 let mut cx = EditorTestContext::new(cx).await;
3017
3018 cx.set_state(indoc! {"
3019 «oneˇ» «twoˇ»
3020 three
3021 four
3022 "});
3023 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3024 cx.assert_editor_state(indoc! {"
3025 «oneˇ» «twoˇ»
3026 three
3027 four
3028 "});
3029
3030 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3031 cx.assert_editor_state(indoc! {"
3032 «oneˇ» «twoˇ»
3033 three
3034 four
3035 "});
3036
3037 // select across line ending
3038 cx.set_state(indoc! {"
3039 one two
3040 t«hree
3041 ˇ» four
3042 "});
3043 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3044 cx.assert_editor_state(indoc! {"
3045 one two
3046 t«hree
3047 ˇ» four
3048 "});
3049
3050 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3051 cx.assert_editor_state(indoc! {"
3052 one two
3053 t«hree
3054 ˇ» four
3055 "});
3056
3057 // Ensure that indenting/outdenting works when the cursor is at column 0.
3058 cx.set_state(indoc! {"
3059 one two
3060 ˇthree
3061 four
3062 "});
3063 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3064 cx.assert_editor_state(indoc! {"
3065 one two
3066 ˇthree
3067 four
3068 "});
3069
3070 cx.set_state(indoc! {"
3071 one two
3072 ˇ three
3073 four
3074 "});
3075 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3076 cx.assert_editor_state(indoc! {"
3077 one two
3078 ˇthree
3079 four
3080 "});
3081}
3082
3083#[gpui::test]
3084async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3085 init_test(cx, |settings| {
3086 settings.defaults.hard_tabs = Some(true);
3087 });
3088
3089 let mut cx = EditorTestContext::new(cx).await;
3090
3091 // select two ranges on one line
3092 cx.set_state(indoc! {"
3093 «oneˇ» «twoˇ»
3094 three
3095 four
3096 "});
3097 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3098 cx.assert_editor_state(indoc! {"
3099 \t«oneˇ» «twoˇ»
3100 three
3101 four
3102 "});
3103 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3104 cx.assert_editor_state(indoc! {"
3105 \t\t«oneˇ» «twoˇ»
3106 three
3107 four
3108 "});
3109 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3110 cx.assert_editor_state(indoc! {"
3111 \t«oneˇ» «twoˇ»
3112 three
3113 four
3114 "});
3115 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3116 cx.assert_editor_state(indoc! {"
3117 «oneˇ» «twoˇ»
3118 three
3119 four
3120 "});
3121
3122 // select across a line ending
3123 cx.set_state(indoc! {"
3124 one two
3125 t«hree
3126 ˇ»four
3127 "});
3128 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3129 cx.assert_editor_state(indoc! {"
3130 one two
3131 \tt«hree
3132 ˇ»four
3133 "});
3134 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3135 cx.assert_editor_state(indoc! {"
3136 one two
3137 \t\tt«hree
3138 ˇ»four
3139 "});
3140 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3141 cx.assert_editor_state(indoc! {"
3142 one two
3143 \tt«hree
3144 ˇ»four
3145 "});
3146 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3147 cx.assert_editor_state(indoc! {"
3148 one two
3149 t«hree
3150 ˇ»four
3151 "});
3152
3153 // Ensure that indenting/outdenting works when the cursor is at column 0.
3154 cx.set_state(indoc! {"
3155 one two
3156 ˇthree
3157 four
3158 "});
3159 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3160 cx.assert_editor_state(indoc! {"
3161 one two
3162 ˇthree
3163 four
3164 "});
3165 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3166 cx.assert_editor_state(indoc! {"
3167 one two
3168 \tˇthree
3169 four
3170 "});
3171 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3172 cx.assert_editor_state(indoc! {"
3173 one two
3174 ˇthree
3175 four
3176 "});
3177}
3178
3179#[gpui::test]
3180fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3181 init_test(cx, |settings| {
3182 settings.languages.extend([
3183 (
3184 "TOML".into(),
3185 LanguageSettingsContent {
3186 tab_size: NonZeroU32::new(2),
3187 ..Default::default()
3188 },
3189 ),
3190 (
3191 "Rust".into(),
3192 LanguageSettingsContent {
3193 tab_size: NonZeroU32::new(4),
3194 ..Default::default()
3195 },
3196 ),
3197 ]);
3198 });
3199
3200 let toml_language = Arc::new(Language::new(
3201 LanguageConfig {
3202 name: "TOML".into(),
3203 ..Default::default()
3204 },
3205 None,
3206 ));
3207 let rust_language = Arc::new(Language::new(
3208 LanguageConfig {
3209 name: "Rust".into(),
3210 ..Default::default()
3211 },
3212 None,
3213 ));
3214
3215 let toml_buffer =
3216 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3217 let rust_buffer =
3218 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3219 let multibuffer = cx.new(|cx| {
3220 let mut multibuffer = MultiBuffer::new(ReadWrite);
3221 multibuffer.push_excerpts(
3222 toml_buffer.clone(),
3223 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3224 cx,
3225 );
3226 multibuffer.push_excerpts(
3227 rust_buffer.clone(),
3228 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3229 cx,
3230 );
3231 multibuffer
3232 });
3233
3234 cx.add_window(|window, cx| {
3235 let mut editor = build_editor(multibuffer, window, cx);
3236
3237 assert_eq!(
3238 editor.text(cx),
3239 indoc! {"
3240 a = 1
3241 b = 2
3242
3243 const c: usize = 3;
3244 "}
3245 );
3246
3247 select_ranges(
3248 &mut editor,
3249 indoc! {"
3250 «aˇ» = 1
3251 b = 2
3252
3253 «const c:ˇ» usize = 3;
3254 "},
3255 window,
3256 cx,
3257 );
3258
3259 editor.tab(&Tab, window, cx);
3260 assert_text_with_selections(
3261 &mut editor,
3262 indoc! {"
3263 «aˇ» = 1
3264 b = 2
3265
3266 «const c:ˇ» usize = 3;
3267 "},
3268 cx,
3269 );
3270 editor.backtab(&Backtab, window, cx);
3271 assert_text_with_selections(
3272 &mut editor,
3273 indoc! {"
3274 «aˇ» = 1
3275 b = 2
3276
3277 «const c:ˇ» usize = 3;
3278 "},
3279 cx,
3280 );
3281
3282 editor
3283 });
3284}
3285
3286#[gpui::test]
3287async fn test_backspace(cx: &mut TestAppContext) {
3288 init_test(cx, |_| {});
3289
3290 let mut cx = EditorTestContext::new(cx).await;
3291
3292 // Basic backspace
3293 cx.set_state(indoc! {"
3294 onˇe two three
3295 fou«rˇ» five six
3296 seven «ˇeight nine
3297 »ten
3298 "});
3299 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3300 cx.assert_editor_state(indoc! {"
3301 oˇe two three
3302 fouˇ five six
3303 seven ˇten
3304 "});
3305
3306 // Test backspace inside and around indents
3307 cx.set_state(indoc! {"
3308 zero
3309 ˇone
3310 ˇtwo
3311 ˇ ˇ ˇ three
3312 ˇ ˇ four
3313 "});
3314 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3315 cx.assert_editor_state(indoc! {"
3316 zero
3317 ˇone
3318 ˇtwo
3319 ˇ threeˇ four
3320 "});
3321}
3322
3323#[gpui::test]
3324async fn test_delete(cx: &mut TestAppContext) {
3325 init_test(cx, |_| {});
3326
3327 let mut cx = EditorTestContext::new(cx).await;
3328 cx.set_state(indoc! {"
3329 onˇe two three
3330 fou«rˇ» five six
3331 seven «ˇeight nine
3332 »ten
3333 "});
3334 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3335 cx.assert_editor_state(indoc! {"
3336 onˇ two three
3337 fouˇ five six
3338 seven ˇten
3339 "});
3340}
3341
3342#[gpui::test]
3343fn test_delete_line(cx: &mut TestAppContext) {
3344 init_test(cx, |_| {});
3345
3346 let editor = cx.add_window(|window, cx| {
3347 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3348 build_editor(buffer, window, cx)
3349 });
3350 _ = editor.update(cx, |editor, window, cx| {
3351 editor.change_selections(None, window, cx, |s| {
3352 s.select_display_ranges([
3353 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3354 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3355 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3356 ])
3357 });
3358 editor.delete_line(&DeleteLine, window, cx);
3359 assert_eq!(editor.display_text(cx), "ghi");
3360 assert_eq!(
3361 editor.selections.display_ranges(cx),
3362 vec![
3363 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3364 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3365 ]
3366 );
3367 });
3368
3369 let editor = cx.add_window(|window, cx| {
3370 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3371 build_editor(buffer, window, cx)
3372 });
3373 _ = editor.update(cx, |editor, window, cx| {
3374 editor.change_selections(None, window, cx, |s| {
3375 s.select_display_ranges([
3376 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3377 ])
3378 });
3379 editor.delete_line(&DeleteLine, window, cx);
3380 assert_eq!(editor.display_text(cx), "ghi\n");
3381 assert_eq!(
3382 editor.selections.display_ranges(cx),
3383 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3384 );
3385 });
3386}
3387
3388#[gpui::test]
3389fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3390 init_test(cx, |_| {});
3391
3392 cx.add_window(|window, cx| {
3393 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3394 let mut editor = build_editor(buffer.clone(), window, cx);
3395 let buffer = buffer.read(cx).as_singleton().unwrap();
3396
3397 assert_eq!(
3398 editor.selections.ranges::<Point>(cx),
3399 &[Point::new(0, 0)..Point::new(0, 0)]
3400 );
3401
3402 // When on single line, replace newline at end by space
3403 editor.join_lines(&JoinLines, window, cx);
3404 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3405 assert_eq!(
3406 editor.selections.ranges::<Point>(cx),
3407 &[Point::new(0, 3)..Point::new(0, 3)]
3408 );
3409
3410 // When multiple lines are selected, remove newlines that are spanned by the selection
3411 editor.change_selections(None, window, cx, |s| {
3412 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3413 });
3414 editor.join_lines(&JoinLines, window, cx);
3415 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3416 assert_eq!(
3417 editor.selections.ranges::<Point>(cx),
3418 &[Point::new(0, 11)..Point::new(0, 11)]
3419 );
3420
3421 // Undo should be transactional
3422 editor.undo(&Undo, window, cx);
3423 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3424 assert_eq!(
3425 editor.selections.ranges::<Point>(cx),
3426 &[Point::new(0, 5)..Point::new(2, 2)]
3427 );
3428
3429 // When joining an empty line don't insert a space
3430 editor.change_selections(None, window, cx, |s| {
3431 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3432 });
3433 editor.join_lines(&JoinLines, window, cx);
3434 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3435 assert_eq!(
3436 editor.selections.ranges::<Point>(cx),
3437 [Point::new(2, 3)..Point::new(2, 3)]
3438 );
3439
3440 // We can remove trailing newlines
3441 editor.join_lines(&JoinLines, window, cx);
3442 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3443 assert_eq!(
3444 editor.selections.ranges::<Point>(cx),
3445 [Point::new(2, 3)..Point::new(2, 3)]
3446 );
3447
3448 // We don't blow up on the last line
3449 editor.join_lines(&JoinLines, window, cx);
3450 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3451 assert_eq!(
3452 editor.selections.ranges::<Point>(cx),
3453 [Point::new(2, 3)..Point::new(2, 3)]
3454 );
3455
3456 // reset to test indentation
3457 editor.buffer.update(cx, |buffer, cx| {
3458 buffer.edit(
3459 [
3460 (Point::new(1, 0)..Point::new(1, 2), " "),
3461 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3462 ],
3463 None,
3464 cx,
3465 )
3466 });
3467
3468 // We remove any leading spaces
3469 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3470 editor.change_selections(None, window, cx, |s| {
3471 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3472 });
3473 editor.join_lines(&JoinLines, window, cx);
3474 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3475
3476 // We don't insert a space for a line containing only spaces
3477 editor.join_lines(&JoinLines, window, cx);
3478 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3479
3480 // We ignore any leading tabs
3481 editor.join_lines(&JoinLines, window, cx);
3482 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3483
3484 editor
3485 });
3486}
3487
3488#[gpui::test]
3489fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3490 init_test(cx, |_| {});
3491
3492 cx.add_window(|window, cx| {
3493 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3494 let mut editor = build_editor(buffer.clone(), window, cx);
3495 let buffer = buffer.read(cx).as_singleton().unwrap();
3496
3497 editor.change_selections(None, window, cx, |s| {
3498 s.select_ranges([
3499 Point::new(0, 2)..Point::new(1, 1),
3500 Point::new(1, 2)..Point::new(1, 2),
3501 Point::new(3, 1)..Point::new(3, 2),
3502 ])
3503 });
3504
3505 editor.join_lines(&JoinLines, window, cx);
3506 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3507
3508 assert_eq!(
3509 editor.selections.ranges::<Point>(cx),
3510 [
3511 Point::new(0, 7)..Point::new(0, 7),
3512 Point::new(1, 3)..Point::new(1, 3)
3513 ]
3514 );
3515 editor
3516 });
3517}
3518
3519#[gpui::test]
3520async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3521 init_test(cx, |_| {});
3522
3523 let mut cx = EditorTestContext::new(cx).await;
3524
3525 let diff_base = r#"
3526 Line 0
3527 Line 1
3528 Line 2
3529 Line 3
3530 "#
3531 .unindent();
3532
3533 cx.set_state(
3534 &r#"
3535 ˇLine 0
3536 Line 1
3537 Line 2
3538 Line 3
3539 "#
3540 .unindent(),
3541 );
3542
3543 cx.set_head_text(&diff_base);
3544 executor.run_until_parked();
3545
3546 // Join lines
3547 cx.update_editor(|editor, window, cx| {
3548 editor.join_lines(&JoinLines, window, cx);
3549 });
3550 executor.run_until_parked();
3551
3552 cx.assert_editor_state(
3553 &r#"
3554 Line 0ˇ Line 1
3555 Line 2
3556 Line 3
3557 "#
3558 .unindent(),
3559 );
3560 // Join again
3561 cx.update_editor(|editor, window, cx| {
3562 editor.join_lines(&JoinLines, window, cx);
3563 });
3564 executor.run_until_parked();
3565
3566 cx.assert_editor_state(
3567 &r#"
3568 Line 0 Line 1ˇ Line 2
3569 Line 3
3570 "#
3571 .unindent(),
3572 );
3573}
3574
3575#[gpui::test]
3576async fn test_custom_newlines_cause_no_false_positive_diffs(
3577 executor: BackgroundExecutor,
3578 cx: &mut TestAppContext,
3579) {
3580 init_test(cx, |_| {});
3581 let mut cx = EditorTestContext::new(cx).await;
3582 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3583 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3584 executor.run_until_parked();
3585
3586 cx.update_editor(|editor, window, cx| {
3587 let snapshot = editor.snapshot(window, cx);
3588 assert_eq!(
3589 snapshot
3590 .buffer_snapshot
3591 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3592 .collect::<Vec<_>>(),
3593 Vec::new(),
3594 "Should not have any diffs for files with custom newlines"
3595 );
3596 });
3597}
3598
3599#[gpui::test]
3600async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3601 init_test(cx, |_| {});
3602
3603 let mut cx = EditorTestContext::new(cx).await;
3604
3605 // Test sort_lines_case_insensitive()
3606 cx.set_state(indoc! {"
3607 «z
3608 y
3609 x
3610 Z
3611 Y
3612 Xˇ»
3613 "});
3614 cx.update_editor(|e, window, cx| {
3615 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3616 });
3617 cx.assert_editor_state(indoc! {"
3618 «x
3619 X
3620 y
3621 Y
3622 z
3623 Zˇ»
3624 "});
3625
3626 // Test reverse_lines()
3627 cx.set_state(indoc! {"
3628 «5
3629 4
3630 3
3631 2
3632 1ˇ»
3633 "});
3634 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
3635 cx.assert_editor_state(indoc! {"
3636 «1
3637 2
3638 3
3639 4
3640 5ˇ»
3641 "});
3642
3643 // Skip testing shuffle_line()
3644
3645 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
3646 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
3647
3648 // Don't manipulate when cursor is on single line, but expand the selection
3649 cx.set_state(indoc! {"
3650 ddˇdd
3651 ccc
3652 bb
3653 a
3654 "});
3655 cx.update_editor(|e, window, cx| {
3656 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3657 });
3658 cx.assert_editor_state(indoc! {"
3659 «ddddˇ»
3660 ccc
3661 bb
3662 a
3663 "});
3664
3665 // Basic manipulate case
3666 // Start selection moves to column 0
3667 // End of selection shrinks to fit shorter line
3668 cx.set_state(indoc! {"
3669 dd«d
3670 ccc
3671 bb
3672 aaaaaˇ»
3673 "});
3674 cx.update_editor(|e, window, cx| {
3675 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3676 });
3677 cx.assert_editor_state(indoc! {"
3678 «aaaaa
3679 bb
3680 ccc
3681 dddˇ»
3682 "});
3683
3684 // Manipulate case with newlines
3685 cx.set_state(indoc! {"
3686 dd«d
3687 ccc
3688
3689 bb
3690 aaaaa
3691
3692 ˇ»
3693 "});
3694 cx.update_editor(|e, window, cx| {
3695 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3696 });
3697 cx.assert_editor_state(indoc! {"
3698 «
3699
3700 aaaaa
3701 bb
3702 ccc
3703 dddˇ»
3704
3705 "});
3706
3707 // Adding new line
3708 cx.set_state(indoc! {"
3709 aa«a
3710 bbˇ»b
3711 "});
3712 cx.update_editor(|e, window, cx| {
3713 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
3714 });
3715 cx.assert_editor_state(indoc! {"
3716 «aaa
3717 bbb
3718 added_lineˇ»
3719 "});
3720
3721 // Removing line
3722 cx.set_state(indoc! {"
3723 aa«a
3724 bbbˇ»
3725 "});
3726 cx.update_editor(|e, window, cx| {
3727 e.manipulate_lines(window, cx, |lines| {
3728 lines.pop();
3729 })
3730 });
3731 cx.assert_editor_state(indoc! {"
3732 «aaaˇ»
3733 "});
3734
3735 // Removing all lines
3736 cx.set_state(indoc! {"
3737 aa«a
3738 bbbˇ»
3739 "});
3740 cx.update_editor(|e, window, cx| {
3741 e.manipulate_lines(window, cx, |lines| {
3742 lines.drain(..);
3743 })
3744 });
3745 cx.assert_editor_state(indoc! {"
3746 ˇ
3747 "});
3748}
3749
3750#[gpui::test]
3751async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
3752 init_test(cx, |_| {});
3753
3754 let mut cx = EditorTestContext::new(cx).await;
3755
3756 // Consider continuous selection as single selection
3757 cx.set_state(indoc! {"
3758 Aaa«aa
3759 cˇ»c«c
3760 bb
3761 aaaˇ»aa
3762 "});
3763 cx.update_editor(|e, window, cx| {
3764 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3765 });
3766 cx.assert_editor_state(indoc! {"
3767 «Aaaaa
3768 ccc
3769 bb
3770 aaaaaˇ»
3771 "});
3772
3773 cx.set_state(indoc! {"
3774 Aaa«aa
3775 cˇ»c«c
3776 bb
3777 aaaˇ»aa
3778 "});
3779 cx.update_editor(|e, window, cx| {
3780 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3781 });
3782 cx.assert_editor_state(indoc! {"
3783 «Aaaaa
3784 ccc
3785 bbˇ»
3786 "});
3787
3788 // Consider non continuous selection as distinct dedup operations
3789 cx.set_state(indoc! {"
3790 «aaaaa
3791 bb
3792 aaaaa
3793 aaaaaˇ»
3794
3795 aaa«aaˇ»
3796 "});
3797 cx.update_editor(|e, window, cx| {
3798 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3799 });
3800 cx.assert_editor_state(indoc! {"
3801 «aaaaa
3802 bbˇ»
3803
3804 «aaaaaˇ»
3805 "});
3806}
3807
3808#[gpui::test]
3809async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
3810 init_test(cx, |_| {});
3811
3812 let mut cx = EditorTestContext::new(cx).await;
3813
3814 cx.set_state(indoc! {"
3815 «Aaa
3816 aAa
3817 Aaaˇ»
3818 "});
3819 cx.update_editor(|e, window, cx| {
3820 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3821 });
3822 cx.assert_editor_state(indoc! {"
3823 «Aaa
3824 aAaˇ»
3825 "});
3826
3827 cx.set_state(indoc! {"
3828 «Aaa
3829 aAa
3830 aaAˇ»
3831 "});
3832 cx.update_editor(|e, window, cx| {
3833 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3834 });
3835 cx.assert_editor_state(indoc! {"
3836 «Aaaˇ»
3837 "});
3838}
3839
3840#[gpui::test]
3841async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
3842 init_test(cx, |_| {});
3843
3844 let mut cx = EditorTestContext::new(cx).await;
3845
3846 // Manipulate with multiple selections on a single line
3847 cx.set_state(indoc! {"
3848 dd«dd
3849 cˇ»c«c
3850 bb
3851 aaaˇ»aa
3852 "});
3853 cx.update_editor(|e, window, cx| {
3854 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3855 });
3856 cx.assert_editor_state(indoc! {"
3857 «aaaaa
3858 bb
3859 ccc
3860 ddddˇ»
3861 "});
3862
3863 // Manipulate with multiple disjoin selections
3864 cx.set_state(indoc! {"
3865 5«
3866 4
3867 3
3868 2
3869 1ˇ»
3870
3871 dd«dd
3872 ccc
3873 bb
3874 aaaˇ»aa
3875 "});
3876 cx.update_editor(|e, window, cx| {
3877 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3878 });
3879 cx.assert_editor_state(indoc! {"
3880 «1
3881 2
3882 3
3883 4
3884 5ˇ»
3885
3886 «aaaaa
3887 bb
3888 ccc
3889 ddddˇ»
3890 "});
3891
3892 // Adding lines on each selection
3893 cx.set_state(indoc! {"
3894 2«
3895 1ˇ»
3896
3897 bb«bb
3898 aaaˇ»aa
3899 "});
3900 cx.update_editor(|e, window, cx| {
3901 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
3902 });
3903 cx.assert_editor_state(indoc! {"
3904 «2
3905 1
3906 added lineˇ»
3907
3908 «bbbb
3909 aaaaa
3910 added lineˇ»
3911 "});
3912
3913 // Removing lines on each selection
3914 cx.set_state(indoc! {"
3915 2«
3916 1ˇ»
3917
3918 bb«bb
3919 aaaˇ»aa
3920 "});
3921 cx.update_editor(|e, window, cx| {
3922 e.manipulate_lines(window, cx, |lines| {
3923 lines.pop();
3924 })
3925 });
3926 cx.assert_editor_state(indoc! {"
3927 «2ˇ»
3928
3929 «bbbbˇ»
3930 "});
3931}
3932
3933#[gpui::test]
3934async fn test_toggle_case(cx: &mut TestAppContext) {
3935 init_test(cx, |_| {});
3936
3937 let mut cx = EditorTestContext::new(cx).await;
3938
3939 // If all lower case -> upper case
3940 cx.set_state(indoc! {"
3941 «hello worldˇ»
3942 "});
3943 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
3944 cx.assert_editor_state(indoc! {"
3945 «HELLO WORLDˇ»
3946 "});
3947
3948 // If all upper case -> lower case
3949 cx.set_state(indoc! {"
3950 «HELLO WORLDˇ»
3951 "});
3952 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
3953 cx.assert_editor_state(indoc! {"
3954 «hello worldˇ»
3955 "});
3956
3957 // If any upper case characters are identified -> lower case
3958 // This matches JetBrains IDEs
3959 cx.set_state(indoc! {"
3960 «hEllo worldˇ»
3961 "});
3962 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
3963 cx.assert_editor_state(indoc! {"
3964 «hello worldˇ»
3965 "});
3966}
3967
3968#[gpui::test]
3969async fn test_manipulate_text(cx: &mut TestAppContext) {
3970 init_test(cx, |_| {});
3971
3972 let mut cx = EditorTestContext::new(cx).await;
3973
3974 // Test convert_to_upper_case()
3975 cx.set_state(indoc! {"
3976 «hello worldˇ»
3977 "});
3978 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3979 cx.assert_editor_state(indoc! {"
3980 «HELLO WORLDˇ»
3981 "});
3982
3983 // Test convert_to_lower_case()
3984 cx.set_state(indoc! {"
3985 «HELLO WORLDˇ»
3986 "});
3987 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
3988 cx.assert_editor_state(indoc! {"
3989 «hello worldˇ»
3990 "});
3991
3992 // Test multiple line, single selection case
3993 cx.set_state(indoc! {"
3994 «The quick brown
3995 fox jumps over
3996 the lazy dogˇ»
3997 "});
3998 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
3999 cx.assert_editor_state(indoc! {"
4000 «The Quick Brown
4001 Fox Jumps Over
4002 The Lazy Dogˇ»
4003 "});
4004
4005 // Test multiple line, single selection case
4006 cx.set_state(indoc! {"
4007 «The quick brown
4008 fox jumps over
4009 the lazy dogˇ»
4010 "});
4011 cx.update_editor(|e, window, cx| {
4012 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4013 });
4014 cx.assert_editor_state(indoc! {"
4015 «TheQuickBrown
4016 FoxJumpsOver
4017 TheLazyDogˇ»
4018 "});
4019
4020 // From here on out, test more complex cases of manipulate_text()
4021
4022 // Test no selection case - should affect words cursors are in
4023 // Cursor at beginning, middle, and end of word
4024 cx.set_state(indoc! {"
4025 ˇhello big beauˇtiful worldˇ
4026 "});
4027 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4028 cx.assert_editor_state(indoc! {"
4029 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4030 "});
4031
4032 // Test multiple selections on a single line and across multiple lines
4033 cx.set_state(indoc! {"
4034 «Theˇ» quick «brown
4035 foxˇ» jumps «overˇ»
4036 the «lazyˇ» dog
4037 "});
4038 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4039 cx.assert_editor_state(indoc! {"
4040 «THEˇ» quick «BROWN
4041 FOXˇ» jumps «OVERˇ»
4042 the «LAZYˇ» dog
4043 "});
4044
4045 // Test case where text length grows
4046 cx.set_state(indoc! {"
4047 «tschüߡ»
4048 "});
4049 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4050 cx.assert_editor_state(indoc! {"
4051 «TSCHÜSSˇ»
4052 "});
4053
4054 // Test to make sure we don't crash when text shrinks
4055 cx.set_state(indoc! {"
4056 aaa_bbbˇ
4057 "});
4058 cx.update_editor(|e, window, cx| {
4059 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4060 });
4061 cx.assert_editor_state(indoc! {"
4062 «aaaBbbˇ»
4063 "});
4064
4065 // Test to make sure we all aware of the fact that each word can grow and shrink
4066 // Final selections should be aware of this fact
4067 cx.set_state(indoc! {"
4068 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4069 "});
4070 cx.update_editor(|e, window, cx| {
4071 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4072 });
4073 cx.assert_editor_state(indoc! {"
4074 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4075 "});
4076
4077 cx.set_state(indoc! {"
4078 «hElLo, WoRld!ˇ»
4079 "});
4080 cx.update_editor(|e, window, cx| {
4081 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4082 });
4083 cx.assert_editor_state(indoc! {"
4084 «HeLlO, wOrLD!ˇ»
4085 "});
4086}
4087
4088#[gpui::test]
4089fn test_duplicate_line(cx: &mut TestAppContext) {
4090 init_test(cx, |_| {});
4091
4092 let editor = cx.add_window(|window, cx| {
4093 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4094 build_editor(buffer, window, cx)
4095 });
4096 _ = editor.update(cx, |editor, window, cx| {
4097 editor.change_selections(None, window, cx, |s| {
4098 s.select_display_ranges([
4099 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4100 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4101 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4102 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4103 ])
4104 });
4105 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4106 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4107 assert_eq!(
4108 editor.selections.display_ranges(cx),
4109 vec![
4110 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4111 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4112 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4113 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4114 ]
4115 );
4116 });
4117
4118 let editor = cx.add_window(|window, cx| {
4119 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4120 build_editor(buffer, window, cx)
4121 });
4122 _ = editor.update(cx, |editor, window, cx| {
4123 editor.change_selections(None, window, cx, |s| {
4124 s.select_display_ranges([
4125 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4126 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4127 ])
4128 });
4129 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4130 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4131 assert_eq!(
4132 editor.selections.display_ranges(cx),
4133 vec![
4134 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4135 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4136 ]
4137 );
4138 });
4139
4140 // With `move_upwards` the selections stay in place, except for
4141 // the lines inserted above them
4142 let editor = cx.add_window(|window, cx| {
4143 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4144 build_editor(buffer, window, cx)
4145 });
4146 _ = editor.update(cx, |editor, window, cx| {
4147 editor.change_selections(None, window, cx, |s| {
4148 s.select_display_ranges([
4149 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4150 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4151 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4152 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4153 ])
4154 });
4155 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4156 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4157 assert_eq!(
4158 editor.selections.display_ranges(cx),
4159 vec![
4160 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4161 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4162 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4163 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4164 ]
4165 );
4166 });
4167
4168 let editor = cx.add_window(|window, cx| {
4169 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4170 build_editor(buffer, window, cx)
4171 });
4172 _ = editor.update(cx, |editor, window, cx| {
4173 editor.change_selections(None, window, cx, |s| {
4174 s.select_display_ranges([
4175 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4176 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4177 ])
4178 });
4179 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4180 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4181 assert_eq!(
4182 editor.selections.display_ranges(cx),
4183 vec![
4184 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4185 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4186 ]
4187 );
4188 });
4189
4190 let editor = cx.add_window(|window, cx| {
4191 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4192 build_editor(buffer, window, cx)
4193 });
4194 _ = editor.update(cx, |editor, window, cx| {
4195 editor.change_selections(None, window, cx, |s| {
4196 s.select_display_ranges([
4197 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4198 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4199 ])
4200 });
4201 editor.duplicate_selection(&DuplicateSelection, window, cx);
4202 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4203 assert_eq!(
4204 editor.selections.display_ranges(cx),
4205 vec![
4206 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4207 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4208 ]
4209 );
4210 });
4211}
4212
4213#[gpui::test]
4214fn test_move_line_up_down(cx: &mut TestAppContext) {
4215 init_test(cx, |_| {});
4216
4217 let editor = cx.add_window(|window, cx| {
4218 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4219 build_editor(buffer, window, cx)
4220 });
4221 _ = editor.update(cx, |editor, window, cx| {
4222 editor.fold_creases(
4223 vec![
4224 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4225 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4226 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4227 ],
4228 true,
4229 window,
4230 cx,
4231 );
4232 editor.change_selections(None, window, cx, |s| {
4233 s.select_display_ranges([
4234 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4235 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4236 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4237 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4238 ])
4239 });
4240 assert_eq!(
4241 editor.display_text(cx),
4242 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4243 );
4244
4245 editor.move_line_up(&MoveLineUp, window, cx);
4246 assert_eq!(
4247 editor.display_text(cx),
4248 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4249 );
4250 assert_eq!(
4251 editor.selections.display_ranges(cx),
4252 vec![
4253 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4254 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4255 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4256 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4257 ]
4258 );
4259 });
4260
4261 _ = editor.update(cx, |editor, window, cx| {
4262 editor.move_line_down(&MoveLineDown, window, cx);
4263 assert_eq!(
4264 editor.display_text(cx),
4265 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4266 );
4267 assert_eq!(
4268 editor.selections.display_ranges(cx),
4269 vec![
4270 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4271 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4272 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4273 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4274 ]
4275 );
4276 });
4277
4278 _ = editor.update(cx, |editor, window, cx| {
4279 editor.move_line_down(&MoveLineDown, window, cx);
4280 assert_eq!(
4281 editor.display_text(cx),
4282 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4283 );
4284 assert_eq!(
4285 editor.selections.display_ranges(cx),
4286 vec![
4287 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4288 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4289 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4290 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4291 ]
4292 );
4293 });
4294
4295 _ = editor.update(cx, |editor, window, cx| {
4296 editor.move_line_up(&MoveLineUp, window, cx);
4297 assert_eq!(
4298 editor.display_text(cx),
4299 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4300 );
4301 assert_eq!(
4302 editor.selections.display_ranges(cx),
4303 vec![
4304 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4305 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4306 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4307 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4308 ]
4309 );
4310 });
4311}
4312
4313#[gpui::test]
4314fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4315 init_test(cx, |_| {});
4316
4317 let editor = cx.add_window(|window, cx| {
4318 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4319 build_editor(buffer, window, cx)
4320 });
4321 _ = editor.update(cx, |editor, window, cx| {
4322 let snapshot = editor.buffer.read(cx).snapshot(cx);
4323 editor.insert_blocks(
4324 [BlockProperties {
4325 style: BlockStyle::Fixed,
4326 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4327 height: Some(1),
4328 render: Arc::new(|_| div().into_any()),
4329 priority: 0,
4330 }],
4331 Some(Autoscroll::fit()),
4332 cx,
4333 );
4334 editor.change_selections(None, window, cx, |s| {
4335 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4336 });
4337 editor.move_line_down(&MoveLineDown, window, cx);
4338 });
4339}
4340
4341#[gpui::test]
4342async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4343 init_test(cx, |_| {});
4344
4345 let mut cx = EditorTestContext::new(cx).await;
4346 cx.set_state(
4347 &"
4348 ˇzero
4349 one
4350 two
4351 three
4352 four
4353 five
4354 "
4355 .unindent(),
4356 );
4357
4358 // Create a four-line block that replaces three lines of text.
4359 cx.update_editor(|editor, window, cx| {
4360 let snapshot = editor.snapshot(window, cx);
4361 let snapshot = &snapshot.buffer_snapshot;
4362 let placement = BlockPlacement::Replace(
4363 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4364 );
4365 editor.insert_blocks(
4366 [BlockProperties {
4367 placement,
4368 height: Some(4),
4369 style: BlockStyle::Sticky,
4370 render: Arc::new(|_| gpui::div().into_any_element()),
4371 priority: 0,
4372 }],
4373 None,
4374 cx,
4375 );
4376 });
4377
4378 // Move down so that the cursor touches the block.
4379 cx.update_editor(|editor, window, cx| {
4380 editor.move_down(&Default::default(), window, cx);
4381 });
4382 cx.assert_editor_state(
4383 &"
4384 zero
4385 «one
4386 two
4387 threeˇ»
4388 four
4389 five
4390 "
4391 .unindent(),
4392 );
4393
4394 // Move down past the block.
4395 cx.update_editor(|editor, window, cx| {
4396 editor.move_down(&Default::default(), window, cx);
4397 });
4398 cx.assert_editor_state(
4399 &"
4400 zero
4401 one
4402 two
4403 three
4404 ˇfour
4405 five
4406 "
4407 .unindent(),
4408 );
4409}
4410
4411#[gpui::test]
4412fn test_transpose(cx: &mut TestAppContext) {
4413 init_test(cx, |_| {});
4414
4415 _ = cx.add_window(|window, cx| {
4416 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4417 editor.set_style(EditorStyle::default(), window, cx);
4418 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4419 editor.transpose(&Default::default(), window, cx);
4420 assert_eq!(editor.text(cx), "bac");
4421 assert_eq!(editor.selections.ranges(cx), [2..2]);
4422
4423 editor.transpose(&Default::default(), window, cx);
4424 assert_eq!(editor.text(cx), "bca");
4425 assert_eq!(editor.selections.ranges(cx), [3..3]);
4426
4427 editor.transpose(&Default::default(), window, cx);
4428 assert_eq!(editor.text(cx), "bac");
4429 assert_eq!(editor.selections.ranges(cx), [3..3]);
4430
4431 editor
4432 });
4433
4434 _ = cx.add_window(|window, cx| {
4435 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4436 editor.set_style(EditorStyle::default(), window, cx);
4437 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4438 editor.transpose(&Default::default(), window, cx);
4439 assert_eq!(editor.text(cx), "acb\nde");
4440 assert_eq!(editor.selections.ranges(cx), [3..3]);
4441
4442 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4443 editor.transpose(&Default::default(), window, cx);
4444 assert_eq!(editor.text(cx), "acbd\ne");
4445 assert_eq!(editor.selections.ranges(cx), [5..5]);
4446
4447 editor.transpose(&Default::default(), window, cx);
4448 assert_eq!(editor.text(cx), "acbde\n");
4449 assert_eq!(editor.selections.ranges(cx), [6..6]);
4450
4451 editor.transpose(&Default::default(), window, cx);
4452 assert_eq!(editor.text(cx), "acbd\ne");
4453 assert_eq!(editor.selections.ranges(cx), [6..6]);
4454
4455 editor
4456 });
4457
4458 _ = cx.add_window(|window, cx| {
4459 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4460 editor.set_style(EditorStyle::default(), window, cx);
4461 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4462 editor.transpose(&Default::default(), window, cx);
4463 assert_eq!(editor.text(cx), "bacd\ne");
4464 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4465
4466 editor.transpose(&Default::default(), window, cx);
4467 assert_eq!(editor.text(cx), "bcade\n");
4468 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4469
4470 editor.transpose(&Default::default(), window, cx);
4471 assert_eq!(editor.text(cx), "bcda\ne");
4472 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4473
4474 editor.transpose(&Default::default(), window, cx);
4475 assert_eq!(editor.text(cx), "bcade\n");
4476 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4477
4478 editor.transpose(&Default::default(), window, cx);
4479 assert_eq!(editor.text(cx), "bcaed\n");
4480 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4481
4482 editor
4483 });
4484
4485 _ = cx.add_window(|window, cx| {
4486 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4487 editor.set_style(EditorStyle::default(), window, cx);
4488 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4489 editor.transpose(&Default::default(), window, cx);
4490 assert_eq!(editor.text(cx), "🏀🍐✋");
4491 assert_eq!(editor.selections.ranges(cx), [8..8]);
4492
4493 editor.transpose(&Default::default(), window, cx);
4494 assert_eq!(editor.text(cx), "🏀✋🍐");
4495 assert_eq!(editor.selections.ranges(cx), [11..11]);
4496
4497 editor.transpose(&Default::default(), window, cx);
4498 assert_eq!(editor.text(cx), "🏀🍐✋");
4499 assert_eq!(editor.selections.ranges(cx), [11..11]);
4500
4501 editor
4502 });
4503}
4504
4505#[gpui::test]
4506async fn test_rewrap(cx: &mut TestAppContext) {
4507 init_test(cx, |settings| {
4508 settings.languages.extend([
4509 (
4510 "Markdown".into(),
4511 LanguageSettingsContent {
4512 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4513 ..Default::default()
4514 },
4515 ),
4516 (
4517 "Plain Text".into(),
4518 LanguageSettingsContent {
4519 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4520 ..Default::default()
4521 },
4522 ),
4523 ])
4524 });
4525
4526 let mut cx = EditorTestContext::new(cx).await;
4527
4528 let language_with_c_comments = Arc::new(Language::new(
4529 LanguageConfig {
4530 line_comments: vec!["// ".into()],
4531 ..LanguageConfig::default()
4532 },
4533 None,
4534 ));
4535 let language_with_pound_comments = Arc::new(Language::new(
4536 LanguageConfig {
4537 line_comments: vec!["# ".into()],
4538 ..LanguageConfig::default()
4539 },
4540 None,
4541 ));
4542 let markdown_language = Arc::new(Language::new(
4543 LanguageConfig {
4544 name: "Markdown".into(),
4545 ..LanguageConfig::default()
4546 },
4547 None,
4548 ));
4549 let language_with_doc_comments = Arc::new(Language::new(
4550 LanguageConfig {
4551 line_comments: vec!["// ".into(), "/// ".into()],
4552 ..LanguageConfig::default()
4553 },
4554 Some(tree_sitter_rust::LANGUAGE.into()),
4555 ));
4556
4557 let plaintext_language = Arc::new(Language::new(
4558 LanguageConfig {
4559 name: "Plain Text".into(),
4560 ..LanguageConfig::default()
4561 },
4562 None,
4563 ));
4564
4565 assert_rewrap(
4566 indoc! {"
4567 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4568 "},
4569 indoc! {"
4570 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4571 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4572 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4573 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4574 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4575 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4576 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4577 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4578 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4579 // porttitor id. Aliquam id accumsan eros.
4580 "},
4581 language_with_c_comments.clone(),
4582 &mut cx,
4583 );
4584
4585 // Test that rewrapping works inside of a selection
4586 assert_rewrap(
4587 indoc! {"
4588 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.ˇ»
4589 "},
4590 indoc! {"
4591 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4592 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4593 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4594 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4595 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4596 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4597 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4598 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4599 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4600 // porttitor id. Aliquam id accumsan eros.ˇ»
4601 "},
4602 language_with_c_comments.clone(),
4603 &mut cx,
4604 );
4605
4606 // Test that cursors that expand to the same region are collapsed.
4607 assert_rewrap(
4608 indoc! {"
4609 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4610 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4611 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4612 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4613 "},
4614 indoc! {"
4615 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4616 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4617 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
4618 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4619 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
4620 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4621 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4622 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4623 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4624 // porttitor id. Aliquam id accumsan eros.
4625 "},
4626 language_with_c_comments.clone(),
4627 &mut cx,
4628 );
4629
4630 // Test that non-contiguous selections are treated separately.
4631 assert_rewrap(
4632 indoc! {"
4633 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4634 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4635 //
4636 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4637 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4638 "},
4639 indoc! {"
4640 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4641 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4642 // auctor, eu lacinia sapien scelerisque.
4643 //
4644 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
4645 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4646 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
4647 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
4648 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
4649 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
4650 // vulputate turpis porttitor id. Aliquam id accumsan eros.
4651 "},
4652 language_with_c_comments.clone(),
4653 &mut cx,
4654 );
4655
4656 // Test that different comment prefixes are supported.
4657 assert_rewrap(
4658 indoc! {"
4659 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4660 "},
4661 indoc! {"
4662 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4663 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4664 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4665 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4666 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
4667 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
4668 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
4669 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
4670 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
4671 # accumsan eros.
4672 "},
4673 language_with_pound_comments.clone(),
4674 &mut cx,
4675 );
4676
4677 // Test that rewrapping is ignored outside of comments in most languages.
4678 assert_rewrap(
4679 indoc! {"
4680 /// Adds two numbers.
4681 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4682 fn add(a: u32, b: u32) -> u32 {
4683 a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + bˇ
4684 }
4685 "},
4686 indoc! {"
4687 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
4688 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4689 fn add(a: u32, b: u32) -> u32 {
4690 a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + bˇ
4691 }
4692 "},
4693 language_with_doc_comments.clone(),
4694 &mut cx,
4695 );
4696
4697 // Test that rewrapping works in Markdown and Plain Text languages.
4698 assert_rewrap(
4699 indoc! {"
4700 # Hello
4701
4702 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi.
4703 "},
4704 indoc! {"
4705 # Hello
4706
4707 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4708 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4709 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4710 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4711 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4712 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4713 Integer sit amet scelerisque nisi.
4714 "},
4715 markdown_language,
4716 &mut cx,
4717 );
4718
4719 assert_rewrap(
4720 indoc! {"
4721 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi.
4722 "},
4723 indoc! {"
4724 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4725 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4726 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4727 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4728 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4729 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4730 Integer sit amet scelerisque nisi.
4731 "},
4732 plaintext_language,
4733 &mut cx,
4734 );
4735
4736 // Test rewrapping unaligned comments in a selection.
4737 assert_rewrap(
4738 indoc! {"
4739 fn foo() {
4740 if true {
4741 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4742 // Praesent semper egestas tellus id dignissim.ˇ»
4743 do_something();
4744 } else {
4745 //
4746 }
4747 }
4748 "},
4749 indoc! {"
4750 fn foo() {
4751 if true {
4752 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4753 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4754 // egestas tellus id dignissim.ˇ»
4755 do_something();
4756 } else {
4757 //
4758 }
4759 }
4760 "},
4761 language_with_doc_comments.clone(),
4762 &mut cx,
4763 );
4764
4765 assert_rewrap(
4766 indoc! {"
4767 fn foo() {
4768 if true {
4769 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4770 // Praesent semper egestas tellus id dignissim.»
4771 do_something();
4772 } else {
4773 //
4774 }
4775
4776 }
4777 "},
4778 indoc! {"
4779 fn foo() {
4780 if true {
4781 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4782 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4783 // egestas tellus id dignissim.»
4784 do_something();
4785 } else {
4786 //
4787 }
4788
4789 }
4790 "},
4791 language_with_doc_comments.clone(),
4792 &mut cx,
4793 );
4794
4795 #[track_caller]
4796 fn assert_rewrap(
4797 unwrapped_text: &str,
4798 wrapped_text: &str,
4799 language: Arc<Language>,
4800 cx: &mut EditorTestContext,
4801 ) {
4802 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4803 cx.set_state(unwrapped_text);
4804 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
4805 cx.assert_editor_state(wrapped_text);
4806 }
4807}
4808
4809#[gpui::test]
4810async fn test_hard_wrap(cx: &mut TestAppContext) {
4811 init_test(cx, |_| {});
4812 let mut cx = EditorTestContext::new(cx).await;
4813
4814 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
4815 cx.update_editor(|editor, _, cx| {
4816 editor.set_hard_wrap(Some(14), cx);
4817 });
4818
4819 cx.set_state(indoc!(
4820 "
4821 one two three ˇ
4822 "
4823 ));
4824 cx.simulate_input("four");
4825 cx.run_until_parked();
4826
4827 cx.assert_editor_state(indoc!(
4828 "
4829 one two three
4830 fourˇ
4831 "
4832 ));
4833
4834 cx.update_editor(|editor, window, cx| {
4835 editor.newline(&Default::default(), window, cx);
4836 });
4837 cx.run_until_parked();
4838 cx.assert_editor_state(indoc!(
4839 "
4840 one two three
4841 four
4842 ˇ
4843 "
4844 ));
4845
4846 cx.simulate_input("five");
4847 cx.run_until_parked();
4848 cx.assert_editor_state(indoc!(
4849 "
4850 one two three
4851 four
4852 fiveˇ
4853 "
4854 ));
4855
4856 cx.update_editor(|editor, window, cx| {
4857 editor.newline(&Default::default(), window, cx);
4858 });
4859 cx.run_until_parked();
4860 cx.simulate_input("# ");
4861 cx.run_until_parked();
4862 cx.assert_editor_state(indoc!(
4863 "
4864 one two three
4865 four
4866 five
4867 # ˇ
4868 "
4869 ));
4870
4871 cx.update_editor(|editor, window, cx| {
4872 editor.newline(&Default::default(), window, cx);
4873 });
4874 cx.run_until_parked();
4875 cx.assert_editor_state(indoc!(
4876 "
4877 one two three
4878 four
4879 five
4880 #\x20
4881 #ˇ
4882 "
4883 ));
4884
4885 cx.simulate_input(" 6");
4886 cx.run_until_parked();
4887 cx.assert_editor_state(indoc!(
4888 "
4889 one two three
4890 four
4891 five
4892 #
4893 # 6ˇ
4894 "
4895 ));
4896}
4897
4898#[gpui::test]
4899async fn test_clipboard(cx: &mut TestAppContext) {
4900 init_test(cx, |_| {});
4901
4902 let mut cx = EditorTestContext::new(cx).await;
4903
4904 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
4905 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4906 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
4907
4908 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
4909 cx.set_state("two ˇfour ˇsix ˇ");
4910 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4911 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
4912
4913 // Paste again but with only two cursors. Since the number of cursors doesn't
4914 // match the number of slices in the clipboard, the entire clipboard text
4915 // is pasted at each cursor.
4916 cx.set_state("ˇtwo one✅ four three six five ˇ");
4917 cx.update_editor(|e, window, cx| {
4918 e.handle_input("( ", window, cx);
4919 e.paste(&Paste, window, cx);
4920 e.handle_input(") ", window, cx);
4921 });
4922 cx.assert_editor_state(
4923 &([
4924 "( one✅ ",
4925 "three ",
4926 "five ) ˇtwo one✅ four three six five ( one✅ ",
4927 "three ",
4928 "five ) ˇ",
4929 ]
4930 .join("\n")),
4931 );
4932
4933 // Cut with three selections, one of which is full-line.
4934 cx.set_state(indoc! {"
4935 1«2ˇ»3
4936 4ˇ567
4937 «8ˇ»9"});
4938 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4939 cx.assert_editor_state(indoc! {"
4940 1ˇ3
4941 ˇ9"});
4942
4943 // Paste with three selections, noticing how the copied selection that was full-line
4944 // gets inserted before the second cursor.
4945 cx.set_state(indoc! {"
4946 1ˇ3
4947 9ˇ
4948 «oˇ»ne"});
4949 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4950 cx.assert_editor_state(indoc! {"
4951 12ˇ3
4952 4567
4953 9ˇ
4954 8ˇne"});
4955
4956 // Copy with a single cursor only, which writes the whole line into the clipboard.
4957 cx.set_state(indoc! {"
4958 The quick brown
4959 fox juˇmps over
4960 the lazy dog"});
4961 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4962 assert_eq!(
4963 cx.read_from_clipboard()
4964 .and_then(|item| item.text().as_deref().map(str::to_string)),
4965 Some("fox jumps over\n".to_string())
4966 );
4967
4968 // Paste with three selections, noticing how the copied full-line selection is inserted
4969 // before the empty selections but replaces the selection that is non-empty.
4970 cx.set_state(indoc! {"
4971 Tˇhe quick brown
4972 «foˇ»x jumps over
4973 tˇhe lazy dog"});
4974 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4975 cx.assert_editor_state(indoc! {"
4976 fox jumps over
4977 Tˇhe quick brown
4978 fox jumps over
4979 ˇx jumps over
4980 fox jumps over
4981 tˇhe lazy dog"});
4982}
4983
4984#[gpui::test]
4985async fn test_copy_trim(cx: &mut TestAppContext) {
4986 init_test(cx, |_| {});
4987
4988 let mut cx = EditorTestContext::new(cx).await;
4989 cx.set_state(
4990 r#" «for selection in selections.iter() {
4991 let mut start = selection.start;
4992 let mut end = selection.end;
4993 let is_entire_line = selection.is_empty();
4994 if is_entire_line {
4995 start = Point::new(start.row, 0);ˇ»
4996 end = cmp::min(max_point, Point::new(end.row + 1, 0));
4997 }
4998 "#,
4999 );
5000 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5001 assert_eq!(
5002 cx.read_from_clipboard()
5003 .and_then(|item| item.text().as_deref().map(str::to_string)),
5004 Some(
5005 "for selection in selections.iter() {
5006 let mut start = selection.start;
5007 let mut end = selection.end;
5008 let is_entire_line = selection.is_empty();
5009 if is_entire_line {
5010 start = Point::new(start.row, 0);"
5011 .to_string()
5012 ),
5013 "Regular copying preserves all indentation selected",
5014 );
5015 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5016 assert_eq!(
5017 cx.read_from_clipboard()
5018 .and_then(|item| item.text().as_deref().map(str::to_string)),
5019 Some(
5020 "for selection in selections.iter() {
5021let mut start = selection.start;
5022let mut end = selection.end;
5023let is_entire_line = selection.is_empty();
5024if is_entire_line {
5025 start = Point::new(start.row, 0);"
5026 .to_string()
5027 ),
5028 "Copying with stripping should strip all leading whitespaces"
5029 );
5030
5031 cx.set_state(
5032 r#" « for selection in selections.iter() {
5033 let mut start = selection.start;
5034 let mut end = selection.end;
5035 let is_entire_line = selection.is_empty();
5036 if is_entire_line {
5037 start = Point::new(start.row, 0);ˇ»
5038 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5039 }
5040 "#,
5041 );
5042 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5043 assert_eq!(
5044 cx.read_from_clipboard()
5045 .and_then(|item| item.text().as_deref().map(str::to_string)),
5046 Some(
5047 " for selection in selections.iter() {
5048 let mut start = selection.start;
5049 let mut end = selection.end;
5050 let is_entire_line = selection.is_empty();
5051 if is_entire_line {
5052 start = Point::new(start.row, 0);"
5053 .to_string()
5054 ),
5055 "Regular copying preserves all indentation selected",
5056 );
5057 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5058 assert_eq!(
5059 cx.read_from_clipboard()
5060 .and_then(|item| item.text().as_deref().map(str::to_string)),
5061 Some(
5062 "for selection in selections.iter() {
5063let mut start = selection.start;
5064let mut end = selection.end;
5065let is_entire_line = selection.is_empty();
5066if is_entire_line {
5067 start = Point::new(start.row, 0);"
5068 .to_string()
5069 ),
5070 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5071 );
5072
5073 cx.set_state(
5074 r#" «ˇ for selection in selections.iter() {
5075 let mut start = selection.start;
5076 let mut end = selection.end;
5077 let is_entire_line = selection.is_empty();
5078 if is_entire_line {
5079 start = Point::new(start.row, 0);»
5080 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5081 }
5082 "#,
5083 );
5084 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5085 assert_eq!(
5086 cx.read_from_clipboard()
5087 .and_then(|item| item.text().as_deref().map(str::to_string)),
5088 Some(
5089 " for selection in selections.iter() {
5090 let mut start = selection.start;
5091 let mut end = selection.end;
5092 let is_entire_line = selection.is_empty();
5093 if is_entire_line {
5094 start = Point::new(start.row, 0);"
5095 .to_string()
5096 ),
5097 "Regular copying for reverse selection works the same",
5098 );
5099 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5100 assert_eq!(
5101 cx.read_from_clipboard()
5102 .and_then(|item| item.text().as_deref().map(str::to_string)),
5103 Some(
5104 "for selection in selections.iter() {
5105let mut start = selection.start;
5106let mut end = selection.end;
5107let is_entire_line = selection.is_empty();
5108if is_entire_line {
5109 start = Point::new(start.row, 0);"
5110 .to_string()
5111 ),
5112 "Copying with stripping for reverse selection works the same"
5113 );
5114
5115 cx.set_state(
5116 r#" for selection «in selections.iter() {
5117 let mut start = selection.start;
5118 let mut end = selection.end;
5119 let is_entire_line = selection.is_empty();
5120 if is_entire_line {
5121 start = Point::new(start.row, 0);ˇ»
5122 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5123 }
5124 "#,
5125 );
5126 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5127 assert_eq!(
5128 cx.read_from_clipboard()
5129 .and_then(|item| item.text().as_deref().map(str::to_string)),
5130 Some(
5131 "in selections.iter() {
5132 let mut start = selection.start;
5133 let mut end = selection.end;
5134 let is_entire_line = selection.is_empty();
5135 if is_entire_line {
5136 start = Point::new(start.row, 0);"
5137 .to_string()
5138 ),
5139 "When selecting past the indent, the copying works as usual",
5140 );
5141 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5142 assert_eq!(
5143 cx.read_from_clipboard()
5144 .and_then(|item| item.text().as_deref().map(str::to_string)),
5145 Some(
5146 "in selections.iter() {
5147 let mut start = selection.start;
5148 let mut end = selection.end;
5149 let is_entire_line = selection.is_empty();
5150 if is_entire_line {
5151 start = Point::new(start.row, 0);"
5152 .to_string()
5153 ),
5154 "When selecting past the indent, nothing is trimmed"
5155 );
5156
5157 cx.set_state(
5158 r#" «for selection in selections.iter() {
5159 let mut start = selection.start;
5160
5161 let mut end = selection.end;
5162 let is_entire_line = selection.is_empty();
5163 if is_entire_line {
5164 start = Point::new(start.row, 0);
5165ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
5166 }
5167 "#,
5168 );
5169 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5170 assert_eq!(
5171 cx.read_from_clipboard()
5172 .and_then(|item| item.text().as_deref().map(str::to_string)),
5173 Some(
5174 "for selection in selections.iter() {
5175let mut start = selection.start;
5176
5177let mut end = selection.end;
5178let is_entire_line = selection.is_empty();
5179if is_entire_line {
5180 start = Point::new(start.row, 0);
5181"
5182 .to_string()
5183 ),
5184 "Copying with stripping should ignore empty lines"
5185 );
5186}
5187
5188#[gpui::test]
5189async fn test_paste_multiline(cx: &mut TestAppContext) {
5190 init_test(cx, |_| {});
5191
5192 let mut cx = EditorTestContext::new(cx).await;
5193 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5194
5195 // Cut an indented block, without the leading whitespace.
5196 cx.set_state(indoc! {"
5197 const a: B = (
5198 c(),
5199 «d(
5200 e,
5201 f
5202 )ˇ»
5203 );
5204 "});
5205 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5206 cx.assert_editor_state(indoc! {"
5207 const a: B = (
5208 c(),
5209 ˇ
5210 );
5211 "});
5212
5213 // Paste it at the same position.
5214 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5215 cx.assert_editor_state(indoc! {"
5216 const a: B = (
5217 c(),
5218 d(
5219 e,
5220 f
5221 )ˇ
5222 );
5223 "});
5224
5225 // Paste it at a line with a lower indent level.
5226 cx.set_state(indoc! {"
5227 ˇ
5228 const a: B = (
5229 c(),
5230 );
5231 "});
5232 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5233 cx.assert_editor_state(indoc! {"
5234 d(
5235 e,
5236 f
5237 )ˇ
5238 const a: B = (
5239 c(),
5240 );
5241 "});
5242
5243 // Cut an indented block, with the leading whitespace.
5244 cx.set_state(indoc! {"
5245 const a: B = (
5246 c(),
5247 « d(
5248 e,
5249 f
5250 )
5251 ˇ»);
5252 "});
5253 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5254 cx.assert_editor_state(indoc! {"
5255 const a: B = (
5256 c(),
5257 ˇ);
5258 "});
5259
5260 // Paste it at the same position.
5261 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5262 cx.assert_editor_state(indoc! {"
5263 const a: B = (
5264 c(),
5265 d(
5266 e,
5267 f
5268 )
5269 ˇ);
5270 "});
5271
5272 // Paste it at a line with a higher indent level.
5273 cx.set_state(indoc! {"
5274 const a: B = (
5275 c(),
5276 d(
5277 e,
5278 fˇ
5279 )
5280 );
5281 "});
5282 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5283 cx.assert_editor_state(indoc! {"
5284 const a: B = (
5285 c(),
5286 d(
5287 e,
5288 f d(
5289 e,
5290 f
5291 )
5292 ˇ
5293 )
5294 );
5295 "});
5296
5297 // Copy an indented block, starting mid-line
5298 cx.set_state(indoc! {"
5299 const a: B = (
5300 c(),
5301 somethin«g(
5302 e,
5303 f
5304 )ˇ»
5305 );
5306 "});
5307 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5308
5309 // Paste it on a line with a lower indent level
5310 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
5311 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5312 cx.assert_editor_state(indoc! {"
5313 const a: B = (
5314 c(),
5315 something(
5316 e,
5317 f
5318 )
5319 );
5320 g(
5321 e,
5322 f
5323 )ˇ"});
5324}
5325
5326#[gpui::test]
5327async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
5328 init_test(cx, |_| {});
5329
5330 cx.write_to_clipboard(ClipboardItem::new_string(
5331 " d(\n e\n );\n".into(),
5332 ));
5333
5334 let mut cx = EditorTestContext::new(cx).await;
5335 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5336
5337 cx.set_state(indoc! {"
5338 fn a() {
5339 b();
5340 if c() {
5341 ˇ
5342 }
5343 }
5344 "});
5345
5346 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5347 cx.assert_editor_state(indoc! {"
5348 fn a() {
5349 b();
5350 if c() {
5351 d(
5352 e
5353 );
5354 ˇ
5355 }
5356 }
5357 "});
5358
5359 cx.set_state(indoc! {"
5360 fn a() {
5361 b();
5362 ˇ
5363 }
5364 "});
5365
5366 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5367 cx.assert_editor_state(indoc! {"
5368 fn a() {
5369 b();
5370 d(
5371 e
5372 );
5373 ˇ
5374 }
5375 "});
5376}
5377
5378#[gpui::test]
5379fn test_select_all(cx: &mut TestAppContext) {
5380 init_test(cx, |_| {});
5381
5382 let editor = cx.add_window(|window, cx| {
5383 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
5384 build_editor(buffer, window, cx)
5385 });
5386 _ = editor.update(cx, |editor, window, cx| {
5387 editor.select_all(&SelectAll, window, cx);
5388 assert_eq!(
5389 editor.selections.display_ranges(cx),
5390 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
5391 );
5392 });
5393}
5394
5395#[gpui::test]
5396fn test_select_line(cx: &mut TestAppContext) {
5397 init_test(cx, |_| {});
5398
5399 let editor = cx.add_window(|window, cx| {
5400 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
5401 build_editor(buffer, window, cx)
5402 });
5403 _ = editor.update(cx, |editor, window, cx| {
5404 editor.change_selections(None, window, cx, |s| {
5405 s.select_display_ranges([
5406 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5407 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5408 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5409 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
5410 ])
5411 });
5412 editor.select_line(&SelectLine, window, cx);
5413 assert_eq!(
5414 editor.selections.display_ranges(cx),
5415 vec![
5416 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
5417 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
5418 ]
5419 );
5420 });
5421
5422 _ = editor.update(cx, |editor, window, cx| {
5423 editor.select_line(&SelectLine, window, cx);
5424 assert_eq!(
5425 editor.selections.display_ranges(cx),
5426 vec![
5427 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
5428 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
5429 ]
5430 );
5431 });
5432
5433 _ = editor.update(cx, |editor, window, cx| {
5434 editor.select_line(&SelectLine, window, cx);
5435 assert_eq!(
5436 editor.selections.display_ranges(cx),
5437 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
5438 );
5439 });
5440}
5441
5442#[gpui::test]
5443async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
5444 init_test(cx, |_| {});
5445 let mut cx = EditorTestContext::new(cx).await;
5446
5447 #[track_caller]
5448 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
5449 cx.set_state(initial_state);
5450 cx.update_editor(|e, window, cx| {
5451 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
5452 });
5453 cx.assert_editor_state(expected_state);
5454 }
5455
5456 // Selection starts and ends at the middle of lines, left-to-right
5457 test(
5458 &mut cx,
5459 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
5460 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5461 );
5462 // Same thing, right-to-left
5463 test(
5464 &mut cx,
5465 "aa\nb«b\ncc\ndd\neˇ»e\nff",
5466 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5467 );
5468
5469 // Whole buffer, left-to-right, last line *doesn't* end with newline
5470 test(
5471 &mut cx,
5472 "«ˇaa\nbb\ncc\ndd\nee\nff»",
5473 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5474 );
5475 // Same thing, right-to-left
5476 test(
5477 &mut cx,
5478 "«aa\nbb\ncc\ndd\nee\nffˇ»",
5479 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5480 );
5481
5482 // Whole buffer, left-to-right, last line ends with newline
5483 test(
5484 &mut cx,
5485 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
5486 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5487 );
5488 // Same thing, right-to-left
5489 test(
5490 &mut cx,
5491 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
5492 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5493 );
5494
5495 // Starts at the end of a line, ends at the start of another
5496 test(
5497 &mut cx,
5498 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
5499 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
5500 );
5501}
5502
5503#[gpui::test]
5504async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
5505 init_test(cx, |_| {});
5506
5507 let editor = cx.add_window(|window, cx| {
5508 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
5509 build_editor(buffer, window, cx)
5510 });
5511
5512 // setup
5513 _ = editor.update(cx, |editor, window, cx| {
5514 editor.fold_creases(
5515 vec![
5516 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5517 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5518 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5519 ],
5520 true,
5521 window,
5522 cx,
5523 );
5524 assert_eq!(
5525 editor.display_text(cx),
5526 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5527 );
5528 });
5529
5530 _ = editor.update(cx, |editor, window, cx| {
5531 editor.change_selections(None, window, cx, |s| {
5532 s.select_display_ranges([
5533 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5534 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5535 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5536 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
5537 ])
5538 });
5539 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5540 assert_eq!(
5541 editor.display_text(cx),
5542 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5543 );
5544 });
5545 EditorTestContext::for_editor(editor, cx)
5546 .await
5547 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
5548
5549 _ = editor.update(cx, |editor, window, cx| {
5550 editor.change_selections(None, window, cx, |s| {
5551 s.select_display_ranges([
5552 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
5553 ])
5554 });
5555 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5556 assert_eq!(
5557 editor.display_text(cx),
5558 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
5559 );
5560 assert_eq!(
5561 editor.selections.display_ranges(cx),
5562 [
5563 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
5564 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
5565 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
5566 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
5567 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
5568 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
5569 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
5570 ]
5571 );
5572 });
5573 EditorTestContext::for_editor(editor, cx)
5574 .await
5575 .assert_editor_state(
5576 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
5577 );
5578}
5579
5580#[gpui::test]
5581async fn test_add_selection_above_below(cx: &mut TestAppContext) {
5582 init_test(cx, |_| {});
5583
5584 let mut cx = EditorTestContext::new(cx).await;
5585
5586 cx.set_state(indoc!(
5587 r#"abc
5588 defˇghi
5589
5590 jk
5591 nlmo
5592 "#
5593 ));
5594
5595 cx.update_editor(|editor, window, cx| {
5596 editor.add_selection_above(&Default::default(), window, cx);
5597 });
5598
5599 cx.assert_editor_state(indoc!(
5600 r#"abcˇ
5601 defˇghi
5602
5603 jk
5604 nlmo
5605 "#
5606 ));
5607
5608 cx.update_editor(|editor, window, cx| {
5609 editor.add_selection_above(&Default::default(), window, cx);
5610 });
5611
5612 cx.assert_editor_state(indoc!(
5613 r#"abcˇ
5614 defˇghi
5615
5616 jk
5617 nlmo
5618 "#
5619 ));
5620
5621 cx.update_editor(|editor, window, cx| {
5622 editor.add_selection_below(&Default::default(), window, cx);
5623 });
5624
5625 cx.assert_editor_state(indoc!(
5626 r#"abc
5627 defˇghi
5628
5629 jk
5630 nlmo
5631 "#
5632 ));
5633
5634 cx.update_editor(|editor, window, cx| {
5635 editor.undo_selection(&Default::default(), window, cx);
5636 });
5637
5638 cx.assert_editor_state(indoc!(
5639 r#"abcˇ
5640 defˇghi
5641
5642 jk
5643 nlmo
5644 "#
5645 ));
5646
5647 cx.update_editor(|editor, window, cx| {
5648 editor.redo_selection(&Default::default(), window, cx);
5649 });
5650
5651 cx.assert_editor_state(indoc!(
5652 r#"abc
5653 defˇghi
5654
5655 jk
5656 nlmo
5657 "#
5658 ));
5659
5660 cx.update_editor(|editor, window, cx| {
5661 editor.add_selection_below(&Default::default(), window, cx);
5662 });
5663
5664 cx.assert_editor_state(indoc!(
5665 r#"abc
5666 defˇghi
5667
5668 jk
5669 nlmˇo
5670 "#
5671 ));
5672
5673 cx.update_editor(|editor, window, cx| {
5674 editor.add_selection_below(&Default::default(), window, cx);
5675 });
5676
5677 cx.assert_editor_state(indoc!(
5678 r#"abc
5679 defˇghi
5680
5681 jk
5682 nlmˇo
5683 "#
5684 ));
5685
5686 // change selections
5687 cx.set_state(indoc!(
5688 r#"abc
5689 def«ˇg»hi
5690
5691 jk
5692 nlmo
5693 "#
5694 ));
5695
5696 cx.update_editor(|editor, window, cx| {
5697 editor.add_selection_below(&Default::default(), window, cx);
5698 });
5699
5700 cx.assert_editor_state(indoc!(
5701 r#"abc
5702 def«ˇg»hi
5703
5704 jk
5705 nlm«ˇo»
5706 "#
5707 ));
5708
5709 cx.update_editor(|editor, window, cx| {
5710 editor.add_selection_below(&Default::default(), window, cx);
5711 });
5712
5713 cx.assert_editor_state(indoc!(
5714 r#"abc
5715 def«ˇg»hi
5716
5717 jk
5718 nlm«ˇo»
5719 "#
5720 ));
5721
5722 cx.update_editor(|editor, window, cx| {
5723 editor.add_selection_above(&Default::default(), window, cx);
5724 });
5725
5726 cx.assert_editor_state(indoc!(
5727 r#"abc
5728 def«ˇg»hi
5729
5730 jk
5731 nlmo
5732 "#
5733 ));
5734
5735 cx.update_editor(|editor, window, cx| {
5736 editor.add_selection_above(&Default::default(), window, cx);
5737 });
5738
5739 cx.assert_editor_state(indoc!(
5740 r#"abc
5741 def«ˇg»hi
5742
5743 jk
5744 nlmo
5745 "#
5746 ));
5747
5748 // Change selections again
5749 cx.set_state(indoc!(
5750 r#"a«bc
5751 defgˇ»hi
5752
5753 jk
5754 nlmo
5755 "#
5756 ));
5757
5758 cx.update_editor(|editor, window, cx| {
5759 editor.add_selection_below(&Default::default(), window, cx);
5760 });
5761
5762 cx.assert_editor_state(indoc!(
5763 r#"a«bcˇ»
5764 d«efgˇ»hi
5765
5766 j«kˇ»
5767 nlmo
5768 "#
5769 ));
5770
5771 cx.update_editor(|editor, window, cx| {
5772 editor.add_selection_below(&Default::default(), window, cx);
5773 });
5774 cx.assert_editor_state(indoc!(
5775 r#"a«bcˇ»
5776 d«efgˇ»hi
5777
5778 j«kˇ»
5779 n«lmoˇ»
5780 "#
5781 ));
5782 cx.update_editor(|editor, window, cx| {
5783 editor.add_selection_above(&Default::default(), window, cx);
5784 });
5785
5786 cx.assert_editor_state(indoc!(
5787 r#"a«bcˇ»
5788 d«efgˇ»hi
5789
5790 j«kˇ»
5791 nlmo
5792 "#
5793 ));
5794
5795 // Change selections again
5796 cx.set_state(indoc!(
5797 r#"abc
5798 d«ˇefghi
5799
5800 jk
5801 nlm»o
5802 "#
5803 ));
5804
5805 cx.update_editor(|editor, window, cx| {
5806 editor.add_selection_above(&Default::default(), window, cx);
5807 });
5808
5809 cx.assert_editor_state(indoc!(
5810 r#"a«ˇbc»
5811 d«ˇef»ghi
5812
5813 j«ˇk»
5814 n«ˇlm»o
5815 "#
5816 ));
5817
5818 cx.update_editor(|editor, window, cx| {
5819 editor.add_selection_below(&Default::default(), window, cx);
5820 });
5821
5822 cx.assert_editor_state(indoc!(
5823 r#"abc
5824 d«ˇef»ghi
5825
5826 j«ˇk»
5827 n«ˇlm»o
5828 "#
5829 ));
5830}
5831
5832#[gpui::test]
5833async fn test_select_next(cx: &mut TestAppContext) {
5834 init_test(cx, |_| {});
5835
5836 let mut cx = EditorTestContext::new(cx).await;
5837 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5838
5839 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5840 .unwrap();
5841 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5842
5843 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5844 .unwrap();
5845 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5846
5847 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5848 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5849
5850 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5851 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5852
5853 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5854 .unwrap();
5855 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5856
5857 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5858 .unwrap();
5859 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5860
5861 // Test selection direction should be preserved
5862 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
5863
5864 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5865 .unwrap();
5866 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
5867}
5868
5869#[gpui::test]
5870async fn test_select_all_matches(cx: &mut TestAppContext) {
5871 init_test(cx, |_| {});
5872
5873 let mut cx = EditorTestContext::new(cx).await;
5874
5875 // Test caret-only selections
5876 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5877 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5878 .unwrap();
5879 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5880
5881 // Test left-to-right selections
5882 cx.set_state("abc\n«abcˇ»\nabc");
5883 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5884 .unwrap();
5885 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
5886
5887 // Test right-to-left selections
5888 cx.set_state("abc\n«ˇabc»\nabc");
5889 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5890 .unwrap();
5891 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
5892
5893 // Test selecting whitespace with caret selection
5894 cx.set_state("abc\nˇ abc\nabc");
5895 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5896 .unwrap();
5897 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
5898
5899 // Test selecting whitespace with left-to-right selection
5900 cx.set_state("abc\n«ˇ »abc\nabc");
5901 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5902 .unwrap();
5903 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
5904
5905 // Test no matches with right-to-left selection
5906 cx.set_state("abc\n« ˇ»abc\nabc");
5907 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5908 .unwrap();
5909 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
5910}
5911
5912#[gpui::test]
5913async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
5914 init_test(cx, |_| {});
5915
5916 let mut cx = EditorTestContext::new(cx).await;
5917
5918 let large_body_1 = "\nd".repeat(200);
5919 let large_body_2 = "\ne".repeat(200);
5920
5921 cx.set_state(&format!(
5922 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
5923 ));
5924 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
5925 let scroll_position = editor.scroll_position(cx);
5926 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
5927 scroll_position
5928 });
5929
5930 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5931 .unwrap();
5932 cx.assert_editor_state(&format!(
5933 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
5934 ));
5935 let scroll_position_after_selection =
5936 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
5937 assert_eq!(
5938 initial_scroll_position, scroll_position_after_selection,
5939 "Scroll position should not change after selecting all matches"
5940 );
5941}
5942
5943#[gpui::test]
5944async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
5945 init_test(cx, |_| {});
5946
5947 let mut cx = EditorLspTestContext::new_rust(
5948 lsp::ServerCapabilities {
5949 document_formatting_provider: Some(lsp::OneOf::Left(true)),
5950 ..Default::default()
5951 },
5952 cx,
5953 )
5954 .await;
5955
5956 cx.set_state(indoc! {"
5957 line 1
5958 line 2
5959 linˇe 3
5960 line 4
5961 line 5
5962 "});
5963
5964 // Make an edit
5965 cx.update_editor(|editor, window, cx| {
5966 editor.handle_input("X", window, cx);
5967 });
5968
5969 // Move cursor to a different position
5970 cx.update_editor(|editor, window, cx| {
5971 editor.change_selections(None, window, cx, |s| {
5972 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
5973 });
5974 });
5975
5976 cx.assert_editor_state(indoc! {"
5977 line 1
5978 line 2
5979 linXe 3
5980 line 4
5981 liˇne 5
5982 "});
5983
5984 cx.lsp
5985 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
5986 Ok(Some(vec![lsp::TextEdit::new(
5987 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
5988 "PREFIX ".to_string(),
5989 )]))
5990 });
5991
5992 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
5993 .unwrap()
5994 .await
5995 .unwrap();
5996
5997 cx.assert_editor_state(indoc! {"
5998 PREFIX line 1
5999 line 2
6000 linXe 3
6001 line 4
6002 liˇne 5
6003 "});
6004
6005 // Undo formatting
6006 cx.update_editor(|editor, window, cx| {
6007 editor.undo(&Default::default(), window, cx);
6008 });
6009
6010 // Verify cursor moved back to position after edit
6011 cx.assert_editor_state(indoc! {"
6012 line 1
6013 line 2
6014 linXˇe 3
6015 line 4
6016 line 5
6017 "});
6018}
6019
6020#[gpui::test]
6021async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
6022 init_test(cx, |_| {});
6023
6024 let mut cx = EditorTestContext::new(cx).await;
6025 cx.set_state(
6026 r#"let foo = 2;
6027lˇet foo = 2;
6028let fooˇ = 2;
6029let foo = 2;
6030let foo = ˇ2;"#,
6031 );
6032
6033 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6034 .unwrap();
6035 cx.assert_editor_state(
6036 r#"let foo = 2;
6037«letˇ» foo = 2;
6038let «fooˇ» = 2;
6039let foo = 2;
6040let foo = «2ˇ»;"#,
6041 );
6042
6043 // noop for multiple selections with different contents
6044 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6045 .unwrap();
6046 cx.assert_editor_state(
6047 r#"let foo = 2;
6048«letˇ» foo = 2;
6049let «fooˇ» = 2;
6050let foo = 2;
6051let foo = «2ˇ»;"#,
6052 );
6053
6054 // Test last selection direction should be preserved
6055 cx.set_state(
6056 r#"let foo = 2;
6057let foo = 2;
6058let «fooˇ» = 2;
6059let «ˇfoo» = 2;
6060let foo = 2;"#,
6061 );
6062
6063 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6064 .unwrap();
6065 cx.assert_editor_state(
6066 r#"let foo = 2;
6067let foo = 2;
6068let «fooˇ» = 2;
6069let «ˇfoo» = 2;
6070let «ˇfoo» = 2;"#,
6071 );
6072}
6073
6074#[gpui::test]
6075async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
6076 init_test(cx, |_| {});
6077
6078 let mut cx =
6079 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
6080
6081 cx.assert_editor_state(indoc! {"
6082 ˇbbb
6083 ccc
6084
6085 bbb
6086 ccc
6087 "});
6088 cx.dispatch_action(SelectPrevious::default());
6089 cx.assert_editor_state(indoc! {"
6090 «bbbˇ»
6091 ccc
6092
6093 bbb
6094 ccc
6095 "});
6096 cx.dispatch_action(SelectPrevious::default());
6097 cx.assert_editor_state(indoc! {"
6098 «bbbˇ»
6099 ccc
6100
6101 «bbbˇ»
6102 ccc
6103 "});
6104}
6105
6106#[gpui::test]
6107async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
6108 init_test(cx, |_| {});
6109
6110 let mut cx = EditorTestContext::new(cx).await;
6111 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6112
6113 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6114 .unwrap();
6115 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6116
6117 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6118 .unwrap();
6119 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6120
6121 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6122 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6123
6124 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6125 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6126
6127 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6128 .unwrap();
6129 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
6130
6131 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6132 .unwrap();
6133 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6134}
6135
6136#[gpui::test]
6137async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
6138 init_test(cx, |_| {});
6139
6140 let mut cx = EditorTestContext::new(cx).await;
6141 cx.set_state("aˇ");
6142
6143 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6144 .unwrap();
6145 cx.assert_editor_state("«aˇ»");
6146 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6147 .unwrap();
6148 cx.assert_editor_state("«aˇ»");
6149}
6150
6151#[gpui::test]
6152async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
6153 init_test(cx, |_| {});
6154
6155 let mut cx = EditorTestContext::new(cx).await;
6156 cx.set_state(
6157 r#"let foo = 2;
6158lˇet foo = 2;
6159let fooˇ = 2;
6160let foo = 2;
6161let foo = ˇ2;"#,
6162 );
6163
6164 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6165 .unwrap();
6166 cx.assert_editor_state(
6167 r#"let foo = 2;
6168«letˇ» foo = 2;
6169let «fooˇ» = 2;
6170let foo = 2;
6171let foo = «2ˇ»;"#,
6172 );
6173
6174 // noop for multiple selections with different contents
6175 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6176 .unwrap();
6177 cx.assert_editor_state(
6178 r#"let foo = 2;
6179«letˇ» foo = 2;
6180let «fooˇ» = 2;
6181let foo = 2;
6182let foo = «2ˇ»;"#,
6183 );
6184}
6185
6186#[gpui::test]
6187async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
6188 init_test(cx, |_| {});
6189
6190 let mut cx = EditorTestContext::new(cx).await;
6191 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6192
6193 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6194 .unwrap();
6195 // selection direction is preserved
6196 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
6197
6198 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6199 .unwrap();
6200 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
6201
6202 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6203 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
6204
6205 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6206 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
6207
6208 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6209 .unwrap();
6210 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
6211
6212 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6213 .unwrap();
6214 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
6215}
6216
6217#[gpui::test]
6218async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
6219 init_test(cx, |_| {});
6220
6221 let language = Arc::new(Language::new(
6222 LanguageConfig::default(),
6223 Some(tree_sitter_rust::LANGUAGE.into()),
6224 ));
6225
6226 let text = r#"
6227 use mod1::mod2::{mod3, mod4};
6228
6229 fn fn_1(param1: bool, param2: &str) {
6230 let var1 = "text";
6231 }
6232 "#
6233 .unindent();
6234
6235 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6236 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6237 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6238
6239 editor
6240 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6241 .await;
6242
6243 editor.update_in(cx, |editor, window, cx| {
6244 editor.change_selections(None, window, cx, |s| {
6245 s.select_display_ranges([
6246 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
6247 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
6248 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
6249 ]);
6250 });
6251 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6252 });
6253 editor.update(cx, |editor, cx| {
6254 assert_text_with_selections(
6255 editor,
6256 indoc! {r#"
6257 use mod1::mod2::{mod3, «mod4ˇ»};
6258
6259 fn fn_1«ˇ(param1: bool, param2: &str)» {
6260 let var1 = "«ˇtext»";
6261 }
6262 "#},
6263 cx,
6264 );
6265 });
6266
6267 editor.update_in(cx, |editor, window, cx| {
6268 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6269 });
6270 editor.update(cx, |editor, cx| {
6271 assert_text_with_selections(
6272 editor,
6273 indoc! {r#"
6274 use mod1::mod2::«{mod3, mod4}ˇ»;
6275
6276 «ˇfn fn_1(param1: bool, param2: &str) {
6277 let var1 = "text";
6278 }»
6279 "#},
6280 cx,
6281 );
6282 });
6283
6284 editor.update_in(cx, |editor, window, cx| {
6285 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6286 });
6287 assert_eq!(
6288 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6289 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6290 );
6291
6292 // Trying to expand the selected syntax node one more time has no effect.
6293 editor.update_in(cx, |editor, window, cx| {
6294 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6295 });
6296 assert_eq!(
6297 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6298 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6299 );
6300
6301 editor.update_in(cx, |editor, window, cx| {
6302 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6303 });
6304 editor.update(cx, |editor, cx| {
6305 assert_text_with_selections(
6306 editor,
6307 indoc! {r#"
6308 use mod1::mod2::«{mod3, mod4}ˇ»;
6309
6310 «ˇfn fn_1(param1: bool, param2: &str) {
6311 let var1 = "text";
6312 }»
6313 "#},
6314 cx,
6315 );
6316 });
6317
6318 editor.update_in(cx, |editor, window, cx| {
6319 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6320 });
6321 editor.update(cx, |editor, cx| {
6322 assert_text_with_selections(
6323 editor,
6324 indoc! {r#"
6325 use mod1::mod2::{mod3, «mod4ˇ»};
6326
6327 fn fn_1«ˇ(param1: bool, param2: &str)» {
6328 let var1 = "«ˇtext»";
6329 }
6330 "#},
6331 cx,
6332 );
6333 });
6334
6335 editor.update_in(cx, |editor, window, cx| {
6336 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6337 });
6338 editor.update(cx, |editor, cx| {
6339 assert_text_with_selections(
6340 editor,
6341 indoc! {r#"
6342 use mod1::mod2::{mod3, mo«ˇ»d4};
6343
6344 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6345 let var1 = "te«ˇ»xt";
6346 }
6347 "#},
6348 cx,
6349 );
6350 });
6351
6352 // Trying to shrink the selected syntax node one more time has no effect.
6353 editor.update_in(cx, |editor, window, cx| {
6354 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6355 });
6356 editor.update_in(cx, |editor, _, cx| {
6357 assert_text_with_selections(
6358 editor,
6359 indoc! {r#"
6360 use mod1::mod2::{mod3, mo«ˇ»d4};
6361
6362 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6363 let var1 = "te«ˇ»xt";
6364 }
6365 "#},
6366 cx,
6367 );
6368 });
6369
6370 // Ensure that we keep expanding the selection if the larger selection starts or ends within
6371 // a fold.
6372 editor.update_in(cx, |editor, window, cx| {
6373 editor.fold_creases(
6374 vec![
6375 Crease::simple(
6376 Point::new(0, 21)..Point::new(0, 24),
6377 FoldPlaceholder::test(),
6378 ),
6379 Crease::simple(
6380 Point::new(3, 20)..Point::new(3, 22),
6381 FoldPlaceholder::test(),
6382 ),
6383 ],
6384 true,
6385 window,
6386 cx,
6387 );
6388 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6389 });
6390 editor.update(cx, |editor, cx| {
6391 assert_text_with_selections(
6392 editor,
6393 indoc! {r#"
6394 use mod1::mod2::«{mod3, mod4}ˇ»;
6395
6396 fn fn_1«ˇ(param1: bool, param2: &str)» {
6397 let var1 = "«ˇtext»";
6398 }
6399 "#},
6400 cx,
6401 );
6402 });
6403}
6404
6405#[gpui::test]
6406async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
6407 init_test(cx, |_| {});
6408
6409 let language = Arc::new(Language::new(
6410 LanguageConfig::default(),
6411 Some(tree_sitter_rust::LANGUAGE.into()),
6412 ));
6413
6414 let text = "let a = 2;";
6415
6416 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6417 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6418 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6419
6420 editor
6421 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6422 .await;
6423
6424 // Test case 1: Cursor at end of word
6425 editor.update_in(cx, |editor, window, cx| {
6426 editor.change_selections(None, window, cx, |s| {
6427 s.select_display_ranges([
6428 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
6429 ]);
6430 });
6431 });
6432 editor.update(cx, |editor, cx| {
6433 assert_text_with_selections(editor, "let aˇ = 2;", cx);
6434 });
6435 editor.update_in(cx, |editor, window, cx| {
6436 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6437 });
6438 editor.update(cx, |editor, cx| {
6439 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
6440 });
6441 editor.update_in(cx, |editor, window, cx| {
6442 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6443 });
6444 editor.update(cx, |editor, cx| {
6445 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
6446 });
6447
6448 // Test case 2: Cursor at end of statement
6449 editor.update_in(cx, |editor, window, cx| {
6450 editor.change_selections(None, window, cx, |s| {
6451 s.select_display_ranges([
6452 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
6453 ]);
6454 });
6455 });
6456 editor.update(cx, |editor, cx| {
6457 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
6458 });
6459 editor.update_in(cx, |editor, window, cx| {
6460 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6461 });
6462 editor.update(cx, |editor, cx| {
6463 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
6464 });
6465}
6466
6467#[gpui::test]
6468async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
6469 init_test(cx, |_| {});
6470
6471 let language = Arc::new(Language::new(
6472 LanguageConfig::default(),
6473 Some(tree_sitter_rust::LANGUAGE.into()),
6474 ));
6475
6476 let text = r#"
6477 use mod1::mod2::{mod3, mod4};
6478
6479 fn fn_1(param1: bool, param2: &str) {
6480 let var1 = "hello world";
6481 }
6482 "#
6483 .unindent();
6484
6485 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6486 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6487 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6488
6489 editor
6490 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6491 .await;
6492
6493 // Test 1: Cursor on a letter of a string word
6494 editor.update_in(cx, |editor, window, cx| {
6495 editor.change_selections(None, window, cx, |s| {
6496 s.select_display_ranges([
6497 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
6498 ]);
6499 });
6500 });
6501 editor.update_in(cx, |editor, window, cx| {
6502 assert_text_with_selections(
6503 editor,
6504 indoc! {r#"
6505 use mod1::mod2::{mod3, mod4};
6506
6507 fn fn_1(param1: bool, param2: &str) {
6508 let var1 = "hˇello world";
6509 }
6510 "#},
6511 cx,
6512 );
6513 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6514 assert_text_with_selections(
6515 editor,
6516 indoc! {r#"
6517 use mod1::mod2::{mod3, mod4};
6518
6519 fn fn_1(param1: bool, param2: &str) {
6520 let var1 = "«ˇhello» world";
6521 }
6522 "#},
6523 cx,
6524 );
6525 });
6526
6527 // Test 2: Partial selection within a word
6528 editor.update_in(cx, |editor, window, cx| {
6529 editor.change_selections(None, window, cx, |s| {
6530 s.select_display_ranges([
6531 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
6532 ]);
6533 });
6534 });
6535 editor.update_in(cx, |editor, window, cx| {
6536 assert_text_with_selections(
6537 editor,
6538 indoc! {r#"
6539 use mod1::mod2::{mod3, mod4};
6540
6541 fn fn_1(param1: bool, param2: &str) {
6542 let var1 = "h«elˇ»lo world";
6543 }
6544 "#},
6545 cx,
6546 );
6547 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6548 assert_text_with_selections(
6549 editor,
6550 indoc! {r#"
6551 use mod1::mod2::{mod3, mod4};
6552
6553 fn fn_1(param1: bool, param2: &str) {
6554 let var1 = "«ˇhello» world";
6555 }
6556 "#},
6557 cx,
6558 );
6559 });
6560
6561 // Test 3: Complete word already selected
6562 editor.update_in(cx, |editor, window, cx| {
6563 editor.change_selections(None, window, cx, |s| {
6564 s.select_display_ranges([
6565 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
6566 ]);
6567 });
6568 });
6569 editor.update_in(cx, |editor, window, cx| {
6570 assert_text_with_selections(
6571 editor,
6572 indoc! {r#"
6573 use mod1::mod2::{mod3, mod4};
6574
6575 fn fn_1(param1: bool, param2: &str) {
6576 let var1 = "«helloˇ» world";
6577 }
6578 "#},
6579 cx,
6580 );
6581 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6582 assert_text_with_selections(
6583 editor,
6584 indoc! {r#"
6585 use mod1::mod2::{mod3, mod4};
6586
6587 fn fn_1(param1: bool, param2: &str) {
6588 let var1 = "«hello worldˇ»";
6589 }
6590 "#},
6591 cx,
6592 );
6593 });
6594
6595 // Test 4: Selection spanning across words
6596 editor.update_in(cx, |editor, window, cx| {
6597 editor.change_selections(None, window, cx, |s| {
6598 s.select_display_ranges([
6599 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
6600 ]);
6601 });
6602 });
6603 editor.update_in(cx, |editor, window, cx| {
6604 assert_text_with_selections(
6605 editor,
6606 indoc! {r#"
6607 use mod1::mod2::{mod3, mod4};
6608
6609 fn fn_1(param1: bool, param2: &str) {
6610 let var1 = "hel«lo woˇ»rld";
6611 }
6612 "#},
6613 cx,
6614 );
6615 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6616 assert_text_with_selections(
6617 editor,
6618 indoc! {r#"
6619 use mod1::mod2::{mod3, mod4};
6620
6621 fn fn_1(param1: bool, param2: &str) {
6622 let var1 = "«ˇhello world»";
6623 }
6624 "#},
6625 cx,
6626 );
6627 });
6628
6629 // Test 5: Expansion beyond string
6630 editor.update_in(cx, |editor, window, cx| {
6631 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6632 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6633 assert_text_with_selections(
6634 editor,
6635 indoc! {r#"
6636 use mod1::mod2::{mod3, mod4};
6637
6638 fn fn_1(param1: bool, param2: &str) {
6639 «ˇlet var1 = "hello world";»
6640 }
6641 "#},
6642 cx,
6643 );
6644 });
6645}
6646
6647#[gpui::test]
6648async fn test_fold_function_bodies(cx: &mut TestAppContext) {
6649 init_test(cx, |_| {});
6650
6651 let base_text = r#"
6652 impl A {
6653 // this is an uncommitted comment
6654
6655 fn b() {
6656 c();
6657 }
6658
6659 // this is another uncommitted comment
6660
6661 fn d() {
6662 // e
6663 // f
6664 }
6665 }
6666
6667 fn g() {
6668 // h
6669 }
6670 "#
6671 .unindent();
6672
6673 let text = r#"
6674 ˇimpl A {
6675
6676 fn b() {
6677 c();
6678 }
6679
6680 fn d() {
6681 // e
6682 // f
6683 }
6684 }
6685
6686 fn g() {
6687 // h
6688 }
6689 "#
6690 .unindent();
6691
6692 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6693 cx.set_state(&text);
6694 cx.set_head_text(&base_text);
6695 cx.update_editor(|editor, window, cx| {
6696 editor.expand_all_diff_hunks(&Default::default(), window, cx);
6697 });
6698
6699 cx.assert_state_with_diff(
6700 "
6701 ˇimpl A {
6702 - // this is an uncommitted comment
6703
6704 fn b() {
6705 c();
6706 }
6707
6708 - // this is another uncommitted comment
6709 -
6710 fn d() {
6711 // e
6712 // f
6713 }
6714 }
6715
6716 fn g() {
6717 // h
6718 }
6719 "
6720 .unindent(),
6721 );
6722
6723 let expected_display_text = "
6724 impl A {
6725 // this is an uncommitted comment
6726
6727 fn b() {
6728 ⋯
6729 }
6730
6731 // this is another uncommitted comment
6732
6733 fn d() {
6734 ⋯
6735 }
6736 }
6737
6738 fn g() {
6739 ⋯
6740 }
6741 "
6742 .unindent();
6743
6744 cx.update_editor(|editor, window, cx| {
6745 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
6746 assert_eq!(editor.display_text(cx), expected_display_text);
6747 });
6748}
6749
6750#[gpui::test]
6751async fn test_autoindent(cx: &mut TestAppContext) {
6752 init_test(cx, |_| {});
6753
6754 let language = Arc::new(
6755 Language::new(
6756 LanguageConfig {
6757 brackets: BracketPairConfig {
6758 pairs: vec![
6759 BracketPair {
6760 start: "{".to_string(),
6761 end: "}".to_string(),
6762 close: false,
6763 surround: false,
6764 newline: true,
6765 },
6766 BracketPair {
6767 start: "(".to_string(),
6768 end: ")".to_string(),
6769 close: false,
6770 surround: false,
6771 newline: true,
6772 },
6773 ],
6774 ..Default::default()
6775 },
6776 ..Default::default()
6777 },
6778 Some(tree_sitter_rust::LANGUAGE.into()),
6779 )
6780 .with_indents_query(
6781 r#"
6782 (_ "(" ")" @end) @indent
6783 (_ "{" "}" @end) @indent
6784 "#,
6785 )
6786 .unwrap(),
6787 );
6788
6789 let text = "fn a() {}";
6790
6791 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6792 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6793 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6794 editor
6795 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6796 .await;
6797
6798 editor.update_in(cx, |editor, window, cx| {
6799 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
6800 editor.newline(&Newline, window, cx);
6801 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
6802 assert_eq!(
6803 editor.selections.ranges(cx),
6804 &[
6805 Point::new(1, 4)..Point::new(1, 4),
6806 Point::new(3, 4)..Point::new(3, 4),
6807 Point::new(5, 0)..Point::new(5, 0)
6808 ]
6809 );
6810 });
6811}
6812
6813#[gpui::test]
6814async fn test_autoindent_selections(cx: &mut TestAppContext) {
6815 init_test(cx, |_| {});
6816
6817 {
6818 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6819 cx.set_state(indoc! {"
6820 impl A {
6821
6822 fn b() {}
6823
6824 «fn c() {
6825
6826 }ˇ»
6827 }
6828 "});
6829
6830 cx.update_editor(|editor, window, cx| {
6831 editor.autoindent(&Default::default(), window, cx);
6832 });
6833
6834 cx.assert_editor_state(indoc! {"
6835 impl A {
6836
6837 fn b() {}
6838
6839 «fn c() {
6840
6841 }ˇ»
6842 }
6843 "});
6844 }
6845
6846 {
6847 let mut cx = EditorTestContext::new_multibuffer(
6848 cx,
6849 [indoc! { "
6850 impl A {
6851 «
6852 // a
6853 fn b(){}
6854 »
6855 «
6856 }
6857 fn c(){}
6858 »
6859 "}],
6860 );
6861
6862 let buffer = cx.update_editor(|editor, _, cx| {
6863 let buffer = editor.buffer().update(cx, |buffer, _| {
6864 buffer.all_buffers().iter().next().unwrap().clone()
6865 });
6866 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6867 buffer
6868 });
6869
6870 cx.run_until_parked();
6871 cx.update_editor(|editor, window, cx| {
6872 editor.select_all(&Default::default(), window, cx);
6873 editor.autoindent(&Default::default(), window, cx)
6874 });
6875 cx.run_until_parked();
6876
6877 cx.update(|_, cx| {
6878 assert_eq!(
6879 buffer.read(cx).text(),
6880 indoc! { "
6881 impl A {
6882
6883 // a
6884 fn b(){}
6885
6886
6887 }
6888 fn c(){}
6889
6890 " }
6891 )
6892 });
6893 }
6894}
6895
6896#[gpui::test]
6897async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
6898 init_test(cx, |_| {});
6899
6900 let mut cx = EditorTestContext::new(cx).await;
6901
6902 let language = Arc::new(Language::new(
6903 LanguageConfig {
6904 brackets: BracketPairConfig {
6905 pairs: vec![
6906 BracketPair {
6907 start: "{".to_string(),
6908 end: "}".to_string(),
6909 close: true,
6910 surround: true,
6911 newline: true,
6912 },
6913 BracketPair {
6914 start: "(".to_string(),
6915 end: ")".to_string(),
6916 close: true,
6917 surround: true,
6918 newline: true,
6919 },
6920 BracketPair {
6921 start: "/*".to_string(),
6922 end: " */".to_string(),
6923 close: true,
6924 surround: true,
6925 newline: true,
6926 },
6927 BracketPair {
6928 start: "[".to_string(),
6929 end: "]".to_string(),
6930 close: false,
6931 surround: false,
6932 newline: true,
6933 },
6934 BracketPair {
6935 start: "\"".to_string(),
6936 end: "\"".to_string(),
6937 close: true,
6938 surround: true,
6939 newline: false,
6940 },
6941 BracketPair {
6942 start: "<".to_string(),
6943 end: ">".to_string(),
6944 close: false,
6945 surround: true,
6946 newline: true,
6947 },
6948 ],
6949 ..Default::default()
6950 },
6951 autoclose_before: "})]".to_string(),
6952 ..Default::default()
6953 },
6954 Some(tree_sitter_rust::LANGUAGE.into()),
6955 ));
6956
6957 cx.language_registry().add(language.clone());
6958 cx.update_buffer(|buffer, cx| {
6959 buffer.set_language(Some(language), cx);
6960 });
6961
6962 cx.set_state(
6963 &r#"
6964 🏀ˇ
6965 εˇ
6966 ❤️ˇ
6967 "#
6968 .unindent(),
6969 );
6970
6971 // autoclose multiple nested brackets at multiple cursors
6972 cx.update_editor(|editor, window, cx| {
6973 editor.handle_input("{", window, cx);
6974 editor.handle_input("{", window, cx);
6975 editor.handle_input("{", window, cx);
6976 });
6977 cx.assert_editor_state(
6978 &"
6979 🏀{{{ˇ}}}
6980 ε{{{ˇ}}}
6981 ❤️{{{ˇ}}}
6982 "
6983 .unindent(),
6984 );
6985
6986 // insert a different closing bracket
6987 cx.update_editor(|editor, window, cx| {
6988 editor.handle_input(")", window, cx);
6989 });
6990 cx.assert_editor_state(
6991 &"
6992 🏀{{{)ˇ}}}
6993 ε{{{)ˇ}}}
6994 ❤️{{{)ˇ}}}
6995 "
6996 .unindent(),
6997 );
6998
6999 // skip over the auto-closed brackets when typing a closing bracket
7000 cx.update_editor(|editor, window, cx| {
7001 editor.move_right(&MoveRight, window, cx);
7002 editor.handle_input("}", window, cx);
7003 editor.handle_input("}", window, cx);
7004 editor.handle_input("}", window, cx);
7005 });
7006 cx.assert_editor_state(
7007 &"
7008 🏀{{{)}}}}ˇ
7009 ε{{{)}}}}ˇ
7010 ❤️{{{)}}}}ˇ
7011 "
7012 .unindent(),
7013 );
7014
7015 // autoclose multi-character pairs
7016 cx.set_state(
7017 &"
7018 ˇ
7019 ˇ
7020 "
7021 .unindent(),
7022 );
7023 cx.update_editor(|editor, window, cx| {
7024 editor.handle_input("/", window, cx);
7025 editor.handle_input("*", window, cx);
7026 });
7027 cx.assert_editor_state(
7028 &"
7029 /*ˇ */
7030 /*ˇ */
7031 "
7032 .unindent(),
7033 );
7034
7035 // one cursor autocloses a multi-character pair, one cursor
7036 // does not autoclose.
7037 cx.set_state(
7038 &"
7039 /ˇ
7040 ˇ
7041 "
7042 .unindent(),
7043 );
7044 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
7045 cx.assert_editor_state(
7046 &"
7047 /*ˇ */
7048 *ˇ
7049 "
7050 .unindent(),
7051 );
7052
7053 // Don't autoclose if the next character isn't whitespace and isn't
7054 // listed in the language's "autoclose_before" section.
7055 cx.set_state("ˇa b");
7056 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7057 cx.assert_editor_state("{ˇa b");
7058
7059 // Don't autoclose if `close` is false for the bracket pair
7060 cx.set_state("ˇ");
7061 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
7062 cx.assert_editor_state("[ˇ");
7063
7064 // Surround with brackets if text is selected
7065 cx.set_state("«aˇ» b");
7066 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7067 cx.assert_editor_state("{«aˇ»} b");
7068
7069 // Autoclose when not immediately after a word character
7070 cx.set_state("a ˇ");
7071 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7072 cx.assert_editor_state("a \"ˇ\"");
7073
7074 // Autoclose pair where the start and end characters are the same
7075 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7076 cx.assert_editor_state("a \"\"ˇ");
7077
7078 // Don't autoclose when immediately after a word character
7079 cx.set_state("aˇ");
7080 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7081 cx.assert_editor_state("a\"ˇ");
7082
7083 // Do autoclose when after a non-word character
7084 cx.set_state("{ˇ");
7085 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7086 cx.assert_editor_state("{\"ˇ\"");
7087
7088 // Non identical pairs autoclose regardless of preceding character
7089 cx.set_state("aˇ");
7090 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7091 cx.assert_editor_state("a{ˇ}");
7092
7093 // Don't autoclose pair if autoclose is disabled
7094 cx.set_state("ˇ");
7095 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7096 cx.assert_editor_state("<ˇ");
7097
7098 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
7099 cx.set_state("«aˇ» b");
7100 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7101 cx.assert_editor_state("<«aˇ»> b");
7102}
7103
7104#[gpui::test]
7105async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
7106 init_test(cx, |settings| {
7107 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7108 });
7109
7110 let mut cx = EditorTestContext::new(cx).await;
7111
7112 let language = Arc::new(Language::new(
7113 LanguageConfig {
7114 brackets: BracketPairConfig {
7115 pairs: vec![
7116 BracketPair {
7117 start: "{".to_string(),
7118 end: "}".to_string(),
7119 close: true,
7120 surround: true,
7121 newline: true,
7122 },
7123 BracketPair {
7124 start: "(".to_string(),
7125 end: ")".to_string(),
7126 close: true,
7127 surround: true,
7128 newline: true,
7129 },
7130 BracketPair {
7131 start: "[".to_string(),
7132 end: "]".to_string(),
7133 close: false,
7134 surround: false,
7135 newline: true,
7136 },
7137 ],
7138 ..Default::default()
7139 },
7140 autoclose_before: "})]".to_string(),
7141 ..Default::default()
7142 },
7143 Some(tree_sitter_rust::LANGUAGE.into()),
7144 ));
7145
7146 cx.language_registry().add(language.clone());
7147 cx.update_buffer(|buffer, cx| {
7148 buffer.set_language(Some(language), cx);
7149 });
7150
7151 cx.set_state(
7152 &"
7153 ˇ
7154 ˇ
7155 ˇ
7156 "
7157 .unindent(),
7158 );
7159
7160 // ensure only matching closing brackets are skipped over
7161 cx.update_editor(|editor, window, cx| {
7162 editor.handle_input("}", window, cx);
7163 editor.move_left(&MoveLeft, window, cx);
7164 editor.handle_input(")", window, cx);
7165 editor.move_left(&MoveLeft, window, cx);
7166 });
7167 cx.assert_editor_state(
7168 &"
7169 ˇ)}
7170 ˇ)}
7171 ˇ)}
7172 "
7173 .unindent(),
7174 );
7175
7176 // skip-over closing brackets at multiple cursors
7177 cx.update_editor(|editor, window, cx| {
7178 editor.handle_input(")", window, cx);
7179 editor.handle_input("}", window, cx);
7180 });
7181 cx.assert_editor_state(
7182 &"
7183 )}ˇ
7184 )}ˇ
7185 )}ˇ
7186 "
7187 .unindent(),
7188 );
7189
7190 // ignore non-close brackets
7191 cx.update_editor(|editor, window, cx| {
7192 editor.handle_input("]", window, cx);
7193 editor.move_left(&MoveLeft, window, cx);
7194 editor.handle_input("]", window, cx);
7195 });
7196 cx.assert_editor_state(
7197 &"
7198 )}]ˇ]
7199 )}]ˇ]
7200 )}]ˇ]
7201 "
7202 .unindent(),
7203 );
7204}
7205
7206#[gpui::test]
7207async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
7208 init_test(cx, |_| {});
7209
7210 let mut cx = EditorTestContext::new(cx).await;
7211
7212 let html_language = Arc::new(
7213 Language::new(
7214 LanguageConfig {
7215 name: "HTML".into(),
7216 brackets: BracketPairConfig {
7217 pairs: vec![
7218 BracketPair {
7219 start: "<".into(),
7220 end: ">".into(),
7221 close: true,
7222 ..Default::default()
7223 },
7224 BracketPair {
7225 start: "{".into(),
7226 end: "}".into(),
7227 close: true,
7228 ..Default::default()
7229 },
7230 BracketPair {
7231 start: "(".into(),
7232 end: ")".into(),
7233 close: true,
7234 ..Default::default()
7235 },
7236 ],
7237 ..Default::default()
7238 },
7239 autoclose_before: "})]>".into(),
7240 ..Default::default()
7241 },
7242 Some(tree_sitter_html::LANGUAGE.into()),
7243 )
7244 .with_injection_query(
7245 r#"
7246 (script_element
7247 (raw_text) @injection.content
7248 (#set! injection.language "javascript"))
7249 "#,
7250 )
7251 .unwrap(),
7252 );
7253
7254 let javascript_language = Arc::new(Language::new(
7255 LanguageConfig {
7256 name: "JavaScript".into(),
7257 brackets: BracketPairConfig {
7258 pairs: vec![
7259 BracketPair {
7260 start: "/*".into(),
7261 end: " */".into(),
7262 close: true,
7263 ..Default::default()
7264 },
7265 BracketPair {
7266 start: "{".into(),
7267 end: "}".into(),
7268 close: true,
7269 ..Default::default()
7270 },
7271 BracketPair {
7272 start: "(".into(),
7273 end: ")".into(),
7274 close: true,
7275 ..Default::default()
7276 },
7277 ],
7278 ..Default::default()
7279 },
7280 autoclose_before: "})]>".into(),
7281 ..Default::default()
7282 },
7283 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
7284 ));
7285
7286 cx.language_registry().add(html_language.clone());
7287 cx.language_registry().add(javascript_language.clone());
7288
7289 cx.update_buffer(|buffer, cx| {
7290 buffer.set_language(Some(html_language), cx);
7291 });
7292
7293 cx.set_state(
7294 &r#"
7295 <body>ˇ
7296 <script>
7297 var x = 1;ˇ
7298 </script>
7299 </body>ˇ
7300 "#
7301 .unindent(),
7302 );
7303
7304 // Precondition: different languages are active at different locations.
7305 cx.update_editor(|editor, window, cx| {
7306 let snapshot = editor.snapshot(window, cx);
7307 let cursors = editor.selections.ranges::<usize>(cx);
7308 let languages = cursors
7309 .iter()
7310 .map(|c| snapshot.language_at(c.start).unwrap().name())
7311 .collect::<Vec<_>>();
7312 assert_eq!(
7313 languages,
7314 &["HTML".into(), "JavaScript".into(), "HTML".into()]
7315 );
7316 });
7317
7318 // Angle brackets autoclose in HTML, but not JavaScript.
7319 cx.update_editor(|editor, window, cx| {
7320 editor.handle_input("<", window, cx);
7321 editor.handle_input("a", window, cx);
7322 });
7323 cx.assert_editor_state(
7324 &r#"
7325 <body><aˇ>
7326 <script>
7327 var x = 1;<aˇ
7328 </script>
7329 </body><aˇ>
7330 "#
7331 .unindent(),
7332 );
7333
7334 // Curly braces and parens autoclose in both HTML and JavaScript.
7335 cx.update_editor(|editor, window, cx| {
7336 editor.handle_input(" b=", window, cx);
7337 editor.handle_input("{", window, cx);
7338 editor.handle_input("c", window, cx);
7339 editor.handle_input("(", window, cx);
7340 });
7341 cx.assert_editor_state(
7342 &r#"
7343 <body><a b={c(ˇ)}>
7344 <script>
7345 var x = 1;<a b={c(ˇ)}
7346 </script>
7347 </body><a b={c(ˇ)}>
7348 "#
7349 .unindent(),
7350 );
7351
7352 // Brackets that were already autoclosed are skipped.
7353 cx.update_editor(|editor, window, cx| {
7354 editor.handle_input(")", window, cx);
7355 editor.handle_input("d", window, cx);
7356 editor.handle_input("}", window, cx);
7357 });
7358 cx.assert_editor_state(
7359 &r#"
7360 <body><a b={c()d}ˇ>
7361 <script>
7362 var x = 1;<a b={c()d}ˇ
7363 </script>
7364 </body><a b={c()d}ˇ>
7365 "#
7366 .unindent(),
7367 );
7368 cx.update_editor(|editor, window, cx| {
7369 editor.handle_input(">", window, cx);
7370 });
7371 cx.assert_editor_state(
7372 &r#"
7373 <body><a b={c()d}>ˇ
7374 <script>
7375 var x = 1;<a b={c()d}>ˇ
7376 </script>
7377 </body><a b={c()d}>ˇ
7378 "#
7379 .unindent(),
7380 );
7381
7382 // Reset
7383 cx.set_state(
7384 &r#"
7385 <body>ˇ
7386 <script>
7387 var x = 1;ˇ
7388 </script>
7389 </body>ˇ
7390 "#
7391 .unindent(),
7392 );
7393
7394 cx.update_editor(|editor, window, cx| {
7395 editor.handle_input("<", window, cx);
7396 });
7397 cx.assert_editor_state(
7398 &r#"
7399 <body><ˇ>
7400 <script>
7401 var x = 1;<ˇ
7402 </script>
7403 </body><ˇ>
7404 "#
7405 .unindent(),
7406 );
7407
7408 // When backspacing, the closing angle brackets are removed.
7409 cx.update_editor(|editor, window, cx| {
7410 editor.backspace(&Backspace, window, cx);
7411 });
7412 cx.assert_editor_state(
7413 &r#"
7414 <body>ˇ
7415 <script>
7416 var x = 1;ˇ
7417 </script>
7418 </body>ˇ
7419 "#
7420 .unindent(),
7421 );
7422
7423 // Block comments autoclose in JavaScript, but not HTML.
7424 cx.update_editor(|editor, window, cx| {
7425 editor.handle_input("/", window, cx);
7426 editor.handle_input("*", window, cx);
7427 });
7428 cx.assert_editor_state(
7429 &r#"
7430 <body>/*ˇ
7431 <script>
7432 var x = 1;/*ˇ */
7433 </script>
7434 </body>/*ˇ
7435 "#
7436 .unindent(),
7437 );
7438}
7439
7440#[gpui::test]
7441async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
7442 init_test(cx, |_| {});
7443
7444 let mut cx = EditorTestContext::new(cx).await;
7445
7446 let rust_language = Arc::new(
7447 Language::new(
7448 LanguageConfig {
7449 name: "Rust".into(),
7450 brackets: serde_json::from_value(json!([
7451 { "start": "{", "end": "}", "close": true, "newline": true },
7452 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
7453 ]))
7454 .unwrap(),
7455 autoclose_before: "})]>".into(),
7456 ..Default::default()
7457 },
7458 Some(tree_sitter_rust::LANGUAGE.into()),
7459 )
7460 .with_override_query("(string_literal) @string")
7461 .unwrap(),
7462 );
7463
7464 cx.language_registry().add(rust_language.clone());
7465 cx.update_buffer(|buffer, cx| {
7466 buffer.set_language(Some(rust_language), cx);
7467 });
7468
7469 cx.set_state(
7470 &r#"
7471 let x = ˇ
7472 "#
7473 .unindent(),
7474 );
7475
7476 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
7477 cx.update_editor(|editor, window, cx| {
7478 editor.handle_input("\"", window, cx);
7479 });
7480 cx.assert_editor_state(
7481 &r#"
7482 let x = "ˇ"
7483 "#
7484 .unindent(),
7485 );
7486
7487 // Inserting another quotation mark. The cursor moves across the existing
7488 // automatically-inserted quotation mark.
7489 cx.update_editor(|editor, window, cx| {
7490 editor.handle_input("\"", window, cx);
7491 });
7492 cx.assert_editor_state(
7493 &r#"
7494 let x = ""ˇ
7495 "#
7496 .unindent(),
7497 );
7498
7499 // Reset
7500 cx.set_state(
7501 &r#"
7502 let x = ˇ
7503 "#
7504 .unindent(),
7505 );
7506
7507 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
7508 cx.update_editor(|editor, window, cx| {
7509 editor.handle_input("\"", window, cx);
7510 editor.handle_input(" ", window, cx);
7511 editor.move_left(&Default::default(), window, cx);
7512 editor.handle_input("\\", window, cx);
7513 editor.handle_input("\"", window, cx);
7514 });
7515 cx.assert_editor_state(
7516 &r#"
7517 let x = "\"ˇ "
7518 "#
7519 .unindent(),
7520 );
7521
7522 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
7523 // mark. Nothing is inserted.
7524 cx.update_editor(|editor, window, cx| {
7525 editor.move_right(&Default::default(), window, cx);
7526 editor.handle_input("\"", window, cx);
7527 });
7528 cx.assert_editor_state(
7529 &r#"
7530 let x = "\" "ˇ
7531 "#
7532 .unindent(),
7533 );
7534}
7535
7536#[gpui::test]
7537async fn test_surround_with_pair(cx: &mut TestAppContext) {
7538 init_test(cx, |_| {});
7539
7540 let language = Arc::new(Language::new(
7541 LanguageConfig {
7542 brackets: BracketPairConfig {
7543 pairs: vec![
7544 BracketPair {
7545 start: "{".to_string(),
7546 end: "}".to_string(),
7547 close: true,
7548 surround: true,
7549 newline: true,
7550 },
7551 BracketPair {
7552 start: "/* ".to_string(),
7553 end: "*/".to_string(),
7554 close: true,
7555 surround: true,
7556 ..Default::default()
7557 },
7558 ],
7559 ..Default::default()
7560 },
7561 ..Default::default()
7562 },
7563 Some(tree_sitter_rust::LANGUAGE.into()),
7564 ));
7565
7566 let text = r#"
7567 a
7568 b
7569 c
7570 "#
7571 .unindent();
7572
7573 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7574 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7575 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7576 editor
7577 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7578 .await;
7579
7580 editor.update_in(cx, |editor, window, cx| {
7581 editor.change_selections(None, window, cx, |s| {
7582 s.select_display_ranges([
7583 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7584 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7585 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
7586 ])
7587 });
7588
7589 editor.handle_input("{", window, cx);
7590 editor.handle_input("{", window, cx);
7591 editor.handle_input("{", window, cx);
7592 assert_eq!(
7593 editor.text(cx),
7594 "
7595 {{{a}}}
7596 {{{b}}}
7597 {{{c}}}
7598 "
7599 .unindent()
7600 );
7601 assert_eq!(
7602 editor.selections.display_ranges(cx),
7603 [
7604 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
7605 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
7606 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
7607 ]
7608 );
7609
7610 editor.undo(&Undo, window, cx);
7611 editor.undo(&Undo, window, cx);
7612 editor.undo(&Undo, window, cx);
7613 assert_eq!(
7614 editor.text(cx),
7615 "
7616 a
7617 b
7618 c
7619 "
7620 .unindent()
7621 );
7622 assert_eq!(
7623 editor.selections.display_ranges(cx),
7624 [
7625 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7626 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7627 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7628 ]
7629 );
7630
7631 // Ensure inserting the first character of a multi-byte bracket pair
7632 // doesn't surround the selections with the bracket.
7633 editor.handle_input("/", window, cx);
7634 assert_eq!(
7635 editor.text(cx),
7636 "
7637 /
7638 /
7639 /
7640 "
7641 .unindent()
7642 );
7643 assert_eq!(
7644 editor.selections.display_ranges(cx),
7645 [
7646 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7647 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7648 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7649 ]
7650 );
7651
7652 editor.undo(&Undo, window, cx);
7653 assert_eq!(
7654 editor.text(cx),
7655 "
7656 a
7657 b
7658 c
7659 "
7660 .unindent()
7661 );
7662 assert_eq!(
7663 editor.selections.display_ranges(cx),
7664 [
7665 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7666 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7667 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7668 ]
7669 );
7670
7671 // Ensure inserting the last character of a multi-byte bracket pair
7672 // doesn't surround the selections with the bracket.
7673 editor.handle_input("*", window, cx);
7674 assert_eq!(
7675 editor.text(cx),
7676 "
7677 *
7678 *
7679 *
7680 "
7681 .unindent()
7682 );
7683 assert_eq!(
7684 editor.selections.display_ranges(cx),
7685 [
7686 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7687 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7688 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7689 ]
7690 );
7691 });
7692}
7693
7694#[gpui::test]
7695async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
7696 init_test(cx, |_| {});
7697
7698 let language = Arc::new(Language::new(
7699 LanguageConfig {
7700 brackets: BracketPairConfig {
7701 pairs: vec![BracketPair {
7702 start: "{".to_string(),
7703 end: "}".to_string(),
7704 close: true,
7705 surround: true,
7706 newline: true,
7707 }],
7708 ..Default::default()
7709 },
7710 autoclose_before: "}".to_string(),
7711 ..Default::default()
7712 },
7713 Some(tree_sitter_rust::LANGUAGE.into()),
7714 ));
7715
7716 let text = r#"
7717 a
7718 b
7719 c
7720 "#
7721 .unindent();
7722
7723 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7724 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7725 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7726 editor
7727 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7728 .await;
7729
7730 editor.update_in(cx, |editor, window, cx| {
7731 editor.change_selections(None, window, cx, |s| {
7732 s.select_ranges([
7733 Point::new(0, 1)..Point::new(0, 1),
7734 Point::new(1, 1)..Point::new(1, 1),
7735 Point::new(2, 1)..Point::new(2, 1),
7736 ])
7737 });
7738
7739 editor.handle_input("{", window, cx);
7740 editor.handle_input("{", window, cx);
7741 editor.handle_input("_", window, cx);
7742 assert_eq!(
7743 editor.text(cx),
7744 "
7745 a{{_}}
7746 b{{_}}
7747 c{{_}}
7748 "
7749 .unindent()
7750 );
7751 assert_eq!(
7752 editor.selections.ranges::<Point>(cx),
7753 [
7754 Point::new(0, 4)..Point::new(0, 4),
7755 Point::new(1, 4)..Point::new(1, 4),
7756 Point::new(2, 4)..Point::new(2, 4)
7757 ]
7758 );
7759
7760 editor.backspace(&Default::default(), window, cx);
7761 editor.backspace(&Default::default(), window, cx);
7762 assert_eq!(
7763 editor.text(cx),
7764 "
7765 a{}
7766 b{}
7767 c{}
7768 "
7769 .unindent()
7770 );
7771 assert_eq!(
7772 editor.selections.ranges::<Point>(cx),
7773 [
7774 Point::new(0, 2)..Point::new(0, 2),
7775 Point::new(1, 2)..Point::new(1, 2),
7776 Point::new(2, 2)..Point::new(2, 2)
7777 ]
7778 );
7779
7780 editor.delete_to_previous_word_start(&Default::default(), window, cx);
7781 assert_eq!(
7782 editor.text(cx),
7783 "
7784 a
7785 b
7786 c
7787 "
7788 .unindent()
7789 );
7790 assert_eq!(
7791 editor.selections.ranges::<Point>(cx),
7792 [
7793 Point::new(0, 1)..Point::new(0, 1),
7794 Point::new(1, 1)..Point::new(1, 1),
7795 Point::new(2, 1)..Point::new(2, 1)
7796 ]
7797 );
7798 });
7799}
7800
7801#[gpui::test]
7802async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
7803 init_test(cx, |settings| {
7804 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7805 });
7806
7807 let mut cx = EditorTestContext::new(cx).await;
7808
7809 let language = Arc::new(Language::new(
7810 LanguageConfig {
7811 brackets: BracketPairConfig {
7812 pairs: vec![
7813 BracketPair {
7814 start: "{".to_string(),
7815 end: "}".to_string(),
7816 close: true,
7817 surround: true,
7818 newline: true,
7819 },
7820 BracketPair {
7821 start: "(".to_string(),
7822 end: ")".to_string(),
7823 close: true,
7824 surround: true,
7825 newline: true,
7826 },
7827 BracketPair {
7828 start: "[".to_string(),
7829 end: "]".to_string(),
7830 close: false,
7831 surround: true,
7832 newline: true,
7833 },
7834 ],
7835 ..Default::default()
7836 },
7837 autoclose_before: "})]".to_string(),
7838 ..Default::default()
7839 },
7840 Some(tree_sitter_rust::LANGUAGE.into()),
7841 ));
7842
7843 cx.language_registry().add(language.clone());
7844 cx.update_buffer(|buffer, cx| {
7845 buffer.set_language(Some(language), cx);
7846 });
7847
7848 cx.set_state(
7849 &"
7850 {(ˇ)}
7851 [[ˇ]]
7852 {(ˇ)}
7853 "
7854 .unindent(),
7855 );
7856
7857 cx.update_editor(|editor, window, cx| {
7858 editor.backspace(&Default::default(), window, cx);
7859 editor.backspace(&Default::default(), window, cx);
7860 });
7861
7862 cx.assert_editor_state(
7863 &"
7864 ˇ
7865 ˇ]]
7866 ˇ
7867 "
7868 .unindent(),
7869 );
7870
7871 cx.update_editor(|editor, window, cx| {
7872 editor.handle_input("{", window, cx);
7873 editor.handle_input("{", window, cx);
7874 editor.move_right(&MoveRight, window, cx);
7875 editor.move_right(&MoveRight, window, cx);
7876 editor.move_left(&MoveLeft, window, cx);
7877 editor.move_left(&MoveLeft, window, cx);
7878 editor.backspace(&Default::default(), window, cx);
7879 });
7880
7881 cx.assert_editor_state(
7882 &"
7883 {ˇ}
7884 {ˇ}]]
7885 {ˇ}
7886 "
7887 .unindent(),
7888 );
7889
7890 cx.update_editor(|editor, window, cx| {
7891 editor.backspace(&Default::default(), window, cx);
7892 });
7893
7894 cx.assert_editor_state(
7895 &"
7896 ˇ
7897 ˇ]]
7898 ˇ
7899 "
7900 .unindent(),
7901 );
7902}
7903
7904#[gpui::test]
7905async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
7906 init_test(cx, |_| {});
7907
7908 let language = Arc::new(Language::new(
7909 LanguageConfig::default(),
7910 Some(tree_sitter_rust::LANGUAGE.into()),
7911 ));
7912
7913 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
7914 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7915 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7916 editor
7917 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7918 .await;
7919
7920 editor.update_in(cx, |editor, window, cx| {
7921 editor.set_auto_replace_emoji_shortcode(true);
7922
7923 editor.handle_input("Hello ", window, cx);
7924 editor.handle_input(":wave", window, cx);
7925 assert_eq!(editor.text(cx), "Hello :wave".unindent());
7926
7927 editor.handle_input(":", window, cx);
7928 assert_eq!(editor.text(cx), "Hello 👋".unindent());
7929
7930 editor.handle_input(" :smile", window, cx);
7931 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
7932
7933 editor.handle_input(":", window, cx);
7934 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
7935
7936 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
7937 editor.handle_input(":wave", window, cx);
7938 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
7939
7940 editor.handle_input(":", window, cx);
7941 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
7942
7943 editor.handle_input(":1", window, cx);
7944 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
7945
7946 editor.handle_input(":", window, cx);
7947 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
7948
7949 // Ensure shortcode does not get replaced when it is part of a word
7950 editor.handle_input(" Test:wave", window, cx);
7951 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
7952
7953 editor.handle_input(":", window, cx);
7954 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
7955
7956 editor.set_auto_replace_emoji_shortcode(false);
7957
7958 // Ensure shortcode does not get replaced when auto replace is off
7959 editor.handle_input(" :wave", window, cx);
7960 assert_eq!(
7961 editor.text(cx),
7962 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
7963 );
7964
7965 editor.handle_input(":", window, cx);
7966 assert_eq!(
7967 editor.text(cx),
7968 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
7969 );
7970 });
7971}
7972
7973#[gpui::test]
7974async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
7975 init_test(cx, |_| {});
7976
7977 let (text, insertion_ranges) = marked_text_ranges(
7978 indoc! {"
7979 ˇ
7980 "},
7981 false,
7982 );
7983
7984 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7985 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7986
7987 _ = editor.update_in(cx, |editor, window, cx| {
7988 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
7989
7990 editor
7991 .insert_snippet(&insertion_ranges, snippet, window, cx)
7992 .unwrap();
7993
7994 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7995 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7996 assert_eq!(editor.text(cx), expected_text);
7997 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7998 }
7999
8000 assert(
8001 editor,
8002 cx,
8003 indoc! {"
8004 type «» =•
8005 "},
8006 );
8007
8008 assert!(editor.context_menu_visible(), "There should be a matches");
8009 });
8010}
8011
8012#[gpui::test]
8013async fn test_snippets(cx: &mut TestAppContext) {
8014 init_test(cx, |_| {});
8015
8016 let (text, insertion_ranges) = marked_text_ranges(
8017 indoc! {"
8018 a.ˇ b
8019 a.ˇ b
8020 a.ˇ b
8021 "},
8022 false,
8023 );
8024
8025 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
8026 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8027
8028 editor.update_in(cx, |editor, window, cx| {
8029 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
8030
8031 editor
8032 .insert_snippet(&insertion_ranges, snippet, window, cx)
8033 .unwrap();
8034
8035 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
8036 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
8037 assert_eq!(editor.text(cx), expected_text);
8038 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
8039 }
8040
8041 assert(
8042 editor,
8043 cx,
8044 indoc! {"
8045 a.f(«one», two, «three») b
8046 a.f(«one», two, «three») b
8047 a.f(«one», two, «three») b
8048 "},
8049 );
8050
8051 // Can't move earlier than the first tab stop
8052 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
8053 assert(
8054 editor,
8055 cx,
8056 indoc! {"
8057 a.f(«one», two, «three») b
8058 a.f(«one», two, «three») b
8059 a.f(«one», two, «three») b
8060 "},
8061 );
8062
8063 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8064 assert(
8065 editor,
8066 cx,
8067 indoc! {"
8068 a.f(one, «two», three) b
8069 a.f(one, «two», three) b
8070 a.f(one, «two», three) b
8071 "},
8072 );
8073
8074 editor.move_to_prev_snippet_tabstop(window, cx);
8075 assert(
8076 editor,
8077 cx,
8078 indoc! {"
8079 a.f(«one», two, «three») b
8080 a.f(«one», two, «three») b
8081 a.f(«one», two, «three») b
8082 "},
8083 );
8084
8085 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8086 assert(
8087 editor,
8088 cx,
8089 indoc! {"
8090 a.f(one, «two», three) b
8091 a.f(one, «two», three) b
8092 a.f(one, «two», three) b
8093 "},
8094 );
8095 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8096 assert(
8097 editor,
8098 cx,
8099 indoc! {"
8100 a.f(one, two, three)ˇ b
8101 a.f(one, two, three)ˇ b
8102 a.f(one, two, three)ˇ b
8103 "},
8104 );
8105
8106 // As soon as the last tab stop is reached, snippet state is gone
8107 editor.move_to_prev_snippet_tabstop(window, cx);
8108 assert(
8109 editor,
8110 cx,
8111 indoc! {"
8112 a.f(one, two, three)ˇ b
8113 a.f(one, two, three)ˇ b
8114 a.f(one, two, three)ˇ b
8115 "},
8116 );
8117 });
8118}
8119
8120#[gpui::test]
8121async fn test_document_format_during_save(cx: &mut TestAppContext) {
8122 init_test(cx, |_| {});
8123
8124 let fs = FakeFs::new(cx.executor());
8125 fs.insert_file(path!("/file.rs"), Default::default()).await;
8126
8127 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
8128
8129 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8130 language_registry.add(rust_lang());
8131 let mut fake_servers = language_registry.register_fake_lsp(
8132 "Rust",
8133 FakeLspAdapter {
8134 capabilities: lsp::ServerCapabilities {
8135 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8136 ..Default::default()
8137 },
8138 ..Default::default()
8139 },
8140 );
8141
8142 let buffer = project
8143 .update(cx, |project, cx| {
8144 project.open_local_buffer(path!("/file.rs"), cx)
8145 })
8146 .await
8147 .unwrap();
8148
8149 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8150 let (editor, cx) = cx.add_window_view(|window, cx| {
8151 build_editor_with_project(project.clone(), buffer, window, cx)
8152 });
8153 editor.update_in(cx, |editor, window, cx| {
8154 editor.set_text("one\ntwo\nthree\n", window, cx)
8155 });
8156 assert!(cx.read(|cx| editor.is_dirty(cx)));
8157
8158 cx.executor().start_waiting();
8159 let fake_server = fake_servers.next().await.unwrap();
8160
8161 {
8162 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8163 move |params, _| async move {
8164 assert_eq!(
8165 params.text_document.uri,
8166 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8167 );
8168 assert_eq!(params.options.tab_size, 4);
8169 Ok(Some(vec![lsp::TextEdit::new(
8170 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8171 ", ".to_string(),
8172 )]))
8173 },
8174 );
8175 let save = editor
8176 .update_in(cx, |editor, window, cx| {
8177 editor.save(true, project.clone(), window, cx)
8178 })
8179 .unwrap();
8180 cx.executor().start_waiting();
8181 save.await;
8182
8183 assert_eq!(
8184 editor.update(cx, |editor, cx| editor.text(cx)),
8185 "one, two\nthree\n"
8186 );
8187 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8188 }
8189
8190 {
8191 editor.update_in(cx, |editor, window, cx| {
8192 editor.set_text("one\ntwo\nthree\n", window, cx)
8193 });
8194 assert!(cx.read(|cx| editor.is_dirty(cx)));
8195
8196 // Ensure we can still save even if formatting hangs.
8197 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8198 move |params, _| async move {
8199 assert_eq!(
8200 params.text_document.uri,
8201 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8202 );
8203 futures::future::pending::<()>().await;
8204 unreachable!()
8205 },
8206 );
8207 let save = editor
8208 .update_in(cx, |editor, window, cx| {
8209 editor.save(true, project.clone(), window, cx)
8210 })
8211 .unwrap();
8212 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8213 cx.executor().start_waiting();
8214 save.await;
8215 assert_eq!(
8216 editor.update(cx, |editor, cx| editor.text(cx)),
8217 "one\ntwo\nthree\n"
8218 );
8219 }
8220
8221 // For non-dirty buffer, no formatting request should be sent
8222 {
8223 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8224
8225 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8226 panic!("Should not be invoked on non-dirty buffer");
8227 });
8228 let save = editor
8229 .update_in(cx, |editor, window, cx| {
8230 editor.save(true, project.clone(), window, cx)
8231 })
8232 .unwrap();
8233 cx.executor().start_waiting();
8234 save.await;
8235 }
8236
8237 // Set rust language override and assert overridden tabsize is sent to language server
8238 update_test_language_settings(cx, |settings| {
8239 settings.languages.insert(
8240 "Rust".into(),
8241 LanguageSettingsContent {
8242 tab_size: NonZeroU32::new(8),
8243 ..Default::default()
8244 },
8245 );
8246 });
8247
8248 {
8249 editor.update_in(cx, |editor, window, cx| {
8250 editor.set_text("somehting_new\n", window, cx)
8251 });
8252 assert!(cx.read(|cx| editor.is_dirty(cx)));
8253 let _formatting_request_signal = fake_server
8254 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8255 assert_eq!(
8256 params.text_document.uri,
8257 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8258 );
8259 assert_eq!(params.options.tab_size, 8);
8260 Ok(Some(vec![]))
8261 });
8262 let save = editor
8263 .update_in(cx, |editor, window, cx| {
8264 editor.save(true, project.clone(), window, cx)
8265 })
8266 .unwrap();
8267 cx.executor().start_waiting();
8268 save.await;
8269 }
8270}
8271
8272#[gpui::test]
8273async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
8274 init_test(cx, |_| {});
8275
8276 let cols = 4;
8277 let rows = 10;
8278 let sample_text_1 = sample_text(rows, cols, 'a');
8279 assert_eq!(
8280 sample_text_1,
8281 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
8282 );
8283 let sample_text_2 = sample_text(rows, cols, 'l');
8284 assert_eq!(
8285 sample_text_2,
8286 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
8287 );
8288 let sample_text_3 = sample_text(rows, cols, 'v');
8289 assert_eq!(
8290 sample_text_3,
8291 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
8292 );
8293
8294 let fs = FakeFs::new(cx.executor());
8295 fs.insert_tree(
8296 path!("/a"),
8297 json!({
8298 "main.rs": sample_text_1,
8299 "other.rs": sample_text_2,
8300 "lib.rs": sample_text_3,
8301 }),
8302 )
8303 .await;
8304
8305 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
8306 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
8307 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
8308
8309 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8310 language_registry.add(rust_lang());
8311 let mut fake_servers = language_registry.register_fake_lsp(
8312 "Rust",
8313 FakeLspAdapter {
8314 capabilities: lsp::ServerCapabilities {
8315 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8316 ..Default::default()
8317 },
8318 ..Default::default()
8319 },
8320 );
8321
8322 let worktree = project.update(cx, |project, cx| {
8323 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
8324 assert_eq!(worktrees.len(), 1);
8325 worktrees.pop().unwrap()
8326 });
8327 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
8328
8329 let buffer_1 = project
8330 .update(cx, |project, cx| {
8331 project.open_buffer((worktree_id, "main.rs"), cx)
8332 })
8333 .await
8334 .unwrap();
8335 let buffer_2 = project
8336 .update(cx, |project, cx| {
8337 project.open_buffer((worktree_id, "other.rs"), cx)
8338 })
8339 .await
8340 .unwrap();
8341 let buffer_3 = project
8342 .update(cx, |project, cx| {
8343 project.open_buffer((worktree_id, "lib.rs"), cx)
8344 })
8345 .await
8346 .unwrap();
8347
8348 let multi_buffer = cx.new(|cx| {
8349 let mut multi_buffer = MultiBuffer::new(ReadWrite);
8350 multi_buffer.push_excerpts(
8351 buffer_1.clone(),
8352 [
8353 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8354 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8355 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8356 ],
8357 cx,
8358 );
8359 multi_buffer.push_excerpts(
8360 buffer_2.clone(),
8361 [
8362 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8363 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8364 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8365 ],
8366 cx,
8367 );
8368 multi_buffer.push_excerpts(
8369 buffer_3.clone(),
8370 [
8371 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8372 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8373 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8374 ],
8375 cx,
8376 );
8377 multi_buffer
8378 });
8379 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
8380 Editor::new(
8381 EditorMode::full(),
8382 multi_buffer,
8383 Some(project.clone()),
8384 window,
8385 cx,
8386 )
8387 });
8388
8389 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8390 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8391 s.select_ranges(Some(1..2))
8392 });
8393 editor.insert("|one|two|three|", window, cx);
8394 });
8395 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8396 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8397 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8398 s.select_ranges(Some(60..70))
8399 });
8400 editor.insert("|four|five|six|", window, cx);
8401 });
8402 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8403
8404 // First two buffers should be edited, but not the third one.
8405 assert_eq!(
8406 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8407 "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}",
8408 );
8409 buffer_1.update(cx, |buffer, _| {
8410 assert!(buffer.is_dirty());
8411 assert_eq!(
8412 buffer.text(),
8413 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
8414 )
8415 });
8416 buffer_2.update(cx, |buffer, _| {
8417 assert!(buffer.is_dirty());
8418 assert_eq!(
8419 buffer.text(),
8420 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
8421 )
8422 });
8423 buffer_3.update(cx, |buffer, _| {
8424 assert!(!buffer.is_dirty());
8425 assert_eq!(buffer.text(), sample_text_3,)
8426 });
8427 cx.executor().run_until_parked();
8428
8429 cx.executor().start_waiting();
8430 let save = multi_buffer_editor
8431 .update_in(cx, |editor, window, cx| {
8432 editor.save(true, project.clone(), window, cx)
8433 })
8434 .unwrap();
8435
8436 let fake_server = fake_servers.next().await.unwrap();
8437 fake_server
8438 .server
8439 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
8440 Ok(Some(vec![lsp::TextEdit::new(
8441 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8442 format!("[{} formatted]", params.text_document.uri),
8443 )]))
8444 })
8445 .detach();
8446 save.await;
8447
8448 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
8449 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
8450 assert_eq!(
8451 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8452 uri!(
8453 "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}"
8454 ),
8455 );
8456 buffer_1.update(cx, |buffer, _| {
8457 assert!(!buffer.is_dirty());
8458 assert_eq!(
8459 buffer.text(),
8460 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
8461 )
8462 });
8463 buffer_2.update(cx, |buffer, _| {
8464 assert!(!buffer.is_dirty());
8465 assert_eq!(
8466 buffer.text(),
8467 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
8468 )
8469 });
8470 buffer_3.update(cx, |buffer, _| {
8471 assert!(!buffer.is_dirty());
8472 assert_eq!(buffer.text(), sample_text_3,)
8473 });
8474}
8475
8476#[gpui::test]
8477async fn test_range_format_during_save(cx: &mut TestAppContext) {
8478 init_test(cx, |_| {});
8479
8480 let fs = FakeFs::new(cx.executor());
8481 fs.insert_file(path!("/file.rs"), Default::default()).await;
8482
8483 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8484
8485 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8486 language_registry.add(rust_lang());
8487 let mut fake_servers = language_registry.register_fake_lsp(
8488 "Rust",
8489 FakeLspAdapter {
8490 capabilities: lsp::ServerCapabilities {
8491 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
8492 ..Default::default()
8493 },
8494 ..Default::default()
8495 },
8496 );
8497
8498 let buffer = project
8499 .update(cx, |project, cx| {
8500 project.open_local_buffer(path!("/file.rs"), cx)
8501 })
8502 .await
8503 .unwrap();
8504
8505 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8506 let (editor, cx) = cx.add_window_view(|window, cx| {
8507 build_editor_with_project(project.clone(), buffer, window, cx)
8508 });
8509 editor.update_in(cx, |editor, window, cx| {
8510 editor.set_text("one\ntwo\nthree\n", window, cx)
8511 });
8512 assert!(cx.read(|cx| editor.is_dirty(cx)));
8513
8514 cx.executor().start_waiting();
8515 let fake_server = fake_servers.next().await.unwrap();
8516
8517 let save = editor
8518 .update_in(cx, |editor, window, cx| {
8519 editor.save(true, project.clone(), window, cx)
8520 })
8521 .unwrap();
8522 fake_server
8523 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8524 assert_eq!(
8525 params.text_document.uri,
8526 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8527 );
8528 assert_eq!(params.options.tab_size, 4);
8529 Ok(Some(vec![lsp::TextEdit::new(
8530 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8531 ", ".to_string(),
8532 )]))
8533 })
8534 .next()
8535 .await;
8536 cx.executor().start_waiting();
8537 save.await;
8538 assert_eq!(
8539 editor.update(cx, |editor, cx| editor.text(cx)),
8540 "one, two\nthree\n"
8541 );
8542 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8543
8544 editor.update_in(cx, |editor, window, cx| {
8545 editor.set_text("one\ntwo\nthree\n", window, cx)
8546 });
8547 assert!(cx.read(|cx| editor.is_dirty(cx)));
8548
8549 // Ensure we can still save even if formatting hangs.
8550 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
8551 move |params, _| async move {
8552 assert_eq!(
8553 params.text_document.uri,
8554 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8555 );
8556 futures::future::pending::<()>().await;
8557 unreachable!()
8558 },
8559 );
8560 let save = editor
8561 .update_in(cx, |editor, window, cx| {
8562 editor.save(true, project.clone(), window, cx)
8563 })
8564 .unwrap();
8565 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8566 cx.executor().start_waiting();
8567 save.await;
8568 assert_eq!(
8569 editor.update(cx, |editor, cx| editor.text(cx)),
8570 "one\ntwo\nthree\n"
8571 );
8572 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8573
8574 // For non-dirty buffer, no formatting request should be sent
8575 let save = editor
8576 .update_in(cx, |editor, window, cx| {
8577 editor.save(true, project.clone(), window, cx)
8578 })
8579 .unwrap();
8580 let _pending_format_request = fake_server
8581 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
8582 panic!("Should not be invoked on non-dirty buffer");
8583 })
8584 .next();
8585 cx.executor().start_waiting();
8586 save.await;
8587
8588 // Set Rust language override and assert overridden tabsize is sent to language server
8589 update_test_language_settings(cx, |settings| {
8590 settings.languages.insert(
8591 "Rust".into(),
8592 LanguageSettingsContent {
8593 tab_size: NonZeroU32::new(8),
8594 ..Default::default()
8595 },
8596 );
8597 });
8598
8599 editor.update_in(cx, |editor, window, cx| {
8600 editor.set_text("somehting_new\n", window, cx)
8601 });
8602 assert!(cx.read(|cx| editor.is_dirty(cx)));
8603 let save = editor
8604 .update_in(cx, |editor, window, cx| {
8605 editor.save(true, project.clone(), window, cx)
8606 })
8607 .unwrap();
8608 fake_server
8609 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8610 assert_eq!(
8611 params.text_document.uri,
8612 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8613 );
8614 assert_eq!(params.options.tab_size, 8);
8615 Ok(Some(vec![]))
8616 })
8617 .next()
8618 .await;
8619 cx.executor().start_waiting();
8620 save.await;
8621}
8622
8623#[gpui::test]
8624async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
8625 init_test(cx, |settings| {
8626 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8627 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8628 ))
8629 });
8630
8631 let fs = FakeFs::new(cx.executor());
8632 fs.insert_file(path!("/file.rs"), Default::default()).await;
8633
8634 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8635
8636 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8637 language_registry.add(Arc::new(Language::new(
8638 LanguageConfig {
8639 name: "Rust".into(),
8640 matcher: LanguageMatcher {
8641 path_suffixes: vec!["rs".to_string()],
8642 ..Default::default()
8643 },
8644 ..LanguageConfig::default()
8645 },
8646 Some(tree_sitter_rust::LANGUAGE.into()),
8647 )));
8648 update_test_language_settings(cx, |settings| {
8649 // Enable Prettier formatting for the same buffer, and ensure
8650 // LSP is called instead of Prettier.
8651 settings.defaults.prettier = Some(PrettierSettings {
8652 allowed: true,
8653 ..PrettierSettings::default()
8654 });
8655 });
8656 let mut fake_servers = language_registry.register_fake_lsp(
8657 "Rust",
8658 FakeLspAdapter {
8659 capabilities: lsp::ServerCapabilities {
8660 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8661 ..Default::default()
8662 },
8663 ..Default::default()
8664 },
8665 );
8666
8667 let buffer = project
8668 .update(cx, |project, cx| {
8669 project.open_local_buffer(path!("/file.rs"), cx)
8670 })
8671 .await
8672 .unwrap();
8673
8674 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8675 let (editor, cx) = cx.add_window_view(|window, cx| {
8676 build_editor_with_project(project.clone(), buffer, window, cx)
8677 });
8678 editor.update_in(cx, |editor, window, cx| {
8679 editor.set_text("one\ntwo\nthree\n", window, cx)
8680 });
8681
8682 cx.executor().start_waiting();
8683 let fake_server = fake_servers.next().await.unwrap();
8684
8685 let format = editor
8686 .update_in(cx, |editor, window, cx| {
8687 editor.perform_format(
8688 project.clone(),
8689 FormatTrigger::Manual,
8690 FormatTarget::Buffers,
8691 window,
8692 cx,
8693 )
8694 })
8695 .unwrap();
8696 fake_server
8697 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8698 assert_eq!(
8699 params.text_document.uri,
8700 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8701 );
8702 assert_eq!(params.options.tab_size, 4);
8703 Ok(Some(vec![lsp::TextEdit::new(
8704 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8705 ", ".to_string(),
8706 )]))
8707 })
8708 .next()
8709 .await;
8710 cx.executor().start_waiting();
8711 format.await;
8712 assert_eq!(
8713 editor.update(cx, |editor, cx| editor.text(cx)),
8714 "one, two\nthree\n"
8715 );
8716
8717 editor.update_in(cx, |editor, window, cx| {
8718 editor.set_text("one\ntwo\nthree\n", window, cx)
8719 });
8720 // Ensure we don't lock if formatting hangs.
8721 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8722 move |params, _| async move {
8723 assert_eq!(
8724 params.text_document.uri,
8725 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8726 );
8727 futures::future::pending::<()>().await;
8728 unreachable!()
8729 },
8730 );
8731 let format = editor
8732 .update_in(cx, |editor, window, cx| {
8733 editor.perform_format(
8734 project,
8735 FormatTrigger::Manual,
8736 FormatTarget::Buffers,
8737 window,
8738 cx,
8739 )
8740 })
8741 .unwrap();
8742 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8743 cx.executor().start_waiting();
8744 format.await;
8745 assert_eq!(
8746 editor.update(cx, |editor, cx| editor.text(cx)),
8747 "one\ntwo\nthree\n"
8748 );
8749}
8750
8751#[gpui::test]
8752async fn test_multiple_formatters(cx: &mut TestAppContext) {
8753 init_test(cx, |settings| {
8754 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
8755 settings.defaults.formatter =
8756 Some(language_settings::SelectedFormatter::List(FormatterList(
8757 vec![
8758 Formatter::LanguageServer { name: None },
8759 Formatter::CodeActions(
8760 [
8761 ("code-action-1".into(), true),
8762 ("code-action-2".into(), true),
8763 ]
8764 .into_iter()
8765 .collect(),
8766 ),
8767 ]
8768 .into(),
8769 )))
8770 });
8771
8772 let fs = FakeFs::new(cx.executor());
8773 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
8774 .await;
8775
8776 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8777 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8778 language_registry.add(rust_lang());
8779
8780 let mut fake_servers = language_registry.register_fake_lsp(
8781 "Rust",
8782 FakeLspAdapter {
8783 capabilities: lsp::ServerCapabilities {
8784 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8785 execute_command_provider: Some(lsp::ExecuteCommandOptions {
8786 commands: vec!["the-command-for-code-action-1".into()],
8787 ..Default::default()
8788 }),
8789 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
8790 ..Default::default()
8791 },
8792 ..Default::default()
8793 },
8794 );
8795
8796 let buffer = project
8797 .update(cx, |project, cx| {
8798 project.open_local_buffer(path!("/file.rs"), cx)
8799 })
8800 .await
8801 .unwrap();
8802
8803 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8804 let (editor, cx) = cx.add_window_view(|window, cx| {
8805 build_editor_with_project(project.clone(), buffer, window, cx)
8806 });
8807
8808 cx.executor().start_waiting();
8809
8810 let fake_server = fake_servers.next().await.unwrap();
8811 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8812 move |_params, _| async move {
8813 Ok(Some(vec![lsp::TextEdit::new(
8814 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8815 "applied-formatting\n".to_string(),
8816 )]))
8817 },
8818 );
8819 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
8820 move |params, _| async move {
8821 assert_eq!(
8822 params.context.only,
8823 Some(vec!["code-action-1".into(), "code-action-2".into()])
8824 );
8825 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
8826 Ok(Some(vec![
8827 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
8828 kind: Some("code-action-1".into()),
8829 edit: Some(lsp::WorkspaceEdit::new(
8830 [(
8831 uri.clone(),
8832 vec![lsp::TextEdit::new(
8833 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8834 "applied-code-action-1-edit\n".to_string(),
8835 )],
8836 )]
8837 .into_iter()
8838 .collect(),
8839 )),
8840 command: Some(lsp::Command {
8841 command: "the-command-for-code-action-1".into(),
8842 ..Default::default()
8843 }),
8844 ..Default::default()
8845 }),
8846 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
8847 kind: Some("code-action-2".into()),
8848 edit: Some(lsp::WorkspaceEdit::new(
8849 [(
8850 uri.clone(),
8851 vec![lsp::TextEdit::new(
8852 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8853 "applied-code-action-2-edit\n".to_string(),
8854 )],
8855 )]
8856 .into_iter()
8857 .collect(),
8858 )),
8859 ..Default::default()
8860 }),
8861 ]))
8862 },
8863 );
8864
8865 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
8866 move |params, _| async move { Ok(params) }
8867 });
8868
8869 let command_lock = Arc::new(futures::lock::Mutex::new(()));
8870 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
8871 let fake = fake_server.clone();
8872 let lock = command_lock.clone();
8873 move |params, _| {
8874 assert_eq!(params.command, "the-command-for-code-action-1");
8875 let fake = fake.clone();
8876 let lock = lock.clone();
8877 async move {
8878 lock.lock().await;
8879 fake.server
8880 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
8881 label: None,
8882 edit: lsp::WorkspaceEdit {
8883 changes: Some(
8884 [(
8885 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
8886 vec![lsp::TextEdit {
8887 range: lsp::Range::new(
8888 lsp::Position::new(0, 0),
8889 lsp::Position::new(0, 0),
8890 ),
8891 new_text: "applied-code-action-1-command\n".into(),
8892 }],
8893 )]
8894 .into_iter()
8895 .collect(),
8896 ),
8897 ..Default::default()
8898 },
8899 })
8900 .await
8901 .unwrap();
8902 Ok(Some(json!(null)))
8903 }
8904 }
8905 });
8906
8907 cx.executor().start_waiting();
8908 editor
8909 .update_in(cx, |editor, window, cx| {
8910 editor.perform_format(
8911 project.clone(),
8912 FormatTrigger::Manual,
8913 FormatTarget::Buffers,
8914 window,
8915 cx,
8916 )
8917 })
8918 .unwrap()
8919 .await;
8920 editor.update(cx, |editor, cx| {
8921 assert_eq!(
8922 editor.text(cx),
8923 r#"
8924 applied-code-action-2-edit
8925 applied-code-action-1-command
8926 applied-code-action-1-edit
8927 applied-formatting
8928 one
8929 two
8930 three
8931 "#
8932 .unindent()
8933 );
8934 });
8935
8936 editor.update_in(cx, |editor, window, cx| {
8937 editor.undo(&Default::default(), window, cx);
8938 assert_eq!(editor.text(cx), "one \ntwo \nthree");
8939 });
8940
8941 // Perform a manual edit while waiting for an LSP command
8942 // that's being run as part of a formatting code action.
8943 let lock_guard = command_lock.lock().await;
8944 let format = editor
8945 .update_in(cx, |editor, window, cx| {
8946 editor.perform_format(
8947 project.clone(),
8948 FormatTrigger::Manual,
8949 FormatTarget::Buffers,
8950 window,
8951 cx,
8952 )
8953 })
8954 .unwrap();
8955 cx.run_until_parked();
8956 editor.update(cx, |editor, cx| {
8957 assert_eq!(
8958 editor.text(cx),
8959 r#"
8960 applied-code-action-1-edit
8961 applied-formatting
8962 one
8963 two
8964 three
8965 "#
8966 .unindent()
8967 );
8968
8969 editor.buffer.update(cx, |buffer, cx| {
8970 let ix = buffer.len(cx);
8971 buffer.edit([(ix..ix, "edited\n")], None, cx);
8972 });
8973 });
8974
8975 // Allow the LSP command to proceed. Because the buffer was edited,
8976 // the second code action will not be run.
8977 drop(lock_guard);
8978 format.await;
8979 editor.update_in(cx, |editor, window, cx| {
8980 assert_eq!(
8981 editor.text(cx),
8982 r#"
8983 applied-code-action-1-command
8984 applied-code-action-1-edit
8985 applied-formatting
8986 one
8987 two
8988 three
8989 edited
8990 "#
8991 .unindent()
8992 );
8993
8994 // The manual edit is undone first, because it is the last thing the user did
8995 // (even though the command completed afterwards).
8996 editor.undo(&Default::default(), window, cx);
8997 assert_eq!(
8998 editor.text(cx),
8999 r#"
9000 applied-code-action-1-command
9001 applied-code-action-1-edit
9002 applied-formatting
9003 one
9004 two
9005 three
9006 "#
9007 .unindent()
9008 );
9009
9010 // All the formatting (including the command, which completed after the manual edit)
9011 // is undone together.
9012 editor.undo(&Default::default(), window, cx);
9013 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9014 });
9015}
9016
9017#[gpui::test]
9018async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
9019 init_test(cx, |settings| {
9020 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
9021 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
9022 ))
9023 });
9024
9025 let fs = FakeFs::new(cx.executor());
9026 fs.insert_file(path!("/file.ts"), Default::default()).await;
9027
9028 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9029
9030 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9031 language_registry.add(Arc::new(Language::new(
9032 LanguageConfig {
9033 name: "TypeScript".into(),
9034 matcher: LanguageMatcher {
9035 path_suffixes: vec!["ts".to_string()],
9036 ..Default::default()
9037 },
9038 ..LanguageConfig::default()
9039 },
9040 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
9041 )));
9042 update_test_language_settings(cx, |settings| {
9043 settings.defaults.prettier = Some(PrettierSettings {
9044 allowed: true,
9045 ..PrettierSettings::default()
9046 });
9047 });
9048 let mut fake_servers = language_registry.register_fake_lsp(
9049 "TypeScript",
9050 FakeLspAdapter {
9051 capabilities: lsp::ServerCapabilities {
9052 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9053 ..Default::default()
9054 },
9055 ..Default::default()
9056 },
9057 );
9058
9059 let buffer = project
9060 .update(cx, |project, cx| {
9061 project.open_local_buffer(path!("/file.ts"), cx)
9062 })
9063 .await
9064 .unwrap();
9065
9066 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9067 let (editor, cx) = cx.add_window_view(|window, cx| {
9068 build_editor_with_project(project.clone(), buffer, window, cx)
9069 });
9070 editor.update_in(cx, |editor, window, cx| {
9071 editor.set_text(
9072 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9073 window,
9074 cx,
9075 )
9076 });
9077
9078 cx.executor().start_waiting();
9079 let fake_server = fake_servers.next().await.unwrap();
9080
9081 let format = editor
9082 .update_in(cx, |editor, window, cx| {
9083 editor.perform_code_action_kind(
9084 project.clone(),
9085 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9086 window,
9087 cx,
9088 )
9089 })
9090 .unwrap();
9091 fake_server
9092 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
9093 assert_eq!(
9094 params.text_document.uri,
9095 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9096 );
9097 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
9098 lsp::CodeAction {
9099 title: "Organize Imports".to_string(),
9100 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
9101 edit: Some(lsp::WorkspaceEdit {
9102 changes: Some(
9103 [(
9104 params.text_document.uri.clone(),
9105 vec![lsp::TextEdit::new(
9106 lsp::Range::new(
9107 lsp::Position::new(1, 0),
9108 lsp::Position::new(2, 0),
9109 ),
9110 "".to_string(),
9111 )],
9112 )]
9113 .into_iter()
9114 .collect(),
9115 ),
9116 ..Default::default()
9117 }),
9118 ..Default::default()
9119 },
9120 )]))
9121 })
9122 .next()
9123 .await;
9124 cx.executor().start_waiting();
9125 format.await;
9126 assert_eq!(
9127 editor.update(cx, |editor, cx| editor.text(cx)),
9128 "import { a } from 'module';\n\nconst x = a;\n"
9129 );
9130
9131 editor.update_in(cx, |editor, window, cx| {
9132 editor.set_text(
9133 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9134 window,
9135 cx,
9136 )
9137 });
9138 // Ensure we don't lock if code action hangs.
9139 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9140 move |params, _| async move {
9141 assert_eq!(
9142 params.text_document.uri,
9143 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9144 );
9145 futures::future::pending::<()>().await;
9146 unreachable!()
9147 },
9148 );
9149 let format = editor
9150 .update_in(cx, |editor, window, cx| {
9151 editor.perform_code_action_kind(
9152 project,
9153 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9154 window,
9155 cx,
9156 )
9157 })
9158 .unwrap();
9159 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
9160 cx.executor().start_waiting();
9161 format.await;
9162 assert_eq!(
9163 editor.update(cx, |editor, cx| editor.text(cx)),
9164 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
9165 );
9166}
9167
9168#[gpui::test]
9169async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
9170 init_test(cx, |_| {});
9171
9172 let mut cx = EditorLspTestContext::new_rust(
9173 lsp::ServerCapabilities {
9174 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9175 ..Default::default()
9176 },
9177 cx,
9178 )
9179 .await;
9180
9181 cx.set_state(indoc! {"
9182 one.twoˇ
9183 "});
9184
9185 // The format request takes a long time. When it completes, it inserts
9186 // a newline and an indent before the `.`
9187 cx.lsp
9188 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
9189 let executor = cx.background_executor().clone();
9190 async move {
9191 executor.timer(Duration::from_millis(100)).await;
9192 Ok(Some(vec![lsp::TextEdit {
9193 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
9194 new_text: "\n ".into(),
9195 }]))
9196 }
9197 });
9198
9199 // Submit a format request.
9200 let format_1 = cx
9201 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9202 .unwrap();
9203 cx.executor().run_until_parked();
9204
9205 // Submit a second format request.
9206 let format_2 = cx
9207 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9208 .unwrap();
9209 cx.executor().run_until_parked();
9210
9211 // Wait for both format requests to complete
9212 cx.executor().advance_clock(Duration::from_millis(200));
9213 cx.executor().start_waiting();
9214 format_1.await.unwrap();
9215 cx.executor().start_waiting();
9216 format_2.await.unwrap();
9217
9218 // The formatting edits only happens once.
9219 cx.assert_editor_state(indoc! {"
9220 one
9221 .twoˇ
9222 "});
9223}
9224
9225#[gpui::test]
9226async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
9227 init_test(cx, |settings| {
9228 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
9229 });
9230
9231 let mut cx = EditorLspTestContext::new_rust(
9232 lsp::ServerCapabilities {
9233 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9234 ..Default::default()
9235 },
9236 cx,
9237 )
9238 .await;
9239
9240 // Set up a buffer white some trailing whitespace and no trailing newline.
9241 cx.set_state(
9242 &[
9243 "one ", //
9244 "twoˇ", //
9245 "three ", //
9246 "four", //
9247 ]
9248 .join("\n"),
9249 );
9250
9251 // Submit a format request.
9252 let format = cx
9253 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9254 .unwrap();
9255
9256 // Record which buffer changes have been sent to the language server
9257 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
9258 cx.lsp
9259 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
9260 let buffer_changes = buffer_changes.clone();
9261 move |params, _| {
9262 buffer_changes.lock().extend(
9263 params
9264 .content_changes
9265 .into_iter()
9266 .map(|e| (e.range.unwrap(), e.text)),
9267 );
9268 }
9269 });
9270
9271 // Handle formatting requests to the language server.
9272 cx.lsp
9273 .set_request_handler::<lsp::request::Formatting, _, _>({
9274 let buffer_changes = buffer_changes.clone();
9275 move |_, _| {
9276 // When formatting is requested, trailing whitespace has already been stripped,
9277 // and the trailing newline has already been added.
9278 assert_eq!(
9279 &buffer_changes.lock()[1..],
9280 &[
9281 (
9282 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
9283 "".into()
9284 ),
9285 (
9286 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
9287 "".into()
9288 ),
9289 (
9290 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
9291 "\n".into()
9292 ),
9293 ]
9294 );
9295
9296 // Insert blank lines between each line of the buffer.
9297 async move {
9298 Ok(Some(vec![
9299 lsp::TextEdit {
9300 range: lsp::Range::new(
9301 lsp::Position::new(1, 0),
9302 lsp::Position::new(1, 0),
9303 ),
9304 new_text: "\n".into(),
9305 },
9306 lsp::TextEdit {
9307 range: lsp::Range::new(
9308 lsp::Position::new(2, 0),
9309 lsp::Position::new(2, 0),
9310 ),
9311 new_text: "\n".into(),
9312 },
9313 ]))
9314 }
9315 }
9316 });
9317
9318 // After formatting the buffer, the trailing whitespace is stripped,
9319 // a newline is appended, and the edits provided by the language server
9320 // have been applied.
9321 format.await.unwrap();
9322 cx.assert_editor_state(
9323 &[
9324 "one", //
9325 "", //
9326 "twoˇ", //
9327 "", //
9328 "three", //
9329 "four", //
9330 "", //
9331 ]
9332 .join("\n"),
9333 );
9334
9335 // Undoing the formatting undoes the trailing whitespace removal, the
9336 // trailing newline, and the LSP edits.
9337 cx.update_buffer(|buffer, cx| buffer.undo(cx));
9338 cx.assert_editor_state(
9339 &[
9340 "one ", //
9341 "twoˇ", //
9342 "three ", //
9343 "four", //
9344 ]
9345 .join("\n"),
9346 );
9347}
9348
9349#[gpui::test]
9350async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
9351 cx: &mut TestAppContext,
9352) {
9353 init_test(cx, |_| {});
9354
9355 cx.update(|cx| {
9356 cx.update_global::<SettingsStore, _>(|settings, cx| {
9357 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9358 settings.auto_signature_help = Some(true);
9359 });
9360 });
9361 });
9362
9363 let mut cx = EditorLspTestContext::new_rust(
9364 lsp::ServerCapabilities {
9365 signature_help_provider: Some(lsp::SignatureHelpOptions {
9366 ..Default::default()
9367 }),
9368 ..Default::default()
9369 },
9370 cx,
9371 )
9372 .await;
9373
9374 let language = Language::new(
9375 LanguageConfig {
9376 name: "Rust".into(),
9377 brackets: BracketPairConfig {
9378 pairs: vec![
9379 BracketPair {
9380 start: "{".to_string(),
9381 end: "}".to_string(),
9382 close: true,
9383 surround: true,
9384 newline: true,
9385 },
9386 BracketPair {
9387 start: "(".to_string(),
9388 end: ")".to_string(),
9389 close: true,
9390 surround: true,
9391 newline: true,
9392 },
9393 BracketPair {
9394 start: "/*".to_string(),
9395 end: " */".to_string(),
9396 close: true,
9397 surround: true,
9398 newline: true,
9399 },
9400 BracketPair {
9401 start: "[".to_string(),
9402 end: "]".to_string(),
9403 close: false,
9404 surround: false,
9405 newline: true,
9406 },
9407 BracketPair {
9408 start: "\"".to_string(),
9409 end: "\"".to_string(),
9410 close: true,
9411 surround: true,
9412 newline: false,
9413 },
9414 BracketPair {
9415 start: "<".to_string(),
9416 end: ">".to_string(),
9417 close: false,
9418 surround: true,
9419 newline: true,
9420 },
9421 ],
9422 ..Default::default()
9423 },
9424 autoclose_before: "})]".to_string(),
9425 ..Default::default()
9426 },
9427 Some(tree_sitter_rust::LANGUAGE.into()),
9428 );
9429 let language = Arc::new(language);
9430
9431 cx.language_registry().add(language.clone());
9432 cx.update_buffer(|buffer, cx| {
9433 buffer.set_language(Some(language), cx);
9434 });
9435
9436 cx.set_state(
9437 &r#"
9438 fn main() {
9439 sampleˇ
9440 }
9441 "#
9442 .unindent(),
9443 );
9444
9445 cx.update_editor(|editor, window, cx| {
9446 editor.handle_input("(", window, cx);
9447 });
9448 cx.assert_editor_state(
9449 &"
9450 fn main() {
9451 sample(ˇ)
9452 }
9453 "
9454 .unindent(),
9455 );
9456
9457 let mocked_response = lsp::SignatureHelp {
9458 signatures: vec![lsp::SignatureInformation {
9459 label: "fn sample(param1: u8, param2: u8)".to_string(),
9460 documentation: None,
9461 parameters: Some(vec![
9462 lsp::ParameterInformation {
9463 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9464 documentation: None,
9465 },
9466 lsp::ParameterInformation {
9467 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9468 documentation: None,
9469 },
9470 ]),
9471 active_parameter: None,
9472 }],
9473 active_signature: Some(0),
9474 active_parameter: Some(0),
9475 };
9476 handle_signature_help_request(&mut cx, mocked_response).await;
9477
9478 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9479 .await;
9480
9481 cx.editor(|editor, _, _| {
9482 let signature_help_state = editor.signature_help_state.popover().cloned();
9483 assert_eq!(
9484 signature_help_state.unwrap().label,
9485 "param1: u8, param2: u8"
9486 );
9487 });
9488}
9489
9490#[gpui::test]
9491async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
9492 init_test(cx, |_| {});
9493
9494 cx.update(|cx| {
9495 cx.update_global::<SettingsStore, _>(|settings, cx| {
9496 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9497 settings.auto_signature_help = Some(false);
9498 settings.show_signature_help_after_edits = Some(false);
9499 });
9500 });
9501 });
9502
9503 let mut cx = EditorLspTestContext::new_rust(
9504 lsp::ServerCapabilities {
9505 signature_help_provider: Some(lsp::SignatureHelpOptions {
9506 ..Default::default()
9507 }),
9508 ..Default::default()
9509 },
9510 cx,
9511 )
9512 .await;
9513
9514 let language = Language::new(
9515 LanguageConfig {
9516 name: "Rust".into(),
9517 brackets: BracketPairConfig {
9518 pairs: vec![
9519 BracketPair {
9520 start: "{".to_string(),
9521 end: "}".to_string(),
9522 close: true,
9523 surround: true,
9524 newline: true,
9525 },
9526 BracketPair {
9527 start: "(".to_string(),
9528 end: ")".to_string(),
9529 close: true,
9530 surround: true,
9531 newline: true,
9532 },
9533 BracketPair {
9534 start: "/*".to_string(),
9535 end: " */".to_string(),
9536 close: true,
9537 surround: true,
9538 newline: true,
9539 },
9540 BracketPair {
9541 start: "[".to_string(),
9542 end: "]".to_string(),
9543 close: false,
9544 surround: false,
9545 newline: true,
9546 },
9547 BracketPair {
9548 start: "\"".to_string(),
9549 end: "\"".to_string(),
9550 close: true,
9551 surround: true,
9552 newline: false,
9553 },
9554 BracketPair {
9555 start: "<".to_string(),
9556 end: ">".to_string(),
9557 close: false,
9558 surround: true,
9559 newline: true,
9560 },
9561 ],
9562 ..Default::default()
9563 },
9564 autoclose_before: "})]".to_string(),
9565 ..Default::default()
9566 },
9567 Some(tree_sitter_rust::LANGUAGE.into()),
9568 );
9569 let language = Arc::new(language);
9570
9571 cx.language_registry().add(language.clone());
9572 cx.update_buffer(|buffer, cx| {
9573 buffer.set_language(Some(language), cx);
9574 });
9575
9576 // Ensure that signature_help is not called when no signature help is enabled.
9577 cx.set_state(
9578 &r#"
9579 fn main() {
9580 sampleˇ
9581 }
9582 "#
9583 .unindent(),
9584 );
9585 cx.update_editor(|editor, window, cx| {
9586 editor.handle_input("(", window, cx);
9587 });
9588 cx.assert_editor_state(
9589 &"
9590 fn main() {
9591 sample(ˇ)
9592 }
9593 "
9594 .unindent(),
9595 );
9596 cx.editor(|editor, _, _| {
9597 assert!(editor.signature_help_state.task().is_none());
9598 });
9599
9600 let mocked_response = lsp::SignatureHelp {
9601 signatures: vec![lsp::SignatureInformation {
9602 label: "fn sample(param1: u8, param2: u8)".to_string(),
9603 documentation: None,
9604 parameters: Some(vec![
9605 lsp::ParameterInformation {
9606 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9607 documentation: None,
9608 },
9609 lsp::ParameterInformation {
9610 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9611 documentation: None,
9612 },
9613 ]),
9614 active_parameter: None,
9615 }],
9616 active_signature: Some(0),
9617 active_parameter: Some(0),
9618 };
9619
9620 // Ensure that signature_help is called when enabled afte edits
9621 cx.update(|_, cx| {
9622 cx.update_global::<SettingsStore, _>(|settings, cx| {
9623 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9624 settings.auto_signature_help = Some(false);
9625 settings.show_signature_help_after_edits = Some(true);
9626 });
9627 });
9628 });
9629 cx.set_state(
9630 &r#"
9631 fn main() {
9632 sampleˇ
9633 }
9634 "#
9635 .unindent(),
9636 );
9637 cx.update_editor(|editor, window, cx| {
9638 editor.handle_input("(", window, cx);
9639 });
9640 cx.assert_editor_state(
9641 &"
9642 fn main() {
9643 sample(ˇ)
9644 }
9645 "
9646 .unindent(),
9647 );
9648 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9649 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9650 .await;
9651 cx.update_editor(|editor, _, _| {
9652 let signature_help_state = editor.signature_help_state.popover().cloned();
9653 assert!(signature_help_state.is_some());
9654 assert_eq!(
9655 signature_help_state.unwrap().label,
9656 "param1: u8, param2: u8"
9657 );
9658 editor.signature_help_state = SignatureHelpState::default();
9659 });
9660
9661 // Ensure that signature_help is called when auto signature help override is enabled
9662 cx.update(|_, cx| {
9663 cx.update_global::<SettingsStore, _>(|settings, cx| {
9664 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9665 settings.auto_signature_help = Some(true);
9666 settings.show_signature_help_after_edits = Some(false);
9667 });
9668 });
9669 });
9670 cx.set_state(
9671 &r#"
9672 fn main() {
9673 sampleˇ
9674 }
9675 "#
9676 .unindent(),
9677 );
9678 cx.update_editor(|editor, window, cx| {
9679 editor.handle_input("(", window, cx);
9680 });
9681 cx.assert_editor_state(
9682 &"
9683 fn main() {
9684 sample(ˇ)
9685 }
9686 "
9687 .unindent(),
9688 );
9689 handle_signature_help_request(&mut cx, mocked_response).await;
9690 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9691 .await;
9692 cx.editor(|editor, _, _| {
9693 let signature_help_state = editor.signature_help_state.popover().cloned();
9694 assert!(signature_help_state.is_some());
9695 assert_eq!(
9696 signature_help_state.unwrap().label,
9697 "param1: u8, param2: u8"
9698 );
9699 });
9700}
9701
9702#[gpui::test]
9703async fn test_signature_help(cx: &mut TestAppContext) {
9704 init_test(cx, |_| {});
9705 cx.update(|cx| {
9706 cx.update_global::<SettingsStore, _>(|settings, cx| {
9707 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9708 settings.auto_signature_help = Some(true);
9709 });
9710 });
9711 });
9712
9713 let mut cx = EditorLspTestContext::new_rust(
9714 lsp::ServerCapabilities {
9715 signature_help_provider: Some(lsp::SignatureHelpOptions {
9716 ..Default::default()
9717 }),
9718 ..Default::default()
9719 },
9720 cx,
9721 )
9722 .await;
9723
9724 // A test that directly calls `show_signature_help`
9725 cx.update_editor(|editor, window, cx| {
9726 editor.show_signature_help(&ShowSignatureHelp, window, cx);
9727 });
9728
9729 let mocked_response = lsp::SignatureHelp {
9730 signatures: vec![lsp::SignatureInformation {
9731 label: "fn sample(param1: u8, param2: u8)".to_string(),
9732 documentation: None,
9733 parameters: Some(vec![
9734 lsp::ParameterInformation {
9735 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9736 documentation: None,
9737 },
9738 lsp::ParameterInformation {
9739 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9740 documentation: None,
9741 },
9742 ]),
9743 active_parameter: None,
9744 }],
9745 active_signature: Some(0),
9746 active_parameter: Some(0),
9747 };
9748 handle_signature_help_request(&mut cx, mocked_response).await;
9749
9750 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9751 .await;
9752
9753 cx.editor(|editor, _, _| {
9754 let signature_help_state = editor.signature_help_state.popover().cloned();
9755 assert!(signature_help_state.is_some());
9756 assert_eq!(
9757 signature_help_state.unwrap().label,
9758 "param1: u8, param2: u8"
9759 );
9760 });
9761
9762 // When exiting outside from inside the brackets, `signature_help` is closed.
9763 cx.set_state(indoc! {"
9764 fn main() {
9765 sample(ˇ);
9766 }
9767
9768 fn sample(param1: u8, param2: u8) {}
9769 "});
9770
9771 cx.update_editor(|editor, window, cx| {
9772 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
9773 });
9774
9775 let mocked_response = lsp::SignatureHelp {
9776 signatures: Vec::new(),
9777 active_signature: None,
9778 active_parameter: None,
9779 };
9780 handle_signature_help_request(&mut cx, mocked_response).await;
9781
9782 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
9783 .await;
9784
9785 cx.editor(|editor, _, _| {
9786 assert!(!editor.signature_help_state.is_shown());
9787 });
9788
9789 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
9790 cx.set_state(indoc! {"
9791 fn main() {
9792 sample(ˇ);
9793 }
9794
9795 fn sample(param1: u8, param2: u8) {}
9796 "});
9797
9798 let mocked_response = lsp::SignatureHelp {
9799 signatures: vec![lsp::SignatureInformation {
9800 label: "fn sample(param1: u8, param2: u8)".to_string(),
9801 documentation: None,
9802 parameters: Some(vec![
9803 lsp::ParameterInformation {
9804 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9805 documentation: None,
9806 },
9807 lsp::ParameterInformation {
9808 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9809 documentation: None,
9810 },
9811 ]),
9812 active_parameter: None,
9813 }],
9814 active_signature: Some(0),
9815 active_parameter: Some(0),
9816 };
9817 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9818 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9819 .await;
9820 cx.editor(|editor, _, _| {
9821 assert!(editor.signature_help_state.is_shown());
9822 });
9823
9824 // Restore the popover with more parameter input
9825 cx.set_state(indoc! {"
9826 fn main() {
9827 sample(param1, param2ˇ);
9828 }
9829
9830 fn sample(param1: u8, param2: u8) {}
9831 "});
9832
9833 let mocked_response = lsp::SignatureHelp {
9834 signatures: vec![lsp::SignatureInformation {
9835 label: "fn sample(param1: u8, param2: u8)".to_string(),
9836 documentation: None,
9837 parameters: Some(vec![
9838 lsp::ParameterInformation {
9839 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9840 documentation: None,
9841 },
9842 lsp::ParameterInformation {
9843 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9844 documentation: None,
9845 },
9846 ]),
9847 active_parameter: None,
9848 }],
9849 active_signature: Some(0),
9850 active_parameter: Some(1),
9851 };
9852 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9853 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9854 .await;
9855
9856 // When selecting a range, the popover is gone.
9857 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
9858 cx.update_editor(|editor, window, cx| {
9859 editor.change_selections(None, window, cx, |s| {
9860 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
9861 })
9862 });
9863 cx.assert_editor_state(indoc! {"
9864 fn main() {
9865 sample(param1, «ˇparam2»);
9866 }
9867
9868 fn sample(param1: u8, param2: u8) {}
9869 "});
9870 cx.editor(|editor, _, _| {
9871 assert!(!editor.signature_help_state.is_shown());
9872 });
9873
9874 // When unselecting again, the popover is back if within the brackets.
9875 cx.update_editor(|editor, window, cx| {
9876 editor.change_selections(None, window, cx, |s| {
9877 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9878 })
9879 });
9880 cx.assert_editor_state(indoc! {"
9881 fn main() {
9882 sample(param1, ˇparam2);
9883 }
9884
9885 fn sample(param1: u8, param2: u8) {}
9886 "});
9887 handle_signature_help_request(&mut cx, mocked_response).await;
9888 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9889 .await;
9890 cx.editor(|editor, _, _| {
9891 assert!(editor.signature_help_state.is_shown());
9892 });
9893
9894 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
9895 cx.update_editor(|editor, window, cx| {
9896 editor.change_selections(None, window, cx, |s| {
9897 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
9898 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9899 })
9900 });
9901 cx.assert_editor_state(indoc! {"
9902 fn main() {
9903 sample(param1, ˇparam2);
9904 }
9905
9906 fn sample(param1: u8, param2: u8) {}
9907 "});
9908
9909 let mocked_response = lsp::SignatureHelp {
9910 signatures: vec![lsp::SignatureInformation {
9911 label: "fn sample(param1: u8, param2: u8)".to_string(),
9912 documentation: None,
9913 parameters: Some(vec![
9914 lsp::ParameterInformation {
9915 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9916 documentation: None,
9917 },
9918 lsp::ParameterInformation {
9919 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9920 documentation: None,
9921 },
9922 ]),
9923 active_parameter: None,
9924 }],
9925 active_signature: Some(0),
9926 active_parameter: Some(1),
9927 };
9928 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9929 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9930 .await;
9931 cx.update_editor(|editor, _, cx| {
9932 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
9933 });
9934 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
9935 .await;
9936 cx.update_editor(|editor, window, cx| {
9937 editor.change_selections(None, window, cx, |s| {
9938 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
9939 })
9940 });
9941 cx.assert_editor_state(indoc! {"
9942 fn main() {
9943 sample(param1, «ˇparam2»);
9944 }
9945
9946 fn sample(param1: u8, param2: u8) {}
9947 "});
9948 cx.update_editor(|editor, window, cx| {
9949 editor.change_selections(None, window, cx, |s| {
9950 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9951 })
9952 });
9953 cx.assert_editor_state(indoc! {"
9954 fn main() {
9955 sample(param1, ˇparam2);
9956 }
9957
9958 fn sample(param1: u8, param2: u8) {}
9959 "});
9960 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
9961 .await;
9962}
9963
9964#[gpui::test]
9965async fn test_completion_mode(cx: &mut TestAppContext) {
9966 init_test(cx, |_| {});
9967 let mut cx = EditorLspTestContext::new_rust(
9968 lsp::ServerCapabilities {
9969 completion_provider: Some(lsp::CompletionOptions {
9970 resolve_provider: Some(true),
9971 ..Default::default()
9972 }),
9973 ..Default::default()
9974 },
9975 cx,
9976 )
9977 .await;
9978
9979 struct Run {
9980 run_description: &'static str,
9981 initial_state: String,
9982 buffer_marked_text: String,
9983 completion_text: &'static str,
9984 expected_with_insert_mode: String,
9985 expected_with_replace_mode: String,
9986 expected_with_replace_subsequence_mode: String,
9987 expected_with_replace_suffix_mode: String,
9988 }
9989
9990 let runs = [
9991 Run {
9992 run_description: "Start of word matches completion text",
9993 initial_state: "before ediˇ after".into(),
9994 buffer_marked_text: "before <edi|> after".into(),
9995 completion_text: "editor",
9996 expected_with_insert_mode: "before editorˇ after".into(),
9997 expected_with_replace_mode: "before editorˇ after".into(),
9998 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
9999 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10000 },
10001 Run {
10002 run_description: "Accept same text at the middle of the word",
10003 initial_state: "before ediˇtor after".into(),
10004 buffer_marked_text: "before <edi|tor> after".into(),
10005 completion_text: "editor",
10006 expected_with_insert_mode: "before editorˇtor after".into(),
10007 expected_with_replace_mode: "before editorˇ after".into(),
10008 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10009 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10010 },
10011 Run {
10012 run_description: "End of word matches completion text -- cursor at end",
10013 initial_state: "before torˇ after".into(),
10014 buffer_marked_text: "before <tor|> after".into(),
10015 completion_text: "editor",
10016 expected_with_insert_mode: "before editorˇ after".into(),
10017 expected_with_replace_mode: "before editorˇ after".into(),
10018 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10019 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10020 },
10021 Run {
10022 run_description: "End of word matches completion text -- cursor at start",
10023 initial_state: "before ˇtor after".into(),
10024 buffer_marked_text: "before <|tor> after".into(),
10025 completion_text: "editor",
10026 expected_with_insert_mode: "before editorˇtor after".into(),
10027 expected_with_replace_mode: "before editorˇ after".into(),
10028 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10029 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10030 },
10031 Run {
10032 run_description: "Prepend text containing whitespace",
10033 initial_state: "pˇfield: bool".into(),
10034 buffer_marked_text: "<p|field>: bool".into(),
10035 completion_text: "pub ",
10036 expected_with_insert_mode: "pub ˇfield: bool".into(),
10037 expected_with_replace_mode: "pub ˇ: bool".into(),
10038 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
10039 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
10040 },
10041 Run {
10042 run_description: "Add element to start of list",
10043 initial_state: "[element_ˇelement_2]".into(),
10044 buffer_marked_text: "[<element_|element_2>]".into(),
10045 completion_text: "element_1",
10046 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
10047 expected_with_replace_mode: "[element_1ˇ]".into(),
10048 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
10049 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
10050 },
10051 Run {
10052 run_description: "Add element to start of list -- first and second elements are equal",
10053 initial_state: "[elˇelement]".into(),
10054 buffer_marked_text: "[<el|element>]".into(),
10055 completion_text: "element",
10056 expected_with_insert_mode: "[elementˇelement]".into(),
10057 expected_with_replace_mode: "[elementˇ]".into(),
10058 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
10059 expected_with_replace_suffix_mode: "[elementˇ]".into(),
10060 },
10061 Run {
10062 run_description: "Ends with matching suffix",
10063 initial_state: "SubˇError".into(),
10064 buffer_marked_text: "<Sub|Error>".into(),
10065 completion_text: "SubscriptionError",
10066 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
10067 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10068 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10069 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
10070 },
10071 Run {
10072 run_description: "Suffix is a subsequence -- contiguous",
10073 initial_state: "SubˇErr".into(),
10074 buffer_marked_text: "<Sub|Err>".into(),
10075 completion_text: "SubscriptionError",
10076 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
10077 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10078 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10079 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
10080 },
10081 Run {
10082 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
10083 initial_state: "Suˇscrirr".into(),
10084 buffer_marked_text: "<Su|scrirr>".into(),
10085 completion_text: "SubscriptionError",
10086 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
10087 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10088 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10089 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
10090 },
10091 Run {
10092 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
10093 initial_state: "foo(indˇix)".into(),
10094 buffer_marked_text: "foo(<ind|ix>)".into(),
10095 completion_text: "node_index",
10096 expected_with_insert_mode: "foo(node_indexˇix)".into(),
10097 expected_with_replace_mode: "foo(node_indexˇ)".into(),
10098 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
10099 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
10100 },
10101 ];
10102
10103 for run in runs {
10104 let run_variations = [
10105 (LspInsertMode::Insert, run.expected_with_insert_mode),
10106 (LspInsertMode::Replace, run.expected_with_replace_mode),
10107 (
10108 LspInsertMode::ReplaceSubsequence,
10109 run.expected_with_replace_subsequence_mode,
10110 ),
10111 (
10112 LspInsertMode::ReplaceSuffix,
10113 run.expected_with_replace_suffix_mode,
10114 ),
10115 ];
10116
10117 for (lsp_insert_mode, expected_text) in run_variations {
10118 eprintln!(
10119 "run = {:?}, mode = {lsp_insert_mode:.?}",
10120 run.run_description,
10121 );
10122
10123 update_test_language_settings(&mut cx, |settings| {
10124 settings.defaults.completions = Some(CompletionSettings {
10125 lsp_insert_mode,
10126 words: WordsCompletionMode::Disabled,
10127 lsp: true,
10128 lsp_fetch_timeout_ms: 0,
10129 });
10130 });
10131
10132 cx.set_state(&run.initial_state);
10133 cx.update_editor(|editor, window, cx| {
10134 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10135 });
10136
10137 let counter = Arc::new(AtomicUsize::new(0));
10138 handle_completion_request_with_insert_and_replace(
10139 &mut cx,
10140 &run.buffer_marked_text,
10141 vec![run.completion_text],
10142 counter.clone(),
10143 )
10144 .await;
10145 cx.condition(|editor, _| editor.context_menu_visible())
10146 .await;
10147 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10148
10149 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10150 editor
10151 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10152 .unwrap()
10153 });
10154 cx.assert_editor_state(&expected_text);
10155 handle_resolve_completion_request(&mut cx, None).await;
10156 apply_additional_edits.await.unwrap();
10157 }
10158 }
10159}
10160
10161#[gpui::test]
10162async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
10163 init_test(cx, |_| {});
10164 let mut cx = EditorLspTestContext::new_rust(
10165 lsp::ServerCapabilities {
10166 completion_provider: Some(lsp::CompletionOptions {
10167 resolve_provider: Some(true),
10168 ..Default::default()
10169 }),
10170 ..Default::default()
10171 },
10172 cx,
10173 )
10174 .await;
10175
10176 let initial_state = "SubˇError";
10177 let buffer_marked_text = "<Sub|Error>";
10178 let completion_text = "SubscriptionError";
10179 let expected_with_insert_mode = "SubscriptionErrorˇError";
10180 let expected_with_replace_mode = "SubscriptionErrorˇ";
10181
10182 update_test_language_settings(&mut cx, |settings| {
10183 settings.defaults.completions = Some(CompletionSettings {
10184 words: WordsCompletionMode::Disabled,
10185 // set the opposite here to ensure that the action is overriding the default behavior
10186 lsp_insert_mode: LspInsertMode::Insert,
10187 lsp: true,
10188 lsp_fetch_timeout_ms: 0,
10189 });
10190 });
10191
10192 cx.set_state(initial_state);
10193 cx.update_editor(|editor, window, cx| {
10194 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10195 });
10196
10197 let counter = Arc::new(AtomicUsize::new(0));
10198 handle_completion_request_with_insert_and_replace(
10199 &mut cx,
10200 &buffer_marked_text,
10201 vec![completion_text],
10202 counter.clone(),
10203 )
10204 .await;
10205 cx.condition(|editor, _| editor.context_menu_visible())
10206 .await;
10207 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10208
10209 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10210 editor
10211 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10212 .unwrap()
10213 });
10214 cx.assert_editor_state(&expected_with_replace_mode);
10215 handle_resolve_completion_request(&mut cx, None).await;
10216 apply_additional_edits.await.unwrap();
10217
10218 update_test_language_settings(&mut cx, |settings| {
10219 settings.defaults.completions = Some(CompletionSettings {
10220 words: WordsCompletionMode::Disabled,
10221 // set the opposite here to ensure that the action is overriding the default behavior
10222 lsp_insert_mode: LspInsertMode::Replace,
10223 lsp: true,
10224 lsp_fetch_timeout_ms: 0,
10225 });
10226 });
10227
10228 cx.set_state(initial_state);
10229 cx.update_editor(|editor, window, cx| {
10230 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10231 });
10232 handle_completion_request_with_insert_and_replace(
10233 &mut cx,
10234 &buffer_marked_text,
10235 vec![completion_text],
10236 counter.clone(),
10237 )
10238 .await;
10239 cx.condition(|editor, _| editor.context_menu_visible())
10240 .await;
10241 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
10242
10243 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10244 editor
10245 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
10246 .unwrap()
10247 });
10248 cx.assert_editor_state(&expected_with_insert_mode);
10249 handle_resolve_completion_request(&mut cx, None).await;
10250 apply_additional_edits.await.unwrap();
10251}
10252
10253#[gpui::test]
10254async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
10255 init_test(cx, |_| {});
10256 let mut cx = EditorLspTestContext::new_rust(
10257 lsp::ServerCapabilities {
10258 completion_provider: Some(lsp::CompletionOptions {
10259 resolve_provider: Some(true),
10260 ..Default::default()
10261 }),
10262 ..Default::default()
10263 },
10264 cx,
10265 )
10266 .await;
10267
10268 // scenario: surrounding text matches completion text
10269 let completion_text = "to_offset";
10270 let initial_state = indoc! {"
10271 1. buf.to_offˇsuffix
10272 2. buf.to_offˇsuf
10273 3. buf.to_offˇfix
10274 4. buf.to_offˇ
10275 5. into_offˇensive
10276 6. ˇsuffix
10277 7. let ˇ //
10278 8. aaˇzz
10279 9. buf.to_off«zzzzzˇ»suffix
10280 10. buf.«ˇzzzzz»suffix
10281 11. to_off«ˇzzzzz»
10282
10283 buf.to_offˇsuffix // newest cursor
10284 "};
10285 let completion_marked_buffer = indoc! {"
10286 1. buf.to_offsuffix
10287 2. buf.to_offsuf
10288 3. buf.to_offfix
10289 4. buf.to_off
10290 5. into_offensive
10291 6. suffix
10292 7. let //
10293 8. aazz
10294 9. buf.to_offzzzzzsuffix
10295 10. buf.zzzzzsuffix
10296 11. to_offzzzzz
10297
10298 buf.<to_off|suffix> // newest cursor
10299 "};
10300 let expected = indoc! {"
10301 1. buf.to_offsetˇ
10302 2. buf.to_offsetˇsuf
10303 3. buf.to_offsetˇfix
10304 4. buf.to_offsetˇ
10305 5. into_offsetˇensive
10306 6. to_offsetˇsuffix
10307 7. let to_offsetˇ //
10308 8. aato_offsetˇzz
10309 9. buf.to_offsetˇ
10310 10. buf.to_offsetˇsuffix
10311 11. to_offsetˇ
10312
10313 buf.to_offsetˇ // newest cursor
10314 "};
10315 cx.set_state(initial_state);
10316 cx.update_editor(|editor, window, cx| {
10317 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10318 });
10319 handle_completion_request_with_insert_and_replace(
10320 &mut cx,
10321 completion_marked_buffer,
10322 vec![completion_text],
10323 Arc::new(AtomicUsize::new(0)),
10324 )
10325 .await;
10326 cx.condition(|editor, _| editor.context_menu_visible())
10327 .await;
10328 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10329 editor
10330 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10331 .unwrap()
10332 });
10333 cx.assert_editor_state(expected);
10334 handle_resolve_completion_request(&mut cx, None).await;
10335 apply_additional_edits.await.unwrap();
10336
10337 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
10338 let completion_text = "foo_and_bar";
10339 let initial_state = indoc! {"
10340 1. ooanbˇ
10341 2. zooanbˇ
10342 3. ooanbˇz
10343 4. zooanbˇz
10344 5. ooanˇ
10345 6. oanbˇ
10346
10347 ooanbˇ
10348 "};
10349 let completion_marked_buffer = indoc! {"
10350 1. ooanb
10351 2. zooanb
10352 3. ooanbz
10353 4. zooanbz
10354 5. ooan
10355 6. oanb
10356
10357 <ooanb|>
10358 "};
10359 let expected = indoc! {"
10360 1. foo_and_barˇ
10361 2. zfoo_and_barˇ
10362 3. foo_and_barˇz
10363 4. zfoo_and_barˇz
10364 5. ooanfoo_and_barˇ
10365 6. oanbfoo_and_barˇ
10366
10367 foo_and_barˇ
10368 "};
10369 cx.set_state(initial_state);
10370 cx.update_editor(|editor, window, cx| {
10371 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10372 });
10373 handle_completion_request_with_insert_and_replace(
10374 &mut cx,
10375 completion_marked_buffer,
10376 vec![completion_text],
10377 Arc::new(AtomicUsize::new(0)),
10378 )
10379 .await;
10380 cx.condition(|editor, _| editor.context_menu_visible())
10381 .await;
10382 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10383 editor
10384 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10385 .unwrap()
10386 });
10387 cx.assert_editor_state(expected);
10388 handle_resolve_completion_request(&mut cx, None).await;
10389 apply_additional_edits.await.unwrap();
10390
10391 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
10392 // (expects the same as if it was inserted at the end)
10393 let completion_text = "foo_and_bar";
10394 let initial_state = indoc! {"
10395 1. ooˇanb
10396 2. zooˇanb
10397 3. ooˇanbz
10398 4. zooˇanbz
10399
10400 ooˇanb
10401 "};
10402 let completion_marked_buffer = indoc! {"
10403 1. ooanb
10404 2. zooanb
10405 3. ooanbz
10406 4. zooanbz
10407
10408 <oo|anb>
10409 "};
10410 let expected = indoc! {"
10411 1. foo_and_barˇ
10412 2. zfoo_and_barˇ
10413 3. foo_and_barˇz
10414 4. zfoo_and_barˇz
10415
10416 foo_and_barˇ
10417 "};
10418 cx.set_state(initial_state);
10419 cx.update_editor(|editor, window, cx| {
10420 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10421 });
10422 handle_completion_request_with_insert_and_replace(
10423 &mut cx,
10424 completion_marked_buffer,
10425 vec![completion_text],
10426 Arc::new(AtomicUsize::new(0)),
10427 )
10428 .await;
10429 cx.condition(|editor, _| editor.context_menu_visible())
10430 .await;
10431 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10432 editor
10433 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10434 .unwrap()
10435 });
10436 cx.assert_editor_state(expected);
10437 handle_resolve_completion_request(&mut cx, None).await;
10438 apply_additional_edits.await.unwrap();
10439}
10440
10441// This used to crash
10442#[gpui::test]
10443async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
10444 init_test(cx, |_| {});
10445
10446 let buffer_text = indoc! {"
10447 fn main() {
10448 10.satu;
10449
10450 //
10451 // separate cursors so they open in different excerpts (manually reproducible)
10452 //
10453
10454 10.satu20;
10455 }
10456 "};
10457 let multibuffer_text_with_selections = indoc! {"
10458 fn main() {
10459 10.satuˇ;
10460
10461 //
10462
10463 //
10464
10465 10.satuˇ20;
10466 }
10467 "};
10468 let expected_multibuffer = indoc! {"
10469 fn main() {
10470 10.saturating_sub()ˇ;
10471
10472 //
10473
10474 //
10475
10476 10.saturating_sub()ˇ;
10477 }
10478 "};
10479
10480 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
10481 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
10482
10483 let fs = FakeFs::new(cx.executor());
10484 fs.insert_tree(
10485 path!("/a"),
10486 json!({
10487 "main.rs": buffer_text,
10488 }),
10489 )
10490 .await;
10491
10492 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
10493 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10494 language_registry.add(rust_lang());
10495 let mut fake_servers = language_registry.register_fake_lsp(
10496 "Rust",
10497 FakeLspAdapter {
10498 capabilities: lsp::ServerCapabilities {
10499 completion_provider: Some(lsp::CompletionOptions {
10500 resolve_provider: None,
10501 ..lsp::CompletionOptions::default()
10502 }),
10503 ..lsp::ServerCapabilities::default()
10504 },
10505 ..FakeLspAdapter::default()
10506 },
10507 );
10508 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10509 let cx = &mut VisualTestContext::from_window(*workspace, cx);
10510 let buffer = project
10511 .update(cx, |project, cx| {
10512 project.open_local_buffer(path!("/a/main.rs"), cx)
10513 })
10514 .await
10515 .unwrap();
10516
10517 let multi_buffer = cx.new(|cx| {
10518 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
10519 multi_buffer.push_excerpts(
10520 buffer.clone(),
10521 [ExcerptRange::new(0..first_excerpt_end)],
10522 cx,
10523 );
10524 multi_buffer.push_excerpts(
10525 buffer.clone(),
10526 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
10527 cx,
10528 );
10529 multi_buffer
10530 });
10531
10532 let editor = workspace
10533 .update(cx, |_, window, cx| {
10534 cx.new(|cx| {
10535 Editor::new(
10536 EditorMode::Full {
10537 scale_ui_elements_with_buffer_font_size: false,
10538 show_active_line_background: false,
10539 sized_by_content: false,
10540 },
10541 multi_buffer.clone(),
10542 Some(project.clone()),
10543 window,
10544 cx,
10545 )
10546 })
10547 })
10548 .unwrap();
10549
10550 let pane = workspace
10551 .update(cx, |workspace, _, _| workspace.active_pane().clone())
10552 .unwrap();
10553 pane.update_in(cx, |pane, window, cx| {
10554 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
10555 });
10556
10557 let fake_server = fake_servers.next().await.unwrap();
10558
10559 editor.update_in(cx, |editor, window, cx| {
10560 editor.change_selections(None, window, cx, |s| {
10561 s.select_ranges([
10562 Point::new(1, 11)..Point::new(1, 11),
10563 Point::new(7, 11)..Point::new(7, 11),
10564 ])
10565 });
10566
10567 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
10568 });
10569
10570 editor.update_in(cx, |editor, window, cx| {
10571 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10572 });
10573
10574 fake_server
10575 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
10576 let completion_item = lsp::CompletionItem {
10577 label: "saturating_sub()".into(),
10578 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
10579 lsp::InsertReplaceEdit {
10580 new_text: "saturating_sub()".to_owned(),
10581 insert: lsp::Range::new(
10582 lsp::Position::new(7, 7),
10583 lsp::Position::new(7, 11),
10584 ),
10585 replace: lsp::Range::new(
10586 lsp::Position::new(7, 7),
10587 lsp::Position::new(7, 13),
10588 ),
10589 },
10590 )),
10591 ..lsp::CompletionItem::default()
10592 };
10593
10594 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
10595 })
10596 .next()
10597 .await
10598 .unwrap();
10599
10600 cx.condition(&editor, |editor, _| editor.context_menu_visible())
10601 .await;
10602
10603 editor
10604 .update_in(cx, |editor, window, cx| {
10605 editor
10606 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10607 .unwrap()
10608 })
10609 .await
10610 .unwrap();
10611
10612 editor.update(cx, |editor, cx| {
10613 assert_text_with_selections(editor, expected_multibuffer, cx);
10614 })
10615}
10616
10617#[gpui::test]
10618async fn test_completion(cx: &mut TestAppContext) {
10619 init_test(cx, |_| {});
10620
10621 let mut cx = EditorLspTestContext::new_rust(
10622 lsp::ServerCapabilities {
10623 completion_provider: Some(lsp::CompletionOptions {
10624 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10625 resolve_provider: Some(true),
10626 ..Default::default()
10627 }),
10628 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10629 ..Default::default()
10630 },
10631 cx,
10632 )
10633 .await;
10634 let counter = Arc::new(AtomicUsize::new(0));
10635
10636 cx.set_state(indoc! {"
10637 oneˇ
10638 two
10639 three
10640 "});
10641 cx.simulate_keystroke(".");
10642 handle_completion_request(
10643 &mut cx,
10644 indoc! {"
10645 one.|<>
10646 two
10647 three
10648 "},
10649 vec!["first_completion", "second_completion"],
10650 counter.clone(),
10651 )
10652 .await;
10653 cx.condition(|editor, _| editor.context_menu_visible())
10654 .await;
10655 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10656
10657 let _handler = handle_signature_help_request(
10658 &mut cx,
10659 lsp::SignatureHelp {
10660 signatures: vec![lsp::SignatureInformation {
10661 label: "test signature".to_string(),
10662 documentation: None,
10663 parameters: Some(vec![lsp::ParameterInformation {
10664 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
10665 documentation: None,
10666 }]),
10667 active_parameter: None,
10668 }],
10669 active_signature: None,
10670 active_parameter: None,
10671 },
10672 );
10673 cx.update_editor(|editor, window, cx| {
10674 assert!(
10675 !editor.signature_help_state.is_shown(),
10676 "No signature help was called for"
10677 );
10678 editor.show_signature_help(&ShowSignatureHelp, window, cx);
10679 });
10680 cx.run_until_parked();
10681 cx.update_editor(|editor, _, _| {
10682 assert!(
10683 !editor.signature_help_state.is_shown(),
10684 "No signature help should be shown when completions menu is open"
10685 );
10686 });
10687
10688 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10689 editor.context_menu_next(&Default::default(), window, cx);
10690 editor
10691 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10692 .unwrap()
10693 });
10694 cx.assert_editor_state(indoc! {"
10695 one.second_completionˇ
10696 two
10697 three
10698 "});
10699
10700 handle_resolve_completion_request(
10701 &mut cx,
10702 Some(vec![
10703 (
10704 //This overlaps with the primary completion edit which is
10705 //misbehavior from the LSP spec, test that we filter it out
10706 indoc! {"
10707 one.second_ˇcompletion
10708 two
10709 threeˇ
10710 "},
10711 "overlapping additional edit",
10712 ),
10713 (
10714 indoc! {"
10715 one.second_completion
10716 two
10717 threeˇ
10718 "},
10719 "\nadditional edit",
10720 ),
10721 ]),
10722 )
10723 .await;
10724 apply_additional_edits.await.unwrap();
10725 cx.assert_editor_state(indoc! {"
10726 one.second_completionˇ
10727 two
10728 three
10729 additional edit
10730 "});
10731
10732 cx.set_state(indoc! {"
10733 one.second_completion
10734 twoˇ
10735 threeˇ
10736 additional edit
10737 "});
10738 cx.simulate_keystroke(" ");
10739 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
10740 cx.simulate_keystroke("s");
10741 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
10742
10743 cx.assert_editor_state(indoc! {"
10744 one.second_completion
10745 two sˇ
10746 three sˇ
10747 additional edit
10748 "});
10749 handle_completion_request(
10750 &mut cx,
10751 indoc! {"
10752 one.second_completion
10753 two s
10754 three <s|>
10755 additional edit
10756 "},
10757 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
10758 counter.clone(),
10759 )
10760 .await;
10761 cx.condition(|editor, _| editor.context_menu_visible())
10762 .await;
10763 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
10764
10765 cx.simulate_keystroke("i");
10766
10767 handle_completion_request(
10768 &mut cx,
10769 indoc! {"
10770 one.second_completion
10771 two si
10772 three <si|>
10773 additional edit
10774 "},
10775 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
10776 counter.clone(),
10777 )
10778 .await;
10779 cx.condition(|editor, _| editor.context_menu_visible())
10780 .await;
10781 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
10782
10783 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10784 editor
10785 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10786 .unwrap()
10787 });
10788 cx.assert_editor_state(indoc! {"
10789 one.second_completion
10790 two sixth_completionˇ
10791 three sixth_completionˇ
10792 additional edit
10793 "});
10794
10795 apply_additional_edits.await.unwrap();
10796
10797 update_test_language_settings(&mut cx, |settings| {
10798 settings.defaults.show_completions_on_input = Some(false);
10799 });
10800 cx.set_state("editorˇ");
10801 cx.simulate_keystroke(".");
10802 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
10803 cx.simulate_keystrokes("c l o");
10804 cx.assert_editor_state("editor.cloˇ");
10805 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
10806 cx.update_editor(|editor, window, cx| {
10807 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10808 });
10809 handle_completion_request(
10810 &mut cx,
10811 "editor.<clo|>",
10812 vec!["close", "clobber"],
10813 counter.clone(),
10814 )
10815 .await;
10816 cx.condition(|editor, _| editor.context_menu_visible())
10817 .await;
10818 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
10819
10820 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10821 editor
10822 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10823 .unwrap()
10824 });
10825 cx.assert_editor_state("editor.closeˇ");
10826 handle_resolve_completion_request(&mut cx, None).await;
10827 apply_additional_edits.await.unwrap();
10828}
10829
10830#[gpui::test]
10831async fn test_word_completion(cx: &mut TestAppContext) {
10832 let lsp_fetch_timeout_ms = 10;
10833 init_test(cx, |language_settings| {
10834 language_settings.defaults.completions = Some(CompletionSettings {
10835 words: WordsCompletionMode::Fallback,
10836 lsp: true,
10837 lsp_fetch_timeout_ms: 10,
10838 lsp_insert_mode: LspInsertMode::Insert,
10839 });
10840 });
10841
10842 let mut cx = EditorLspTestContext::new_rust(
10843 lsp::ServerCapabilities {
10844 completion_provider: Some(lsp::CompletionOptions {
10845 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10846 ..lsp::CompletionOptions::default()
10847 }),
10848 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10849 ..lsp::ServerCapabilities::default()
10850 },
10851 cx,
10852 )
10853 .await;
10854
10855 let throttle_completions = Arc::new(AtomicBool::new(false));
10856
10857 let lsp_throttle_completions = throttle_completions.clone();
10858 let _completion_requests_handler =
10859 cx.lsp
10860 .server
10861 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
10862 let lsp_throttle_completions = lsp_throttle_completions.clone();
10863 let cx = cx.clone();
10864 async move {
10865 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
10866 cx.background_executor()
10867 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
10868 .await;
10869 }
10870 Ok(Some(lsp::CompletionResponse::Array(vec![
10871 lsp::CompletionItem {
10872 label: "first".into(),
10873 ..lsp::CompletionItem::default()
10874 },
10875 lsp::CompletionItem {
10876 label: "last".into(),
10877 ..lsp::CompletionItem::default()
10878 },
10879 ])))
10880 }
10881 });
10882
10883 cx.set_state(indoc! {"
10884 oneˇ
10885 two
10886 three
10887 "});
10888 cx.simulate_keystroke(".");
10889 cx.executor().run_until_parked();
10890 cx.condition(|editor, _| editor.context_menu_visible())
10891 .await;
10892 cx.update_editor(|editor, window, cx| {
10893 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10894 {
10895 assert_eq!(
10896 completion_menu_entries(&menu),
10897 &["first", "last"],
10898 "When LSP server is fast to reply, no fallback word completions are used"
10899 );
10900 } else {
10901 panic!("expected completion menu to be open");
10902 }
10903 editor.cancel(&Cancel, window, cx);
10904 });
10905 cx.executor().run_until_parked();
10906 cx.condition(|editor, _| !editor.context_menu_visible())
10907 .await;
10908
10909 throttle_completions.store(true, atomic::Ordering::Release);
10910 cx.simulate_keystroke(".");
10911 cx.executor()
10912 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
10913 cx.executor().run_until_parked();
10914 cx.condition(|editor, _| editor.context_menu_visible())
10915 .await;
10916 cx.update_editor(|editor, _, _| {
10917 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10918 {
10919 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
10920 "When LSP server is slow, document words can be shown instead, if configured accordingly");
10921 } else {
10922 panic!("expected completion menu to be open");
10923 }
10924 });
10925}
10926
10927#[gpui::test]
10928async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
10929 init_test(cx, |language_settings| {
10930 language_settings.defaults.completions = Some(CompletionSettings {
10931 words: WordsCompletionMode::Enabled,
10932 lsp: true,
10933 lsp_fetch_timeout_ms: 0,
10934 lsp_insert_mode: LspInsertMode::Insert,
10935 });
10936 });
10937
10938 let mut cx = EditorLspTestContext::new_rust(
10939 lsp::ServerCapabilities {
10940 completion_provider: Some(lsp::CompletionOptions {
10941 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10942 ..lsp::CompletionOptions::default()
10943 }),
10944 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10945 ..lsp::ServerCapabilities::default()
10946 },
10947 cx,
10948 )
10949 .await;
10950
10951 let _completion_requests_handler =
10952 cx.lsp
10953 .server
10954 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
10955 Ok(Some(lsp::CompletionResponse::Array(vec![
10956 lsp::CompletionItem {
10957 label: "first".into(),
10958 ..lsp::CompletionItem::default()
10959 },
10960 lsp::CompletionItem {
10961 label: "last".into(),
10962 ..lsp::CompletionItem::default()
10963 },
10964 ])))
10965 });
10966
10967 cx.set_state(indoc! {"ˇ
10968 first
10969 last
10970 second
10971 "});
10972 cx.simulate_keystroke(".");
10973 cx.executor().run_until_parked();
10974 cx.condition(|editor, _| editor.context_menu_visible())
10975 .await;
10976 cx.update_editor(|editor, _, _| {
10977 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10978 {
10979 assert_eq!(
10980 completion_menu_entries(&menu),
10981 &["first", "last", "second"],
10982 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
10983 );
10984 } else {
10985 panic!("expected completion menu to be open");
10986 }
10987 });
10988}
10989
10990#[gpui::test]
10991async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
10992 init_test(cx, |language_settings| {
10993 language_settings.defaults.completions = Some(CompletionSettings {
10994 words: WordsCompletionMode::Disabled,
10995 lsp: true,
10996 lsp_fetch_timeout_ms: 0,
10997 lsp_insert_mode: LspInsertMode::Insert,
10998 });
10999 });
11000
11001 let mut cx = EditorLspTestContext::new_rust(
11002 lsp::ServerCapabilities {
11003 completion_provider: Some(lsp::CompletionOptions {
11004 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11005 ..lsp::CompletionOptions::default()
11006 }),
11007 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11008 ..lsp::ServerCapabilities::default()
11009 },
11010 cx,
11011 )
11012 .await;
11013
11014 let _completion_requests_handler =
11015 cx.lsp
11016 .server
11017 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11018 panic!("LSP completions should not be queried when dealing with word completions")
11019 });
11020
11021 cx.set_state(indoc! {"ˇ
11022 first
11023 last
11024 second
11025 "});
11026 cx.update_editor(|editor, window, cx| {
11027 editor.show_word_completions(&ShowWordCompletions, window, cx);
11028 });
11029 cx.executor().run_until_parked();
11030 cx.condition(|editor, _| editor.context_menu_visible())
11031 .await;
11032 cx.update_editor(|editor, _, _| {
11033 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11034 {
11035 assert_eq!(
11036 completion_menu_entries(&menu),
11037 &["first", "last", "second"],
11038 "`ShowWordCompletions` action should show word completions"
11039 );
11040 } else {
11041 panic!("expected completion menu to be open");
11042 }
11043 });
11044
11045 cx.simulate_keystroke("l");
11046 cx.executor().run_until_parked();
11047 cx.condition(|editor, _| editor.context_menu_visible())
11048 .await;
11049 cx.update_editor(|editor, _, _| {
11050 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11051 {
11052 assert_eq!(
11053 completion_menu_entries(&menu),
11054 &["last"],
11055 "After showing word completions, further editing should filter them and not query the LSP"
11056 );
11057 } else {
11058 panic!("expected completion menu to be open");
11059 }
11060 });
11061}
11062
11063#[gpui::test]
11064async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
11065 init_test(cx, |language_settings| {
11066 language_settings.defaults.completions = Some(CompletionSettings {
11067 words: WordsCompletionMode::Fallback,
11068 lsp: false,
11069 lsp_fetch_timeout_ms: 0,
11070 lsp_insert_mode: LspInsertMode::Insert,
11071 });
11072 });
11073
11074 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
11075
11076 cx.set_state(indoc! {"ˇ
11077 0_usize
11078 let
11079 33
11080 4.5f32
11081 "});
11082 cx.update_editor(|editor, window, cx| {
11083 editor.show_completions(&ShowCompletions::default(), window, cx);
11084 });
11085 cx.executor().run_until_parked();
11086 cx.condition(|editor, _| editor.context_menu_visible())
11087 .await;
11088 cx.update_editor(|editor, window, cx| {
11089 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11090 {
11091 assert_eq!(
11092 completion_menu_entries(&menu),
11093 &["let"],
11094 "With no digits in the completion query, no digits should be in the word completions"
11095 );
11096 } else {
11097 panic!("expected completion menu to be open");
11098 }
11099 editor.cancel(&Cancel, window, cx);
11100 });
11101
11102 cx.set_state(indoc! {"3ˇ
11103 0_usize
11104 let
11105 3
11106 33.35f32
11107 "});
11108 cx.update_editor(|editor, window, cx| {
11109 editor.show_completions(&ShowCompletions::default(), window, cx);
11110 });
11111 cx.executor().run_until_parked();
11112 cx.condition(|editor, _| editor.context_menu_visible())
11113 .await;
11114 cx.update_editor(|editor, _, _| {
11115 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11116 {
11117 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
11118 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
11119 } else {
11120 panic!("expected completion menu to be open");
11121 }
11122 });
11123}
11124
11125fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
11126 let position = || lsp::Position {
11127 line: params.text_document_position.position.line,
11128 character: params.text_document_position.position.character,
11129 };
11130 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11131 range: lsp::Range {
11132 start: position(),
11133 end: position(),
11134 },
11135 new_text: text.to_string(),
11136 }))
11137}
11138
11139#[gpui::test]
11140async fn test_multiline_completion(cx: &mut TestAppContext) {
11141 init_test(cx, |_| {});
11142
11143 let fs = FakeFs::new(cx.executor());
11144 fs.insert_tree(
11145 path!("/a"),
11146 json!({
11147 "main.ts": "a",
11148 }),
11149 )
11150 .await;
11151
11152 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11153 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11154 let typescript_language = Arc::new(Language::new(
11155 LanguageConfig {
11156 name: "TypeScript".into(),
11157 matcher: LanguageMatcher {
11158 path_suffixes: vec!["ts".to_string()],
11159 ..LanguageMatcher::default()
11160 },
11161 line_comments: vec!["// ".into()],
11162 ..LanguageConfig::default()
11163 },
11164 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
11165 ));
11166 language_registry.add(typescript_language.clone());
11167 let mut fake_servers = language_registry.register_fake_lsp(
11168 "TypeScript",
11169 FakeLspAdapter {
11170 capabilities: lsp::ServerCapabilities {
11171 completion_provider: Some(lsp::CompletionOptions {
11172 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11173 ..lsp::CompletionOptions::default()
11174 }),
11175 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11176 ..lsp::ServerCapabilities::default()
11177 },
11178 // Emulate vtsls label generation
11179 label_for_completion: Some(Box::new(|item, _| {
11180 let text = if let Some(description) = item
11181 .label_details
11182 .as_ref()
11183 .and_then(|label_details| label_details.description.as_ref())
11184 {
11185 format!("{} {}", item.label, description)
11186 } else if let Some(detail) = &item.detail {
11187 format!("{} {}", item.label, detail)
11188 } else {
11189 item.label.clone()
11190 };
11191 let len = text.len();
11192 Some(language::CodeLabel {
11193 text,
11194 runs: Vec::new(),
11195 filter_range: 0..len,
11196 })
11197 })),
11198 ..FakeLspAdapter::default()
11199 },
11200 );
11201 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11202 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11203 let worktree_id = workspace
11204 .update(cx, |workspace, _window, cx| {
11205 workspace.project().update(cx, |project, cx| {
11206 project.worktrees(cx).next().unwrap().read(cx).id()
11207 })
11208 })
11209 .unwrap();
11210 let _buffer = project
11211 .update(cx, |project, cx| {
11212 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
11213 })
11214 .await
11215 .unwrap();
11216 let editor = workspace
11217 .update(cx, |workspace, window, cx| {
11218 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
11219 })
11220 .unwrap()
11221 .await
11222 .unwrap()
11223 .downcast::<Editor>()
11224 .unwrap();
11225 let fake_server = fake_servers.next().await.unwrap();
11226
11227 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
11228 let multiline_label_2 = "a\nb\nc\n";
11229 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
11230 let multiline_description = "d\ne\nf\n";
11231 let multiline_detail_2 = "g\nh\ni\n";
11232
11233 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
11234 move |params, _| async move {
11235 Ok(Some(lsp::CompletionResponse::Array(vec![
11236 lsp::CompletionItem {
11237 label: multiline_label.to_string(),
11238 text_edit: gen_text_edit(¶ms, "new_text_1"),
11239 ..lsp::CompletionItem::default()
11240 },
11241 lsp::CompletionItem {
11242 label: "single line label 1".to_string(),
11243 detail: Some(multiline_detail.to_string()),
11244 text_edit: gen_text_edit(¶ms, "new_text_2"),
11245 ..lsp::CompletionItem::default()
11246 },
11247 lsp::CompletionItem {
11248 label: "single line label 2".to_string(),
11249 label_details: Some(lsp::CompletionItemLabelDetails {
11250 description: Some(multiline_description.to_string()),
11251 detail: None,
11252 }),
11253 text_edit: gen_text_edit(¶ms, "new_text_2"),
11254 ..lsp::CompletionItem::default()
11255 },
11256 lsp::CompletionItem {
11257 label: multiline_label_2.to_string(),
11258 detail: Some(multiline_detail_2.to_string()),
11259 text_edit: gen_text_edit(¶ms, "new_text_3"),
11260 ..lsp::CompletionItem::default()
11261 },
11262 lsp::CompletionItem {
11263 label: "Label with many spaces and \t but without newlines".to_string(),
11264 detail: Some(
11265 "Details with many spaces and \t but without newlines".to_string(),
11266 ),
11267 text_edit: gen_text_edit(¶ms, "new_text_4"),
11268 ..lsp::CompletionItem::default()
11269 },
11270 ])))
11271 },
11272 );
11273
11274 editor.update_in(cx, |editor, window, cx| {
11275 cx.focus_self(window);
11276 editor.move_to_end(&MoveToEnd, window, cx);
11277 editor.handle_input(".", window, cx);
11278 });
11279 cx.run_until_parked();
11280 completion_handle.next().await.unwrap();
11281
11282 editor.update(cx, |editor, _| {
11283 assert!(editor.context_menu_visible());
11284 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11285 {
11286 let completion_labels = menu
11287 .completions
11288 .borrow()
11289 .iter()
11290 .map(|c| c.label.text.clone())
11291 .collect::<Vec<_>>();
11292 assert_eq!(
11293 completion_labels,
11294 &[
11295 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
11296 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
11297 "single line label 2 d e f ",
11298 "a b c g h i ",
11299 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
11300 ],
11301 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
11302 );
11303
11304 for completion in menu
11305 .completions
11306 .borrow()
11307 .iter() {
11308 assert_eq!(
11309 completion.label.filter_range,
11310 0..completion.label.text.len(),
11311 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
11312 );
11313 }
11314 } else {
11315 panic!("expected completion menu to be open");
11316 }
11317 });
11318}
11319
11320#[gpui::test]
11321async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
11322 init_test(cx, |_| {});
11323 let mut cx = EditorLspTestContext::new_rust(
11324 lsp::ServerCapabilities {
11325 completion_provider: Some(lsp::CompletionOptions {
11326 trigger_characters: Some(vec![".".to_string()]),
11327 ..Default::default()
11328 }),
11329 ..Default::default()
11330 },
11331 cx,
11332 )
11333 .await;
11334 cx.lsp
11335 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11336 Ok(Some(lsp::CompletionResponse::Array(vec![
11337 lsp::CompletionItem {
11338 label: "first".into(),
11339 ..Default::default()
11340 },
11341 lsp::CompletionItem {
11342 label: "last".into(),
11343 ..Default::default()
11344 },
11345 ])))
11346 });
11347 cx.set_state("variableˇ");
11348 cx.simulate_keystroke(".");
11349 cx.executor().run_until_parked();
11350
11351 cx.update_editor(|editor, _, _| {
11352 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11353 {
11354 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
11355 } else {
11356 panic!("expected completion menu to be open");
11357 }
11358 });
11359
11360 cx.update_editor(|editor, window, cx| {
11361 editor.move_page_down(&MovePageDown::default(), window, cx);
11362 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11363 {
11364 assert!(
11365 menu.selected_item == 1,
11366 "expected PageDown to select the last item from the context menu"
11367 );
11368 } else {
11369 panic!("expected completion menu to stay open after PageDown");
11370 }
11371 });
11372
11373 cx.update_editor(|editor, window, cx| {
11374 editor.move_page_up(&MovePageUp::default(), window, cx);
11375 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11376 {
11377 assert!(
11378 menu.selected_item == 0,
11379 "expected PageUp to select the first item from the context menu"
11380 );
11381 } else {
11382 panic!("expected completion menu to stay open after PageUp");
11383 }
11384 });
11385}
11386
11387#[gpui::test]
11388async fn test_as_is_completions(cx: &mut TestAppContext) {
11389 init_test(cx, |_| {});
11390 let mut cx = EditorLspTestContext::new_rust(
11391 lsp::ServerCapabilities {
11392 completion_provider: Some(lsp::CompletionOptions {
11393 ..Default::default()
11394 }),
11395 ..Default::default()
11396 },
11397 cx,
11398 )
11399 .await;
11400 cx.lsp
11401 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11402 Ok(Some(lsp::CompletionResponse::Array(vec![
11403 lsp::CompletionItem {
11404 label: "unsafe".into(),
11405 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11406 range: lsp::Range {
11407 start: lsp::Position {
11408 line: 1,
11409 character: 2,
11410 },
11411 end: lsp::Position {
11412 line: 1,
11413 character: 3,
11414 },
11415 },
11416 new_text: "unsafe".to_string(),
11417 })),
11418 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
11419 ..Default::default()
11420 },
11421 ])))
11422 });
11423 cx.set_state("fn a() {}\n nˇ");
11424 cx.executor().run_until_parked();
11425 cx.update_editor(|editor, window, cx| {
11426 editor.show_completions(
11427 &ShowCompletions {
11428 trigger: Some("\n".into()),
11429 },
11430 window,
11431 cx,
11432 );
11433 });
11434 cx.executor().run_until_parked();
11435
11436 cx.update_editor(|editor, window, cx| {
11437 editor.confirm_completion(&Default::default(), window, cx)
11438 });
11439 cx.executor().run_until_parked();
11440 cx.assert_editor_state("fn a() {}\n unsafeˇ");
11441}
11442
11443#[gpui::test]
11444async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
11445 init_test(cx, |_| {});
11446
11447 let mut cx = EditorLspTestContext::new_rust(
11448 lsp::ServerCapabilities {
11449 completion_provider: Some(lsp::CompletionOptions {
11450 trigger_characters: Some(vec![".".to_string()]),
11451 resolve_provider: Some(true),
11452 ..Default::default()
11453 }),
11454 ..Default::default()
11455 },
11456 cx,
11457 )
11458 .await;
11459
11460 cx.set_state("fn main() { let a = 2ˇ; }");
11461 cx.simulate_keystroke(".");
11462 let completion_item = lsp::CompletionItem {
11463 label: "Some".into(),
11464 kind: Some(lsp::CompletionItemKind::SNIPPET),
11465 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
11466 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
11467 kind: lsp::MarkupKind::Markdown,
11468 value: "```rust\nSome(2)\n```".to_string(),
11469 })),
11470 deprecated: Some(false),
11471 sort_text: Some("Some".to_string()),
11472 filter_text: Some("Some".to_string()),
11473 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
11474 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11475 range: lsp::Range {
11476 start: lsp::Position {
11477 line: 0,
11478 character: 22,
11479 },
11480 end: lsp::Position {
11481 line: 0,
11482 character: 22,
11483 },
11484 },
11485 new_text: "Some(2)".to_string(),
11486 })),
11487 additional_text_edits: Some(vec![lsp::TextEdit {
11488 range: lsp::Range {
11489 start: lsp::Position {
11490 line: 0,
11491 character: 20,
11492 },
11493 end: lsp::Position {
11494 line: 0,
11495 character: 22,
11496 },
11497 },
11498 new_text: "".to_string(),
11499 }]),
11500 ..Default::default()
11501 };
11502
11503 let closure_completion_item = completion_item.clone();
11504 let counter = Arc::new(AtomicUsize::new(0));
11505 let counter_clone = counter.clone();
11506 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
11507 let task_completion_item = closure_completion_item.clone();
11508 counter_clone.fetch_add(1, atomic::Ordering::Release);
11509 async move {
11510 Ok(Some(lsp::CompletionResponse::Array(vec![
11511 task_completion_item,
11512 ])))
11513 }
11514 });
11515
11516 cx.condition(|editor, _| editor.context_menu_visible())
11517 .await;
11518 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
11519 assert!(request.next().await.is_some());
11520 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11521
11522 cx.simulate_keystrokes("S o m");
11523 cx.condition(|editor, _| editor.context_menu_visible())
11524 .await;
11525 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
11526 assert!(request.next().await.is_some());
11527 assert!(request.next().await.is_some());
11528 assert!(request.next().await.is_some());
11529 request.close();
11530 assert!(request.next().await.is_none());
11531 assert_eq!(
11532 counter.load(atomic::Ordering::Acquire),
11533 4,
11534 "With the completions menu open, only one LSP request should happen per input"
11535 );
11536}
11537
11538#[gpui::test]
11539async fn test_toggle_comment(cx: &mut TestAppContext) {
11540 init_test(cx, |_| {});
11541 let mut cx = EditorTestContext::new(cx).await;
11542 let language = Arc::new(Language::new(
11543 LanguageConfig {
11544 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
11545 ..Default::default()
11546 },
11547 Some(tree_sitter_rust::LANGUAGE.into()),
11548 ));
11549 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
11550
11551 // If multiple selections intersect a line, the line is only toggled once.
11552 cx.set_state(indoc! {"
11553 fn a() {
11554 «//b();
11555 ˇ»// «c();
11556 //ˇ» d();
11557 }
11558 "});
11559
11560 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11561
11562 cx.assert_editor_state(indoc! {"
11563 fn a() {
11564 «b();
11565 c();
11566 ˇ» d();
11567 }
11568 "});
11569
11570 // The comment prefix is inserted at the same column for every line in a
11571 // selection.
11572 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11573
11574 cx.assert_editor_state(indoc! {"
11575 fn a() {
11576 // «b();
11577 // c();
11578 ˇ»// d();
11579 }
11580 "});
11581
11582 // If a selection ends at the beginning of a line, that line is not toggled.
11583 cx.set_selections_state(indoc! {"
11584 fn a() {
11585 // b();
11586 «// c();
11587 ˇ» // d();
11588 }
11589 "});
11590
11591 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11592
11593 cx.assert_editor_state(indoc! {"
11594 fn a() {
11595 // b();
11596 «c();
11597 ˇ» // d();
11598 }
11599 "});
11600
11601 // If a selection span a single line and is empty, the line is toggled.
11602 cx.set_state(indoc! {"
11603 fn a() {
11604 a();
11605 b();
11606 ˇ
11607 }
11608 "});
11609
11610 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11611
11612 cx.assert_editor_state(indoc! {"
11613 fn a() {
11614 a();
11615 b();
11616 //•ˇ
11617 }
11618 "});
11619
11620 // If a selection span multiple lines, empty lines are not toggled.
11621 cx.set_state(indoc! {"
11622 fn a() {
11623 «a();
11624
11625 c();ˇ»
11626 }
11627 "});
11628
11629 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11630
11631 cx.assert_editor_state(indoc! {"
11632 fn a() {
11633 // «a();
11634
11635 // c();ˇ»
11636 }
11637 "});
11638
11639 // If a selection includes multiple comment prefixes, all lines are uncommented.
11640 cx.set_state(indoc! {"
11641 fn a() {
11642 «// a();
11643 /// b();
11644 //! c();ˇ»
11645 }
11646 "});
11647
11648 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11649
11650 cx.assert_editor_state(indoc! {"
11651 fn a() {
11652 «a();
11653 b();
11654 c();ˇ»
11655 }
11656 "});
11657}
11658
11659#[gpui::test]
11660async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
11661 init_test(cx, |_| {});
11662 let mut cx = EditorTestContext::new(cx).await;
11663 let language = Arc::new(Language::new(
11664 LanguageConfig {
11665 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
11666 ..Default::default()
11667 },
11668 Some(tree_sitter_rust::LANGUAGE.into()),
11669 ));
11670 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
11671
11672 let toggle_comments = &ToggleComments {
11673 advance_downwards: false,
11674 ignore_indent: true,
11675 };
11676
11677 // If multiple selections intersect a line, the line is only toggled once.
11678 cx.set_state(indoc! {"
11679 fn a() {
11680 // «b();
11681 // c();
11682 // ˇ» d();
11683 }
11684 "});
11685
11686 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11687
11688 cx.assert_editor_state(indoc! {"
11689 fn a() {
11690 «b();
11691 c();
11692 ˇ» d();
11693 }
11694 "});
11695
11696 // The comment prefix is inserted at the beginning of each line
11697 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11698
11699 cx.assert_editor_state(indoc! {"
11700 fn a() {
11701 // «b();
11702 // c();
11703 // ˇ» d();
11704 }
11705 "});
11706
11707 // If a selection ends at the beginning of a line, that line is not toggled.
11708 cx.set_selections_state(indoc! {"
11709 fn a() {
11710 // b();
11711 // «c();
11712 ˇ»// d();
11713 }
11714 "});
11715
11716 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11717
11718 cx.assert_editor_state(indoc! {"
11719 fn a() {
11720 // b();
11721 «c();
11722 ˇ»// d();
11723 }
11724 "});
11725
11726 // If a selection span a single line and is empty, the line is toggled.
11727 cx.set_state(indoc! {"
11728 fn a() {
11729 a();
11730 b();
11731 ˇ
11732 }
11733 "});
11734
11735 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11736
11737 cx.assert_editor_state(indoc! {"
11738 fn a() {
11739 a();
11740 b();
11741 //ˇ
11742 }
11743 "});
11744
11745 // If a selection span multiple lines, empty lines are not toggled.
11746 cx.set_state(indoc! {"
11747 fn a() {
11748 «a();
11749
11750 c();ˇ»
11751 }
11752 "});
11753
11754 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11755
11756 cx.assert_editor_state(indoc! {"
11757 fn a() {
11758 // «a();
11759
11760 // c();ˇ»
11761 }
11762 "});
11763
11764 // If a selection includes multiple comment prefixes, all lines are uncommented.
11765 cx.set_state(indoc! {"
11766 fn a() {
11767 // «a();
11768 /// b();
11769 //! c();ˇ»
11770 }
11771 "});
11772
11773 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11774
11775 cx.assert_editor_state(indoc! {"
11776 fn a() {
11777 «a();
11778 b();
11779 c();ˇ»
11780 }
11781 "});
11782}
11783
11784#[gpui::test]
11785async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
11786 init_test(cx, |_| {});
11787
11788 let language = Arc::new(Language::new(
11789 LanguageConfig {
11790 line_comments: vec!["// ".into()],
11791 ..Default::default()
11792 },
11793 Some(tree_sitter_rust::LANGUAGE.into()),
11794 ));
11795
11796 let mut cx = EditorTestContext::new(cx).await;
11797
11798 cx.language_registry().add(language.clone());
11799 cx.update_buffer(|buffer, cx| {
11800 buffer.set_language(Some(language), cx);
11801 });
11802
11803 let toggle_comments = &ToggleComments {
11804 advance_downwards: true,
11805 ignore_indent: false,
11806 };
11807
11808 // Single cursor on one line -> advance
11809 // Cursor moves horizontally 3 characters as well on non-blank line
11810 cx.set_state(indoc!(
11811 "fn a() {
11812 ˇdog();
11813 cat();
11814 }"
11815 ));
11816 cx.update_editor(|editor, window, cx| {
11817 editor.toggle_comments(toggle_comments, window, cx);
11818 });
11819 cx.assert_editor_state(indoc!(
11820 "fn a() {
11821 // dog();
11822 catˇ();
11823 }"
11824 ));
11825
11826 // Single selection on one line -> don't advance
11827 cx.set_state(indoc!(
11828 "fn a() {
11829 «dog()ˇ»;
11830 cat();
11831 }"
11832 ));
11833 cx.update_editor(|editor, window, cx| {
11834 editor.toggle_comments(toggle_comments, window, cx);
11835 });
11836 cx.assert_editor_state(indoc!(
11837 "fn a() {
11838 // «dog()ˇ»;
11839 cat();
11840 }"
11841 ));
11842
11843 // Multiple cursors on one line -> advance
11844 cx.set_state(indoc!(
11845 "fn a() {
11846 ˇdˇog();
11847 cat();
11848 }"
11849 ));
11850 cx.update_editor(|editor, window, cx| {
11851 editor.toggle_comments(toggle_comments, window, cx);
11852 });
11853 cx.assert_editor_state(indoc!(
11854 "fn a() {
11855 // dog();
11856 catˇ(ˇ);
11857 }"
11858 ));
11859
11860 // Multiple cursors on one line, with selection -> don't advance
11861 cx.set_state(indoc!(
11862 "fn a() {
11863 ˇdˇog«()ˇ»;
11864 cat();
11865 }"
11866 ));
11867 cx.update_editor(|editor, window, cx| {
11868 editor.toggle_comments(toggle_comments, window, cx);
11869 });
11870 cx.assert_editor_state(indoc!(
11871 "fn a() {
11872 // ˇdˇog«()ˇ»;
11873 cat();
11874 }"
11875 ));
11876
11877 // Single cursor on one line -> advance
11878 // Cursor moves to column 0 on blank line
11879 cx.set_state(indoc!(
11880 "fn a() {
11881 ˇdog();
11882
11883 cat();
11884 }"
11885 ));
11886 cx.update_editor(|editor, window, cx| {
11887 editor.toggle_comments(toggle_comments, window, cx);
11888 });
11889 cx.assert_editor_state(indoc!(
11890 "fn a() {
11891 // dog();
11892 ˇ
11893 cat();
11894 }"
11895 ));
11896
11897 // Single cursor on one line -> advance
11898 // Cursor starts and ends at column 0
11899 cx.set_state(indoc!(
11900 "fn a() {
11901 ˇ dog();
11902 cat();
11903 }"
11904 ));
11905 cx.update_editor(|editor, window, cx| {
11906 editor.toggle_comments(toggle_comments, window, cx);
11907 });
11908 cx.assert_editor_state(indoc!(
11909 "fn a() {
11910 // dog();
11911 ˇ cat();
11912 }"
11913 ));
11914}
11915
11916#[gpui::test]
11917async fn test_toggle_block_comment(cx: &mut TestAppContext) {
11918 init_test(cx, |_| {});
11919
11920 let mut cx = EditorTestContext::new(cx).await;
11921
11922 let html_language = Arc::new(
11923 Language::new(
11924 LanguageConfig {
11925 name: "HTML".into(),
11926 block_comment: Some(("<!-- ".into(), " -->".into())),
11927 ..Default::default()
11928 },
11929 Some(tree_sitter_html::LANGUAGE.into()),
11930 )
11931 .with_injection_query(
11932 r#"
11933 (script_element
11934 (raw_text) @injection.content
11935 (#set! injection.language "javascript"))
11936 "#,
11937 )
11938 .unwrap(),
11939 );
11940
11941 let javascript_language = Arc::new(Language::new(
11942 LanguageConfig {
11943 name: "JavaScript".into(),
11944 line_comments: vec!["// ".into()],
11945 ..Default::default()
11946 },
11947 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
11948 ));
11949
11950 cx.language_registry().add(html_language.clone());
11951 cx.language_registry().add(javascript_language.clone());
11952 cx.update_buffer(|buffer, cx| {
11953 buffer.set_language(Some(html_language), cx);
11954 });
11955
11956 // Toggle comments for empty selections
11957 cx.set_state(
11958 &r#"
11959 <p>A</p>ˇ
11960 <p>B</p>ˇ
11961 <p>C</p>ˇ
11962 "#
11963 .unindent(),
11964 );
11965 cx.update_editor(|editor, window, cx| {
11966 editor.toggle_comments(&ToggleComments::default(), window, cx)
11967 });
11968 cx.assert_editor_state(
11969 &r#"
11970 <!-- <p>A</p>ˇ -->
11971 <!-- <p>B</p>ˇ -->
11972 <!-- <p>C</p>ˇ -->
11973 "#
11974 .unindent(),
11975 );
11976 cx.update_editor(|editor, window, cx| {
11977 editor.toggle_comments(&ToggleComments::default(), window, cx)
11978 });
11979 cx.assert_editor_state(
11980 &r#"
11981 <p>A</p>ˇ
11982 <p>B</p>ˇ
11983 <p>C</p>ˇ
11984 "#
11985 .unindent(),
11986 );
11987
11988 // Toggle comments for mixture of empty and non-empty selections, where
11989 // multiple selections occupy a given line.
11990 cx.set_state(
11991 &r#"
11992 <p>A«</p>
11993 <p>ˇ»B</p>ˇ
11994 <p>C«</p>
11995 <p>ˇ»D</p>ˇ
11996 "#
11997 .unindent(),
11998 );
11999
12000 cx.update_editor(|editor, window, cx| {
12001 editor.toggle_comments(&ToggleComments::default(), window, cx)
12002 });
12003 cx.assert_editor_state(
12004 &r#"
12005 <!-- <p>A«</p>
12006 <p>ˇ»B</p>ˇ -->
12007 <!-- <p>C«</p>
12008 <p>ˇ»D</p>ˇ -->
12009 "#
12010 .unindent(),
12011 );
12012 cx.update_editor(|editor, window, cx| {
12013 editor.toggle_comments(&ToggleComments::default(), window, cx)
12014 });
12015 cx.assert_editor_state(
12016 &r#"
12017 <p>A«</p>
12018 <p>ˇ»B</p>ˇ
12019 <p>C«</p>
12020 <p>ˇ»D</p>ˇ
12021 "#
12022 .unindent(),
12023 );
12024
12025 // Toggle comments when different languages are active for different
12026 // selections.
12027 cx.set_state(
12028 &r#"
12029 ˇ<script>
12030 ˇvar x = new Y();
12031 ˇ</script>
12032 "#
12033 .unindent(),
12034 );
12035 cx.executor().run_until_parked();
12036 cx.update_editor(|editor, window, cx| {
12037 editor.toggle_comments(&ToggleComments::default(), window, cx)
12038 });
12039 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
12040 // Uncommenting and commenting from this position brings in even more wrong artifacts.
12041 cx.assert_editor_state(
12042 &r#"
12043 <!-- ˇ<script> -->
12044 // ˇvar x = new Y();
12045 <!-- ˇ</script> -->
12046 "#
12047 .unindent(),
12048 );
12049}
12050
12051#[gpui::test]
12052fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
12053 init_test(cx, |_| {});
12054
12055 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12056 let multibuffer = cx.new(|cx| {
12057 let mut multibuffer = MultiBuffer::new(ReadWrite);
12058 multibuffer.push_excerpts(
12059 buffer.clone(),
12060 [
12061 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
12062 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
12063 ],
12064 cx,
12065 );
12066 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
12067 multibuffer
12068 });
12069
12070 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12071 editor.update_in(cx, |editor, window, cx| {
12072 assert_eq!(editor.text(cx), "aaaa\nbbbb");
12073 editor.change_selections(None, window, cx, |s| {
12074 s.select_ranges([
12075 Point::new(0, 0)..Point::new(0, 0),
12076 Point::new(1, 0)..Point::new(1, 0),
12077 ])
12078 });
12079
12080 editor.handle_input("X", window, cx);
12081 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
12082 assert_eq!(
12083 editor.selections.ranges(cx),
12084 [
12085 Point::new(0, 1)..Point::new(0, 1),
12086 Point::new(1, 1)..Point::new(1, 1),
12087 ]
12088 );
12089
12090 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
12091 editor.change_selections(None, window, cx, |s| {
12092 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
12093 });
12094 editor.backspace(&Default::default(), window, cx);
12095 assert_eq!(editor.text(cx), "Xa\nbbb");
12096 assert_eq!(
12097 editor.selections.ranges(cx),
12098 [Point::new(1, 0)..Point::new(1, 0)]
12099 );
12100
12101 editor.change_selections(None, window, cx, |s| {
12102 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
12103 });
12104 editor.backspace(&Default::default(), window, cx);
12105 assert_eq!(editor.text(cx), "X\nbb");
12106 assert_eq!(
12107 editor.selections.ranges(cx),
12108 [Point::new(0, 1)..Point::new(0, 1)]
12109 );
12110 });
12111}
12112
12113#[gpui::test]
12114fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
12115 init_test(cx, |_| {});
12116
12117 let markers = vec![('[', ']').into(), ('(', ')').into()];
12118 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
12119 indoc! {"
12120 [aaaa
12121 (bbbb]
12122 cccc)",
12123 },
12124 markers.clone(),
12125 );
12126 let excerpt_ranges = markers.into_iter().map(|marker| {
12127 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
12128 ExcerptRange::new(context.clone())
12129 });
12130 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
12131 let multibuffer = cx.new(|cx| {
12132 let mut multibuffer = MultiBuffer::new(ReadWrite);
12133 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
12134 multibuffer
12135 });
12136
12137 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12138 editor.update_in(cx, |editor, window, cx| {
12139 let (expected_text, selection_ranges) = marked_text_ranges(
12140 indoc! {"
12141 aaaa
12142 bˇbbb
12143 bˇbbˇb
12144 cccc"
12145 },
12146 true,
12147 );
12148 assert_eq!(editor.text(cx), expected_text);
12149 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
12150
12151 editor.handle_input("X", window, cx);
12152
12153 let (expected_text, expected_selections) = marked_text_ranges(
12154 indoc! {"
12155 aaaa
12156 bXˇbbXb
12157 bXˇbbXˇb
12158 cccc"
12159 },
12160 false,
12161 );
12162 assert_eq!(editor.text(cx), expected_text);
12163 assert_eq!(editor.selections.ranges(cx), expected_selections);
12164
12165 editor.newline(&Newline, window, cx);
12166 let (expected_text, expected_selections) = marked_text_ranges(
12167 indoc! {"
12168 aaaa
12169 bX
12170 ˇbbX
12171 b
12172 bX
12173 ˇbbX
12174 ˇb
12175 cccc"
12176 },
12177 false,
12178 );
12179 assert_eq!(editor.text(cx), expected_text);
12180 assert_eq!(editor.selections.ranges(cx), expected_selections);
12181 });
12182}
12183
12184#[gpui::test]
12185fn test_refresh_selections(cx: &mut TestAppContext) {
12186 init_test(cx, |_| {});
12187
12188 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12189 let mut excerpt1_id = None;
12190 let multibuffer = cx.new(|cx| {
12191 let mut multibuffer = MultiBuffer::new(ReadWrite);
12192 excerpt1_id = multibuffer
12193 .push_excerpts(
12194 buffer.clone(),
12195 [
12196 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12197 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12198 ],
12199 cx,
12200 )
12201 .into_iter()
12202 .next();
12203 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12204 multibuffer
12205 });
12206
12207 let editor = cx.add_window(|window, cx| {
12208 let mut editor = build_editor(multibuffer.clone(), window, cx);
12209 let snapshot = editor.snapshot(window, cx);
12210 editor.change_selections(None, window, cx, |s| {
12211 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
12212 });
12213 editor.begin_selection(
12214 Point::new(2, 1).to_display_point(&snapshot),
12215 true,
12216 1,
12217 window,
12218 cx,
12219 );
12220 assert_eq!(
12221 editor.selections.ranges(cx),
12222 [
12223 Point::new(1, 3)..Point::new(1, 3),
12224 Point::new(2, 1)..Point::new(2, 1),
12225 ]
12226 );
12227 editor
12228 });
12229
12230 // Refreshing selections is a no-op when excerpts haven't changed.
12231 _ = editor.update(cx, |editor, window, cx| {
12232 editor.change_selections(None, window, cx, |s| s.refresh());
12233 assert_eq!(
12234 editor.selections.ranges(cx),
12235 [
12236 Point::new(1, 3)..Point::new(1, 3),
12237 Point::new(2, 1)..Point::new(2, 1),
12238 ]
12239 );
12240 });
12241
12242 multibuffer.update(cx, |multibuffer, cx| {
12243 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12244 });
12245 _ = editor.update(cx, |editor, window, cx| {
12246 // Removing an excerpt causes the first selection to become degenerate.
12247 assert_eq!(
12248 editor.selections.ranges(cx),
12249 [
12250 Point::new(0, 0)..Point::new(0, 0),
12251 Point::new(0, 1)..Point::new(0, 1)
12252 ]
12253 );
12254
12255 // Refreshing selections will relocate the first selection to the original buffer
12256 // location.
12257 editor.change_selections(None, window, cx, |s| s.refresh());
12258 assert_eq!(
12259 editor.selections.ranges(cx),
12260 [
12261 Point::new(0, 1)..Point::new(0, 1),
12262 Point::new(0, 3)..Point::new(0, 3)
12263 ]
12264 );
12265 assert!(editor.selections.pending_anchor().is_some());
12266 });
12267}
12268
12269#[gpui::test]
12270fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
12271 init_test(cx, |_| {});
12272
12273 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12274 let mut excerpt1_id = None;
12275 let multibuffer = cx.new(|cx| {
12276 let mut multibuffer = MultiBuffer::new(ReadWrite);
12277 excerpt1_id = multibuffer
12278 .push_excerpts(
12279 buffer.clone(),
12280 [
12281 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12282 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12283 ],
12284 cx,
12285 )
12286 .into_iter()
12287 .next();
12288 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12289 multibuffer
12290 });
12291
12292 let editor = cx.add_window(|window, cx| {
12293 let mut editor = build_editor(multibuffer.clone(), window, cx);
12294 let snapshot = editor.snapshot(window, cx);
12295 editor.begin_selection(
12296 Point::new(1, 3).to_display_point(&snapshot),
12297 false,
12298 1,
12299 window,
12300 cx,
12301 );
12302 assert_eq!(
12303 editor.selections.ranges(cx),
12304 [Point::new(1, 3)..Point::new(1, 3)]
12305 );
12306 editor
12307 });
12308
12309 multibuffer.update(cx, |multibuffer, cx| {
12310 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12311 });
12312 _ = editor.update(cx, |editor, window, cx| {
12313 assert_eq!(
12314 editor.selections.ranges(cx),
12315 [Point::new(0, 0)..Point::new(0, 0)]
12316 );
12317
12318 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
12319 editor.change_selections(None, window, cx, |s| s.refresh());
12320 assert_eq!(
12321 editor.selections.ranges(cx),
12322 [Point::new(0, 3)..Point::new(0, 3)]
12323 );
12324 assert!(editor.selections.pending_anchor().is_some());
12325 });
12326}
12327
12328#[gpui::test]
12329async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
12330 init_test(cx, |_| {});
12331
12332 let language = Arc::new(
12333 Language::new(
12334 LanguageConfig {
12335 brackets: BracketPairConfig {
12336 pairs: vec![
12337 BracketPair {
12338 start: "{".to_string(),
12339 end: "}".to_string(),
12340 close: true,
12341 surround: true,
12342 newline: true,
12343 },
12344 BracketPair {
12345 start: "/* ".to_string(),
12346 end: " */".to_string(),
12347 close: true,
12348 surround: true,
12349 newline: true,
12350 },
12351 ],
12352 ..Default::default()
12353 },
12354 ..Default::default()
12355 },
12356 Some(tree_sitter_rust::LANGUAGE.into()),
12357 )
12358 .with_indents_query("")
12359 .unwrap(),
12360 );
12361
12362 let text = concat!(
12363 "{ }\n", //
12364 " x\n", //
12365 " /* */\n", //
12366 "x\n", //
12367 "{{} }\n", //
12368 );
12369
12370 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
12371 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12372 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
12373 editor
12374 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
12375 .await;
12376
12377 editor.update_in(cx, |editor, window, cx| {
12378 editor.change_selections(None, window, cx, |s| {
12379 s.select_display_ranges([
12380 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
12381 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
12382 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
12383 ])
12384 });
12385 editor.newline(&Newline, window, cx);
12386
12387 assert_eq!(
12388 editor.buffer().read(cx).read(cx).text(),
12389 concat!(
12390 "{ \n", // Suppress rustfmt
12391 "\n", //
12392 "}\n", //
12393 " x\n", //
12394 " /* \n", //
12395 " \n", //
12396 " */\n", //
12397 "x\n", //
12398 "{{} \n", //
12399 "}\n", //
12400 )
12401 );
12402 });
12403}
12404
12405#[gpui::test]
12406fn test_highlighted_ranges(cx: &mut TestAppContext) {
12407 init_test(cx, |_| {});
12408
12409 let editor = cx.add_window(|window, cx| {
12410 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
12411 build_editor(buffer.clone(), window, cx)
12412 });
12413
12414 _ = editor.update(cx, |editor, window, cx| {
12415 struct Type1;
12416 struct Type2;
12417
12418 let buffer = editor.buffer.read(cx).snapshot(cx);
12419
12420 let anchor_range =
12421 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
12422
12423 editor.highlight_background::<Type1>(
12424 &[
12425 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
12426 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
12427 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
12428 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
12429 ],
12430 |_| Hsla::red(),
12431 cx,
12432 );
12433 editor.highlight_background::<Type2>(
12434 &[
12435 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
12436 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
12437 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
12438 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
12439 ],
12440 |_| Hsla::green(),
12441 cx,
12442 );
12443
12444 let snapshot = editor.snapshot(window, cx);
12445 let mut highlighted_ranges = editor.background_highlights_in_range(
12446 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
12447 &snapshot,
12448 cx.theme().colors(),
12449 );
12450 // Enforce a consistent ordering based on color without relying on the ordering of the
12451 // highlight's `TypeId` which is non-executor.
12452 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
12453 assert_eq!(
12454 highlighted_ranges,
12455 &[
12456 (
12457 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
12458 Hsla::red(),
12459 ),
12460 (
12461 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
12462 Hsla::red(),
12463 ),
12464 (
12465 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
12466 Hsla::green(),
12467 ),
12468 (
12469 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
12470 Hsla::green(),
12471 ),
12472 ]
12473 );
12474 assert_eq!(
12475 editor.background_highlights_in_range(
12476 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
12477 &snapshot,
12478 cx.theme().colors(),
12479 ),
12480 &[(
12481 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
12482 Hsla::red(),
12483 )]
12484 );
12485 });
12486}
12487
12488#[gpui::test]
12489async fn test_following(cx: &mut TestAppContext) {
12490 init_test(cx, |_| {});
12491
12492 let fs = FakeFs::new(cx.executor());
12493 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
12494
12495 let buffer = project.update(cx, |project, cx| {
12496 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
12497 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
12498 });
12499 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
12500 let follower = cx.update(|cx| {
12501 cx.open_window(
12502 WindowOptions {
12503 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
12504 gpui::Point::new(px(0.), px(0.)),
12505 gpui::Point::new(px(10.), px(80.)),
12506 ))),
12507 ..Default::default()
12508 },
12509 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
12510 )
12511 .unwrap()
12512 });
12513
12514 let is_still_following = Rc::new(RefCell::new(true));
12515 let follower_edit_event_count = Rc::new(RefCell::new(0));
12516 let pending_update = Rc::new(RefCell::new(None));
12517 let leader_entity = leader.root(cx).unwrap();
12518 let follower_entity = follower.root(cx).unwrap();
12519 _ = follower.update(cx, {
12520 let update = pending_update.clone();
12521 let is_still_following = is_still_following.clone();
12522 let follower_edit_event_count = follower_edit_event_count.clone();
12523 |_, window, cx| {
12524 cx.subscribe_in(
12525 &leader_entity,
12526 window,
12527 move |_, leader, event, window, cx| {
12528 leader.read(cx).add_event_to_update_proto(
12529 event,
12530 &mut update.borrow_mut(),
12531 window,
12532 cx,
12533 );
12534 },
12535 )
12536 .detach();
12537
12538 cx.subscribe_in(
12539 &follower_entity,
12540 window,
12541 move |_, _, event: &EditorEvent, _window, _cx| {
12542 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
12543 *is_still_following.borrow_mut() = false;
12544 }
12545
12546 if let EditorEvent::BufferEdited = event {
12547 *follower_edit_event_count.borrow_mut() += 1;
12548 }
12549 },
12550 )
12551 .detach();
12552 }
12553 });
12554
12555 // Update the selections only
12556 _ = leader.update(cx, |leader, window, cx| {
12557 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
12558 });
12559 follower
12560 .update(cx, |follower, window, cx| {
12561 follower.apply_update_proto(
12562 &project,
12563 pending_update.borrow_mut().take().unwrap(),
12564 window,
12565 cx,
12566 )
12567 })
12568 .unwrap()
12569 .await
12570 .unwrap();
12571 _ = follower.update(cx, |follower, _, cx| {
12572 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
12573 });
12574 assert!(*is_still_following.borrow());
12575 assert_eq!(*follower_edit_event_count.borrow(), 0);
12576
12577 // Update the scroll position only
12578 _ = leader.update(cx, |leader, window, cx| {
12579 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
12580 });
12581 follower
12582 .update(cx, |follower, window, cx| {
12583 follower.apply_update_proto(
12584 &project,
12585 pending_update.borrow_mut().take().unwrap(),
12586 window,
12587 cx,
12588 )
12589 })
12590 .unwrap()
12591 .await
12592 .unwrap();
12593 assert_eq!(
12594 follower
12595 .update(cx, |follower, _, cx| follower.scroll_position(cx))
12596 .unwrap(),
12597 gpui::Point::new(1.5, 3.5)
12598 );
12599 assert!(*is_still_following.borrow());
12600 assert_eq!(*follower_edit_event_count.borrow(), 0);
12601
12602 // Update the selections and scroll position. The follower's scroll position is updated
12603 // via autoscroll, not via the leader's exact scroll position.
12604 _ = leader.update(cx, |leader, window, cx| {
12605 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
12606 leader.request_autoscroll(Autoscroll::newest(), cx);
12607 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
12608 });
12609 follower
12610 .update(cx, |follower, window, cx| {
12611 follower.apply_update_proto(
12612 &project,
12613 pending_update.borrow_mut().take().unwrap(),
12614 window,
12615 cx,
12616 )
12617 })
12618 .unwrap()
12619 .await
12620 .unwrap();
12621 _ = follower.update(cx, |follower, _, cx| {
12622 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
12623 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
12624 });
12625 assert!(*is_still_following.borrow());
12626
12627 // Creating a pending selection that precedes another selection
12628 _ = leader.update(cx, |leader, window, cx| {
12629 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
12630 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
12631 });
12632 follower
12633 .update(cx, |follower, window, cx| {
12634 follower.apply_update_proto(
12635 &project,
12636 pending_update.borrow_mut().take().unwrap(),
12637 window,
12638 cx,
12639 )
12640 })
12641 .unwrap()
12642 .await
12643 .unwrap();
12644 _ = follower.update(cx, |follower, _, cx| {
12645 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
12646 });
12647 assert!(*is_still_following.borrow());
12648
12649 // Extend the pending selection so that it surrounds another selection
12650 _ = leader.update(cx, |leader, window, cx| {
12651 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
12652 });
12653 follower
12654 .update(cx, |follower, window, cx| {
12655 follower.apply_update_proto(
12656 &project,
12657 pending_update.borrow_mut().take().unwrap(),
12658 window,
12659 cx,
12660 )
12661 })
12662 .unwrap()
12663 .await
12664 .unwrap();
12665 _ = follower.update(cx, |follower, _, cx| {
12666 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
12667 });
12668
12669 // Scrolling locally breaks the follow
12670 _ = follower.update(cx, |follower, window, cx| {
12671 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
12672 follower.set_scroll_anchor(
12673 ScrollAnchor {
12674 anchor: top_anchor,
12675 offset: gpui::Point::new(0.0, 0.5),
12676 },
12677 window,
12678 cx,
12679 );
12680 });
12681 assert!(!(*is_still_following.borrow()));
12682}
12683
12684#[gpui::test]
12685async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
12686 init_test(cx, |_| {});
12687
12688 let fs = FakeFs::new(cx.executor());
12689 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
12690 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12691 let pane = workspace
12692 .update(cx, |workspace, _, _| workspace.active_pane().clone())
12693 .unwrap();
12694
12695 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12696
12697 let leader = pane.update_in(cx, |_, window, cx| {
12698 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
12699 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
12700 });
12701
12702 // Start following the editor when it has no excerpts.
12703 let mut state_message =
12704 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
12705 let workspace_entity = workspace.root(cx).unwrap();
12706 let follower_1 = cx
12707 .update_window(*workspace.deref(), |_, window, cx| {
12708 Editor::from_state_proto(
12709 workspace_entity,
12710 ViewId {
12711 creator: CollaboratorId::PeerId(PeerId::default()),
12712 id: 0,
12713 },
12714 &mut state_message,
12715 window,
12716 cx,
12717 )
12718 })
12719 .unwrap()
12720 .unwrap()
12721 .await
12722 .unwrap();
12723
12724 let update_message = Rc::new(RefCell::new(None));
12725 follower_1.update_in(cx, {
12726 let update = update_message.clone();
12727 |_, window, cx| {
12728 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
12729 leader.read(cx).add_event_to_update_proto(
12730 event,
12731 &mut update.borrow_mut(),
12732 window,
12733 cx,
12734 );
12735 })
12736 .detach();
12737 }
12738 });
12739
12740 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
12741 (
12742 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
12743 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
12744 )
12745 });
12746
12747 // Insert some excerpts.
12748 leader.update(cx, |leader, cx| {
12749 leader.buffer.update(cx, |multibuffer, cx| {
12750 multibuffer.set_excerpts_for_path(
12751 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
12752 buffer_1.clone(),
12753 vec![
12754 Point::row_range(0..3),
12755 Point::row_range(1..6),
12756 Point::row_range(12..15),
12757 ],
12758 0,
12759 cx,
12760 );
12761 multibuffer.set_excerpts_for_path(
12762 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
12763 buffer_2.clone(),
12764 vec![Point::row_range(0..6), Point::row_range(8..12)],
12765 0,
12766 cx,
12767 );
12768 });
12769 });
12770
12771 // Apply the update of adding the excerpts.
12772 follower_1
12773 .update_in(cx, |follower, window, cx| {
12774 follower.apply_update_proto(
12775 &project,
12776 update_message.borrow().clone().unwrap(),
12777 window,
12778 cx,
12779 )
12780 })
12781 .await
12782 .unwrap();
12783 assert_eq!(
12784 follower_1.update(cx, |editor, cx| editor.text(cx)),
12785 leader.update(cx, |editor, cx| editor.text(cx))
12786 );
12787 update_message.borrow_mut().take();
12788
12789 // Start following separately after it already has excerpts.
12790 let mut state_message =
12791 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
12792 let workspace_entity = workspace.root(cx).unwrap();
12793 let follower_2 = cx
12794 .update_window(*workspace.deref(), |_, window, cx| {
12795 Editor::from_state_proto(
12796 workspace_entity,
12797 ViewId {
12798 creator: CollaboratorId::PeerId(PeerId::default()),
12799 id: 0,
12800 },
12801 &mut state_message,
12802 window,
12803 cx,
12804 )
12805 })
12806 .unwrap()
12807 .unwrap()
12808 .await
12809 .unwrap();
12810 assert_eq!(
12811 follower_2.update(cx, |editor, cx| editor.text(cx)),
12812 leader.update(cx, |editor, cx| editor.text(cx))
12813 );
12814
12815 // Remove some excerpts.
12816 leader.update(cx, |leader, cx| {
12817 leader.buffer.update(cx, |multibuffer, cx| {
12818 let excerpt_ids = multibuffer.excerpt_ids();
12819 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
12820 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
12821 });
12822 });
12823
12824 // Apply the update of removing the excerpts.
12825 follower_1
12826 .update_in(cx, |follower, window, cx| {
12827 follower.apply_update_proto(
12828 &project,
12829 update_message.borrow().clone().unwrap(),
12830 window,
12831 cx,
12832 )
12833 })
12834 .await
12835 .unwrap();
12836 follower_2
12837 .update_in(cx, |follower, window, cx| {
12838 follower.apply_update_proto(
12839 &project,
12840 update_message.borrow().clone().unwrap(),
12841 window,
12842 cx,
12843 )
12844 })
12845 .await
12846 .unwrap();
12847 update_message.borrow_mut().take();
12848 assert_eq!(
12849 follower_1.update(cx, |editor, cx| editor.text(cx)),
12850 leader.update(cx, |editor, cx| editor.text(cx))
12851 );
12852}
12853
12854#[gpui::test]
12855async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
12856 init_test(cx, |_| {});
12857
12858 let mut cx = EditorTestContext::new(cx).await;
12859 let lsp_store =
12860 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
12861
12862 cx.set_state(indoc! {"
12863 ˇfn func(abc def: i32) -> u32 {
12864 }
12865 "});
12866
12867 cx.update(|_, cx| {
12868 lsp_store.update(cx, |lsp_store, cx| {
12869 lsp_store
12870 .update_diagnostics(
12871 LanguageServerId(0),
12872 lsp::PublishDiagnosticsParams {
12873 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
12874 version: None,
12875 diagnostics: vec![
12876 lsp::Diagnostic {
12877 range: lsp::Range::new(
12878 lsp::Position::new(0, 11),
12879 lsp::Position::new(0, 12),
12880 ),
12881 severity: Some(lsp::DiagnosticSeverity::ERROR),
12882 ..Default::default()
12883 },
12884 lsp::Diagnostic {
12885 range: lsp::Range::new(
12886 lsp::Position::new(0, 12),
12887 lsp::Position::new(0, 15),
12888 ),
12889 severity: Some(lsp::DiagnosticSeverity::ERROR),
12890 ..Default::default()
12891 },
12892 lsp::Diagnostic {
12893 range: lsp::Range::new(
12894 lsp::Position::new(0, 25),
12895 lsp::Position::new(0, 28),
12896 ),
12897 severity: Some(lsp::DiagnosticSeverity::ERROR),
12898 ..Default::default()
12899 },
12900 ],
12901 },
12902 &[],
12903 cx,
12904 )
12905 .unwrap()
12906 });
12907 });
12908
12909 executor.run_until_parked();
12910
12911 cx.update_editor(|editor, window, cx| {
12912 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12913 });
12914
12915 cx.assert_editor_state(indoc! {"
12916 fn func(abc def: i32) -> ˇu32 {
12917 }
12918 "});
12919
12920 cx.update_editor(|editor, window, cx| {
12921 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12922 });
12923
12924 cx.assert_editor_state(indoc! {"
12925 fn func(abc ˇdef: i32) -> u32 {
12926 }
12927 "});
12928
12929 cx.update_editor(|editor, window, cx| {
12930 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12931 });
12932
12933 cx.assert_editor_state(indoc! {"
12934 fn func(abcˇ def: i32) -> u32 {
12935 }
12936 "});
12937
12938 cx.update_editor(|editor, window, cx| {
12939 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12940 });
12941
12942 cx.assert_editor_state(indoc! {"
12943 fn func(abc def: i32) -> ˇu32 {
12944 }
12945 "});
12946}
12947
12948#[gpui::test]
12949async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
12950 init_test(cx, |_| {});
12951
12952 let mut cx = EditorTestContext::new(cx).await;
12953
12954 let diff_base = r#"
12955 use some::mod;
12956
12957 const A: u32 = 42;
12958
12959 fn main() {
12960 println!("hello");
12961
12962 println!("world");
12963 }
12964 "#
12965 .unindent();
12966
12967 // Edits are modified, removed, modified, added
12968 cx.set_state(
12969 &r#"
12970 use some::modified;
12971
12972 ˇ
12973 fn main() {
12974 println!("hello there");
12975
12976 println!("around the");
12977 println!("world");
12978 }
12979 "#
12980 .unindent(),
12981 );
12982
12983 cx.set_head_text(&diff_base);
12984 executor.run_until_parked();
12985
12986 cx.update_editor(|editor, window, cx| {
12987 //Wrap around the bottom of the buffer
12988 for _ in 0..3 {
12989 editor.go_to_next_hunk(&GoToHunk, window, cx);
12990 }
12991 });
12992
12993 cx.assert_editor_state(
12994 &r#"
12995 ˇuse some::modified;
12996
12997
12998 fn main() {
12999 println!("hello there");
13000
13001 println!("around the");
13002 println!("world");
13003 }
13004 "#
13005 .unindent(),
13006 );
13007
13008 cx.update_editor(|editor, window, cx| {
13009 //Wrap around the top of the buffer
13010 for _ in 0..2 {
13011 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13012 }
13013 });
13014
13015 cx.assert_editor_state(
13016 &r#"
13017 use some::modified;
13018
13019
13020 fn main() {
13021 ˇ println!("hello there");
13022
13023 println!("around the");
13024 println!("world");
13025 }
13026 "#
13027 .unindent(),
13028 );
13029
13030 cx.update_editor(|editor, window, cx| {
13031 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13032 });
13033
13034 cx.assert_editor_state(
13035 &r#"
13036 use some::modified;
13037
13038 ˇ
13039 fn main() {
13040 println!("hello there");
13041
13042 println!("around the");
13043 println!("world");
13044 }
13045 "#
13046 .unindent(),
13047 );
13048
13049 cx.update_editor(|editor, window, cx| {
13050 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13051 });
13052
13053 cx.assert_editor_state(
13054 &r#"
13055 ˇuse some::modified;
13056
13057
13058 fn main() {
13059 println!("hello there");
13060
13061 println!("around the");
13062 println!("world");
13063 }
13064 "#
13065 .unindent(),
13066 );
13067
13068 cx.update_editor(|editor, window, cx| {
13069 for _ in 0..2 {
13070 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13071 }
13072 });
13073
13074 cx.assert_editor_state(
13075 &r#"
13076 use some::modified;
13077
13078
13079 fn main() {
13080 ˇ println!("hello there");
13081
13082 println!("around the");
13083 println!("world");
13084 }
13085 "#
13086 .unindent(),
13087 );
13088
13089 cx.update_editor(|editor, window, cx| {
13090 editor.fold(&Fold, window, cx);
13091 });
13092
13093 cx.update_editor(|editor, window, cx| {
13094 editor.go_to_next_hunk(&GoToHunk, window, cx);
13095 });
13096
13097 cx.assert_editor_state(
13098 &r#"
13099 ˇuse some::modified;
13100
13101
13102 fn main() {
13103 println!("hello there");
13104
13105 println!("around the");
13106 println!("world");
13107 }
13108 "#
13109 .unindent(),
13110 );
13111}
13112
13113#[test]
13114fn test_split_words() {
13115 fn split(text: &str) -> Vec<&str> {
13116 split_words(text).collect()
13117 }
13118
13119 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
13120 assert_eq!(split("hello_world"), &["hello_", "world"]);
13121 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
13122 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
13123 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
13124 assert_eq!(split("helloworld"), &["helloworld"]);
13125
13126 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
13127}
13128
13129#[gpui::test]
13130async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
13131 init_test(cx, |_| {});
13132
13133 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
13134 let mut assert = |before, after| {
13135 let _state_context = cx.set_state(before);
13136 cx.run_until_parked();
13137 cx.update_editor(|editor, window, cx| {
13138 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
13139 });
13140 cx.run_until_parked();
13141 cx.assert_editor_state(after);
13142 };
13143
13144 // Outside bracket jumps to outside of matching bracket
13145 assert("console.logˇ(var);", "console.log(var)ˇ;");
13146 assert("console.log(var)ˇ;", "console.logˇ(var);");
13147
13148 // Inside bracket jumps to inside of matching bracket
13149 assert("console.log(ˇvar);", "console.log(varˇ);");
13150 assert("console.log(varˇ);", "console.log(ˇvar);");
13151
13152 // When outside a bracket and inside, favor jumping to the inside bracket
13153 assert(
13154 "console.log('foo', [1, 2, 3]ˇ);",
13155 "console.log(ˇ'foo', [1, 2, 3]);",
13156 );
13157 assert(
13158 "console.log(ˇ'foo', [1, 2, 3]);",
13159 "console.log('foo', [1, 2, 3]ˇ);",
13160 );
13161
13162 // Bias forward if two options are equally likely
13163 assert(
13164 "let result = curried_fun()ˇ();",
13165 "let result = curried_fun()()ˇ;",
13166 );
13167
13168 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
13169 assert(
13170 indoc! {"
13171 function test() {
13172 console.log('test')ˇ
13173 }"},
13174 indoc! {"
13175 function test() {
13176 console.logˇ('test')
13177 }"},
13178 );
13179}
13180
13181#[gpui::test]
13182async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
13183 init_test(cx, |_| {});
13184
13185 let fs = FakeFs::new(cx.executor());
13186 fs.insert_tree(
13187 path!("/a"),
13188 json!({
13189 "main.rs": "fn main() { let a = 5; }",
13190 "other.rs": "// Test file",
13191 }),
13192 )
13193 .await;
13194 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13195
13196 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13197 language_registry.add(Arc::new(Language::new(
13198 LanguageConfig {
13199 name: "Rust".into(),
13200 matcher: LanguageMatcher {
13201 path_suffixes: vec!["rs".to_string()],
13202 ..Default::default()
13203 },
13204 brackets: BracketPairConfig {
13205 pairs: vec![BracketPair {
13206 start: "{".to_string(),
13207 end: "}".to_string(),
13208 close: true,
13209 surround: true,
13210 newline: true,
13211 }],
13212 disabled_scopes_by_bracket_ix: Vec::new(),
13213 },
13214 ..Default::default()
13215 },
13216 Some(tree_sitter_rust::LANGUAGE.into()),
13217 )));
13218 let mut fake_servers = language_registry.register_fake_lsp(
13219 "Rust",
13220 FakeLspAdapter {
13221 capabilities: lsp::ServerCapabilities {
13222 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
13223 first_trigger_character: "{".to_string(),
13224 more_trigger_character: None,
13225 }),
13226 ..Default::default()
13227 },
13228 ..Default::default()
13229 },
13230 );
13231
13232 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13233
13234 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13235
13236 let worktree_id = workspace
13237 .update(cx, |workspace, _, cx| {
13238 workspace.project().update(cx, |project, cx| {
13239 project.worktrees(cx).next().unwrap().read(cx).id()
13240 })
13241 })
13242 .unwrap();
13243
13244 let buffer = project
13245 .update(cx, |project, cx| {
13246 project.open_local_buffer(path!("/a/main.rs"), cx)
13247 })
13248 .await
13249 .unwrap();
13250 let editor_handle = workspace
13251 .update(cx, |workspace, window, cx| {
13252 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
13253 })
13254 .unwrap()
13255 .await
13256 .unwrap()
13257 .downcast::<Editor>()
13258 .unwrap();
13259
13260 cx.executor().start_waiting();
13261 let fake_server = fake_servers.next().await.unwrap();
13262
13263 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
13264 |params, _| async move {
13265 assert_eq!(
13266 params.text_document_position.text_document.uri,
13267 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
13268 );
13269 assert_eq!(
13270 params.text_document_position.position,
13271 lsp::Position::new(0, 21),
13272 );
13273
13274 Ok(Some(vec![lsp::TextEdit {
13275 new_text: "]".to_string(),
13276 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13277 }]))
13278 },
13279 );
13280
13281 editor_handle.update_in(cx, |editor, window, cx| {
13282 window.focus(&editor.focus_handle(cx));
13283 editor.change_selections(None, window, cx, |s| {
13284 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
13285 });
13286 editor.handle_input("{", window, cx);
13287 });
13288
13289 cx.executor().run_until_parked();
13290
13291 buffer.update(cx, |buffer, _| {
13292 assert_eq!(
13293 buffer.text(),
13294 "fn main() { let a = {5}; }",
13295 "No extra braces from on type formatting should appear in the buffer"
13296 )
13297 });
13298}
13299
13300#[gpui::test]
13301async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
13302 init_test(cx, |_| {});
13303
13304 let fs = FakeFs::new(cx.executor());
13305 fs.insert_tree(
13306 path!("/a"),
13307 json!({
13308 "main.rs": "fn main() { let a = 5; }",
13309 "other.rs": "// Test file",
13310 }),
13311 )
13312 .await;
13313
13314 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13315
13316 let server_restarts = Arc::new(AtomicUsize::new(0));
13317 let closure_restarts = Arc::clone(&server_restarts);
13318 let language_server_name = "test language server";
13319 let language_name: LanguageName = "Rust".into();
13320
13321 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13322 language_registry.add(Arc::new(Language::new(
13323 LanguageConfig {
13324 name: language_name.clone(),
13325 matcher: LanguageMatcher {
13326 path_suffixes: vec!["rs".to_string()],
13327 ..Default::default()
13328 },
13329 ..Default::default()
13330 },
13331 Some(tree_sitter_rust::LANGUAGE.into()),
13332 )));
13333 let mut fake_servers = language_registry.register_fake_lsp(
13334 "Rust",
13335 FakeLspAdapter {
13336 name: language_server_name,
13337 initialization_options: Some(json!({
13338 "testOptionValue": true
13339 })),
13340 initializer: Some(Box::new(move |fake_server| {
13341 let task_restarts = Arc::clone(&closure_restarts);
13342 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
13343 task_restarts.fetch_add(1, atomic::Ordering::Release);
13344 futures::future::ready(Ok(()))
13345 });
13346 })),
13347 ..Default::default()
13348 },
13349 );
13350
13351 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13352 let _buffer = project
13353 .update(cx, |project, cx| {
13354 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
13355 })
13356 .await
13357 .unwrap();
13358 let _fake_server = fake_servers.next().await.unwrap();
13359 update_test_language_settings(cx, |language_settings| {
13360 language_settings.languages.insert(
13361 language_name.clone(),
13362 LanguageSettingsContent {
13363 tab_size: NonZeroU32::new(8),
13364 ..Default::default()
13365 },
13366 );
13367 });
13368 cx.executor().run_until_parked();
13369 assert_eq!(
13370 server_restarts.load(atomic::Ordering::Acquire),
13371 0,
13372 "Should not restart LSP server on an unrelated change"
13373 );
13374
13375 update_test_project_settings(cx, |project_settings| {
13376 project_settings.lsp.insert(
13377 "Some other server name".into(),
13378 LspSettings {
13379 binary: None,
13380 settings: None,
13381 initialization_options: Some(json!({
13382 "some other init value": false
13383 })),
13384 enable_lsp_tasks: false,
13385 },
13386 );
13387 });
13388 cx.executor().run_until_parked();
13389 assert_eq!(
13390 server_restarts.load(atomic::Ordering::Acquire),
13391 0,
13392 "Should not restart LSP server on an unrelated LSP settings change"
13393 );
13394
13395 update_test_project_settings(cx, |project_settings| {
13396 project_settings.lsp.insert(
13397 language_server_name.into(),
13398 LspSettings {
13399 binary: None,
13400 settings: None,
13401 initialization_options: Some(json!({
13402 "anotherInitValue": false
13403 })),
13404 enable_lsp_tasks: false,
13405 },
13406 );
13407 });
13408 cx.executor().run_until_parked();
13409 assert_eq!(
13410 server_restarts.load(atomic::Ordering::Acquire),
13411 1,
13412 "Should restart LSP server on a related LSP settings change"
13413 );
13414
13415 update_test_project_settings(cx, |project_settings| {
13416 project_settings.lsp.insert(
13417 language_server_name.into(),
13418 LspSettings {
13419 binary: None,
13420 settings: None,
13421 initialization_options: Some(json!({
13422 "anotherInitValue": false
13423 })),
13424 enable_lsp_tasks: false,
13425 },
13426 );
13427 });
13428 cx.executor().run_until_parked();
13429 assert_eq!(
13430 server_restarts.load(atomic::Ordering::Acquire),
13431 1,
13432 "Should not restart LSP server on a related LSP settings change that is the same"
13433 );
13434
13435 update_test_project_settings(cx, |project_settings| {
13436 project_settings.lsp.insert(
13437 language_server_name.into(),
13438 LspSettings {
13439 binary: None,
13440 settings: None,
13441 initialization_options: None,
13442 enable_lsp_tasks: false,
13443 },
13444 );
13445 });
13446 cx.executor().run_until_parked();
13447 assert_eq!(
13448 server_restarts.load(atomic::Ordering::Acquire),
13449 2,
13450 "Should restart LSP server on another related LSP settings change"
13451 );
13452}
13453
13454#[gpui::test]
13455async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
13456 init_test(cx, |_| {});
13457
13458 let mut cx = EditorLspTestContext::new_rust(
13459 lsp::ServerCapabilities {
13460 completion_provider: Some(lsp::CompletionOptions {
13461 trigger_characters: Some(vec![".".to_string()]),
13462 resolve_provider: Some(true),
13463 ..Default::default()
13464 }),
13465 ..Default::default()
13466 },
13467 cx,
13468 )
13469 .await;
13470
13471 cx.set_state("fn main() { let a = 2ˇ; }");
13472 cx.simulate_keystroke(".");
13473 let completion_item = lsp::CompletionItem {
13474 label: "some".into(),
13475 kind: Some(lsp::CompletionItemKind::SNIPPET),
13476 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13477 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13478 kind: lsp::MarkupKind::Markdown,
13479 value: "```rust\nSome(2)\n```".to_string(),
13480 })),
13481 deprecated: Some(false),
13482 sort_text: Some("fffffff2".to_string()),
13483 filter_text: Some("some".to_string()),
13484 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13485 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13486 range: lsp::Range {
13487 start: lsp::Position {
13488 line: 0,
13489 character: 22,
13490 },
13491 end: lsp::Position {
13492 line: 0,
13493 character: 22,
13494 },
13495 },
13496 new_text: "Some(2)".to_string(),
13497 })),
13498 additional_text_edits: Some(vec![lsp::TextEdit {
13499 range: lsp::Range {
13500 start: lsp::Position {
13501 line: 0,
13502 character: 20,
13503 },
13504 end: lsp::Position {
13505 line: 0,
13506 character: 22,
13507 },
13508 },
13509 new_text: "".to_string(),
13510 }]),
13511 ..Default::default()
13512 };
13513
13514 let closure_completion_item = completion_item.clone();
13515 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13516 let task_completion_item = closure_completion_item.clone();
13517 async move {
13518 Ok(Some(lsp::CompletionResponse::Array(vec![
13519 task_completion_item,
13520 ])))
13521 }
13522 });
13523
13524 request.next().await;
13525
13526 cx.condition(|editor, _| editor.context_menu_visible())
13527 .await;
13528 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13529 editor
13530 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13531 .unwrap()
13532 });
13533 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
13534
13535 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
13536 let task_completion_item = completion_item.clone();
13537 async move { Ok(task_completion_item) }
13538 })
13539 .next()
13540 .await
13541 .unwrap();
13542 apply_additional_edits.await.unwrap();
13543 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
13544}
13545
13546#[gpui::test]
13547async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
13548 init_test(cx, |_| {});
13549
13550 let mut cx = EditorLspTestContext::new_rust(
13551 lsp::ServerCapabilities {
13552 completion_provider: Some(lsp::CompletionOptions {
13553 trigger_characters: Some(vec![".".to_string()]),
13554 resolve_provider: Some(true),
13555 ..Default::default()
13556 }),
13557 ..Default::default()
13558 },
13559 cx,
13560 )
13561 .await;
13562
13563 cx.set_state("fn main() { let a = 2ˇ; }");
13564 cx.simulate_keystroke(".");
13565
13566 let item1 = lsp::CompletionItem {
13567 label: "method id()".to_string(),
13568 filter_text: Some("id".to_string()),
13569 detail: None,
13570 documentation: None,
13571 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13572 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13573 new_text: ".id".to_string(),
13574 })),
13575 ..lsp::CompletionItem::default()
13576 };
13577
13578 let item2 = lsp::CompletionItem {
13579 label: "other".to_string(),
13580 filter_text: Some("other".to_string()),
13581 detail: None,
13582 documentation: None,
13583 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13584 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13585 new_text: ".other".to_string(),
13586 })),
13587 ..lsp::CompletionItem::default()
13588 };
13589
13590 let item1 = item1.clone();
13591 cx.set_request_handler::<lsp::request::Completion, _, _>({
13592 let item1 = item1.clone();
13593 move |_, _, _| {
13594 let item1 = item1.clone();
13595 let item2 = item2.clone();
13596 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
13597 }
13598 })
13599 .next()
13600 .await;
13601
13602 cx.condition(|editor, _| editor.context_menu_visible())
13603 .await;
13604 cx.update_editor(|editor, _, _| {
13605 let context_menu = editor.context_menu.borrow_mut();
13606 let context_menu = context_menu
13607 .as_ref()
13608 .expect("Should have the context menu deployed");
13609 match context_menu {
13610 CodeContextMenu::Completions(completions_menu) => {
13611 let completions = completions_menu.completions.borrow_mut();
13612 assert_eq!(
13613 completions
13614 .iter()
13615 .map(|completion| &completion.label.text)
13616 .collect::<Vec<_>>(),
13617 vec!["method id()", "other"]
13618 )
13619 }
13620 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13621 }
13622 });
13623
13624 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
13625 let item1 = item1.clone();
13626 move |_, item_to_resolve, _| {
13627 let item1 = item1.clone();
13628 async move {
13629 if item1 == item_to_resolve {
13630 Ok(lsp::CompletionItem {
13631 label: "method id()".to_string(),
13632 filter_text: Some("id".to_string()),
13633 detail: Some("Now resolved!".to_string()),
13634 documentation: Some(lsp::Documentation::String("Docs".to_string())),
13635 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13636 range: lsp::Range::new(
13637 lsp::Position::new(0, 22),
13638 lsp::Position::new(0, 22),
13639 ),
13640 new_text: ".id".to_string(),
13641 })),
13642 ..lsp::CompletionItem::default()
13643 })
13644 } else {
13645 Ok(item_to_resolve)
13646 }
13647 }
13648 }
13649 })
13650 .next()
13651 .await
13652 .unwrap();
13653 cx.run_until_parked();
13654
13655 cx.update_editor(|editor, window, cx| {
13656 editor.context_menu_next(&Default::default(), window, cx);
13657 });
13658
13659 cx.update_editor(|editor, _, _| {
13660 let context_menu = editor.context_menu.borrow_mut();
13661 let context_menu = context_menu
13662 .as_ref()
13663 .expect("Should have the context menu deployed");
13664 match context_menu {
13665 CodeContextMenu::Completions(completions_menu) => {
13666 let completions = completions_menu.completions.borrow_mut();
13667 assert_eq!(
13668 completions
13669 .iter()
13670 .map(|completion| &completion.label.text)
13671 .collect::<Vec<_>>(),
13672 vec!["method id() Now resolved!", "other"],
13673 "Should update first completion label, but not second as the filter text did not match."
13674 );
13675 }
13676 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13677 }
13678 });
13679}
13680
13681#[gpui::test]
13682async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
13683 init_test(cx, |_| {});
13684
13685 let mut cx = EditorLspTestContext::new_rust(
13686 lsp::ServerCapabilities {
13687 completion_provider: Some(lsp::CompletionOptions {
13688 trigger_characters: Some(vec![".".to_string()]),
13689 resolve_provider: Some(true),
13690 ..Default::default()
13691 }),
13692 ..Default::default()
13693 },
13694 cx,
13695 )
13696 .await;
13697
13698 cx.set_state("fn main() { let a = 2ˇ; }");
13699 cx.simulate_keystroke(".");
13700
13701 let unresolved_item_1 = lsp::CompletionItem {
13702 label: "id".to_string(),
13703 filter_text: Some("id".to_string()),
13704 detail: None,
13705 documentation: None,
13706 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13707 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13708 new_text: ".id".to_string(),
13709 })),
13710 ..lsp::CompletionItem::default()
13711 };
13712 let resolved_item_1 = lsp::CompletionItem {
13713 additional_text_edits: Some(vec![lsp::TextEdit {
13714 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
13715 new_text: "!!".to_string(),
13716 }]),
13717 ..unresolved_item_1.clone()
13718 };
13719 let unresolved_item_2 = lsp::CompletionItem {
13720 label: "other".to_string(),
13721 filter_text: Some("other".to_string()),
13722 detail: None,
13723 documentation: None,
13724 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13725 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13726 new_text: ".other".to_string(),
13727 })),
13728 ..lsp::CompletionItem::default()
13729 };
13730 let resolved_item_2 = lsp::CompletionItem {
13731 additional_text_edits: Some(vec![lsp::TextEdit {
13732 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
13733 new_text: "??".to_string(),
13734 }]),
13735 ..unresolved_item_2.clone()
13736 };
13737
13738 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
13739 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
13740 cx.lsp
13741 .server
13742 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
13743 let unresolved_item_1 = unresolved_item_1.clone();
13744 let resolved_item_1 = resolved_item_1.clone();
13745 let unresolved_item_2 = unresolved_item_2.clone();
13746 let resolved_item_2 = resolved_item_2.clone();
13747 let resolve_requests_1 = resolve_requests_1.clone();
13748 let resolve_requests_2 = resolve_requests_2.clone();
13749 move |unresolved_request, _| {
13750 let unresolved_item_1 = unresolved_item_1.clone();
13751 let resolved_item_1 = resolved_item_1.clone();
13752 let unresolved_item_2 = unresolved_item_2.clone();
13753 let resolved_item_2 = resolved_item_2.clone();
13754 let resolve_requests_1 = resolve_requests_1.clone();
13755 let resolve_requests_2 = resolve_requests_2.clone();
13756 async move {
13757 if unresolved_request == unresolved_item_1 {
13758 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
13759 Ok(resolved_item_1.clone())
13760 } else if unresolved_request == unresolved_item_2 {
13761 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
13762 Ok(resolved_item_2.clone())
13763 } else {
13764 panic!("Unexpected completion item {unresolved_request:?}")
13765 }
13766 }
13767 }
13768 })
13769 .detach();
13770
13771 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13772 let unresolved_item_1 = unresolved_item_1.clone();
13773 let unresolved_item_2 = unresolved_item_2.clone();
13774 async move {
13775 Ok(Some(lsp::CompletionResponse::Array(vec![
13776 unresolved_item_1,
13777 unresolved_item_2,
13778 ])))
13779 }
13780 })
13781 .next()
13782 .await;
13783
13784 cx.condition(|editor, _| editor.context_menu_visible())
13785 .await;
13786 cx.update_editor(|editor, _, _| {
13787 let context_menu = editor.context_menu.borrow_mut();
13788 let context_menu = context_menu
13789 .as_ref()
13790 .expect("Should have the context menu deployed");
13791 match context_menu {
13792 CodeContextMenu::Completions(completions_menu) => {
13793 let completions = completions_menu.completions.borrow_mut();
13794 assert_eq!(
13795 completions
13796 .iter()
13797 .map(|completion| &completion.label.text)
13798 .collect::<Vec<_>>(),
13799 vec!["id", "other"]
13800 )
13801 }
13802 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13803 }
13804 });
13805 cx.run_until_parked();
13806
13807 cx.update_editor(|editor, window, cx| {
13808 editor.context_menu_next(&ContextMenuNext, window, cx);
13809 });
13810 cx.run_until_parked();
13811 cx.update_editor(|editor, window, cx| {
13812 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
13813 });
13814 cx.run_until_parked();
13815 cx.update_editor(|editor, window, cx| {
13816 editor.context_menu_next(&ContextMenuNext, window, cx);
13817 });
13818 cx.run_until_parked();
13819 cx.update_editor(|editor, window, cx| {
13820 editor
13821 .compose_completion(&ComposeCompletion::default(), window, cx)
13822 .expect("No task returned")
13823 })
13824 .await
13825 .expect("Completion failed");
13826 cx.run_until_parked();
13827
13828 cx.update_editor(|editor, _, cx| {
13829 assert_eq!(
13830 resolve_requests_1.load(atomic::Ordering::Acquire),
13831 1,
13832 "Should always resolve once despite multiple selections"
13833 );
13834 assert_eq!(
13835 resolve_requests_2.load(atomic::Ordering::Acquire),
13836 1,
13837 "Should always resolve once after multiple selections and applying the completion"
13838 );
13839 assert_eq!(
13840 editor.text(cx),
13841 "fn main() { let a = ??.other; }",
13842 "Should use resolved data when applying the completion"
13843 );
13844 });
13845}
13846
13847#[gpui::test]
13848async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
13849 init_test(cx, |_| {});
13850
13851 let item_0 = lsp::CompletionItem {
13852 label: "abs".into(),
13853 insert_text: Some("abs".into()),
13854 data: Some(json!({ "very": "special"})),
13855 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
13856 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
13857 lsp::InsertReplaceEdit {
13858 new_text: "abs".to_string(),
13859 insert: lsp::Range::default(),
13860 replace: lsp::Range::default(),
13861 },
13862 )),
13863 ..lsp::CompletionItem::default()
13864 };
13865 let items = iter::once(item_0.clone())
13866 .chain((11..51).map(|i| lsp::CompletionItem {
13867 label: format!("item_{}", i),
13868 insert_text: Some(format!("item_{}", i)),
13869 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
13870 ..lsp::CompletionItem::default()
13871 }))
13872 .collect::<Vec<_>>();
13873
13874 let default_commit_characters = vec!["?".to_string()];
13875 let default_data = json!({ "default": "data"});
13876 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
13877 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
13878 let default_edit_range = lsp::Range {
13879 start: lsp::Position {
13880 line: 0,
13881 character: 5,
13882 },
13883 end: lsp::Position {
13884 line: 0,
13885 character: 5,
13886 },
13887 };
13888
13889 let mut cx = EditorLspTestContext::new_rust(
13890 lsp::ServerCapabilities {
13891 completion_provider: Some(lsp::CompletionOptions {
13892 trigger_characters: Some(vec![".".to_string()]),
13893 resolve_provider: Some(true),
13894 ..Default::default()
13895 }),
13896 ..Default::default()
13897 },
13898 cx,
13899 )
13900 .await;
13901
13902 cx.set_state("fn main() { let a = 2ˇ; }");
13903 cx.simulate_keystroke(".");
13904
13905 let completion_data = default_data.clone();
13906 let completion_characters = default_commit_characters.clone();
13907 let completion_items = items.clone();
13908 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13909 let default_data = completion_data.clone();
13910 let default_commit_characters = completion_characters.clone();
13911 let items = completion_items.clone();
13912 async move {
13913 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13914 items,
13915 item_defaults: Some(lsp::CompletionListItemDefaults {
13916 data: Some(default_data.clone()),
13917 commit_characters: Some(default_commit_characters.clone()),
13918 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
13919 default_edit_range,
13920 )),
13921 insert_text_format: Some(default_insert_text_format),
13922 insert_text_mode: Some(default_insert_text_mode),
13923 }),
13924 ..lsp::CompletionList::default()
13925 })))
13926 }
13927 })
13928 .next()
13929 .await;
13930
13931 let resolved_items = Arc::new(Mutex::new(Vec::new()));
13932 cx.lsp
13933 .server
13934 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
13935 let closure_resolved_items = resolved_items.clone();
13936 move |item_to_resolve, _| {
13937 let closure_resolved_items = closure_resolved_items.clone();
13938 async move {
13939 closure_resolved_items.lock().push(item_to_resolve.clone());
13940 Ok(item_to_resolve)
13941 }
13942 }
13943 })
13944 .detach();
13945
13946 cx.condition(|editor, _| editor.context_menu_visible())
13947 .await;
13948 cx.run_until_parked();
13949 cx.update_editor(|editor, _, _| {
13950 let menu = editor.context_menu.borrow_mut();
13951 match menu.as_ref().expect("should have the completions menu") {
13952 CodeContextMenu::Completions(completions_menu) => {
13953 assert_eq!(
13954 completions_menu
13955 .entries
13956 .borrow()
13957 .iter()
13958 .map(|mat| mat.string.clone())
13959 .collect::<Vec<String>>(),
13960 items
13961 .iter()
13962 .map(|completion| completion.label.clone())
13963 .collect::<Vec<String>>()
13964 );
13965 }
13966 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
13967 }
13968 });
13969 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
13970 // with 4 from the end.
13971 assert_eq!(
13972 *resolved_items.lock(),
13973 [&items[0..16], &items[items.len() - 4..items.len()]]
13974 .concat()
13975 .iter()
13976 .cloned()
13977 .map(|mut item| {
13978 if item.data.is_none() {
13979 item.data = Some(default_data.clone());
13980 }
13981 item
13982 })
13983 .collect::<Vec<lsp::CompletionItem>>(),
13984 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
13985 );
13986 resolved_items.lock().clear();
13987
13988 cx.update_editor(|editor, window, cx| {
13989 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
13990 });
13991 cx.run_until_parked();
13992 // Completions that have already been resolved are skipped.
13993 assert_eq!(
13994 *resolved_items.lock(),
13995 items[items.len() - 16..items.len() - 4]
13996 .iter()
13997 .cloned()
13998 .map(|mut item| {
13999 if item.data.is_none() {
14000 item.data = Some(default_data.clone());
14001 }
14002 item
14003 })
14004 .collect::<Vec<lsp::CompletionItem>>()
14005 );
14006 resolved_items.lock().clear();
14007}
14008
14009#[gpui::test]
14010async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
14011 init_test(cx, |_| {});
14012
14013 let mut cx = EditorLspTestContext::new(
14014 Language::new(
14015 LanguageConfig {
14016 matcher: LanguageMatcher {
14017 path_suffixes: vec!["jsx".into()],
14018 ..Default::default()
14019 },
14020 overrides: [(
14021 "element".into(),
14022 LanguageConfigOverride {
14023 completion_query_characters: Override::Set(['-'].into_iter().collect()),
14024 ..Default::default()
14025 },
14026 )]
14027 .into_iter()
14028 .collect(),
14029 ..Default::default()
14030 },
14031 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
14032 )
14033 .with_override_query("(jsx_self_closing_element) @element")
14034 .unwrap(),
14035 lsp::ServerCapabilities {
14036 completion_provider: Some(lsp::CompletionOptions {
14037 trigger_characters: Some(vec![":".to_string()]),
14038 ..Default::default()
14039 }),
14040 ..Default::default()
14041 },
14042 cx,
14043 )
14044 .await;
14045
14046 cx.lsp
14047 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14048 Ok(Some(lsp::CompletionResponse::Array(vec![
14049 lsp::CompletionItem {
14050 label: "bg-blue".into(),
14051 ..Default::default()
14052 },
14053 lsp::CompletionItem {
14054 label: "bg-red".into(),
14055 ..Default::default()
14056 },
14057 lsp::CompletionItem {
14058 label: "bg-yellow".into(),
14059 ..Default::default()
14060 },
14061 ])))
14062 });
14063
14064 cx.set_state(r#"<p class="bgˇ" />"#);
14065
14066 // Trigger completion when typing a dash, because the dash is an extra
14067 // word character in the 'element' scope, which contains the cursor.
14068 cx.simulate_keystroke("-");
14069 cx.executor().run_until_parked();
14070 cx.update_editor(|editor, _, _| {
14071 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14072 {
14073 assert_eq!(
14074 completion_menu_entries(&menu),
14075 &["bg-red", "bg-blue", "bg-yellow"]
14076 );
14077 } else {
14078 panic!("expected completion menu to be open");
14079 }
14080 });
14081
14082 cx.simulate_keystroke("l");
14083 cx.executor().run_until_parked();
14084 cx.update_editor(|editor, _, _| {
14085 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14086 {
14087 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
14088 } else {
14089 panic!("expected completion menu to be open");
14090 }
14091 });
14092
14093 // When filtering completions, consider the character after the '-' to
14094 // be the start of a subword.
14095 cx.set_state(r#"<p class="yelˇ" />"#);
14096 cx.simulate_keystroke("l");
14097 cx.executor().run_until_parked();
14098 cx.update_editor(|editor, _, _| {
14099 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14100 {
14101 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
14102 } else {
14103 panic!("expected completion menu to be open");
14104 }
14105 });
14106}
14107
14108fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
14109 let entries = menu.entries.borrow();
14110 entries.iter().map(|mat| mat.string.clone()).collect()
14111}
14112
14113#[gpui::test]
14114async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
14115 init_test(cx, |settings| {
14116 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
14117 FormatterList(vec![Formatter::Prettier].into()),
14118 ))
14119 });
14120
14121 let fs = FakeFs::new(cx.executor());
14122 fs.insert_file(path!("/file.ts"), Default::default()).await;
14123
14124 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
14125 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14126
14127 language_registry.add(Arc::new(Language::new(
14128 LanguageConfig {
14129 name: "TypeScript".into(),
14130 matcher: LanguageMatcher {
14131 path_suffixes: vec!["ts".to_string()],
14132 ..Default::default()
14133 },
14134 ..Default::default()
14135 },
14136 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14137 )));
14138 update_test_language_settings(cx, |settings| {
14139 settings.defaults.prettier = Some(PrettierSettings {
14140 allowed: true,
14141 ..PrettierSettings::default()
14142 });
14143 });
14144
14145 let test_plugin = "test_plugin";
14146 let _ = language_registry.register_fake_lsp(
14147 "TypeScript",
14148 FakeLspAdapter {
14149 prettier_plugins: vec![test_plugin],
14150 ..Default::default()
14151 },
14152 );
14153
14154 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
14155 let buffer = project
14156 .update(cx, |project, cx| {
14157 project.open_local_buffer(path!("/file.ts"), cx)
14158 })
14159 .await
14160 .unwrap();
14161
14162 let buffer_text = "one\ntwo\nthree\n";
14163 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14164 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14165 editor.update_in(cx, |editor, window, cx| {
14166 editor.set_text(buffer_text, window, cx)
14167 });
14168
14169 editor
14170 .update_in(cx, |editor, window, cx| {
14171 editor.perform_format(
14172 project.clone(),
14173 FormatTrigger::Manual,
14174 FormatTarget::Buffers,
14175 window,
14176 cx,
14177 )
14178 })
14179 .unwrap()
14180 .await;
14181 assert_eq!(
14182 editor.update(cx, |editor, cx| editor.text(cx)),
14183 buffer_text.to_string() + prettier_format_suffix,
14184 "Test prettier formatting was not applied to the original buffer text",
14185 );
14186
14187 update_test_language_settings(cx, |settings| {
14188 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
14189 });
14190 let format = editor.update_in(cx, |editor, window, cx| {
14191 editor.perform_format(
14192 project.clone(),
14193 FormatTrigger::Manual,
14194 FormatTarget::Buffers,
14195 window,
14196 cx,
14197 )
14198 });
14199 format.await.unwrap();
14200 assert_eq!(
14201 editor.update(cx, |editor, cx| editor.text(cx)),
14202 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
14203 "Autoformatting (via test prettier) was not applied to the original buffer text",
14204 );
14205}
14206
14207#[gpui::test]
14208async fn test_addition_reverts(cx: &mut TestAppContext) {
14209 init_test(cx, |_| {});
14210 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14211 let base_text = indoc! {r#"
14212 struct Row;
14213 struct Row1;
14214 struct Row2;
14215
14216 struct Row4;
14217 struct Row5;
14218 struct Row6;
14219
14220 struct Row8;
14221 struct Row9;
14222 struct Row10;"#};
14223
14224 // When addition hunks are not adjacent to carets, no hunk revert is performed
14225 assert_hunk_revert(
14226 indoc! {r#"struct Row;
14227 struct Row1;
14228 struct Row1.1;
14229 struct Row1.2;
14230 struct Row2;ˇ
14231
14232 struct Row4;
14233 struct Row5;
14234 struct Row6;
14235
14236 struct Row8;
14237 ˇstruct Row9;
14238 struct Row9.1;
14239 struct Row9.2;
14240 struct Row9.3;
14241 struct Row10;"#},
14242 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
14243 indoc! {r#"struct Row;
14244 struct Row1;
14245 struct Row1.1;
14246 struct Row1.2;
14247 struct Row2;ˇ
14248
14249 struct Row4;
14250 struct Row5;
14251 struct Row6;
14252
14253 struct Row8;
14254 ˇstruct Row9;
14255 struct Row9.1;
14256 struct Row9.2;
14257 struct Row9.3;
14258 struct Row10;"#},
14259 base_text,
14260 &mut cx,
14261 );
14262 // Same for selections
14263 assert_hunk_revert(
14264 indoc! {r#"struct Row;
14265 struct Row1;
14266 struct Row2;
14267 struct Row2.1;
14268 struct Row2.2;
14269 «ˇ
14270 struct Row4;
14271 struct» Row5;
14272 «struct Row6;
14273 ˇ»
14274 struct Row9.1;
14275 struct Row9.2;
14276 struct Row9.3;
14277 struct Row8;
14278 struct Row9;
14279 struct Row10;"#},
14280 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
14281 indoc! {r#"struct Row;
14282 struct Row1;
14283 struct Row2;
14284 struct Row2.1;
14285 struct Row2.2;
14286 «ˇ
14287 struct Row4;
14288 struct» Row5;
14289 «struct Row6;
14290 ˇ»
14291 struct Row9.1;
14292 struct Row9.2;
14293 struct Row9.3;
14294 struct Row8;
14295 struct Row9;
14296 struct Row10;"#},
14297 base_text,
14298 &mut cx,
14299 );
14300
14301 // When carets and selections intersect the addition hunks, those are reverted.
14302 // Adjacent carets got merged.
14303 assert_hunk_revert(
14304 indoc! {r#"struct Row;
14305 ˇ// something on the top
14306 struct Row1;
14307 struct Row2;
14308 struct Roˇw3.1;
14309 struct Row2.2;
14310 struct Row2.3;ˇ
14311
14312 struct Row4;
14313 struct ˇRow5.1;
14314 struct Row5.2;
14315 struct «Rowˇ»5.3;
14316 struct Row5;
14317 struct Row6;
14318 ˇ
14319 struct Row9.1;
14320 struct «Rowˇ»9.2;
14321 struct «ˇRow»9.3;
14322 struct Row8;
14323 struct Row9;
14324 «ˇ// something on bottom»
14325 struct Row10;"#},
14326 vec![
14327 DiffHunkStatusKind::Added,
14328 DiffHunkStatusKind::Added,
14329 DiffHunkStatusKind::Added,
14330 DiffHunkStatusKind::Added,
14331 DiffHunkStatusKind::Added,
14332 ],
14333 indoc! {r#"struct Row;
14334 ˇstruct Row1;
14335 struct Row2;
14336 ˇ
14337 struct Row4;
14338 ˇstruct Row5;
14339 struct Row6;
14340 ˇ
14341 ˇstruct Row8;
14342 struct Row9;
14343 ˇstruct Row10;"#},
14344 base_text,
14345 &mut cx,
14346 );
14347}
14348
14349#[gpui::test]
14350async fn test_modification_reverts(cx: &mut TestAppContext) {
14351 init_test(cx, |_| {});
14352 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14353 let base_text = indoc! {r#"
14354 struct Row;
14355 struct Row1;
14356 struct Row2;
14357
14358 struct Row4;
14359 struct Row5;
14360 struct Row6;
14361
14362 struct Row8;
14363 struct Row9;
14364 struct Row10;"#};
14365
14366 // Modification hunks behave the same as the addition ones.
14367 assert_hunk_revert(
14368 indoc! {r#"struct Row;
14369 struct Row1;
14370 struct Row33;
14371 ˇ
14372 struct Row4;
14373 struct Row5;
14374 struct Row6;
14375 ˇ
14376 struct Row99;
14377 struct Row9;
14378 struct Row10;"#},
14379 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
14380 indoc! {r#"struct Row;
14381 struct Row1;
14382 struct Row33;
14383 ˇ
14384 struct Row4;
14385 struct Row5;
14386 struct Row6;
14387 ˇ
14388 struct Row99;
14389 struct Row9;
14390 struct Row10;"#},
14391 base_text,
14392 &mut cx,
14393 );
14394 assert_hunk_revert(
14395 indoc! {r#"struct Row;
14396 struct Row1;
14397 struct Row33;
14398 «ˇ
14399 struct Row4;
14400 struct» Row5;
14401 «struct Row6;
14402 ˇ»
14403 struct Row99;
14404 struct Row9;
14405 struct Row10;"#},
14406 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
14407 indoc! {r#"struct Row;
14408 struct Row1;
14409 struct Row33;
14410 «ˇ
14411 struct Row4;
14412 struct» Row5;
14413 «struct Row6;
14414 ˇ»
14415 struct Row99;
14416 struct Row9;
14417 struct Row10;"#},
14418 base_text,
14419 &mut cx,
14420 );
14421
14422 assert_hunk_revert(
14423 indoc! {r#"ˇstruct Row1.1;
14424 struct Row1;
14425 «ˇstr»uct Row22;
14426
14427 struct ˇRow44;
14428 struct Row5;
14429 struct «Rˇ»ow66;ˇ
14430
14431 «struˇ»ct Row88;
14432 struct Row9;
14433 struct Row1011;ˇ"#},
14434 vec![
14435 DiffHunkStatusKind::Modified,
14436 DiffHunkStatusKind::Modified,
14437 DiffHunkStatusKind::Modified,
14438 DiffHunkStatusKind::Modified,
14439 DiffHunkStatusKind::Modified,
14440 DiffHunkStatusKind::Modified,
14441 ],
14442 indoc! {r#"struct Row;
14443 ˇstruct Row1;
14444 struct Row2;
14445 ˇ
14446 struct Row4;
14447 ˇstruct Row5;
14448 struct Row6;
14449 ˇ
14450 struct Row8;
14451 ˇstruct Row9;
14452 struct Row10;ˇ"#},
14453 base_text,
14454 &mut cx,
14455 );
14456}
14457
14458#[gpui::test]
14459async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
14460 init_test(cx, |_| {});
14461 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14462 let base_text = indoc! {r#"
14463 one
14464
14465 two
14466 three
14467 "#};
14468
14469 cx.set_head_text(base_text);
14470 cx.set_state("\nˇ\n");
14471 cx.executor().run_until_parked();
14472 cx.update_editor(|editor, _window, cx| {
14473 editor.expand_selected_diff_hunks(cx);
14474 });
14475 cx.executor().run_until_parked();
14476 cx.update_editor(|editor, window, cx| {
14477 editor.backspace(&Default::default(), window, cx);
14478 });
14479 cx.run_until_parked();
14480 cx.assert_state_with_diff(
14481 indoc! {r#"
14482
14483 - two
14484 - threeˇ
14485 +
14486 "#}
14487 .to_string(),
14488 );
14489}
14490
14491#[gpui::test]
14492async fn test_deletion_reverts(cx: &mut TestAppContext) {
14493 init_test(cx, |_| {});
14494 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14495 let base_text = indoc! {r#"struct Row;
14496struct Row1;
14497struct Row2;
14498
14499struct Row4;
14500struct Row5;
14501struct Row6;
14502
14503struct Row8;
14504struct Row9;
14505struct Row10;"#};
14506
14507 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
14508 assert_hunk_revert(
14509 indoc! {r#"struct Row;
14510 struct Row2;
14511
14512 ˇstruct Row4;
14513 struct Row5;
14514 struct Row6;
14515 ˇ
14516 struct Row8;
14517 struct Row10;"#},
14518 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14519 indoc! {r#"struct Row;
14520 struct Row2;
14521
14522 ˇstruct Row4;
14523 struct Row5;
14524 struct Row6;
14525 ˇ
14526 struct Row8;
14527 struct Row10;"#},
14528 base_text,
14529 &mut cx,
14530 );
14531 assert_hunk_revert(
14532 indoc! {r#"struct Row;
14533 struct Row2;
14534
14535 «ˇstruct Row4;
14536 struct» Row5;
14537 «struct Row6;
14538 ˇ»
14539 struct Row8;
14540 struct Row10;"#},
14541 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14542 indoc! {r#"struct Row;
14543 struct Row2;
14544
14545 «ˇstruct Row4;
14546 struct» Row5;
14547 «struct Row6;
14548 ˇ»
14549 struct Row8;
14550 struct Row10;"#},
14551 base_text,
14552 &mut cx,
14553 );
14554
14555 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
14556 assert_hunk_revert(
14557 indoc! {r#"struct Row;
14558 ˇstruct Row2;
14559
14560 struct Row4;
14561 struct Row5;
14562 struct Row6;
14563
14564 struct Row8;ˇ
14565 struct Row10;"#},
14566 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14567 indoc! {r#"struct Row;
14568 struct Row1;
14569 ˇstruct Row2;
14570
14571 struct Row4;
14572 struct Row5;
14573 struct Row6;
14574
14575 struct Row8;ˇ
14576 struct Row9;
14577 struct Row10;"#},
14578 base_text,
14579 &mut cx,
14580 );
14581 assert_hunk_revert(
14582 indoc! {r#"struct Row;
14583 struct Row2«ˇ;
14584 struct Row4;
14585 struct» Row5;
14586 «struct Row6;
14587
14588 struct Row8;ˇ»
14589 struct Row10;"#},
14590 vec![
14591 DiffHunkStatusKind::Deleted,
14592 DiffHunkStatusKind::Deleted,
14593 DiffHunkStatusKind::Deleted,
14594 ],
14595 indoc! {r#"struct Row;
14596 struct Row1;
14597 struct Row2«ˇ;
14598
14599 struct Row4;
14600 struct» Row5;
14601 «struct Row6;
14602
14603 struct Row8;ˇ»
14604 struct Row9;
14605 struct Row10;"#},
14606 base_text,
14607 &mut cx,
14608 );
14609}
14610
14611#[gpui::test]
14612async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
14613 init_test(cx, |_| {});
14614
14615 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
14616 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
14617 let base_text_3 =
14618 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
14619
14620 let text_1 = edit_first_char_of_every_line(base_text_1);
14621 let text_2 = edit_first_char_of_every_line(base_text_2);
14622 let text_3 = edit_first_char_of_every_line(base_text_3);
14623
14624 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
14625 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
14626 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
14627
14628 let multibuffer = cx.new(|cx| {
14629 let mut multibuffer = MultiBuffer::new(ReadWrite);
14630 multibuffer.push_excerpts(
14631 buffer_1.clone(),
14632 [
14633 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14634 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14635 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14636 ],
14637 cx,
14638 );
14639 multibuffer.push_excerpts(
14640 buffer_2.clone(),
14641 [
14642 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14643 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14644 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14645 ],
14646 cx,
14647 );
14648 multibuffer.push_excerpts(
14649 buffer_3.clone(),
14650 [
14651 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14652 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14653 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14654 ],
14655 cx,
14656 );
14657 multibuffer
14658 });
14659
14660 let fs = FakeFs::new(cx.executor());
14661 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
14662 let (editor, cx) = cx
14663 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
14664 editor.update_in(cx, |editor, _window, cx| {
14665 for (buffer, diff_base) in [
14666 (buffer_1.clone(), base_text_1),
14667 (buffer_2.clone(), base_text_2),
14668 (buffer_3.clone(), base_text_3),
14669 ] {
14670 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
14671 editor
14672 .buffer
14673 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
14674 }
14675 });
14676 cx.executor().run_until_parked();
14677
14678 editor.update_in(cx, |editor, window, cx| {
14679 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}");
14680 editor.select_all(&SelectAll, window, cx);
14681 editor.git_restore(&Default::default(), window, cx);
14682 });
14683 cx.executor().run_until_parked();
14684
14685 // When all ranges are selected, all buffer hunks are reverted.
14686 editor.update(cx, |editor, cx| {
14687 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");
14688 });
14689 buffer_1.update(cx, |buffer, _| {
14690 assert_eq!(buffer.text(), base_text_1);
14691 });
14692 buffer_2.update(cx, |buffer, _| {
14693 assert_eq!(buffer.text(), base_text_2);
14694 });
14695 buffer_3.update(cx, |buffer, _| {
14696 assert_eq!(buffer.text(), base_text_3);
14697 });
14698
14699 editor.update_in(cx, |editor, window, cx| {
14700 editor.undo(&Default::default(), window, cx);
14701 });
14702
14703 editor.update_in(cx, |editor, window, cx| {
14704 editor.change_selections(None, window, cx, |s| {
14705 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
14706 });
14707 editor.git_restore(&Default::default(), window, cx);
14708 });
14709
14710 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
14711 // but not affect buffer_2 and its related excerpts.
14712 editor.update(cx, |editor, cx| {
14713 assert_eq!(
14714 editor.text(cx),
14715 "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}"
14716 );
14717 });
14718 buffer_1.update(cx, |buffer, _| {
14719 assert_eq!(buffer.text(), base_text_1);
14720 });
14721 buffer_2.update(cx, |buffer, _| {
14722 assert_eq!(
14723 buffer.text(),
14724 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
14725 );
14726 });
14727 buffer_3.update(cx, |buffer, _| {
14728 assert_eq!(
14729 buffer.text(),
14730 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
14731 );
14732 });
14733
14734 fn edit_first_char_of_every_line(text: &str) -> String {
14735 text.split('\n')
14736 .map(|line| format!("X{}", &line[1..]))
14737 .collect::<Vec<_>>()
14738 .join("\n")
14739 }
14740}
14741
14742#[gpui::test]
14743async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
14744 init_test(cx, |_| {});
14745
14746 let cols = 4;
14747 let rows = 10;
14748 let sample_text_1 = sample_text(rows, cols, 'a');
14749 assert_eq!(
14750 sample_text_1,
14751 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
14752 );
14753 let sample_text_2 = sample_text(rows, cols, 'l');
14754 assert_eq!(
14755 sample_text_2,
14756 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
14757 );
14758 let sample_text_3 = sample_text(rows, cols, 'v');
14759 assert_eq!(
14760 sample_text_3,
14761 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
14762 );
14763
14764 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
14765 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
14766 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
14767
14768 let multi_buffer = cx.new(|cx| {
14769 let mut multibuffer = MultiBuffer::new(ReadWrite);
14770 multibuffer.push_excerpts(
14771 buffer_1.clone(),
14772 [
14773 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14774 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14775 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14776 ],
14777 cx,
14778 );
14779 multibuffer.push_excerpts(
14780 buffer_2.clone(),
14781 [
14782 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14783 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14784 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14785 ],
14786 cx,
14787 );
14788 multibuffer.push_excerpts(
14789 buffer_3.clone(),
14790 [
14791 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14792 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14793 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14794 ],
14795 cx,
14796 );
14797 multibuffer
14798 });
14799
14800 let fs = FakeFs::new(cx.executor());
14801 fs.insert_tree(
14802 "/a",
14803 json!({
14804 "main.rs": sample_text_1,
14805 "other.rs": sample_text_2,
14806 "lib.rs": sample_text_3,
14807 }),
14808 )
14809 .await;
14810 let project = Project::test(fs, ["/a".as_ref()], cx).await;
14811 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14812 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14813 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
14814 Editor::new(
14815 EditorMode::full(),
14816 multi_buffer,
14817 Some(project.clone()),
14818 window,
14819 cx,
14820 )
14821 });
14822 let multibuffer_item_id = workspace
14823 .update(cx, |workspace, window, cx| {
14824 assert!(
14825 workspace.active_item(cx).is_none(),
14826 "active item should be None before the first item is added"
14827 );
14828 workspace.add_item_to_active_pane(
14829 Box::new(multi_buffer_editor.clone()),
14830 None,
14831 true,
14832 window,
14833 cx,
14834 );
14835 let active_item = workspace
14836 .active_item(cx)
14837 .expect("should have an active item after adding the multi buffer");
14838 assert!(
14839 !active_item.is_singleton(cx),
14840 "A multi buffer was expected to active after adding"
14841 );
14842 active_item.item_id()
14843 })
14844 .unwrap();
14845 cx.executor().run_until_parked();
14846
14847 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14848 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14849 s.select_ranges(Some(1..2))
14850 });
14851 editor.open_excerpts(&OpenExcerpts, window, cx);
14852 });
14853 cx.executor().run_until_parked();
14854 let first_item_id = workspace
14855 .update(cx, |workspace, window, cx| {
14856 let active_item = workspace
14857 .active_item(cx)
14858 .expect("should have an active item after navigating into the 1st buffer");
14859 let first_item_id = active_item.item_id();
14860 assert_ne!(
14861 first_item_id, multibuffer_item_id,
14862 "Should navigate into the 1st buffer and activate it"
14863 );
14864 assert!(
14865 active_item.is_singleton(cx),
14866 "New active item should be a singleton buffer"
14867 );
14868 assert_eq!(
14869 active_item
14870 .act_as::<Editor>(cx)
14871 .expect("should have navigated into an editor for the 1st buffer")
14872 .read(cx)
14873 .text(cx),
14874 sample_text_1
14875 );
14876
14877 workspace
14878 .go_back(workspace.active_pane().downgrade(), window, cx)
14879 .detach_and_log_err(cx);
14880
14881 first_item_id
14882 })
14883 .unwrap();
14884 cx.executor().run_until_parked();
14885 workspace
14886 .update(cx, |workspace, _, cx| {
14887 let active_item = workspace
14888 .active_item(cx)
14889 .expect("should have an active item after navigating back");
14890 assert_eq!(
14891 active_item.item_id(),
14892 multibuffer_item_id,
14893 "Should navigate back to the multi buffer"
14894 );
14895 assert!(!active_item.is_singleton(cx));
14896 })
14897 .unwrap();
14898
14899 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14900 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14901 s.select_ranges(Some(39..40))
14902 });
14903 editor.open_excerpts(&OpenExcerpts, window, cx);
14904 });
14905 cx.executor().run_until_parked();
14906 let second_item_id = workspace
14907 .update(cx, |workspace, window, cx| {
14908 let active_item = workspace
14909 .active_item(cx)
14910 .expect("should have an active item after navigating into the 2nd buffer");
14911 let second_item_id = active_item.item_id();
14912 assert_ne!(
14913 second_item_id, multibuffer_item_id,
14914 "Should navigate away from the multibuffer"
14915 );
14916 assert_ne!(
14917 second_item_id, first_item_id,
14918 "Should navigate into the 2nd buffer and activate it"
14919 );
14920 assert!(
14921 active_item.is_singleton(cx),
14922 "New active item should be a singleton buffer"
14923 );
14924 assert_eq!(
14925 active_item
14926 .act_as::<Editor>(cx)
14927 .expect("should have navigated into an editor")
14928 .read(cx)
14929 .text(cx),
14930 sample_text_2
14931 );
14932
14933 workspace
14934 .go_back(workspace.active_pane().downgrade(), window, cx)
14935 .detach_and_log_err(cx);
14936
14937 second_item_id
14938 })
14939 .unwrap();
14940 cx.executor().run_until_parked();
14941 workspace
14942 .update(cx, |workspace, _, cx| {
14943 let active_item = workspace
14944 .active_item(cx)
14945 .expect("should have an active item after navigating back from the 2nd buffer");
14946 assert_eq!(
14947 active_item.item_id(),
14948 multibuffer_item_id,
14949 "Should navigate back from the 2nd buffer to the multi buffer"
14950 );
14951 assert!(!active_item.is_singleton(cx));
14952 })
14953 .unwrap();
14954
14955 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14956 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14957 s.select_ranges(Some(70..70))
14958 });
14959 editor.open_excerpts(&OpenExcerpts, window, cx);
14960 });
14961 cx.executor().run_until_parked();
14962 workspace
14963 .update(cx, |workspace, window, cx| {
14964 let active_item = workspace
14965 .active_item(cx)
14966 .expect("should have an active item after navigating into the 3rd buffer");
14967 let third_item_id = active_item.item_id();
14968 assert_ne!(
14969 third_item_id, multibuffer_item_id,
14970 "Should navigate into the 3rd buffer and activate it"
14971 );
14972 assert_ne!(third_item_id, first_item_id);
14973 assert_ne!(third_item_id, second_item_id);
14974 assert!(
14975 active_item.is_singleton(cx),
14976 "New active item should be a singleton buffer"
14977 );
14978 assert_eq!(
14979 active_item
14980 .act_as::<Editor>(cx)
14981 .expect("should have navigated into an editor")
14982 .read(cx)
14983 .text(cx),
14984 sample_text_3
14985 );
14986
14987 workspace
14988 .go_back(workspace.active_pane().downgrade(), window, cx)
14989 .detach_and_log_err(cx);
14990 })
14991 .unwrap();
14992 cx.executor().run_until_parked();
14993 workspace
14994 .update(cx, |workspace, _, cx| {
14995 let active_item = workspace
14996 .active_item(cx)
14997 .expect("should have an active item after navigating back from the 3rd buffer");
14998 assert_eq!(
14999 active_item.item_id(),
15000 multibuffer_item_id,
15001 "Should navigate back from the 3rd buffer to the multi buffer"
15002 );
15003 assert!(!active_item.is_singleton(cx));
15004 })
15005 .unwrap();
15006}
15007
15008#[gpui::test]
15009async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15010 init_test(cx, |_| {});
15011
15012 let mut cx = EditorTestContext::new(cx).await;
15013
15014 let diff_base = r#"
15015 use some::mod;
15016
15017 const A: u32 = 42;
15018
15019 fn main() {
15020 println!("hello");
15021
15022 println!("world");
15023 }
15024 "#
15025 .unindent();
15026
15027 cx.set_state(
15028 &r#"
15029 use some::modified;
15030
15031 ˇ
15032 fn main() {
15033 println!("hello there");
15034
15035 println!("around the");
15036 println!("world");
15037 }
15038 "#
15039 .unindent(),
15040 );
15041
15042 cx.set_head_text(&diff_base);
15043 executor.run_until_parked();
15044
15045 cx.update_editor(|editor, window, cx| {
15046 editor.go_to_next_hunk(&GoToHunk, window, cx);
15047 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15048 });
15049 executor.run_until_parked();
15050 cx.assert_state_with_diff(
15051 r#"
15052 use some::modified;
15053
15054
15055 fn main() {
15056 - println!("hello");
15057 + ˇ println!("hello there");
15058
15059 println!("around the");
15060 println!("world");
15061 }
15062 "#
15063 .unindent(),
15064 );
15065
15066 cx.update_editor(|editor, window, cx| {
15067 for _ in 0..2 {
15068 editor.go_to_next_hunk(&GoToHunk, window, cx);
15069 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15070 }
15071 });
15072 executor.run_until_parked();
15073 cx.assert_state_with_diff(
15074 r#"
15075 - use some::mod;
15076 + ˇuse some::modified;
15077
15078
15079 fn main() {
15080 - println!("hello");
15081 + println!("hello there");
15082
15083 + println!("around the");
15084 println!("world");
15085 }
15086 "#
15087 .unindent(),
15088 );
15089
15090 cx.update_editor(|editor, window, cx| {
15091 editor.go_to_next_hunk(&GoToHunk, window, cx);
15092 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15093 });
15094 executor.run_until_parked();
15095 cx.assert_state_with_diff(
15096 r#"
15097 - use some::mod;
15098 + use some::modified;
15099
15100 - const A: u32 = 42;
15101 ˇ
15102 fn main() {
15103 - println!("hello");
15104 + println!("hello there");
15105
15106 + println!("around the");
15107 println!("world");
15108 }
15109 "#
15110 .unindent(),
15111 );
15112
15113 cx.update_editor(|editor, window, cx| {
15114 editor.cancel(&Cancel, window, cx);
15115 });
15116
15117 cx.assert_state_with_diff(
15118 r#"
15119 use some::modified;
15120
15121 ˇ
15122 fn main() {
15123 println!("hello there");
15124
15125 println!("around the");
15126 println!("world");
15127 }
15128 "#
15129 .unindent(),
15130 );
15131}
15132
15133#[gpui::test]
15134async fn test_diff_base_change_with_expanded_diff_hunks(
15135 executor: BackgroundExecutor,
15136 cx: &mut TestAppContext,
15137) {
15138 init_test(cx, |_| {});
15139
15140 let mut cx = EditorTestContext::new(cx).await;
15141
15142 let diff_base = r#"
15143 use some::mod1;
15144 use some::mod2;
15145
15146 const A: u32 = 42;
15147 const B: u32 = 42;
15148 const C: u32 = 42;
15149
15150 fn main() {
15151 println!("hello");
15152
15153 println!("world");
15154 }
15155 "#
15156 .unindent();
15157
15158 cx.set_state(
15159 &r#"
15160 use some::mod2;
15161
15162 const A: u32 = 42;
15163 const C: u32 = 42;
15164
15165 fn main(ˇ) {
15166 //println!("hello");
15167
15168 println!("world");
15169 //
15170 //
15171 }
15172 "#
15173 .unindent(),
15174 );
15175
15176 cx.set_head_text(&diff_base);
15177 executor.run_until_parked();
15178
15179 cx.update_editor(|editor, window, cx| {
15180 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15181 });
15182 executor.run_until_parked();
15183 cx.assert_state_with_diff(
15184 r#"
15185 - use some::mod1;
15186 use some::mod2;
15187
15188 const A: u32 = 42;
15189 - const B: u32 = 42;
15190 const C: u32 = 42;
15191
15192 fn main(ˇ) {
15193 - println!("hello");
15194 + //println!("hello");
15195
15196 println!("world");
15197 + //
15198 + //
15199 }
15200 "#
15201 .unindent(),
15202 );
15203
15204 cx.set_head_text("new diff base!");
15205 executor.run_until_parked();
15206 cx.assert_state_with_diff(
15207 r#"
15208 - new diff base!
15209 + use some::mod2;
15210 +
15211 + const A: u32 = 42;
15212 + const C: u32 = 42;
15213 +
15214 + fn main(ˇ) {
15215 + //println!("hello");
15216 +
15217 + println!("world");
15218 + //
15219 + //
15220 + }
15221 "#
15222 .unindent(),
15223 );
15224}
15225
15226#[gpui::test]
15227async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
15228 init_test(cx, |_| {});
15229
15230 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
15231 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
15232 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
15233 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
15234 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
15235 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
15236
15237 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
15238 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
15239 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
15240
15241 let multi_buffer = cx.new(|cx| {
15242 let mut multibuffer = MultiBuffer::new(ReadWrite);
15243 multibuffer.push_excerpts(
15244 buffer_1.clone(),
15245 [
15246 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15247 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15248 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15249 ],
15250 cx,
15251 );
15252 multibuffer.push_excerpts(
15253 buffer_2.clone(),
15254 [
15255 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15256 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15257 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15258 ],
15259 cx,
15260 );
15261 multibuffer.push_excerpts(
15262 buffer_3.clone(),
15263 [
15264 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15265 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15266 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15267 ],
15268 cx,
15269 );
15270 multibuffer
15271 });
15272
15273 let editor =
15274 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
15275 editor
15276 .update(cx, |editor, _window, cx| {
15277 for (buffer, diff_base) in [
15278 (buffer_1.clone(), file_1_old),
15279 (buffer_2.clone(), file_2_old),
15280 (buffer_3.clone(), file_3_old),
15281 ] {
15282 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
15283 editor
15284 .buffer
15285 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
15286 }
15287 })
15288 .unwrap();
15289
15290 let mut cx = EditorTestContext::for_editor(editor, cx).await;
15291 cx.run_until_parked();
15292
15293 cx.assert_editor_state(
15294 &"
15295 ˇaaa
15296 ccc
15297 ddd
15298
15299 ggg
15300 hhh
15301
15302
15303 lll
15304 mmm
15305 NNN
15306
15307 qqq
15308 rrr
15309
15310 uuu
15311 111
15312 222
15313 333
15314
15315 666
15316 777
15317
15318 000
15319 !!!"
15320 .unindent(),
15321 );
15322
15323 cx.update_editor(|editor, window, cx| {
15324 editor.select_all(&SelectAll, window, cx);
15325 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15326 });
15327 cx.executor().run_until_parked();
15328
15329 cx.assert_state_with_diff(
15330 "
15331 «aaa
15332 - bbb
15333 ccc
15334 ddd
15335
15336 ggg
15337 hhh
15338
15339
15340 lll
15341 mmm
15342 - nnn
15343 + NNN
15344
15345 qqq
15346 rrr
15347
15348 uuu
15349 111
15350 222
15351 333
15352
15353 + 666
15354 777
15355
15356 000
15357 !!!ˇ»"
15358 .unindent(),
15359 );
15360}
15361
15362#[gpui::test]
15363async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
15364 init_test(cx, |_| {});
15365
15366 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
15367 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
15368
15369 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
15370 let multi_buffer = cx.new(|cx| {
15371 let mut multibuffer = MultiBuffer::new(ReadWrite);
15372 multibuffer.push_excerpts(
15373 buffer.clone(),
15374 [
15375 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
15376 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
15377 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
15378 ],
15379 cx,
15380 );
15381 multibuffer
15382 });
15383
15384 let editor =
15385 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
15386 editor
15387 .update(cx, |editor, _window, cx| {
15388 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
15389 editor
15390 .buffer
15391 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
15392 })
15393 .unwrap();
15394
15395 let mut cx = EditorTestContext::for_editor(editor, cx).await;
15396 cx.run_until_parked();
15397
15398 cx.update_editor(|editor, window, cx| {
15399 editor.expand_all_diff_hunks(&Default::default(), window, cx)
15400 });
15401 cx.executor().run_until_parked();
15402
15403 // When the start of a hunk coincides with the start of its excerpt,
15404 // the hunk is expanded. When the start of a a hunk is earlier than
15405 // the start of its excerpt, the hunk is not expanded.
15406 cx.assert_state_with_diff(
15407 "
15408 ˇaaa
15409 - bbb
15410 + BBB
15411
15412 - ddd
15413 - eee
15414 + DDD
15415 + EEE
15416 fff
15417
15418 iii
15419 "
15420 .unindent(),
15421 );
15422}
15423
15424#[gpui::test]
15425async fn test_edits_around_expanded_insertion_hunks(
15426 executor: BackgroundExecutor,
15427 cx: &mut TestAppContext,
15428) {
15429 init_test(cx, |_| {});
15430
15431 let mut cx = EditorTestContext::new(cx).await;
15432
15433 let diff_base = r#"
15434 use some::mod1;
15435 use some::mod2;
15436
15437 const A: u32 = 42;
15438
15439 fn main() {
15440 println!("hello");
15441
15442 println!("world");
15443 }
15444 "#
15445 .unindent();
15446 executor.run_until_parked();
15447 cx.set_state(
15448 &r#"
15449 use some::mod1;
15450 use some::mod2;
15451
15452 const A: u32 = 42;
15453 const B: u32 = 42;
15454 const C: u32 = 42;
15455 ˇ
15456
15457 fn main() {
15458 println!("hello");
15459
15460 println!("world");
15461 }
15462 "#
15463 .unindent(),
15464 );
15465
15466 cx.set_head_text(&diff_base);
15467 executor.run_until_parked();
15468
15469 cx.update_editor(|editor, window, cx| {
15470 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15471 });
15472 executor.run_until_parked();
15473
15474 cx.assert_state_with_diff(
15475 r#"
15476 use some::mod1;
15477 use some::mod2;
15478
15479 const A: u32 = 42;
15480 + const B: u32 = 42;
15481 + const C: u32 = 42;
15482 + ˇ
15483
15484 fn main() {
15485 println!("hello");
15486
15487 println!("world");
15488 }
15489 "#
15490 .unindent(),
15491 );
15492
15493 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
15494 executor.run_until_parked();
15495
15496 cx.assert_state_with_diff(
15497 r#"
15498 use some::mod1;
15499 use some::mod2;
15500
15501 const A: u32 = 42;
15502 + const B: u32 = 42;
15503 + const C: u32 = 42;
15504 + const D: u32 = 42;
15505 + ˇ
15506
15507 fn main() {
15508 println!("hello");
15509
15510 println!("world");
15511 }
15512 "#
15513 .unindent(),
15514 );
15515
15516 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
15517 executor.run_until_parked();
15518
15519 cx.assert_state_with_diff(
15520 r#"
15521 use some::mod1;
15522 use some::mod2;
15523
15524 const A: u32 = 42;
15525 + const B: u32 = 42;
15526 + const C: u32 = 42;
15527 + const D: u32 = 42;
15528 + const E: u32 = 42;
15529 + ˇ
15530
15531 fn main() {
15532 println!("hello");
15533
15534 println!("world");
15535 }
15536 "#
15537 .unindent(),
15538 );
15539
15540 cx.update_editor(|editor, window, cx| {
15541 editor.delete_line(&DeleteLine, window, cx);
15542 });
15543 executor.run_until_parked();
15544
15545 cx.assert_state_with_diff(
15546 r#"
15547 use some::mod1;
15548 use some::mod2;
15549
15550 const A: u32 = 42;
15551 + const B: u32 = 42;
15552 + const C: u32 = 42;
15553 + const D: u32 = 42;
15554 + const E: u32 = 42;
15555 ˇ
15556 fn main() {
15557 println!("hello");
15558
15559 println!("world");
15560 }
15561 "#
15562 .unindent(),
15563 );
15564
15565 cx.update_editor(|editor, window, cx| {
15566 editor.move_up(&MoveUp, window, cx);
15567 editor.delete_line(&DeleteLine, 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 });
15573 executor.run_until_parked();
15574 cx.assert_state_with_diff(
15575 r#"
15576 use some::mod1;
15577 use some::mod2;
15578
15579 const A: u32 = 42;
15580 + const B: u32 = 42;
15581 ˇ
15582 fn main() {
15583 println!("hello");
15584
15585 println!("world");
15586 }
15587 "#
15588 .unindent(),
15589 );
15590
15591 cx.update_editor(|editor, window, cx| {
15592 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
15593 editor.delete_line(&DeleteLine, window, cx);
15594 });
15595 executor.run_until_parked();
15596 cx.assert_state_with_diff(
15597 r#"
15598 ˇ
15599 fn main() {
15600 println!("hello");
15601
15602 println!("world");
15603 }
15604 "#
15605 .unindent(),
15606 );
15607}
15608
15609#[gpui::test]
15610async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
15611 init_test(cx, |_| {});
15612
15613 let mut cx = EditorTestContext::new(cx).await;
15614 cx.set_head_text(indoc! { "
15615 one
15616 two
15617 three
15618 four
15619 five
15620 "
15621 });
15622 cx.set_state(indoc! { "
15623 one
15624 ˇthree
15625 five
15626 "});
15627 cx.run_until_parked();
15628 cx.update_editor(|editor, window, cx| {
15629 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15630 });
15631 cx.assert_state_with_diff(
15632 indoc! { "
15633 one
15634 - two
15635 ˇthree
15636 - four
15637 five
15638 "}
15639 .to_string(),
15640 );
15641 cx.update_editor(|editor, window, cx| {
15642 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15643 });
15644
15645 cx.assert_state_with_diff(
15646 indoc! { "
15647 one
15648 ˇthree
15649 five
15650 "}
15651 .to_string(),
15652 );
15653
15654 cx.set_state(indoc! { "
15655 one
15656 ˇTWO
15657 three
15658 four
15659 five
15660 "});
15661 cx.run_until_parked();
15662 cx.update_editor(|editor, window, cx| {
15663 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15664 });
15665
15666 cx.assert_state_with_diff(
15667 indoc! { "
15668 one
15669 - two
15670 + ˇTWO
15671 three
15672 four
15673 five
15674 "}
15675 .to_string(),
15676 );
15677 cx.update_editor(|editor, window, cx| {
15678 editor.move_up(&Default::default(), window, cx);
15679 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15680 });
15681 cx.assert_state_with_diff(
15682 indoc! { "
15683 one
15684 ˇTWO
15685 three
15686 four
15687 five
15688 "}
15689 .to_string(),
15690 );
15691}
15692
15693#[gpui::test]
15694async fn test_edits_around_expanded_deletion_hunks(
15695 executor: BackgroundExecutor,
15696 cx: &mut TestAppContext,
15697) {
15698 init_test(cx, |_| {});
15699
15700 let mut cx = EditorTestContext::new(cx).await;
15701
15702 let diff_base = r#"
15703 use some::mod1;
15704 use some::mod2;
15705
15706 const A: u32 = 42;
15707 const B: u32 = 42;
15708 const C: u32 = 42;
15709
15710
15711 fn main() {
15712 println!("hello");
15713
15714 println!("world");
15715 }
15716 "#
15717 .unindent();
15718 executor.run_until_parked();
15719 cx.set_state(
15720 &r#"
15721 use some::mod1;
15722 use some::mod2;
15723
15724 ˇconst B: u32 = 42;
15725 const C: u32 = 42;
15726
15727
15728 fn main() {
15729 println!("hello");
15730
15731 println!("world");
15732 }
15733 "#
15734 .unindent(),
15735 );
15736
15737 cx.set_head_text(&diff_base);
15738 executor.run_until_parked();
15739
15740 cx.update_editor(|editor, window, cx| {
15741 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15742 });
15743 executor.run_until_parked();
15744
15745 cx.assert_state_with_diff(
15746 r#"
15747 use some::mod1;
15748 use some::mod2;
15749
15750 - const A: u32 = 42;
15751 ˇconst B: u32 = 42;
15752 const C: u32 = 42;
15753
15754
15755 fn main() {
15756 println!("hello");
15757
15758 println!("world");
15759 }
15760 "#
15761 .unindent(),
15762 );
15763
15764 cx.update_editor(|editor, window, cx| {
15765 editor.delete_line(&DeleteLine, window, cx);
15766 });
15767 executor.run_until_parked();
15768 cx.assert_state_with_diff(
15769 r#"
15770 use some::mod1;
15771 use some::mod2;
15772
15773 - const A: u32 = 42;
15774 - const B: u32 = 42;
15775 ˇconst C: u32 = 42;
15776
15777
15778 fn main() {
15779 println!("hello");
15780
15781 println!("world");
15782 }
15783 "#
15784 .unindent(),
15785 );
15786
15787 cx.update_editor(|editor, window, cx| {
15788 editor.delete_line(&DeleteLine, window, cx);
15789 });
15790 executor.run_until_parked();
15791 cx.assert_state_with_diff(
15792 r#"
15793 use some::mod1;
15794 use some::mod2;
15795
15796 - const A: u32 = 42;
15797 - const B: u32 = 42;
15798 - const C: u32 = 42;
15799 ˇ
15800
15801 fn main() {
15802 println!("hello");
15803
15804 println!("world");
15805 }
15806 "#
15807 .unindent(),
15808 );
15809
15810 cx.update_editor(|editor, window, cx| {
15811 editor.handle_input("replacement", window, cx);
15812 });
15813 executor.run_until_parked();
15814 cx.assert_state_with_diff(
15815 r#"
15816 use some::mod1;
15817 use some::mod2;
15818
15819 - const A: u32 = 42;
15820 - const B: u32 = 42;
15821 - const C: u32 = 42;
15822 -
15823 + replacementˇ
15824
15825 fn main() {
15826 println!("hello");
15827
15828 println!("world");
15829 }
15830 "#
15831 .unindent(),
15832 );
15833}
15834
15835#[gpui::test]
15836async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15837 init_test(cx, |_| {});
15838
15839 let mut cx = EditorTestContext::new(cx).await;
15840
15841 let base_text = r#"
15842 one
15843 two
15844 three
15845 four
15846 five
15847 "#
15848 .unindent();
15849 executor.run_until_parked();
15850 cx.set_state(
15851 &r#"
15852 one
15853 two
15854 fˇour
15855 five
15856 "#
15857 .unindent(),
15858 );
15859
15860 cx.set_head_text(&base_text);
15861 executor.run_until_parked();
15862
15863 cx.update_editor(|editor, window, cx| {
15864 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15865 });
15866 executor.run_until_parked();
15867
15868 cx.assert_state_with_diff(
15869 r#"
15870 one
15871 two
15872 - three
15873 fˇour
15874 five
15875 "#
15876 .unindent(),
15877 );
15878
15879 cx.update_editor(|editor, window, cx| {
15880 editor.backspace(&Backspace, window, cx);
15881 editor.backspace(&Backspace, window, cx);
15882 });
15883 executor.run_until_parked();
15884 cx.assert_state_with_diff(
15885 r#"
15886 one
15887 two
15888 - threeˇ
15889 - four
15890 + our
15891 five
15892 "#
15893 .unindent(),
15894 );
15895}
15896
15897#[gpui::test]
15898async fn test_edit_after_expanded_modification_hunk(
15899 executor: BackgroundExecutor,
15900 cx: &mut TestAppContext,
15901) {
15902 init_test(cx, |_| {});
15903
15904 let mut cx = EditorTestContext::new(cx).await;
15905
15906 let diff_base = r#"
15907 use some::mod1;
15908 use some::mod2;
15909
15910 const A: u32 = 42;
15911 const B: u32 = 42;
15912 const C: u32 = 42;
15913 const D: u32 = 42;
15914
15915
15916 fn main() {
15917 println!("hello");
15918
15919 println!("world");
15920 }"#
15921 .unindent();
15922
15923 cx.set_state(
15924 &r#"
15925 use some::mod1;
15926 use some::mod2;
15927
15928 const A: u32 = 42;
15929 const B: u32 = 42;
15930 const C: u32 = 43ˇ
15931 const D: u32 = 42;
15932
15933
15934 fn main() {
15935 println!("hello");
15936
15937 println!("world");
15938 }"#
15939 .unindent(),
15940 );
15941
15942 cx.set_head_text(&diff_base);
15943 executor.run_until_parked();
15944 cx.update_editor(|editor, window, cx| {
15945 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15946 });
15947 executor.run_until_parked();
15948
15949 cx.assert_state_with_diff(
15950 r#"
15951 use some::mod1;
15952 use some::mod2;
15953
15954 const A: u32 = 42;
15955 const B: u32 = 42;
15956 - const C: u32 = 42;
15957 + const C: u32 = 43ˇ
15958 const D: u32 = 42;
15959
15960
15961 fn main() {
15962 println!("hello");
15963
15964 println!("world");
15965 }"#
15966 .unindent(),
15967 );
15968
15969 cx.update_editor(|editor, window, cx| {
15970 editor.handle_input("\nnew_line\n", window, cx);
15971 });
15972 executor.run_until_parked();
15973
15974 cx.assert_state_with_diff(
15975 r#"
15976 use some::mod1;
15977 use some::mod2;
15978
15979 const A: u32 = 42;
15980 const B: u32 = 42;
15981 - const C: u32 = 42;
15982 + const C: u32 = 43
15983 + new_line
15984 + ˇ
15985 const D: u32 = 42;
15986
15987
15988 fn main() {
15989 println!("hello");
15990
15991 println!("world");
15992 }"#
15993 .unindent(),
15994 );
15995}
15996
15997#[gpui::test]
15998async fn test_stage_and_unstage_added_file_hunk(
15999 executor: BackgroundExecutor,
16000 cx: &mut TestAppContext,
16001) {
16002 init_test(cx, |_| {});
16003
16004 let mut cx = EditorTestContext::new(cx).await;
16005 cx.update_editor(|editor, _, cx| {
16006 editor.set_expand_all_diff_hunks(cx);
16007 });
16008
16009 let working_copy = r#"
16010 ˇfn main() {
16011 println!("hello, world!");
16012 }
16013 "#
16014 .unindent();
16015
16016 cx.set_state(&working_copy);
16017 executor.run_until_parked();
16018
16019 cx.assert_state_with_diff(
16020 r#"
16021 + ˇfn main() {
16022 + println!("hello, world!");
16023 + }
16024 "#
16025 .unindent(),
16026 );
16027 cx.assert_index_text(None);
16028
16029 cx.update_editor(|editor, window, cx| {
16030 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16031 });
16032 executor.run_until_parked();
16033 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
16034 cx.assert_state_with_diff(
16035 r#"
16036 + ˇfn main() {
16037 + println!("hello, world!");
16038 + }
16039 "#
16040 .unindent(),
16041 );
16042
16043 cx.update_editor(|editor, window, cx| {
16044 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16045 });
16046 executor.run_until_parked();
16047 cx.assert_index_text(None);
16048}
16049
16050async fn setup_indent_guides_editor(
16051 text: &str,
16052 cx: &mut TestAppContext,
16053) -> (BufferId, EditorTestContext) {
16054 init_test(cx, |_| {});
16055
16056 let mut cx = EditorTestContext::new(cx).await;
16057
16058 let buffer_id = cx.update_editor(|editor, window, cx| {
16059 editor.set_text(text, window, cx);
16060 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
16061
16062 buffer_ids[0]
16063 });
16064
16065 (buffer_id, cx)
16066}
16067
16068fn assert_indent_guides(
16069 range: Range<u32>,
16070 expected: Vec<IndentGuide>,
16071 active_indices: Option<Vec<usize>>,
16072 cx: &mut EditorTestContext,
16073) {
16074 let indent_guides = cx.update_editor(|editor, window, cx| {
16075 let snapshot = editor.snapshot(window, cx).display_snapshot;
16076 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
16077 editor,
16078 MultiBufferRow(range.start)..MultiBufferRow(range.end),
16079 true,
16080 &snapshot,
16081 cx,
16082 );
16083
16084 indent_guides.sort_by(|a, b| {
16085 a.depth.cmp(&b.depth).then(
16086 a.start_row
16087 .cmp(&b.start_row)
16088 .then(a.end_row.cmp(&b.end_row)),
16089 )
16090 });
16091 indent_guides
16092 });
16093
16094 if let Some(expected) = active_indices {
16095 let active_indices = cx.update_editor(|editor, window, cx| {
16096 let snapshot = editor.snapshot(window, cx).display_snapshot;
16097 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
16098 });
16099
16100 assert_eq!(
16101 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
16102 expected,
16103 "Active indent guide indices do not match"
16104 );
16105 }
16106
16107 assert_eq!(indent_guides, expected, "Indent guides do not match");
16108}
16109
16110fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
16111 IndentGuide {
16112 buffer_id,
16113 start_row: MultiBufferRow(start_row),
16114 end_row: MultiBufferRow(end_row),
16115 depth,
16116 tab_size: 4,
16117 settings: IndentGuideSettings {
16118 enabled: true,
16119 line_width: 1,
16120 active_line_width: 1,
16121 ..Default::default()
16122 },
16123 }
16124}
16125
16126#[gpui::test]
16127async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
16128 let (buffer_id, mut cx) = setup_indent_guides_editor(
16129 &"
16130 fn main() {
16131 let a = 1;
16132 }"
16133 .unindent(),
16134 cx,
16135 )
16136 .await;
16137
16138 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
16139}
16140
16141#[gpui::test]
16142async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
16143 let (buffer_id, mut cx) = setup_indent_guides_editor(
16144 &"
16145 fn main() {
16146 let a = 1;
16147 let b = 2;
16148 }"
16149 .unindent(),
16150 cx,
16151 )
16152 .await;
16153
16154 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
16155}
16156
16157#[gpui::test]
16158async fn test_indent_guide_nested(cx: &mut TestAppContext) {
16159 let (buffer_id, mut cx) = setup_indent_guides_editor(
16160 &"
16161 fn main() {
16162 let a = 1;
16163 if a == 3 {
16164 let b = 2;
16165 } else {
16166 let c = 3;
16167 }
16168 }"
16169 .unindent(),
16170 cx,
16171 )
16172 .await;
16173
16174 assert_indent_guides(
16175 0..8,
16176 vec![
16177 indent_guide(buffer_id, 1, 6, 0),
16178 indent_guide(buffer_id, 3, 3, 1),
16179 indent_guide(buffer_id, 5, 5, 1),
16180 ],
16181 None,
16182 &mut cx,
16183 );
16184}
16185
16186#[gpui::test]
16187async fn test_indent_guide_tab(cx: &mut TestAppContext) {
16188 let (buffer_id, mut cx) = setup_indent_guides_editor(
16189 &"
16190 fn main() {
16191 let a = 1;
16192 let b = 2;
16193 let c = 3;
16194 }"
16195 .unindent(),
16196 cx,
16197 )
16198 .await;
16199
16200 assert_indent_guides(
16201 0..5,
16202 vec![
16203 indent_guide(buffer_id, 1, 3, 0),
16204 indent_guide(buffer_id, 2, 2, 1),
16205 ],
16206 None,
16207 &mut cx,
16208 );
16209}
16210
16211#[gpui::test]
16212async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
16213 let (buffer_id, mut cx) = setup_indent_guides_editor(
16214 &"
16215 fn main() {
16216 let a = 1;
16217
16218 let c = 3;
16219 }"
16220 .unindent(),
16221 cx,
16222 )
16223 .await;
16224
16225 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
16226}
16227
16228#[gpui::test]
16229async fn test_indent_guide_complex(cx: &mut TestAppContext) {
16230 let (buffer_id, mut cx) = setup_indent_guides_editor(
16231 &"
16232 fn main() {
16233 let a = 1;
16234
16235 let c = 3;
16236
16237 if a == 3 {
16238 let b = 2;
16239 } else {
16240 let c = 3;
16241 }
16242 }"
16243 .unindent(),
16244 cx,
16245 )
16246 .await;
16247
16248 assert_indent_guides(
16249 0..11,
16250 vec![
16251 indent_guide(buffer_id, 1, 9, 0),
16252 indent_guide(buffer_id, 6, 6, 1),
16253 indent_guide(buffer_id, 8, 8, 1),
16254 ],
16255 None,
16256 &mut cx,
16257 );
16258}
16259
16260#[gpui::test]
16261async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
16262 let (buffer_id, mut cx) = setup_indent_guides_editor(
16263 &"
16264 fn main() {
16265 let a = 1;
16266
16267 let c = 3;
16268
16269 if a == 3 {
16270 let b = 2;
16271 } else {
16272 let c = 3;
16273 }
16274 }"
16275 .unindent(),
16276 cx,
16277 )
16278 .await;
16279
16280 assert_indent_guides(
16281 1..11,
16282 vec![
16283 indent_guide(buffer_id, 1, 9, 0),
16284 indent_guide(buffer_id, 6, 6, 1),
16285 indent_guide(buffer_id, 8, 8, 1),
16286 ],
16287 None,
16288 &mut cx,
16289 );
16290}
16291
16292#[gpui::test]
16293async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
16294 let (buffer_id, mut cx) = setup_indent_guides_editor(
16295 &"
16296 fn main() {
16297 let a = 1;
16298
16299 let c = 3;
16300
16301 if a == 3 {
16302 let b = 2;
16303 } else {
16304 let c = 3;
16305 }
16306 }"
16307 .unindent(),
16308 cx,
16309 )
16310 .await;
16311
16312 assert_indent_guides(
16313 1..10,
16314 vec![
16315 indent_guide(buffer_id, 1, 9, 0),
16316 indent_guide(buffer_id, 6, 6, 1),
16317 indent_guide(buffer_id, 8, 8, 1),
16318 ],
16319 None,
16320 &mut cx,
16321 );
16322}
16323
16324#[gpui::test]
16325async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
16326 let (buffer_id, mut cx) = setup_indent_guides_editor(
16327 &"
16328 block1
16329 block2
16330 block3
16331 block4
16332 block2
16333 block1
16334 block1"
16335 .unindent(),
16336 cx,
16337 )
16338 .await;
16339
16340 assert_indent_guides(
16341 1..10,
16342 vec![
16343 indent_guide(buffer_id, 1, 4, 0),
16344 indent_guide(buffer_id, 2, 3, 1),
16345 indent_guide(buffer_id, 3, 3, 2),
16346 ],
16347 None,
16348 &mut cx,
16349 );
16350}
16351
16352#[gpui::test]
16353async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
16354 let (buffer_id, mut cx) = setup_indent_guides_editor(
16355 &"
16356 block1
16357 block2
16358 block3
16359
16360 block1
16361 block1"
16362 .unindent(),
16363 cx,
16364 )
16365 .await;
16366
16367 assert_indent_guides(
16368 0..6,
16369 vec![
16370 indent_guide(buffer_id, 1, 2, 0),
16371 indent_guide(buffer_id, 2, 2, 1),
16372 ],
16373 None,
16374 &mut cx,
16375 );
16376}
16377
16378#[gpui::test]
16379async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
16380 let (buffer_id, mut cx) = setup_indent_guides_editor(
16381 &"
16382 block1
16383
16384
16385
16386 block2
16387 "
16388 .unindent(),
16389 cx,
16390 )
16391 .await;
16392
16393 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
16394}
16395
16396#[gpui::test]
16397async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
16398 let (buffer_id, mut cx) = setup_indent_guides_editor(
16399 &"
16400 def a:
16401 \tb = 3
16402 \tif True:
16403 \t\tc = 4
16404 \t\td = 5
16405 \tprint(b)
16406 "
16407 .unindent(),
16408 cx,
16409 )
16410 .await;
16411
16412 assert_indent_guides(
16413 0..6,
16414 vec![
16415 indent_guide(buffer_id, 1, 6, 0),
16416 indent_guide(buffer_id, 3, 4, 1),
16417 ],
16418 None,
16419 &mut cx,
16420 );
16421}
16422
16423#[gpui::test]
16424async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
16425 let (buffer_id, mut cx) = setup_indent_guides_editor(
16426 &"
16427 fn main() {
16428 let a = 1;
16429 }"
16430 .unindent(),
16431 cx,
16432 )
16433 .await;
16434
16435 cx.update_editor(|editor, window, cx| {
16436 editor.change_selections(None, window, cx, |s| {
16437 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16438 });
16439 });
16440
16441 assert_indent_guides(
16442 0..3,
16443 vec![indent_guide(buffer_id, 1, 1, 0)],
16444 Some(vec![0]),
16445 &mut cx,
16446 );
16447}
16448
16449#[gpui::test]
16450async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
16451 let (buffer_id, mut cx) = setup_indent_guides_editor(
16452 &"
16453 fn main() {
16454 if 1 == 2 {
16455 let a = 1;
16456 }
16457 }"
16458 .unindent(),
16459 cx,
16460 )
16461 .await;
16462
16463 cx.update_editor(|editor, window, cx| {
16464 editor.change_selections(None, window, cx, |s| {
16465 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16466 });
16467 });
16468
16469 assert_indent_guides(
16470 0..4,
16471 vec![
16472 indent_guide(buffer_id, 1, 3, 0),
16473 indent_guide(buffer_id, 2, 2, 1),
16474 ],
16475 Some(vec![1]),
16476 &mut cx,
16477 );
16478
16479 cx.update_editor(|editor, window, cx| {
16480 editor.change_selections(None, window, cx, |s| {
16481 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
16482 });
16483 });
16484
16485 assert_indent_guides(
16486 0..4,
16487 vec![
16488 indent_guide(buffer_id, 1, 3, 0),
16489 indent_guide(buffer_id, 2, 2, 1),
16490 ],
16491 Some(vec![1]),
16492 &mut cx,
16493 );
16494
16495 cx.update_editor(|editor, window, cx| {
16496 editor.change_selections(None, window, cx, |s| {
16497 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
16498 });
16499 });
16500
16501 assert_indent_guides(
16502 0..4,
16503 vec![
16504 indent_guide(buffer_id, 1, 3, 0),
16505 indent_guide(buffer_id, 2, 2, 1),
16506 ],
16507 Some(vec![0]),
16508 &mut cx,
16509 );
16510}
16511
16512#[gpui::test]
16513async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
16514 let (buffer_id, mut cx) = setup_indent_guides_editor(
16515 &"
16516 fn main() {
16517 let a = 1;
16518
16519 let b = 2;
16520 }"
16521 .unindent(),
16522 cx,
16523 )
16524 .await;
16525
16526 cx.update_editor(|editor, window, cx| {
16527 editor.change_selections(None, window, cx, |s| {
16528 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
16529 });
16530 });
16531
16532 assert_indent_guides(
16533 0..5,
16534 vec![indent_guide(buffer_id, 1, 3, 0)],
16535 Some(vec![0]),
16536 &mut cx,
16537 );
16538}
16539
16540#[gpui::test]
16541async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
16542 let (buffer_id, mut cx) = setup_indent_guides_editor(
16543 &"
16544 def m:
16545 a = 1
16546 pass"
16547 .unindent(),
16548 cx,
16549 )
16550 .await;
16551
16552 cx.update_editor(|editor, window, cx| {
16553 editor.change_selections(None, window, cx, |s| {
16554 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16555 });
16556 });
16557
16558 assert_indent_guides(
16559 0..3,
16560 vec![indent_guide(buffer_id, 1, 2, 0)],
16561 Some(vec![0]),
16562 &mut cx,
16563 );
16564}
16565
16566#[gpui::test]
16567async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
16568 init_test(cx, |_| {});
16569 let mut cx = EditorTestContext::new(cx).await;
16570 let text = indoc! {
16571 "
16572 impl A {
16573 fn b() {
16574 0;
16575 3;
16576 5;
16577 6;
16578 7;
16579 }
16580 }
16581 "
16582 };
16583 let base_text = indoc! {
16584 "
16585 impl A {
16586 fn b() {
16587 0;
16588 1;
16589 2;
16590 3;
16591 4;
16592 }
16593 fn c() {
16594 5;
16595 6;
16596 7;
16597 }
16598 }
16599 "
16600 };
16601
16602 cx.update_editor(|editor, window, cx| {
16603 editor.set_text(text, window, cx);
16604
16605 editor.buffer().update(cx, |multibuffer, cx| {
16606 let buffer = multibuffer.as_singleton().unwrap();
16607 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
16608
16609 multibuffer.set_all_diff_hunks_expanded(cx);
16610 multibuffer.add_diff(diff, cx);
16611
16612 buffer.read(cx).remote_id()
16613 })
16614 });
16615 cx.run_until_parked();
16616
16617 cx.assert_state_with_diff(
16618 indoc! { "
16619 impl A {
16620 fn b() {
16621 0;
16622 - 1;
16623 - 2;
16624 3;
16625 - 4;
16626 - }
16627 - fn c() {
16628 5;
16629 6;
16630 7;
16631 }
16632 }
16633 ˇ"
16634 }
16635 .to_string(),
16636 );
16637
16638 let mut actual_guides = cx.update_editor(|editor, window, cx| {
16639 editor
16640 .snapshot(window, cx)
16641 .buffer_snapshot
16642 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
16643 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
16644 .collect::<Vec<_>>()
16645 });
16646 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
16647 assert_eq!(
16648 actual_guides,
16649 vec![
16650 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
16651 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
16652 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
16653 ]
16654 );
16655}
16656
16657#[gpui::test]
16658async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16659 init_test(cx, |_| {});
16660 let mut cx = EditorTestContext::new(cx).await;
16661
16662 let diff_base = r#"
16663 a
16664 b
16665 c
16666 "#
16667 .unindent();
16668
16669 cx.set_state(
16670 &r#"
16671 ˇA
16672 b
16673 C
16674 "#
16675 .unindent(),
16676 );
16677 cx.set_head_text(&diff_base);
16678 cx.update_editor(|editor, window, cx| {
16679 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16680 });
16681 executor.run_until_parked();
16682
16683 let both_hunks_expanded = r#"
16684 - a
16685 + ˇA
16686 b
16687 - c
16688 + C
16689 "#
16690 .unindent();
16691
16692 cx.assert_state_with_diff(both_hunks_expanded.clone());
16693
16694 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16695 let snapshot = editor.snapshot(window, cx);
16696 let hunks = editor
16697 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16698 .collect::<Vec<_>>();
16699 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16700 let buffer_id = hunks[0].buffer_id;
16701 hunks
16702 .into_iter()
16703 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16704 .collect::<Vec<_>>()
16705 });
16706 assert_eq!(hunk_ranges.len(), 2);
16707
16708 cx.update_editor(|editor, _, cx| {
16709 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16710 });
16711 executor.run_until_parked();
16712
16713 let second_hunk_expanded = r#"
16714 ˇA
16715 b
16716 - c
16717 + C
16718 "#
16719 .unindent();
16720
16721 cx.assert_state_with_diff(second_hunk_expanded);
16722
16723 cx.update_editor(|editor, _, cx| {
16724 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16725 });
16726 executor.run_until_parked();
16727
16728 cx.assert_state_with_diff(both_hunks_expanded.clone());
16729
16730 cx.update_editor(|editor, _, cx| {
16731 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
16732 });
16733 executor.run_until_parked();
16734
16735 let first_hunk_expanded = r#"
16736 - a
16737 + ˇA
16738 b
16739 C
16740 "#
16741 .unindent();
16742
16743 cx.assert_state_with_diff(first_hunk_expanded);
16744
16745 cx.update_editor(|editor, _, cx| {
16746 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
16747 });
16748 executor.run_until_parked();
16749
16750 cx.assert_state_with_diff(both_hunks_expanded);
16751
16752 cx.set_state(
16753 &r#"
16754 ˇA
16755 b
16756 "#
16757 .unindent(),
16758 );
16759 cx.run_until_parked();
16760
16761 // TODO this cursor position seems bad
16762 cx.assert_state_with_diff(
16763 r#"
16764 - ˇa
16765 + A
16766 b
16767 "#
16768 .unindent(),
16769 );
16770
16771 cx.update_editor(|editor, window, cx| {
16772 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16773 });
16774
16775 cx.assert_state_with_diff(
16776 r#"
16777 - ˇa
16778 + A
16779 b
16780 - c
16781 "#
16782 .unindent(),
16783 );
16784
16785 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16786 let snapshot = editor.snapshot(window, cx);
16787 let hunks = editor
16788 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16789 .collect::<Vec<_>>();
16790 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16791 let buffer_id = hunks[0].buffer_id;
16792 hunks
16793 .into_iter()
16794 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16795 .collect::<Vec<_>>()
16796 });
16797 assert_eq!(hunk_ranges.len(), 2);
16798
16799 cx.update_editor(|editor, _, cx| {
16800 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
16801 });
16802 executor.run_until_parked();
16803
16804 cx.assert_state_with_diff(
16805 r#"
16806 - ˇa
16807 + A
16808 b
16809 "#
16810 .unindent(),
16811 );
16812}
16813
16814#[gpui::test]
16815async fn test_toggle_deletion_hunk_at_start_of_file(
16816 executor: BackgroundExecutor,
16817 cx: &mut TestAppContext,
16818) {
16819 init_test(cx, |_| {});
16820 let mut cx = EditorTestContext::new(cx).await;
16821
16822 let diff_base = r#"
16823 a
16824 b
16825 c
16826 "#
16827 .unindent();
16828
16829 cx.set_state(
16830 &r#"
16831 ˇb
16832 c
16833 "#
16834 .unindent(),
16835 );
16836 cx.set_head_text(&diff_base);
16837 cx.update_editor(|editor, window, cx| {
16838 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16839 });
16840 executor.run_until_parked();
16841
16842 let hunk_expanded = r#"
16843 - a
16844 ˇb
16845 c
16846 "#
16847 .unindent();
16848
16849 cx.assert_state_with_diff(hunk_expanded.clone());
16850
16851 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16852 let snapshot = editor.snapshot(window, cx);
16853 let hunks = editor
16854 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16855 .collect::<Vec<_>>();
16856 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16857 let buffer_id = hunks[0].buffer_id;
16858 hunks
16859 .into_iter()
16860 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16861 .collect::<Vec<_>>()
16862 });
16863 assert_eq!(hunk_ranges.len(), 1);
16864
16865 cx.update_editor(|editor, _, cx| {
16866 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16867 });
16868 executor.run_until_parked();
16869
16870 let hunk_collapsed = r#"
16871 ˇb
16872 c
16873 "#
16874 .unindent();
16875
16876 cx.assert_state_with_diff(hunk_collapsed);
16877
16878 cx.update_editor(|editor, _, cx| {
16879 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16880 });
16881 executor.run_until_parked();
16882
16883 cx.assert_state_with_diff(hunk_expanded.clone());
16884}
16885
16886#[gpui::test]
16887async fn test_display_diff_hunks(cx: &mut TestAppContext) {
16888 init_test(cx, |_| {});
16889
16890 let fs = FakeFs::new(cx.executor());
16891 fs.insert_tree(
16892 path!("/test"),
16893 json!({
16894 ".git": {},
16895 "file-1": "ONE\n",
16896 "file-2": "TWO\n",
16897 "file-3": "THREE\n",
16898 }),
16899 )
16900 .await;
16901
16902 fs.set_head_for_repo(
16903 path!("/test/.git").as_ref(),
16904 &[
16905 ("file-1".into(), "one\n".into()),
16906 ("file-2".into(), "two\n".into()),
16907 ("file-3".into(), "three\n".into()),
16908 ],
16909 );
16910
16911 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
16912 let mut buffers = vec![];
16913 for i in 1..=3 {
16914 let buffer = project
16915 .update(cx, |project, cx| {
16916 let path = format!(path!("/test/file-{}"), i);
16917 project.open_local_buffer(path, cx)
16918 })
16919 .await
16920 .unwrap();
16921 buffers.push(buffer);
16922 }
16923
16924 let multibuffer = cx.new(|cx| {
16925 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
16926 multibuffer.set_all_diff_hunks_expanded(cx);
16927 for buffer in &buffers {
16928 let snapshot = buffer.read(cx).snapshot();
16929 multibuffer.set_excerpts_for_path(
16930 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
16931 buffer.clone(),
16932 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
16933 DEFAULT_MULTIBUFFER_CONTEXT,
16934 cx,
16935 );
16936 }
16937 multibuffer
16938 });
16939
16940 let editor = cx.add_window(|window, cx| {
16941 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
16942 });
16943 cx.run_until_parked();
16944
16945 let snapshot = editor
16946 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16947 .unwrap();
16948 let hunks = snapshot
16949 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
16950 .map(|hunk| match hunk {
16951 DisplayDiffHunk::Unfolded {
16952 display_row_range, ..
16953 } => display_row_range,
16954 DisplayDiffHunk::Folded { .. } => unreachable!(),
16955 })
16956 .collect::<Vec<_>>();
16957 assert_eq!(
16958 hunks,
16959 [
16960 DisplayRow(2)..DisplayRow(4),
16961 DisplayRow(7)..DisplayRow(9),
16962 DisplayRow(12)..DisplayRow(14),
16963 ]
16964 );
16965}
16966
16967#[gpui::test]
16968async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
16969 init_test(cx, |_| {});
16970
16971 let mut cx = EditorTestContext::new(cx).await;
16972 cx.set_head_text(indoc! { "
16973 one
16974 two
16975 three
16976 four
16977 five
16978 "
16979 });
16980 cx.set_index_text(indoc! { "
16981 one
16982 two
16983 three
16984 four
16985 five
16986 "
16987 });
16988 cx.set_state(indoc! {"
16989 one
16990 TWO
16991 ˇTHREE
16992 FOUR
16993 five
16994 "});
16995 cx.run_until_parked();
16996 cx.update_editor(|editor, window, cx| {
16997 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16998 });
16999 cx.run_until_parked();
17000 cx.assert_index_text(Some(indoc! {"
17001 one
17002 TWO
17003 THREE
17004 FOUR
17005 five
17006 "}));
17007 cx.set_state(indoc! { "
17008 one
17009 TWO
17010 ˇTHREE-HUNDRED
17011 FOUR
17012 five
17013 "});
17014 cx.run_until_parked();
17015 cx.update_editor(|editor, window, cx| {
17016 let snapshot = editor.snapshot(window, cx);
17017 let hunks = editor
17018 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17019 .collect::<Vec<_>>();
17020 assert_eq!(hunks.len(), 1);
17021 assert_eq!(
17022 hunks[0].status(),
17023 DiffHunkStatus {
17024 kind: DiffHunkStatusKind::Modified,
17025 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
17026 }
17027 );
17028
17029 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17030 });
17031 cx.run_until_parked();
17032 cx.assert_index_text(Some(indoc! {"
17033 one
17034 TWO
17035 THREE-HUNDRED
17036 FOUR
17037 five
17038 "}));
17039}
17040
17041#[gpui::test]
17042fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
17043 init_test(cx, |_| {});
17044
17045 let editor = cx.add_window(|window, cx| {
17046 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
17047 build_editor(buffer, window, cx)
17048 });
17049
17050 let render_args = Arc::new(Mutex::new(None));
17051 let snapshot = editor
17052 .update(cx, |editor, window, cx| {
17053 let snapshot = editor.buffer().read(cx).snapshot(cx);
17054 let range =
17055 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
17056
17057 struct RenderArgs {
17058 row: MultiBufferRow,
17059 folded: bool,
17060 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
17061 }
17062
17063 let crease = Crease::inline(
17064 range,
17065 FoldPlaceholder::test(),
17066 {
17067 let toggle_callback = render_args.clone();
17068 move |row, folded, callback, _window, _cx| {
17069 *toggle_callback.lock() = Some(RenderArgs {
17070 row,
17071 folded,
17072 callback,
17073 });
17074 div()
17075 }
17076 },
17077 |_row, _folded, _window, _cx| div(),
17078 );
17079
17080 editor.insert_creases(Some(crease), cx);
17081 let snapshot = editor.snapshot(window, cx);
17082 let _div = snapshot.render_crease_toggle(
17083 MultiBufferRow(1),
17084 false,
17085 cx.entity().clone(),
17086 window,
17087 cx,
17088 );
17089 snapshot
17090 })
17091 .unwrap();
17092
17093 let render_args = render_args.lock().take().unwrap();
17094 assert_eq!(render_args.row, MultiBufferRow(1));
17095 assert!(!render_args.folded);
17096 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
17097
17098 cx.update_window(*editor, |_, window, cx| {
17099 (render_args.callback)(true, window, cx)
17100 })
17101 .unwrap();
17102 let snapshot = editor
17103 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17104 .unwrap();
17105 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
17106
17107 cx.update_window(*editor, |_, window, cx| {
17108 (render_args.callback)(false, window, cx)
17109 })
17110 .unwrap();
17111 let snapshot = editor
17112 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17113 .unwrap();
17114 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
17115}
17116
17117#[gpui::test]
17118async fn test_input_text(cx: &mut TestAppContext) {
17119 init_test(cx, |_| {});
17120 let mut cx = EditorTestContext::new(cx).await;
17121
17122 cx.set_state(
17123 &r#"ˇone
17124 two
17125
17126 three
17127 fourˇ
17128 five
17129
17130 siˇx"#
17131 .unindent(),
17132 );
17133
17134 cx.dispatch_action(HandleInput(String::new()));
17135 cx.assert_editor_state(
17136 &r#"ˇone
17137 two
17138
17139 three
17140 fourˇ
17141 five
17142
17143 siˇx"#
17144 .unindent(),
17145 );
17146
17147 cx.dispatch_action(HandleInput("AAAA".to_string()));
17148 cx.assert_editor_state(
17149 &r#"AAAAˇone
17150 two
17151
17152 three
17153 fourAAAAˇ
17154 five
17155
17156 siAAAAˇx"#
17157 .unindent(),
17158 );
17159}
17160
17161#[gpui::test]
17162async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
17163 init_test(cx, |_| {});
17164
17165 let mut cx = EditorTestContext::new(cx).await;
17166 cx.set_state(
17167 r#"let foo = 1;
17168let foo = 2;
17169let foo = 3;
17170let fooˇ = 4;
17171let foo = 5;
17172let foo = 6;
17173let foo = 7;
17174let foo = 8;
17175let foo = 9;
17176let foo = 10;
17177let foo = 11;
17178let foo = 12;
17179let foo = 13;
17180let foo = 14;
17181let foo = 15;"#,
17182 );
17183
17184 cx.update_editor(|e, window, cx| {
17185 assert_eq!(
17186 e.next_scroll_position,
17187 NextScrollCursorCenterTopBottom::Center,
17188 "Default next scroll direction is center",
17189 );
17190
17191 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17192 assert_eq!(
17193 e.next_scroll_position,
17194 NextScrollCursorCenterTopBottom::Top,
17195 "After center, next scroll direction should be top",
17196 );
17197
17198 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17199 assert_eq!(
17200 e.next_scroll_position,
17201 NextScrollCursorCenterTopBottom::Bottom,
17202 "After top, next scroll direction should be bottom",
17203 );
17204
17205 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17206 assert_eq!(
17207 e.next_scroll_position,
17208 NextScrollCursorCenterTopBottom::Center,
17209 "After bottom, scrolling should start over",
17210 );
17211
17212 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17213 assert_eq!(
17214 e.next_scroll_position,
17215 NextScrollCursorCenterTopBottom::Top,
17216 "Scrolling continues if retriggered fast enough"
17217 );
17218 });
17219
17220 cx.executor()
17221 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
17222 cx.executor().run_until_parked();
17223 cx.update_editor(|e, _, _| {
17224 assert_eq!(
17225 e.next_scroll_position,
17226 NextScrollCursorCenterTopBottom::Center,
17227 "If scrolling is not triggered fast enough, it should reset"
17228 );
17229 });
17230}
17231
17232#[gpui::test]
17233async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
17234 init_test(cx, |_| {});
17235 let mut cx = EditorLspTestContext::new_rust(
17236 lsp::ServerCapabilities {
17237 definition_provider: Some(lsp::OneOf::Left(true)),
17238 references_provider: Some(lsp::OneOf::Left(true)),
17239 ..lsp::ServerCapabilities::default()
17240 },
17241 cx,
17242 )
17243 .await;
17244
17245 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
17246 let go_to_definition = cx
17247 .lsp
17248 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
17249 move |params, _| async move {
17250 if empty_go_to_definition {
17251 Ok(None)
17252 } else {
17253 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
17254 uri: params.text_document_position_params.text_document.uri,
17255 range: lsp::Range::new(
17256 lsp::Position::new(4, 3),
17257 lsp::Position::new(4, 6),
17258 ),
17259 })))
17260 }
17261 },
17262 );
17263 let references = cx
17264 .lsp
17265 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
17266 Ok(Some(vec![lsp::Location {
17267 uri: params.text_document_position.text_document.uri,
17268 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
17269 }]))
17270 });
17271 (go_to_definition, references)
17272 };
17273
17274 cx.set_state(
17275 &r#"fn one() {
17276 let mut a = ˇtwo();
17277 }
17278
17279 fn two() {}"#
17280 .unindent(),
17281 );
17282 set_up_lsp_handlers(false, &mut cx);
17283 let navigated = cx
17284 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17285 .await
17286 .expect("Failed to navigate to definition");
17287 assert_eq!(
17288 navigated,
17289 Navigated::Yes,
17290 "Should have navigated to definition from the GetDefinition response"
17291 );
17292 cx.assert_editor_state(
17293 &r#"fn one() {
17294 let mut a = two();
17295 }
17296
17297 fn «twoˇ»() {}"#
17298 .unindent(),
17299 );
17300
17301 let editors = cx.update_workspace(|workspace, _, cx| {
17302 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17303 });
17304 cx.update_editor(|_, _, test_editor_cx| {
17305 assert_eq!(
17306 editors.len(),
17307 1,
17308 "Initially, only one, test, editor should be open in the workspace"
17309 );
17310 assert_eq!(
17311 test_editor_cx.entity(),
17312 editors.last().expect("Asserted len is 1").clone()
17313 );
17314 });
17315
17316 set_up_lsp_handlers(true, &mut cx);
17317 let navigated = cx
17318 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17319 .await
17320 .expect("Failed to navigate to lookup references");
17321 assert_eq!(
17322 navigated,
17323 Navigated::Yes,
17324 "Should have navigated to references as a fallback after empty GoToDefinition response"
17325 );
17326 // We should not change the selections in the existing file,
17327 // if opening another milti buffer with the references
17328 cx.assert_editor_state(
17329 &r#"fn one() {
17330 let mut a = two();
17331 }
17332
17333 fn «twoˇ»() {}"#
17334 .unindent(),
17335 );
17336 let editors = cx.update_workspace(|workspace, _, cx| {
17337 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17338 });
17339 cx.update_editor(|_, _, test_editor_cx| {
17340 assert_eq!(
17341 editors.len(),
17342 2,
17343 "After falling back to references search, we open a new editor with the results"
17344 );
17345 let references_fallback_text = editors
17346 .into_iter()
17347 .find(|new_editor| *new_editor != test_editor_cx.entity())
17348 .expect("Should have one non-test editor now")
17349 .read(test_editor_cx)
17350 .text(test_editor_cx);
17351 assert_eq!(
17352 references_fallback_text, "fn one() {\n let mut a = two();\n}",
17353 "Should use the range from the references response and not the GoToDefinition one"
17354 );
17355 });
17356}
17357
17358#[gpui::test]
17359async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
17360 init_test(cx, |_| {});
17361 cx.update(|cx| {
17362 let mut editor_settings = EditorSettings::get_global(cx).clone();
17363 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
17364 EditorSettings::override_global(editor_settings, cx);
17365 });
17366 let mut cx = EditorLspTestContext::new_rust(
17367 lsp::ServerCapabilities {
17368 definition_provider: Some(lsp::OneOf::Left(true)),
17369 references_provider: Some(lsp::OneOf::Left(true)),
17370 ..lsp::ServerCapabilities::default()
17371 },
17372 cx,
17373 )
17374 .await;
17375 let original_state = r#"fn one() {
17376 let mut a = ˇtwo();
17377 }
17378
17379 fn two() {}"#
17380 .unindent();
17381 cx.set_state(&original_state);
17382
17383 let mut go_to_definition = cx
17384 .lsp
17385 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
17386 move |_, _| async move { Ok(None) },
17387 );
17388 let _references = cx
17389 .lsp
17390 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
17391 panic!("Should not call for references with no go to definition fallback")
17392 });
17393
17394 let navigated = cx
17395 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17396 .await
17397 .expect("Failed to navigate to lookup references");
17398 go_to_definition
17399 .next()
17400 .await
17401 .expect("Should have called the go_to_definition handler");
17402
17403 assert_eq!(
17404 navigated,
17405 Navigated::No,
17406 "Should have navigated to references as a fallback after empty GoToDefinition response"
17407 );
17408 cx.assert_editor_state(&original_state);
17409 let editors = cx.update_workspace(|workspace, _, cx| {
17410 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17411 });
17412 cx.update_editor(|_, _, _| {
17413 assert_eq!(
17414 editors.len(),
17415 1,
17416 "After unsuccessful fallback, no other editor should have been opened"
17417 );
17418 });
17419}
17420
17421#[gpui::test]
17422async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
17423 init_test(cx, |_| {});
17424
17425 let language = Arc::new(Language::new(
17426 LanguageConfig::default(),
17427 Some(tree_sitter_rust::LANGUAGE.into()),
17428 ));
17429
17430 let text = r#"
17431 #[cfg(test)]
17432 mod tests() {
17433 #[test]
17434 fn runnable_1() {
17435 let a = 1;
17436 }
17437
17438 #[test]
17439 fn runnable_2() {
17440 let a = 1;
17441 let b = 2;
17442 }
17443 }
17444 "#
17445 .unindent();
17446
17447 let fs = FakeFs::new(cx.executor());
17448 fs.insert_file("/file.rs", Default::default()).await;
17449
17450 let project = Project::test(fs, ["/a".as_ref()], cx).await;
17451 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17452 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17453 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
17454 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
17455
17456 let editor = cx.new_window_entity(|window, cx| {
17457 Editor::new(
17458 EditorMode::full(),
17459 multi_buffer,
17460 Some(project.clone()),
17461 window,
17462 cx,
17463 )
17464 });
17465
17466 editor.update_in(cx, |editor, window, cx| {
17467 let snapshot = editor.buffer().read(cx).snapshot(cx);
17468 editor.tasks.insert(
17469 (buffer.read(cx).remote_id(), 3),
17470 RunnableTasks {
17471 templates: vec![],
17472 offset: snapshot.anchor_before(43),
17473 column: 0,
17474 extra_variables: HashMap::default(),
17475 context_range: BufferOffset(43)..BufferOffset(85),
17476 },
17477 );
17478 editor.tasks.insert(
17479 (buffer.read(cx).remote_id(), 8),
17480 RunnableTasks {
17481 templates: vec![],
17482 offset: snapshot.anchor_before(86),
17483 column: 0,
17484 extra_variables: HashMap::default(),
17485 context_range: BufferOffset(86)..BufferOffset(191),
17486 },
17487 );
17488
17489 // Test finding task when cursor is inside function body
17490 editor.change_selections(None, window, cx, |s| {
17491 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
17492 });
17493 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
17494 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
17495
17496 // Test finding task when cursor is on function name
17497 editor.change_selections(None, window, cx, |s| {
17498 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
17499 });
17500 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
17501 assert_eq!(row, 8, "Should find task when cursor is on function name");
17502 });
17503}
17504
17505#[gpui::test]
17506async fn test_folding_buffers(cx: &mut TestAppContext) {
17507 init_test(cx, |_| {});
17508
17509 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
17510 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
17511 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
17512
17513 let fs = FakeFs::new(cx.executor());
17514 fs.insert_tree(
17515 path!("/a"),
17516 json!({
17517 "first.rs": sample_text_1,
17518 "second.rs": sample_text_2,
17519 "third.rs": sample_text_3,
17520 }),
17521 )
17522 .await;
17523 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17524 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17525 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17526 let worktree = project.update(cx, |project, cx| {
17527 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17528 assert_eq!(worktrees.len(), 1);
17529 worktrees.pop().unwrap()
17530 });
17531 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17532
17533 let buffer_1 = project
17534 .update(cx, |project, cx| {
17535 project.open_buffer((worktree_id, "first.rs"), cx)
17536 })
17537 .await
17538 .unwrap();
17539 let buffer_2 = project
17540 .update(cx, |project, cx| {
17541 project.open_buffer((worktree_id, "second.rs"), cx)
17542 })
17543 .await
17544 .unwrap();
17545 let buffer_3 = project
17546 .update(cx, |project, cx| {
17547 project.open_buffer((worktree_id, "third.rs"), cx)
17548 })
17549 .await
17550 .unwrap();
17551
17552 let multi_buffer = cx.new(|cx| {
17553 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17554 multi_buffer.push_excerpts(
17555 buffer_1.clone(),
17556 [
17557 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17558 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17559 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17560 ],
17561 cx,
17562 );
17563 multi_buffer.push_excerpts(
17564 buffer_2.clone(),
17565 [
17566 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17567 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17568 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17569 ],
17570 cx,
17571 );
17572 multi_buffer.push_excerpts(
17573 buffer_3.clone(),
17574 [
17575 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17576 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17577 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17578 ],
17579 cx,
17580 );
17581 multi_buffer
17582 });
17583 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17584 Editor::new(
17585 EditorMode::full(),
17586 multi_buffer.clone(),
17587 Some(project.clone()),
17588 window,
17589 cx,
17590 )
17591 });
17592
17593 assert_eq!(
17594 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17595 "\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",
17596 );
17597
17598 multi_buffer_editor.update(cx, |editor, cx| {
17599 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
17600 });
17601 assert_eq!(
17602 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17603 "\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",
17604 "After folding the first buffer, its text should not be displayed"
17605 );
17606
17607 multi_buffer_editor.update(cx, |editor, cx| {
17608 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
17609 });
17610 assert_eq!(
17611 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17612 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
17613 "After folding the second buffer, its text should not be displayed"
17614 );
17615
17616 multi_buffer_editor.update(cx, |editor, cx| {
17617 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
17618 });
17619 assert_eq!(
17620 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17621 "\n\n\n\n\n",
17622 "After folding the third buffer, its text should not be displayed"
17623 );
17624
17625 // Emulate selection inside the fold logic, that should work
17626 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17627 editor
17628 .snapshot(window, cx)
17629 .next_line_boundary(Point::new(0, 4));
17630 });
17631
17632 multi_buffer_editor.update(cx, |editor, cx| {
17633 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
17634 });
17635 assert_eq!(
17636 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17637 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
17638 "After unfolding the second buffer, its text should be displayed"
17639 );
17640
17641 // Typing inside of buffer 1 causes that buffer to be unfolded.
17642 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17643 assert_eq!(
17644 multi_buffer
17645 .read(cx)
17646 .snapshot(cx)
17647 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
17648 .collect::<String>(),
17649 "bbbb"
17650 );
17651 editor.change_selections(None, window, cx, |selections| {
17652 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
17653 });
17654 editor.handle_input("B", window, cx);
17655 });
17656
17657 assert_eq!(
17658 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17659 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
17660 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
17661 );
17662
17663 multi_buffer_editor.update(cx, |editor, cx| {
17664 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
17665 });
17666 assert_eq!(
17667 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17668 "\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",
17669 "After unfolding the all buffers, all original text should be displayed"
17670 );
17671}
17672
17673#[gpui::test]
17674async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
17675 init_test(cx, |_| {});
17676
17677 let sample_text_1 = "1111\n2222\n3333".to_string();
17678 let sample_text_2 = "4444\n5555\n6666".to_string();
17679 let sample_text_3 = "7777\n8888\n9999".to_string();
17680
17681 let fs = FakeFs::new(cx.executor());
17682 fs.insert_tree(
17683 path!("/a"),
17684 json!({
17685 "first.rs": sample_text_1,
17686 "second.rs": sample_text_2,
17687 "third.rs": sample_text_3,
17688 }),
17689 )
17690 .await;
17691 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17692 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17693 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17694 let worktree = project.update(cx, |project, cx| {
17695 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17696 assert_eq!(worktrees.len(), 1);
17697 worktrees.pop().unwrap()
17698 });
17699 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17700
17701 let buffer_1 = project
17702 .update(cx, |project, cx| {
17703 project.open_buffer((worktree_id, "first.rs"), cx)
17704 })
17705 .await
17706 .unwrap();
17707 let buffer_2 = project
17708 .update(cx, |project, cx| {
17709 project.open_buffer((worktree_id, "second.rs"), cx)
17710 })
17711 .await
17712 .unwrap();
17713 let buffer_3 = project
17714 .update(cx, |project, cx| {
17715 project.open_buffer((worktree_id, "third.rs"), cx)
17716 })
17717 .await
17718 .unwrap();
17719
17720 let multi_buffer = cx.new(|cx| {
17721 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17722 multi_buffer.push_excerpts(
17723 buffer_1.clone(),
17724 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
17725 cx,
17726 );
17727 multi_buffer.push_excerpts(
17728 buffer_2.clone(),
17729 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
17730 cx,
17731 );
17732 multi_buffer.push_excerpts(
17733 buffer_3.clone(),
17734 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
17735 cx,
17736 );
17737 multi_buffer
17738 });
17739
17740 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17741 Editor::new(
17742 EditorMode::full(),
17743 multi_buffer,
17744 Some(project.clone()),
17745 window,
17746 cx,
17747 )
17748 });
17749
17750 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
17751 assert_eq!(
17752 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17753 full_text,
17754 );
17755
17756 multi_buffer_editor.update(cx, |editor, cx| {
17757 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
17758 });
17759 assert_eq!(
17760 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17761 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
17762 "After folding the first buffer, its text should not be displayed"
17763 );
17764
17765 multi_buffer_editor.update(cx, |editor, cx| {
17766 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
17767 });
17768
17769 assert_eq!(
17770 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17771 "\n\n\n\n\n\n7777\n8888\n9999",
17772 "After folding the second buffer, its text should not be displayed"
17773 );
17774
17775 multi_buffer_editor.update(cx, |editor, cx| {
17776 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
17777 });
17778 assert_eq!(
17779 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17780 "\n\n\n\n\n",
17781 "After folding the third buffer, its text should not be displayed"
17782 );
17783
17784 multi_buffer_editor.update(cx, |editor, cx| {
17785 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
17786 });
17787 assert_eq!(
17788 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17789 "\n\n\n\n4444\n5555\n6666\n\n",
17790 "After unfolding the second buffer, its text should be displayed"
17791 );
17792
17793 multi_buffer_editor.update(cx, |editor, cx| {
17794 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
17795 });
17796 assert_eq!(
17797 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17798 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
17799 "After unfolding the first buffer, its text should be displayed"
17800 );
17801
17802 multi_buffer_editor.update(cx, |editor, cx| {
17803 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
17804 });
17805 assert_eq!(
17806 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17807 full_text,
17808 "After unfolding all buffers, all original text should be displayed"
17809 );
17810}
17811
17812#[gpui::test]
17813async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
17814 init_test(cx, |_| {});
17815
17816 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
17817
17818 let fs = FakeFs::new(cx.executor());
17819 fs.insert_tree(
17820 path!("/a"),
17821 json!({
17822 "main.rs": sample_text,
17823 }),
17824 )
17825 .await;
17826 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17827 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17828 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17829 let worktree = project.update(cx, |project, cx| {
17830 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17831 assert_eq!(worktrees.len(), 1);
17832 worktrees.pop().unwrap()
17833 });
17834 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17835
17836 let buffer_1 = project
17837 .update(cx, |project, cx| {
17838 project.open_buffer((worktree_id, "main.rs"), cx)
17839 })
17840 .await
17841 .unwrap();
17842
17843 let multi_buffer = cx.new(|cx| {
17844 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17845 multi_buffer.push_excerpts(
17846 buffer_1.clone(),
17847 [ExcerptRange::new(
17848 Point::new(0, 0)
17849 ..Point::new(
17850 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
17851 0,
17852 ),
17853 )],
17854 cx,
17855 );
17856 multi_buffer
17857 });
17858 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17859 Editor::new(
17860 EditorMode::full(),
17861 multi_buffer,
17862 Some(project.clone()),
17863 window,
17864 cx,
17865 )
17866 });
17867
17868 let selection_range = Point::new(1, 0)..Point::new(2, 0);
17869 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17870 enum TestHighlight {}
17871 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
17872 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
17873 editor.highlight_text::<TestHighlight>(
17874 vec![highlight_range.clone()],
17875 HighlightStyle::color(Hsla::green()),
17876 cx,
17877 );
17878 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
17879 });
17880
17881 let full_text = format!("\n\n{sample_text}");
17882 assert_eq!(
17883 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17884 full_text,
17885 );
17886}
17887
17888#[gpui::test]
17889async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
17890 init_test(cx, |_| {});
17891 cx.update(|cx| {
17892 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
17893 "keymaps/default-linux.json",
17894 cx,
17895 )
17896 .unwrap();
17897 cx.bind_keys(default_key_bindings);
17898 });
17899
17900 let (editor, cx) = cx.add_window_view(|window, cx| {
17901 let multi_buffer = MultiBuffer::build_multi(
17902 [
17903 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
17904 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
17905 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
17906 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
17907 ],
17908 cx,
17909 );
17910 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
17911
17912 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
17913 // fold all but the second buffer, so that we test navigating between two
17914 // adjacent folded buffers, as well as folded buffers at the start and
17915 // end the multibuffer
17916 editor.fold_buffer(buffer_ids[0], cx);
17917 editor.fold_buffer(buffer_ids[2], cx);
17918 editor.fold_buffer(buffer_ids[3], cx);
17919
17920 editor
17921 });
17922 cx.simulate_resize(size(px(1000.), px(1000.)));
17923
17924 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
17925 cx.assert_excerpts_with_selections(indoc! {"
17926 [EXCERPT]
17927 ˇ[FOLDED]
17928 [EXCERPT]
17929 a1
17930 b1
17931 [EXCERPT]
17932 [FOLDED]
17933 [EXCERPT]
17934 [FOLDED]
17935 "
17936 });
17937 cx.simulate_keystroke("down");
17938 cx.assert_excerpts_with_selections(indoc! {"
17939 [EXCERPT]
17940 [FOLDED]
17941 [EXCERPT]
17942 ˇa1
17943 b1
17944 [EXCERPT]
17945 [FOLDED]
17946 [EXCERPT]
17947 [FOLDED]
17948 "
17949 });
17950 cx.simulate_keystroke("down");
17951 cx.assert_excerpts_with_selections(indoc! {"
17952 [EXCERPT]
17953 [FOLDED]
17954 [EXCERPT]
17955 a1
17956 ˇb1
17957 [EXCERPT]
17958 [FOLDED]
17959 [EXCERPT]
17960 [FOLDED]
17961 "
17962 });
17963 cx.simulate_keystroke("down");
17964 cx.assert_excerpts_with_selections(indoc! {"
17965 [EXCERPT]
17966 [FOLDED]
17967 [EXCERPT]
17968 a1
17969 b1
17970 ˇ[EXCERPT]
17971 [FOLDED]
17972 [EXCERPT]
17973 [FOLDED]
17974 "
17975 });
17976 cx.simulate_keystroke("down");
17977 cx.assert_excerpts_with_selections(indoc! {"
17978 [EXCERPT]
17979 [FOLDED]
17980 [EXCERPT]
17981 a1
17982 b1
17983 [EXCERPT]
17984 ˇ[FOLDED]
17985 [EXCERPT]
17986 [FOLDED]
17987 "
17988 });
17989 for _ in 0..5 {
17990 cx.simulate_keystroke("down");
17991 cx.assert_excerpts_with_selections(indoc! {"
17992 [EXCERPT]
17993 [FOLDED]
17994 [EXCERPT]
17995 a1
17996 b1
17997 [EXCERPT]
17998 [FOLDED]
17999 [EXCERPT]
18000 ˇ[FOLDED]
18001 "
18002 });
18003 }
18004
18005 cx.simulate_keystroke("up");
18006 cx.assert_excerpts_with_selections(indoc! {"
18007 [EXCERPT]
18008 [FOLDED]
18009 [EXCERPT]
18010 a1
18011 b1
18012 [EXCERPT]
18013 ˇ[FOLDED]
18014 [EXCERPT]
18015 [FOLDED]
18016 "
18017 });
18018 cx.simulate_keystroke("up");
18019 cx.assert_excerpts_with_selections(indoc! {"
18020 [EXCERPT]
18021 [FOLDED]
18022 [EXCERPT]
18023 a1
18024 b1
18025 ˇ[EXCERPT]
18026 [FOLDED]
18027 [EXCERPT]
18028 [FOLDED]
18029 "
18030 });
18031 cx.simulate_keystroke("up");
18032 cx.assert_excerpts_with_selections(indoc! {"
18033 [EXCERPT]
18034 [FOLDED]
18035 [EXCERPT]
18036 a1
18037 ˇb1
18038 [EXCERPT]
18039 [FOLDED]
18040 [EXCERPT]
18041 [FOLDED]
18042 "
18043 });
18044 cx.simulate_keystroke("up");
18045 cx.assert_excerpts_with_selections(indoc! {"
18046 [EXCERPT]
18047 [FOLDED]
18048 [EXCERPT]
18049 ˇa1
18050 b1
18051 [EXCERPT]
18052 [FOLDED]
18053 [EXCERPT]
18054 [FOLDED]
18055 "
18056 });
18057 for _ in 0..5 {
18058 cx.simulate_keystroke("up");
18059 cx.assert_excerpts_with_selections(indoc! {"
18060 [EXCERPT]
18061 ˇ[FOLDED]
18062 [EXCERPT]
18063 a1
18064 b1
18065 [EXCERPT]
18066 [FOLDED]
18067 [EXCERPT]
18068 [FOLDED]
18069 "
18070 });
18071 }
18072}
18073
18074#[gpui::test]
18075async fn test_inline_completion_text(cx: &mut TestAppContext) {
18076 init_test(cx, |_| {});
18077
18078 // Simple insertion
18079 assert_highlighted_edits(
18080 "Hello, world!",
18081 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
18082 true,
18083 cx,
18084 |highlighted_edits, cx| {
18085 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
18086 assert_eq!(highlighted_edits.highlights.len(), 1);
18087 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
18088 assert_eq!(
18089 highlighted_edits.highlights[0].1.background_color,
18090 Some(cx.theme().status().created_background)
18091 );
18092 },
18093 )
18094 .await;
18095
18096 // Replacement
18097 assert_highlighted_edits(
18098 "This is a test.",
18099 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
18100 false,
18101 cx,
18102 |highlighted_edits, cx| {
18103 assert_eq!(highlighted_edits.text, "That is a test.");
18104 assert_eq!(highlighted_edits.highlights.len(), 1);
18105 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
18106 assert_eq!(
18107 highlighted_edits.highlights[0].1.background_color,
18108 Some(cx.theme().status().created_background)
18109 );
18110 },
18111 )
18112 .await;
18113
18114 // Multiple edits
18115 assert_highlighted_edits(
18116 "Hello, world!",
18117 vec![
18118 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
18119 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
18120 ],
18121 false,
18122 cx,
18123 |highlighted_edits, cx| {
18124 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
18125 assert_eq!(highlighted_edits.highlights.len(), 2);
18126 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
18127 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
18128 assert_eq!(
18129 highlighted_edits.highlights[0].1.background_color,
18130 Some(cx.theme().status().created_background)
18131 );
18132 assert_eq!(
18133 highlighted_edits.highlights[1].1.background_color,
18134 Some(cx.theme().status().created_background)
18135 );
18136 },
18137 )
18138 .await;
18139
18140 // Multiple lines with edits
18141 assert_highlighted_edits(
18142 "First line\nSecond line\nThird line\nFourth line",
18143 vec![
18144 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
18145 (
18146 Point::new(2, 0)..Point::new(2, 10),
18147 "New third line".to_string(),
18148 ),
18149 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
18150 ],
18151 false,
18152 cx,
18153 |highlighted_edits, cx| {
18154 assert_eq!(
18155 highlighted_edits.text,
18156 "Second modified\nNew third line\nFourth updated line"
18157 );
18158 assert_eq!(highlighted_edits.highlights.len(), 3);
18159 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
18160 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
18161 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
18162 for highlight in &highlighted_edits.highlights {
18163 assert_eq!(
18164 highlight.1.background_color,
18165 Some(cx.theme().status().created_background)
18166 );
18167 }
18168 },
18169 )
18170 .await;
18171}
18172
18173#[gpui::test]
18174async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
18175 init_test(cx, |_| {});
18176
18177 // Deletion
18178 assert_highlighted_edits(
18179 "Hello, world!",
18180 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
18181 true,
18182 cx,
18183 |highlighted_edits, cx| {
18184 assert_eq!(highlighted_edits.text, "Hello, world!");
18185 assert_eq!(highlighted_edits.highlights.len(), 1);
18186 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
18187 assert_eq!(
18188 highlighted_edits.highlights[0].1.background_color,
18189 Some(cx.theme().status().deleted_background)
18190 );
18191 },
18192 )
18193 .await;
18194
18195 // Insertion
18196 assert_highlighted_edits(
18197 "Hello, world!",
18198 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
18199 true,
18200 cx,
18201 |highlighted_edits, cx| {
18202 assert_eq!(highlighted_edits.highlights.len(), 1);
18203 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
18204 assert_eq!(
18205 highlighted_edits.highlights[0].1.background_color,
18206 Some(cx.theme().status().created_background)
18207 );
18208 },
18209 )
18210 .await;
18211}
18212
18213async fn assert_highlighted_edits(
18214 text: &str,
18215 edits: Vec<(Range<Point>, String)>,
18216 include_deletions: bool,
18217 cx: &mut TestAppContext,
18218 assertion_fn: impl Fn(HighlightedText, &App),
18219) {
18220 let window = cx.add_window(|window, cx| {
18221 let buffer = MultiBuffer::build_simple(text, cx);
18222 Editor::new(EditorMode::full(), buffer, None, window, cx)
18223 });
18224 let cx = &mut VisualTestContext::from_window(*window, cx);
18225
18226 let (buffer, snapshot) = window
18227 .update(cx, |editor, _window, cx| {
18228 (
18229 editor.buffer().clone(),
18230 editor.buffer().read(cx).snapshot(cx),
18231 )
18232 })
18233 .unwrap();
18234
18235 let edits = edits
18236 .into_iter()
18237 .map(|(range, edit)| {
18238 (
18239 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
18240 edit,
18241 )
18242 })
18243 .collect::<Vec<_>>();
18244
18245 let text_anchor_edits = edits
18246 .clone()
18247 .into_iter()
18248 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
18249 .collect::<Vec<_>>();
18250
18251 let edit_preview = window
18252 .update(cx, |_, _window, cx| {
18253 buffer
18254 .read(cx)
18255 .as_singleton()
18256 .unwrap()
18257 .read(cx)
18258 .preview_edits(text_anchor_edits.into(), cx)
18259 })
18260 .unwrap()
18261 .await;
18262
18263 cx.update(|_window, cx| {
18264 let highlighted_edits = inline_completion_edit_text(
18265 &snapshot.as_singleton().unwrap().2,
18266 &edits,
18267 &edit_preview,
18268 include_deletions,
18269 cx,
18270 );
18271 assertion_fn(highlighted_edits, cx)
18272 });
18273}
18274
18275#[track_caller]
18276fn assert_breakpoint(
18277 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
18278 path: &Arc<Path>,
18279 expected: Vec<(u32, Breakpoint)>,
18280) {
18281 if expected.len() == 0usize {
18282 assert!(!breakpoints.contains_key(path), "{}", path.display());
18283 } else {
18284 let mut breakpoint = breakpoints
18285 .get(path)
18286 .unwrap()
18287 .into_iter()
18288 .map(|breakpoint| {
18289 (
18290 breakpoint.row,
18291 Breakpoint {
18292 message: breakpoint.message.clone(),
18293 state: breakpoint.state,
18294 condition: breakpoint.condition.clone(),
18295 hit_condition: breakpoint.hit_condition.clone(),
18296 },
18297 )
18298 })
18299 .collect::<Vec<_>>();
18300
18301 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
18302
18303 assert_eq!(expected, breakpoint);
18304 }
18305}
18306
18307fn add_log_breakpoint_at_cursor(
18308 editor: &mut Editor,
18309 log_message: &str,
18310 window: &mut Window,
18311 cx: &mut Context<Editor>,
18312) {
18313 let (anchor, bp) = editor
18314 .breakpoints_at_cursors(window, cx)
18315 .first()
18316 .and_then(|(anchor, bp)| {
18317 if let Some(bp) = bp {
18318 Some((*anchor, bp.clone()))
18319 } else {
18320 None
18321 }
18322 })
18323 .unwrap_or_else(|| {
18324 let cursor_position: Point = editor.selections.newest(cx).head();
18325
18326 let breakpoint_position = editor
18327 .snapshot(window, cx)
18328 .display_snapshot
18329 .buffer_snapshot
18330 .anchor_before(Point::new(cursor_position.row, 0));
18331
18332 (breakpoint_position, Breakpoint::new_log(&log_message))
18333 });
18334
18335 editor.edit_breakpoint_at_anchor(
18336 anchor,
18337 bp,
18338 BreakpointEditAction::EditLogMessage(log_message.into()),
18339 cx,
18340 );
18341}
18342
18343#[gpui::test]
18344async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
18345 init_test(cx, |_| {});
18346
18347 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18348 let fs = FakeFs::new(cx.executor());
18349 fs.insert_tree(
18350 path!("/a"),
18351 json!({
18352 "main.rs": sample_text,
18353 }),
18354 )
18355 .await;
18356 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18357 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18358 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18359
18360 let fs = FakeFs::new(cx.executor());
18361 fs.insert_tree(
18362 path!("/a"),
18363 json!({
18364 "main.rs": sample_text,
18365 }),
18366 )
18367 .await;
18368 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18369 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18370 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18371 let worktree_id = workspace
18372 .update(cx, |workspace, _window, cx| {
18373 workspace.project().update(cx, |project, cx| {
18374 project.worktrees(cx).next().unwrap().read(cx).id()
18375 })
18376 })
18377 .unwrap();
18378
18379 let buffer = project
18380 .update(cx, |project, cx| {
18381 project.open_buffer((worktree_id, "main.rs"), cx)
18382 })
18383 .await
18384 .unwrap();
18385
18386 let (editor, cx) = cx.add_window_view(|window, cx| {
18387 Editor::new(
18388 EditorMode::full(),
18389 MultiBuffer::build_from_buffer(buffer, cx),
18390 Some(project.clone()),
18391 window,
18392 cx,
18393 )
18394 });
18395
18396 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18397 let abs_path = project.read_with(cx, |project, cx| {
18398 project
18399 .absolute_path(&project_path, cx)
18400 .map(|path_buf| Arc::from(path_buf.to_owned()))
18401 .unwrap()
18402 });
18403
18404 // assert we can add breakpoint on the first line
18405 editor.update_in(cx, |editor, window, cx| {
18406 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18407 editor.move_to_end(&MoveToEnd, window, cx);
18408 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18409 });
18410
18411 let breakpoints = editor.update(cx, |editor, cx| {
18412 editor
18413 .breakpoint_store()
18414 .as_ref()
18415 .unwrap()
18416 .read(cx)
18417 .all_breakpoints(cx)
18418 .clone()
18419 });
18420
18421 assert_eq!(1, breakpoints.len());
18422 assert_breakpoint(
18423 &breakpoints,
18424 &abs_path,
18425 vec![
18426 (0, Breakpoint::new_standard()),
18427 (3, Breakpoint::new_standard()),
18428 ],
18429 );
18430
18431 editor.update_in(cx, |editor, window, cx| {
18432 editor.move_to_beginning(&MoveToBeginning, window, cx);
18433 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18434 });
18435
18436 let breakpoints = editor.update(cx, |editor, cx| {
18437 editor
18438 .breakpoint_store()
18439 .as_ref()
18440 .unwrap()
18441 .read(cx)
18442 .all_breakpoints(cx)
18443 .clone()
18444 });
18445
18446 assert_eq!(1, breakpoints.len());
18447 assert_breakpoint(
18448 &breakpoints,
18449 &abs_path,
18450 vec![(3, Breakpoint::new_standard())],
18451 );
18452
18453 editor.update_in(cx, |editor, window, cx| {
18454 editor.move_to_end(&MoveToEnd, window, cx);
18455 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18456 });
18457
18458 let breakpoints = editor.update(cx, |editor, cx| {
18459 editor
18460 .breakpoint_store()
18461 .as_ref()
18462 .unwrap()
18463 .read(cx)
18464 .all_breakpoints(cx)
18465 .clone()
18466 });
18467
18468 assert_eq!(0, breakpoints.len());
18469 assert_breakpoint(&breakpoints, &abs_path, vec![]);
18470}
18471
18472#[gpui::test]
18473async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
18474 init_test(cx, |_| {});
18475
18476 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18477
18478 let fs = FakeFs::new(cx.executor());
18479 fs.insert_tree(
18480 path!("/a"),
18481 json!({
18482 "main.rs": sample_text,
18483 }),
18484 )
18485 .await;
18486 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18487 let (workspace, cx) =
18488 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
18489
18490 let worktree_id = workspace.update(cx, |workspace, cx| {
18491 workspace.project().update(cx, |project, cx| {
18492 project.worktrees(cx).next().unwrap().read(cx).id()
18493 })
18494 });
18495
18496 let buffer = project
18497 .update(cx, |project, cx| {
18498 project.open_buffer((worktree_id, "main.rs"), cx)
18499 })
18500 .await
18501 .unwrap();
18502
18503 let (editor, cx) = cx.add_window_view(|window, cx| {
18504 Editor::new(
18505 EditorMode::full(),
18506 MultiBuffer::build_from_buffer(buffer, cx),
18507 Some(project.clone()),
18508 window,
18509 cx,
18510 )
18511 });
18512
18513 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18514 let abs_path = project.read_with(cx, |project, cx| {
18515 project
18516 .absolute_path(&project_path, cx)
18517 .map(|path_buf| Arc::from(path_buf.to_owned()))
18518 .unwrap()
18519 });
18520
18521 editor.update_in(cx, |editor, window, cx| {
18522 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
18523 });
18524
18525 let breakpoints = editor.update(cx, |editor, cx| {
18526 editor
18527 .breakpoint_store()
18528 .as_ref()
18529 .unwrap()
18530 .read(cx)
18531 .all_breakpoints(cx)
18532 .clone()
18533 });
18534
18535 assert_breakpoint(
18536 &breakpoints,
18537 &abs_path,
18538 vec![(0, Breakpoint::new_log("hello world"))],
18539 );
18540
18541 // Removing a log message from a log breakpoint should remove it
18542 editor.update_in(cx, |editor, window, cx| {
18543 add_log_breakpoint_at_cursor(editor, "", window, cx);
18544 });
18545
18546 let breakpoints = editor.update(cx, |editor, cx| {
18547 editor
18548 .breakpoint_store()
18549 .as_ref()
18550 .unwrap()
18551 .read(cx)
18552 .all_breakpoints(cx)
18553 .clone()
18554 });
18555
18556 assert_breakpoint(&breakpoints, &abs_path, vec![]);
18557
18558 editor.update_in(cx, |editor, window, cx| {
18559 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18560 editor.move_to_end(&MoveToEnd, window, cx);
18561 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18562 // Not adding a log message to a standard breakpoint shouldn't remove it
18563 add_log_breakpoint_at_cursor(editor, "", window, cx);
18564 });
18565
18566 let breakpoints = editor.update(cx, |editor, cx| {
18567 editor
18568 .breakpoint_store()
18569 .as_ref()
18570 .unwrap()
18571 .read(cx)
18572 .all_breakpoints(cx)
18573 .clone()
18574 });
18575
18576 assert_breakpoint(
18577 &breakpoints,
18578 &abs_path,
18579 vec![
18580 (0, Breakpoint::new_standard()),
18581 (3, Breakpoint::new_standard()),
18582 ],
18583 );
18584
18585 editor.update_in(cx, |editor, window, cx| {
18586 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
18587 });
18588
18589 let breakpoints = editor.update(cx, |editor, cx| {
18590 editor
18591 .breakpoint_store()
18592 .as_ref()
18593 .unwrap()
18594 .read(cx)
18595 .all_breakpoints(cx)
18596 .clone()
18597 });
18598
18599 assert_breakpoint(
18600 &breakpoints,
18601 &abs_path,
18602 vec![
18603 (0, Breakpoint::new_standard()),
18604 (3, Breakpoint::new_log("hello world")),
18605 ],
18606 );
18607
18608 editor.update_in(cx, |editor, window, cx| {
18609 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
18610 });
18611
18612 let breakpoints = editor.update(cx, |editor, cx| {
18613 editor
18614 .breakpoint_store()
18615 .as_ref()
18616 .unwrap()
18617 .read(cx)
18618 .all_breakpoints(cx)
18619 .clone()
18620 });
18621
18622 assert_breakpoint(
18623 &breakpoints,
18624 &abs_path,
18625 vec![
18626 (0, Breakpoint::new_standard()),
18627 (3, Breakpoint::new_log("hello Earth!!")),
18628 ],
18629 );
18630}
18631
18632/// This also tests that Editor::breakpoint_at_cursor_head is working properly
18633/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
18634/// or when breakpoints were placed out of order. This tests for a regression too
18635#[gpui::test]
18636async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
18637 init_test(cx, |_| {});
18638
18639 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18640 let fs = FakeFs::new(cx.executor());
18641 fs.insert_tree(
18642 path!("/a"),
18643 json!({
18644 "main.rs": sample_text,
18645 }),
18646 )
18647 .await;
18648 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18649 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18650 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18651
18652 let fs = FakeFs::new(cx.executor());
18653 fs.insert_tree(
18654 path!("/a"),
18655 json!({
18656 "main.rs": sample_text,
18657 }),
18658 )
18659 .await;
18660 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18661 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18662 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18663 let worktree_id = workspace
18664 .update(cx, |workspace, _window, cx| {
18665 workspace.project().update(cx, |project, cx| {
18666 project.worktrees(cx).next().unwrap().read(cx).id()
18667 })
18668 })
18669 .unwrap();
18670
18671 let buffer = project
18672 .update(cx, |project, cx| {
18673 project.open_buffer((worktree_id, "main.rs"), cx)
18674 })
18675 .await
18676 .unwrap();
18677
18678 let (editor, cx) = cx.add_window_view(|window, cx| {
18679 Editor::new(
18680 EditorMode::full(),
18681 MultiBuffer::build_from_buffer(buffer, cx),
18682 Some(project.clone()),
18683 window,
18684 cx,
18685 )
18686 });
18687
18688 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18689 let abs_path = project.read_with(cx, |project, cx| {
18690 project
18691 .absolute_path(&project_path, cx)
18692 .map(|path_buf| Arc::from(path_buf.to_owned()))
18693 .unwrap()
18694 });
18695
18696 // assert we can add breakpoint on the first line
18697 editor.update_in(cx, |editor, window, cx| {
18698 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18699 editor.move_to_end(&MoveToEnd, window, cx);
18700 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18701 editor.move_up(&MoveUp, window, cx);
18702 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18703 });
18704
18705 let breakpoints = editor.update(cx, |editor, cx| {
18706 editor
18707 .breakpoint_store()
18708 .as_ref()
18709 .unwrap()
18710 .read(cx)
18711 .all_breakpoints(cx)
18712 .clone()
18713 });
18714
18715 assert_eq!(1, breakpoints.len());
18716 assert_breakpoint(
18717 &breakpoints,
18718 &abs_path,
18719 vec![
18720 (0, Breakpoint::new_standard()),
18721 (2, Breakpoint::new_standard()),
18722 (3, Breakpoint::new_standard()),
18723 ],
18724 );
18725
18726 editor.update_in(cx, |editor, window, cx| {
18727 editor.move_to_beginning(&MoveToBeginning, window, cx);
18728 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18729 editor.move_to_end(&MoveToEnd, window, cx);
18730 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18731 // Disabling a breakpoint that doesn't exist should do nothing
18732 editor.move_up(&MoveUp, window, cx);
18733 editor.move_up(&MoveUp, window, cx);
18734 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18735 });
18736
18737 let breakpoints = editor.update(cx, |editor, cx| {
18738 editor
18739 .breakpoint_store()
18740 .as_ref()
18741 .unwrap()
18742 .read(cx)
18743 .all_breakpoints(cx)
18744 .clone()
18745 });
18746
18747 let disable_breakpoint = {
18748 let mut bp = Breakpoint::new_standard();
18749 bp.state = BreakpointState::Disabled;
18750 bp
18751 };
18752
18753 assert_eq!(1, breakpoints.len());
18754 assert_breakpoint(
18755 &breakpoints,
18756 &abs_path,
18757 vec![
18758 (0, disable_breakpoint.clone()),
18759 (2, Breakpoint::new_standard()),
18760 (3, disable_breakpoint.clone()),
18761 ],
18762 );
18763
18764 editor.update_in(cx, |editor, window, cx| {
18765 editor.move_to_beginning(&MoveToBeginning, window, cx);
18766 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
18767 editor.move_to_end(&MoveToEnd, window, cx);
18768 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
18769 editor.move_up(&MoveUp, window, cx);
18770 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18771 });
18772
18773 let breakpoints = editor.update(cx, |editor, cx| {
18774 editor
18775 .breakpoint_store()
18776 .as_ref()
18777 .unwrap()
18778 .read(cx)
18779 .all_breakpoints(cx)
18780 .clone()
18781 });
18782
18783 assert_eq!(1, breakpoints.len());
18784 assert_breakpoint(
18785 &breakpoints,
18786 &abs_path,
18787 vec![
18788 (0, Breakpoint::new_standard()),
18789 (2, disable_breakpoint),
18790 (3, Breakpoint::new_standard()),
18791 ],
18792 );
18793}
18794
18795#[gpui::test]
18796async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
18797 init_test(cx, |_| {});
18798 let capabilities = lsp::ServerCapabilities {
18799 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
18800 prepare_provider: Some(true),
18801 work_done_progress_options: Default::default(),
18802 })),
18803 ..Default::default()
18804 };
18805 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
18806
18807 cx.set_state(indoc! {"
18808 struct Fˇoo {}
18809 "});
18810
18811 cx.update_editor(|editor, _, cx| {
18812 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
18813 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
18814 editor.highlight_background::<DocumentHighlightRead>(
18815 &[highlight_range],
18816 |c| c.editor_document_highlight_read_background,
18817 cx,
18818 );
18819 });
18820
18821 let mut prepare_rename_handler = cx
18822 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
18823 move |_, _, _| async move {
18824 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
18825 start: lsp::Position {
18826 line: 0,
18827 character: 7,
18828 },
18829 end: lsp::Position {
18830 line: 0,
18831 character: 10,
18832 },
18833 })))
18834 },
18835 );
18836 let prepare_rename_task = cx
18837 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
18838 .expect("Prepare rename was not started");
18839 prepare_rename_handler.next().await.unwrap();
18840 prepare_rename_task.await.expect("Prepare rename failed");
18841
18842 let mut rename_handler =
18843 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
18844 let edit = lsp::TextEdit {
18845 range: lsp::Range {
18846 start: lsp::Position {
18847 line: 0,
18848 character: 7,
18849 },
18850 end: lsp::Position {
18851 line: 0,
18852 character: 10,
18853 },
18854 },
18855 new_text: "FooRenamed".to_string(),
18856 };
18857 Ok(Some(lsp::WorkspaceEdit::new(
18858 // Specify the same edit twice
18859 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
18860 )))
18861 });
18862 let rename_task = cx
18863 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
18864 .expect("Confirm rename was not started");
18865 rename_handler.next().await.unwrap();
18866 rename_task.await.expect("Confirm rename failed");
18867 cx.run_until_parked();
18868
18869 // Despite two edits, only one is actually applied as those are identical
18870 cx.assert_editor_state(indoc! {"
18871 struct FooRenamedˇ {}
18872 "});
18873}
18874
18875#[gpui::test]
18876async fn test_rename_without_prepare(cx: &mut TestAppContext) {
18877 init_test(cx, |_| {});
18878 // These capabilities indicate that the server does not support prepare rename.
18879 let capabilities = lsp::ServerCapabilities {
18880 rename_provider: Some(lsp::OneOf::Left(true)),
18881 ..Default::default()
18882 };
18883 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
18884
18885 cx.set_state(indoc! {"
18886 struct Fˇoo {}
18887 "});
18888
18889 cx.update_editor(|editor, _window, cx| {
18890 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
18891 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
18892 editor.highlight_background::<DocumentHighlightRead>(
18893 &[highlight_range],
18894 |c| c.editor_document_highlight_read_background,
18895 cx,
18896 );
18897 });
18898
18899 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
18900 .expect("Prepare rename was not started")
18901 .await
18902 .expect("Prepare rename failed");
18903
18904 let mut rename_handler =
18905 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
18906 let edit = lsp::TextEdit {
18907 range: lsp::Range {
18908 start: lsp::Position {
18909 line: 0,
18910 character: 7,
18911 },
18912 end: lsp::Position {
18913 line: 0,
18914 character: 10,
18915 },
18916 },
18917 new_text: "FooRenamed".to_string(),
18918 };
18919 Ok(Some(lsp::WorkspaceEdit::new(
18920 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
18921 )))
18922 });
18923 let rename_task = cx
18924 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
18925 .expect("Confirm rename was not started");
18926 rename_handler.next().await.unwrap();
18927 rename_task.await.expect("Confirm rename failed");
18928 cx.run_until_parked();
18929
18930 // Correct range is renamed, as `surrounding_word` is used to find it.
18931 cx.assert_editor_state(indoc! {"
18932 struct FooRenamedˇ {}
18933 "});
18934}
18935
18936#[gpui::test]
18937async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
18938 init_test(cx, |_| {});
18939 let mut cx = EditorTestContext::new(cx).await;
18940
18941 let language = Arc::new(
18942 Language::new(
18943 LanguageConfig::default(),
18944 Some(tree_sitter_html::LANGUAGE.into()),
18945 )
18946 .with_brackets_query(
18947 r#"
18948 ("<" @open "/>" @close)
18949 ("</" @open ">" @close)
18950 ("<" @open ">" @close)
18951 ("\"" @open "\"" @close)
18952 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
18953 "#,
18954 )
18955 .unwrap(),
18956 );
18957 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
18958
18959 cx.set_state(indoc! {"
18960 <span>ˇ</span>
18961 "});
18962 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18963 cx.assert_editor_state(indoc! {"
18964 <span>
18965 ˇ
18966 </span>
18967 "});
18968
18969 cx.set_state(indoc! {"
18970 <span><span></span>ˇ</span>
18971 "});
18972 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18973 cx.assert_editor_state(indoc! {"
18974 <span><span></span>
18975 ˇ</span>
18976 "});
18977
18978 cx.set_state(indoc! {"
18979 <span>ˇ
18980 </span>
18981 "});
18982 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18983 cx.assert_editor_state(indoc! {"
18984 <span>
18985 ˇ
18986 </span>
18987 "});
18988}
18989
18990#[gpui::test(iterations = 10)]
18991async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
18992 init_test(cx, |_| {});
18993
18994 let fs = FakeFs::new(cx.executor());
18995 fs.insert_tree(
18996 path!("/dir"),
18997 json!({
18998 "a.ts": "a",
18999 }),
19000 )
19001 .await;
19002
19003 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
19004 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19005 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19006
19007 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19008 language_registry.add(Arc::new(Language::new(
19009 LanguageConfig {
19010 name: "TypeScript".into(),
19011 matcher: LanguageMatcher {
19012 path_suffixes: vec!["ts".to_string()],
19013 ..Default::default()
19014 },
19015 ..Default::default()
19016 },
19017 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19018 )));
19019 let mut fake_language_servers = language_registry.register_fake_lsp(
19020 "TypeScript",
19021 FakeLspAdapter {
19022 capabilities: lsp::ServerCapabilities {
19023 code_lens_provider: Some(lsp::CodeLensOptions {
19024 resolve_provider: Some(true),
19025 }),
19026 execute_command_provider: Some(lsp::ExecuteCommandOptions {
19027 commands: vec!["_the/command".to_string()],
19028 ..lsp::ExecuteCommandOptions::default()
19029 }),
19030 ..lsp::ServerCapabilities::default()
19031 },
19032 ..FakeLspAdapter::default()
19033 },
19034 );
19035
19036 let (buffer, _handle) = project
19037 .update(cx, |p, cx| {
19038 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
19039 })
19040 .await
19041 .unwrap();
19042 cx.executor().run_until_parked();
19043
19044 let fake_server = fake_language_servers.next().await.unwrap();
19045
19046 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
19047 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
19048 drop(buffer_snapshot);
19049 let actions = cx
19050 .update_window(*workspace, |_, window, cx| {
19051 project.code_actions(&buffer, anchor..anchor, window, cx)
19052 })
19053 .unwrap();
19054
19055 fake_server
19056 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
19057 Ok(Some(vec![
19058 lsp::CodeLens {
19059 range: lsp::Range::default(),
19060 command: Some(lsp::Command {
19061 title: "Code lens command".to_owned(),
19062 command: "_the/command".to_owned(),
19063 arguments: None,
19064 }),
19065 data: None,
19066 },
19067 lsp::CodeLens {
19068 range: lsp::Range::default(),
19069 command: Some(lsp::Command {
19070 title: "Command not in capabilities".to_owned(),
19071 command: "not in capabilities".to_owned(),
19072 arguments: None,
19073 }),
19074 data: None,
19075 },
19076 lsp::CodeLens {
19077 range: lsp::Range {
19078 start: lsp::Position {
19079 line: 1,
19080 character: 1,
19081 },
19082 end: lsp::Position {
19083 line: 1,
19084 character: 1,
19085 },
19086 },
19087 command: Some(lsp::Command {
19088 title: "Command not in range".to_owned(),
19089 command: "_the/command".to_owned(),
19090 arguments: None,
19091 }),
19092 data: None,
19093 },
19094 ]))
19095 })
19096 .next()
19097 .await;
19098
19099 let actions = actions.await.unwrap();
19100 assert_eq!(
19101 actions.len(),
19102 1,
19103 "Should have only one valid action for the 0..0 range"
19104 );
19105 let action = actions[0].clone();
19106 let apply = project.update(cx, |project, cx| {
19107 project.apply_code_action(buffer.clone(), action, true, cx)
19108 });
19109
19110 // Resolving the code action does not populate its edits. In absence of
19111 // edits, we must execute the given command.
19112 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
19113 |mut lens, _| async move {
19114 let lens_command = lens.command.as_mut().expect("should have a command");
19115 assert_eq!(lens_command.title, "Code lens command");
19116 lens_command.arguments = Some(vec![json!("the-argument")]);
19117 Ok(lens)
19118 },
19119 );
19120
19121 // While executing the command, the language server sends the editor
19122 // a `workspaceEdit` request.
19123 fake_server
19124 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
19125 let fake = fake_server.clone();
19126 move |params, _| {
19127 assert_eq!(params.command, "_the/command");
19128 let fake = fake.clone();
19129 async move {
19130 fake.server
19131 .request::<lsp::request::ApplyWorkspaceEdit>(
19132 lsp::ApplyWorkspaceEditParams {
19133 label: None,
19134 edit: lsp::WorkspaceEdit {
19135 changes: Some(
19136 [(
19137 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
19138 vec![lsp::TextEdit {
19139 range: lsp::Range::new(
19140 lsp::Position::new(0, 0),
19141 lsp::Position::new(0, 0),
19142 ),
19143 new_text: "X".into(),
19144 }],
19145 )]
19146 .into_iter()
19147 .collect(),
19148 ),
19149 ..Default::default()
19150 },
19151 },
19152 )
19153 .await
19154 .unwrap();
19155 Ok(Some(json!(null)))
19156 }
19157 }
19158 })
19159 .next()
19160 .await;
19161
19162 // Applying the code lens command returns a project transaction containing the edits
19163 // sent by the language server in its `workspaceEdit` request.
19164 let transaction = apply.await.unwrap();
19165 assert!(transaction.0.contains_key(&buffer));
19166 buffer.update(cx, |buffer, cx| {
19167 assert_eq!(buffer.text(), "Xa");
19168 buffer.undo(cx);
19169 assert_eq!(buffer.text(), "a");
19170 });
19171}
19172
19173#[gpui::test]
19174async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
19175 init_test(cx, |_| {});
19176
19177 let fs = FakeFs::new(cx.executor());
19178 let main_text = r#"fn main() {
19179println!("1");
19180println!("2");
19181println!("3");
19182println!("4");
19183println!("5");
19184}"#;
19185 let lib_text = "mod foo {}";
19186 fs.insert_tree(
19187 path!("/a"),
19188 json!({
19189 "lib.rs": lib_text,
19190 "main.rs": main_text,
19191 }),
19192 )
19193 .await;
19194
19195 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19196 let (workspace, cx) =
19197 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19198 let worktree_id = workspace.update(cx, |workspace, cx| {
19199 workspace.project().update(cx, |project, cx| {
19200 project.worktrees(cx).next().unwrap().read(cx).id()
19201 })
19202 });
19203
19204 let expected_ranges = vec![
19205 Point::new(0, 0)..Point::new(0, 0),
19206 Point::new(1, 0)..Point::new(1, 1),
19207 Point::new(2, 0)..Point::new(2, 2),
19208 Point::new(3, 0)..Point::new(3, 3),
19209 ];
19210
19211 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
19212 let editor_1 = workspace
19213 .update_in(cx, |workspace, window, cx| {
19214 workspace.open_path(
19215 (worktree_id, "main.rs"),
19216 Some(pane_1.downgrade()),
19217 true,
19218 window,
19219 cx,
19220 )
19221 })
19222 .unwrap()
19223 .await
19224 .downcast::<Editor>()
19225 .unwrap();
19226 pane_1.update(cx, |pane, cx| {
19227 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19228 open_editor.update(cx, |editor, cx| {
19229 assert_eq!(
19230 editor.display_text(cx),
19231 main_text,
19232 "Original main.rs text on initial open",
19233 );
19234 assert_eq!(
19235 editor
19236 .selections
19237 .all::<Point>(cx)
19238 .into_iter()
19239 .map(|s| s.range())
19240 .collect::<Vec<_>>(),
19241 vec![Point::zero()..Point::zero()],
19242 "Default selections on initial open",
19243 );
19244 })
19245 });
19246 editor_1.update_in(cx, |editor, window, cx| {
19247 editor.change_selections(None, window, cx, |s| {
19248 s.select_ranges(expected_ranges.clone());
19249 });
19250 });
19251
19252 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
19253 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
19254 });
19255 let editor_2 = workspace
19256 .update_in(cx, |workspace, window, cx| {
19257 workspace.open_path(
19258 (worktree_id, "main.rs"),
19259 Some(pane_2.downgrade()),
19260 true,
19261 window,
19262 cx,
19263 )
19264 })
19265 .unwrap()
19266 .await
19267 .downcast::<Editor>()
19268 .unwrap();
19269 pane_2.update(cx, |pane, cx| {
19270 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19271 open_editor.update(cx, |editor, cx| {
19272 assert_eq!(
19273 editor.display_text(cx),
19274 main_text,
19275 "Original main.rs text on initial open in another panel",
19276 );
19277 assert_eq!(
19278 editor
19279 .selections
19280 .all::<Point>(cx)
19281 .into_iter()
19282 .map(|s| s.range())
19283 .collect::<Vec<_>>(),
19284 vec![Point::zero()..Point::zero()],
19285 "Default selections on initial open in another panel",
19286 );
19287 })
19288 });
19289
19290 editor_2.update_in(cx, |editor, window, cx| {
19291 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
19292 });
19293
19294 let _other_editor_1 = workspace
19295 .update_in(cx, |workspace, window, cx| {
19296 workspace.open_path(
19297 (worktree_id, "lib.rs"),
19298 Some(pane_1.downgrade()),
19299 true,
19300 window,
19301 cx,
19302 )
19303 })
19304 .unwrap()
19305 .await
19306 .downcast::<Editor>()
19307 .unwrap();
19308 pane_1
19309 .update_in(cx, |pane, window, cx| {
19310 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
19311 .unwrap()
19312 })
19313 .await
19314 .unwrap();
19315 drop(editor_1);
19316 pane_1.update(cx, |pane, cx| {
19317 pane.active_item()
19318 .unwrap()
19319 .downcast::<Editor>()
19320 .unwrap()
19321 .update(cx, |editor, cx| {
19322 assert_eq!(
19323 editor.display_text(cx),
19324 lib_text,
19325 "Other file should be open and active",
19326 );
19327 });
19328 assert_eq!(pane.items().count(), 1, "No other editors should be open");
19329 });
19330
19331 let _other_editor_2 = workspace
19332 .update_in(cx, |workspace, window, cx| {
19333 workspace.open_path(
19334 (worktree_id, "lib.rs"),
19335 Some(pane_2.downgrade()),
19336 true,
19337 window,
19338 cx,
19339 )
19340 })
19341 .unwrap()
19342 .await
19343 .downcast::<Editor>()
19344 .unwrap();
19345 pane_2
19346 .update_in(cx, |pane, window, cx| {
19347 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
19348 .unwrap()
19349 })
19350 .await
19351 .unwrap();
19352 drop(editor_2);
19353 pane_2.update(cx, |pane, cx| {
19354 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19355 open_editor.update(cx, |editor, cx| {
19356 assert_eq!(
19357 editor.display_text(cx),
19358 lib_text,
19359 "Other file should be open and active in another panel too",
19360 );
19361 });
19362 assert_eq!(
19363 pane.items().count(),
19364 1,
19365 "No other editors should be open in another pane",
19366 );
19367 });
19368
19369 let _editor_1_reopened = workspace
19370 .update_in(cx, |workspace, window, cx| {
19371 workspace.open_path(
19372 (worktree_id, "main.rs"),
19373 Some(pane_1.downgrade()),
19374 true,
19375 window,
19376 cx,
19377 )
19378 })
19379 .unwrap()
19380 .await
19381 .downcast::<Editor>()
19382 .unwrap();
19383 let _editor_2_reopened = workspace
19384 .update_in(cx, |workspace, window, cx| {
19385 workspace.open_path(
19386 (worktree_id, "main.rs"),
19387 Some(pane_2.downgrade()),
19388 true,
19389 window,
19390 cx,
19391 )
19392 })
19393 .unwrap()
19394 .await
19395 .downcast::<Editor>()
19396 .unwrap();
19397 pane_1.update(cx, |pane, cx| {
19398 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19399 open_editor.update(cx, |editor, cx| {
19400 assert_eq!(
19401 editor.display_text(cx),
19402 main_text,
19403 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
19404 );
19405 assert_eq!(
19406 editor
19407 .selections
19408 .all::<Point>(cx)
19409 .into_iter()
19410 .map(|s| s.range())
19411 .collect::<Vec<_>>(),
19412 expected_ranges,
19413 "Previous editor in the 1st panel had selections and should get them restored on reopen",
19414 );
19415 })
19416 });
19417 pane_2.update(cx, |pane, cx| {
19418 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19419 open_editor.update(cx, |editor, cx| {
19420 assert_eq!(
19421 editor.display_text(cx),
19422 r#"fn main() {
19423⋯rintln!("1");
19424⋯intln!("2");
19425⋯ntln!("3");
19426println!("4");
19427println!("5");
19428}"#,
19429 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
19430 );
19431 assert_eq!(
19432 editor
19433 .selections
19434 .all::<Point>(cx)
19435 .into_iter()
19436 .map(|s| s.range())
19437 .collect::<Vec<_>>(),
19438 vec![Point::zero()..Point::zero()],
19439 "Previous editor in the 2nd pane had no selections changed hence should restore none",
19440 );
19441 })
19442 });
19443}
19444
19445#[gpui::test]
19446async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
19447 init_test(cx, |_| {});
19448
19449 let fs = FakeFs::new(cx.executor());
19450 let main_text = r#"fn main() {
19451println!("1");
19452println!("2");
19453println!("3");
19454println!("4");
19455println!("5");
19456}"#;
19457 let lib_text = "mod foo {}";
19458 fs.insert_tree(
19459 path!("/a"),
19460 json!({
19461 "lib.rs": lib_text,
19462 "main.rs": main_text,
19463 }),
19464 )
19465 .await;
19466
19467 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19468 let (workspace, cx) =
19469 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19470 let worktree_id = workspace.update(cx, |workspace, cx| {
19471 workspace.project().update(cx, |project, cx| {
19472 project.worktrees(cx).next().unwrap().read(cx).id()
19473 })
19474 });
19475
19476 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
19477 let editor = workspace
19478 .update_in(cx, |workspace, window, cx| {
19479 workspace.open_path(
19480 (worktree_id, "main.rs"),
19481 Some(pane.downgrade()),
19482 true,
19483 window,
19484 cx,
19485 )
19486 })
19487 .unwrap()
19488 .await
19489 .downcast::<Editor>()
19490 .unwrap();
19491 pane.update(cx, |pane, cx| {
19492 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19493 open_editor.update(cx, |editor, cx| {
19494 assert_eq!(
19495 editor.display_text(cx),
19496 main_text,
19497 "Original main.rs text on initial open",
19498 );
19499 })
19500 });
19501 editor.update_in(cx, |editor, window, cx| {
19502 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
19503 });
19504
19505 cx.update_global(|store: &mut SettingsStore, cx| {
19506 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
19507 s.restore_on_file_reopen = Some(false);
19508 });
19509 });
19510 editor.update_in(cx, |editor, window, cx| {
19511 editor.fold_ranges(
19512 vec![
19513 Point::new(1, 0)..Point::new(1, 1),
19514 Point::new(2, 0)..Point::new(2, 2),
19515 Point::new(3, 0)..Point::new(3, 3),
19516 ],
19517 false,
19518 window,
19519 cx,
19520 );
19521 });
19522 pane.update_in(cx, |pane, window, cx| {
19523 pane.close_all_items(&CloseAllItems::default(), window, cx)
19524 .unwrap()
19525 })
19526 .await
19527 .unwrap();
19528 pane.update(cx, |pane, _| {
19529 assert!(pane.active_item().is_none());
19530 });
19531 cx.update_global(|store: &mut SettingsStore, cx| {
19532 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
19533 s.restore_on_file_reopen = Some(true);
19534 });
19535 });
19536
19537 let _editor_reopened = workspace
19538 .update_in(cx, |workspace, window, cx| {
19539 workspace.open_path(
19540 (worktree_id, "main.rs"),
19541 Some(pane.downgrade()),
19542 true,
19543 window,
19544 cx,
19545 )
19546 })
19547 .unwrap()
19548 .await
19549 .downcast::<Editor>()
19550 .unwrap();
19551 pane.update(cx, |pane, cx| {
19552 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19553 open_editor.update(cx, |editor, cx| {
19554 assert_eq!(
19555 editor.display_text(cx),
19556 main_text,
19557 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
19558 );
19559 })
19560 });
19561}
19562
19563#[gpui::test]
19564async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
19565 struct EmptyModalView {
19566 focus_handle: gpui::FocusHandle,
19567 }
19568 impl EventEmitter<DismissEvent> for EmptyModalView {}
19569 impl Render for EmptyModalView {
19570 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
19571 div()
19572 }
19573 }
19574 impl Focusable for EmptyModalView {
19575 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
19576 self.focus_handle.clone()
19577 }
19578 }
19579 impl workspace::ModalView for EmptyModalView {}
19580 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
19581 EmptyModalView {
19582 focus_handle: cx.focus_handle(),
19583 }
19584 }
19585
19586 init_test(cx, |_| {});
19587
19588 let fs = FakeFs::new(cx.executor());
19589 let project = Project::test(fs, [], cx).await;
19590 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19591 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
19592 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19593 let editor = cx.new_window_entity(|window, cx| {
19594 Editor::new(
19595 EditorMode::full(),
19596 buffer,
19597 Some(project.clone()),
19598 window,
19599 cx,
19600 )
19601 });
19602 workspace
19603 .update(cx, |workspace, window, cx| {
19604 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
19605 })
19606 .unwrap();
19607 editor.update_in(cx, |editor, window, cx| {
19608 editor.open_context_menu(&OpenContextMenu, window, cx);
19609 assert!(editor.mouse_context_menu.is_some());
19610 });
19611 workspace
19612 .update(cx, |workspace, window, cx| {
19613 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
19614 })
19615 .unwrap();
19616 cx.read(|cx| {
19617 assert!(editor.read(cx).mouse_context_menu.is_none());
19618 });
19619}
19620
19621#[gpui::test]
19622async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
19623 init_test(cx, |_| {});
19624
19625 let fs = FakeFs::new(cx.executor());
19626 fs.insert_file(path!("/file.html"), Default::default())
19627 .await;
19628
19629 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
19630
19631 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19632 let html_language = Arc::new(Language::new(
19633 LanguageConfig {
19634 name: "HTML".into(),
19635 matcher: LanguageMatcher {
19636 path_suffixes: vec!["html".to_string()],
19637 ..LanguageMatcher::default()
19638 },
19639 brackets: BracketPairConfig {
19640 pairs: vec![BracketPair {
19641 start: "<".into(),
19642 end: ">".into(),
19643 close: true,
19644 ..Default::default()
19645 }],
19646 ..Default::default()
19647 },
19648 ..Default::default()
19649 },
19650 Some(tree_sitter_html::LANGUAGE.into()),
19651 ));
19652 language_registry.add(html_language);
19653 let mut fake_servers = language_registry.register_fake_lsp(
19654 "HTML",
19655 FakeLspAdapter {
19656 capabilities: lsp::ServerCapabilities {
19657 completion_provider: Some(lsp::CompletionOptions {
19658 resolve_provider: Some(true),
19659 ..Default::default()
19660 }),
19661 ..Default::default()
19662 },
19663 ..Default::default()
19664 },
19665 );
19666
19667 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19668 let cx = &mut VisualTestContext::from_window(*workspace, cx);
19669
19670 let worktree_id = workspace
19671 .update(cx, |workspace, _window, cx| {
19672 workspace.project().update(cx, |project, cx| {
19673 project.worktrees(cx).next().unwrap().read(cx).id()
19674 })
19675 })
19676 .unwrap();
19677 project
19678 .update(cx, |project, cx| {
19679 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
19680 })
19681 .await
19682 .unwrap();
19683 let editor = workspace
19684 .update(cx, |workspace, window, cx| {
19685 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
19686 })
19687 .unwrap()
19688 .await
19689 .unwrap()
19690 .downcast::<Editor>()
19691 .unwrap();
19692
19693 let fake_server = fake_servers.next().await.unwrap();
19694 editor.update_in(cx, |editor, window, cx| {
19695 editor.set_text("<ad></ad>", window, cx);
19696 editor.change_selections(None, window, cx, |selections| {
19697 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
19698 });
19699 let Some((buffer, _)) = editor
19700 .buffer
19701 .read(cx)
19702 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
19703 else {
19704 panic!("Failed to get buffer for selection position");
19705 };
19706 let buffer = buffer.read(cx);
19707 let buffer_id = buffer.remote_id();
19708 let opening_range =
19709 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
19710 let closing_range =
19711 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
19712 let mut linked_ranges = HashMap::default();
19713 linked_ranges.insert(
19714 buffer_id,
19715 vec![(opening_range.clone(), vec![closing_range.clone()])],
19716 );
19717 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
19718 });
19719 let mut completion_handle =
19720 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
19721 Ok(Some(lsp::CompletionResponse::Array(vec![
19722 lsp::CompletionItem {
19723 label: "head".to_string(),
19724 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
19725 lsp::InsertReplaceEdit {
19726 new_text: "head".to_string(),
19727 insert: lsp::Range::new(
19728 lsp::Position::new(0, 1),
19729 lsp::Position::new(0, 3),
19730 ),
19731 replace: lsp::Range::new(
19732 lsp::Position::new(0, 1),
19733 lsp::Position::new(0, 3),
19734 ),
19735 },
19736 )),
19737 ..Default::default()
19738 },
19739 ])))
19740 });
19741 editor.update_in(cx, |editor, window, cx| {
19742 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
19743 });
19744 cx.run_until_parked();
19745 completion_handle.next().await.unwrap();
19746 editor.update(cx, |editor, _| {
19747 assert!(
19748 editor.context_menu_visible(),
19749 "Completion menu should be visible"
19750 );
19751 });
19752 editor.update_in(cx, |editor, window, cx| {
19753 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
19754 });
19755 cx.executor().run_until_parked();
19756 editor.update(cx, |editor, cx| {
19757 assert_eq!(editor.text(cx), "<head></head>");
19758 });
19759}
19760
19761fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
19762 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
19763 point..point
19764}
19765
19766fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
19767 let (text, ranges) = marked_text_ranges(marked_text, true);
19768 assert_eq!(editor.text(cx), text);
19769 assert_eq!(
19770 editor.selections.ranges(cx),
19771 ranges,
19772 "Assert selections are {}",
19773 marked_text
19774 );
19775}
19776
19777pub fn handle_signature_help_request(
19778 cx: &mut EditorLspTestContext,
19779 mocked_response: lsp::SignatureHelp,
19780) -> impl Future<Output = ()> + use<> {
19781 let mut request =
19782 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
19783 let mocked_response = mocked_response.clone();
19784 async move { Ok(Some(mocked_response)) }
19785 });
19786
19787 async move {
19788 request.next().await;
19789 }
19790}
19791
19792/// Handle completion request passing a marked string specifying where the completion
19793/// should be triggered from using '|' character, what range should be replaced, and what completions
19794/// should be returned using '<' and '>' to delimit the range.
19795///
19796/// Also see `handle_completion_request_with_insert_and_replace`.
19797#[track_caller]
19798pub fn handle_completion_request(
19799 cx: &mut EditorLspTestContext,
19800 marked_string: &str,
19801 completions: Vec<&'static str>,
19802 counter: Arc<AtomicUsize>,
19803) -> impl Future<Output = ()> {
19804 let complete_from_marker: TextRangeMarker = '|'.into();
19805 let replace_range_marker: TextRangeMarker = ('<', '>').into();
19806 let (_, mut marked_ranges) = marked_text_ranges_by(
19807 marked_string,
19808 vec![complete_from_marker.clone(), replace_range_marker.clone()],
19809 );
19810
19811 let complete_from_position =
19812 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
19813 let replace_range =
19814 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
19815
19816 let mut request =
19817 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
19818 let completions = completions.clone();
19819 counter.fetch_add(1, atomic::Ordering::Release);
19820 async move {
19821 assert_eq!(params.text_document_position.text_document.uri, url.clone());
19822 assert_eq!(
19823 params.text_document_position.position,
19824 complete_from_position
19825 );
19826 Ok(Some(lsp::CompletionResponse::Array(
19827 completions
19828 .iter()
19829 .map(|completion_text| lsp::CompletionItem {
19830 label: completion_text.to_string(),
19831 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
19832 range: replace_range,
19833 new_text: completion_text.to_string(),
19834 })),
19835 ..Default::default()
19836 })
19837 .collect(),
19838 )))
19839 }
19840 });
19841
19842 async move {
19843 request.next().await;
19844 }
19845}
19846
19847/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
19848/// given instead, which also contains an `insert` range.
19849///
19850/// This function uses the cursor position to mimic what Rust-Analyzer provides as the `insert` range,
19851/// that is, `replace_range.start..cursor_pos`.
19852pub fn handle_completion_request_with_insert_and_replace(
19853 cx: &mut EditorLspTestContext,
19854 marked_string: &str,
19855 completions: Vec<&'static str>,
19856 counter: Arc<AtomicUsize>,
19857) -> impl Future<Output = ()> {
19858 let complete_from_marker: TextRangeMarker = '|'.into();
19859 let replace_range_marker: TextRangeMarker = ('<', '>').into();
19860 let (_, mut marked_ranges) = marked_text_ranges_by(
19861 marked_string,
19862 vec![complete_from_marker.clone(), replace_range_marker.clone()],
19863 );
19864
19865 let complete_from_position =
19866 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
19867 let replace_range =
19868 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
19869
19870 let mut request =
19871 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
19872 let completions = completions.clone();
19873 counter.fetch_add(1, atomic::Ordering::Release);
19874 async move {
19875 assert_eq!(params.text_document_position.text_document.uri, url.clone());
19876 assert_eq!(
19877 params.text_document_position.position, complete_from_position,
19878 "marker `|` position doesn't match",
19879 );
19880 Ok(Some(lsp::CompletionResponse::Array(
19881 completions
19882 .iter()
19883 .map(|completion_text| lsp::CompletionItem {
19884 label: completion_text.to_string(),
19885 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
19886 lsp::InsertReplaceEdit {
19887 insert: lsp::Range {
19888 start: replace_range.start,
19889 end: complete_from_position,
19890 },
19891 replace: replace_range,
19892 new_text: completion_text.to_string(),
19893 },
19894 )),
19895 ..Default::default()
19896 })
19897 .collect(),
19898 )))
19899 }
19900 });
19901
19902 async move {
19903 request.next().await;
19904 }
19905}
19906
19907fn handle_resolve_completion_request(
19908 cx: &mut EditorLspTestContext,
19909 edits: Option<Vec<(&'static str, &'static str)>>,
19910) -> impl Future<Output = ()> {
19911 let edits = edits.map(|edits| {
19912 edits
19913 .iter()
19914 .map(|(marked_string, new_text)| {
19915 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
19916 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
19917 lsp::TextEdit::new(replace_range, new_text.to_string())
19918 })
19919 .collect::<Vec<_>>()
19920 });
19921
19922 let mut request =
19923 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
19924 let edits = edits.clone();
19925 async move {
19926 Ok(lsp::CompletionItem {
19927 additional_text_edits: edits,
19928 ..Default::default()
19929 })
19930 }
19931 });
19932
19933 async move {
19934 request.next().await;
19935 }
19936}
19937
19938pub(crate) fn update_test_language_settings(
19939 cx: &mut TestAppContext,
19940 f: impl Fn(&mut AllLanguageSettingsContent),
19941) {
19942 cx.update(|cx| {
19943 SettingsStore::update_global(cx, |store, cx| {
19944 store.update_user_settings::<AllLanguageSettings>(cx, f);
19945 });
19946 });
19947}
19948
19949pub(crate) fn update_test_project_settings(
19950 cx: &mut TestAppContext,
19951 f: impl Fn(&mut ProjectSettings),
19952) {
19953 cx.update(|cx| {
19954 SettingsStore::update_global(cx, |store, cx| {
19955 store.update_user_settings::<ProjectSettings>(cx, f);
19956 });
19957 });
19958}
19959
19960pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
19961 cx.update(|cx| {
19962 assets::Assets.load_test_fonts(cx);
19963 let store = SettingsStore::test(cx);
19964 cx.set_global(store);
19965 theme::init(theme::LoadThemes::JustBase, cx);
19966 release_channel::init(SemanticVersion::default(), cx);
19967 client::init_settings(cx);
19968 language::init(cx);
19969 Project::init_settings(cx);
19970 workspace::init_settings(cx);
19971 crate::init(cx);
19972 });
19973
19974 update_test_language_settings(cx, f);
19975}
19976
19977#[track_caller]
19978fn assert_hunk_revert(
19979 not_reverted_text_with_selections: &str,
19980 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
19981 expected_reverted_text_with_selections: &str,
19982 base_text: &str,
19983 cx: &mut EditorLspTestContext,
19984) {
19985 cx.set_state(not_reverted_text_with_selections);
19986 cx.set_head_text(base_text);
19987 cx.executor().run_until_parked();
19988
19989 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
19990 let snapshot = editor.snapshot(window, cx);
19991 let reverted_hunk_statuses = snapshot
19992 .buffer_snapshot
19993 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
19994 .map(|hunk| hunk.status().kind)
19995 .collect::<Vec<_>>();
19996
19997 editor.git_restore(&Default::default(), window, cx);
19998 reverted_hunk_statuses
19999 });
20000 cx.executor().run_until_parked();
20001 cx.assert_editor_state(expected_reverted_text_with_selections);
20002 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
20003}