1use super::*;
2use crate::{
3 JoinLines,
4 scroll::scroll_amount::ScrollAmount,
5 test::{
6 assert_text_with_selections, build_editor,
7 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
8 editor_test_context::EditorTestContext,
9 select_ranges,
10 },
11};
12use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
13use futures::StreamExt;
14use gpui::{
15 BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext,
16 WindowBounds, WindowOptions, div,
17};
18use indoc::indoc;
19use language::{
20 BracketPairConfig,
21 Capability::ReadWrite,
22 FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName,
23 Override, Point,
24 language_settings::{
25 AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
26 LanguageSettingsContent, PrettierSettings,
27 },
28};
29use language_settings::{Formatter, FormatterList, IndentGuideSettings};
30use lsp::CompletionParams;
31use multi_buffer::{IndentGuide, PathKey};
32use parking_lot::Mutex;
33use pretty_assertions::{assert_eq, assert_ne};
34use project::{
35 FakeFs,
36 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
37 project_settings::{LspSettings, ProjectSettings},
38};
39use serde_json::{self, json};
40use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
41use std::{
42 iter,
43 sync::atomic::{self, AtomicUsize},
44};
45use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
46use text::ToPoint as _;
47use unindent::Unindent;
48use util::{
49 assert_set_eq, path,
50 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
51 uri,
52};
53use workspace::{
54 CloseAllItems, CloseInactiveItems, NavigationEntry, ViewId,
55 item::{FollowEvent, FollowableItem, Item, ItemHandle},
56};
57
58#[gpui::test]
59fn test_edit_events(cx: &mut TestAppContext) {
60 init_test(cx, |_| {});
61
62 let buffer = cx.new(|cx| {
63 let mut buffer = language::Buffer::local("123456", cx);
64 buffer.set_group_interval(Duration::from_secs(1));
65 buffer
66 });
67
68 let events = Rc::new(RefCell::new(Vec::new()));
69 let editor1 = cx.add_window({
70 let events = events.clone();
71 |window, cx| {
72 let entity = cx.entity().clone();
73 cx.subscribe_in(
74 &entity,
75 window,
76 move |_, _, event: &EditorEvent, _, _| match event {
77 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
78 EditorEvent::BufferEdited => {
79 events.borrow_mut().push(("editor1", "buffer edited"))
80 }
81 _ => {}
82 },
83 )
84 .detach();
85 Editor::for_buffer(buffer.clone(), None, window, cx)
86 }
87 });
88
89 let editor2 = cx.add_window({
90 let events = events.clone();
91 |window, cx| {
92 cx.subscribe_in(
93 &cx.entity().clone(),
94 window,
95 move |_, _, event: &EditorEvent, _, _| match event {
96 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
97 EditorEvent::BufferEdited => {
98 events.borrow_mut().push(("editor2", "buffer edited"))
99 }
100 _ => {}
101 },
102 )
103 .detach();
104 Editor::for_buffer(buffer.clone(), None, window, cx)
105 }
106 });
107
108 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
109
110 // Mutating editor 1 will emit an `Edited` event only for that editor.
111 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
112 assert_eq!(
113 mem::take(&mut *events.borrow_mut()),
114 [
115 ("editor1", "edited"),
116 ("editor1", "buffer edited"),
117 ("editor2", "buffer edited"),
118 ]
119 );
120
121 // Mutating editor 2 will emit an `Edited` event only for that editor.
122 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
123 assert_eq!(
124 mem::take(&mut *events.borrow_mut()),
125 [
126 ("editor2", "edited"),
127 ("editor1", "buffer edited"),
128 ("editor2", "buffer edited"),
129 ]
130 );
131
132 // Undoing on editor 1 will emit an `Edited` event only for that editor.
133 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
134 assert_eq!(
135 mem::take(&mut *events.borrow_mut()),
136 [
137 ("editor1", "edited"),
138 ("editor1", "buffer edited"),
139 ("editor2", "buffer edited"),
140 ]
141 );
142
143 // Redoing on editor 1 will emit an `Edited` event only for that editor.
144 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
145 assert_eq!(
146 mem::take(&mut *events.borrow_mut()),
147 [
148 ("editor1", "edited"),
149 ("editor1", "buffer edited"),
150 ("editor2", "buffer edited"),
151 ]
152 );
153
154 // Undoing on editor 2 will emit an `Edited` event only for that editor.
155 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
156 assert_eq!(
157 mem::take(&mut *events.borrow_mut()),
158 [
159 ("editor2", "edited"),
160 ("editor1", "buffer edited"),
161 ("editor2", "buffer edited"),
162 ]
163 );
164
165 // Redoing on editor 2 will emit an `Edited` event only for that editor.
166 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
167 assert_eq!(
168 mem::take(&mut *events.borrow_mut()),
169 [
170 ("editor2", "edited"),
171 ("editor1", "buffer edited"),
172 ("editor2", "buffer edited"),
173 ]
174 );
175
176 // No event is emitted when the mutation is a no-op.
177 _ = editor2.update(cx, |editor, window, cx| {
178 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
179
180 editor.backspace(&Backspace, window, cx);
181 });
182 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
183}
184
185#[gpui::test]
186fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
187 init_test(cx, |_| {});
188
189 let mut now = Instant::now();
190 let group_interval = Duration::from_millis(1);
191 let buffer = cx.new(|cx| {
192 let mut buf = language::Buffer::local("123456", cx);
193 buf.set_group_interval(group_interval);
194 buf
195 });
196 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
197 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
198
199 _ = editor.update(cx, |editor, window, cx| {
200 editor.start_transaction_at(now, window, cx);
201 editor.change_selections(None, window, cx, |s| s.select_ranges([2..4]));
202
203 editor.insert("cd", window, cx);
204 editor.end_transaction_at(now, cx);
205 assert_eq!(editor.text(cx), "12cd56");
206 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
207
208 editor.start_transaction_at(now, window, cx);
209 editor.change_selections(None, window, cx, |s| s.select_ranges([4..5]));
210 editor.insert("e", window, cx);
211 editor.end_transaction_at(now, cx);
212 assert_eq!(editor.text(cx), "12cde6");
213 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
214
215 now += group_interval + Duration::from_millis(1);
216 editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]));
217
218 // Simulate an edit in another editor
219 buffer.update(cx, |buffer, cx| {
220 buffer.start_transaction_at(now, cx);
221 buffer.edit([(0..1, "a")], None, cx);
222 buffer.edit([(1..1, "b")], None, cx);
223 buffer.end_transaction_at(now, cx);
224 });
225
226 assert_eq!(editor.text(cx), "ab2cde6");
227 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
228
229 // Last transaction happened past the group interval in a different editor.
230 // Undo it individually and don't restore selections.
231 editor.undo(&Undo, window, cx);
232 assert_eq!(editor.text(cx), "12cde6");
233 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
234
235 // First two transactions happened within the group interval in this editor.
236 // Undo them together and restore selections.
237 editor.undo(&Undo, window, cx);
238 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
239 assert_eq!(editor.text(cx), "123456");
240 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
241
242 // Redo the first two transactions together.
243 editor.redo(&Redo, window, cx);
244 assert_eq!(editor.text(cx), "12cde6");
245 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
246
247 // Redo the last transaction on its own.
248 editor.redo(&Redo, window, cx);
249 assert_eq!(editor.text(cx), "ab2cde6");
250 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
251
252 // Test empty transactions.
253 editor.start_transaction_at(now, window, cx);
254 editor.end_transaction_at(now, cx);
255 editor.undo(&Undo, window, cx);
256 assert_eq!(editor.text(cx), "12cde6");
257 });
258}
259
260#[gpui::test]
261fn test_ime_composition(cx: &mut TestAppContext) {
262 init_test(cx, |_| {});
263
264 let buffer = cx.new(|cx| {
265 let mut buffer = language::Buffer::local("abcde", cx);
266 // Ensure automatic grouping doesn't occur.
267 buffer.set_group_interval(Duration::ZERO);
268 buffer
269 });
270
271 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
272 cx.add_window(|window, cx| {
273 let mut editor = build_editor(buffer.clone(), window, cx);
274
275 // Start a new IME composition.
276 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
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 assert_eq!(editor.text(cx), "äbcde");
280 assert_eq!(
281 editor.marked_text_ranges(cx),
282 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
283 );
284
285 // Finalize IME composition.
286 editor.replace_text_in_range(None, "ā", window, cx);
287 assert_eq!(editor.text(cx), "ābcde");
288 assert_eq!(editor.marked_text_ranges(cx), None);
289
290 // IME composition edits are grouped and are undone/redone at once.
291 editor.undo(&Default::default(), window, cx);
292 assert_eq!(editor.text(cx), "abcde");
293 assert_eq!(editor.marked_text_ranges(cx), None);
294 editor.redo(&Default::default(), window, cx);
295 assert_eq!(editor.text(cx), "ābcde");
296 assert_eq!(editor.marked_text_ranges(cx), None);
297
298 // Start a new IME composition.
299 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
300 assert_eq!(
301 editor.marked_text_ranges(cx),
302 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
303 );
304
305 // Undoing during an IME composition cancels it.
306 editor.undo(&Default::default(), window, cx);
307 assert_eq!(editor.text(cx), "ābcde");
308 assert_eq!(editor.marked_text_ranges(cx), None);
309
310 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
311 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
312 assert_eq!(editor.text(cx), "ābcdè");
313 assert_eq!(
314 editor.marked_text_ranges(cx),
315 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
316 );
317
318 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
319 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
320 assert_eq!(editor.text(cx), "ābcdę");
321 assert_eq!(editor.marked_text_ranges(cx), None);
322
323 // Start a new IME composition with multiple cursors.
324 editor.change_selections(None, window, cx, |s| {
325 s.select_ranges([
326 OffsetUtf16(1)..OffsetUtf16(1),
327 OffsetUtf16(3)..OffsetUtf16(3),
328 OffsetUtf16(5)..OffsetUtf16(5),
329 ])
330 });
331 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
332 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
333 assert_eq!(
334 editor.marked_text_ranges(cx),
335 Some(vec![
336 OffsetUtf16(0)..OffsetUtf16(3),
337 OffsetUtf16(4)..OffsetUtf16(7),
338 OffsetUtf16(8)..OffsetUtf16(11)
339 ])
340 );
341
342 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
343 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
344 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
345 assert_eq!(
346 editor.marked_text_ranges(cx),
347 Some(vec![
348 OffsetUtf16(1)..OffsetUtf16(2),
349 OffsetUtf16(5)..OffsetUtf16(6),
350 OffsetUtf16(9)..OffsetUtf16(10)
351 ])
352 );
353
354 // Finalize IME composition with multiple cursors.
355 editor.replace_text_in_range(Some(9..10), "2", window, cx);
356 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
357 assert_eq!(editor.marked_text_ranges(cx), None);
358
359 editor
360 });
361}
362
363#[gpui::test]
364fn test_selection_with_mouse(cx: &mut TestAppContext) {
365 init_test(cx, |_| {});
366
367 let editor = cx.add_window(|window, cx| {
368 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
369 build_editor(buffer, window, cx)
370 });
371
372 _ = editor.update(cx, |editor, window, cx| {
373 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
374 });
375 assert_eq!(
376 editor
377 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
378 .unwrap(),
379 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
380 );
381
382 _ = editor.update(cx, |editor, window, cx| {
383 editor.update_selection(
384 DisplayPoint::new(DisplayRow(3), 3),
385 0,
386 gpui::Point::<f32>::default(),
387 window,
388 cx,
389 );
390 });
391
392 assert_eq!(
393 editor
394 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
395 .unwrap(),
396 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
397 );
398
399 _ = editor.update(cx, |editor, window, cx| {
400 editor.update_selection(
401 DisplayPoint::new(DisplayRow(1), 1),
402 0,
403 gpui::Point::<f32>::default(),
404 window,
405 cx,
406 );
407 });
408
409 assert_eq!(
410 editor
411 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
412 .unwrap(),
413 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
414 );
415
416 _ = editor.update(cx, |editor, window, cx| {
417 editor.end_selection(window, cx);
418 editor.update_selection(
419 DisplayPoint::new(DisplayRow(3), 3),
420 0,
421 gpui::Point::<f32>::default(),
422 window,
423 cx,
424 );
425 });
426
427 assert_eq!(
428 editor
429 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
430 .unwrap(),
431 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
432 );
433
434 _ = editor.update(cx, |editor, window, cx| {
435 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
436 editor.update_selection(
437 DisplayPoint::new(DisplayRow(0), 0),
438 0,
439 gpui::Point::<f32>::default(),
440 window,
441 cx,
442 );
443 });
444
445 assert_eq!(
446 editor
447 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
448 .unwrap(),
449 [
450 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
451 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
452 ]
453 );
454
455 _ = editor.update(cx, |editor, window, cx| {
456 editor.end_selection(window, cx);
457 });
458
459 assert_eq!(
460 editor
461 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
462 .unwrap(),
463 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
464 );
465}
466
467#[gpui::test]
468fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
469 init_test(cx, |_| {});
470
471 let editor = cx.add_window(|window, cx| {
472 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
473 build_editor(buffer, window, cx)
474 });
475
476 _ = editor.update(cx, |editor, window, cx| {
477 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
478 });
479
480 _ = editor.update(cx, |editor, window, cx| {
481 editor.end_selection(window, cx);
482 });
483
484 _ = editor.update(cx, |editor, window, cx| {
485 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
486 });
487
488 _ = editor.update(cx, |editor, window, cx| {
489 editor.end_selection(window, cx);
490 });
491
492 assert_eq!(
493 editor
494 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
495 .unwrap(),
496 [
497 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
498 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
499 ]
500 );
501
502 _ = editor.update(cx, |editor, window, cx| {
503 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
504 });
505
506 _ = editor.update(cx, |editor, window, cx| {
507 editor.end_selection(window, cx);
508 });
509
510 assert_eq!(
511 editor
512 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
513 .unwrap(),
514 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
515 );
516}
517
518#[gpui::test]
519fn test_canceling_pending_selection(cx: &mut TestAppContext) {
520 init_test(cx, |_| {});
521
522 let editor = cx.add_window(|window, cx| {
523 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
524 build_editor(buffer, window, cx)
525 });
526
527 _ = editor.update(cx, |editor, window, cx| {
528 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
529 assert_eq!(
530 editor.selections.display_ranges(cx),
531 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
532 );
533 });
534
535 _ = editor.update(cx, |editor, window, cx| {
536 editor.update_selection(
537 DisplayPoint::new(DisplayRow(3), 3),
538 0,
539 gpui::Point::<f32>::default(),
540 window,
541 cx,
542 );
543 assert_eq!(
544 editor.selections.display_ranges(cx),
545 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
546 );
547 });
548
549 _ = editor.update(cx, |editor, window, cx| {
550 editor.cancel(&Cancel, window, cx);
551 editor.update_selection(
552 DisplayPoint::new(DisplayRow(1), 1),
553 0,
554 gpui::Point::<f32>::default(),
555 window,
556 cx,
557 );
558 assert_eq!(
559 editor.selections.display_ranges(cx),
560 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
561 );
562 });
563}
564
565#[gpui::test]
566fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
567 init_test(cx, |_| {});
568
569 let editor = cx.add_window(|window, cx| {
570 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
571 build_editor(buffer, window, cx)
572 });
573
574 _ = editor.update(cx, |editor, window, cx| {
575 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
576 assert_eq!(
577 editor.selections.display_ranges(cx),
578 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
579 );
580
581 editor.move_down(&Default::default(), window, cx);
582 assert_eq!(
583 editor.selections.display_ranges(cx),
584 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
585 );
586
587 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
588 assert_eq!(
589 editor.selections.display_ranges(cx),
590 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
591 );
592
593 editor.move_up(&Default::default(), window, cx);
594 assert_eq!(
595 editor.selections.display_ranges(cx),
596 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
597 );
598 });
599}
600
601#[gpui::test]
602fn test_clone(cx: &mut TestAppContext) {
603 init_test(cx, |_| {});
604
605 let (text, selection_ranges) = marked_text_ranges(
606 indoc! {"
607 one
608 two
609 threeˇ
610 four
611 fiveˇ
612 "},
613 true,
614 );
615
616 let editor = cx.add_window(|window, cx| {
617 let buffer = MultiBuffer::build_simple(&text, cx);
618 build_editor(buffer, window, cx)
619 });
620
621 _ = editor.update(cx, |editor, window, cx| {
622 editor.change_selections(None, window, cx, |s| {
623 s.select_ranges(selection_ranges.clone())
624 });
625 editor.fold_creases(
626 vec![
627 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
628 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
629 ],
630 true,
631 window,
632 cx,
633 );
634 });
635
636 let cloned_editor = editor
637 .update(cx, |editor, _, cx| {
638 cx.open_window(Default::default(), |window, cx| {
639 cx.new(|cx| editor.clone(window, cx))
640 })
641 })
642 .unwrap()
643 .unwrap();
644
645 let snapshot = editor
646 .update(cx, |e, window, cx| e.snapshot(window, cx))
647 .unwrap();
648 let cloned_snapshot = cloned_editor
649 .update(cx, |e, window, cx| e.snapshot(window, cx))
650 .unwrap();
651
652 assert_eq!(
653 cloned_editor
654 .update(cx, |e, _, cx| e.display_text(cx))
655 .unwrap(),
656 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
657 );
658 assert_eq!(
659 cloned_snapshot
660 .folds_in_range(0..text.len())
661 .collect::<Vec<_>>(),
662 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
663 );
664 assert_set_eq!(
665 cloned_editor
666 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
667 .unwrap(),
668 editor
669 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
670 .unwrap()
671 );
672 assert_set_eq!(
673 cloned_editor
674 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
675 .unwrap(),
676 editor
677 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
678 .unwrap()
679 );
680}
681
682#[gpui::test]
683async fn test_navigation_history(cx: &mut TestAppContext) {
684 init_test(cx, |_| {});
685
686 use workspace::item::Item;
687
688 let fs = FakeFs::new(cx.executor());
689 let project = Project::test(fs, [], cx).await;
690 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
691 let pane = workspace
692 .update(cx, |workspace, _, _| workspace.active_pane().clone())
693 .unwrap();
694
695 _ = workspace.update(cx, |_v, window, cx| {
696 cx.new(|cx| {
697 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
698 let mut editor = build_editor(buffer.clone(), window, cx);
699 let handle = cx.entity();
700 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
701
702 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
703 editor.nav_history.as_mut().unwrap().pop_backward(cx)
704 }
705
706 // Move the cursor a small distance.
707 // Nothing is added to the navigation history.
708 editor.change_selections(None, window, cx, |s| {
709 s.select_display_ranges([
710 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
711 ])
712 });
713 editor.change_selections(None, window, cx, |s| {
714 s.select_display_ranges([
715 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
716 ])
717 });
718 assert!(pop_history(&mut editor, cx).is_none());
719
720 // Move the cursor a large distance.
721 // The history can jump back to the previous position.
722 editor.change_selections(None, window, cx, |s| {
723 s.select_display_ranges([
724 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
725 ])
726 });
727 let nav_entry = pop_history(&mut editor, cx).unwrap();
728 editor.navigate(nav_entry.data.unwrap(), window, cx);
729 assert_eq!(nav_entry.item.id(), cx.entity_id());
730 assert_eq!(
731 editor.selections.display_ranges(cx),
732 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
733 );
734 assert!(pop_history(&mut editor, cx).is_none());
735
736 // Move the cursor a small distance via the mouse.
737 // Nothing is added to the navigation history.
738 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
739 editor.end_selection(window, cx);
740 assert_eq!(
741 editor.selections.display_ranges(cx),
742 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
743 );
744 assert!(pop_history(&mut editor, cx).is_none());
745
746 // Move the cursor a large distance via the mouse.
747 // The history can jump back to the previous position.
748 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
749 editor.end_selection(window, cx);
750 assert_eq!(
751 editor.selections.display_ranges(cx),
752 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
753 );
754 let nav_entry = pop_history(&mut editor, cx).unwrap();
755 editor.navigate(nav_entry.data.unwrap(), window, cx);
756 assert_eq!(nav_entry.item.id(), cx.entity_id());
757 assert_eq!(
758 editor.selections.display_ranges(cx),
759 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
760 );
761 assert!(pop_history(&mut editor, cx).is_none());
762
763 // Set scroll position to check later
764 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
765 let original_scroll_position = editor.scroll_manager.anchor();
766
767 // Jump to the end of the document and adjust scroll
768 editor.move_to_end(&MoveToEnd, window, cx);
769 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
770 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
771
772 let nav_entry = pop_history(&mut editor, cx).unwrap();
773 editor.navigate(nav_entry.data.unwrap(), window, cx);
774 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
775
776 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
777 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
778 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
779 let invalid_point = Point::new(9999, 0);
780 editor.navigate(
781 Box::new(NavigationData {
782 cursor_anchor: invalid_anchor,
783 cursor_position: invalid_point,
784 scroll_anchor: ScrollAnchor {
785 anchor: invalid_anchor,
786 offset: Default::default(),
787 },
788 scroll_top_row: invalid_point.row,
789 }),
790 window,
791 cx,
792 );
793 assert_eq!(
794 editor.selections.display_ranges(cx),
795 &[editor.max_point(cx)..editor.max_point(cx)]
796 );
797 assert_eq!(
798 editor.scroll_position(cx),
799 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
800 );
801
802 editor
803 })
804 });
805}
806
807#[gpui::test]
808fn test_cancel(cx: &mut TestAppContext) {
809 init_test(cx, |_| {});
810
811 let editor = cx.add_window(|window, cx| {
812 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
813 build_editor(buffer, window, cx)
814 });
815
816 _ = editor.update(cx, |editor, window, cx| {
817 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
818 editor.update_selection(
819 DisplayPoint::new(DisplayRow(1), 1),
820 0,
821 gpui::Point::<f32>::default(),
822 window,
823 cx,
824 );
825 editor.end_selection(window, cx);
826
827 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
828 editor.update_selection(
829 DisplayPoint::new(DisplayRow(0), 3),
830 0,
831 gpui::Point::<f32>::default(),
832 window,
833 cx,
834 );
835 editor.end_selection(window, cx);
836 assert_eq!(
837 editor.selections.display_ranges(cx),
838 [
839 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
840 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
841 ]
842 );
843 });
844
845 _ = editor.update(cx, |editor, window, cx| {
846 editor.cancel(&Cancel, window, cx);
847 assert_eq!(
848 editor.selections.display_ranges(cx),
849 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
850 );
851 });
852
853 _ = editor.update(cx, |editor, window, cx| {
854 editor.cancel(&Cancel, window, cx);
855 assert_eq!(
856 editor.selections.display_ranges(cx),
857 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
858 );
859 });
860}
861
862#[gpui::test]
863fn test_fold_action(cx: &mut TestAppContext) {
864 init_test(cx, |_| {});
865
866 let editor = cx.add_window(|window, cx| {
867 let buffer = MultiBuffer::build_simple(
868 &"
869 impl Foo {
870 // Hello!
871
872 fn a() {
873 1
874 }
875
876 fn b() {
877 2
878 }
879
880 fn c() {
881 3
882 }
883 }
884 "
885 .unindent(),
886 cx,
887 );
888 build_editor(buffer.clone(), window, cx)
889 });
890
891 _ = editor.update(cx, |editor, window, cx| {
892 editor.change_selections(None, window, cx, |s| {
893 s.select_display_ranges([
894 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
895 ]);
896 });
897 editor.fold(&Fold, window, cx);
898 assert_eq!(
899 editor.display_text(cx),
900 "
901 impl Foo {
902 // Hello!
903
904 fn a() {
905 1
906 }
907
908 fn b() {⋯
909 }
910
911 fn c() {⋯
912 }
913 }
914 "
915 .unindent(),
916 );
917
918 editor.fold(&Fold, window, cx);
919 assert_eq!(
920 editor.display_text(cx),
921 "
922 impl Foo {⋯
923 }
924 "
925 .unindent(),
926 );
927
928 editor.unfold_lines(&UnfoldLines, window, cx);
929 assert_eq!(
930 editor.display_text(cx),
931 "
932 impl Foo {
933 // Hello!
934
935 fn a() {
936 1
937 }
938
939 fn b() {⋯
940 }
941
942 fn c() {⋯
943 }
944 }
945 "
946 .unindent(),
947 );
948
949 editor.unfold_lines(&UnfoldLines, window, cx);
950 assert_eq!(
951 editor.display_text(cx),
952 editor.buffer.read(cx).read(cx).text()
953 );
954 });
955}
956
957#[gpui::test]
958fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
959 init_test(cx, |_| {});
960
961 let editor = cx.add_window(|window, cx| {
962 let buffer = MultiBuffer::build_simple(
963 &"
964 class Foo:
965 # Hello!
966
967 def a():
968 print(1)
969
970 def b():
971 print(2)
972
973 def c():
974 print(3)
975 "
976 .unindent(),
977 cx,
978 );
979 build_editor(buffer.clone(), window, cx)
980 });
981
982 _ = editor.update(cx, |editor, window, cx| {
983 editor.change_selections(None, window, cx, |s| {
984 s.select_display_ranges([
985 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
986 ]);
987 });
988 editor.fold(&Fold, window, cx);
989 assert_eq!(
990 editor.display_text(cx),
991 "
992 class Foo:
993 # Hello!
994
995 def a():
996 print(1)
997
998 def b():⋯
999
1000 def c():⋯
1001 "
1002 .unindent(),
1003 );
1004
1005 editor.fold(&Fold, window, cx);
1006 assert_eq!(
1007 editor.display_text(cx),
1008 "
1009 class Foo:⋯
1010 "
1011 .unindent(),
1012 );
1013
1014 editor.unfold_lines(&UnfoldLines, window, cx);
1015 assert_eq!(
1016 editor.display_text(cx),
1017 "
1018 class Foo:
1019 # Hello!
1020
1021 def a():
1022 print(1)
1023
1024 def b():⋯
1025
1026 def c():⋯
1027 "
1028 .unindent(),
1029 );
1030
1031 editor.unfold_lines(&UnfoldLines, window, cx);
1032 assert_eq!(
1033 editor.display_text(cx),
1034 editor.buffer.read(cx).read(cx).text()
1035 );
1036 });
1037}
1038
1039#[gpui::test]
1040fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1041 init_test(cx, |_| {});
1042
1043 let editor = cx.add_window(|window, cx| {
1044 let buffer = MultiBuffer::build_simple(
1045 &"
1046 class Foo:
1047 # Hello!
1048
1049 def a():
1050 print(1)
1051
1052 def b():
1053 print(2)
1054
1055
1056 def c():
1057 print(3)
1058
1059
1060 "
1061 .unindent(),
1062 cx,
1063 );
1064 build_editor(buffer.clone(), window, cx)
1065 });
1066
1067 _ = editor.update(cx, |editor, window, cx| {
1068 editor.change_selections(None, window, cx, |s| {
1069 s.select_display_ranges([
1070 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1071 ]);
1072 });
1073 editor.fold(&Fold, window, cx);
1074 assert_eq!(
1075 editor.display_text(cx),
1076 "
1077 class Foo:
1078 # Hello!
1079
1080 def a():
1081 print(1)
1082
1083 def b():⋯
1084
1085
1086 def c():⋯
1087
1088
1089 "
1090 .unindent(),
1091 );
1092
1093 editor.fold(&Fold, window, cx);
1094 assert_eq!(
1095 editor.display_text(cx),
1096 "
1097 class Foo:⋯
1098
1099
1100 "
1101 .unindent(),
1102 );
1103
1104 editor.unfold_lines(&UnfoldLines, window, cx);
1105 assert_eq!(
1106 editor.display_text(cx),
1107 "
1108 class Foo:
1109 # Hello!
1110
1111 def a():
1112 print(1)
1113
1114 def b():⋯
1115
1116
1117 def c():⋯
1118
1119
1120 "
1121 .unindent(),
1122 );
1123
1124 editor.unfold_lines(&UnfoldLines, window, cx);
1125 assert_eq!(
1126 editor.display_text(cx),
1127 editor.buffer.read(cx).read(cx).text()
1128 );
1129 });
1130}
1131
1132#[gpui::test]
1133fn test_fold_at_level(cx: &mut TestAppContext) {
1134 init_test(cx, |_| {});
1135
1136 let editor = cx.add_window(|window, cx| {
1137 let buffer = MultiBuffer::build_simple(
1138 &"
1139 class Foo:
1140 # Hello!
1141
1142 def a():
1143 print(1)
1144
1145 def b():
1146 print(2)
1147
1148
1149 class Bar:
1150 # World!
1151
1152 def a():
1153 print(1)
1154
1155 def b():
1156 print(2)
1157
1158
1159 "
1160 .unindent(),
1161 cx,
1162 );
1163 build_editor(buffer.clone(), window, cx)
1164 });
1165
1166 _ = editor.update(cx, |editor, window, cx| {
1167 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1168 assert_eq!(
1169 editor.display_text(cx),
1170 "
1171 class Foo:
1172 # Hello!
1173
1174 def a():⋯
1175
1176 def b():⋯
1177
1178
1179 class Bar:
1180 # World!
1181
1182 def a():⋯
1183
1184 def b():⋯
1185
1186
1187 "
1188 .unindent(),
1189 );
1190
1191 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1192 assert_eq!(
1193 editor.display_text(cx),
1194 "
1195 class Foo:⋯
1196
1197
1198 class Bar:⋯
1199
1200
1201 "
1202 .unindent(),
1203 );
1204
1205 editor.unfold_all(&UnfoldAll, window, cx);
1206 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1207 assert_eq!(
1208 editor.display_text(cx),
1209 "
1210 class Foo:
1211 # Hello!
1212
1213 def a():
1214 print(1)
1215
1216 def b():
1217 print(2)
1218
1219
1220 class Bar:
1221 # World!
1222
1223 def a():
1224 print(1)
1225
1226 def b():
1227 print(2)
1228
1229
1230 "
1231 .unindent(),
1232 );
1233
1234 assert_eq!(
1235 editor.display_text(cx),
1236 editor.buffer.read(cx).read(cx).text()
1237 );
1238 });
1239}
1240
1241#[gpui::test]
1242fn test_move_cursor(cx: &mut TestAppContext) {
1243 init_test(cx, |_| {});
1244
1245 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1246 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1247
1248 buffer.update(cx, |buffer, cx| {
1249 buffer.edit(
1250 vec![
1251 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1252 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1253 ],
1254 None,
1255 cx,
1256 );
1257 });
1258 _ = editor.update(cx, |editor, window, cx| {
1259 assert_eq!(
1260 editor.selections.display_ranges(cx),
1261 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1262 );
1263
1264 editor.move_down(&MoveDown, window, cx);
1265 assert_eq!(
1266 editor.selections.display_ranges(cx),
1267 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1268 );
1269
1270 editor.move_right(&MoveRight, window, cx);
1271 assert_eq!(
1272 editor.selections.display_ranges(cx),
1273 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1274 );
1275
1276 editor.move_left(&MoveLeft, window, cx);
1277 assert_eq!(
1278 editor.selections.display_ranges(cx),
1279 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1280 );
1281
1282 editor.move_up(&MoveUp, window, cx);
1283 assert_eq!(
1284 editor.selections.display_ranges(cx),
1285 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1286 );
1287
1288 editor.move_to_end(&MoveToEnd, window, cx);
1289 assert_eq!(
1290 editor.selections.display_ranges(cx),
1291 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1292 );
1293
1294 editor.move_to_beginning(&MoveToBeginning, window, cx);
1295 assert_eq!(
1296 editor.selections.display_ranges(cx),
1297 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1298 );
1299
1300 editor.change_selections(None, window, cx, |s| {
1301 s.select_display_ranges([
1302 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1303 ]);
1304 });
1305 editor.select_to_beginning(&SelectToBeginning, window, cx);
1306 assert_eq!(
1307 editor.selections.display_ranges(cx),
1308 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1309 );
1310
1311 editor.select_to_end(&SelectToEnd, window, cx);
1312 assert_eq!(
1313 editor.selections.display_ranges(cx),
1314 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1315 );
1316 });
1317}
1318
1319// TODO: Re-enable this test
1320#[cfg(target_os = "macos")]
1321#[gpui::test]
1322fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1323 init_test(cx, |_| {});
1324
1325 let editor = cx.add_window(|window, cx| {
1326 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1327 build_editor(buffer.clone(), window, cx)
1328 });
1329
1330 assert_eq!('🟥'.len_utf8(), 4);
1331 assert_eq!('α'.len_utf8(), 2);
1332
1333 _ = editor.update(cx, |editor, window, cx| {
1334 editor.fold_creases(
1335 vec![
1336 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1337 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1338 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1339 ],
1340 true,
1341 window,
1342 cx,
1343 );
1344 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1345
1346 editor.move_right(&MoveRight, window, cx);
1347 assert_eq!(
1348 editor.selections.display_ranges(cx),
1349 &[empty_range(0, "🟥".len())]
1350 );
1351 editor.move_right(&MoveRight, window, cx);
1352 assert_eq!(
1353 editor.selections.display_ranges(cx),
1354 &[empty_range(0, "🟥🟧".len())]
1355 );
1356 editor.move_right(&MoveRight, window, cx);
1357 assert_eq!(
1358 editor.selections.display_ranges(cx),
1359 &[empty_range(0, "🟥🟧⋯".len())]
1360 );
1361
1362 editor.move_down(&MoveDown, window, cx);
1363 assert_eq!(
1364 editor.selections.display_ranges(cx),
1365 &[empty_range(1, "ab⋯e".len())]
1366 );
1367 editor.move_left(&MoveLeft, window, cx);
1368 assert_eq!(
1369 editor.selections.display_ranges(cx),
1370 &[empty_range(1, "ab⋯".len())]
1371 );
1372 editor.move_left(&MoveLeft, window, cx);
1373 assert_eq!(
1374 editor.selections.display_ranges(cx),
1375 &[empty_range(1, "ab".len())]
1376 );
1377 editor.move_left(&MoveLeft, window, cx);
1378 assert_eq!(
1379 editor.selections.display_ranges(cx),
1380 &[empty_range(1, "a".len())]
1381 );
1382
1383 editor.move_down(&MoveDown, window, cx);
1384 assert_eq!(
1385 editor.selections.display_ranges(cx),
1386 &[empty_range(2, "α".len())]
1387 );
1388 editor.move_right(&MoveRight, window, cx);
1389 assert_eq!(
1390 editor.selections.display_ranges(cx),
1391 &[empty_range(2, "αβ".len())]
1392 );
1393 editor.move_right(&MoveRight, window, cx);
1394 assert_eq!(
1395 editor.selections.display_ranges(cx),
1396 &[empty_range(2, "αβ⋯".len())]
1397 );
1398 editor.move_right(&MoveRight, window, cx);
1399 assert_eq!(
1400 editor.selections.display_ranges(cx),
1401 &[empty_range(2, "αβ⋯ε".len())]
1402 );
1403
1404 editor.move_up(&MoveUp, window, cx);
1405 assert_eq!(
1406 editor.selections.display_ranges(cx),
1407 &[empty_range(1, "ab⋯e".len())]
1408 );
1409 editor.move_down(&MoveDown, window, cx);
1410 assert_eq!(
1411 editor.selections.display_ranges(cx),
1412 &[empty_range(2, "αβ⋯ε".len())]
1413 );
1414 editor.move_up(&MoveUp, window, cx);
1415 assert_eq!(
1416 editor.selections.display_ranges(cx),
1417 &[empty_range(1, "ab⋯e".len())]
1418 );
1419
1420 editor.move_up(&MoveUp, window, cx);
1421 assert_eq!(
1422 editor.selections.display_ranges(cx),
1423 &[empty_range(0, "🟥🟧".len())]
1424 );
1425 editor.move_left(&MoveLeft, window, cx);
1426 assert_eq!(
1427 editor.selections.display_ranges(cx),
1428 &[empty_range(0, "🟥".len())]
1429 );
1430 editor.move_left(&MoveLeft, window, cx);
1431 assert_eq!(
1432 editor.selections.display_ranges(cx),
1433 &[empty_range(0, "".len())]
1434 );
1435 });
1436}
1437
1438#[gpui::test]
1439fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1440 init_test(cx, |_| {});
1441
1442 let editor = cx.add_window(|window, cx| {
1443 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1444 build_editor(buffer.clone(), window, cx)
1445 });
1446 _ = editor.update(cx, |editor, window, cx| {
1447 editor.change_selections(None, window, cx, |s| {
1448 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1449 });
1450
1451 // moving above start of document should move selection to start of document,
1452 // but the next move down should still be at the original goal_x
1453 editor.move_up(&MoveUp, window, cx);
1454 assert_eq!(
1455 editor.selections.display_ranges(cx),
1456 &[empty_range(0, "".len())]
1457 );
1458
1459 editor.move_down(&MoveDown, window, cx);
1460 assert_eq!(
1461 editor.selections.display_ranges(cx),
1462 &[empty_range(1, "abcd".len())]
1463 );
1464
1465 editor.move_down(&MoveDown, window, cx);
1466 assert_eq!(
1467 editor.selections.display_ranges(cx),
1468 &[empty_range(2, "αβγ".len())]
1469 );
1470
1471 editor.move_down(&MoveDown, window, cx);
1472 assert_eq!(
1473 editor.selections.display_ranges(cx),
1474 &[empty_range(3, "abcd".len())]
1475 );
1476
1477 editor.move_down(&MoveDown, window, cx);
1478 assert_eq!(
1479 editor.selections.display_ranges(cx),
1480 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1481 );
1482
1483 // moving past end of document should not change goal_x
1484 editor.move_down(&MoveDown, window, cx);
1485 assert_eq!(
1486 editor.selections.display_ranges(cx),
1487 &[empty_range(5, "".len())]
1488 );
1489
1490 editor.move_down(&MoveDown, window, cx);
1491 assert_eq!(
1492 editor.selections.display_ranges(cx),
1493 &[empty_range(5, "".len())]
1494 );
1495
1496 editor.move_up(&MoveUp, window, cx);
1497 assert_eq!(
1498 editor.selections.display_ranges(cx),
1499 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1500 );
1501
1502 editor.move_up(&MoveUp, window, cx);
1503 assert_eq!(
1504 editor.selections.display_ranges(cx),
1505 &[empty_range(3, "abcd".len())]
1506 );
1507
1508 editor.move_up(&MoveUp, window, cx);
1509 assert_eq!(
1510 editor.selections.display_ranges(cx),
1511 &[empty_range(2, "αβγ".len())]
1512 );
1513 });
1514}
1515
1516#[gpui::test]
1517fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1518 init_test(cx, |_| {});
1519 let move_to_beg = MoveToBeginningOfLine {
1520 stop_at_soft_wraps: true,
1521 stop_at_indent: true,
1522 };
1523
1524 let delete_to_beg = DeleteToBeginningOfLine {
1525 stop_at_indent: false,
1526 };
1527
1528 let move_to_end = MoveToEndOfLine {
1529 stop_at_soft_wraps: true,
1530 };
1531
1532 let editor = cx.add_window(|window, cx| {
1533 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1534 build_editor(buffer, window, cx)
1535 });
1536 _ = editor.update(cx, |editor, window, cx| {
1537 editor.change_selections(None, window, cx, |s| {
1538 s.select_display_ranges([
1539 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1540 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1541 ]);
1542 });
1543 });
1544
1545 _ = editor.update(cx, |editor, window, cx| {
1546 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1547 assert_eq!(
1548 editor.selections.display_ranges(cx),
1549 &[
1550 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1551 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1552 ]
1553 );
1554 });
1555
1556 _ = editor.update(cx, |editor, window, cx| {
1557 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1558 assert_eq!(
1559 editor.selections.display_ranges(cx),
1560 &[
1561 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1562 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1563 ]
1564 );
1565 });
1566
1567 _ = editor.update(cx, |editor, window, cx| {
1568 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1569 assert_eq!(
1570 editor.selections.display_ranges(cx),
1571 &[
1572 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1573 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1574 ]
1575 );
1576 });
1577
1578 _ = editor.update(cx, |editor, window, cx| {
1579 editor.move_to_end_of_line(&move_to_end, window, cx);
1580 assert_eq!(
1581 editor.selections.display_ranges(cx),
1582 &[
1583 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1584 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1585 ]
1586 );
1587 });
1588
1589 // Moving to the end of line again is a no-op.
1590 _ = editor.update(cx, |editor, window, cx| {
1591 editor.move_to_end_of_line(&move_to_end, window, cx);
1592 assert_eq!(
1593 editor.selections.display_ranges(cx),
1594 &[
1595 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1596 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1597 ]
1598 );
1599 });
1600
1601 _ = editor.update(cx, |editor, window, cx| {
1602 editor.move_left(&MoveLeft, window, cx);
1603 editor.select_to_beginning_of_line(
1604 &SelectToBeginningOfLine {
1605 stop_at_soft_wraps: true,
1606 stop_at_indent: true,
1607 },
1608 window,
1609 cx,
1610 );
1611 assert_eq!(
1612 editor.selections.display_ranges(cx),
1613 &[
1614 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1615 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1616 ]
1617 );
1618 });
1619
1620 _ = editor.update(cx, |editor, window, cx| {
1621 editor.select_to_beginning_of_line(
1622 &SelectToBeginningOfLine {
1623 stop_at_soft_wraps: true,
1624 stop_at_indent: true,
1625 },
1626 window,
1627 cx,
1628 );
1629 assert_eq!(
1630 editor.selections.display_ranges(cx),
1631 &[
1632 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1633 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1634 ]
1635 );
1636 });
1637
1638 _ = editor.update(cx, |editor, window, cx| {
1639 editor.select_to_beginning_of_line(
1640 &SelectToBeginningOfLine {
1641 stop_at_soft_wraps: true,
1642 stop_at_indent: true,
1643 },
1644 window,
1645 cx,
1646 );
1647 assert_eq!(
1648 editor.selections.display_ranges(cx),
1649 &[
1650 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1651 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1652 ]
1653 );
1654 });
1655
1656 _ = editor.update(cx, |editor, window, cx| {
1657 editor.select_to_end_of_line(
1658 &SelectToEndOfLine {
1659 stop_at_soft_wraps: true,
1660 },
1661 window,
1662 cx,
1663 );
1664 assert_eq!(
1665 editor.selections.display_ranges(cx),
1666 &[
1667 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1668 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1669 ]
1670 );
1671 });
1672
1673 _ = editor.update(cx, |editor, window, cx| {
1674 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1675 assert_eq!(editor.display_text(cx), "ab\n de");
1676 assert_eq!(
1677 editor.selections.display_ranges(cx),
1678 &[
1679 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1680 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1681 ]
1682 );
1683 });
1684
1685 _ = editor.update(cx, |editor, window, cx| {
1686 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1687 assert_eq!(editor.display_text(cx), "\n");
1688 assert_eq!(
1689 editor.selections.display_ranges(cx),
1690 &[
1691 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1692 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1693 ]
1694 );
1695 });
1696}
1697
1698#[gpui::test]
1699fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1700 init_test(cx, |_| {});
1701 let move_to_beg = MoveToBeginningOfLine {
1702 stop_at_soft_wraps: false,
1703 stop_at_indent: false,
1704 };
1705
1706 let move_to_end = MoveToEndOfLine {
1707 stop_at_soft_wraps: false,
1708 };
1709
1710 let editor = cx.add_window(|window, cx| {
1711 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1712 build_editor(buffer, window, cx)
1713 });
1714
1715 _ = editor.update(cx, |editor, window, cx| {
1716 editor.set_wrap_width(Some(140.0.into()), cx);
1717
1718 // We expect the following lines after wrapping
1719 // ```
1720 // thequickbrownfox
1721 // jumpedoverthelazydo
1722 // gs
1723 // ```
1724 // The final `gs` was soft-wrapped onto a new line.
1725 assert_eq!(
1726 "thequickbrownfox\njumpedoverthelaz\nydogs",
1727 editor.display_text(cx),
1728 );
1729
1730 // First, let's assert behavior on the first line, that was not soft-wrapped.
1731 // Start the cursor at the `k` on the first line
1732 editor.change_selections(None, window, cx, |s| {
1733 s.select_display_ranges([
1734 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1735 ]);
1736 });
1737
1738 // Moving to the beginning of the line should put us at the beginning of the line.
1739 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1740 assert_eq!(
1741 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1742 editor.selections.display_ranges(cx)
1743 );
1744
1745 // Moving to the end of the line should put us at the end of the line.
1746 editor.move_to_end_of_line(&move_to_end, window, cx);
1747 assert_eq!(
1748 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1749 editor.selections.display_ranges(cx)
1750 );
1751
1752 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1753 // Start the cursor at the last line (`y` that was wrapped to a new line)
1754 editor.change_selections(None, window, cx, |s| {
1755 s.select_display_ranges([
1756 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1757 ]);
1758 });
1759
1760 // Moving to the beginning of the line should put us at the start of the second line of
1761 // display text, i.e., the `j`.
1762 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1763 assert_eq!(
1764 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1765 editor.selections.display_ranges(cx)
1766 );
1767
1768 // Moving to the beginning of the line again should be a no-op.
1769 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1770 assert_eq!(
1771 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1772 editor.selections.display_ranges(cx)
1773 );
1774
1775 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1776 // next display line.
1777 editor.move_to_end_of_line(&move_to_end, window, cx);
1778 assert_eq!(
1779 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1780 editor.selections.display_ranges(cx)
1781 );
1782
1783 // Moving to the end of the line again should be a no-op.
1784 editor.move_to_end_of_line(&move_to_end, window, cx);
1785 assert_eq!(
1786 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1787 editor.selections.display_ranges(cx)
1788 );
1789 });
1790}
1791
1792#[gpui::test]
1793fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1794 init_test(cx, |_| {});
1795
1796 let move_to_beg = MoveToBeginningOfLine {
1797 stop_at_soft_wraps: true,
1798 stop_at_indent: true,
1799 };
1800
1801 let select_to_beg = SelectToBeginningOfLine {
1802 stop_at_soft_wraps: true,
1803 stop_at_indent: true,
1804 };
1805
1806 let delete_to_beg = DeleteToBeginningOfLine {
1807 stop_at_indent: true,
1808 };
1809
1810 let move_to_end = MoveToEndOfLine {
1811 stop_at_soft_wraps: false,
1812 };
1813
1814 let editor = cx.add_window(|window, cx| {
1815 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1816 build_editor(buffer, window, cx)
1817 });
1818
1819 _ = editor.update(cx, |editor, window, cx| {
1820 editor.change_selections(None, window, cx, |s| {
1821 s.select_display_ranges([
1822 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1823 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1824 ]);
1825 });
1826
1827 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1828 // and the second cursor at the first non-whitespace character in the line.
1829 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1830 assert_eq!(
1831 editor.selections.display_ranges(cx),
1832 &[
1833 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1834 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1835 ]
1836 );
1837
1838 // Moving to the beginning of the line again should be a no-op for the first cursor,
1839 // and should move the second cursor to the beginning of the line.
1840 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1841 assert_eq!(
1842 editor.selections.display_ranges(cx),
1843 &[
1844 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1845 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1846 ]
1847 );
1848
1849 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1850 // and should move the second cursor back to the first non-whitespace character in the line.
1851 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1852 assert_eq!(
1853 editor.selections.display_ranges(cx),
1854 &[
1855 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1856 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1857 ]
1858 );
1859
1860 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1861 // and to the first non-whitespace character in the line for the second cursor.
1862 editor.move_to_end_of_line(&move_to_end, window, cx);
1863 editor.move_left(&MoveLeft, window, cx);
1864 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1865 assert_eq!(
1866 editor.selections.display_ranges(cx),
1867 &[
1868 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1869 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1870 ]
1871 );
1872
1873 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1874 // and should select to the beginning of the line for the second cursor.
1875 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1876 assert_eq!(
1877 editor.selections.display_ranges(cx),
1878 &[
1879 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1880 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1881 ]
1882 );
1883
1884 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1885 // and should delete to the first non-whitespace character in the line for the second cursor.
1886 editor.move_to_end_of_line(&move_to_end, window, cx);
1887 editor.move_left(&MoveLeft, window, cx);
1888 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1889 assert_eq!(editor.text(cx), "c\n f");
1890 });
1891}
1892
1893#[gpui::test]
1894fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1895 init_test(cx, |_| {});
1896
1897 let editor = cx.add_window(|window, cx| {
1898 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1899 build_editor(buffer, window, cx)
1900 });
1901 _ = editor.update(cx, |editor, window, cx| {
1902 editor.change_selections(None, window, cx, |s| {
1903 s.select_display_ranges([
1904 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1905 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1906 ])
1907 });
1908
1909 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1910 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1911
1912 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1913 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", editor, cx);
1914
1915 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1916 assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1917
1918 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1919 assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1920
1921 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1922 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1923
1924 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1925 assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1926
1927 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1928 assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1929
1930 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1931 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1932
1933 editor.move_right(&MoveRight, window, cx);
1934 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1935 assert_selection_ranges(
1936 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1937 editor,
1938 cx,
1939 );
1940
1941 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1942 assert_selection_ranges(
1943 "use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}",
1944 editor,
1945 cx,
1946 );
1947
1948 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1949 assert_selection_ranges(
1950 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1951 editor,
1952 cx,
1953 );
1954 });
1955}
1956
1957#[gpui::test]
1958fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1959 init_test(cx, |_| {});
1960
1961 let editor = cx.add_window(|window, cx| {
1962 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1963 build_editor(buffer, window, cx)
1964 });
1965
1966 _ = editor.update(cx, |editor, window, cx| {
1967 editor.set_wrap_width(Some(140.0.into()), cx);
1968 assert_eq!(
1969 editor.display_text(cx),
1970 "use one::{\n two::three::\n four::five\n};"
1971 );
1972
1973 editor.change_selections(None, window, cx, |s| {
1974 s.select_display_ranges([
1975 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1976 ]);
1977 });
1978
1979 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1980 assert_eq!(
1981 editor.selections.display_ranges(cx),
1982 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1983 );
1984
1985 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1986 assert_eq!(
1987 editor.selections.display_ranges(cx),
1988 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1989 );
1990
1991 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1992 assert_eq!(
1993 editor.selections.display_ranges(cx),
1994 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1995 );
1996
1997 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1998 assert_eq!(
1999 editor.selections.display_ranges(cx),
2000 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2001 );
2002
2003 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2004 assert_eq!(
2005 editor.selections.display_ranges(cx),
2006 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2007 );
2008
2009 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2010 assert_eq!(
2011 editor.selections.display_ranges(cx),
2012 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2013 );
2014 });
2015}
2016
2017#[gpui::test]
2018async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2019 init_test(cx, |_| {});
2020 let mut cx = EditorTestContext::new(cx).await;
2021
2022 let line_height = cx.editor(|editor, window, _| {
2023 editor
2024 .style()
2025 .unwrap()
2026 .text
2027 .line_height_in_pixels(window.rem_size())
2028 });
2029 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2030
2031 cx.set_state(
2032 &r#"ˇone
2033 two
2034
2035 three
2036 fourˇ
2037 five
2038
2039 six"#
2040 .unindent(),
2041 );
2042
2043 cx.update_editor(|editor, window, cx| {
2044 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2045 });
2046 cx.assert_editor_state(
2047 &r#"one
2048 two
2049 ˇ
2050 three
2051 four
2052 five
2053 ˇ
2054 six"#
2055 .unindent(),
2056 );
2057
2058 cx.update_editor(|editor, window, cx| {
2059 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2060 });
2061 cx.assert_editor_state(
2062 &r#"one
2063 two
2064
2065 three
2066 four
2067 five
2068 ˇ
2069 sixˇ"#
2070 .unindent(),
2071 );
2072
2073 cx.update_editor(|editor, window, cx| {
2074 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2075 });
2076 cx.assert_editor_state(
2077 &r#"one
2078 two
2079
2080 three
2081 four
2082 five
2083
2084 sixˇ"#
2085 .unindent(),
2086 );
2087
2088 cx.update_editor(|editor, window, cx| {
2089 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2090 });
2091 cx.assert_editor_state(
2092 &r#"one
2093 two
2094
2095 three
2096 four
2097 five
2098 ˇ
2099 six"#
2100 .unindent(),
2101 );
2102
2103 cx.update_editor(|editor, window, cx| {
2104 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2105 });
2106 cx.assert_editor_state(
2107 &r#"one
2108 two
2109 ˇ
2110 three
2111 four
2112 five
2113
2114 six"#
2115 .unindent(),
2116 );
2117
2118 cx.update_editor(|editor, window, cx| {
2119 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2120 });
2121 cx.assert_editor_state(
2122 &r#"ˇone
2123 two
2124
2125 three
2126 four
2127 five
2128
2129 six"#
2130 .unindent(),
2131 );
2132}
2133
2134#[gpui::test]
2135async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2136 init_test(cx, |_| {});
2137 let mut cx = EditorTestContext::new(cx).await;
2138 let line_height = cx.editor(|editor, window, _| {
2139 editor
2140 .style()
2141 .unwrap()
2142 .text
2143 .line_height_in_pixels(window.rem_size())
2144 });
2145 let window = cx.window;
2146 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2147
2148 cx.set_state(
2149 r#"ˇone
2150 two
2151 three
2152 four
2153 five
2154 six
2155 seven
2156 eight
2157 nine
2158 ten
2159 "#,
2160 );
2161
2162 cx.update_editor(|editor, window, cx| {
2163 assert_eq!(
2164 editor.snapshot(window, cx).scroll_position(),
2165 gpui::Point::new(0., 0.)
2166 );
2167 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2168 assert_eq!(
2169 editor.snapshot(window, cx).scroll_position(),
2170 gpui::Point::new(0., 3.)
2171 );
2172 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2173 assert_eq!(
2174 editor.snapshot(window, cx).scroll_position(),
2175 gpui::Point::new(0., 6.)
2176 );
2177 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2178 assert_eq!(
2179 editor.snapshot(window, cx).scroll_position(),
2180 gpui::Point::new(0., 3.)
2181 );
2182
2183 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2184 assert_eq!(
2185 editor.snapshot(window, cx).scroll_position(),
2186 gpui::Point::new(0., 1.)
2187 );
2188 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2189 assert_eq!(
2190 editor.snapshot(window, cx).scroll_position(),
2191 gpui::Point::new(0., 3.)
2192 );
2193 });
2194}
2195
2196#[gpui::test]
2197async fn test_autoscroll(cx: &mut TestAppContext) {
2198 init_test(cx, |_| {});
2199 let mut cx = EditorTestContext::new(cx).await;
2200
2201 let line_height = cx.update_editor(|editor, window, cx| {
2202 editor.set_vertical_scroll_margin(2, cx);
2203 editor
2204 .style()
2205 .unwrap()
2206 .text
2207 .line_height_in_pixels(window.rem_size())
2208 });
2209 let window = cx.window;
2210 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2211
2212 cx.set_state(
2213 r#"ˇone
2214 two
2215 three
2216 four
2217 five
2218 six
2219 seven
2220 eight
2221 nine
2222 ten
2223 "#,
2224 );
2225 cx.update_editor(|editor, window, cx| {
2226 assert_eq!(
2227 editor.snapshot(window, cx).scroll_position(),
2228 gpui::Point::new(0., 0.0)
2229 );
2230 });
2231
2232 // Add a cursor below the visible area. Since both cursors cannot fit
2233 // on screen, the editor autoscrolls to reveal the newest cursor, and
2234 // allows the vertical scroll margin below that cursor.
2235 cx.update_editor(|editor, window, cx| {
2236 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2237 selections.select_ranges([
2238 Point::new(0, 0)..Point::new(0, 0),
2239 Point::new(6, 0)..Point::new(6, 0),
2240 ]);
2241 })
2242 });
2243 cx.update_editor(|editor, window, cx| {
2244 assert_eq!(
2245 editor.snapshot(window, cx).scroll_position(),
2246 gpui::Point::new(0., 3.0)
2247 );
2248 });
2249
2250 // Move down. The editor cursor scrolls down to track the newest cursor.
2251 cx.update_editor(|editor, window, cx| {
2252 editor.move_down(&Default::default(), window, cx);
2253 });
2254 cx.update_editor(|editor, window, cx| {
2255 assert_eq!(
2256 editor.snapshot(window, cx).scroll_position(),
2257 gpui::Point::new(0., 4.0)
2258 );
2259 });
2260
2261 // Add a cursor above the visible area. Since both cursors fit on screen,
2262 // the editor scrolls to show both.
2263 cx.update_editor(|editor, window, cx| {
2264 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2265 selections.select_ranges([
2266 Point::new(1, 0)..Point::new(1, 0),
2267 Point::new(6, 0)..Point::new(6, 0),
2268 ]);
2269 })
2270 });
2271 cx.update_editor(|editor, window, cx| {
2272 assert_eq!(
2273 editor.snapshot(window, cx).scroll_position(),
2274 gpui::Point::new(0., 1.0)
2275 );
2276 });
2277}
2278
2279#[gpui::test]
2280async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2281 init_test(cx, |_| {});
2282 let mut cx = EditorTestContext::new(cx).await;
2283
2284 let line_height = cx.editor(|editor, window, _cx| {
2285 editor
2286 .style()
2287 .unwrap()
2288 .text
2289 .line_height_in_pixels(window.rem_size())
2290 });
2291 let window = cx.window;
2292 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2293 cx.set_state(
2294 &r#"
2295 ˇone
2296 two
2297 threeˇ
2298 four
2299 five
2300 six
2301 seven
2302 eight
2303 nine
2304 ten
2305 "#
2306 .unindent(),
2307 );
2308
2309 cx.update_editor(|editor, window, cx| {
2310 editor.move_page_down(&MovePageDown::default(), window, cx)
2311 });
2312 cx.assert_editor_state(
2313 &r#"
2314 one
2315 two
2316 three
2317 ˇfour
2318 five
2319 sixˇ
2320 seven
2321 eight
2322 nine
2323 ten
2324 "#
2325 .unindent(),
2326 );
2327
2328 cx.update_editor(|editor, window, cx| {
2329 editor.move_page_down(&MovePageDown::default(), window, cx)
2330 });
2331 cx.assert_editor_state(
2332 &r#"
2333 one
2334 two
2335 three
2336 four
2337 five
2338 six
2339 ˇseven
2340 eight
2341 nineˇ
2342 ten
2343 "#
2344 .unindent(),
2345 );
2346
2347 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2348 cx.assert_editor_state(
2349 &r#"
2350 one
2351 two
2352 three
2353 ˇfour
2354 five
2355 sixˇ
2356 seven
2357 eight
2358 nine
2359 ten
2360 "#
2361 .unindent(),
2362 );
2363
2364 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2365 cx.assert_editor_state(
2366 &r#"
2367 ˇone
2368 two
2369 threeˇ
2370 four
2371 five
2372 six
2373 seven
2374 eight
2375 nine
2376 ten
2377 "#
2378 .unindent(),
2379 );
2380
2381 // Test select collapsing
2382 cx.update_editor(|editor, window, cx| {
2383 editor.move_page_down(&MovePageDown::default(), window, cx);
2384 editor.move_page_down(&MovePageDown::default(), window, cx);
2385 editor.move_page_down(&MovePageDown::default(), window, cx);
2386 });
2387 cx.assert_editor_state(
2388 &r#"
2389 one
2390 two
2391 three
2392 four
2393 five
2394 six
2395 seven
2396 eight
2397 nine
2398 ˇten
2399 ˇ"#
2400 .unindent(),
2401 );
2402}
2403
2404#[gpui::test]
2405async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2406 init_test(cx, |_| {});
2407 let mut cx = EditorTestContext::new(cx).await;
2408 cx.set_state("one «two threeˇ» four");
2409 cx.update_editor(|editor, window, cx| {
2410 editor.delete_to_beginning_of_line(
2411 &DeleteToBeginningOfLine {
2412 stop_at_indent: false,
2413 },
2414 window,
2415 cx,
2416 );
2417 assert_eq!(editor.text(cx), " four");
2418 });
2419}
2420
2421#[gpui::test]
2422fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2423 init_test(cx, |_| {});
2424
2425 let editor = cx.add_window(|window, cx| {
2426 let buffer = MultiBuffer::build_simple("one two three four", cx);
2427 build_editor(buffer.clone(), window, cx)
2428 });
2429
2430 _ = editor.update(cx, |editor, window, cx| {
2431 editor.change_selections(None, window, cx, |s| {
2432 s.select_display_ranges([
2433 // an empty selection - the preceding word fragment is deleted
2434 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2435 // characters selected - they are deleted
2436 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2437 ])
2438 });
2439 editor.delete_to_previous_word_start(
2440 &DeleteToPreviousWordStart {
2441 ignore_newlines: false,
2442 },
2443 window,
2444 cx,
2445 );
2446 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2447 });
2448
2449 _ = editor.update(cx, |editor, window, cx| {
2450 editor.change_selections(None, window, cx, |s| {
2451 s.select_display_ranges([
2452 // an empty selection - the following word fragment is deleted
2453 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2454 // characters selected - they are deleted
2455 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2456 ])
2457 });
2458 editor.delete_to_next_word_end(
2459 &DeleteToNextWordEnd {
2460 ignore_newlines: false,
2461 },
2462 window,
2463 cx,
2464 );
2465 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2466 });
2467}
2468
2469#[gpui::test]
2470fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2471 init_test(cx, |_| {});
2472
2473 let editor = cx.add_window(|window, cx| {
2474 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2475 build_editor(buffer.clone(), window, cx)
2476 });
2477 let del_to_prev_word_start = DeleteToPreviousWordStart {
2478 ignore_newlines: false,
2479 };
2480 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2481 ignore_newlines: true,
2482 };
2483
2484 _ = editor.update(cx, |editor, window, cx| {
2485 editor.change_selections(None, window, cx, |s| {
2486 s.select_display_ranges([
2487 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2488 ])
2489 });
2490 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2491 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2492 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2493 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2494 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2495 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2496 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2497 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2498 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2499 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2500 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2501 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2502 });
2503}
2504
2505#[gpui::test]
2506fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2507 init_test(cx, |_| {});
2508
2509 let editor = cx.add_window(|window, cx| {
2510 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2511 build_editor(buffer.clone(), window, cx)
2512 });
2513 let del_to_next_word_end = DeleteToNextWordEnd {
2514 ignore_newlines: false,
2515 };
2516 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2517 ignore_newlines: true,
2518 };
2519
2520 _ = editor.update(cx, |editor, window, cx| {
2521 editor.change_selections(None, window, cx, |s| {
2522 s.select_display_ranges([
2523 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2524 ])
2525 });
2526 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2527 assert_eq!(
2528 editor.buffer.read(cx).read(cx).text(),
2529 "one\n two\nthree\n four"
2530 );
2531 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2532 assert_eq!(
2533 editor.buffer.read(cx).read(cx).text(),
2534 "\n two\nthree\n four"
2535 );
2536 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2537 assert_eq!(
2538 editor.buffer.read(cx).read(cx).text(),
2539 "two\nthree\n four"
2540 );
2541 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2542 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2543 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2544 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2545 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2546 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2547 });
2548}
2549
2550#[gpui::test]
2551fn test_newline(cx: &mut TestAppContext) {
2552 init_test(cx, |_| {});
2553
2554 let editor = cx.add_window(|window, cx| {
2555 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2556 build_editor(buffer.clone(), window, cx)
2557 });
2558
2559 _ = editor.update(cx, |editor, window, cx| {
2560 editor.change_selections(None, window, cx, |s| {
2561 s.select_display_ranges([
2562 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2563 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2564 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2565 ])
2566 });
2567
2568 editor.newline(&Newline, window, cx);
2569 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2570 });
2571}
2572
2573#[gpui::test]
2574fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2575 init_test(cx, |_| {});
2576
2577 let editor = cx.add_window(|window, cx| {
2578 let buffer = MultiBuffer::build_simple(
2579 "
2580 a
2581 b(
2582 X
2583 )
2584 c(
2585 X
2586 )
2587 "
2588 .unindent()
2589 .as_str(),
2590 cx,
2591 );
2592 let mut editor = build_editor(buffer.clone(), window, cx);
2593 editor.change_selections(None, window, cx, |s| {
2594 s.select_ranges([
2595 Point::new(2, 4)..Point::new(2, 5),
2596 Point::new(5, 4)..Point::new(5, 5),
2597 ])
2598 });
2599 editor
2600 });
2601
2602 _ = editor.update(cx, |editor, window, cx| {
2603 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2604 editor.buffer.update(cx, |buffer, cx| {
2605 buffer.edit(
2606 [
2607 (Point::new(1, 2)..Point::new(3, 0), ""),
2608 (Point::new(4, 2)..Point::new(6, 0), ""),
2609 ],
2610 None,
2611 cx,
2612 );
2613 assert_eq!(
2614 buffer.read(cx).text(),
2615 "
2616 a
2617 b()
2618 c()
2619 "
2620 .unindent()
2621 );
2622 });
2623 assert_eq!(
2624 editor.selections.ranges(cx),
2625 &[
2626 Point::new(1, 2)..Point::new(1, 2),
2627 Point::new(2, 2)..Point::new(2, 2),
2628 ],
2629 );
2630
2631 editor.newline(&Newline, window, cx);
2632 assert_eq!(
2633 editor.text(cx),
2634 "
2635 a
2636 b(
2637 )
2638 c(
2639 )
2640 "
2641 .unindent()
2642 );
2643
2644 // The selections are moved after the inserted newlines
2645 assert_eq!(
2646 editor.selections.ranges(cx),
2647 &[
2648 Point::new(2, 0)..Point::new(2, 0),
2649 Point::new(4, 0)..Point::new(4, 0),
2650 ],
2651 );
2652 });
2653}
2654
2655#[gpui::test]
2656async fn test_newline_above(cx: &mut TestAppContext) {
2657 init_test(cx, |settings| {
2658 settings.defaults.tab_size = NonZeroU32::new(4)
2659 });
2660
2661 let language = Arc::new(
2662 Language::new(
2663 LanguageConfig::default(),
2664 Some(tree_sitter_rust::LANGUAGE.into()),
2665 )
2666 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2667 .unwrap(),
2668 );
2669
2670 let mut cx = EditorTestContext::new(cx).await;
2671 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2672 cx.set_state(indoc! {"
2673 const a: ˇA = (
2674 (ˇ
2675 «const_functionˇ»(ˇ),
2676 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2677 )ˇ
2678 ˇ);ˇ
2679 "});
2680
2681 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2682 cx.assert_editor_state(indoc! {"
2683 ˇ
2684 const a: A = (
2685 ˇ
2686 (
2687 ˇ
2688 ˇ
2689 const_function(),
2690 ˇ
2691 ˇ
2692 ˇ
2693 ˇ
2694 something_else,
2695 ˇ
2696 )
2697 ˇ
2698 ˇ
2699 );
2700 "});
2701}
2702
2703#[gpui::test]
2704async fn test_newline_below(cx: &mut TestAppContext) {
2705 init_test(cx, |settings| {
2706 settings.defaults.tab_size = NonZeroU32::new(4)
2707 });
2708
2709 let language = Arc::new(
2710 Language::new(
2711 LanguageConfig::default(),
2712 Some(tree_sitter_rust::LANGUAGE.into()),
2713 )
2714 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2715 .unwrap(),
2716 );
2717
2718 let mut cx = EditorTestContext::new(cx).await;
2719 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2720 cx.set_state(indoc! {"
2721 const a: ˇA = (
2722 (ˇ
2723 «const_functionˇ»(ˇ),
2724 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2725 )ˇ
2726 ˇ);ˇ
2727 "});
2728
2729 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2730 cx.assert_editor_state(indoc! {"
2731 const a: A = (
2732 ˇ
2733 (
2734 ˇ
2735 const_function(),
2736 ˇ
2737 ˇ
2738 something_else,
2739 ˇ
2740 ˇ
2741 ˇ
2742 ˇ
2743 )
2744 ˇ
2745 );
2746 ˇ
2747 ˇ
2748 "});
2749}
2750
2751#[gpui::test]
2752async fn test_newline_comments(cx: &mut TestAppContext) {
2753 init_test(cx, |settings| {
2754 settings.defaults.tab_size = NonZeroU32::new(4)
2755 });
2756
2757 let language = Arc::new(Language::new(
2758 LanguageConfig {
2759 line_comments: vec!["//".into()],
2760 ..LanguageConfig::default()
2761 },
2762 None,
2763 ));
2764 {
2765 let mut cx = EditorTestContext::new(cx).await;
2766 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2767 cx.set_state(indoc! {"
2768 // Fooˇ
2769 "});
2770
2771 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2772 cx.assert_editor_state(indoc! {"
2773 // Foo
2774 //ˇ
2775 "});
2776 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2777 cx.set_state(indoc! {"
2778 ˇ// Foo
2779 "});
2780 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2781 cx.assert_editor_state(indoc! {"
2782
2783 ˇ// Foo
2784 "});
2785 }
2786 // Ensure that comment continuations can be disabled.
2787 update_test_language_settings(cx, |settings| {
2788 settings.defaults.extend_comment_on_newline = Some(false);
2789 });
2790 let mut cx = EditorTestContext::new(cx).await;
2791 cx.set_state(indoc! {"
2792 // Fooˇ
2793 "});
2794 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2795 cx.assert_editor_state(indoc! {"
2796 // Foo
2797 ˇ
2798 "});
2799}
2800
2801#[gpui::test]
2802fn test_insert_with_old_selections(cx: &mut TestAppContext) {
2803 init_test(cx, |_| {});
2804
2805 let editor = cx.add_window(|window, cx| {
2806 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
2807 let mut editor = build_editor(buffer.clone(), window, cx);
2808 editor.change_selections(None, window, cx, |s| {
2809 s.select_ranges([3..4, 11..12, 19..20])
2810 });
2811 editor
2812 });
2813
2814 _ = editor.update(cx, |editor, window, cx| {
2815 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2816 editor.buffer.update(cx, |buffer, cx| {
2817 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
2818 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
2819 });
2820 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
2821
2822 editor.insert("Z", window, cx);
2823 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
2824
2825 // The selections are moved after the inserted characters
2826 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
2827 });
2828}
2829
2830#[gpui::test]
2831async fn test_tab(cx: &mut TestAppContext) {
2832 init_test(cx, |settings| {
2833 settings.defaults.tab_size = NonZeroU32::new(3)
2834 });
2835
2836 let mut cx = EditorTestContext::new(cx).await;
2837 cx.set_state(indoc! {"
2838 ˇabˇc
2839 ˇ🏀ˇ🏀ˇefg
2840 dˇ
2841 "});
2842 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2843 cx.assert_editor_state(indoc! {"
2844 ˇab ˇc
2845 ˇ🏀 ˇ🏀 ˇefg
2846 d ˇ
2847 "});
2848
2849 cx.set_state(indoc! {"
2850 a
2851 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2852 "});
2853 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2854 cx.assert_editor_state(indoc! {"
2855 a
2856 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2857 "});
2858}
2859
2860#[gpui::test]
2861async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
2862 init_test(cx, |_| {});
2863
2864 let mut cx = EditorTestContext::new(cx).await;
2865 let language = Arc::new(
2866 Language::new(
2867 LanguageConfig::default(),
2868 Some(tree_sitter_rust::LANGUAGE.into()),
2869 )
2870 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2871 .unwrap(),
2872 );
2873 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2874
2875 // cursors that are already at the suggested indent level insert
2876 // a soft tab. cursors that are to the left of the suggested indent
2877 // auto-indent their line.
2878 cx.set_state(indoc! {"
2879 ˇ
2880 const a: B = (
2881 c(
2882 d(
2883 ˇ
2884 )
2885 ˇ
2886 ˇ )
2887 );
2888 "});
2889 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2890 cx.assert_editor_state(indoc! {"
2891 ˇ
2892 const a: B = (
2893 c(
2894 d(
2895 ˇ
2896 )
2897 ˇ
2898 ˇ)
2899 );
2900 "});
2901
2902 // handle auto-indent when there are multiple cursors on the same line
2903 cx.set_state(indoc! {"
2904 const a: B = (
2905 c(
2906 ˇ ˇ
2907 ˇ )
2908 );
2909 "});
2910 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2911 cx.assert_editor_state(indoc! {"
2912 const a: B = (
2913 c(
2914 ˇ
2915 ˇ)
2916 );
2917 "});
2918}
2919
2920#[gpui::test]
2921async fn test_tab_with_mixed_whitespace(cx: &mut TestAppContext) {
2922 init_test(cx, |settings| {
2923 settings.defaults.tab_size = NonZeroU32::new(4)
2924 });
2925
2926 let language = Arc::new(
2927 Language::new(
2928 LanguageConfig::default(),
2929 Some(tree_sitter_rust::LANGUAGE.into()),
2930 )
2931 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
2932 .unwrap(),
2933 );
2934
2935 let mut cx = EditorTestContext::new(cx).await;
2936 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2937 cx.set_state(indoc! {"
2938 fn a() {
2939 if b {
2940 \t ˇc
2941 }
2942 }
2943 "});
2944
2945 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2946 cx.assert_editor_state(indoc! {"
2947 fn a() {
2948 if b {
2949 ˇc
2950 }
2951 }
2952 "});
2953}
2954
2955#[gpui::test]
2956async fn test_indent_outdent(cx: &mut TestAppContext) {
2957 init_test(cx, |settings| {
2958 settings.defaults.tab_size = NonZeroU32::new(4);
2959 });
2960
2961 let mut cx = EditorTestContext::new(cx).await;
2962
2963 cx.set_state(indoc! {"
2964 «oneˇ» «twoˇ»
2965 three
2966 four
2967 "});
2968 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2969 cx.assert_editor_state(indoc! {"
2970 «oneˇ» «twoˇ»
2971 three
2972 four
2973 "});
2974
2975 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
2976 cx.assert_editor_state(indoc! {"
2977 «oneˇ» «twoˇ»
2978 three
2979 four
2980 "});
2981
2982 // select across line ending
2983 cx.set_state(indoc! {"
2984 one two
2985 t«hree
2986 ˇ» four
2987 "});
2988 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2989 cx.assert_editor_state(indoc! {"
2990 one two
2991 t«hree
2992 ˇ» four
2993 "});
2994
2995 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
2996 cx.assert_editor_state(indoc! {"
2997 one two
2998 t«hree
2999 ˇ» four
3000 "});
3001
3002 // Ensure that indenting/outdenting works when the cursor is at column 0.
3003 cx.set_state(indoc! {"
3004 one two
3005 ˇthree
3006 four
3007 "});
3008 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3009 cx.assert_editor_state(indoc! {"
3010 one two
3011 ˇthree
3012 four
3013 "});
3014
3015 cx.set_state(indoc! {"
3016 one two
3017 ˇ three
3018 four
3019 "});
3020 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3021 cx.assert_editor_state(indoc! {"
3022 one two
3023 ˇthree
3024 four
3025 "});
3026}
3027
3028#[gpui::test]
3029async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3030 init_test(cx, |settings| {
3031 settings.defaults.hard_tabs = Some(true);
3032 });
3033
3034 let mut cx = EditorTestContext::new(cx).await;
3035
3036 // select two ranges on one line
3037 cx.set_state(indoc! {"
3038 «oneˇ» «twoˇ»
3039 three
3040 four
3041 "});
3042 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3043 cx.assert_editor_state(indoc! {"
3044 \t«oneˇ» «twoˇ»
3045 three
3046 four
3047 "});
3048 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3049 cx.assert_editor_state(indoc! {"
3050 \t\t«oneˇ» «twoˇ»
3051 three
3052 four
3053 "});
3054 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3055 cx.assert_editor_state(indoc! {"
3056 \t«oneˇ» «twoˇ»
3057 three
3058 four
3059 "});
3060 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3061 cx.assert_editor_state(indoc! {"
3062 «oneˇ» «twoˇ»
3063 three
3064 four
3065 "});
3066
3067 // select across a line ending
3068 cx.set_state(indoc! {"
3069 one two
3070 t«hree
3071 ˇ»four
3072 "});
3073 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3074 cx.assert_editor_state(indoc! {"
3075 one two
3076 \tt«hree
3077 ˇ»four
3078 "});
3079 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3080 cx.assert_editor_state(indoc! {"
3081 one two
3082 \t\tt«hree
3083 ˇ»four
3084 "});
3085 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3086 cx.assert_editor_state(indoc! {"
3087 one two
3088 \tt«hree
3089 ˇ»four
3090 "});
3091 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3092 cx.assert_editor_state(indoc! {"
3093 one two
3094 t«hree
3095 ˇ»four
3096 "});
3097
3098 // Ensure that indenting/outdenting works when the cursor is at column 0.
3099 cx.set_state(indoc! {"
3100 one two
3101 ˇthree
3102 four
3103 "});
3104 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3105 cx.assert_editor_state(indoc! {"
3106 one two
3107 ˇthree
3108 four
3109 "});
3110 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3111 cx.assert_editor_state(indoc! {"
3112 one two
3113 \tˇthree
3114 four
3115 "});
3116 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3117 cx.assert_editor_state(indoc! {"
3118 one two
3119 ˇthree
3120 four
3121 "});
3122}
3123
3124#[gpui::test]
3125fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3126 init_test(cx, |settings| {
3127 settings.languages.extend([
3128 (
3129 "TOML".into(),
3130 LanguageSettingsContent {
3131 tab_size: NonZeroU32::new(2),
3132 ..Default::default()
3133 },
3134 ),
3135 (
3136 "Rust".into(),
3137 LanguageSettingsContent {
3138 tab_size: NonZeroU32::new(4),
3139 ..Default::default()
3140 },
3141 ),
3142 ]);
3143 });
3144
3145 let toml_language = Arc::new(Language::new(
3146 LanguageConfig {
3147 name: "TOML".into(),
3148 ..Default::default()
3149 },
3150 None,
3151 ));
3152 let rust_language = Arc::new(Language::new(
3153 LanguageConfig {
3154 name: "Rust".into(),
3155 ..Default::default()
3156 },
3157 None,
3158 ));
3159
3160 let toml_buffer =
3161 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3162 let rust_buffer =
3163 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3164 let multibuffer = cx.new(|cx| {
3165 let mut multibuffer = MultiBuffer::new(ReadWrite);
3166 multibuffer.push_excerpts(
3167 toml_buffer.clone(),
3168 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3169 cx,
3170 );
3171 multibuffer.push_excerpts(
3172 rust_buffer.clone(),
3173 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3174 cx,
3175 );
3176 multibuffer
3177 });
3178
3179 cx.add_window(|window, cx| {
3180 let mut editor = build_editor(multibuffer, window, cx);
3181
3182 assert_eq!(
3183 editor.text(cx),
3184 indoc! {"
3185 a = 1
3186 b = 2
3187
3188 const c: usize = 3;
3189 "}
3190 );
3191
3192 select_ranges(
3193 &mut editor,
3194 indoc! {"
3195 «aˇ» = 1
3196 b = 2
3197
3198 «const c:ˇ» usize = 3;
3199 "},
3200 window,
3201 cx,
3202 );
3203
3204 editor.tab(&Tab, window, cx);
3205 assert_text_with_selections(
3206 &mut editor,
3207 indoc! {"
3208 «aˇ» = 1
3209 b = 2
3210
3211 «const c:ˇ» usize = 3;
3212 "},
3213 cx,
3214 );
3215 editor.backtab(&Backtab, window, cx);
3216 assert_text_with_selections(
3217 &mut editor,
3218 indoc! {"
3219 «aˇ» = 1
3220 b = 2
3221
3222 «const c:ˇ» usize = 3;
3223 "},
3224 cx,
3225 );
3226
3227 editor
3228 });
3229}
3230
3231#[gpui::test]
3232async fn test_backspace(cx: &mut TestAppContext) {
3233 init_test(cx, |_| {});
3234
3235 let mut cx = EditorTestContext::new(cx).await;
3236
3237 // Basic backspace
3238 cx.set_state(indoc! {"
3239 onˇe two three
3240 fou«rˇ» five six
3241 seven «ˇeight nine
3242 »ten
3243 "});
3244 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3245 cx.assert_editor_state(indoc! {"
3246 oˇe two three
3247 fouˇ five six
3248 seven ˇten
3249 "});
3250
3251 // Test backspace inside and around indents
3252 cx.set_state(indoc! {"
3253 zero
3254 ˇone
3255 ˇtwo
3256 ˇ ˇ ˇ three
3257 ˇ ˇ four
3258 "});
3259 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3260 cx.assert_editor_state(indoc! {"
3261 zero
3262 ˇone
3263 ˇtwo
3264 ˇ threeˇ four
3265 "});
3266}
3267
3268#[gpui::test]
3269async fn test_delete(cx: &mut TestAppContext) {
3270 init_test(cx, |_| {});
3271
3272 let mut cx = EditorTestContext::new(cx).await;
3273 cx.set_state(indoc! {"
3274 onˇe two three
3275 fou«rˇ» five six
3276 seven «ˇeight nine
3277 »ten
3278 "});
3279 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3280 cx.assert_editor_state(indoc! {"
3281 onˇ two three
3282 fouˇ five six
3283 seven ˇten
3284 "});
3285}
3286
3287#[gpui::test]
3288fn test_delete_line(cx: &mut TestAppContext) {
3289 init_test(cx, |_| {});
3290
3291 let editor = cx.add_window(|window, cx| {
3292 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3293 build_editor(buffer, window, cx)
3294 });
3295 _ = editor.update(cx, |editor, window, cx| {
3296 editor.change_selections(None, window, cx, |s| {
3297 s.select_display_ranges([
3298 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3299 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3300 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3301 ])
3302 });
3303 editor.delete_line(&DeleteLine, window, cx);
3304 assert_eq!(editor.display_text(cx), "ghi");
3305 assert_eq!(
3306 editor.selections.display_ranges(cx),
3307 vec![
3308 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3309 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3310 ]
3311 );
3312 });
3313
3314 let editor = cx.add_window(|window, cx| {
3315 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3316 build_editor(buffer, window, cx)
3317 });
3318 _ = editor.update(cx, |editor, window, cx| {
3319 editor.change_selections(None, window, cx, |s| {
3320 s.select_display_ranges([
3321 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3322 ])
3323 });
3324 editor.delete_line(&DeleteLine, window, cx);
3325 assert_eq!(editor.display_text(cx), "ghi\n");
3326 assert_eq!(
3327 editor.selections.display_ranges(cx),
3328 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3329 );
3330 });
3331}
3332
3333#[gpui::test]
3334fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3335 init_test(cx, |_| {});
3336
3337 cx.add_window(|window, cx| {
3338 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3339 let mut editor = build_editor(buffer.clone(), window, cx);
3340 let buffer = buffer.read(cx).as_singleton().unwrap();
3341
3342 assert_eq!(
3343 editor.selections.ranges::<Point>(cx),
3344 &[Point::new(0, 0)..Point::new(0, 0)]
3345 );
3346
3347 // When on single line, replace newline at end by space
3348 editor.join_lines(&JoinLines, window, cx);
3349 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3350 assert_eq!(
3351 editor.selections.ranges::<Point>(cx),
3352 &[Point::new(0, 3)..Point::new(0, 3)]
3353 );
3354
3355 // When multiple lines are selected, remove newlines that are spanned by the selection
3356 editor.change_selections(None, window, cx, |s| {
3357 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3358 });
3359 editor.join_lines(&JoinLines, window, cx);
3360 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3361 assert_eq!(
3362 editor.selections.ranges::<Point>(cx),
3363 &[Point::new(0, 11)..Point::new(0, 11)]
3364 );
3365
3366 // Undo should be transactional
3367 editor.undo(&Undo, window, cx);
3368 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3369 assert_eq!(
3370 editor.selections.ranges::<Point>(cx),
3371 &[Point::new(0, 5)..Point::new(2, 2)]
3372 );
3373
3374 // When joining an empty line don't insert a space
3375 editor.change_selections(None, window, cx, |s| {
3376 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3377 });
3378 editor.join_lines(&JoinLines, window, cx);
3379 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3380 assert_eq!(
3381 editor.selections.ranges::<Point>(cx),
3382 [Point::new(2, 3)..Point::new(2, 3)]
3383 );
3384
3385 // We can remove trailing newlines
3386 editor.join_lines(&JoinLines, window, cx);
3387 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3388 assert_eq!(
3389 editor.selections.ranges::<Point>(cx),
3390 [Point::new(2, 3)..Point::new(2, 3)]
3391 );
3392
3393 // We don't blow up on the last line
3394 editor.join_lines(&JoinLines, window, cx);
3395 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3396 assert_eq!(
3397 editor.selections.ranges::<Point>(cx),
3398 [Point::new(2, 3)..Point::new(2, 3)]
3399 );
3400
3401 // reset to test indentation
3402 editor.buffer.update(cx, |buffer, cx| {
3403 buffer.edit(
3404 [
3405 (Point::new(1, 0)..Point::new(1, 2), " "),
3406 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3407 ],
3408 None,
3409 cx,
3410 )
3411 });
3412
3413 // We remove any leading spaces
3414 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3415 editor.change_selections(None, window, cx, |s| {
3416 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3417 });
3418 editor.join_lines(&JoinLines, window, cx);
3419 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3420
3421 // We don't insert a space for a line containing only spaces
3422 editor.join_lines(&JoinLines, window, cx);
3423 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3424
3425 // We ignore any leading tabs
3426 editor.join_lines(&JoinLines, window, cx);
3427 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3428
3429 editor
3430 });
3431}
3432
3433#[gpui::test]
3434fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3435 init_test(cx, |_| {});
3436
3437 cx.add_window(|window, cx| {
3438 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3439 let mut editor = build_editor(buffer.clone(), window, cx);
3440 let buffer = buffer.read(cx).as_singleton().unwrap();
3441
3442 editor.change_selections(None, window, cx, |s| {
3443 s.select_ranges([
3444 Point::new(0, 2)..Point::new(1, 1),
3445 Point::new(1, 2)..Point::new(1, 2),
3446 Point::new(3, 1)..Point::new(3, 2),
3447 ])
3448 });
3449
3450 editor.join_lines(&JoinLines, window, cx);
3451 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3452
3453 assert_eq!(
3454 editor.selections.ranges::<Point>(cx),
3455 [
3456 Point::new(0, 7)..Point::new(0, 7),
3457 Point::new(1, 3)..Point::new(1, 3)
3458 ]
3459 );
3460 editor
3461 });
3462}
3463
3464#[gpui::test]
3465async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3466 init_test(cx, |_| {});
3467
3468 let mut cx = EditorTestContext::new(cx).await;
3469
3470 let diff_base = r#"
3471 Line 0
3472 Line 1
3473 Line 2
3474 Line 3
3475 "#
3476 .unindent();
3477
3478 cx.set_state(
3479 &r#"
3480 ˇLine 0
3481 Line 1
3482 Line 2
3483 Line 3
3484 "#
3485 .unindent(),
3486 );
3487
3488 cx.set_head_text(&diff_base);
3489 executor.run_until_parked();
3490
3491 // Join lines
3492 cx.update_editor(|editor, window, cx| {
3493 editor.join_lines(&JoinLines, window, cx);
3494 });
3495 executor.run_until_parked();
3496
3497 cx.assert_editor_state(
3498 &r#"
3499 Line 0ˇ Line 1
3500 Line 2
3501 Line 3
3502 "#
3503 .unindent(),
3504 );
3505 // Join again
3506 cx.update_editor(|editor, window, cx| {
3507 editor.join_lines(&JoinLines, window, cx);
3508 });
3509 executor.run_until_parked();
3510
3511 cx.assert_editor_state(
3512 &r#"
3513 Line 0 Line 1ˇ Line 2
3514 Line 3
3515 "#
3516 .unindent(),
3517 );
3518}
3519
3520#[gpui::test]
3521async fn test_custom_newlines_cause_no_false_positive_diffs(
3522 executor: BackgroundExecutor,
3523 cx: &mut TestAppContext,
3524) {
3525 init_test(cx, |_| {});
3526 let mut cx = EditorTestContext::new(cx).await;
3527 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3528 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3529 executor.run_until_parked();
3530
3531 cx.update_editor(|editor, window, cx| {
3532 let snapshot = editor.snapshot(window, cx);
3533 assert_eq!(
3534 snapshot
3535 .buffer_snapshot
3536 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3537 .collect::<Vec<_>>(),
3538 Vec::new(),
3539 "Should not have any diffs for files with custom newlines"
3540 );
3541 });
3542}
3543
3544#[gpui::test]
3545async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3546 init_test(cx, |_| {});
3547
3548 let mut cx = EditorTestContext::new(cx).await;
3549
3550 // Test sort_lines_case_insensitive()
3551 cx.set_state(indoc! {"
3552 «z
3553 y
3554 x
3555 Z
3556 Y
3557 Xˇ»
3558 "});
3559 cx.update_editor(|e, window, cx| {
3560 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3561 });
3562 cx.assert_editor_state(indoc! {"
3563 «x
3564 X
3565 y
3566 Y
3567 z
3568 Zˇ»
3569 "});
3570
3571 // Test reverse_lines()
3572 cx.set_state(indoc! {"
3573 «5
3574 4
3575 3
3576 2
3577 1ˇ»
3578 "});
3579 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
3580 cx.assert_editor_state(indoc! {"
3581 «1
3582 2
3583 3
3584 4
3585 5ˇ»
3586 "});
3587
3588 // Skip testing shuffle_line()
3589
3590 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
3591 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
3592
3593 // Don't manipulate when cursor is on single line, but expand the selection
3594 cx.set_state(indoc! {"
3595 ddˇdd
3596 ccc
3597 bb
3598 a
3599 "});
3600 cx.update_editor(|e, window, cx| {
3601 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3602 });
3603 cx.assert_editor_state(indoc! {"
3604 «ddddˇ»
3605 ccc
3606 bb
3607 a
3608 "});
3609
3610 // Basic manipulate case
3611 // Start selection moves to column 0
3612 // End of selection shrinks to fit shorter line
3613 cx.set_state(indoc! {"
3614 dd«d
3615 ccc
3616 bb
3617 aaaaaˇ»
3618 "});
3619 cx.update_editor(|e, window, cx| {
3620 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3621 });
3622 cx.assert_editor_state(indoc! {"
3623 «aaaaa
3624 bb
3625 ccc
3626 dddˇ»
3627 "});
3628
3629 // Manipulate case with newlines
3630 cx.set_state(indoc! {"
3631 dd«d
3632 ccc
3633
3634 bb
3635 aaaaa
3636
3637 ˇ»
3638 "});
3639 cx.update_editor(|e, window, cx| {
3640 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3641 });
3642 cx.assert_editor_state(indoc! {"
3643 «
3644
3645 aaaaa
3646 bb
3647 ccc
3648 dddˇ»
3649
3650 "});
3651
3652 // Adding new line
3653 cx.set_state(indoc! {"
3654 aa«a
3655 bbˇ»b
3656 "});
3657 cx.update_editor(|e, window, cx| {
3658 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
3659 });
3660 cx.assert_editor_state(indoc! {"
3661 «aaa
3662 bbb
3663 added_lineˇ»
3664 "});
3665
3666 // Removing line
3667 cx.set_state(indoc! {"
3668 aa«a
3669 bbbˇ»
3670 "});
3671 cx.update_editor(|e, window, cx| {
3672 e.manipulate_lines(window, cx, |lines| {
3673 lines.pop();
3674 })
3675 });
3676 cx.assert_editor_state(indoc! {"
3677 «aaaˇ»
3678 "});
3679
3680 // Removing all lines
3681 cx.set_state(indoc! {"
3682 aa«a
3683 bbbˇ»
3684 "});
3685 cx.update_editor(|e, window, cx| {
3686 e.manipulate_lines(window, cx, |lines| {
3687 lines.drain(..);
3688 })
3689 });
3690 cx.assert_editor_state(indoc! {"
3691 ˇ
3692 "});
3693}
3694
3695#[gpui::test]
3696async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
3697 init_test(cx, |_| {});
3698
3699 let mut cx = EditorTestContext::new(cx).await;
3700
3701 // Consider continuous selection as single selection
3702 cx.set_state(indoc! {"
3703 Aaa«aa
3704 cˇ»c«c
3705 bb
3706 aaaˇ»aa
3707 "});
3708 cx.update_editor(|e, window, cx| {
3709 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3710 });
3711 cx.assert_editor_state(indoc! {"
3712 «Aaaaa
3713 ccc
3714 bb
3715 aaaaaˇ»
3716 "});
3717
3718 cx.set_state(indoc! {"
3719 Aaa«aa
3720 cˇ»c«c
3721 bb
3722 aaaˇ»aa
3723 "});
3724 cx.update_editor(|e, window, cx| {
3725 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3726 });
3727 cx.assert_editor_state(indoc! {"
3728 «Aaaaa
3729 ccc
3730 bbˇ»
3731 "});
3732
3733 // Consider non continuous selection as distinct dedup operations
3734 cx.set_state(indoc! {"
3735 «aaaaa
3736 bb
3737 aaaaa
3738 aaaaaˇ»
3739
3740 aaa«aaˇ»
3741 "});
3742 cx.update_editor(|e, window, cx| {
3743 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3744 });
3745 cx.assert_editor_state(indoc! {"
3746 «aaaaa
3747 bbˇ»
3748
3749 «aaaaaˇ»
3750 "});
3751}
3752
3753#[gpui::test]
3754async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
3755 init_test(cx, |_| {});
3756
3757 let mut cx = EditorTestContext::new(cx).await;
3758
3759 cx.set_state(indoc! {"
3760 «Aaa
3761 aAa
3762 Aaaˇ»
3763 "});
3764 cx.update_editor(|e, window, cx| {
3765 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3766 });
3767 cx.assert_editor_state(indoc! {"
3768 «Aaa
3769 aAaˇ»
3770 "});
3771
3772 cx.set_state(indoc! {"
3773 «Aaa
3774 aAa
3775 aaAˇ»
3776 "});
3777 cx.update_editor(|e, window, cx| {
3778 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3779 });
3780 cx.assert_editor_state(indoc! {"
3781 «Aaaˇ»
3782 "});
3783}
3784
3785#[gpui::test]
3786async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
3787 init_test(cx, |_| {});
3788
3789 let mut cx = EditorTestContext::new(cx).await;
3790
3791 // Manipulate with multiple selections on a single line
3792 cx.set_state(indoc! {"
3793 dd«dd
3794 cˇ»c«c
3795 bb
3796 aaaˇ»aa
3797 "});
3798 cx.update_editor(|e, window, cx| {
3799 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3800 });
3801 cx.assert_editor_state(indoc! {"
3802 «aaaaa
3803 bb
3804 ccc
3805 ddddˇ»
3806 "});
3807
3808 // Manipulate with multiple disjoin selections
3809 cx.set_state(indoc! {"
3810 5«
3811 4
3812 3
3813 2
3814 1ˇ»
3815
3816 dd«dd
3817 ccc
3818 bb
3819 aaaˇ»aa
3820 "});
3821 cx.update_editor(|e, window, cx| {
3822 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3823 });
3824 cx.assert_editor_state(indoc! {"
3825 «1
3826 2
3827 3
3828 4
3829 5ˇ»
3830
3831 «aaaaa
3832 bb
3833 ccc
3834 ddddˇ»
3835 "});
3836
3837 // Adding lines on each selection
3838 cx.set_state(indoc! {"
3839 2«
3840 1ˇ»
3841
3842 bb«bb
3843 aaaˇ»aa
3844 "});
3845 cx.update_editor(|e, window, cx| {
3846 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
3847 });
3848 cx.assert_editor_state(indoc! {"
3849 «2
3850 1
3851 added lineˇ»
3852
3853 «bbbb
3854 aaaaa
3855 added lineˇ»
3856 "});
3857
3858 // Removing lines on each selection
3859 cx.set_state(indoc! {"
3860 2«
3861 1ˇ»
3862
3863 bb«bb
3864 aaaˇ»aa
3865 "});
3866 cx.update_editor(|e, window, cx| {
3867 e.manipulate_lines(window, cx, |lines| {
3868 lines.pop();
3869 })
3870 });
3871 cx.assert_editor_state(indoc! {"
3872 «2ˇ»
3873
3874 «bbbbˇ»
3875 "});
3876}
3877
3878#[gpui::test]
3879async fn test_manipulate_text(cx: &mut TestAppContext) {
3880 init_test(cx, |_| {});
3881
3882 let mut cx = EditorTestContext::new(cx).await;
3883
3884 // Test convert_to_upper_case()
3885 cx.set_state(indoc! {"
3886 «hello worldˇ»
3887 "});
3888 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3889 cx.assert_editor_state(indoc! {"
3890 «HELLO WORLDˇ»
3891 "});
3892
3893 // Test convert_to_lower_case()
3894 cx.set_state(indoc! {"
3895 «HELLO WORLDˇ»
3896 "});
3897 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
3898 cx.assert_editor_state(indoc! {"
3899 «hello worldˇ»
3900 "});
3901
3902 // Test multiple line, single selection case
3903 cx.set_state(indoc! {"
3904 «The quick brown
3905 fox jumps over
3906 the lazy dogˇ»
3907 "});
3908 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
3909 cx.assert_editor_state(indoc! {"
3910 «The Quick Brown
3911 Fox Jumps Over
3912 The Lazy Dogˇ»
3913 "});
3914
3915 // Test multiple line, single selection case
3916 cx.set_state(indoc! {"
3917 «The quick brown
3918 fox jumps over
3919 the lazy dogˇ»
3920 "});
3921 cx.update_editor(|e, window, cx| {
3922 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
3923 });
3924 cx.assert_editor_state(indoc! {"
3925 «TheQuickBrown
3926 FoxJumpsOver
3927 TheLazyDogˇ»
3928 "});
3929
3930 // From here on out, test more complex cases of manipulate_text()
3931
3932 // Test no selection case - should affect words cursors are in
3933 // Cursor at beginning, middle, and end of word
3934 cx.set_state(indoc! {"
3935 ˇhello big beauˇtiful worldˇ
3936 "});
3937 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3938 cx.assert_editor_state(indoc! {"
3939 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
3940 "});
3941
3942 // Test multiple selections on a single line and across multiple lines
3943 cx.set_state(indoc! {"
3944 «Theˇ» quick «brown
3945 foxˇ» jumps «overˇ»
3946 the «lazyˇ» dog
3947 "});
3948 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3949 cx.assert_editor_state(indoc! {"
3950 «THEˇ» quick «BROWN
3951 FOXˇ» jumps «OVERˇ»
3952 the «LAZYˇ» dog
3953 "});
3954
3955 // Test case where text length grows
3956 cx.set_state(indoc! {"
3957 «tschüߡ»
3958 "});
3959 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3960 cx.assert_editor_state(indoc! {"
3961 «TSCHÜSSˇ»
3962 "});
3963
3964 // Test to make sure we don't crash when text shrinks
3965 cx.set_state(indoc! {"
3966 aaa_bbbˇ
3967 "});
3968 cx.update_editor(|e, window, cx| {
3969 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
3970 });
3971 cx.assert_editor_state(indoc! {"
3972 «aaaBbbˇ»
3973 "});
3974
3975 // Test to make sure we all aware of the fact that each word can grow and shrink
3976 // Final selections should be aware of this fact
3977 cx.set_state(indoc! {"
3978 aaa_bˇbb bbˇb_ccc ˇccc_ddd
3979 "});
3980 cx.update_editor(|e, window, cx| {
3981 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
3982 });
3983 cx.assert_editor_state(indoc! {"
3984 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
3985 "});
3986
3987 cx.set_state(indoc! {"
3988 «hElLo, WoRld!ˇ»
3989 "});
3990 cx.update_editor(|e, window, cx| {
3991 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
3992 });
3993 cx.assert_editor_state(indoc! {"
3994 «HeLlO, wOrLD!ˇ»
3995 "});
3996}
3997
3998#[gpui::test]
3999fn test_duplicate_line(cx: &mut TestAppContext) {
4000 init_test(cx, |_| {});
4001
4002 let editor = cx.add_window(|window, cx| {
4003 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4004 build_editor(buffer, window, cx)
4005 });
4006 _ = editor.update(cx, |editor, window, cx| {
4007 editor.change_selections(None, window, cx, |s| {
4008 s.select_display_ranges([
4009 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4010 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4011 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4012 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4013 ])
4014 });
4015 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4016 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4017 assert_eq!(
4018 editor.selections.display_ranges(cx),
4019 vec![
4020 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4021 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4022 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4023 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4024 ]
4025 );
4026 });
4027
4028 let editor = cx.add_window(|window, cx| {
4029 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4030 build_editor(buffer, window, cx)
4031 });
4032 _ = editor.update(cx, |editor, window, cx| {
4033 editor.change_selections(None, window, cx, |s| {
4034 s.select_display_ranges([
4035 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4036 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4037 ])
4038 });
4039 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4040 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4041 assert_eq!(
4042 editor.selections.display_ranges(cx),
4043 vec![
4044 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4045 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4046 ]
4047 );
4048 });
4049
4050 // With `move_upwards` the selections stay in place, except for
4051 // the lines inserted above them
4052 let editor = cx.add_window(|window, cx| {
4053 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4054 build_editor(buffer, window, cx)
4055 });
4056 _ = editor.update(cx, |editor, window, cx| {
4057 editor.change_selections(None, window, cx, |s| {
4058 s.select_display_ranges([
4059 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4060 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4061 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4062 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4063 ])
4064 });
4065 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4066 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4067 assert_eq!(
4068 editor.selections.display_ranges(cx),
4069 vec![
4070 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4071 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4072 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4073 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4074 ]
4075 );
4076 });
4077
4078 let editor = cx.add_window(|window, cx| {
4079 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4080 build_editor(buffer, window, cx)
4081 });
4082 _ = editor.update(cx, |editor, window, cx| {
4083 editor.change_selections(None, window, cx, |s| {
4084 s.select_display_ranges([
4085 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4086 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4087 ])
4088 });
4089 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4090 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4091 assert_eq!(
4092 editor.selections.display_ranges(cx),
4093 vec![
4094 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4095 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4096 ]
4097 );
4098 });
4099
4100 let editor = cx.add_window(|window, cx| {
4101 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4102 build_editor(buffer, window, cx)
4103 });
4104 _ = editor.update(cx, |editor, window, cx| {
4105 editor.change_selections(None, window, cx, |s| {
4106 s.select_display_ranges([
4107 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4108 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4109 ])
4110 });
4111 editor.duplicate_selection(&DuplicateSelection, window, cx);
4112 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4113 assert_eq!(
4114 editor.selections.display_ranges(cx),
4115 vec![
4116 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4117 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4118 ]
4119 );
4120 });
4121}
4122
4123#[gpui::test]
4124fn test_move_line_up_down(cx: &mut TestAppContext) {
4125 init_test(cx, |_| {});
4126
4127 let editor = cx.add_window(|window, cx| {
4128 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4129 build_editor(buffer, window, cx)
4130 });
4131 _ = editor.update(cx, |editor, window, cx| {
4132 editor.fold_creases(
4133 vec![
4134 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4135 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4136 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4137 ],
4138 true,
4139 window,
4140 cx,
4141 );
4142 editor.change_selections(None, window, cx, |s| {
4143 s.select_display_ranges([
4144 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4145 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4146 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4147 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4148 ])
4149 });
4150 assert_eq!(
4151 editor.display_text(cx),
4152 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4153 );
4154
4155 editor.move_line_up(&MoveLineUp, window, cx);
4156 assert_eq!(
4157 editor.display_text(cx),
4158 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4159 );
4160 assert_eq!(
4161 editor.selections.display_ranges(cx),
4162 vec![
4163 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4164 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4165 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4166 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4167 ]
4168 );
4169 });
4170
4171 _ = editor.update(cx, |editor, window, cx| {
4172 editor.move_line_down(&MoveLineDown, window, cx);
4173 assert_eq!(
4174 editor.display_text(cx),
4175 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4176 );
4177 assert_eq!(
4178 editor.selections.display_ranges(cx),
4179 vec![
4180 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4181 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4182 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4183 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4184 ]
4185 );
4186 });
4187
4188 _ = editor.update(cx, |editor, window, cx| {
4189 editor.move_line_down(&MoveLineDown, window, cx);
4190 assert_eq!(
4191 editor.display_text(cx),
4192 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4193 );
4194 assert_eq!(
4195 editor.selections.display_ranges(cx),
4196 vec![
4197 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4198 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4199 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4200 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4201 ]
4202 );
4203 });
4204
4205 _ = editor.update(cx, |editor, window, cx| {
4206 editor.move_line_up(&MoveLineUp, window, cx);
4207 assert_eq!(
4208 editor.display_text(cx),
4209 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4210 );
4211 assert_eq!(
4212 editor.selections.display_ranges(cx),
4213 vec![
4214 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4215 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4216 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4217 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4218 ]
4219 );
4220 });
4221}
4222
4223#[gpui::test]
4224fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4225 init_test(cx, |_| {});
4226
4227 let editor = cx.add_window(|window, cx| {
4228 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4229 build_editor(buffer, window, cx)
4230 });
4231 _ = editor.update(cx, |editor, window, cx| {
4232 let snapshot = editor.buffer.read(cx).snapshot(cx);
4233 editor.insert_blocks(
4234 [BlockProperties {
4235 style: BlockStyle::Fixed,
4236 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4237 height: 1,
4238 render: Arc::new(|_| div().into_any()),
4239 priority: 0,
4240 }],
4241 Some(Autoscroll::fit()),
4242 cx,
4243 );
4244 editor.change_selections(None, window, cx, |s| {
4245 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4246 });
4247 editor.move_line_down(&MoveLineDown, window, cx);
4248 });
4249}
4250
4251#[gpui::test]
4252async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4253 init_test(cx, |_| {});
4254
4255 let mut cx = EditorTestContext::new(cx).await;
4256 cx.set_state(
4257 &"
4258 ˇzero
4259 one
4260 two
4261 three
4262 four
4263 five
4264 "
4265 .unindent(),
4266 );
4267
4268 // Create a four-line block that replaces three lines of text.
4269 cx.update_editor(|editor, window, cx| {
4270 let snapshot = editor.snapshot(window, cx);
4271 let snapshot = &snapshot.buffer_snapshot;
4272 let placement = BlockPlacement::Replace(
4273 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4274 );
4275 editor.insert_blocks(
4276 [BlockProperties {
4277 placement,
4278 height: 4,
4279 style: BlockStyle::Sticky,
4280 render: Arc::new(|_| gpui::div().into_any_element()),
4281 priority: 0,
4282 }],
4283 None,
4284 cx,
4285 );
4286 });
4287
4288 // Move down so that the cursor touches the block.
4289 cx.update_editor(|editor, window, cx| {
4290 editor.move_down(&Default::default(), window, cx);
4291 });
4292 cx.assert_editor_state(
4293 &"
4294 zero
4295 «one
4296 two
4297 threeˇ»
4298 four
4299 five
4300 "
4301 .unindent(),
4302 );
4303
4304 // Move down past the block.
4305 cx.update_editor(|editor, window, cx| {
4306 editor.move_down(&Default::default(), window, cx);
4307 });
4308 cx.assert_editor_state(
4309 &"
4310 zero
4311 one
4312 two
4313 three
4314 ˇfour
4315 five
4316 "
4317 .unindent(),
4318 );
4319}
4320
4321#[gpui::test]
4322fn test_transpose(cx: &mut TestAppContext) {
4323 init_test(cx, |_| {});
4324
4325 _ = cx.add_window(|window, cx| {
4326 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4327 editor.set_style(EditorStyle::default(), window, cx);
4328 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4329 editor.transpose(&Default::default(), window, cx);
4330 assert_eq!(editor.text(cx), "bac");
4331 assert_eq!(editor.selections.ranges(cx), [2..2]);
4332
4333 editor.transpose(&Default::default(), window, cx);
4334 assert_eq!(editor.text(cx), "bca");
4335 assert_eq!(editor.selections.ranges(cx), [3..3]);
4336
4337 editor.transpose(&Default::default(), window, cx);
4338 assert_eq!(editor.text(cx), "bac");
4339 assert_eq!(editor.selections.ranges(cx), [3..3]);
4340
4341 editor
4342 });
4343
4344 _ = cx.add_window(|window, cx| {
4345 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4346 editor.set_style(EditorStyle::default(), window, cx);
4347 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4348 editor.transpose(&Default::default(), window, cx);
4349 assert_eq!(editor.text(cx), "acb\nde");
4350 assert_eq!(editor.selections.ranges(cx), [3..3]);
4351
4352 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4353 editor.transpose(&Default::default(), window, cx);
4354 assert_eq!(editor.text(cx), "acbd\ne");
4355 assert_eq!(editor.selections.ranges(cx), [5..5]);
4356
4357 editor.transpose(&Default::default(), window, cx);
4358 assert_eq!(editor.text(cx), "acbde\n");
4359 assert_eq!(editor.selections.ranges(cx), [6..6]);
4360
4361 editor.transpose(&Default::default(), window, cx);
4362 assert_eq!(editor.text(cx), "acbd\ne");
4363 assert_eq!(editor.selections.ranges(cx), [6..6]);
4364
4365 editor
4366 });
4367
4368 _ = cx.add_window(|window, cx| {
4369 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4370 editor.set_style(EditorStyle::default(), window, cx);
4371 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4372 editor.transpose(&Default::default(), window, cx);
4373 assert_eq!(editor.text(cx), "bacd\ne");
4374 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4375
4376 editor.transpose(&Default::default(), window, cx);
4377 assert_eq!(editor.text(cx), "bcade\n");
4378 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4379
4380 editor.transpose(&Default::default(), window, cx);
4381 assert_eq!(editor.text(cx), "bcda\ne");
4382 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4383
4384 editor.transpose(&Default::default(), window, cx);
4385 assert_eq!(editor.text(cx), "bcade\n");
4386 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4387
4388 editor.transpose(&Default::default(), window, cx);
4389 assert_eq!(editor.text(cx), "bcaed\n");
4390 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4391
4392 editor
4393 });
4394
4395 _ = cx.add_window(|window, cx| {
4396 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4397 editor.set_style(EditorStyle::default(), window, cx);
4398 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4399 editor.transpose(&Default::default(), window, cx);
4400 assert_eq!(editor.text(cx), "🏀🍐✋");
4401 assert_eq!(editor.selections.ranges(cx), [8..8]);
4402
4403 editor.transpose(&Default::default(), window, cx);
4404 assert_eq!(editor.text(cx), "🏀✋🍐");
4405 assert_eq!(editor.selections.ranges(cx), [11..11]);
4406
4407 editor.transpose(&Default::default(), window, cx);
4408 assert_eq!(editor.text(cx), "🏀🍐✋");
4409 assert_eq!(editor.selections.ranges(cx), [11..11]);
4410
4411 editor
4412 });
4413}
4414
4415#[gpui::test]
4416async fn test_rewrap(cx: &mut TestAppContext) {
4417 init_test(cx, |settings| {
4418 settings.languages.extend([
4419 (
4420 "Markdown".into(),
4421 LanguageSettingsContent {
4422 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4423 ..Default::default()
4424 },
4425 ),
4426 (
4427 "Plain Text".into(),
4428 LanguageSettingsContent {
4429 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4430 ..Default::default()
4431 },
4432 ),
4433 ])
4434 });
4435
4436 let mut cx = EditorTestContext::new(cx).await;
4437
4438 let language_with_c_comments = Arc::new(Language::new(
4439 LanguageConfig {
4440 line_comments: vec!["// ".into()],
4441 ..LanguageConfig::default()
4442 },
4443 None,
4444 ));
4445 let language_with_pound_comments = Arc::new(Language::new(
4446 LanguageConfig {
4447 line_comments: vec!["# ".into()],
4448 ..LanguageConfig::default()
4449 },
4450 None,
4451 ));
4452 let markdown_language = Arc::new(Language::new(
4453 LanguageConfig {
4454 name: "Markdown".into(),
4455 ..LanguageConfig::default()
4456 },
4457 None,
4458 ));
4459 let language_with_doc_comments = Arc::new(Language::new(
4460 LanguageConfig {
4461 line_comments: vec!["// ".into(), "/// ".into()],
4462 ..LanguageConfig::default()
4463 },
4464 Some(tree_sitter_rust::LANGUAGE.into()),
4465 ));
4466
4467 let plaintext_language = Arc::new(Language::new(
4468 LanguageConfig {
4469 name: "Plain Text".into(),
4470 ..LanguageConfig::default()
4471 },
4472 None,
4473 ));
4474
4475 assert_rewrap(
4476 indoc! {"
4477 // ˇ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.
4478 "},
4479 indoc! {"
4480 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4481 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4482 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4483 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4484 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4485 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4486 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4487 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4488 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4489 // porttitor id. Aliquam id accumsan eros.
4490 "},
4491 language_with_c_comments.clone(),
4492 &mut cx,
4493 );
4494
4495 // Test that rewrapping works inside of a selection
4496 assert_rewrap(
4497 indoc! {"
4498 «// 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.ˇ»
4499 "},
4500 indoc! {"
4501 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4502 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4503 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4504 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4505 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4506 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4507 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4508 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4509 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4510 // porttitor id. Aliquam id accumsan eros.ˇ»
4511 "},
4512 language_with_c_comments.clone(),
4513 &mut cx,
4514 );
4515
4516 // Test that cursors that expand to the same region are collapsed.
4517 assert_rewrap(
4518 indoc! {"
4519 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4520 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4521 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4522 // ˇ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.
4523 "},
4524 indoc! {"
4525 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4526 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4527 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
4528 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4529 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
4530 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4531 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4532 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4533 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4534 // porttitor id. Aliquam id accumsan eros.
4535 "},
4536 language_with_c_comments.clone(),
4537 &mut cx,
4538 );
4539
4540 // Test that non-contiguous selections are treated separately.
4541 assert_rewrap(
4542 indoc! {"
4543 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4544 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4545 //
4546 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4547 // ˇ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.
4548 "},
4549 indoc! {"
4550 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4551 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4552 // auctor, eu lacinia sapien scelerisque.
4553 //
4554 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
4555 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4556 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
4557 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
4558 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
4559 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
4560 // vulputate turpis porttitor id. Aliquam id accumsan eros.
4561 "},
4562 language_with_c_comments.clone(),
4563 &mut cx,
4564 );
4565
4566 // Test that different comment prefixes are supported.
4567 assert_rewrap(
4568 indoc! {"
4569 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4570 "},
4571 indoc! {"
4572 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4573 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4574 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4575 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4576 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
4577 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
4578 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
4579 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
4580 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
4581 # accumsan eros.
4582 "},
4583 language_with_pound_comments.clone(),
4584 &mut cx,
4585 );
4586
4587 // Test that rewrapping is ignored outside of comments in most languages.
4588 assert_rewrap(
4589 indoc! {"
4590 /// Adds two numbers.
4591 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4592 fn add(a: u32, b: u32) -> u32 {
4593 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ˇ
4594 }
4595 "},
4596 indoc! {"
4597 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
4598 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4599 fn add(a: u32, b: u32) -> u32 {
4600 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ˇ
4601 }
4602 "},
4603 language_with_doc_comments.clone(),
4604 &mut cx,
4605 );
4606
4607 // Test that rewrapping works in Markdown and Plain Text languages.
4608 assert_rewrap(
4609 indoc! {"
4610 # Hello
4611
4612 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.
4613 "},
4614 indoc! {"
4615 # Hello
4616
4617 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4618 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4619 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4620 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4621 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4622 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4623 Integer sit amet scelerisque nisi.
4624 "},
4625 markdown_language,
4626 &mut cx,
4627 );
4628
4629 assert_rewrap(
4630 indoc! {"
4631 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.
4632 "},
4633 indoc! {"
4634 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4635 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4636 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4637 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4638 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4639 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4640 Integer sit amet scelerisque nisi.
4641 "},
4642 plaintext_language,
4643 &mut cx,
4644 );
4645
4646 // Test rewrapping unaligned comments in a selection.
4647 assert_rewrap(
4648 indoc! {"
4649 fn foo() {
4650 if true {
4651 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4652 // Praesent semper egestas tellus id dignissim.ˇ»
4653 do_something();
4654 } else {
4655 //
4656 }
4657 }
4658 "},
4659 indoc! {"
4660 fn foo() {
4661 if true {
4662 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4663 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4664 // egestas tellus id dignissim.ˇ»
4665 do_something();
4666 } else {
4667 //
4668 }
4669 }
4670 "},
4671 language_with_doc_comments.clone(),
4672 &mut cx,
4673 );
4674
4675 assert_rewrap(
4676 indoc! {"
4677 fn foo() {
4678 if true {
4679 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4680 // Praesent semper egestas tellus id dignissim.»
4681 do_something();
4682 } else {
4683 //
4684 }
4685
4686 }
4687 "},
4688 indoc! {"
4689 fn foo() {
4690 if true {
4691 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4692 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4693 // egestas tellus id dignissim.»
4694 do_something();
4695 } else {
4696 //
4697 }
4698
4699 }
4700 "},
4701 language_with_doc_comments.clone(),
4702 &mut cx,
4703 );
4704
4705 #[track_caller]
4706 fn assert_rewrap(
4707 unwrapped_text: &str,
4708 wrapped_text: &str,
4709 language: Arc<Language>,
4710 cx: &mut EditorTestContext,
4711 ) {
4712 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4713 cx.set_state(unwrapped_text);
4714 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
4715 cx.assert_editor_state(wrapped_text);
4716 }
4717}
4718
4719#[gpui::test]
4720async fn test_hard_wrap(cx: &mut TestAppContext) {
4721 init_test(cx, |_| {});
4722 let mut cx = EditorTestContext::new(cx).await;
4723
4724 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
4725 cx.update_editor(|editor, _, cx| {
4726 editor.set_hard_wrap(Some(14), cx);
4727 });
4728
4729 cx.set_state(indoc!(
4730 "
4731 one two three ˇ
4732 "
4733 ));
4734 cx.simulate_input("four");
4735 cx.run_until_parked();
4736
4737 cx.assert_editor_state(indoc!(
4738 "
4739 one two three
4740 fourˇ
4741 "
4742 ));
4743
4744 cx.update_editor(|editor, window, cx| {
4745 editor.newline(&Default::default(), window, cx);
4746 });
4747 cx.run_until_parked();
4748 cx.assert_editor_state(indoc!(
4749 "
4750 one two three
4751 four
4752 ˇ
4753 "
4754 ));
4755
4756 cx.simulate_input("five");
4757 cx.run_until_parked();
4758 cx.assert_editor_state(indoc!(
4759 "
4760 one two three
4761 four
4762 fiveˇ
4763 "
4764 ));
4765
4766 cx.update_editor(|editor, window, cx| {
4767 editor.newline(&Default::default(), window, cx);
4768 });
4769 cx.run_until_parked();
4770 cx.simulate_input("# ");
4771 cx.run_until_parked();
4772 cx.assert_editor_state(indoc!(
4773 "
4774 one two three
4775 four
4776 five
4777 # ˇ
4778 "
4779 ));
4780
4781 cx.update_editor(|editor, window, cx| {
4782 editor.newline(&Default::default(), window, cx);
4783 });
4784 cx.run_until_parked();
4785 cx.assert_editor_state(indoc!(
4786 "
4787 one two three
4788 four
4789 five
4790 #\x20
4791 #ˇ
4792 "
4793 ));
4794
4795 cx.simulate_input(" 6");
4796 cx.run_until_parked();
4797 cx.assert_editor_state(indoc!(
4798 "
4799 one two three
4800 four
4801 five
4802 #
4803 # 6ˇ
4804 "
4805 ));
4806}
4807
4808#[gpui::test]
4809async fn test_clipboard(cx: &mut TestAppContext) {
4810 init_test(cx, |_| {});
4811
4812 let mut cx = EditorTestContext::new(cx).await;
4813
4814 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
4815 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4816 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
4817
4818 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
4819 cx.set_state("two ˇfour ˇsix ˇ");
4820 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4821 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
4822
4823 // Paste again but with only two cursors. Since the number of cursors doesn't
4824 // match the number of slices in the clipboard, the entire clipboard text
4825 // is pasted at each cursor.
4826 cx.set_state("ˇtwo one✅ four three six five ˇ");
4827 cx.update_editor(|e, window, cx| {
4828 e.handle_input("( ", window, cx);
4829 e.paste(&Paste, window, cx);
4830 e.handle_input(") ", window, cx);
4831 });
4832 cx.assert_editor_state(
4833 &([
4834 "( one✅ ",
4835 "three ",
4836 "five ) ˇtwo one✅ four three six five ( one✅ ",
4837 "three ",
4838 "five ) ˇ",
4839 ]
4840 .join("\n")),
4841 );
4842
4843 // Cut with three selections, one of which is full-line.
4844 cx.set_state(indoc! {"
4845 1«2ˇ»3
4846 4ˇ567
4847 «8ˇ»9"});
4848 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4849 cx.assert_editor_state(indoc! {"
4850 1ˇ3
4851 ˇ9"});
4852
4853 // Paste with three selections, noticing how the copied selection that was full-line
4854 // gets inserted before the second cursor.
4855 cx.set_state(indoc! {"
4856 1ˇ3
4857 9ˇ
4858 «oˇ»ne"});
4859 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4860 cx.assert_editor_state(indoc! {"
4861 12ˇ3
4862 4567
4863 9ˇ
4864 8ˇne"});
4865
4866 // Copy with a single cursor only, which writes the whole line into the clipboard.
4867 cx.set_state(indoc! {"
4868 The quick brown
4869 fox juˇmps over
4870 the lazy dog"});
4871 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4872 assert_eq!(
4873 cx.read_from_clipboard()
4874 .and_then(|item| item.text().as_deref().map(str::to_string)),
4875 Some("fox jumps over\n".to_string())
4876 );
4877
4878 // Paste with three selections, noticing how the copied full-line selection is inserted
4879 // before the empty selections but replaces the selection that is non-empty.
4880 cx.set_state(indoc! {"
4881 Tˇhe quick brown
4882 «foˇ»x jumps over
4883 tˇhe lazy dog"});
4884 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4885 cx.assert_editor_state(indoc! {"
4886 fox jumps over
4887 Tˇhe quick brown
4888 fox jumps over
4889 ˇx jumps over
4890 fox jumps over
4891 tˇhe lazy dog"});
4892}
4893
4894#[gpui::test]
4895async fn test_copy_trim(cx: &mut TestAppContext) {
4896 init_test(cx, |_| {});
4897
4898 let mut cx = EditorTestContext::new(cx).await;
4899 cx.set_state(
4900 r#" «for selection in selections.iter() {
4901 let mut start = selection.start;
4902 let mut end = selection.end;
4903 let is_entire_line = selection.is_empty();
4904 if is_entire_line {
4905 start = Point::new(start.row, 0);ˇ»
4906 end = cmp::min(max_point, Point::new(end.row + 1, 0));
4907 }
4908 "#,
4909 );
4910 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4911 assert_eq!(
4912 cx.read_from_clipboard()
4913 .and_then(|item| item.text().as_deref().map(str::to_string)),
4914 Some(
4915 "for selection in selections.iter() {
4916 let mut start = selection.start;
4917 let mut end = selection.end;
4918 let is_entire_line = selection.is_empty();
4919 if is_entire_line {
4920 start = Point::new(start.row, 0);"
4921 .to_string()
4922 ),
4923 "Regular copying preserves all indentation selected",
4924 );
4925 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
4926 assert_eq!(
4927 cx.read_from_clipboard()
4928 .and_then(|item| item.text().as_deref().map(str::to_string)),
4929 Some(
4930 "for selection in selections.iter() {
4931let mut start = selection.start;
4932let mut end = selection.end;
4933let is_entire_line = selection.is_empty();
4934if is_entire_line {
4935 start = Point::new(start.row, 0);"
4936 .to_string()
4937 ),
4938 "Copying with stripping should strip all leading whitespaces"
4939 );
4940
4941 cx.set_state(
4942 r#" « for selection in selections.iter() {
4943 let mut start = selection.start;
4944 let mut end = selection.end;
4945 let is_entire_line = selection.is_empty();
4946 if is_entire_line {
4947 start = Point::new(start.row, 0);ˇ»
4948 end = cmp::min(max_point, Point::new(end.row + 1, 0));
4949 }
4950 "#,
4951 );
4952 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4953 assert_eq!(
4954 cx.read_from_clipboard()
4955 .and_then(|item| item.text().as_deref().map(str::to_string)),
4956 Some(
4957 " for selection in selections.iter() {
4958 let mut start = selection.start;
4959 let mut end = selection.end;
4960 let is_entire_line = selection.is_empty();
4961 if is_entire_line {
4962 start = Point::new(start.row, 0);"
4963 .to_string()
4964 ),
4965 "Regular copying preserves all indentation selected",
4966 );
4967 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
4968 assert_eq!(
4969 cx.read_from_clipboard()
4970 .and_then(|item| item.text().as_deref().map(str::to_string)),
4971 Some(
4972 "for selection in selections.iter() {
4973let mut start = selection.start;
4974let mut end = selection.end;
4975let is_entire_line = selection.is_empty();
4976if is_entire_line {
4977 start = Point::new(start.row, 0);"
4978 .to_string()
4979 ),
4980 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
4981 );
4982
4983 cx.set_state(
4984 r#" «ˇ for selection in selections.iter() {
4985 let mut start = selection.start;
4986 let mut end = selection.end;
4987 let is_entire_line = selection.is_empty();
4988 if is_entire_line {
4989 start = Point::new(start.row, 0);»
4990 end = cmp::min(max_point, Point::new(end.row + 1, 0));
4991 }
4992 "#,
4993 );
4994 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4995 assert_eq!(
4996 cx.read_from_clipboard()
4997 .and_then(|item| item.text().as_deref().map(str::to_string)),
4998 Some(
4999 " for selection in selections.iter() {
5000 let mut start = selection.start;
5001 let mut end = selection.end;
5002 let is_entire_line = selection.is_empty();
5003 if is_entire_line {
5004 start = Point::new(start.row, 0);"
5005 .to_string()
5006 ),
5007 "Regular copying for reverse selection works the same",
5008 );
5009 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5010 assert_eq!(
5011 cx.read_from_clipboard()
5012 .and_then(|item| item.text().as_deref().map(str::to_string)),
5013 Some(
5014 "for selection in selections.iter() {
5015let mut start = selection.start;
5016let mut end = selection.end;
5017let is_entire_line = selection.is_empty();
5018if is_entire_line {
5019 start = Point::new(start.row, 0);"
5020 .to_string()
5021 ),
5022 "Copying with stripping for reverse selection works the same"
5023 );
5024
5025 cx.set_state(
5026 r#" for selection «in selections.iter() {
5027 let mut start = selection.start;
5028 let mut end = selection.end;
5029 let is_entire_line = selection.is_empty();
5030 if is_entire_line {
5031 start = Point::new(start.row, 0);ˇ»
5032 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5033 }
5034 "#,
5035 );
5036 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5037 assert_eq!(
5038 cx.read_from_clipboard()
5039 .and_then(|item| item.text().as_deref().map(str::to_string)),
5040 Some(
5041 "in selections.iter() {
5042 let mut start = selection.start;
5043 let mut end = selection.end;
5044 let is_entire_line = selection.is_empty();
5045 if is_entire_line {
5046 start = Point::new(start.row, 0);"
5047 .to_string()
5048 ),
5049 "When selecting past the indent, the copying works as usual",
5050 );
5051 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5052 assert_eq!(
5053 cx.read_from_clipboard()
5054 .and_then(|item| item.text().as_deref().map(str::to_string)),
5055 Some(
5056 "in selections.iter() {
5057 let mut start = selection.start;
5058 let mut end = selection.end;
5059 let is_entire_line = selection.is_empty();
5060 if is_entire_line {
5061 start = Point::new(start.row, 0);"
5062 .to_string()
5063 ),
5064 "When selecting past the indent, nothing is trimmed"
5065 );
5066}
5067
5068#[gpui::test]
5069async fn test_paste_multiline(cx: &mut TestAppContext) {
5070 init_test(cx, |_| {});
5071
5072 let mut cx = EditorTestContext::new(cx).await;
5073 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5074
5075 // Cut an indented block, without the leading whitespace.
5076 cx.set_state(indoc! {"
5077 const a: B = (
5078 c(),
5079 «d(
5080 e,
5081 f
5082 )ˇ»
5083 );
5084 "});
5085 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5086 cx.assert_editor_state(indoc! {"
5087 const a: B = (
5088 c(),
5089 ˇ
5090 );
5091 "});
5092
5093 // Paste it at the same position.
5094 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5095 cx.assert_editor_state(indoc! {"
5096 const a: B = (
5097 c(),
5098 d(
5099 e,
5100 f
5101 )ˇ
5102 );
5103 "});
5104
5105 // Paste it at a line with a lower indent level.
5106 cx.set_state(indoc! {"
5107 ˇ
5108 const a: B = (
5109 c(),
5110 );
5111 "});
5112 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5113 cx.assert_editor_state(indoc! {"
5114 d(
5115 e,
5116 f
5117 )ˇ
5118 const a: B = (
5119 c(),
5120 );
5121 "});
5122
5123 // Cut an indented block, with the leading whitespace.
5124 cx.set_state(indoc! {"
5125 const a: B = (
5126 c(),
5127 « d(
5128 e,
5129 f
5130 )
5131 ˇ»);
5132 "});
5133 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5134 cx.assert_editor_state(indoc! {"
5135 const a: B = (
5136 c(),
5137 ˇ);
5138 "});
5139
5140 // Paste it at the same position.
5141 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5142 cx.assert_editor_state(indoc! {"
5143 const a: B = (
5144 c(),
5145 d(
5146 e,
5147 f
5148 )
5149 ˇ);
5150 "});
5151
5152 // Paste it at a line with a higher indent level.
5153 cx.set_state(indoc! {"
5154 const a: B = (
5155 c(),
5156 d(
5157 e,
5158 fˇ
5159 )
5160 );
5161 "});
5162 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5163 cx.assert_editor_state(indoc! {"
5164 const a: B = (
5165 c(),
5166 d(
5167 e,
5168 f d(
5169 e,
5170 f
5171 )
5172 ˇ
5173 )
5174 );
5175 "});
5176
5177 // Copy an indented block, starting mid-line
5178 cx.set_state(indoc! {"
5179 const a: B = (
5180 c(),
5181 somethin«g(
5182 e,
5183 f
5184 )ˇ»
5185 );
5186 "});
5187 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5188
5189 // Paste it on a line with a lower indent level
5190 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
5191 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5192 cx.assert_editor_state(indoc! {"
5193 const a: B = (
5194 c(),
5195 something(
5196 e,
5197 f
5198 )
5199 );
5200 g(
5201 e,
5202 f
5203 )ˇ"});
5204}
5205
5206#[gpui::test]
5207async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
5208 init_test(cx, |_| {});
5209
5210 cx.write_to_clipboard(ClipboardItem::new_string(
5211 " d(\n e\n );\n".into(),
5212 ));
5213
5214 let mut cx = EditorTestContext::new(cx).await;
5215 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5216
5217 cx.set_state(indoc! {"
5218 fn a() {
5219 b();
5220 if c() {
5221 ˇ
5222 }
5223 }
5224 "});
5225
5226 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5227 cx.assert_editor_state(indoc! {"
5228 fn a() {
5229 b();
5230 if c() {
5231 d(
5232 e
5233 );
5234 ˇ
5235 }
5236 }
5237 "});
5238
5239 cx.set_state(indoc! {"
5240 fn a() {
5241 b();
5242 ˇ
5243 }
5244 "});
5245
5246 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5247 cx.assert_editor_state(indoc! {"
5248 fn a() {
5249 b();
5250 d(
5251 e
5252 );
5253 ˇ
5254 }
5255 "});
5256}
5257
5258#[gpui::test]
5259fn test_select_all(cx: &mut TestAppContext) {
5260 init_test(cx, |_| {});
5261
5262 let editor = cx.add_window(|window, cx| {
5263 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
5264 build_editor(buffer, window, cx)
5265 });
5266 _ = editor.update(cx, |editor, window, cx| {
5267 editor.select_all(&SelectAll, window, cx);
5268 assert_eq!(
5269 editor.selections.display_ranges(cx),
5270 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
5271 );
5272 });
5273}
5274
5275#[gpui::test]
5276fn test_select_line(cx: &mut TestAppContext) {
5277 init_test(cx, |_| {});
5278
5279 let editor = cx.add_window(|window, cx| {
5280 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
5281 build_editor(buffer, window, cx)
5282 });
5283 _ = editor.update(cx, |editor, window, cx| {
5284 editor.change_selections(None, window, cx, |s| {
5285 s.select_display_ranges([
5286 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5287 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5288 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5289 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
5290 ])
5291 });
5292 editor.select_line(&SelectLine, window, cx);
5293 assert_eq!(
5294 editor.selections.display_ranges(cx),
5295 vec![
5296 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
5297 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
5298 ]
5299 );
5300 });
5301
5302 _ = editor.update(cx, |editor, window, cx| {
5303 editor.select_line(&SelectLine, window, cx);
5304 assert_eq!(
5305 editor.selections.display_ranges(cx),
5306 vec![
5307 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
5308 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
5309 ]
5310 );
5311 });
5312
5313 _ = editor.update(cx, |editor, window, cx| {
5314 editor.select_line(&SelectLine, window, cx);
5315 assert_eq!(
5316 editor.selections.display_ranges(cx),
5317 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
5318 );
5319 });
5320}
5321
5322#[gpui::test]
5323async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
5324 init_test(cx, |_| {});
5325 let mut cx = EditorTestContext::new(cx).await;
5326
5327 #[track_caller]
5328 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
5329 cx.set_state(initial_state);
5330 cx.update_editor(|e, window, cx| {
5331 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
5332 });
5333 cx.assert_editor_state(expected_state);
5334 }
5335
5336 // Selection starts and ends at the middle of lines, left-to-right
5337 test(
5338 &mut cx,
5339 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
5340 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5341 );
5342 // Same thing, right-to-left
5343 test(
5344 &mut cx,
5345 "aa\nb«b\ncc\ndd\neˇ»e\nff",
5346 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5347 );
5348
5349 // Whole buffer, left-to-right, last line *doesn't* end with newline
5350 test(
5351 &mut cx,
5352 "«ˇaa\nbb\ncc\ndd\nee\nff»",
5353 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5354 );
5355 // Same thing, right-to-left
5356 test(
5357 &mut cx,
5358 "«aa\nbb\ncc\ndd\nee\nffˇ»",
5359 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5360 );
5361
5362 // Whole buffer, left-to-right, last line ends with newline
5363 test(
5364 &mut cx,
5365 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
5366 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5367 );
5368 // Same thing, right-to-left
5369 test(
5370 &mut cx,
5371 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
5372 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5373 );
5374
5375 // Starts at the end of a line, ends at the start of another
5376 test(
5377 &mut cx,
5378 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
5379 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
5380 );
5381}
5382
5383#[gpui::test]
5384async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
5385 init_test(cx, |_| {});
5386
5387 let editor = cx.add_window(|window, cx| {
5388 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
5389 build_editor(buffer, window, cx)
5390 });
5391
5392 // setup
5393 _ = editor.update(cx, |editor, window, cx| {
5394 editor.fold_creases(
5395 vec![
5396 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5397 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5398 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5399 ],
5400 true,
5401 window,
5402 cx,
5403 );
5404 assert_eq!(
5405 editor.display_text(cx),
5406 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5407 );
5408 });
5409
5410 _ = editor.update(cx, |editor, window, cx| {
5411 editor.change_selections(None, window, cx, |s| {
5412 s.select_display_ranges([
5413 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5414 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5415 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5416 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
5417 ])
5418 });
5419 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5420 assert_eq!(
5421 editor.display_text(cx),
5422 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5423 );
5424 });
5425 EditorTestContext::for_editor(editor, cx)
5426 .await
5427 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
5428
5429 _ = editor.update(cx, |editor, window, cx| {
5430 editor.change_selections(None, window, cx, |s| {
5431 s.select_display_ranges([
5432 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
5433 ])
5434 });
5435 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5436 assert_eq!(
5437 editor.display_text(cx),
5438 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
5439 );
5440 assert_eq!(
5441 editor.selections.display_ranges(cx),
5442 [
5443 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
5444 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
5445 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
5446 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
5447 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
5448 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
5449 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
5450 ]
5451 );
5452 });
5453 EditorTestContext::for_editor(editor, cx)
5454 .await
5455 .assert_editor_state(
5456 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
5457 );
5458}
5459
5460#[gpui::test]
5461async fn test_add_selection_above_below(cx: &mut TestAppContext) {
5462 init_test(cx, |_| {});
5463
5464 let mut cx = EditorTestContext::new(cx).await;
5465
5466 cx.set_state(indoc!(
5467 r#"abc
5468 defˇghi
5469
5470 jk
5471 nlmo
5472 "#
5473 ));
5474
5475 cx.update_editor(|editor, window, cx| {
5476 editor.add_selection_above(&Default::default(), window, cx);
5477 });
5478
5479 cx.assert_editor_state(indoc!(
5480 r#"abcˇ
5481 defˇghi
5482
5483 jk
5484 nlmo
5485 "#
5486 ));
5487
5488 cx.update_editor(|editor, window, cx| {
5489 editor.add_selection_above(&Default::default(), window, cx);
5490 });
5491
5492 cx.assert_editor_state(indoc!(
5493 r#"abcˇ
5494 defˇghi
5495
5496 jk
5497 nlmo
5498 "#
5499 ));
5500
5501 cx.update_editor(|editor, window, cx| {
5502 editor.add_selection_below(&Default::default(), window, cx);
5503 });
5504
5505 cx.assert_editor_state(indoc!(
5506 r#"abc
5507 defˇghi
5508
5509 jk
5510 nlmo
5511 "#
5512 ));
5513
5514 cx.update_editor(|editor, window, cx| {
5515 editor.undo_selection(&Default::default(), window, cx);
5516 });
5517
5518 cx.assert_editor_state(indoc!(
5519 r#"abcˇ
5520 defˇghi
5521
5522 jk
5523 nlmo
5524 "#
5525 ));
5526
5527 cx.update_editor(|editor, window, cx| {
5528 editor.redo_selection(&Default::default(), window, cx);
5529 });
5530
5531 cx.assert_editor_state(indoc!(
5532 r#"abc
5533 defˇghi
5534
5535 jk
5536 nlmo
5537 "#
5538 ));
5539
5540 cx.update_editor(|editor, window, cx| {
5541 editor.add_selection_below(&Default::default(), window, cx);
5542 });
5543
5544 cx.assert_editor_state(indoc!(
5545 r#"abc
5546 defˇghi
5547
5548 jk
5549 nlmˇo
5550 "#
5551 ));
5552
5553 cx.update_editor(|editor, window, cx| {
5554 editor.add_selection_below(&Default::default(), window, cx);
5555 });
5556
5557 cx.assert_editor_state(indoc!(
5558 r#"abc
5559 defˇghi
5560
5561 jk
5562 nlmˇo
5563 "#
5564 ));
5565
5566 // change selections
5567 cx.set_state(indoc!(
5568 r#"abc
5569 def«ˇg»hi
5570
5571 jk
5572 nlmo
5573 "#
5574 ));
5575
5576 cx.update_editor(|editor, window, cx| {
5577 editor.add_selection_below(&Default::default(), window, cx);
5578 });
5579
5580 cx.assert_editor_state(indoc!(
5581 r#"abc
5582 def«ˇg»hi
5583
5584 jk
5585 nlm«ˇo»
5586 "#
5587 ));
5588
5589 cx.update_editor(|editor, window, cx| {
5590 editor.add_selection_below(&Default::default(), window, cx);
5591 });
5592
5593 cx.assert_editor_state(indoc!(
5594 r#"abc
5595 def«ˇg»hi
5596
5597 jk
5598 nlm«ˇo»
5599 "#
5600 ));
5601
5602 cx.update_editor(|editor, window, cx| {
5603 editor.add_selection_above(&Default::default(), window, cx);
5604 });
5605
5606 cx.assert_editor_state(indoc!(
5607 r#"abc
5608 def«ˇg»hi
5609
5610 jk
5611 nlmo
5612 "#
5613 ));
5614
5615 cx.update_editor(|editor, window, cx| {
5616 editor.add_selection_above(&Default::default(), window, cx);
5617 });
5618
5619 cx.assert_editor_state(indoc!(
5620 r#"abc
5621 def«ˇg»hi
5622
5623 jk
5624 nlmo
5625 "#
5626 ));
5627
5628 // Change selections again
5629 cx.set_state(indoc!(
5630 r#"a«bc
5631 defgˇ»hi
5632
5633 jk
5634 nlmo
5635 "#
5636 ));
5637
5638 cx.update_editor(|editor, window, cx| {
5639 editor.add_selection_below(&Default::default(), window, cx);
5640 });
5641
5642 cx.assert_editor_state(indoc!(
5643 r#"a«bcˇ»
5644 d«efgˇ»hi
5645
5646 j«kˇ»
5647 nlmo
5648 "#
5649 ));
5650
5651 cx.update_editor(|editor, window, cx| {
5652 editor.add_selection_below(&Default::default(), window, cx);
5653 });
5654 cx.assert_editor_state(indoc!(
5655 r#"a«bcˇ»
5656 d«efgˇ»hi
5657
5658 j«kˇ»
5659 n«lmoˇ»
5660 "#
5661 ));
5662 cx.update_editor(|editor, window, cx| {
5663 editor.add_selection_above(&Default::default(), window, cx);
5664 });
5665
5666 cx.assert_editor_state(indoc!(
5667 r#"a«bcˇ»
5668 d«efgˇ»hi
5669
5670 j«kˇ»
5671 nlmo
5672 "#
5673 ));
5674
5675 // Change selections again
5676 cx.set_state(indoc!(
5677 r#"abc
5678 d«ˇefghi
5679
5680 jk
5681 nlm»o
5682 "#
5683 ));
5684
5685 cx.update_editor(|editor, window, cx| {
5686 editor.add_selection_above(&Default::default(), window, cx);
5687 });
5688
5689 cx.assert_editor_state(indoc!(
5690 r#"a«ˇbc»
5691 d«ˇef»ghi
5692
5693 j«ˇk»
5694 n«ˇlm»o
5695 "#
5696 ));
5697
5698 cx.update_editor(|editor, window, cx| {
5699 editor.add_selection_below(&Default::default(), window, cx);
5700 });
5701
5702 cx.assert_editor_state(indoc!(
5703 r#"abc
5704 d«ˇef»ghi
5705
5706 j«ˇk»
5707 n«ˇlm»o
5708 "#
5709 ));
5710}
5711
5712#[gpui::test]
5713async fn test_select_next(cx: &mut TestAppContext) {
5714 init_test(cx, |_| {});
5715
5716 let mut cx = EditorTestContext::new(cx).await;
5717 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5718
5719 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5720 .unwrap();
5721 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5722
5723 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5724 .unwrap();
5725 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5726
5727 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5728 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5729
5730 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5731 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5732
5733 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5734 .unwrap();
5735 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5736
5737 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5738 .unwrap();
5739 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5740}
5741
5742#[gpui::test]
5743async fn test_select_all_matches(cx: &mut TestAppContext) {
5744 init_test(cx, |_| {});
5745
5746 let mut cx = EditorTestContext::new(cx).await;
5747
5748 // Test caret-only selections
5749 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5750 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5751 .unwrap();
5752 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5753
5754 // Test left-to-right selections
5755 cx.set_state("abc\n«abcˇ»\nabc");
5756 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5757 .unwrap();
5758 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
5759
5760 // Test right-to-left selections
5761 cx.set_state("abc\n«ˇabc»\nabc");
5762 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5763 .unwrap();
5764 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
5765
5766 // Test selecting whitespace with caret selection
5767 cx.set_state("abc\nˇ abc\nabc");
5768 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5769 .unwrap();
5770 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
5771
5772 // Test selecting whitespace with left-to-right selection
5773 cx.set_state("abc\n«ˇ »abc\nabc");
5774 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5775 .unwrap();
5776 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
5777
5778 // Test no matches with right-to-left selection
5779 cx.set_state("abc\n« ˇ»abc\nabc");
5780 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5781 .unwrap();
5782 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
5783}
5784
5785#[gpui::test]
5786async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
5787 init_test(cx, |_| {});
5788
5789 let mut cx = EditorTestContext::new(cx).await;
5790 cx.set_state(
5791 r#"let foo = 2;
5792lˇet foo = 2;
5793let fooˇ = 2;
5794let foo = 2;
5795let foo = ˇ2;"#,
5796 );
5797
5798 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5799 .unwrap();
5800 cx.assert_editor_state(
5801 r#"let foo = 2;
5802«letˇ» foo = 2;
5803let «fooˇ» = 2;
5804let foo = 2;
5805let foo = «2ˇ»;"#,
5806 );
5807
5808 // noop for multiple selections with different contents
5809 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5810 .unwrap();
5811 cx.assert_editor_state(
5812 r#"let foo = 2;
5813«letˇ» foo = 2;
5814let «fooˇ» = 2;
5815let foo = 2;
5816let foo = «2ˇ»;"#,
5817 );
5818}
5819
5820#[gpui::test]
5821async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
5822 init_test(cx, |_| {});
5823
5824 let mut cx =
5825 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
5826
5827 cx.assert_editor_state(indoc! {"
5828 ˇbbb
5829 ccc
5830
5831 bbb
5832 ccc
5833 "});
5834 cx.dispatch_action(SelectPrevious::default());
5835 cx.assert_editor_state(indoc! {"
5836 «bbbˇ»
5837 ccc
5838
5839 bbb
5840 ccc
5841 "});
5842 cx.dispatch_action(SelectPrevious::default());
5843 cx.assert_editor_state(indoc! {"
5844 «bbbˇ»
5845 ccc
5846
5847 «bbbˇ»
5848 ccc
5849 "});
5850}
5851
5852#[gpui::test]
5853async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
5854 init_test(cx, |_| {});
5855
5856 let mut cx = EditorTestContext::new(cx).await;
5857 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5858
5859 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5860 .unwrap();
5861 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5862
5863 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5864 .unwrap();
5865 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
5866
5867 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5868 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5869
5870 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5871 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
5872
5873 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5874 .unwrap();
5875 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
5876
5877 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5878 .unwrap();
5879 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndef«abcˇ»\n«abcˇ»");
5880
5881 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5882 .unwrap();
5883 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
5884}
5885
5886#[gpui::test]
5887async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
5888 init_test(cx, |_| {});
5889
5890 let mut cx = EditorTestContext::new(cx).await;
5891 cx.set_state("aˇ");
5892
5893 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5894 .unwrap();
5895 cx.assert_editor_state("«aˇ»");
5896 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5897 .unwrap();
5898 cx.assert_editor_state("«aˇ»");
5899}
5900
5901#[gpui::test]
5902async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
5903 init_test(cx, |_| {});
5904
5905 let mut cx = EditorTestContext::new(cx).await;
5906 cx.set_state(
5907 r#"let foo = 2;
5908lˇet foo = 2;
5909let fooˇ = 2;
5910let foo = 2;
5911let foo = ˇ2;"#,
5912 );
5913
5914 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5915 .unwrap();
5916 cx.assert_editor_state(
5917 r#"let foo = 2;
5918«letˇ» foo = 2;
5919let «fooˇ» = 2;
5920let foo = 2;
5921let foo = «2ˇ»;"#,
5922 );
5923
5924 // noop for multiple selections with different contents
5925 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5926 .unwrap();
5927 cx.assert_editor_state(
5928 r#"let foo = 2;
5929«letˇ» foo = 2;
5930let «fooˇ» = 2;
5931let foo = 2;
5932let foo = «2ˇ»;"#,
5933 );
5934}
5935
5936#[gpui::test]
5937async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
5938 init_test(cx, |_| {});
5939
5940 let mut cx = EditorTestContext::new(cx).await;
5941 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
5942
5943 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5944 .unwrap();
5945 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
5946
5947 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5948 .unwrap();
5949 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
5950
5951 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5952 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
5953
5954 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5955 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
5956
5957 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5958 .unwrap();
5959 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
5960
5961 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5962 .unwrap();
5963 cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
5964}
5965
5966#[gpui::test]
5967async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
5968 init_test(cx, |_| {});
5969
5970 let language = Arc::new(Language::new(
5971 LanguageConfig::default(),
5972 Some(tree_sitter_rust::LANGUAGE.into()),
5973 ));
5974
5975 let text = r#"
5976 use mod1::mod2::{mod3, mod4};
5977
5978 fn fn_1(param1: bool, param2: &str) {
5979 let var1 = "text";
5980 }
5981 "#
5982 .unindent();
5983
5984 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
5985 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
5986 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
5987
5988 editor
5989 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
5990 .await;
5991
5992 editor.update_in(cx, |editor, window, cx| {
5993 editor.change_selections(None, window, cx, |s| {
5994 s.select_display_ranges([
5995 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
5996 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
5997 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
5998 ]);
5999 });
6000 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6001 });
6002 editor.update(cx, |editor, cx| {
6003 assert_text_with_selections(
6004 editor,
6005 indoc! {r#"
6006 use mod1::mod2::{mod3, «mod4ˇ»};
6007
6008 fn fn_1«ˇ(param1: bool, param2: &str)» {
6009 let var1 = "«ˇtext»";
6010 }
6011 "#},
6012 cx,
6013 );
6014 });
6015
6016 editor.update_in(cx, |editor, window, cx| {
6017 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6018 });
6019 editor.update(cx, |editor, cx| {
6020 assert_text_with_selections(
6021 editor,
6022 indoc! {r#"
6023 use mod1::mod2::«{mod3, mod4}ˇ»;
6024
6025 «ˇfn fn_1(param1: bool, param2: &str) {
6026 let var1 = "text";
6027 }»
6028 "#},
6029 cx,
6030 );
6031 });
6032
6033 editor.update_in(cx, |editor, window, cx| {
6034 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6035 });
6036 assert_eq!(
6037 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6038 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6039 );
6040
6041 // Trying to expand the selected syntax node one more time has no effect.
6042 editor.update_in(cx, |editor, window, cx| {
6043 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6044 });
6045 assert_eq!(
6046 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6047 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6048 );
6049
6050 editor.update_in(cx, |editor, window, cx| {
6051 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6052 });
6053 editor.update(cx, |editor, cx| {
6054 assert_text_with_selections(
6055 editor,
6056 indoc! {r#"
6057 use mod1::mod2::«{mod3, mod4}ˇ»;
6058
6059 «ˇfn fn_1(param1: bool, param2: &str) {
6060 let var1 = "text";
6061 }»
6062 "#},
6063 cx,
6064 );
6065 });
6066
6067 editor.update_in(cx, |editor, window, cx| {
6068 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6069 });
6070 editor.update(cx, |editor, cx| {
6071 assert_text_with_selections(
6072 editor,
6073 indoc! {r#"
6074 use mod1::mod2::{mod3, «mod4ˇ»};
6075
6076 fn fn_1«ˇ(param1: bool, param2: &str)» {
6077 let var1 = "«ˇtext»";
6078 }
6079 "#},
6080 cx,
6081 );
6082 });
6083
6084 editor.update_in(cx, |editor, window, cx| {
6085 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6086 });
6087 editor.update(cx, |editor, cx| {
6088 assert_text_with_selections(
6089 editor,
6090 indoc! {r#"
6091 use mod1::mod2::{mod3, mo«ˇ»d4};
6092
6093 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6094 let var1 = "te«ˇ»xt";
6095 }
6096 "#},
6097 cx,
6098 );
6099 });
6100
6101 // Trying to shrink the selected syntax node one more time has no effect.
6102 editor.update_in(cx, |editor, window, cx| {
6103 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6104 });
6105 editor.update_in(cx, |editor, _, cx| {
6106 assert_text_with_selections(
6107 editor,
6108 indoc! {r#"
6109 use mod1::mod2::{mod3, mo«ˇ»d4};
6110
6111 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6112 let var1 = "te«ˇ»xt";
6113 }
6114 "#},
6115 cx,
6116 );
6117 });
6118
6119 // Ensure that we keep expanding the selection if the larger selection starts or ends within
6120 // a fold.
6121 editor.update_in(cx, |editor, window, cx| {
6122 editor.fold_creases(
6123 vec![
6124 Crease::simple(
6125 Point::new(0, 21)..Point::new(0, 24),
6126 FoldPlaceholder::test(),
6127 ),
6128 Crease::simple(
6129 Point::new(3, 20)..Point::new(3, 22),
6130 FoldPlaceholder::test(),
6131 ),
6132 ],
6133 true,
6134 window,
6135 cx,
6136 );
6137 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6138 });
6139 editor.update(cx, |editor, cx| {
6140 assert_text_with_selections(
6141 editor,
6142 indoc! {r#"
6143 use mod1::mod2::«{mod3, mod4}ˇ»;
6144
6145 fn fn_1«ˇ(param1: bool, param2: &str)» {
6146 «ˇlet var1 = "text";»
6147 }
6148 "#},
6149 cx,
6150 );
6151 });
6152}
6153
6154#[gpui::test]
6155async fn test_fold_function_bodies(cx: &mut TestAppContext) {
6156 init_test(cx, |_| {});
6157
6158 let base_text = r#"
6159 impl A {
6160 // this is an uncommitted comment
6161
6162 fn b() {
6163 c();
6164 }
6165
6166 // this is another uncommitted comment
6167
6168 fn d() {
6169 // e
6170 // f
6171 }
6172 }
6173
6174 fn g() {
6175 // h
6176 }
6177 "#
6178 .unindent();
6179
6180 let text = r#"
6181 ˇimpl A {
6182
6183 fn b() {
6184 c();
6185 }
6186
6187 fn d() {
6188 // e
6189 // f
6190 }
6191 }
6192
6193 fn g() {
6194 // h
6195 }
6196 "#
6197 .unindent();
6198
6199 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6200 cx.set_state(&text);
6201 cx.set_head_text(&base_text);
6202 cx.update_editor(|editor, window, cx| {
6203 editor.expand_all_diff_hunks(&Default::default(), window, cx);
6204 });
6205
6206 cx.assert_state_with_diff(
6207 "
6208 ˇimpl A {
6209 - // this is an uncommitted comment
6210
6211 fn b() {
6212 c();
6213 }
6214
6215 - // this is another uncommitted comment
6216 -
6217 fn d() {
6218 // e
6219 // f
6220 }
6221 }
6222
6223 fn g() {
6224 // h
6225 }
6226 "
6227 .unindent(),
6228 );
6229
6230 let expected_display_text = "
6231 impl A {
6232 // this is an uncommitted comment
6233
6234 fn b() {
6235 ⋯
6236 }
6237
6238 // this is another uncommitted comment
6239
6240 fn d() {
6241 ⋯
6242 }
6243 }
6244
6245 fn g() {
6246 ⋯
6247 }
6248 "
6249 .unindent();
6250
6251 cx.update_editor(|editor, window, cx| {
6252 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
6253 assert_eq!(editor.display_text(cx), expected_display_text);
6254 });
6255}
6256
6257#[gpui::test]
6258async fn test_autoindent(cx: &mut TestAppContext) {
6259 init_test(cx, |_| {});
6260
6261 let language = Arc::new(
6262 Language::new(
6263 LanguageConfig {
6264 brackets: BracketPairConfig {
6265 pairs: vec![
6266 BracketPair {
6267 start: "{".to_string(),
6268 end: "}".to_string(),
6269 close: false,
6270 surround: false,
6271 newline: true,
6272 },
6273 BracketPair {
6274 start: "(".to_string(),
6275 end: ")".to_string(),
6276 close: false,
6277 surround: false,
6278 newline: true,
6279 },
6280 ],
6281 ..Default::default()
6282 },
6283 ..Default::default()
6284 },
6285 Some(tree_sitter_rust::LANGUAGE.into()),
6286 )
6287 .with_indents_query(
6288 r#"
6289 (_ "(" ")" @end) @indent
6290 (_ "{" "}" @end) @indent
6291 "#,
6292 )
6293 .unwrap(),
6294 );
6295
6296 let text = "fn a() {}";
6297
6298 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6299 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6300 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6301 editor
6302 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6303 .await;
6304
6305 editor.update_in(cx, |editor, window, cx| {
6306 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
6307 editor.newline(&Newline, window, cx);
6308 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
6309 assert_eq!(
6310 editor.selections.ranges(cx),
6311 &[
6312 Point::new(1, 4)..Point::new(1, 4),
6313 Point::new(3, 4)..Point::new(3, 4),
6314 Point::new(5, 0)..Point::new(5, 0)
6315 ]
6316 );
6317 });
6318}
6319
6320#[gpui::test]
6321async fn test_autoindent_selections(cx: &mut TestAppContext) {
6322 init_test(cx, |_| {});
6323
6324 {
6325 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6326 cx.set_state(indoc! {"
6327 impl A {
6328
6329 fn b() {}
6330
6331 «fn c() {
6332
6333 }ˇ»
6334 }
6335 "});
6336
6337 cx.update_editor(|editor, window, cx| {
6338 editor.autoindent(&Default::default(), window, cx);
6339 });
6340
6341 cx.assert_editor_state(indoc! {"
6342 impl A {
6343
6344 fn b() {}
6345
6346 «fn c() {
6347
6348 }ˇ»
6349 }
6350 "});
6351 }
6352
6353 {
6354 let mut cx = EditorTestContext::new_multibuffer(
6355 cx,
6356 [indoc! { "
6357 impl A {
6358 «
6359 // a
6360 fn b(){}
6361 »
6362 «
6363 }
6364 fn c(){}
6365 »
6366 "}],
6367 );
6368
6369 let buffer = cx.update_editor(|editor, _, cx| {
6370 let buffer = editor.buffer().update(cx, |buffer, _| {
6371 buffer.all_buffers().iter().next().unwrap().clone()
6372 });
6373 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6374 buffer
6375 });
6376
6377 cx.run_until_parked();
6378 cx.update_editor(|editor, window, cx| {
6379 editor.select_all(&Default::default(), window, cx);
6380 editor.autoindent(&Default::default(), window, cx)
6381 });
6382 cx.run_until_parked();
6383
6384 cx.update(|_, cx| {
6385 pretty_assertions::assert_eq!(
6386 buffer.read(cx).text(),
6387 indoc! { "
6388 impl A {
6389
6390 // a
6391 fn b(){}
6392
6393
6394 }
6395 fn c(){}
6396
6397 " }
6398 )
6399 });
6400 }
6401}
6402
6403#[gpui::test]
6404async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
6405 init_test(cx, |_| {});
6406
6407 let mut cx = EditorTestContext::new(cx).await;
6408
6409 let language = Arc::new(Language::new(
6410 LanguageConfig {
6411 brackets: BracketPairConfig {
6412 pairs: vec![
6413 BracketPair {
6414 start: "{".to_string(),
6415 end: "}".to_string(),
6416 close: true,
6417 surround: true,
6418 newline: true,
6419 },
6420 BracketPair {
6421 start: "(".to_string(),
6422 end: ")".to_string(),
6423 close: true,
6424 surround: true,
6425 newline: true,
6426 },
6427 BracketPair {
6428 start: "/*".to_string(),
6429 end: " */".to_string(),
6430 close: true,
6431 surround: true,
6432 newline: true,
6433 },
6434 BracketPair {
6435 start: "[".to_string(),
6436 end: "]".to_string(),
6437 close: false,
6438 surround: false,
6439 newline: true,
6440 },
6441 BracketPair {
6442 start: "\"".to_string(),
6443 end: "\"".to_string(),
6444 close: true,
6445 surround: true,
6446 newline: false,
6447 },
6448 BracketPair {
6449 start: "<".to_string(),
6450 end: ">".to_string(),
6451 close: false,
6452 surround: true,
6453 newline: true,
6454 },
6455 ],
6456 ..Default::default()
6457 },
6458 autoclose_before: "})]".to_string(),
6459 ..Default::default()
6460 },
6461 Some(tree_sitter_rust::LANGUAGE.into()),
6462 ));
6463
6464 cx.language_registry().add(language.clone());
6465 cx.update_buffer(|buffer, cx| {
6466 buffer.set_language(Some(language), cx);
6467 });
6468
6469 cx.set_state(
6470 &r#"
6471 🏀ˇ
6472 εˇ
6473 ❤️ˇ
6474 "#
6475 .unindent(),
6476 );
6477
6478 // autoclose multiple nested brackets at multiple cursors
6479 cx.update_editor(|editor, window, cx| {
6480 editor.handle_input("{", window, cx);
6481 editor.handle_input("{", window, cx);
6482 editor.handle_input("{", window, cx);
6483 });
6484 cx.assert_editor_state(
6485 &"
6486 🏀{{{ˇ}}}
6487 ε{{{ˇ}}}
6488 ❤️{{{ˇ}}}
6489 "
6490 .unindent(),
6491 );
6492
6493 // insert a different closing bracket
6494 cx.update_editor(|editor, window, cx| {
6495 editor.handle_input(")", window, cx);
6496 });
6497 cx.assert_editor_state(
6498 &"
6499 🏀{{{)ˇ}}}
6500 ε{{{)ˇ}}}
6501 ❤️{{{)ˇ}}}
6502 "
6503 .unindent(),
6504 );
6505
6506 // skip over the auto-closed brackets when typing a closing bracket
6507 cx.update_editor(|editor, window, cx| {
6508 editor.move_right(&MoveRight, window, cx);
6509 editor.handle_input("}", window, cx);
6510 editor.handle_input("}", window, cx);
6511 editor.handle_input("}", window, cx);
6512 });
6513 cx.assert_editor_state(
6514 &"
6515 🏀{{{)}}}}ˇ
6516 ε{{{)}}}}ˇ
6517 ❤️{{{)}}}}ˇ
6518 "
6519 .unindent(),
6520 );
6521
6522 // autoclose multi-character pairs
6523 cx.set_state(
6524 &"
6525 ˇ
6526 ˇ
6527 "
6528 .unindent(),
6529 );
6530 cx.update_editor(|editor, window, cx| {
6531 editor.handle_input("/", window, cx);
6532 editor.handle_input("*", window, cx);
6533 });
6534 cx.assert_editor_state(
6535 &"
6536 /*ˇ */
6537 /*ˇ */
6538 "
6539 .unindent(),
6540 );
6541
6542 // one cursor autocloses a multi-character pair, one cursor
6543 // does not autoclose.
6544 cx.set_state(
6545 &"
6546 /ˇ
6547 ˇ
6548 "
6549 .unindent(),
6550 );
6551 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
6552 cx.assert_editor_state(
6553 &"
6554 /*ˇ */
6555 *ˇ
6556 "
6557 .unindent(),
6558 );
6559
6560 // Don't autoclose if the next character isn't whitespace and isn't
6561 // listed in the language's "autoclose_before" section.
6562 cx.set_state("ˇa b");
6563 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6564 cx.assert_editor_state("{ˇa b");
6565
6566 // Don't autoclose if `close` is false for the bracket pair
6567 cx.set_state("ˇ");
6568 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
6569 cx.assert_editor_state("[ˇ");
6570
6571 // Surround with brackets if text is selected
6572 cx.set_state("«aˇ» b");
6573 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6574 cx.assert_editor_state("{«aˇ»} b");
6575
6576 // Autoclose when not immediately after a word character
6577 cx.set_state("a ˇ");
6578 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6579 cx.assert_editor_state("a \"ˇ\"");
6580
6581 // Autoclose pair where the start and end characters are the same
6582 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6583 cx.assert_editor_state("a \"\"ˇ");
6584
6585 // Don't autoclose when immediately after a word character
6586 cx.set_state("aˇ");
6587 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6588 cx.assert_editor_state("a\"ˇ");
6589
6590 // Do autoclose when after a non-word character
6591 cx.set_state("{ˇ");
6592 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6593 cx.assert_editor_state("{\"ˇ\"");
6594
6595 // Non identical pairs autoclose regardless of preceding character
6596 cx.set_state("aˇ");
6597 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6598 cx.assert_editor_state("a{ˇ}");
6599
6600 // Don't autoclose pair if autoclose is disabled
6601 cx.set_state("ˇ");
6602 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6603 cx.assert_editor_state("<ˇ");
6604
6605 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
6606 cx.set_state("«aˇ» b");
6607 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6608 cx.assert_editor_state("<«aˇ»> b");
6609}
6610
6611#[gpui::test]
6612async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
6613 init_test(cx, |settings| {
6614 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
6615 });
6616
6617 let mut cx = EditorTestContext::new(cx).await;
6618
6619 let language = Arc::new(Language::new(
6620 LanguageConfig {
6621 brackets: BracketPairConfig {
6622 pairs: vec![
6623 BracketPair {
6624 start: "{".to_string(),
6625 end: "}".to_string(),
6626 close: true,
6627 surround: true,
6628 newline: true,
6629 },
6630 BracketPair {
6631 start: "(".to_string(),
6632 end: ")".to_string(),
6633 close: true,
6634 surround: true,
6635 newline: true,
6636 },
6637 BracketPair {
6638 start: "[".to_string(),
6639 end: "]".to_string(),
6640 close: false,
6641 surround: false,
6642 newline: true,
6643 },
6644 ],
6645 ..Default::default()
6646 },
6647 autoclose_before: "})]".to_string(),
6648 ..Default::default()
6649 },
6650 Some(tree_sitter_rust::LANGUAGE.into()),
6651 ));
6652
6653 cx.language_registry().add(language.clone());
6654 cx.update_buffer(|buffer, cx| {
6655 buffer.set_language(Some(language), cx);
6656 });
6657
6658 cx.set_state(
6659 &"
6660 ˇ
6661 ˇ
6662 ˇ
6663 "
6664 .unindent(),
6665 );
6666
6667 // ensure only matching closing brackets are skipped over
6668 cx.update_editor(|editor, window, cx| {
6669 editor.handle_input("}", window, cx);
6670 editor.move_left(&MoveLeft, window, cx);
6671 editor.handle_input(")", window, cx);
6672 editor.move_left(&MoveLeft, window, cx);
6673 });
6674 cx.assert_editor_state(
6675 &"
6676 ˇ)}
6677 ˇ)}
6678 ˇ)}
6679 "
6680 .unindent(),
6681 );
6682
6683 // skip-over closing brackets at multiple cursors
6684 cx.update_editor(|editor, window, cx| {
6685 editor.handle_input(")", window, cx);
6686 editor.handle_input("}", window, cx);
6687 });
6688 cx.assert_editor_state(
6689 &"
6690 )}ˇ
6691 )}ˇ
6692 )}ˇ
6693 "
6694 .unindent(),
6695 );
6696
6697 // ignore non-close brackets
6698 cx.update_editor(|editor, window, cx| {
6699 editor.handle_input("]", window, cx);
6700 editor.move_left(&MoveLeft, window, cx);
6701 editor.handle_input("]", window, cx);
6702 });
6703 cx.assert_editor_state(
6704 &"
6705 )}]ˇ]
6706 )}]ˇ]
6707 )}]ˇ]
6708 "
6709 .unindent(),
6710 );
6711}
6712
6713#[gpui::test]
6714async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
6715 init_test(cx, |_| {});
6716
6717 let mut cx = EditorTestContext::new(cx).await;
6718
6719 let html_language = Arc::new(
6720 Language::new(
6721 LanguageConfig {
6722 name: "HTML".into(),
6723 brackets: BracketPairConfig {
6724 pairs: vec![
6725 BracketPair {
6726 start: "<".into(),
6727 end: ">".into(),
6728 close: true,
6729 ..Default::default()
6730 },
6731 BracketPair {
6732 start: "{".into(),
6733 end: "}".into(),
6734 close: true,
6735 ..Default::default()
6736 },
6737 BracketPair {
6738 start: "(".into(),
6739 end: ")".into(),
6740 close: true,
6741 ..Default::default()
6742 },
6743 ],
6744 ..Default::default()
6745 },
6746 autoclose_before: "})]>".into(),
6747 ..Default::default()
6748 },
6749 Some(tree_sitter_html::LANGUAGE.into()),
6750 )
6751 .with_injection_query(
6752 r#"
6753 (script_element
6754 (raw_text) @injection.content
6755 (#set! injection.language "javascript"))
6756 "#,
6757 )
6758 .unwrap(),
6759 );
6760
6761 let javascript_language = Arc::new(Language::new(
6762 LanguageConfig {
6763 name: "JavaScript".into(),
6764 brackets: BracketPairConfig {
6765 pairs: vec![
6766 BracketPair {
6767 start: "/*".into(),
6768 end: " */".into(),
6769 close: true,
6770 ..Default::default()
6771 },
6772 BracketPair {
6773 start: "{".into(),
6774 end: "}".into(),
6775 close: true,
6776 ..Default::default()
6777 },
6778 BracketPair {
6779 start: "(".into(),
6780 end: ")".into(),
6781 close: true,
6782 ..Default::default()
6783 },
6784 ],
6785 ..Default::default()
6786 },
6787 autoclose_before: "})]>".into(),
6788 ..Default::default()
6789 },
6790 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
6791 ));
6792
6793 cx.language_registry().add(html_language.clone());
6794 cx.language_registry().add(javascript_language.clone());
6795
6796 cx.update_buffer(|buffer, cx| {
6797 buffer.set_language(Some(html_language), cx);
6798 });
6799
6800 cx.set_state(
6801 &r#"
6802 <body>ˇ
6803 <script>
6804 var x = 1;ˇ
6805 </script>
6806 </body>ˇ
6807 "#
6808 .unindent(),
6809 );
6810
6811 // Precondition: different languages are active at different locations.
6812 cx.update_editor(|editor, window, cx| {
6813 let snapshot = editor.snapshot(window, cx);
6814 let cursors = editor.selections.ranges::<usize>(cx);
6815 let languages = cursors
6816 .iter()
6817 .map(|c| snapshot.language_at(c.start).unwrap().name())
6818 .collect::<Vec<_>>();
6819 assert_eq!(
6820 languages,
6821 &["HTML".into(), "JavaScript".into(), "HTML".into()]
6822 );
6823 });
6824
6825 // Angle brackets autoclose in HTML, but not JavaScript.
6826 cx.update_editor(|editor, window, cx| {
6827 editor.handle_input("<", window, cx);
6828 editor.handle_input("a", window, cx);
6829 });
6830 cx.assert_editor_state(
6831 &r#"
6832 <body><aˇ>
6833 <script>
6834 var x = 1;<aˇ
6835 </script>
6836 </body><aˇ>
6837 "#
6838 .unindent(),
6839 );
6840
6841 // Curly braces and parens autoclose in both HTML and JavaScript.
6842 cx.update_editor(|editor, window, cx| {
6843 editor.handle_input(" b=", window, cx);
6844 editor.handle_input("{", window, cx);
6845 editor.handle_input("c", window, cx);
6846 editor.handle_input("(", window, cx);
6847 });
6848 cx.assert_editor_state(
6849 &r#"
6850 <body><a b={c(ˇ)}>
6851 <script>
6852 var x = 1;<a b={c(ˇ)}
6853 </script>
6854 </body><a b={c(ˇ)}>
6855 "#
6856 .unindent(),
6857 );
6858
6859 // Brackets that were already autoclosed are skipped.
6860 cx.update_editor(|editor, window, cx| {
6861 editor.handle_input(")", window, cx);
6862 editor.handle_input("d", window, cx);
6863 editor.handle_input("}", window, cx);
6864 });
6865 cx.assert_editor_state(
6866 &r#"
6867 <body><a b={c()d}ˇ>
6868 <script>
6869 var x = 1;<a b={c()d}ˇ
6870 </script>
6871 </body><a b={c()d}ˇ>
6872 "#
6873 .unindent(),
6874 );
6875 cx.update_editor(|editor, window, cx| {
6876 editor.handle_input(">", window, cx);
6877 });
6878 cx.assert_editor_state(
6879 &r#"
6880 <body><a b={c()d}>ˇ
6881 <script>
6882 var x = 1;<a b={c()d}>ˇ
6883 </script>
6884 </body><a b={c()d}>ˇ
6885 "#
6886 .unindent(),
6887 );
6888
6889 // Reset
6890 cx.set_state(
6891 &r#"
6892 <body>ˇ
6893 <script>
6894 var x = 1;ˇ
6895 </script>
6896 </body>ˇ
6897 "#
6898 .unindent(),
6899 );
6900
6901 cx.update_editor(|editor, window, cx| {
6902 editor.handle_input("<", window, cx);
6903 });
6904 cx.assert_editor_state(
6905 &r#"
6906 <body><ˇ>
6907 <script>
6908 var x = 1;<ˇ
6909 </script>
6910 </body><ˇ>
6911 "#
6912 .unindent(),
6913 );
6914
6915 // When backspacing, the closing angle brackets are removed.
6916 cx.update_editor(|editor, window, cx| {
6917 editor.backspace(&Backspace, window, cx);
6918 });
6919 cx.assert_editor_state(
6920 &r#"
6921 <body>ˇ
6922 <script>
6923 var x = 1;ˇ
6924 </script>
6925 </body>ˇ
6926 "#
6927 .unindent(),
6928 );
6929
6930 // Block comments autoclose in JavaScript, but not HTML.
6931 cx.update_editor(|editor, window, cx| {
6932 editor.handle_input("/", window, cx);
6933 editor.handle_input("*", window, cx);
6934 });
6935 cx.assert_editor_state(
6936 &r#"
6937 <body>/*ˇ
6938 <script>
6939 var x = 1;/*ˇ */
6940 </script>
6941 </body>/*ˇ
6942 "#
6943 .unindent(),
6944 );
6945}
6946
6947#[gpui::test]
6948async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
6949 init_test(cx, |_| {});
6950
6951 let mut cx = EditorTestContext::new(cx).await;
6952
6953 let rust_language = Arc::new(
6954 Language::new(
6955 LanguageConfig {
6956 name: "Rust".into(),
6957 brackets: serde_json::from_value(json!([
6958 { "start": "{", "end": "}", "close": true, "newline": true },
6959 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
6960 ]))
6961 .unwrap(),
6962 autoclose_before: "})]>".into(),
6963 ..Default::default()
6964 },
6965 Some(tree_sitter_rust::LANGUAGE.into()),
6966 )
6967 .with_override_query("(string_literal) @string")
6968 .unwrap(),
6969 );
6970
6971 cx.language_registry().add(rust_language.clone());
6972 cx.update_buffer(|buffer, cx| {
6973 buffer.set_language(Some(rust_language), cx);
6974 });
6975
6976 cx.set_state(
6977 &r#"
6978 let x = ˇ
6979 "#
6980 .unindent(),
6981 );
6982
6983 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
6984 cx.update_editor(|editor, window, cx| {
6985 editor.handle_input("\"", window, cx);
6986 });
6987 cx.assert_editor_state(
6988 &r#"
6989 let x = "ˇ"
6990 "#
6991 .unindent(),
6992 );
6993
6994 // Inserting another quotation mark. The cursor moves across the existing
6995 // automatically-inserted quotation mark.
6996 cx.update_editor(|editor, window, cx| {
6997 editor.handle_input("\"", window, cx);
6998 });
6999 cx.assert_editor_state(
7000 &r#"
7001 let x = ""ˇ
7002 "#
7003 .unindent(),
7004 );
7005
7006 // Reset
7007 cx.set_state(
7008 &r#"
7009 let x = ˇ
7010 "#
7011 .unindent(),
7012 );
7013
7014 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
7015 cx.update_editor(|editor, window, cx| {
7016 editor.handle_input("\"", window, cx);
7017 editor.handle_input(" ", window, cx);
7018 editor.move_left(&Default::default(), window, cx);
7019 editor.handle_input("\\", window, cx);
7020 editor.handle_input("\"", window, cx);
7021 });
7022 cx.assert_editor_state(
7023 &r#"
7024 let x = "\"ˇ "
7025 "#
7026 .unindent(),
7027 );
7028
7029 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
7030 // mark. Nothing is inserted.
7031 cx.update_editor(|editor, window, cx| {
7032 editor.move_right(&Default::default(), window, cx);
7033 editor.handle_input("\"", window, cx);
7034 });
7035 cx.assert_editor_state(
7036 &r#"
7037 let x = "\" "ˇ
7038 "#
7039 .unindent(),
7040 );
7041}
7042
7043#[gpui::test]
7044async fn test_surround_with_pair(cx: &mut TestAppContext) {
7045 init_test(cx, |_| {});
7046
7047 let language = Arc::new(Language::new(
7048 LanguageConfig {
7049 brackets: BracketPairConfig {
7050 pairs: vec![
7051 BracketPair {
7052 start: "{".to_string(),
7053 end: "}".to_string(),
7054 close: true,
7055 surround: true,
7056 newline: true,
7057 },
7058 BracketPair {
7059 start: "/* ".to_string(),
7060 end: "*/".to_string(),
7061 close: true,
7062 surround: true,
7063 ..Default::default()
7064 },
7065 ],
7066 ..Default::default()
7067 },
7068 ..Default::default()
7069 },
7070 Some(tree_sitter_rust::LANGUAGE.into()),
7071 ));
7072
7073 let text = r#"
7074 a
7075 b
7076 c
7077 "#
7078 .unindent();
7079
7080 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7081 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7082 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7083 editor
7084 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7085 .await;
7086
7087 editor.update_in(cx, |editor, window, cx| {
7088 editor.change_selections(None, window, cx, |s| {
7089 s.select_display_ranges([
7090 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7091 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7092 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
7093 ])
7094 });
7095
7096 editor.handle_input("{", window, cx);
7097 editor.handle_input("{", window, cx);
7098 editor.handle_input("{", window, cx);
7099 assert_eq!(
7100 editor.text(cx),
7101 "
7102 {{{a}}}
7103 {{{b}}}
7104 {{{c}}}
7105 "
7106 .unindent()
7107 );
7108 assert_eq!(
7109 editor.selections.display_ranges(cx),
7110 [
7111 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
7112 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
7113 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
7114 ]
7115 );
7116
7117 editor.undo(&Undo, window, cx);
7118 editor.undo(&Undo, window, cx);
7119 editor.undo(&Undo, window, cx);
7120 assert_eq!(
7121 editor.text(cx),
7122 "
7123 a
7124 b
7125 c
7126 "
7127 .unindent()
7128 );
7129 assert_eq!(
7130 editor.selections.display_ranges(cx),
7131 [
7132 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7133 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7134 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7135 ]
7136 );
7137
7138 // Ensure inserting the first character of a multi-byte bracket pair
7139 // doesn't surround the selections with the bracket.
7140 editor.handle_input("/", window, cx);
7141 assert_eq!(
7142 editor.text(cx),
7143 "
7144 /
7145 /
7146 /
7147 "
7148 .unindent()
7149 );
7150 assert_eq!(
7151 editor.selections.display_ranges(cx),
7152 [
7153 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7154 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7155 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7156 ]
7157 );
7158
7159 editor.undo(&Undo, window, cx);
7160 assert_eq!(
7161 editor.text(cx),
7162 "
7163 a
7164 b
7165 c
7166 "
7167 .unindent()
7168 );
7169 assert_eq!(
7170 editor.selections.display_ranges(cx),
7171 [
7172 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7173 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7174 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7175 ]
7176 );
7177
7178 // Ensure inserting the last character of a multi-byte bracket pair
7179 // doesn't surround the selections with the bracket.
7180 editor.handle_input("*", window, cx);
7181 assert_eq!(
7182 editor.text(cx),
7183 "
7184 *
7185 *
7186 *
7187 "
7188 .unindent()
7189 );
7190 assert_eq!(
7191 editor.selections.display_ranges(cx),
7192 [
7193 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7194 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7195 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7196 ]
7197 );
7198 });
7199}
7200
7201#[gpui::test]
7202async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
7203 init_test(cx, |_| {});
7204
7205 let language = Arc::new(Language::new(
7206 LanguageConfig {
7207 brackets: BracketPairConfig {
7208 pairs: vec![BracketPair {
7209 start: "{".to_string(),
7210 end: "}".to_string(),
7211 close: true,
7212 surround: true,
7213 newline: true,
7214 }],
7215 ..Default::default()
7216 },
7217 autoclose_before: "}".to_string(),
7218 ..Default::default()
7219 },
7220 Some(tree_sitter_rust::LANGUAGE.into()),
7221 ));
7222
7223 let text = r#"
7224 a
7225 b
7226 c
7227 "#
7228 .unindent();
7229
7230 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7231 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7232 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7233 editor
7234 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7235 .await;
7236
7237 editor.update_in(cx, |editor, window, cx| {
7238 editor.change_selections(None, window, cx, |s| {
7239 s.select_ranges([
7240 Point::new(0, 1)..Point::new(0, 1),
7241 Point::new(1, 1)..Point::new(1, 1),
7242 Point::new(2, 1)..Point::new(2, 1),
7243 ])
7244 });
7245
7246 editor.handle_input("{", window, cx);
7247 editor.handle_input("{", window, cx);
7248 editor.handle_input("_", window, cx);
7249 assert_eq!(
7250 editor.text(cx),
7251 "
7252 a{{_}}
7253 b{{_}}
7254 c{{_}}
7255 "
7256 .unindent()
7257 );
7258 assert_eq!(
7259 editor.selections.ranges::<Point>(cx),
7260 [
7261 Point::new(0, 4)..Point::new(0, 4),
7262 Point::new(1, 4)..Point::new(1, 4),
7263 Point::new(2, 4)..Point::new(2, 4)
7264 ]
7265 );
7266
7267 editor.backspace(&Default::default(), window, cx);
7268 editor.backspace(&Default::default(), window, cx);
7269 assert_eq!(
7270 editor.text(cx),
7271 "
7272 a{}
7273 b{}
7274 c{}
7275 "
7276 .unindent()
7277 );
7278 assert_eq!(
7279 editor.selections.ranges::<Point>(cx),
7280 [
7281 Point::new(0, 2)..Point::new(0, 2),
7282 Point::new(1, 2)..Point::new(1, 2),
7283 Point::new(2, 2)..Point::new(2, 2)
7284 ]
7285 );
7286
7287 editor.delete_to_previous_word_start(&Default::default(), window, cx);
7288 assert_eq!(
7289 editor.text(cx),
7290 "
7291 a
7292 b
7293 c
7294 "
7295 .unindent()
7296 );
7297 assert_eq!(
7298 editor.selections.ranges::<Point>(cx),
7299 [
7300 Point::new(0, 1)..Point::new(0, 1),
7301 Point::new(1, 1)..Point::new(1, 1),
7302 Point::new(2, 1)..Point::new(2, 1)
7303 ]
7304 );
7305 });
7306}
7307
7308#[gpui::test]
7309async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
7310 init_test(cx, |settings| {
7311 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7312 });
7313
7314 let mut cx = EditorTestContext::new(cx).await;
7315
7316 let language = Arc::new(Language::new(
7317 LanguageConfig {
7318 brackets: BracketPairConfig {
7319 pairs: vec![
7320 BracketPair {
7321 start: "{".to_string(),
7322 end: "}".to_string(),
7323 close: true,
7324 surround: true,
7325 newline: true,
7326 },
7327 BracketPair {
7328 start: "(".to_string(),
7329 end: ")".to_string(),
7330 close: true,
7331 surround: true,
7332 newline: true,
7333 },
7334 BracketPair {
7335 start: "[".to_string(),
7336 end: "]".to_string(),
7337 close: false,
7338 surround: true,
7339 newline: true,
7340 },
7341 ],
7342 ..Default::default()
7343 },
7344 autoclose_before: "})]".to_string(),
7345 ..Default::default()
7346 },
7347 Some(tree_sitter_rust::LANGUAGE.into()),
7348 ));
7349
7350 cx.language_registry().add(language.clone());
7351 cx.update_buffer(|buffer, cx| {
7352 buffer.set_language(Some(language), cx);
7353 });
7354
7355 cx.set_state(
7356 &"
7357 {(ˇ)}
7358 [[ˇ]]
7359 {(ˇ)}
7360 "
7361 .unindent(),
7362 );
7363
7364 cx.update_editor(|editor, window, cx| {
7365 editor.backspace(&Default::default(), window, cx);
7366 editor.backspace(&Default::default(), window, cx);
7367 });
7368
7369 cx.assert_editor_state(
7370 &"
7371 ˇ
7372 ˇ]]
7373 ˇ
7374 "
7375 .unindent(),
7376 );
7377
7378 cx.update_editor(|editor, window, cx| {
7379 editor.handle_input("{", window, cx);
7380 editor.handle_input("{", window, cx);
7381 editor.move_right(&MoveRight, window, cx);
7382 editor.move_right(&MoveRight, window, cx);
7383 editor.move_left(&MoveLeft, window, cx);
7384 editor.move_left(&MoveLeft, window, cx);
7385 editor.backspace(&Default::default(), window, cx);
7386 });
7387
7388 cx.assert_editor_state(
7389 &"
7390 {ˇ}
7391 {ˇ}]]
7392 {ˇ}
7393 "
7394 .unindent(),
7395 );
7396
7397 cx.update_editor(|editor, window, cx| {
7398 editor.backspace(&Default::default(), window, cx);
7399 });
7400
7401 cx.assert_editor_state(
7402 &"
7403 ˇ
7404 ˇ]]
7405 ˇ
7406 "
7407 .unindent(),
7408 );
7409}
7410
7411#[gpui::test]
7412async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
7413 init_test(cx, |_| {});
7414
7415 let language = Arc::new(Language::new(
7416 LanguageConfig::default(),
7417 Some(tree_sitter_rust::LANGUAGE.into()),
7418 ));
7419
7420 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
7421 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7422 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7423 editor
7424 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7425 .await;
7426
7427 editor.update_in(cx, |editor, window, cx| {
7428 editor.set_auto_replace_emoji_shortcode(true);
7429
7430 editor.handle_input("Hello ", window, cx);
7431 editor.handle_input(":wave", window, cx);
7432 assert_eq!(editor.text(cx), "Hello :wave".unindent());
7433
7434 editor.handle_input(":", window, cx);
7435 assert_eq!(editor.text(cx), "Hello 👋".unindent());
7436
7437 editor.handle_input(" :smile", window, cx);
7438 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
7439
7440 editor.handle_input(":", window, cx);
7441 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
7442
7443 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
7444 editor.handle_input(":wave", window, cx);
7445 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
7446
7447 editor.handle_input(":", window, cx);
7448 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
7449
7450 editor.handle_input(":1", window, cx);
7451 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
7452
7453 editor.handle_input(":", window, cx);
7454 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
7455
7456 // Ensure shortcode does not get replaced when it is part of a word
7457 editor.handle_input(" Test:wave", window, cx);
7458 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
7459
7460 editor.handle_input(":", window, cx);
7461 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
7462
7463 editor.set_auto_replace_emoji_shortcode(false);
7464
7465 // Ensure shortcode does not get replaced when auto replace is off
7466 editor.handle_input(" :wave", window, cx);
7467 assert_eq!(
7468 editor.text(cx),
7469 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
7470 );
7471
7472 editor.handle_input(":", window, cx);
7473 assert_eq!(
7474 editor.text(cx),
7475 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
7476 );
7477 });
7478}
7479
7480#[gpui::test]
7481async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
7482 init_test(cx, |_| {});
7483
7484 let (text, insertion_ranges) = marked_text_ranges(
7485 indoc! {"
7486 ˇ
7487 "},
7488 false,
7489 );
7490
7491 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7492 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7493
7494 _ = editor.update_in(cx, |editor, window, cx| {
7495 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
7496
7497 editor
7498 .insert_snippet(&insertion_ranges, snippet, window, cx)
7499 .unwrap();
7500
7501 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7502 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7503 assert_eq!(editor.text(cx), expected_text);
7504 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7505 }
7506
7507 assert(
7508 editor,
7509 cx,
7510 indoc! {"
7511 type «» =•
7512 "},
7513 );
7514
7515 assert!(editor.context_menu_visible(), "There should be a matches");
7516 });
7517}
7518
7519#[gpui::test]
7520async fn test_snippets(cx: &mut TestAppContext) {
7521 init_test(cx, |_| {});
7522
7523 let (text, insertion_ranges) = marked_text_ranges(
7524 indoc! {"
7525 a.ˇ b
7526 a.ˇ b
7527 a.ˇ b
7528 "},
7529 false,
7530 );
7531
7532 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7533 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7534
7535 editor.update_in(cx, |editor, window, cx| {
7536 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
7537
7538 editor
7539 .insert_snippet(&insertion_ranges, snippet, window, cx)
7540 .unwrap();
7541
7542 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7543 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7544 assert_eq!(editor.text(cx), expected_text);
7545 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7546 }
7547
7548 assert(
7549 editor,
7550 cx,
7551 indoc! {"
7552 a.f(«one», two, «three») b
7553 a.f(«one», two, «three») b
7554 a.f(«one», two, «three») b
7555 "},
7556 );
7557
7558 // Can't move earlier than the first tab stop
7559 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
7560 assert(
7561 editor,
7562 cx,
7563 indoc! {"
7564 a.f(«one», two, «three») b
7565 a.f(«one», two, «three») b
7566 a.f(«one», two, «three») b
7567 "},
7568 );
7569
7570 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7571 assert(
7572 editor,
7573 cx,
7574 indoc! {"
7575 a.f(one, «two», three) b
7576 a.f(one, «two», three) b
7577 a.f(one, «two», three) b
7578 "},
7579 );
7580
7581 editor.move_to_prev_snippet_tabstop(window, cx);
7582 assert(
7583 editor,
7584 cx,
7585 indoc! {"
7586 a.f(«one», two, «three») b
7587 a.f(«one», two, «three») b
7588 a.f(«one», two, «three») b
7589 "},
7590 );
7591
7592 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7593 assert(
7594 editor,
7595 cx,
7596 indoc! {"
7597 a.f(one, «two», three) b
7598 a.f(one, «two», three) b
7599 a.f(one, «two», three) b
7600 "},
7601 );
7602 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7603 assert(
7604 editor,
7605 cx,
7606 indoc! {"
7607 a.f(one, two, three)ˇ b
7608 a.f(one, two, three)ˇ b
7609 a.f(one, two, three)ˇ b
7610 "},
7611 );
7612
7613 // As soon as the last tab stop is reached, snippet state is gone
7614 editor.move_to_prev_snippet_tabstop(window, cx);
7615 assert(
7616 editor,
7617 cx,
7618 indoc! {"
7619 a.f(one, two, three)ˇ b
7620 a.f(one, two, three)ˇ b
7621 a.f(one, two, three)ˇ b
7622 "},
7623 );
7624 });
7625}
7626
7627#[gpui::test]
7628async fn test_document_format_during_save(cx: &mut TestAppContext) {
7629 init_test(cx, |_| {});
7630
7631 let fs = FakeFs::new(cx.executor());
7632 fs.insert_file(path!("/file.rs"), Default::default()).await;
7633
7634 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
7635
7636 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7637 language_registry.add(rust_lang());
7638 let mut fake_servers = language_registry.register_fake_lsp(
7639 "Rust",
7640 FakeLspAdapter {
7641 capabilities: lsp::ServerCapabilities {
7642 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7643 ..Default::default()
7644 },
7645 ..Default::default()
7646 },
7647 );
7648
7649 let buffer = project
7650 .update(cx, |project, cx| {
7651 project.open_local_buffer(path!("/file.rs"), cx)
7652 })
7653 .await
7654 .unwrap();
7655
7656 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7657 let (editor, cx) = cx.add_window_view(|window, cx| {
7658 build_editor_with_project(project.clone(), buffer, window, cx)
7659 });
7660 editor.update_in(cx, |editor, window, cx| {
7661 editor.set_text("one\ntwo\nthree\n", window, cx)
7662 });
7663 assert!(cx.read(|cx| editor.is_dirty(cx)));
7664
7665 cx.executor().start_waiting();
7666 let fake_server = fake_servers.next().await.unwrap();
7667
7668 let save = editor
7669 .update_in(cx, |editor, window, cx| {
7670 editor.save(true, project.clone(), window, cx)
7671 })
7672 .unwrap();
7673 fake_server
7674 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
7675 assert_eq!(
7676 params.text_document.uri,
7677 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7678 );
7679 assert_eq!(params.options.tab_size, 4);
7680 Ok(Some(vec![lsp::TextEdit::new(
7681 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7682 ", ".to_string(),
7683 )]))
7684 })
7685 .next()
7686 .await;
7687 cx.executor().start_waiting();
7688 save.await;
7689
7690 assert_eq!(
7691 editor.update(cx, |editor, cx| editor.text(cx)),
7692 "one, two\nthree\n"
7693 );
7694 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7695
7696 editor.update_in(cx, |editor, window, cx| {
7697 editor.set_text("one\ntwo\nthree\n", window, cx)
7698 });
7699 assert!(cx.read(|cx| editor.is_dirty(cx)));
7700
7701 // Ensure we can still save even if formatting hangs.
7702 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
7703 move |params, _| async move {
7704 assert_eq!(
7705 params.text_document.uri,
7706 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7707 );
7708 futures::future::pending::<()>().await;
7709 unreachable!()
7710 },
7711 );
7712 let save = editor
7713 .update_in(cx, |editor, window, cx| {
7714 editor.save(true, project.clone(), window, cx)
7715 })
7716 .unwrap();
7717 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7718 cx.executor().start_waiting();
7719 save.await;
7720 assert_eq!(
7721 editor.update(cx, |editor, cx| editor.text(cx)),
7722 "one\ntwo\nthree\n"
7723 );
7724 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7725
7726 // For non-dirty buffer, no formatting request should be sent
7727 let save = editor
7728 .update_in(cx, |editor, window, cx| {
7729 editor.save(true, project.clone(), window, cx)
7730 })
7731 .unwrap();
7732 let _pending_format_request = fake_server
7733 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
7734 panic!("Should not be invoked on non-dirty buffer");
7735 })
7736 .next();
7737 cx.executor().start_waiting();
7738 save.await;
7739
7740 // Set rust language override and assert overridden tabsize is sent to language server
7741 update_test_language_settings(cx, |settings| {
7742 settings.languages.insert(
7743 "Rust".into(),
7744 LanguageSettingsContent {
7745 tab_size: NonZeroU32::new(8),
7746 ..Default::default()
7747 },
7748 );
7749 });
7750
7751 editor.update_in(cx, |editor, window, cx| {
7752 editor.set_text("somehting_new\n", window, cx)
7753 });
7754 assert!(cx.read(|cx| editor.is_dirty(cx)));
7755 let save = editor
7756 .update_in(cx, |editor, window, cx| {
7757 editor.save(true, project.clone(), window, cx)
7758 })
7759 .unwrap();
7760 fake_server
7761 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
7762 assert_eq!(
7763 params.text_document.uri,
7764 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7765 );
7766 assert_eq!(params.options.tab_size, 8);
7767 Ok(Some(vec![]))
7768 })
7769 .next()
7770 .await;
7771 cx.executor().start_waiting();
7772 save.await;
7773}
7774
7775#[gpui::test]
7776async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
7777 init_test(cx, |_| {});
7778
7779 let cols = 4;
7780 let rows = 10;
7781 let sample_text_1 = sample_text(rows, cols, 'a');
7782 assert_eq!(
7783 sample_text_1,
7784 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
7785 );
7786 let sample_text_2 = sample_text(rows, cols, 'l');
7787 assert_eq!(
7788 sample_text_2,
7789 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
7790 );
7791 let sample_text_3 = sample_text(rows, cols, 'v');
7792 assert_eq!(
7793 sample_text_3,
7794 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
7795 );
7796
7797 let fs = FakeFs::new(cx.executor());
7798 fs.insert_tree(
7799 path!("/a"),
7800 json!({
7801 "main.rs": sample_text_1,
7802 "other.rs": sample_text_2,
7803 "lib.rs": sample_text_3,
7804 }),
7805 )
7806 .await;
7807
7808 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
7809 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7810 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
7811
7812 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7813 language_registry.add(rust_lang());
7814 let mut fake_servers = language_registry.register_fake_lsp(
7815 "Rust",
7816 FakeLspAdapter {
7817 capabilities: lsp::ServerCapabilities {
7818 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7819 ..Default::default()
7820 },
7821 ..Default::default()
7822 },
7823 );
7824
7825 let worktree = project.update(cx, |project, cx| {
7826 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
7827 assert_eq!(worktrees.len(), 1);
7828 worktrees.pop().unwrap()
7829 });
7830 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
7831
7832 let buffer_1 = project
7833 .update(cx, |project, cx| {
7834 project.open_buffer((worktree_id, "main.rs"), cx)
7835 })
7836 .await
7837 .unwrap();
7838 let buffer_2 = project
7839 .update(cx, |project, cx| {
7840 project.open_buffer((worktree_id, "other.rs"), cx)
7841 })
7842 .await
7843 .unwrap();
7844 let buffer_3 = project
7845 .update(cx, |project, cx| {
7846 project.open_buffer((worktree_id, "lib.rs"), cx)
7847 })
7848 .await
7849 .unwrap();
7850
7851 let multi_buffer = cx.new(|cx| {
7852 let mut multi_buffer = MultiBuffer::new(ReadWrite);
7853 multi_buffer.push_excerpts(
7854 buffer_1.clone(),
7855 [
7856 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
7857 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
7858 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
7859 ],
7860 cx,
7861 );
7862 multi_buffer.push_excerpts(
7863 buffer_2.clone(),
7864 [
7865 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
7866 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
7867 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
7868 ],
7869 cx,
7870 );
7871 multi_buffer.push_excerpts(
7872 buffer_3.clone(),
7873 [
7874 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
7875 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
7876 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
7877 ],
7878 cx,
7879 );
7880 multi_buffer
7881 });
7882 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
7883 Editor::new(
7884 EditorMode::Full,
7885 multi_buffer,
7886 Some(project.clone()),
7887 window,
7888 cx,
7889 )
7890 });
7891
7892 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7893 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7894 s.select_ranges(Some(1..2))
7895 });
7896 editor.insert("|one|two|three|", window, cx);
7897 });
7898 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7899 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7900 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7901 s.select_ranges(Some(60..70))
7902 });
7903 editor.insert("|four|five|six|", window, cx);
7904 });
7905 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7906
7907 // First two buffers should be edited, but not the third one.
7908 assert_eq!(
7909 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
7910 "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}",
7911 );
7912 buffer_1.update(cx, |buffer, _| {
7913 assert!(buffer.is_dirty());
7914 assert_eq!(
7915 buffer.text(),
7916 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
7917 )
7918 });
7919 buffer_2.update(cx, |buffer, _| {
7920 assert!(buffer.is_dirty());
7921 assert_eq!(
7922 buffer.text(),
7923 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
7924 )
7925 });
7926 buffer_3.update(cx, |buffer, _| {
7927 assert!(!buffer.is_dirty());
7928 assert_eq!(buffer.text(), sample_text_3,)
7929 });
7930 cx.executor().run_until_parked();
7931
7932 cx.executor().start_waiting();
7933 let save = multi_buffer_editor
7934 .update_in(cx, |editor, window, cx| {
7935 editor.save(true, project.clone(), window, cx)
7936 })
7937 .unwrap();
7938
7939 let fake_server = fake_servers.next().await.unwrap();
7940 fake_server
7941 .server
7942 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7943 Ok(Some(vec![lsp::TextEdit::new(
7944 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7945 format!("[{} formatted]", params.text_document.uri),
7946 )]))
7947 })
7948 .detach();
7949 save.await;
7950
7951 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
7952 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
7953 assert_eq!(
7954 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
7955 uri!(
7956 "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}"
7957 ),
7958 );
7959 buffer_1.update(cx, |buffer, _| {
7960 assert!(!buffer.is_dirty());
7961 assert_eq!(
7962 buffer.text(),
7963 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
7964 )
7965 });
7966 buffer_2.update(cx, |buffer, _| {
7967 assert!(!buffer.is_dirty());
7968 assert_eq!(
7969 buffer.text(),
7970 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
7971 )
7972 });
7973 buffer_3.update(cx, |buffer, _| {
7974 assert!(!buffer.is_dirty());
7975 assert_eq!(buffer.text(), sample_text_3,)
7976 });
7977}
7978
7979#[gpui::test]
7980async fn test_range_format_during_save(cx: &mut TestAppContext) {
7981 init_test(cx, |_| {});
7982
7983 let fs = FakeFs::new(cx.executor());
7984 fs.insert_file(path!("/file.rs"), Default::default()).await;
7985
7986 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
7987
7988 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7989 language_registry.add(rust_lang());
7990 let mut fake_servers = language_registry.register_fake_lsp(
7991 "Rust",
7992 FakeLspAdapter {
7993 capabilities: lsp::ServerCapabilities {
7994 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
7995 ..Default::default()
7996 },
7997 ..Default::default()
7998 },
7999 );
8000
8001 let buffer = project
8002 .update(cx, |project, cx| {
8003 project.open_local_buffer(path!("/file.rs"), cx)
8004 })
8005 .await
8006 .unwrap();
8007
8008 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8009 let (editor, cx) = cx.add_window_view(|window, cx| {
8010 build_editor_with_project(project.clone(), buffer, window, cx)
8011 });
8012 editor.update_in(cx, |editor, window, cx| {
8013 editor.set_text("one\ntwo\nthree\n", window, cx)
8014 });
8015 assert!(cx.read(|cx| editor.is_dirty(cx)));
8016
8017 cx.executor().start_waiting();
8018 let fake_server = fake_servers.next().await.unwrap();
8019
8020 let save = editor
8021 .update_in(cx, |editor, window, cx| {
8022 editor.save(true, project.clone(), window, cx)
8023 })
8024 .unwrap();
8025 fake_server
8026 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8027 assert_eq!(
8028 params.text_document.uri,
8029 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8030 );
8031 assert_eq!(params.options.tab_size, 4);
8032 Ok(Some(vec![lsp::TextEdit::new(
8033 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8034 ", ".to_string(),
8035 )]))
8036 })
8037 .next()
8038 .await;
8039 cx.executor().start_waiting();
8040 save.await;
8041 assert_eq!(
8042 editor.update(cx, |editor, cx| editor.text(cx)),
8043 "one, two\nthree\n"
8044 );
8045 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8046
8047 editor.update_in(cx, |editor, window, cx| {
8048 editor.set_text("one\ntwo\nthree\n", window, cx)
8049 });
8050 assert!(cx.read(|cx| editor.is_dirty(cx)));
8051
8052 // Ensure we can still save even if formatting hangs.
8053 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
8054 move |params, _| async move {
8055 assert_eq!(
8056 params.text_document.uri,
8057 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8058 );
8059 futures::future::pending::<()>().await;
8060 unreachable!()
8061 },
8062 );
8063 let save = editor
8064 .update_in(cx, |editor, window, cx| {
8065 editor.save(true, project.clone(), window, cx)
8066 })
8067 .unwrap();
8068 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8069 cx.executor().start_waiting();
8070 save.await;
8071 assert_eq!(
8072 editor.update(cx, |editor, cx| editor.text(cx)),
8073 "one\ntwo\nthree\n"
8074 );
8075 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8076
8077 // For non-dirty buffer, no formatting request should be sent
8078 let save = editor
8079 .update_in(cx, |editor, window, cx| {
8080 editor.save(true, project.clone(), window, cx)
8081 })
8082 .unwrap();
8083 let _pending_format_request = fake_server
8084 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
8085 panic!("Should not be invoked on non-dirty buffer");
8086 })
8087 .next();
8088 cx.executor().start_waiting();
8089 save.await;
8090
8091 // Set Rust language override and assert overridden tabsize is sent to language server
8092 update_test_language_settings(cx, |settings| {
8093 settings.languages.insert(
8094 "Rust".into(),
8095 LanguageSettingsContent {
8096 tab_size: NonZeroU32::new(8),
8097 ..Default::default()
8098 },
8099 );
8100 });
8101
8102 editor.update_in(cx, |editor, window, cx| {
8103 editor.set_text("somehting_new\n", window, cx)
8104 });
8105 assert!(cx.read(|cx| editor.is_dirty(cx)));
8106 let save = editor
8107 .update_in(cx, |editor, window, cx| {
8108 editor.save(true, project.clone(), window, cx)
8109 })
8110 .unwrap();
8111 fake_server
8112 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8113 assert_eq!(
8114 params.text_document.uri,
8115 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8116 );
8117 assert_eq!(params.options.tab_size, 8);
8118 Ok(Some(vec![]))
8119 })
8120 .next()
8121 .await;
8122 cx.executor().start_waiting();
8123 save.await;
8124}
8125
8126#[gpui::test]
8127async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
8128 init_test(cx, |settings| {
8129 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8130 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8131 ))
8132 });
8133
8134 let fs = FakeFs::new(cx.executor());
8135 fs.insert_file(path!("/file.rs"), Default::default()).await;
8136
8137 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8138
8139 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8140 language_registry.add(Arc::new(Language::new(
8141 LanguageConfig {
8142 name: "Rust".into(),
8143 matcher: LanguageMatcher {
8144 path_suffixes: vec!["rs".to_string()],
8145 ..Default::default()
8146 },
8147 ..LanguageConfig::default()
8148 },
8149 Some(tree_sitter_rust::LANGUAGE.into()),
8150 )));
8151 update_test_language_settings(cx, |settings| {
8152 // Enable Prettier formatting for the same buffer, and ensure
8153 // LSP is called instead of Prettier.
8154 settings.defaults.prettier = Some(PrettierSettings {
8155 allowed: true,
8156 ..PrettierSettings::default()
8157 });
8158 });
8159 let mut fake_servers = language_registry.register_fake_lsp(
8160 "Rust",
8161 FakeLspAdapter {
8162 capabilities: lsp::ServerCapabilities {
8163 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8164 ..Default::default()
8165 },
8166 ..Default::default()
8167 },
8168 );
8169
8170 let buffer = project
8171 .update(cx, |project, cx| {
8172 project.open_local_buffer(path!("/file.rs"), cx)
8173 })
8174 .await
8175 .unwrap();
8176
8177 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8178 let (editor, cx) = cx.add_window_view(|window, cx| {
8179 build_editor_with_project(project.clone(), buffer, window, cx)
8180 });
8181 editor.update_in(cx, |editor, window, cx| {
8182 editor.set_text("one\ntwo\nthree\n", window, cx)
8183 });
8184
8185 cx.executor().start_waiting();
8186 let fake_server = fake_servers.next().await.unwrap();
8187
8188 let format = editor
8189 .update_in(cx, |editor, window, cx| {
8190 editor.perform_format(
8191 project.clone(),
8192 FormatTrigger::Manual,
8193 FormatTarget::Buffers,
8194 window,
8195 cx,
8196 )
8197 })
8198 .unwrap();
8199 fake_server
8200 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8201 assert_eq!(
8202 params.text_document.uri,
8203 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8204 );
8205 assert_eq!(params.options.tab_size, 4);
8206 Ok(Some(vec![lsp::TextEdit::new(
8207 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8208 ", ".to_string(),
8209 )]))
8210 })
8211 .next()
8212 .await;
8213 cx.executor().start_waiting();
8214 format.await;
8215 assert_eq!(
8216 editor.update(cx, |editor, cx| editor.text(cx)),
8217 "one, two\nthree\n"
8218 );
8219
8220 editor.update_in(cx, |editor, window, cx| {
8221 editor.set_text("one\ntwo\nthree\n", window, cx)
8222 });
8223 // Ensure we don't lock if formatting hangs.
8224 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8225 move |params, _| async move {
8226 assert_eq!(
8227 params.text_document.uri,
8228 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8229 );
8230 futures::future::pending::<()>().await;
8231 unreachable!()
8232 },
8233 );
8234 let format = editor
8235 .update_in(cx, |editor, window, cx| {
8236 editor.perform_format(
8237 project,
8238 FormatTrigger::Manual,
8239 FormatTarget::Buffers,
8240 window,
8241 cx,
8242 )
8243 })
8244 .unwrap();
8245 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8246 cx.executor().start_waiting();
8247 format.await;
8248 assert_eq!(
8249 editor.update(cx, |editor, cx| editor.text(cx)),
8250 "one\ntwo\nthree\n"
8251 );
8252}
8253
8254#[gpui::test]
8255async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
8256 init_test(cx, |settings| {
8257 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8258 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8259 ))
8260 });
8261
8262 let fs = FakeFs::new(cx.executor());
8263 fs.insert_file(path!("/file.ts"), Default::default()).await;
8264
8265 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8266
8267 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8268 language_registry.add(Arc::new(Language::new(
8269 LanguageConfig {
8270 name: "TypeScript".into(),
8271 matcher: LanguageMatcher {
8272 path_suffixes: vec!["ts".to_string()],
8273 ..Default::default()
8274 },
8275 ..LanguageConfig::default()
8276 },
8277 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
8278 )));
8279 update_test_language_settings(cx, |settings| {
8280 settings.defaults.prettier = Some(PrettierSettings {
8281 allowed: true,
8282 ..PrettierSettings::default()
8283 });
8284 });
8285 let mut fake_servers = language_registry.register_fake_lsp(
8286 "TypeScript",
8287 FakeLspAdapter {
8288 capabilities: lsp::ServerCapabilities {
8289 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
8290 ..Default::default()
8291 },
8292 ..Default::default()
8293 },
8294 );
8295
8296 let buffer = project
8297 .update(cx, |project, cx| {
8298 project.open_local_buffer(path!("/file.ts"), cx)
8299 })
8300 .await
8301 .unwrap();
8302
8303 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8304 let (editor, cx) = cx.add_window_view(|window, cx| {
8305 build_editor_with_project(project.clone(), buffer, window, cx)
8306 });
8307 editor.update_in(cx, |editor, window, cx| {
8308 editor.set_text(
8309 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
8310 window,
8311 cx,
8312 )
8313 });
8314
8315 cx.executor().start_waiting();
8316 let fake_server = fake_servers.next().await.unwrap();
8317
8318 let format = editor
8319 .update_in(cx, |editor, window, cx| {
8320 editor.perform_code_action_kind(
8321 project.clone(),
8322 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
8323 window,
8324 cx,
8325 )
8326 })
8327 .unwrap();
8328 fake_server
8329 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
8330 assert_eq!(
8331 params.text_document.uri,
8332 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
8333 );
8334 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
8335 lsp::CodeAction {
8336 title: "Organize Imports".to_string(),
8337 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
8338 edit: Some(lsp::WorkspaceEdit {
8339 changes: Some(
8340 [(
8341 params.text_document.uri.clone(),
8342 vec![lsp::TextEdit::new(
8343 lsp::Range::new(
8344 lsp::Position::new(1, 0),
8345 lsp::Position::new(2, 0),
8346 ),
8347 "".to_string(),
8348 )],
8349 )]
8350 .into_iter()
8351 .collect(),
8352 ),
8353 ..Default::default()
8354 }),
8355 ..Default::default()
8356 },
8357 )]))
8358 })
8359 .next()
8360 .await;
8361 cx.executor().start_waiting();
8362 format.await;
8363 assert_eq!(
8364 editor.update(cx, |editor, cx| editor.text(cx)),
8365 "import { a } from 'module';\n\nconst x = a;\n"
8366 );
8367
8368 editor.update_in(cx, |editor, window, cx| {
8369 editor.set_text(
8370 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
8371 window,
8372 cx,
8373 )
8374 });
8375 // Ensure we don't lock if code action hangs.
8376 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
8377 move |params, _| async move {
8378 assert_eq!(
8379 params.text_document.uri,
8380 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
8381 );
8382 futures::future::pending::<()>().await;
8383 unreachable!()
8384 },
8385 );
8386 let format = editor
8387 .update_in(cx, |editor, window, cx| {
8388 editor.perform_code_action_kind(
8389 project,
8390 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
8391 window,
8392 cx,
8393 )
8394 })
8395 .unwrap();
8396 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
8397 cx.executor().start_waiting();
8398 format.await;
8399 assert_eq!(
8400 editor.update(cx, |editor, cx| editor.text(cx)),
8401 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
8402 );
8403}
8404
8405#[gpui::test]
8406async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
8407 init_test(cx, |_| {});
8408
8409 let mut cx = EditorLspTestContext::new_rust(
8410 lsp::ServerCapabilities {
8411 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8412 ..Default::default()
8413 },
8414 cx,
8415 )
8416 .await;
8417
8418 cx.set_state(indoc! {"
8419 one.twoˇ
8420 "});
8421
8422 // The format request takes a long time. When it completes, it inserts
8423 // a newline and an indent before the `.`
8424 cx.lsp
8425 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
8426 let executor = cx.background_executor().clone();
8427 async move {
8428 executor.timer(Duration::from_millis(100)).await;
8429 Ok(Some(vec![lsp::TextEdit {
8430 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
8431 new_text: "\n ".into(),
8432 }]))
8433 }
8434 });
8435
8436 // Submit a format request.
8437 let format_1 = cx
8438 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8439 .unwrap();
8440 cx.executor().run_until_parked();
8441
8442 // Submit a second format request.
8443 let format_2 = cx
8444 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8445 .unwrap();
8446 cx.executor().run_until_parked();
8447
8448 // Wait for both format requests to complete
8449 cx.executor().advance_clock(Duration::from_millis(200));
8450 cx.executor().start_waiting();
8451 format_1.await.unwrap();
8452 cx.executor().start_waiting();
8453 format_2.await.unwrap();
8454
8455 // The formatting edits only happens once.
8456 cx.assert_editor_state(indoc! {"
8457 one
8458 .twoˇ
8459 "});
8460}
8461
8462#[gpui::test]
8463async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
8464 init_test(cx, |settings| {
8465 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
8466 });
8467
8468 let mut cx = EditorLspTestContext::new_rust(
8469 lsp::ServerCapabilities {
8470 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8471 ..Default::default()
8472 },
8473 cx,
8474 )
8475 .await;
8476
8477 // Set up a buffer white some trailing whitespace and no trailing newline.
8478 cx.set_state(
8479 &[
8480 "one ", //
8481 "twoˇ", //
8482 "three ", //
8483 "four", //
8484 ]
8485 .join("\n"),
8486 );
8487
8488 // Submit a format request.
8489 let format = cx
8490 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8491 .unwrap();
8492
8493 // Record which buffer changes have been sent to the language server
8494 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
8495 cx.lsp
8496 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
8497 let buffer_changes = buffer_changes.clone();
8498 move |params, _| {
8499 buffer_changes.lock().extend(
8500 params
8501 .content_changes
8502 .into_iter()
8503 .map(|e| (e.range.unwrap(), e.text)),
8504 );
8505 }
8506 });
8507
8508 // Handle formatting requests to the language server.
8509 cx.lsp
8510 .set_request_handler::<lsp::request::Formatting, _, _>({
8511 let buffer_changes = buffer_changes.clone();
8512 move |_, _| {
8513 // When formatting is requested, trailing whitespace has already been stripped,
8514 // and the trailing newline has already been added.
8515 assert_eq!(
8516 &buffer_changes.lock()[1..],
8517 &[
8518 (
8519 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
8520 "".into()
8521 ),
8522 (
8523 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
8524 "".into()
8525 ),
8526 (
8527 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
8528 "\n".into()
8529 ),
8530 ]
8531 );
8532
8533 // Insert blank lines between each line of the buffer.
8534 async move {
8535 Ok(Some(vec![
8536 lsp::TextEdit {
8537 range: lsp::Range::new(
8538 lsp::Position::new(1, 0),
8539 lsp::Position::new(1, 0),
8540 ),
8541 new_text: "\n".into(),
8542 },
8543 lsp::TextEdit {
8544 range: lsp::Range::new(
8545 lsp::Position::new(2, 0),
8546 lsp::Position::new(2, 0),
8547 ),
8548 new_text: "\n".into(),
8549 },
8550 ]))
8551 }
8552 }
8553 });
8554
8555 // After formatting the buffer, the trailing whitespace is stripped,
8556 // a newline is appended, and the edits provided by the language server
8557 // have been applied.
8558 format.await.unwrap();
8559 cx.assert_editor_state(
8560 &[
8561 "one", //
8562 "", //
8563 "twoˇ", //
8564 "", //
8565 "three", //
8566 "four", //
8567 "", //
8568 ]
8569 .join("\n"),
8570 );
8571
8572 // Undoing the formatting undoes the trailing whitespace removal, the
8573 // trailing newline, and the LSP edits.
8574 cx.update_buffer(|buffer, cx| buffer.undo(cx));
8575 cx.assert_editor_state(
8576 &[
8577 "one ", //
8578 "twoˇ", //
8579 "three ", //
8580 "four", //
8581 ]
8582 .join("\n"),
8583 );
8584}
8585
8586#[gpui::test]
8587async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
8588 cx: &mut TestAppContext,
8589) {
8590 init_test(cx, |_| {});
8591
8592 cx.update(|cx| {
8593 cx.update_global::<SettingsStore, _>(|settings, cx| {
8594 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8595 settings.auto_signature_help = Some(true);
8596 });
8597 });
8598 });
8599
8600 let mut cx = EditorLspTestContext::new_rust(
8601 lsp::ServerCapabilities {
8602 signature_help_provider: Some(lsp::SignatureHelpOptions {
8603 ..Default::default()
8604 }),
8605 ..Default::default()
8606 },
8607 cx,
8608 )
8609 .await;
8610
8611 let language = Language::new(
8612 LanguageConfig {
8613 name: "Rust".into(),
8614 brackets: BracketPairConfig {
8615 pairs: vec![
8616 BracketPair {
8617 start: "{".to_string(),
8618 end: "}".to_string(),
8619 close: true,
8620 surround: true,
8621 newline: true,
8622 },
8623 BracketPair {
8624 start: "(".to_string(),
8625 end: ")".to_string(),
8626 close: true,
8627 surround: true,
8628 newline: true,
8629 },
8630 BracketPair {
8631 start: "/*".to_string(),
8632 end: " */".to_string(),
8633 close: true,
8634 surround: true,
8635 newline: true,
8636 },
8637 BracketPair {
8638 start: "[".to_string(),
8639 end: "]".to_string(),
8640 close: false,
8641 surround: false,
8642 newline: true,
8643 },
8644 BracketPair {
8645 start: "\"".to_string(),
8646 end: "\"".to_string(),
8647 close: true,
8648 surround: true,
8649 newline: false,
8650 },
8651 BracketPair {
8652 start: "<".to_string(),
8653 end: ">".to_string(),
8654 close: false,
8655 surround: true,
8656 newline: true,
8657 },
8658 ],
8659 ..Default::default()
8660 },
8661 autoclose_before: "})]".to_string(),
8662 ..Default::default()
8663 },
8664 Some(tree_sitter_rust::LANGUAGE.into()),
8665 );
8666 let language = Arc::new(language);
8667
8668 cx.language_registry().add(language.clone());
8669 cx.update_buffer(|buffer, cx| {
8670 buffer.set_language(Some(language), cx);
8671 });
8672
8673 cx.set_state(
8674 &r#"
8675 fn main() {
8676 sampleˇ
8677 }
8678 "#
8679 .unindent(),
8680 );
8681
8682 cx.update_editor(|editor, window, cx| {
8683 editor.handle_input("(", window, cx);
8684 });
8685 cx.assert_editor_state(
8686 &"
8687 fn main() {
8688 sample(ˇ)
8689 }
8690 "
8691 .unindent(),
8692 );
8693
8694 let mocked_response = lsp::SignatureHelp {
8695 signatures: vec![lsp::SignatureInformation {
8696 label: "fn sample(param1: u8, param2: u8)".to_string(),
8697 documentation: None,
8698 parameters: Some(vec![
8699 lsp::ParameterInformation {
8700 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8701 documentation: None,
8702 },
8703 lsp::ParameterInformation {
8704 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8705 documentation: None,
8706 },
8707 ]),
8708 active_parameter: None,
8709 }],
8710 active_signature: Some(0),
8711 active_parameter: Some(0),
8712 };
8713 handle_signature_help_request(&mut cx, mocked_response).await;
8714
8715 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8716 .await;
8717
8718 cx.editor(|editor, _, _| {
8719 let signature_help_state = editor.signature_help_state.popover().cloned();
8720 assert_eq!(
8721 signature_help_state.unwrap().label,
8722 "param1: u8, param2: u8"
8723 );
8724 });
8725}
8726
8727#[gpui::test]
8728async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
8729 init_test(cx, |_| {});
8730
8731 cx.update(|cx| {
8732 cx.update_global::<SettingsStore, _>(|settings, cx| {
8733 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8734 settings.auto_signature_help = Some(false);
8735 settings.show_signature_help_after_edits = Some(false);
8736 });
8737 });
8738 });
8739
8740 let mut cx = EditorLspTestContext::new_rust(
8741 lsp::ServerCapabilities {
8742 signature_help_provider: Some(lsp::SignatureHelpOptions {
8743 ..Default::default()
8744 }),
8745 ..Default::default()
8746 },
8747 cx,
8748 )
8749 .await;
8750
8751 let language = Language::new(
8752 LanguageConfig {
8753 name: "Rust".into(),
8754 brackets: BracketPairConfig {
8755 pairs: vec![
8756 BracketPair {
8757 start: "{".to_string(),
8758 end: "}".to_string(),
8759 close: true,
8760 surround: true,
8761 newline: true,
8762 },
8763 BracketPair {
8764 start: "(".to_string(),
8765 end: ")".to_string(),
8766 close: true,
8767 surround: true,
8768 newline: true,
8769 },
8770 BracketPair {
8771 start: "/*".to_string(),
8772 end: " */".to_string(),
8773 close: true,
8774 surround: true,
8775 newline: true,
8776 },
8777 BracketPair {
8778 start: "[".to_string(),
8779 end: "]".to_string(),
8780 close: false,
8781 surround: false,
8782 newline: true,
8783 },
8784 BracketPair {
8785 start: "\"".to_string(),
8786 end: "\"".to_string(),
8787 close: true,
8788 surround: true,
8789 newline: false,
8790 },
8791 BracketPair {
8792 start: "<".to_string(),
8793 end: ">".to_string(),
8794 close: false,
8795 surround: true,
8796 newline: true,
8797 },
8798 ],
8799 ..Default::default()
8800 },
8801 autoclose_before: "})]".to_string(),
8802 ..Default::default()
8803 },
8804 Some(tree_sitter_rust::LANGUAGE.into()),
8805 );
8806 let language = Arc::new(language);
8807
8808 cx.language_registry().add(language.clone());
8809 cx.update_buffer(|buffer, cx| {
8810 buffer.set_language(Some(language), cx);
8811 });
8812
8813 // Ensure that signature_help is not called when no signature help is enabled.
8814 cx.set_state(
8815 &r#"
8816 fn main() {
8817 sampleˇ
8818 }
8819 "#
8820 .unindent(),
8821 );
8822 cx.update_editor(|editor, window, cx| {
8823 editor.handle_input("(", window, cx);
8824 });
8825 cx.assert_editor_state(
8826 &"
8827 fn main() {
8828 sample(ˇ)
8829 }
8830 "
8831 .unindent(),
8832 );
8833 cx.editor(|editor, _, _| {
8834 assert!(editor.signature_help_state.task().is_none());
8835 });
8836
8837 let mocked_response = lsp::SignatureHelp {
8838 signatures: vec![lsp::SignatureInformation {
8839 label: "fn sample(param1: u8, param2: u8)".to_string(),
8840 documentation: None,
8841 parameters: Some(vec![
8842 lsp::ParameterInformation {
8843 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8844 documentation: None,
8845 },
8846 lsp::ParameterInformation {
8847 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8848 documentation: None,
8849 },
8850 ]),
8851 active_parameter: None,
8852 }],
8853 active_signature: Some(0),
8854 active_parameter: Some(0),
8855 };
8856
8857 // Ensure that signature_help is called when enabled afte edits
8858 cx.update(|_, cx| {
8859 cx.update_global::<SettingsStore, _>(|settings, cx| {
8860 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8861 settings.auto_signature_help = Some(false);
8862 settings.show_signature_help_after_edits = Some(true);
8863 });
8864 });
8865 });
8866 cx.set_state(
8867 &r#"
8868 fn main() {
8869 sampleˇ
8870 }
8871 "#
8872 .unindent(),
8873 );
8874 cx.update_editor(|editor, window, cx| {
8875 editor.handle_input("(", window, cx);
8876 });
8877 cx.assert_editor_state(
8878 &"
8879 fn main() {
8880 sample(ˇ)
8881 }
8882 "
8883 .unindent(),
8884 );
8885 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8886 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8887 .await;
8888 cx.update_editor(|editor, _, _| {
8889 let signature_help_state = editor.signature_help_state.popover().cloned();
8890 assert!(signature_help_state.is_some());
8891 assert_eq!(
8892 signature_help_state.unwrap().label,
8893 "param1: u8, param2: u8"
8894 );
8895 editor.signature_help_state = SignatureHelpState::default();
8896 });
8897
8898 // Ensure that signature_help is called when auto signature help override is enabled
8899 cx.update(|_, cx| {
8900 cx.update_global::<SettingsStore, _>(|settings, cx| {
8901 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8902 settings.auto_signature_help = Some(true);
8903 settings.show_signature_help_after_edits = Some(false);
8904 });
8905 });
8906 });
8907 cx.set_state(
8908 &r#"
8909 fn main() {
8910 sampleˇ
8911 }
8912 "#
8913 .unindent(),
8914 );
8915 cx.update_editor(|editor, window, cx| {
8916 editor.handle_input("(", window, cx);
8917 });
8918 cx.assert_editor_state(
8919 &"
8920 fn main() {
8921 sample(ˇ)
8922 }
8923 "
8924 .unindent(),
8925 );
8926 handle_signature_help_request(&mut cx, mocked_response).await;
8927 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8928 .await;
8929 cx.editor(|editor, _, _| {
8930 let signature_help_state = editor.signature_help_state.popover().cloned();
8931 assert!(signature_help_state.is_some());
8932 assert_eq!(
8933 signature_help_state.unwrap().label,
8934 "param1: u8, param2: u8"
8935 );
8936 });
8937}
8938
8939#[gpui::test]
8940async fn test_signature_help(cx: &mut TestAppContext) {
8941 init_test(cx, |_| {});
8942 cx.update(|cx| {
8943 cx.update_global::<SettingsStore, _>(|settings, cx| {
8944 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8945 settings.auto_signature_help = Some(true);
8946 });
8947 });
8948 });
8949
8950 let mut cx = EditorLspTestContext::new_rust(
8951 lsp::ServerCapabilities {
8952 signature_help_provider: Some(lsp::SignatureHelpOptions {
8953 ..Default::default()
8954 }),
8955 ..Default::default()
8956 },
8957 cx,
8958 )
8959 .await;
8960
8961 // A test that directly calls `show_signature_help`
8962 cx.update_editor(|editor, window, cx| {
8963 editor.show_signature_help(&ShowSignatureHelp, window, cx);
8964 });
8965
8966 let mocked_response = lsp::SignatureHelp {
8967 signatures: vec![lsp::SignatureInformation {
8968 label: "fn sample(param1: u8, param2: u8)".to_string(),
8969 documentation: None,
8970 parameters: Some(vec![
8971 lsp::ParameterInformation {
8972 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8973 documentation: None,
8974 },
8975 lsp::ParameterInformation {
8976 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8977 documentation: None,
8978 },
8979 ]),
8980 active_parameter: None,
8981 }],
8982 active_signature: Some(0),
8983 active_parameter: Some(0),
8984 };
8985 handle_signature_help_request(&mut cx, mocked_response).await;
8986
8987 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8988 .await;
8989
8990 cx.editor(|editor, _, _| {
8991 let signature_help_state = editor.signature_help_state.popover().cloned();
8992 assert!(signature_help_state.is_some());
8993 assert_eq!(
8994 signature_help_state.unwrap().label,
8995 "param1: u8, param2: u8"
8996 );
8997 });
8998
8999 // When exiting outside from inside the brackets, `signature_help` is closed.
9000 cx.set_state(indoc! {"
9001 fn main() {
9002 sample(ˇ);
9003 }
9004
9005 fn sample(param1: u8, param2: u8) {}
9006 "});
9007
9008 cx.update_editor(|editor, window, cx| {
9009 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
9010 });
9011
9012 let mocked_response = lsp::SignatureHelp {
9013 signatures: Vec::new(),
9014 active_signature: None,
9015 active_parameter: None,
9016 };
9017 handle_signature_help_request(&mut cx, mocked_response).await;
9018
9019 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
9020 .await;
9021
9022 cx.editor(|editor, _, _| {
9023 assert!(!editor.signature_help_state.is_shown());
9024 });
9025
9026 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
9027 cx.set_state(indoc! {"
9028 fn main() {
9029 sample(ˇ);
9030 }
9031
9032 fn sample(param1: u8, param2: u8) {}
9033 "});
9034
9035 let mocked_response = lsp::SignatureHelp {
9036 signatures: vec![lsp::SignatureInformation {
9037 label: "fn sample(param1: u8, param2: u8)".to_string(),
9038 documentation: None,
9039 parameters: Some(vec![
9040 lsp::ParameterInformation {
9041 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9042 documentation: None,
9043 },
9044 lsp::ParameterInformation {
9045 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9046 documentation: None,
9047 },
9048 ]),
9049 active_parameter: None,
9050 }],
9051 active_signature: Some(0),
9052 active_parameter: Some(0),
9053 };
9054 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9055 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9056 .await;
9057 cx.editor(|editor, _, _| {
9058 assert!(editor.signature_help_state.is_shown());
9059 });
9060
9061 // Restore the popover with more parameter input
9062 cx.set_state(indoc! {"
9063 fn main() {
9064 sample(param1, param2ˇ);
9065 }
9066
9067 fn sample(param1: u8, param2: u8) {}
9068 "});
9069
9070 let mocked_response = lsp::SignatureHelp {
9071 signatures: vec![lsp::SignatureInformation {
9072 label: "fn sample(param1: u8, param2: u8)".to_string(),
9073 documentation: None,
9074 parameters: Some(vec![
9075 lsp::ParameterInformation {
9076 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9077 documentation: None,
9078 },
9079 lsp::ParameterInformation {
9080 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9081 documentation: None,
9082 },
9083 ]),
9084 active_parameter: None,
9085 }],
9086 active_signature: Some(0),
9087 active_parameter: Some(1),
9088 };
9089 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9090 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9091 .await;
9092
9093 // When selecting a range, the popover is gone.
9094 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
9095 cx.update_editor(|editor, window, cx| {
9096 editor.change_selections(None, window, cx, |s| {
9097 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
9098 })
9099 });
9100 cx.assert_editor_state(indoc! {"
9101 fn main() {
9102 sample(param1, «ˇparam2»);
9103 }
9104
9105 fn sample(param1: u8, param2: u8) {}
9106 "});
9107 cx.editor(|editor, _, _| {
9108 assert!(!editor.signature_help_state.is_shown());
9109 });
9110
9111 // When unselecting again, the popover is back if within the brackets.
9112 cx.update_editor(|editor, window, cx| {
9113 editor.change_selections(None, window, cx, |s| {
9114 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9115 })
9116 });
9117 cx.assert_editor_state(indoc! {"
9118 fn main() {
9119 sample(param1, ˇparam2);
9120 }
9121
9122 fn sample(param1: u8, param2: u8) {}
9123 "});
9124 handle_signature_help_request(&mut cx, mocked_response).await;
9125 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9126 .await;
9127 cx.editor(|editor, _, _| {
9128 assert!(editor.signature_help_state.is_shown());
9129 });
9130
9131 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
9132 cx.update_editor(|editor, window, cx| {
9133 editor.change_selections(None, window, cx, |s| {
9134 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
9135 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9136 })
9137 });
9138 cx.assert_editor_state(indoc! {"
9139 fn main() {
9140 sample(param1, ˇparam2);
9141 }
9142
9143 fn sample(param1: u8, param2: u8) {}
9144 "});
9145
9146 let mocked_response = lsp::SignatureHelp {
9147 signatures: vec![lsp::SignatureInformation {
9148 label: "fn sample(param1: u8, param2: u8)".to_string(),
9149 documentation: None,
9150 parameters: Some(vec![
9151 lsp::ParameterInformation {
9152 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9153 documentation: None,
9154 },
9155 lsp::ParameterInformation {
9156 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9157 documentation: None,
9158 },
9159 ]),
9160 active_parameter: None,
9161 }],
9162 active_signature: Some(0),
9163 active_parameter: Some(1),
9164 };
9165 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9166 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9167 .await;
9168 cx.update_editor(|editor, _, cx| {
9169 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
9170 });
9171 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
9172 .await;
9173 cx.update_editor(|editor, window, cx| {
9174 editor.change_selections(None, window, cx, |s| {
9175 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
9176 })
9177 });
9178 cx.assert_editor_state(indoc! {"
9179 fn main() {
9180 sample(param1, «ˇparam2»);
9181 }
9182
9183 fn sample(param1: u8, param2: u8) {}
9184 "});
9185 cx.update_editor(|editor, window, cx| {
9186 editor.change_selections(None, window, cx, |s| {
9187 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9188 })
9189 });
9190 cx.assert_editor_state(indoc! {"
9191 fn main() {
9192 sample(param1, ˇparam2);
9193 }
9194
9195 fn sample(param1: u8, param2: u8) {}
9196 "});
9197 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
9198 .await;
9199}
9200
9201#[gpui::test]
9202async fn test_completion(cx: &mut TestAppContext) {
9203 init_test(cx, |_| {});
9204
9205 let mut cx = EditorLspTestContext::new_rust(
9206 lsp::ServerCapabilities {
9207 completion_provider: Some(lsp::CompletionOptions {
9208 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9209 resolve_provider: Some(true),
9210 ..Default::default()
9211 }),
9212 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9213 ..Default::default()
9214 },
9215 cx,
9216 )
9217 .await;
9218 let counter = Arc::new(AtomicUsize::new(0));
9219
9220 cx.set_state(indoc! {"
9221 oneˇ
9222 two
9223 three
9224 "});
9225 cx.simulate_keystroke(".");
9226 handle_completion_request(
9227 &mut cx,
9228 indoc! {"
9229 one.|<>
9230 two
9231 three
9232 "},
9233 vec!["first_completion", "second_completion"],
9234 counter.clone(),
9235 )
9236 .await;
9237 cx.condition(|editor, _| editor.context_menu_visible())
9238 .await;
9239 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
9240
9241 let _handler = handle_signature_help_request(
9242 &mut cx,
9243 lsp::SignatureHelp {
9244 signatures: vec![lsp::SignatureInformation {
9245 label: "test signature".to_string(),
9246 documentation: None,
9247 parameters: Some(vec![lsp::ParameterInformation {
9248 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
9249 documentation: None,
9250 }]),
9251 active_parameter: None,
9252 }],
9253 active_signature: None,
9254 active_parameter: None,
9255 },
9256 );
9257 cx.update_editor(|editor, window, cx| {
9258 assert!(
9259 !editor.signature_help_state.is_shown(),
9260 "No signature help was called for"
9261 );
9262 editor.show_signature_help(&ShowSignatureHelp, window, cx);
9263 });
9264 cx.run_until_parked();
9265 cx.update_editor(|editor, _, _| {
9266 assert!(
9267 !editor.signature_help_state.is_shown(),
9268 "No signature help should be shown when completions menu is open"
9269 );
9270 });
9271
9272 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9273 editor.context_menu_next(&Default::default(), window, cx);
9274 editor
9275 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9276 .unwrap()
9277 });
9278 cx.assert_editor_state(indoc! {"
9279 one.second_completionˇ
9280 two
9281 three
9282 "});
9283
9284 handle_resolve_completion_request(
9285 &mut cx,
9286 Some(vec![
9287 (
9288 //This overlaps with the primary completion edit which is
9289 //misbehavior from the LSP spec, test that we filter it out
9290 indoc! {"
9291 one.second_ˇcompletion
9292 two
9293 threeˇ
9294 "},
9295 "overlapping additional edit",
9296 ),
9297 (
9298 indoc! {"
9299 one.second_completion
9300 two
9301 threeˇ
9302 "},
9303 "\nadditional edit",
9304 ),
9305 ]),
9306 )
9307 .await;
9308 apply_additional_edits.await.unwrap();
9309 cx.assert_editor_state(indoc! {"
9310 one.second_completionˇ
9311 two
9312 three
9313 additional edit
9314 "});
9315
9316 cx.set_state(indoc! {"
9317 one.second_completion
9318 twoˇ
9319 threeˇ
9320 additional edit
9321 "});
9322 cx.simulate_keystroke(" ");
9323 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9324 cx.simulate_keystroke("s");
9325 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9326
9327 cx.assert_editor_state(indoc! {"
9328 one.second_completion
9329 two sˇ
9330 three sˇ
9331 additional edit
9332 "});
9333 handle_completion_request(
9334 &mut cx,
9335 indoc! {"
9336 one.second_completion
9337 two s
9338 three <s|>
9339 additional edit
9340 "},
9341 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
9342 counter.clone(),
9343 )
9344 .await;
9345 cx.condition(|editor, _| editor.context_menu_visible())
9346 .await;
9347 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
9348
9349 cx.simulate_keystroke("i");
9350
9351 handle_completion_request(
9352 &mut cx,
9353 indoc! {"
9354 one.second_completion
9355 two si
9356 three <si|>
9357 additional edit
9358 "},
9359 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
9360 counter.clone(),
9361 )
9362 .await;
9363 cx.condition(|editor, _| editor.context_menu_visible())
9364 .await;
9365 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
9366
9367 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9368 editor
9369 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9370 .unwrap()
9371 });
9372 cx.assert_editor_state(indoc! {"
9373 one.second_completion
9374 two sixth_completionˇ
9375 three sixth_completionˇ
9376 additional edit
9377 "});
9378
9379 apply_additional_edits.await.unwrap();
9380
9381 update_test_language_settings(&mut cx, |settings| {
9382 settings.defaults.show_completions_on_input = Some(false);
9383 });
9384 cx.set_state("editorˇ");
9385 cx.simulate_keystroke(".");
9386 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9387 cx.simulate_keystrokes("c l o");
9388 cx.assert_editor_state("editor.cloˇ");
9389 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9390 cx.update_editor(|editor, window, cx| {
9391 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
9392 });
9393 handle_completion_request(
9394 &mut cx,
9395 "editor.<clo|>",
9396 vec!["close", "clobber"],
9397 counter.clone(),
9398 )
9399 .await;
9400 cx.condition(|editor, _| editor.context_menu_visible())
9401 .await;
9402 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
9403
9404 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9405 editor
9406 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9407 .unwrap()
9408 });
9409 cx.assert_editor_state("editor.closeˇ");
9410 handle_resolve_completion_request(&mut cx, None).await;
9411 apply_additional_edits.await.unwrap();
9412}
9413
9414#[gpui::test]
9415async fn test_word_completion(cx: &mut TestAppContext) {
9416 let lsp_fetch_timeout_ms = 10;
9417 init_test(cx, |language_settings| {
9418 language_settings.defaults.completions = Some(CompletionSettings {
9419 words: WordsCompletionMode::Fallback,
9420 lsp: true,
9421 lsp_fetch_timeout_ms: 10,
9422 });
9423 });
9424
9425 let mut cx = EditorLspTestContext::new_rust(
9426 lsp::ServerCapabilities {
9427 completion_provider: Some(lsp::CompletionOptions {
9428 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9429 ..lsp::CompletionOptions::default()
9430 }),
9431 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9432 ..lsp::ServerCapabilities::default()
9433 },
9434 cx,
9435 )
9436 .await;
9437
9438 let throttle_completions = Arc::new(AtomicBool::new(false));
9439
9440 let lsp_throttle_completions = throttle_completions.clone();
9441 let _completion_requests_handler =
9442 cx.lsp
9443 .server
9444 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
9445 let lsp_throttle_completions = lsp_throttle_completions.clone();
9446 let cx = cx.clone();
9447 async move {
9448 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
9449 cx.background_executor()
9450 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
9451 .await;
9452 }
9453 Ok(Some(lsp::CompletionResponse::Array(vec![
9454 lsp::CompletionItem {
9455 label: "first".into(),
9456 ..lsp::CompletionItem::default()
9457 },
9458 lsp::CompletionItem {
9459 label: "last".into(),
9460 ..lsp::CompletionItem::default()
9461 },
9462 ])))
9463 }
9464 });
9465
9466 cx.set_state(indoc! {"
9467 oneˇ
9468 two
9469 three
9470 "});
9471 cx.simulate_keystroke(".");
9472 cx.executor().run_until_parked();
9473 cx.condition(|editor, _| editor.context_menu_visible())
9474 .await;
9475 cx.update_editor(|editor, window, cx| {
9476 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9477 {
9478 assert_eq!(
9479 completion_menu_entries(&menu),
9480 &["first", "last"],
9481 "When LSP server is fast to reply, no fallback word completions are used"
9482 );
9483 } else {
9484 panic!("expected completion menu to be open");
9485 }
9486 editor.cancel(&Cancel, window, cx);
9487 });
9488 cx.executor().run_until_parked();
9489 cx.condition(|editor, _| !editor.context_menu_visible())
9490 .await;
9491
9492 throttle_completions.store(true, atomic::Ordering::Release);
9493 cx.simulate_keystroke(".");
9494 cx.executor()
9495 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
9496 cx.executor().run_until_parked();
9497 cx.condition(|editor, _| editor.context_menu_visible())
9498 .await;
9499 cx.update_editor(|editor, _, _| {
9500 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9501 {
9502 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
9503 "When LSP server is slow, document words can be shown instead, if configured accordingly");
9504 } else {
9505 panic!("expected completion menu to be open");
9506 }
9507 });
9508}
9509
9510#[gpui::test]
9511async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
9512 init_test(cx, |language_settings| {
9513 language_settings.defaults.completions = Some(CompletionSettings {
9514 words: WordsCompletionMode::Enabled,
9515 lsp: true,
9516 lsp_fetch_timeout_ms: 0,
9517 });
9518 });
9519
9520 let mut cx = EditorLspTestContext::new_rust(
9521 lsp::ServerCapabilities {
9522 completion_provider: Some(lsp::CompletionOptions {
9523 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9524 ..lsp::CompletionOptions::default()
9525 }),
9526 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9527 ..lsp::ServerCapabilities::default()
9528 },
9529 cx,
9530 )
9531 .await;
9532
9533 let _completion_requests_handler =
9534 cx.lsp
9535 .server
9536 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
9537 Ok(Some(lsp::CompletionResponse::Array(vec![
9538 lsp::CompletionItem {
9539 label: "first".into(),
9540 ..lsp::CompletionItem::default()
9541 },
9542 lsp::CompletionItem {
9543 label: "last".into(),
9544 ..lsp::CompletionItem::default()
9545 },
9546 ])))
9547 });
9548
9549 cx.set_state(indoc! {"ˇ
9550 first
9551 last
9552 second
9553 "});
9554 cx.simulate_keystroke(".");
9555 cx.executor().run_until_parked();
9556 cx.condition(|editor, _| editor.context_menu_visible())
9557 .await;
9558 cx.update_editor(|editor, _, _| {
9559 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9560 {
9561 assert_eq!(
9562 completion_menu_entries(&menu),
9563 &["first", "last", "second"],
9564 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
9565 );
9566 } else {
9567 panic!("expected completion menu to be open");
9568 }
9569 });
9570}
9571
9572#[gpui::test]
9573async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
9574 init_test(cx, |language_settings| {
9575 language_settings.defaults.completions = Some(CompletionSettings {
9576 words: WordsCompletionMode::Disabled,
9577 lsp: true,
9578 lsp_fetch_timeout_ms: 0,
9579 });
9580 });
9581
9582 let mut cx = EditorLspTestContext::new_rust(
9583 lsp::ServerCapabilities {
9584 completion_provider: Some(lsp::CompletionOptions {
9585 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9586 ..lsp::CompletionOptions::default()
9587 }),
9588 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9589 ..lsp::ServerCapabilities::default()
9590 },
9591 cx,
9592 )
9593 .await;
9594
9595 let _completion_requests_handler =
9596 cx.lsp
9597 .server
9598 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
9599 panic!("LSP completions should not be queried when dealing with word completions")
9600 });
9601
9602 cx.set_state(indoc! {"ˇ
9603 first
9604 last
9605 second
9606 "});
9607 cx.update_editor(|editor, window, cx| {
9608 editor.show_word_completions(&ShowWordCompletions, window, cx);
9609 });
9610 cx.executor().run_until_parked();
9611 cx.condition(|editor, _| editor.context_menu_visible())
9612 .await;
9613 cx.update_editor(|editor, _, _| {
9614 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9615 {
9616 assert_eq!(
9617 completion_menu_entries(&menu),
9618 &["first", "last", "second"],
9619 "`ShowWordCompletions` action should show word completions"
9620 );
9621 } else {
9622 panic!("expected completion menu to be open");
9623 }
9624 });
9625
9626 cx.simulate_keystroke("l");
9627 cx.executor().run_until_parked();
9628 cx.condition(|editor, _| editor.context_menu_visible())
9629 .await;
9630 cx.update_editor(|editor, _, _| {
9631 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9632 {
9633 assert_eq!(
9634 completion_menu_entries(&menu),
9635 &["last"],
9636 "After showing word completions, further editing should filter them and not query the LSP"
9637 );
9638 } else {
9639 panic!("expected completion menu to be open");
9640 }
9641 });
9642}
9643
9644#[gpui::test]
9645async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
9646 init_test(cx, |language_settings| {
9647 language_settings.defaults.completions = Some(CompletionSettings {
9648 words: WordsCompletionMode::Fallback,
9649 lsp: false,
9650 lsp_fetch_timeout_ms: 0,
9651 });
9652 });
9653
9654 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
9655
9656 cx.set_state(indoc! {"ˇ
9657 0_usize
9658 let
9659 33
9660 4.5f32
9661 "});
9662 cx.update_editor(|editor, window, cx| {
9663 editor.show_completions(&ShowCompletions::default(), window, cx);
9664 });
9665 cx.executor().run_until_parked();
9666 cx.condition(|editor, _| editor.context_menu_visible())
9667 .await;
9668 cx.update_editor(|editor, window, cx| {
9669 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9670 {
9671 assert_eq!(
9672 completion_menu_entries(&menu),
9673 &["let"],
9674 "With no digits in the completion query, no digits should be in the word completions"
9675 );
9676 } else {
9677 panic!("expected completion menu to be open");
9678 }
9679 editor.cancel(&Cancel, window, cx);
9680 });
9681
9682 cx.set_state(indoc! {"3ˇ
9683 0_usize
9684 let
9685 3
9686 33.35f32
9687 "});
9688 cx.update_editor(|editor, window, cx| {
9689 editor.show_completions(&ShowCompletions::default(), window, cx);
9690 });
9691 cx.executor().run_until_parked();
9692 cx.condition(|editor, _| editor.context_menu_visible())
9693 .await;
9694 cx.update_editor(|editor, _, _| {
9695 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9696 {
9697 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
9698 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
9699 } else {
9700 panic!("expected completion menu to be open");
9701 }
9702 });
9703}
9704
9705fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
9706 let position = || lsp::Position {
9707 line: params.text_document_position.position.line,
9708 character: params.text_document_position.position.character,
9709 };
9710 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
9711 range: lsp::Range {
9712 start: position(),
9713 end: position(),
9714 },
9715 new_text: text.to_string(),
9716 }))
9717}
9718
9719#[gpui::test]
9720async fn test_multiline_completion(cx: &mut TestAppContext) {
9721 init_test(cx, |_| {});
9722
9723 let fs = FakeFs::new(cx.executor());
9724 fs.insert_tree(
9725 path!("/a"),
9726 json!({
9727 "main.ts": "a",
9728 }),
9729 )
9730 .await;
9731
9732 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
9733 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9734 let typescript_language = Arc::new(Language::new(
9735 LanguageConfig {
9736 name: "TypeScript".into(),
9737 matcher: LanguageMatcher {
9738 path_suffixes: vec!["ts".to_string()],
9739 ..LanguageMatcher::default()
9740 },
9741 line_comments: vec!["// ".into()],
9742 ..LanguageConfig::default()
9743 },
9744 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
9745 ));
9746 language_registry.add(typescript_language.clone());
9747 let mut fake_servers = language_registry.register_fake_lsp(
9748 "TypeScript",
9749 FakeLspAdapter {
9750 capabilities: lsp::ServerCapabilities {
9751 completion_provider: Some(lsp::CompletionOptions {
9752 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9753 ..lsp::CompletionOptions::default()
9754 }),
9755 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9756 ..lsp::ServerCapabilities::default()
9757 },
9758 // Emulate vtsls label generation
9759 label_for_completion: Some(Box::new(|item, _| {
9760 let text = if let Some(description) = item
9761 .label_details
9762 .as_ref()
9763 .and_then(|label_details| label_details.description.as_ref())
9764 {
9765 format!("{} {}", item.label, description)
9766 } else if let Some(detail) = &item.detail {
9767 format!("{} {}", item.label, detail)
9768 } else {
9769 item.label.clone()
9770 };
9771 let len = text.len();
9772 Some(language::CodeLabel {
9773 text,
9774 runs: Vec::new(),
9775 filter_range: 0..len,
9776 })
9777 })),
9778 ..FakeLspAdapter::default()
9779 },
9780 );
9781 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9782 let cx = &mut VisualTestContext::from_window(*workspace, cx);
9783 let worktree_id = workspace
9784 .update(cx, |workspace, _window, cx| {
9785 workspace.project().update(cx, |project, cx| {
9786 project.worktrees(cx).next().unwrap().read(cx).id()
9787 })
9788 })
9789 .unwrap();
9790 let _buffer = project
9791 .update(cx, |project, cx| {
9792 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
9793 })
9794 .await
9795 .unwrap();
9796 let editor = workspace
9797 .update(cx, |workspace, window, cx| {
9798 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
9799 })
9800 .unwrap()
9801 .await
9802 .unwrap()
9803 .downcast::<Editor>()
9804 .unwrap();
9805 let fake_server = fake_servers.next().await.unwrap();
9806
9807 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
9808 let multiline_label_2 = "a\nb\nc\n";
9809 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
9810 let multiline_description = "d\ne\nf\n";
9811 let multiline_detail_2 = "g\nh\ni\n";
9812
9813 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
9814 move |params, _| async move {
9815 Ok(Some(lsp::CompletionResponse::Array(vec![
9816 lsp::CompletionItem {
9817 label: multiline_label.to_string(),
9818 text_edit: gen_text_edit(¶ms, "new_text_1"),
9819 ..lsp::CompletionItem::default()
9820 },
9821 lsp::CompletionItem {
9822 label: "single line label 1".to_string(),
9823 detail: Some(multiline_detail.to_string()),
9824 text_edit: gen_text_edit(¶ms, "new_text_2"),
9825 ..lsp::CompletionItem::default()
9826 },
9827 lsp::CompletionItem {
9828 label: "single line label 2".to_string(),
9829 label_details: Some(lsp::CompletionItemLabelDetails {
9830 description: Some(multiline_description.to_string()),
9831 detail: None,
9832 }),
9833 text_edit: gen_text_edit(¶ms, "new_text_2"),
9834 ..lsp::CompletionItem::default()
9835 },
9836 lsp::CompletionItem {
9837 label: multiline_label_2.to_string(),
9838 detail: Some(multiline_detail_2.to_string()),
9839 text_edit: gen_text_edit(¶ms, "new_text_3"),
9840 ..lsp::CompletionItem::default()
9841 },
9842 lsp::CompletionItem {
9843 label: "Label with many spaces and \t but without newlines".to_string(),
9844 detail: Some(
9845 "Details with many spaces and \t but without newlines".to_string(),
9846 ),
9847 text_edit: gen_text_edit(¶ms, "new_text_4"),
9848 ..lsp::CompletionItem::default()
9849 },
9850 ])))
9851 },
9852 );
9853
9854 editor.update_in(cx, |editor, window, cx| {
9855 cx.focus_self(window);
9856 editor.move_to_end(&MoveToEnd, window, cx);
9857 editor.handle_input(".", window, cx);
9858 });
9859 cx.run_until_parked();
9860 completion_handle.next().await.unwrap();
9861
9862 editor.update(cx, |editor, _| {
9863 assert!(editor.context_menu_visible());
9864 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9865 {
9866 let completion_labels = menu
9867 .completions
9868 .borrow()
9869 .iter()
9870 .map(|c| c.label.text.clone())
9871 .collect::<Vec<_>>();
9872 assert_eq!(
9873 completion_labels,
9874 &[
9875 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
9876 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
9877 "single line label 2 d e f ",
9878 "a b c g h i ",
9879 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
9880 ],
9881 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
9882 );
9883
9884 for completion in menu
9885 .completions
9886 .borrow()
9887 .iter() {
9888 assert_eq!(
9889 completion.label.filter_range,
9890 0..completion.label.text.len(),
9891 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
9892 );
9893 }
9894 } else {
9895 panic!("expected completion menu to be open");
9896 }
9897 });
9898}
9899
9900#[gpui::test]
9901async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
9902 init_test(cx, |_| {});
9903 let mut cx = EditorLspTestContext::new_rust(
9904 lsp::ServerCapabilities {
9905 completion_provider: Some(lsp::CompletionOptions {
9906 trigger_characters: Some(vec![".".to_string()]),
9907 ..Default::default()
9908 }),
9909 ..Default::default()
9910 },
9911 cx,
9912 )
9913 .await;
9914 cx.lsp
9915 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
9916 Ok(Some(lsp::CompletionResponse::Array(vec![
9917 lsp::CompletionItem {
9918 label: "first".into(),
9919 ..Default::default()
9920 },
9921 lsp::CompletionItem {
9922 label: "last".into(),
9923 ..Default::default()
9924 },
9925 ])))
9926 });
9927 cx.set_state("variableˇ");
9928 cx.simulate_keystroke(".");
9929 cx.executor().run_until_parked();
9930
9931 cx.update_editor(|editor, _, _| {
9932 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9933 {
9934 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
9935 } else {
9936 panic!("expected completion menu to be open");
9937 }
9938 });
9939
9940 cx.update_editor(|editor, window, cx| {
9941 editor.move_page_down(&MovePageDown::default(), window, cx);
9942 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9943 {
9944 assert!(
9945 menu.selected_item == 1,
9946 "expected PageDown to select the last item from the context menu"
9947 );
9948 } else {
9949 panic!("expected completion menu to stay open after PageDown");
9950 }
9951 });
9952
9953 cx.update_editor(|editor, window, cx| {
9954 editor.move_page_up(&MovePageUp::default(), window, cx);
9955 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9956 {
9957 assert!(
9958 menu.selected_item == 0,
9959 "expected PageUp to select the first item from the context menu"
9960 );
9961 } else {
9962 panic!("expected completion menu to stay open after PageUp");
9963 }
9964 });
9965}
9966
9967#[gpui::test]
9968async fn test_completion_sort(cx: &mut TestAppContext) {
9969 init_test(cx, |_| {});
9970 let mut cx = EditorLspTestContext::new_rust(
9971 lsp::ServerCapabilities {
9972 completion_provider: Some(lsp::CompletionOptions {
9973 trigger_characters: Some(vec![".".to_string()]),
9974 ..Default::default()
9975 }),
9976 ..Default::default()
9977 },
9978 cx,
9979 )
9980 .await;
9981 cx.lsp
9982 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
9983 Ok(Some(lsp::CompletionResponse::Array(vec![
9984 lsp::CompletionItem {
9985 label: "Range".into(),
9986 sort_text: Some("a".into()),
9987 ..Default::default()
9988 },
9989 lsp::CompletionItem {
9990 label: "r".into(),
9991 sort_text: Some("b".into()),
9992 ..Default::default()
9993 },
9994 lsp::CompletionItem {
9995 label: "ret".into(),
9996 sort_text: Some("c".into()),
9997 ..Default::default()
9998 },
9999 lsp::CompletionItem {
10000 label: "return".into(),
10001 sort_text: Some("d".into()),
10002 ..Default::default()
10003 },
10004 lsp::CompletionItem {
10005 label: "slice".into(),
10006 sort_text: Some("d".into()),
10007 ..Default::default()
10008 },
10009 ])))
10010 });
10011 cx.set_state("rˇ");
10012 cx.executor().run_until_parked();
10013 cx.update_editor(|editor, window, cx| {
10014 editor.show_completions(
10015 &ShowCompletions {
10016 trigger: Some("r".into()),
10017 },
10018 window,
10019 cx,
10020 );
10021 });
10022 cx.executor().run_until_parked();
10023
10024 cx.update_editor(|editor, _, _| {
10025 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10026 {
10027 assert_eq!(
10028 completion_menu_entries(&menu),
10029 &["r", "ret", "Range", "return"]
10030 );
10031 } else {
10032 panic!("expected completion menu to be open");
10033 }
10034 });
10035}
10036
10037#[gpui::test]
10038async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
10039 init_test(cx, |_| {});
10040
10041 let mut cx = EditorLspTestContext::new_rust(
10042 lsp::ServerCapabilities {
10043 completion_provider: Some(lsp::CompletionOptions {
10044 trigger_characters: Some(vec![".".to_string()]),
10045 resolve_provider: Some(true),
10046 ..Default::default()
10047 }),
10048 ..Default::default()
10049 },
10050 cx,
10051 )
10052 .await;
10053
10054 cx.set_state("fn main() { let a = 2ˇ; }");
10055 cx.simulate_keystroke(".");
10056 let completion_item = lsp::CompletionItem {
10057 label: "Some".into(),
10058 kind: Some(lsp::CompletionItemKind::SNIPPET),
10059 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
10060 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
10061 kind: lsp::MarkupKind::Markdown,
10062 value: "```rust\nSome(2)\n```".to_string(),
10063 })),
10064 deprecated: Some(false),
10065 sort_text: Some("Some".to_string()),
10066 filter_text: Some("Some".to_string()),
10067 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
10068 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
10069 range: lsp::Range {
10070 start: lsp::Position {
10071 line: 0,
10072 character: 22,
10073 },
10074 end: lsp::Position {
10075 line: 0,
10076 character: 22,
10077 },
10078 },
10079 new_text: "Some(2)".to_string(),
10080 })),
10081 additional_text_edits: Some(vec![lsp::TextEdit {
10082 range: lsp::Range {
10083 start: lsp::Position {
10084 line: 0,
10085 character: 20,
10086 },
10087 end: lsp::Position {
10088 line: 0,
10089 character: 22,
10090 },
10091 },
10092 new_text: "".to_string(),
10093 }]),
10094 ..Default::default()
10095 };
10096
10097 let closure_completion_item = completion_item.clone();
10098 let counter = Arc::new(AtomicUsize::new(0));
10099 let counter_clone = counter.clone();
10100 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
10101 let task_completion_item = closure_completion_item.clone();
10102 counter_clone.fetch_add(1, atomic::Ordering::Release);
10103 async move {
10104 Ok(Some(lsp::CompletionResponse::Array(vec![
10105 task_completion_item,
10106 ])))
10107 }
10108 });
10109
10110 cx.condition(|editor, _| editor.context_menu_visible())
10111 .await;
10112 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
10113 assert!(request.next().await.is_some());
10114 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10115
10116 cx.simulate_keystrokes("S o m");
10117 cx.condition(|editor, _| editor.context_menu_visible())
10118 .await;
10119 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
10120 assert!(request.next().await.is_some());
10121 assert!(request.next().await.is_some());
10122 assert!(request.next().await.is_some());
10123 request.close();
10124 assert!(request.next().await.is_none());
10125 assert_eq!(
10126 counter.load(atomic::Ordering::Acquire),
10127 4,
10128 "With the completions menu open, only one LSP request should happen per input"
10129 );
10130}
10131
10132#[gpui::test]
10133async fn test_toggle_comment(cx: &mut TestAppContext) {
10134 init_test(cx, |_| {});
10135 let mut cx = EditorTestContext::new(cx).await;
10136 let language = Arc::new(Language::new(
10137 LanguageConfig {
10138 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
10139 ..Default::default()
10140 },
10141 Some(tree_sitter_rust::LANGUAGE.into()),
10142 ));
10143 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
10144
10145 // If multiple selections intersect a line, the line is only toggled once.
10146 cx.set_state(indoc! {"
10147 fn a() {
10148 «//b();
10149 ˇ»// «c();
10150 //ˇ» d();
10151 }
10152 "});
10153
10154 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10155
10156 cx.assert_editor_state(indoc! {"
10157 fn a() {
10158 «b();
10159 c();
10160 ˇ» d();
10161 }
10162 "});
10163
10164 // The comment prefix is inserted at the same column for every line in a
10165 // selection.
10166 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10167
10168 cx.assert_editor_state(indoc! {"
10169 fn a() {
10170 // «b();
10171 // c();
10172 ˇ»// d();
10173 }
10174 "});
10175
10176 // If a selection ends at the beginning of a line, that line is not toggled.
10177 cx.set_selections_state(indoc! {"
10178 fn a() {
10179 // b();
10180 «// c();
10181 ˇ» // d();
10182 }
10183 "});
10184
10185 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10186
10187 cx.assert_editor_state(indoc! {"
10188 fn a() {
10189 // b();
10190 «c();
10191 ˇ» // d();
10192 }
10193 "});
10194
10195 // If a selection span a single line and is empty, the line is toggled.
10196 cx.set_state(indoc! {"
10197 fn a() {
10198 a();
10199 b();
10200 ˇ
10201 }
10202 "});
10203
10204 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10205
10206 cx.assert_editor_state(indoc! {"
10207 fn a() {
10208 a();
10209 b();
10210 //•ˇ
10211 }
10212 "});
10213
10214 // If a selection span multiple lines, empty lines are not toggled.
10215 cx.set_state(indoc! {"
10216 fn a() {
10217 «a();
10218
10219 c();ˇ»
10220 }
10221 "});
10222
10223 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10224
10225 cx.assert_editor_state(indoc! {"
10226 fn a() {
10227 // «a();
10228
10229 // c();ˇ»
10230 }
10231 "});
10232
10233 // If a selection includes multiple comment prefixes, all lines are uncommented.
10234 cx.set_state(indoc! {"
10235 fn a() {
10236 «// a();
10237 /// b();
10238 //! c();ˇ»
10239 }
10240 "});
10241
10242 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10243
10244 cx.assert_editor_state(indoc! {"
10245 fn a() {
10246 «a();
10247 b();
10248 c();ˇ»
10249 }
10250 "});
10251}
10252
10253#[gpui::test]
10254async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
10255 init_test(cx, |_| {});
10256 let mut cx = EditorTestContext::new(cx).await;
10257 let language = Arc::new(Language::new(
10258 LanguageConfig {
10259 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
10260 ..Default::default()
10261 },
10262 Some(tree_sitter_rust::LANGUAGE.into()),
10263 ));
10264 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
10265
10266 let toggle_comments = &ToggleComments {
10267 advance_downwards: false,
10268 ignore_indent: true,
10269 };
10270
10271 // If multiple selections intersect a line, the line is only toggled once.
10272 cx.set_state(indoc! {"
10273 fn a() {
10274 // «b();
10275 // c();
10276 // ˇ» d();
10277 }
10278 "});
10279
10280 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10281
10282 cx.assert_editor_state(indoc! {"
10283 fn a() {
10284 «b();
10285 c();
10286 ˇ» d();
10287 }
10288 "});
10289
10290 // The comment prefix is inserted at the beginning of each line
10291 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10292
10293 cx.assert_editor_state(indoc! {"
10294 fn a() {
10295 // «b();
10296 // c();
10297 // ˇ» d();
10298 }
10299 "});
10300
10301 // If a selection ends at the beginning of a line, that line is not toggled.
10302 cx.set_selections_state(indoc! {"
10303 fn a() {
10304 // b();
10305 // «c();
10306 ˇ»// d();
10307 }
10308 "});
10309
10310 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10311
10312 cx.assert_editor_state(indoc! {"
10313 fn a() {
10314 // b();
10315 «c();
10316 ˇ»// d();
10317 }
10318 "});
10319
10320 // If a selection span a single line and is empty, the line is toggled.
10321 cx.set_state(indoc! {"
10322 fn a() {
10323 a();
10324 b();
10325 ˇ
10326 }
10327 "});
10328
10329 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10330
10331 cx.assert_editor_state(indoc! {"
10332 fn a() {
10333 a();
10334 b();
10335 //ˇ
10336 }
10337 "});
10338
10339 // If a selection span multiple lines, empty lines are not toggled.
10340 cx.set_state(indoc! {"
10341 fn a() {
10342 «a();
10343
10344 c();ˇ»
10345 }
10346 "});
10347
10348 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10349
10350 cx.assert_editor_state(indoc! {"
10351 fn a() {
10352 // «a();
10353
10354 // c();ˇ»
10355 }
10356 "});
10357
10358 // If a selection includes multiple comment prefixes, all lines are uncommented.
10359 cx.set_state(indoc! {"
10360 fn a() {
10361 // «a();
10362 /// b();
10363 //! c();ˇ»
10364 }
10365 "});
10366
10367 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10368
10369 cx.assert_editor_state(indoc! {"
10370 fn a() {
10371 «a();
10372 b();
10373 c();ˇ»
10374 }
10375 "});
10376}
10377
10378#[gpui::test]
10379async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
10380 init_test(cx, |_| {});
10381
10382 let language = Arc::new(Language::new(
10383 LanguageConfig {
10384 line_comments: vec!["// ".into()],
10385 ..Default::default()
10386 },
10387 Some(tree_sitter_rust::LANGUAGE.into()),
10388 ));
10389
10390 let mut cx = EditorTestContext::new(cx).await;
10391
10392 cx.language_registry().add(language.clone());
10393 cx.update_buffer(|buffer, cx| {
10394 buffer.set_language(Some(language), cx);
10395 });
10396
10397 let toggle_comments = &ToggleComments {
10398 advance_downwards: true,
10399 ignore_indent: false,
10400 };
10401
10402 // Single cursor on one line -> advance
10403 // Cursor moves horizontally 3 characters as well on non-blank line
10404 cx.set_state(indoc!(
10405 "fn a() {
10406 ˇdog();
10407 cat();
10408 }"
10409 ));
10410 cx.update_editor(|editor, window, cx| {
10411 editor.toggle_comments(toggle_comments, window, cx);
10412 });
10413 cx.assert_editor_state(indoc!(
10414 "fn a() {
10415 // dog();
10416 catˇ();
10417 }"
10418 ));
10419
10420 // Single selection on one line -> don't advance
10421 cx.set_state(indoc!(
10422 "fn a() {
10423 «dog()ˇ»;
10424 cat();
10425 }"
10426 ));
10427 cx.update_editor(|editor, window, cx| {
10428 editor.toggle_comments(toggle_comments, window, cx);
10429 });
10430 cx.assert_editor_state(indoc!(
10431 "fn a() {
10432 // «dog()ˇ»;
10433 cat();
10434 }"
10435 ));
10436
10437 // Multiple cursors on one line -> advance
10438 cx.set_state(indoc!(
10439 "fn a() {
10440 ˇdˇog();
10441 cat();
10442 }"
10443 ));
10444 cx.update_editor(|editor, window, cx| {
10445 editor.toggle_comments(toggle_comments, window, cx);
10446 });
10447 cx.assert_editor_state(indoc!(
10448 "fn a() {
10449 // dog();
10450 catˇ(ˇ);
10451 }"
10452 ));
10453
10454 // Multiple cursors on one line, with selection -> don't advance
10455 cx.set_state(indoc!(
10456 "fn a() {
10457 ˇdˇog«()ˇ»;
10458 cat();
10459 }"
10460 ));
10461 cx.update_editor(|editor, window, cx| {
10462 editor.toggle_comments(toggle_comments, window, cx);
10463 });
10464 cx.assert_editor_state(indoc!(
10465 "fn a() {
10466 // ˇdˇog«()ˇ»;
10467 cat();
10468 }"
10469 ));
10470
10471 // Single cursor on one line -> advance
10472 // Cursor moves to column 0 on blank line
10473 cx.set_state(indoc!(
10474 "fn a() {
10475 ˇdog();
10476
10477 cat();
10478 }"
10479 ));
10480 cx.update_editor(|editor, window, cx| {
10481 editor.toggle_comments(toggle_comments, window, cx);
10482 });
10483 cx.assert_editor_state(indoc!(
10484 "fn a() {
10485 // dog();
10486 ˇ
10487 cat();
10488 }"
10489 ));
10490
10491 // Single cursor on one line -> advance
10492 // Cursor starts and ends at column 0
10493 cx.set_state(indoc!(
10494 "fn a() {
10495 ˇ dog();
10496 cat();
10497 }"
10498 ));
10499 cx.update_editor(|editor, window, cx| {
10500 editor.toggle_comments(toggle_comments, window, cx);
10501 });
10502 cx.assert_editor_state(indoc!(
10503 "fn a() {
10504 // dog();
10505 ˇ cat();
10506 }"
10507 ));
10508}
10509
10510#[gpui::test]
10511async fn test_toggle_block_comment(cx: &mut TestAppContext) {
10512 init_test(cx, |_| {});
10513
10514 let mut cx = EditorTestContext::new(cx).await;
10515
10516 let html_language = Arc::new(
10517 Language::new(
10518 LanguageConfig {
10519 name: "HTML".into(),
10520 block_comment: Some(("<!-- ".into(), " -->".into())),
10521 ..Default::default()
10522 },
10523 Some(tree_sitter_html::LANGUAGE.into()),
10524 )
10525 .with_injection_query(
10526 r#"
10527 (script_element
10528 (raw_text) @injection.content
10529 (#set! injection.language "javascript"))
10530 "#,
10531 )
10532 .unwrap(),
10533 );
10534
10535 let javascript_language = Arc::new(Language::new(
10536 LanguageConfig {
10537 name: "JavaScript".into(),
10538 line_comments: vec!["// ".into()],
10539 ..Default::default()
10540 },
10541 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10542 ));
10543
10544 cx.language_registry().add(html_language.clone());
10545 cx.language_registry().add(javascript_language.clone());
10546 cx.update_buffer(|buffer, cx| {
10547 buffer.set_language(Some(html_language), cx);
10548 });
10549
10550 // Toggle comments for empty selections
10551 cx.set_state(
10552 &r#"
10553 <p>A</p>ˇ
10554 <p>B</p>ˇ
10555 <p>C</p>ˇ
10556 "#
10557 .unindent(),
10558 );
10559 cx.update_editor(|editor, window, cx| {
10560 editor.toggle_comments(&ToggleComments::default(), window, cx)
10561 });
10562 cx.assert_editor_state(
10563 &r#"
10564 <!-- <p>A</p>ˇ -->
10565 <!-- <p>B</p>ˇ -->
10566 <!-- <p>C</p>ˇ -->
10567 "#
10568 .unindent(),
10569 );
10570 cx.update_editor(|editor, window, cx| {
10571 editor.toggle_comments(&ToggleComments::default(), window, cx)
10572 });
10573 cx.assert_editor_state(
10574 &r#"
10575 <p>A</p>ˇ
10576 <p>B</p>ˇ
10577 <p>C</p>ˇ
10578 "#
10579 .unindent(),
10580 );
10581
10582 // Toggle comments for mixture of empty and non-empty selections, where
10583 // multiple selections occupy a given line.
10584 cx.set_state(
10585 &r#"
10586 <p>A«</p>
10587 <p>ˇ»B</p>ˇ
10588 <p>C«</p>
10589 <p>ˇ»D</p>ˇ
10590 "#
10591 .unindent(),
10592 );
10593
10594 cx.update_editor(|editor, window, cx| {
10595 editor.toggle_comments(&ToggleComments::default(), window, cx)
10596 });
10597 cx.assert_editor_state(
10598 &r#"
10599 <!-- <p>A«</p>
10600 <p>ˇ»B</p>ˇ -->
10601 <!-- <p>C«</p>
10602 <p>ˇ»D</p>ˇ -->
10603 "#
10604 .unindent(),
10605 );
10606 cx.update_editor(|editor, window, cx| {
10607 editor.toggle_comments(&ToggleComments::default(), window, cx)
10608 });
10609 cx.assert_editor_state(
10610 &r#"
10611 <p>A«</p>
10612 <p>ˇ»B</p>ˇ
10613 <p>C«</p>
10614 <p>ˇ»D</p>ˇ
10615 "#
10616 .unindent(),
10617 );
10618
10619 // Toggle comments when different languages are active for different
10620 // selections.
10621 cx.set_state(
10622 &r#"
10623 ˇ<script>
10624 ˇvar x = new Y();
10625 ˇ</script>
10626 "#
10627 .unindent(),
10628 );
10629 cx.executor().run_until_parked();
10630 cx.update_editor(|editor, window, cx| {
10631 editor.toggle_comments(&ToggleComments::default(), window, cx)
10632 });
10633 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
10634 // Uncommenting and commenting from this position brings in even more wrong artifacts.
10635 cx.assert_editor_state(
10636 &r#"
10637 <!-- ˇ<script> -->
10638 // ˇvar x = new Y();
10639 <!-- ˇ</script> -->
10640 "#
10641 .unindent(),
10642 );
10643}
10644
10645#[gpui::test]
10646fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
10647 init_test(cx, |_| {});
10648
10649 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
10650 let multibuffer = cx.new(|cx| {
10651 let mut multibuffer = MultiBuffer::new(ReadWrite);
10652 multibuffer.push_excerpts(
10653 buffer.clone(),
10654 [
10655 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
10656 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
10657 ],
10658 cx,
10659 );
10660 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
10661 multibuffer
10662 });
10663
10664 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
10665 editor.update_in(cx, |editor, window, cx| {
10666 assert_eq!(editor.text(cx), "aaaa\nbbbb");
10667 editor.change_selections(None, window, cx, |s| {
10668 s.select_ranges([
10669 Point::new(0, 0)..Point::new(0, 0),
10670 Point::new(1, 0)..Point::new(1, 0),
10671 ])
10672 });
10673
10674 editor.handle_input("X", window, cx);
10675 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
10676 assert_eq!(
10677 editor.selections.ranges(cx),
10678 [
10679 Point::new(0, 1)..Point::new(0, 1),
10680 Point::new(1, 1)..Point::new(1, 1),
10681 ]
10682 );
10683
10684 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
10685 editor.change_selections(None, window, cx, |s| {
10686 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
10687 });
10688 editor.backspace(&Default::default(), window, cx);
10689 assert_eq!(editor.text(cx), "Xa\nbbb");
10690 assert_eq!(
10691 editor.selections.ranges(cx),
10692 [Point::new(1, 0)..Point::new(1, 0)]
10693 );
10694
10695 editor.change_selections(None, window, cx, |s| {
10696 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
10697 });
10698 editor.backspace(&Default::default(), window, cx);
10699 assert_eq!(editor.text(cx), "X\nbb");
10700 assert_eq!(
10701 editor.selections.ranges(cx),
10702 [Point::new(0, 1)..Point::new(0, 1)]
10703 );
10704 });
10705}
10706
10707#[gpui::test]
10708fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
10709 init_test(cx, |_| {});
10710
10711 let markers = vec![('[', ']').into(), ('(', ')').into()];
10712 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
10713 indoc! {"
10714 [aaaa
10715 (bbbb]
10716 cccc)",
10717 },
10718 markers.clone(),
10719 );
10720 let excerpt_ranges = markers.into_iter().map(|marker| {
10721 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
10722 ExcerptRange::new(context.clone())
10723 });
10724 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
10725 let multibuffer = cx.new(|cx| {
10726 let mut multibuffer = MultiBuffer::new(ReadWrite);
10727 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
10728 multibuffer
10729 });
10730
10731 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
10732 editor.update_in(cx, |editor, window, cx| {
10733 let (expected_text, selection_ranges) = marked_text_ranges(
10734 indoc! {"
10735 aaaa
10736 bˇbbb
10737 bˇbbˇb
10738 cccc"
10739 },
10740 true,
10741 );
10742 assert_eq!(editor.text(cx), expected_text);
10743 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
10744
10745 editor.handle_input("X", window, cx);
10746
10747 let (expected_text, expected_selections) = marked_text_ranges(
10748 indoc! {"
10749 aaaa
10750 bXˇbbXb
10751 bXˇbbXˇb
10752 cccc"
10753 },
10754 false,
10755 );
10756 assert_eq!(editor.text(cx), expected_text);
10757 assert_eq!(editor.selections.ranges(cx), expected_selections);
10758
10759 editor.newline(&Newline, window, cx);
10760 let (expected_text, expected_selections) = marked_text_ranges(
10761 indoc! {"
10762 aaaa
10763 bX
10764 ˇbbX
10765 b
10766 bX
10767 ˇbbX
10768 ˇb
10769 cccc"
10770 },
10771 false,
10772 );
10773 assert_eq!(editor.text(cx), expected_text);
10774 assert_eq!(editor.selections.ranges(cx), expected_selections);
10775 });
10776}
10777
10778#[gpui::test]
10779fn test_refresh_selections(cx: &mut TestAppContext) {
10780 init_test(cx, |_| {});
10781
10782 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
10783 let mut excerpt1_id = None;
10784 let multibuffer = cx.new(|cx| {
10785 let mut multibuffer = MultiBuffer::new(ReadWrite);
10786 excerpt1_id = multibuffer
10787 .push_excerpts(
10788 buffer.clone(),
10789 [
10790 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
10791 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
10792 ],
10793 cx,
10794 )
10795 .into_iter()
10796 .next();
10797 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
10798 multibuffer
10799 });
10800
10801 let editor = cx.add_window(|window, cx| {
10802 let mut editor = build_editor(multibuffer.clone(), window, cx);
10803 let snapshot = editor.snapshot(window, cx);
10804 editor.change_selections(None, window, cx, |s| {
10805 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
10806 });
10807 editor.begin_selection(
10808 Point::new(2, 1).to_display_point(&snapshot),
10809 true,
10810 1,
10811 window,
10812 cx,
10813 );
10814 assert_eq!(
10815 editor.selections.ranges(cx),
10816 [
10817 Point::new(1, 3)..Point::new(1, 3),
10818 Point::new(2, 1)..Point::new(2, 1),
10819 ]
10820 );
10821 editor
10822 });
10823
10824 // Refreshing selections is a no-op when excerpts haven't changed.
10825 _ = editor.update(cx, |editor, window, cx| {
10826 editor.change_selections(None, window, cx, |s| s.refresh());
10827 assert_eq!(
10828 editor.selections.ranges(cx),
10829 [
10830 Point::new(1, 3)..Point::new(1, 3),
10831 Point::new(2, 1)..Point::new(2, 1),
10832 ]
10833 );
10834 });
10835
10836 multibuffer.update(cx, |multibuffer, cx| {
10837 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
10838 });
10839 _ = editor.update(cx, |editor, window, cx| {
10840 // Removing an excerpt causes the first selection to become degenerate.
10841 assert_eq!(
10842 editor.selections.ranges(cx),
10843 [
10844 Point::new(0, 0)..Point::new(0, 0),
10845 Point::new(0, 1)..Point::new(0, 1)
10846 ]
10847 );
10848
10849 // Refreshing selections will relocate the first selection to the original buffer
10850 // location.
10851 editor.change_selections(None, window, cx, |s| s.refresh());
10852 assert_eq!(
10853 editor.selections.ranges(cx),
10854 [
10855 Point::new(0, 1)..Point::new(0, 1),
10856 Point::new(0, 3)..Point::new(0, 3)
10857 ]
10858 );
10859 assert!(editor.selections.pending_anchor().is_some());
10860 });
10861}
10862
10863#[gpui::test]
10864fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
10865 init_test(cx, |_| {});
10866
10867 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
10868 let mut excerpt1_id = None;
10869 let multibuffer = cx.new(|cx| {
10870 let mut multibuffer = MultiBuffer::new(ReadWrite);
10871 excerpt1_id = multibuffer
10872 .push_excerpts(
10873 buffer.clone(),
10874 [
10875 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
10876 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
10877 ],
10878 cx,
10879 )
10880 .into_iter()
10881 .next();
10882 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
10883 multibuffer
10884 });
10885
10886 let editor = cx.add_window(|window, cx| {
10887 let mut editor = build_editor(multibuffer.clone(), window, cx);
10888 let snapshot = editor.snapshot(window, cx);
10889 editor.begin_selection(
10890 Point::new(1, 3).to_display_point(&snapshot),
10891 false,
10892 1,
10893 window,
10894 cx,
10895 );
10896 assert_eq!(
10897 editor.selections.ranges(cx),
10898 [Point::new(1, 3)..Point::new(1, 3)]
10899 );
10900 editor
10901 });
10902
10903 multibuffer.update(cx, |multibuffer, cx| {
10904 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
10905 });
10906 _ = editor.update(cx, |editor, window, cx| {
10907 assert_eq!(
10908 editor.selections.ranges(cx),
10909 [Point::new(0, 0)..Point::new(0, 0)]
10910 );
10911
10912 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
10913 editor.change_selections(None, window, cx, |s| s.refresh());
10914 assert_eq!(
10915 editor.selections.ranges(cx),
10916 [Point::new(0, 3)..Point::new(0, 3)]
10917 );
10918 assert!(editor.selections.pending_anchor().is_some());
10919 });
10920}
10921
10922#[gpui::test]
10923async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
10924 init_test(cx, |_| {});
10925
10926 let language = Arc::new(
10927 Language::new(
10928 LanguageConfig {
10929 brackets: BracketPairConfig {
10930 pairs: vec![
10931 BracketPair {
10932 start: "{".to_string(),
10933 end: "}".to_string(),
10934 close: true,
10935 surround: true,
10936 newline: true,
10937 },
10938 BracketPair {
10939 start: "/* ".to_string(),
10940 end: " */".to_string(),
10941 close: true,
10942 surround: true,
10943 newline: true,
10944 },
10945 ],
10946 ..Default::default()
10947 },
10948 ..Default::default()
10949 },
10950 Some(tree_sitter_rust::LANGUAGE.into()),
10951 )
10952 .with_indents_query("")
10953 .unwrap(),
10954 );
10955
10956 let text = concat!(
10957 "{ }\n", //
10958 " x\n", //
10959 " /* */\n", //
10960 "x\n", //
10961 "{{} }\n", //
10962 );
10963
10964 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10965 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10966 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10967 editor
10968 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10969 .await;
10970
10971 editor.update_in(cx, |editor, window, cx| {
10972 editor.change_selections(None, window, cx, |s| {
10973 s.select_display_ranges([
10974 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
10975 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
10976 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
10977 ])
10978 });
10979 editor.newline(&Newline, window, cx);
10980
10981 assert_eq!(
10982 editor.buffer().read(cx).read(cx).text(),
10983 concat!(
10984 "{ \n", // Suppress rustfmt
10985 "\n", //
10986 "}\n", //
10987 " x\n", //
10988 " /* \n", //
10989 " \n", //
10990 " */\n", //
10991 "x\n", //
10992 "{{} \n", //
10993 "}\n", //
10994 )
10995 );
10996 });
10997}
10998
10999#[gpui::test]
11000fn test_highlighted_ranges(cx: &mut TestAppContext) {
11001 init_test(cx, |_| {});
11002
11003 let editor = cx.add_window(|window, cx| {
11004 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
11005 build_editor(buffer.clone(), window, cx)
11006 });
11007
11008 _ = editor.update(cx, |editor, window, cx| {
11009 struct Type1;
11010 struct Type2;
11011
11012 let buffer = editor.buffer.read(cx).snapshot(cx);
11013
11014 let anchor_range =
11015 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
11016
11017 editor.highlight_background::<Type1>(
11018 &[
11019 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
11020 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
11021 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
11022 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
11023 ],
11024 |_| Hsla::red(),
11025 cx,
11026 );
11027 editor.highlight_background::<Type2>(
11028 &[
11029 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
11030 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
11031 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
11032 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
11033 ],
11034 |_| Hsla::green(),
11035 cx,
11036 );
11037
11038 let snapshot = editor.snapshot(window, cx);
11039 let mut highlighted_ranges = editor.background_highlights_in_range(
11040 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
11041 &snapshot,
11042 cx.theme().colors(),
11043 );
11044 // Enforce a consistent ordering based on color without relying on the ordering of the
11045 // highlight's `TypeId` which is non-executor.
11046 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
11047 assert_eq!(
11048 highlighted_ranges,
11049 &[
11050 (
11051 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
11052 Hsla::red(),
11053 ),
11054 (
11055 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
11056 Hsla::red(),
11057 ),
11058 (
11059 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
11060 Hsla::green(),
11061 ),
11062 (
11063 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
11064 Hsla::green(),
11065 ),
11066 ]
11067 );
11068 assert_eq!(
11069 editor.background_highlights_in_range(
11070 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
11071 &snapshot,
11072 cx.theme().colors(),
11073 ),
11074 &[(
11075 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
11076 Hsla::red(),
11077 )]
11078 );
11079 });
11080}
11081
11082#[gpui::test]
11083async fn test_following(cx: &mut TestAppContext) {
11084 init_test(cx, |_| {});
11085
11086 let fs = FakeFs::new(cx.executor());
11087 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
11088
11089 let buffer = project.update(cx, |project, cx| {
11090 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
11091 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
11092 });
11093 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
11094 let follower = cx.update(|cx| {
11095 cx.open_window(
11096 WindowOptions {
11097 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
11098 gpui::Point::new(px(0.), px(0.)),
11099 gpui::Point::new(px(10.), px(80.)),
11100 ))),
11101 ..Default::default()
11102 },
11103 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
11104 )
11105 .unwrap()
11106 });
11107
11108 let is_still_following = Rc::new(RefCell::new(true));
11109 let follower_edit_event_count = Rc::new(RefCell::new(0));
11110 let pending_update = Rc::new(RefCell::new(None));
11111 let leader_entity = leader.root(cx).unwrap();
11112 let follower_entity = follower.root(cx).unwrap();
11113 _ = follower.update(cx, {
11114 let update = pending_update.clone();
11115 let is_still_following = is_still_following.clone();
11116 let follower_edit_event_count = follower_edit_event_count.clone();
11117 |_, window, cx| {
11118 cx.subscribe_in(
11119 &leader_entity,
11120 window,
11121 move |_, leader, event, window, cx| {
11122 leader.read(cx).add_event_to_update_proto(
11123 event,
11124 &mut update.borrow_mut(),
11125 window,
11126 cx,
11127 );
11128 },
11129 )
11130 .detach();
11131
11132 cx.subscribe_in(
11133 &follower_entity,
11134 window,
11135 move |_, _, event: &EditorEvent, _window, _cx| {
11136 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
11137 *is_still_following.borrow_mut() = false;
11138 }
11139
11140 if let EditorEvent::BufferEdited = event {
11141 *follower_edit_event_count.borrow_mut() += 1;
11142 }
11143 },
11144 )
11145 .detach();
11146 }
11147 });
11148
11149 // Update the selections only
11150 _ = leader.update(cx, |leader, window, cx| {
11151 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
11152 });
11153 follower
11154 .update(cx, |follower, window, cx| {
11155 follower.apply_update_proto(
11156 &project,
11157 pending_update.borrow_mut().take().unwrap(),
11158 window,
11159 cx,
11160 )
11161 })
11162 .unwrap()
11163 .await
11164 .unwrap();
11165 _ = follower.update(cx, |follower, _, cx| {
11166 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
11167 });
11168 assert!(*is_still_following.borrow());
11169 assert_eq!(*follower_edit_event_count.borrow(), 0);
11170
11171 // Update the scroll position only
11172 _ = leader.update(cx, |leader, window, cx| {
11173 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
11174 });
11175 follower
11176 .update(cx, |follower, window, cx| {
11177 follower.apply_update_proto(
11178 &project,
11179 pending_update.borrow_mut().take().unwrap(),
11180 window,
11181 cx,
11182 )
11183 })
11184 .unwrap()
11185 .await
11186 .unwrap();
11187 assert_eq!(
11188 follower
11189 .update(cx, |follower, _, cx| follower.scroll_position(cx))
11190 .unwrap(),
11191 gpui::Point::new(1.5, 3.5)
11192 );
11193 assert!(*is_still_following.borrow());
11194 assert_eq!(*follower_edit_event_count.borrow(), 0);
11195
11196 // Update the selections and scroll position. The follower's scroll position is updated
11197 // via autoscroll, not via the leader's exact scroll position.
11198 _ = leader.update(cx, |leader, window, cx| {
11199 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
11200 leader.request_autoscroll(Autoscroll::newest(), cx);
11201 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
11202 });
11203 follower
11204 .update(cx, |follower, window, cx| {
11205 follower.apply_update_proto(
11206 &project,
11207 pending_update.borrow_mut().take().unwrap(),
11208 window,
11209 cx,
11210 )
11211 })
11212 .unwrap()
11213 .await
11214 .unwrap();
11215 _ = follower.update(cx, |follower, _, cx| {
11216 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
11217 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
11218 });
11219 assert!(*is_still_following.borrow());
11220
11221 // Creating a pending selection that precedes another selection
11222 _ = leader.update(cx, |leader, window, cx| {
11223 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
11224 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
11225 });
11226 follower
11227 .update(cx, |follower, window, cx| {
11228 follower.apply_update_proto(
11229 &project,
11230 pending_update.borrow_mut().take().unwrap(),
11231 window,
11232 cx,
11233 )
11234 })
11235 .unwrap()
11236 .await
11237 .unwrap();
11238 _ = follower.update(cx, |follower, _, cx| {
11239 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
11240 });
11241 assert!(*is_still_following.borrow());
11242
11243 // Extend the pending selection so that it surrounds another selection
11244 _ = leader.update(cx, |leader, window, cx| {
11245 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
11246 });
11247 follower
11248 .update(cx, |follower, window, cx| {
11249 follower.apply_update_proto(
11250 &project,
11251 pending_update.borrow_mut().take().unwrap(),
11252 window,
11253 cx,
11254 )
11255 })
11256 .unwrap()
11257 .await
11258 .unwrap();
11259 _ = follower.update(cx, |follower, _, cx| {
11260 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
11261 });
11262
11263 // Scrolling locally breaks the follow
11264 _ = follower.update(cx, |follower, window, cx| {
11265 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
11266 follower.set_scroll_anchor(
11267 ScrollAnchor {
11268 anchor: top_anchor,
11269 offset: gpui::Point::new(0.0, 0.5),
11270 },
11271 window,
11272 cx,
11273 );
11274 });
11275 assert!(!(*is_still_following.borrow()));
11276}
11277
11278#[gpui::test]
11279async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
11280 init_test(cx, |_| {});
11281
11282 let fs = FakeFs::new(cx.executor());
11283 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
11284 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11285 let pane = workspace
11286 .update(cx, |workspace, _, _| workspace.active_pane().clone())
11287 .unwrap();
11288
11289 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11290
11291 let leader = pane.update_in(cx, |_, window, cx| {
11292 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
11293 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
11294 });
11295
11296 // Start following the editor when it has no excerpts.
11297 let mut state_message =
11298 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
11299 let workspace_entity = workspace.root(cx).unwrap();
11300 let follower_1 = cx
11301 .update_window(*workspace.deref(), |_, window, cx| {
11302 Editor::from_state_proto(
11303 workspace_entity,
11304 ViewId {
11305 creator: Default::default(),
11306 id: 0,
11307 },
11308 &mut state_message,
11309 window,
11310 cx,
11311 )
11312 })
11313 .unwrap()
11314 .unwrap()
11315 .await
11316 .unwrap();
11317
11318 let update_message = Rc::new(RefCell::new(None));
11319 follower_1.update_in(cx, {
11320 let update = update_message.clone();
11321 |_, window, cx| {
11322 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
11323 leader.read(cx).add_event_to_update_proto(
11324 event,
11325 &mut update.borrow_mut(),
11326 window,
11327 cx,
11328 );
11329 })
11330 .detach();
11331 }
11332 });
11333
11334 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
11335 (
11336 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
11337 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
11338 )
11339 });
11340
11341 // Insert some excerpts.
11342 leader.update(cx, |leader, cx| {
11343 leader.buffer.update(cx, |multibuffer, cx| {
11344 let excerpt_ids = multibuffer.push_excerpts(
11345 buffer_1.clone(),
11346 [
11347 ExcerptRange::new(1..6),
11348 ExcerptRange::new(12..15),
11349 ExcerptRange::new(0..3),
11350 ],
11351 cx,
11352 );
11353 multibuffer.insert_excerpts_after(
11354 excerpt_ids[0],
11355 buffer_2.clone(),
11356 [ExcerptRange::new(8..12), ExcerptRange::new(0..6)],
11357 cx,
11358 );
11359 });
11360 });
11361
11362 // Apply the update of adding the excerpts.
11363 follower_1
11364 .update_in(cx, |follower, window, cx| {
11365 follower.apply_update_proto(
11366 &project,
11367 update_message.borrow().clone().unwrap(),
11368 window,
11369 cx,
11370 )
11371 })
11372 .await
11373 .unwrap();
11374 assert_eq!(
11375 follower_1.update(cx, |editor, cx| editor.text(cx)),
11376 leader.update(cx, |editor, cx| editor.text(cx))
11377 );
11378 update_message.borrow_mut().take();
11379
11380 // Start following separately after it already has excerpts.
11381 let mut state_message =
11382 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
11383 let workspace_entity = workspace.root(cx).unwrap();
11384 let follower_2 = cx
11385 .update_window(*workspace.deref(), |_, window, cx| {
11386 Editor::from_state_proto(
11387 workspace_entity,
11388 ViewId {
11389 creator: Default::default(),
11390 id: 0,
11391 },
11392 &mut state_message,
11393 window,
11394 cx,
11395 )
11396 })
11397 .unwrap()
11398 .unwrap()
11399 .await
11400 .unwrap();
11401 assert_eq!(
11402 follower_2.update(cx, |editor, cx| editor.text(cx)),
11403 leader.update(cx, |editor, cx| editor.text(cx))
11404 );
11405
11406 // Remove some excerpts.
11407 leader.update(cx, |leader, cx| {
11408 leader.buffer.update(cx, |multibuffer, cx| {
11409 let excerpt_ids = multibuffer.excerpt_ids();
11410 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
11411 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
11412 });
11413 });
11414
11415 // Apply the update of removing the excerpts.
11416 follower_1
11417 .update_in(cx, |follower, window, cx| {
11418 follower.apply_update_proto(
11419 &project,
11420 update_message.borrow().clone().unwrap(),
11421 window,
11422 cx,
11423 )
11424 })
11425 .await
11426 .unwrap();
11427 follower_2
11428 .update_in(cx, |follower, window, cx| {
11429 follower.apply_update_proto(
11430 &project,
11431 update_message.borrow().clone().unwrap(),
11432 window,
11433 cx,
11434 )
11435 })
11436 .await
11437 .unwrap();
11438 update_message.borrow_mut().take();
11439 assert_eq!(
11440 follower_1.update(cx, |editor, cx| editor.text(cx)),
11441 leader.update(cx, |editor, cx| editor.text(cx))
11442 );
11443}
11444
11445#[gpui::test]
11446async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
11447 init_test(cx, |_| {});
11448
11449 let mut cx = EditorTestContext::new(cx).await;
11450 let lsp_store =
11451 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
11452
11453 cx.set_state(indoc! {"
11454 ˇfn func(abc def: i32) -> u32 {
11455 }
11456 "});
11457
11458 cx.update(|_, cx| {
11459 lsp_store.update(cx, |lsp_store, cx| {
11460 lsp_store
11461 .update_diagnostics(
11462 LanguageServerId(0),
11463 lsp::PublishDiagnosticsParams {
11464 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11465 version: None,
11466 diagnostics: vec![
11467 lsp::Diagnostic {
11468 range: lsp::Range::new(
11469 lsp::Position::new(0, 11),
11470 lsp::Position::new(0, 12),
11471 ),
11472 severity: Some(lsp::DiagnosticSeverity::ERROR),
11473 ..Default::default()
11474 },
11475 lsp::Diagnostic {
11476 range: lsp::Range::new(
11477 lsp::Position::new(0, 12),
11478 lsp::Position::new(0, 15),
11479 ),
11480 severity: Some(lsp::DiagnosticSeverity::ERROR),
11481 ..Default::default()
11482 },
11483 lsp::Diagnostic {
11484 range: lsp::Range::new(
11485 lsp::Position::new(0, 25),
11486 lsp::Position::new(0, 28),
11487 ),
11488 severity: Some(lsp::DiagnosticSeverity::ERROR),
11489 ..Default::default()
11490 },
11491 ],
11492 },
11493 &[],
11494 cx,
11495 )
11496 .unwrap()
11497 });
11498 });
11499
11500 executor.run_until_parked();
11501
11502 cx.update_editor(|editor, window, cx| {
11503 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11504 });
11505
11506 cx.assert_editor_state(indoc! {"
11507 fn func(abc def: i32) -> ˇu32 {
11508 }
11509 "});
11510
11511 cx.update_editor(|editor, window, cx| {
11512 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11513 });
11514
11515 cx.assert_editor_state(indoc! {"
11516 fn func(abc ˇdef: i32) -> u32 {
11517 }
11518 "});
11519
11520 cx.update_editor(|editor, window, cx| {
11521 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11522 });
11523
11524 cx.assert_editor_state(indoc! {"
11525 fn func(abcˇ def: i32) -> u32 {
11526 }
11527 "});
11528
11529 cx.update_editor(|editor, window, cx| {
11530 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11531 });
11532
11533 cx.assert_editor_state(indoc! {"
11534 fn func(abc def: i32) -> ˇu32 {
11535 }
11536 "});
11537}
11538
11539#[gpui::test]
11540async fn cycle_through_same_place_diagnostics(
11541 executor: BackgroundExecutor,
11542 cx: &mut TestAppContext,
11543) {
11544 init_test(cx, |_| {});
11545
11546 let mut cx = EditorTestContext::new(cx).await;
11547 let lsp_store =
11548 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
11549
11550 cx.set_state(indoc! {"
11551 ˇfn func(abc def: i32) -> u32 {
11552 }
11553 "});
11554
11555 cx.update(|_, cx| {
11556 lsp_store.update(cx, |lsp_store, cx| {
11557 lsp_store
11558 .update_diagnostics(
11559 LanguageServerId(0),
11560 lsp::PublishDiagnosticsParams {
11561 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11562 version: None,
11563 diagnostics: vec![
11564 lsp::Diagnostic {
11565 range: lsp::Range::new(
11566 lsp::Position::new(0, 11),
11567 lsp::Position::new(0, 12),
11568 ),
11569 severity: Some(lsp::DiagnosticSeverity::ERROR),
11570 ..Default::default()
11571 },
11572 lsp::Diagnostic {
11573 range: lsp::Range::new(
11574 lsp::Position::new(0, 12),
11575 lsp::Position::new(0, 15),
11576 ),
11577 severity: Some(lsp::DiagnosticSeverity::ERROR),
11578 ..Default::default()
11579 },
11580 lsp::Diagnostic {
11581 range: lsp::Range::new(
11582 lsp::Position::new(0, 12),
11583 lsp::Position::new(0, 15),
11584 ),
11585 severity: Some(lsp::DiagnosticSeverity::ERROR),
11586 ..Default::default()
11587 },
11588 lsp::Diagnostic {
11589 range: lsp::Range::new(
11590 lsp::Position::new(0, 25),
11591 lsp::Position::new(0, 28),
11592 ),
11593 severity: Some(lsp::DiagnosticSeverity::ERROR),
11594 ..Default::default()
11595 },
11596 ],
11597 },
11598 &[],
11599 cx,
11600 )
11601 .unwrap()
11602 });
11603 });
11604 executor.run_until_parked();
11605
11606 //// Backward
11607
11608 // Fourth diagnostic
11609 cx.update_editor(|editor, window, cx| {
11610 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11611 });
11612 cx.assert_editor_state(indoc! {"
11613 fn func(abc def: i32) -> ˇu32 {
11614 }
11615 "});
11616
11617 // Third diagnostic
11618 cx.update_editor(|editor, window, cx| {
11619 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11620 });
11621 cx.assert_editor_state(indoc! {"
11622 fn func(abc ˇdef: i32) -> u32 {
11623 }
11624 "});
11625
11626 // Second diagnostic, same place
11627 cx.update_editor(|editor, window, cx| {
11628 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11629 });
11630 cx.assert_editor_state(indoc! {"
11631 fn func(abc ˇdef: i32) -> u32 {
11632 }
11633 "});
11634
11635 // First diagnostic
11636 cx.update_editor(|editor, window, cx| {
11637 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11638 });
11639 cx.assert_editor_state(indoc! {"
11640 fn func(abcˇ def: i32) -> u32 {
11641 }
11642 "});
11643
11644 // Wrapped over, fourth diagnostic
11645 cx.update_editor(|editor, window, cx| {
11646 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11647 });
11648 cx.assert_editor_state(indoc! {"
11649 fn func(abc def: i32) -> ˇu32 {
11650 }
11651 "});
11652
11653 cx.update_editor(|editor, window, cx| {
11654 editor.move_to_beginning(&MoveToBeginning, window, cx);
11655 });
11656 cx.assert_editor_state(indoc! {"
11657 ˇfn func(abc def: i32) -> u32 {
11658 }
11659 "});
11660
11661 //// Forward
11662
11663 // First diagnostic
11664 cx.update_editor(|editor, window, cx| {
11665 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11666 });
11667 cx.assert_editor_state(indoc! {"
11668 fn func(abcˇ def: i32) -> u32 {
11669 }
11670 "});
11671
11672 // Second diagnostic
11673 cx.update_editor(|editor, window, cx| {
11674 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11675 });
11676 cx.assert_editor_state(indoc! {"
11677 fn func(abc ˇdef: i32) -> u32 {
11678 }
11679 "});
11680
11681 // Third diagnostic, same place
11682 cx.update_editor(|editor, window, cx| {
11683 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11684 });
11685 cx.assert_editor_state(indoc! {"
11686 fn func(abc ˇdef: i32) -> u32 {
11687 }
11688 "});
11689
11690 // Fourth diagnostic
11691 cx.update_editor(|editor, window, cx| {
11692 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11693 });
11694 cx.assert_editor_state(indoc! {"
11695 fn func(abc def: i32) -> ˇu32 {
11696 }
11697 "});
11698
11699 // Wrapped around, first diagnostic
11700 cx.update_editor(|editor, window, cx| {
11701 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11702 });
11703 cx.assert_editor_state(indoc! {"
11704 fn func(abcˇ def: i32) -> u32 {
11705 }
11706 "});
11707}
11708
11709#[gpui::test]
11710async fn active_diagnostics_dismiss_after_invalidation(
11711 executor: BackgroundExecutor,
11712 cx: &mut TestAppContext,
11713) {
11714 init_test(cx, |_| {});
11715
11716 let mut cx = EditorTestContext::new(cx).await;
11717 let lsp_store =
11718 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
11719
11720 cx.set_state(indoc! {"
11721 ˇfn func(abc def: i32) -> u32 {
11722 }
11723 "});
11724
11725 let message = "Something's wrong!";
11726 cx.update(|_, cx| {
11727 lsp_store.update(cx, |lsp_store, cx| {
11728 lsp_store
11729 .update_diagnostics(
11730 LanguageServerId(0),
11731 lsp::PublishDiagnosticsParams {
11732 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11733 version: None,
11734 diagnostics: vec![lsp::Diagnostic {
11735 range: lsp::Range::new(
11736 lsp::Position::new(0, 11),
11737 lsp::Position::new(0, 12),
11738 ),
11739 severity: Some(lsp::DiagnosticSeverity::ERROR),
11740 message: message.to_string(),
11741 ..Default::default()
11742 }],
11743 },
11744 &[],
11745 cx,
11746 )
11747 .unwrap()
11748 });
11749 });
11750 executor.run_until_parked();
11751
11752 cx.update_editor(|editor, window, cx| {
11753 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11754 assert_eq!(
11755 editor
11756 .active_diagnostics
11757 .as_ref()
11758 .map(|diagnostics_group| diagnostics_group.primary_message.as_str()),
11759 Some(message),
11760 "Should have a diagnostics group activated"
11761 );
11762 });
11763 cx.assert_editor_state(indoc! {"
11764 fn func(abcˇ def: i32) -> u32 {
11765 }
11766 "});
11767
11768 cx.update(|_, cx| {
11769 lsp_store.update(cx, |lsp_store, cx| {
11770 lsp_store
11771 .update_diagnostics(
11772 LanguageServerId(0),
11773 lsp::PublishDiagnosticsParams {
11774 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11775 version: None,
11776 diagnostics: Vec::new(),
11777 },
11778 &[],
11779 cx,
11780 )
11781 .unwrap()
11782 });
11783 });
11784 executor.run_until_parked();
11785 cx.update_editor(|editor, _, _| {
11786 assert_eq!(
11787 editor.active_diagnostics, None,
11788 "After no diagnostics set to the editor, no diagnostics should be active"
11789 );
11790 });
11791 cx.assert_editor_state(indoc! {"
11792 fn func(abcˇ def: i32) -> u32 {
11793 }
11794 "});
11795
11796 cx.update_editor(|editor, window, cx| {
11797 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11798 assert_eq!(
11799 editor.active_diagnostics, None,
11800 "Should be no diagnostics to go to and activate"
11801 );
11802 });
11803 cx.assert_editor_state(indoc! {"
11804 fn func(abcˇ def: i32) -> u32 {
11805 }
11806 "});
11807}
11808
11809#[gpui::test]
11810async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
11811 init_test(cx, |_| {});
11812
11813 let mut cx = EditorTestContext::new(cx).await;
11814
11815 cx.set_state(indoc! {"
11816 fn func(abˇc def: i32) -> u32 {
11817 }
11818 "});
11819 let lsp_store =
11820 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
11821
11822 cx.update(|_, cx| {
11823 lsp_store.update(cx, |lsp_store, cx| {
11824 lsp_store.update_diagnostics(
11825 LanguageServerId(0),
11826 lsp::PublishDiagnosticsParams {
11827 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11828 version: None,
11829 diagnostics: vec![lsp::Diagnostic {
11830 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 12)),
11831 severity: Some(lsp::DiagnosticSeverity::ERROR),
11832 message: "we've had problems with <https://link.one>, and <https://link.two> is broken".to_string(),
11833 ..Default::default()
11834 }],
11835 },
11836 &[],
11837 cx,
11838 )
11839 })
11840 }).unwrap();
11841 cx.run_until_parked();
11842 cx.update_editor(|editor, window, cx| {
11843 hover_popover::hover(editor, &Default::default(), window, cx)
11844 });
11845 cx.run_until_parked();
11846 cx.update_editor(|editor, _, _| assert!(editor.hover_state.diagnostic_popover.is_some()))
11847}
11848
11849#[gpui::test]
11850async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
11851 init_test(cx, |_| {});
11852
11853 let mut cx = EditorTestContext::new(cx).await;
11854
11855 let diff_base = r#"
11856 use some::mod;
11857
11858 const A: u32 = 42;
11859
11860 fn main() {
11861 println!("hello");
11862
11863 println!("world");
11864 }
11865 "#
11866 .unindent();
11867
11868 // Edits are modified, removed, modified, added
11869 cx.set_state(
11870 &r#"
11871 use some::modified;
11872
11873 ˇ
11874 fn main() {
11875 println!("hello there");
11876
11877 println!("around the");
11878 println!("world");
11879 }
11880 "#
11881 .unindent(),
11882 );
11883
11884 cx.set_head_text(&diff_base);
11885 executor.run_until_parked();
11886
11887 cx.update_editor(|editor, window, cx| {
11888 //Wrap around the bottom of the buffer
11889 for _ in 0..3 {
11890 editor.go_to_next_hunk(&GoToHunk, window, cx);
11891 }
11892 });
11893
11894 cx.assert_editor_state(
11895 &r#"
11896 ˇuse some::modified;
11897
11898
11899 fn main() {
11900 println!("hello there");
11901
11902 println!("around the");
11903 println!("world");
11904 }
11905 "#
11906 .unindent(),
11907 );
11908
11909 cx.update_editor(|editor, window, cx| {
11910 //Wrap around the top of the buffer
11911 for _ in 0..2 {
11912 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
11913 }
11914 });
11915
11916 cx.assert_editor_state(
11917 &r#"
11918 use some::modified;
11919
11920
11921 fn main() {
11922 ˇ println!("hello there");
11923
11924 println!("around the");
11925 println!("world");
11926 }
11927 "#
11928 .unindent(),
11929 );
11930
11931 cx.update_editor(|editor, window, cx| {
11932 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
11933 });
11934
11935 cx.assert_editor_state(
11936 &r#"
11937 use some::modified;
11938
11939 ˇ
11940 fn main() {
11941 println!("hello there");
11942
11943 println!("around the");
11944 println!("world");
11945 }
11946 "#
11947 .unindent(),
11948 );
11949
11950 cx.update_editor(|editor, window, cx| {
11951 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
11952 });
11953
11954 cx.assert_editor_state(
11955 &r#"
11956 ˇuse some::modified;
11957
11958
11959 fn main() {
11960 println!("hello there");
11961
11962 println!("around the");
11963 println!("world");
11964 }
11965 "#
11966 .unindent(),
11967 );
11968
11969 cx.update_editor(|editor, window, cx| {
11970 for _ in 0..2 {
11971 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
11972 }
11973 });
11974
11975 cx.assert_editor_state(
11976 &r#"
11977 use some::modified;
11978
11979
11980 fn main() {
11981 ˇ println!("hello there");
11982
11983 println!("around the");
11984 println!("world");
11985 }
11986 "#
11987 .unindent(),
11988 );
11989
11990 cx.update_editor(|editor, window, cx| {
11991 editor.fold(&Fold, window, cx);
11992 });
11993
11994 cx.update_editor(|editor, window, cx| {
11995 editor.go_to_next_hunk(&GoToHunk, window, cx);
11996 });
11997
11998 cx.assert_editor_state(
11999 &r#"
12000 ˇuse some::modified;
12001
12002
12003 fn main() {
12004 println!("hello there");
12005
12006 println!("around the");
12007 println!("world");
12008 }
12009 "#
12010 .unindent(),
12011 );
12012}
12013
12014#[test]
12015fn test_split_words() {
12016 fn split(text: &str) -> Vec<&str> {
12017 split_words(text).collect()
12018 }
12019
12020 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
12021 assert_eq!(split("hello_world"), &["hello_", "world"]);
12022 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
12023 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
12024 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
12025 assert_eq!(split("helloworld"), &["helloworld"]);
12026
12027 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
12028}
12029
12030#[gpui::test]
12031async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
12032 init_test(cx, |_| {});
12033
12034 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
12035 let mut assert = |before, after| {
12036 let _state_context = cx.set_state(before);
12037 cx.run_until_parked();
12038 cx.update_editor(|editor, window, cx| {
12039 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
12040 });
12041 cx.run_until_parked();
12042 cx.assert_editor_state(after);
12043 };
12044
12045 // Outside bracket jumps to outside of matching bracket
12046 assert("console.logˇ(var);", "console.log(var)ˇ;");
12047 assert("console.log(var)ˇ;", "console.logˇ(var);");
12048
12049 // Inside bracket jumps to inside of matching bracket
12050 assert("console.log(ˇvar);", "console.log(varˇ);");
12051 assert("console.log(varˇ);", "console.log(ˇvar);");
12052
12053 // When outside a bracket and inside, favor jumping to the inside bracket
12054 assert(
12055 "console.log('foo', [1, 2, 3]ˇ);",
12056 "console.log(ˇ'foo', [1, 2, 3]);",
12057 );
12058 assert(
12059 "console.log(ˇ'foo', [1, 2, 3]);",
12060 "console.log('foo', [1, 2, 3]ˇ);",
12061 );
12062
12063 // Bias forward if two options are equally likely
12064 assert(
12065 "let result = curried_fun()ˇ();",
12066 "let result = curried_fun()()ˇ;",
12067 );
12068
12069 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
12070 assert(
12071 indoc! {"
12072 function test() {
12073 console.log('test')ˇ
12074 }"},
12075 indoc! {"
12076 function test() {
12077 console.logˇ('test')
12078 }"},
12079 );
12080}
12081
12082#[gpui::test]
12083async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
12084 init_test(cx, |_| {});
12085
12086 let fs = FakeFs::new(cx.executor());
12087 fs.insert_tree(
12088 path!("/a"),
12089 json!({
12090 "main.rs": "fn main() { let a = 5; }",
12091 "other.rs": "// Test file",
12092 }),
12093 )
12094 .await;
12095 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12096
12097 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12098 language_registry.add(Arc::new(Language::new(
12099 LanguageConfig {
12100 name: "Rust".into(),
12101 matcher: LanguageMatcher {
12102 path_suffixes: vec!["rs".to_string()],
12103 ..Default::default()
12104 },
12105 brackets: BracketPairConfig {
12106 pairs: vec![BracketPair {
12107 start: "{".to_string(),
12108 end: "}".to_string(),
12109 close: true,
12110 surround: true,
12111 newline: true,
12112 }],
12113 disabled_scopes_by_bracket_ix: Vec::new(),
12114 },
12115 ..Default::default()
12116 },
12117 Some(tree_sitter_rust::LANGUAGE.into()),
12118 )));
12119 let mut fake_servers = language_registry.register_fake_lsp(
12120 "Rust",
12121 FakeLspAdapter {
12122 capabilities: lsp::ServerCapabilities {
12123 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
12124 first_trigger_character: "{".to_string(),
12125 more_trigger_character: None,
12126 }),
12127 ..Default::default()
12128 },
12129 ..Default::default()
12130 },
12131 );
12132
12133 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12134
12135 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12136
12137 let worktree_id = workspace
12138 .update(cx, |workspace, _, cx| {
12139 workspace.project().update(cx, |project, cx| {
12140 project.worktrees(cx).next().unwrap().read(cx).id()
12141 })
12142 })
12143 .unwrap();
12144
12145 let buffer = project
12146 .update(cx, |project, cx| {
12147 project.open_local_buffer(path!("/a/main.rs"), cx)
12148 })
12149 .await
12150 .unwrap();
12151 let editor_handle = workspace
12152 .update(cx, |workspace, window, cx| {
12153 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
12154 })
12155 .unwrap()
12156 .await
12157 .unwrap()
12158 .downcast::<Editor>()
12159 .unwrap();
12160
12161 cx.executor().start_waiting();
12162 let fake_server = fake_servers.next().await.unwrap();
12163
12164 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
12165 |params, _| async move {
12166 assert_eq!(
12167 params.text_document_position.text_document.uri,
12168 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
12169 );
12170 assert_eq!(
12171 params.text_document_position.position,
12172 lsp::Position::new(0, 21),
12173 );
12174
12175 Ok(Some(vec![lsp::TextEdit {
12176 new_text: "]".to_string(),
12177 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12178 }]))
12179 },
12180 );
12181
12182 editor_handle.update_in(cx, |editor, window, cx| {
12183 window.focus(&editor.focus_handle(cx));
12184 editor.change_selections(None, window, cx, |s| {
12185 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
12186 });
12187 editor.handle_input("{", window, cx);
12188 });
12189
12190 cx.executor().run_until_parked();
12191
12192 buffer.update(cx, |buffer, _| {
12193 assert_eq!(
12194 buffer.text(),
12195 "fn main() { let a = {5}; }",
12196 "No extra braces from on type formatting should appear in the buffer"
12197 )
12198 });
12199}
12200
12201#[gpui::test]
12202async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
12203 init_test(cx, |_| {});
12204
12205 let fs = FakeFs::new(cx.executor());
12206 fs.insert_tree(
12207 path!("/a"),
12208 json!({
12209 "main.rs": "fn main() { let a = 5; }",
12210 "other.rs": "// Test file",
12211 }),
12212 )
12213 .await;
12214
12215 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12216
12217 let server_restarts = Arc::new(AtomicUsize::new(0));
12218 let closure_restarts = Arc::clone(&server_restarts);
12219 let language_server_name = "test language server";
12220 let language_name: LanguageName = "Rust".into();
12221
12222 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12223 language_registry.add(Arc::new(Language::new(
12224 LanguageConfig {
12225 name: language_name.clone(),
12226 matcher: LanguageMatcher {
12227 path_suffixes: vec!["rs".to_string()],
12228 ..Default::default()
12229 },
12230 ..Default::default()
12231 },
12232 Some(tree_sitter_rust::LANGUAGE.into()),
12233 )));
12234 let mut fake_servers = language_registry.register_fake_lsp(
12235 "Rust",
12236 FakeLspAdapter {
12237 name: language_server_name,
12238 initialization_options: Some(json!({
12239 "testOptionValue": true
12240 })),
12241 initializer: Some(Box::new(move |fake_server| {
12242 let task_restarts = Arc::clone(&closure_restarts);
12243 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
12244 task_restarts.fetch_add(1, atomic::Ordering::Release);
12245 futures::future::ready(Ok(()))
12246 });
12247 })),
12248 ..Default::default()
12249 },
12250 );
12251
12252 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12253 let _buffer = project
12254 .update(cx, |project, cx| {
12255 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
12256 })
12257 .await
12258 .unwrap();
12259 let _fake_server = fake_servers.next().await.unwrap();
12260 update_test_language_settings(cx, |language_settings| {
12261 language_settings.languages.insert(
12262 language_name.clone(),
12263 LanguageSettingsContent {
12264 tab_size: NonZeroU32::new(8),
12265 ..Default::default()
12266 },
12267 );
12268 });
12269 cx.executor().run_until_parked();
12270 assert_eq!(
12271 server_restarts.load(atomic::Ordering::Acquire),
12272 0,
12273 "Should not restart LSP server on an unrelated change"
12274 );
12275
12276 update_test_project_settings(cx, |project_settings| {
12277 project_settings.lsp.insert(
12278 "Some other server name".into(),
12279 LspSettings {
12280 binary: None,
12281 settings: None,
12282 initialization_options: Some(json!({
12283 "some other init value": false
12284 })),
12285 },
12286 );
12287 });
12288 cx.executor().run_until_parked();
12289 assert_eq!(
12290 server_restarts.load(atomic::Ordering::Acquire),
12291 0,
12292 "Should not restart LSP server on an unrelated LSP settings change"
12293 );
12294
12295 update_test_project_settings(cx, |project_settings| {
12296 project_settings.lsp.insert(
12297 language_server_name.into(),
12298 LspSettings {
12299 binary: None,
12300 settings: None,
12301 initialization_options: Some(json!({
12302 "anotherInitValue": false
12303 })),
12304 },
12305 );
12306 });
12307 cx.executor().run_until_parked();
12308 assert_eq!(
12309 server_restarts.load(atomic::Ordering::Acquire),
12310 1,
12311 "Should restart LSP server on a related LSP settings change"
12312 );
12313
12314 update_test_project_settings(cx, |project_settings| {
12315 project_settings.lsp.insert(
12316 language_server_name.into(),
12317 LspSettings {
12318 binary: None,
12319 settings: None,
12320 initialization_options: Some(json!({
12321 "anotherInitValue": false
12322 })),
12323 },
12324 );
12325 });
12326 cx.executor().run_until_parked();
12327 assert_eq!(
12328 server_restarts.load(atomic::Ordering::Acquire),
12329 1,
12330 "Should not restart LSP server on a related LSP settings change that is the same"
12331 );
12332
12333 update_test_project_settings(cx, |project_settings| {
12334 project_settings.lsp.insert(
12335 language_server_name.into(),
12336 LspSettings {
12337 binary: None,
12338 settings: None,
12339 initialization_options: None,
12340 },
12341 );
12342 });
12343 cx.executor().run_until_parked();
12344 assert_eq!(
12345 server_restarts.load(atomic::Ordering::Acquire),
12346 2,
12347 "Should restart LSP server on another related LSP settings change"
12348 );
12349}
12350
12351#[gpui::test]
12352async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
12353 init_test(cx, |_| {});
12354
12355 let mut cx = EditorLspTestContext::new_rust(
12356 lsp::ServerCapabilities {
12357 completion_provider: Some(lsp::CompletionOptions {
12358 trigger_characters: Some(vec![".".to_string()]),
12359 resolve_provider: Some(true),
12360 ..Default::default()
12361 }),
12362 ..Default::default()
12363 },
12364 cx,
12365 )
12366 .await;
12367
12368 cx.set_state("fn main() { let a = 2ˇ; }");
12369 cx.simulate_keystroke(".");
12370 let completion_item = lsp::CompletionItem {
12371 label: "some".into(),
12372 kind: Some(lsp::CompletionItemKind::SNIPPET),
12373 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
12374 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
12375 kind: lsp::MarkupKind::Markdown,
12376 value: "```rust\nSome(2)\n```".to_string(),
12377 })),
12378 deprecated: Some(false),
12379 sort_text: Some("fffffff2".to_string()),
12380 filter_text: Some("some".to_string()),
12381 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
12382 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12383 range: lsp::Range {
12384 start: lsp::Position {
12385 line: 0,
12386 character: 22,
12387 },
12388 end: lsp::Position {
12389 line: 0,
12390 character: 22,
12391 },
12392 },
12393 new_text: "Some(2)".to_string(),
12394 })),
12395 additional_text_edits: Some(vec![lsp::TextEdit {
12396 range: lsp::Range {
12397 start: lsp::Position {
12398 line: 0,
12399 character: 20,
12400 },
12401 end: lsp::Position {
12402 line: 0,
12403 character: 22,
12404 },
12405 },
12406 new_text: "".to_string(),
12407 }]),
12408 ..Default::default()
12409 };
12410
12411 let closure_completion_item = completion_item.clone();
12412 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
12413 let task_completion_item = closure_completion_item.clone();
12414 async move {
12415 Ok(Some(lsp::CompletionResponse::Array(vec![
12416 task_completion_item,
12417 ])))
12418 }
12419 });
12420
12421 request.next().await;
12422
12423 cx.condition(|editor, _| editor.context_menu_visible())
12424 .await;
12425 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12426 editor
12427 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12428 .unwrap()
12429 });
12430 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
12431
12432 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
12433 let task_completion_item = completion_item.clone();
12434 async move { Ok(task_completion_item) }
12435 })
12436 .next()
12437 .await
12438 .unwrap();
12439 apply_additional_edits.await.unwrap();
12440 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
12441}
12442
12443#[gpui::test]
12444async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
12445 init_test(cx, |_| {});
12446
12447 let mut cx = EditorLspTestContext::new_rust(
12448 lsp::ServerCapabilities {
12449 completion_provider: Some(lsp::CompletionOptions {
12450 trigger_characters: Some(vec![".".to_string()]),
12451 resolve_provider: Some(true),
12452 ..Default::default()
12453 }),
12454 ..Default::default()
12455 },
12456 cx,
12457 )
12458 .await;
12459
12460 cx.set_state("fn main() { let a = 2ˇ; }");
12461 cx.simulate_keystroke(".");
12462
12463 let item1 = lsp::CompletionItem {
12464 label: "method id()".to_string(),
12465 filter_text: Some("id".to_string()),
12466 detail: None,
12467 documentation: None,
12468 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12469 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12470 new_text: ".id".to_string(),
12471 })),
12472 ..lsp::CompletionItem::default()
12473 };
12474
12475 let item2 = lsp::CompletionItem {
12476 label: "other".to_string(),
12477 filter_text: Some("other".to_string()),
12478 detail: None,
12479 documentation: None,
12480 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12481 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12482 new_text: ".other".to_string(),
12483 })),
12484 ..lsp::CompletionItem::default()
12485 };
12486
12487 let item1 = item1.clone();
12488 cx.set_request_handler::<lsp::request::Completion, _, _>({
12489 let item1 = item1.clone();
12490 move |_, _, _| {
12491 let item1 = item1.clone();
12492 let item2 = item2.clone();
12493 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
12494 }
12495 })
12496 .next()
12497 .await;
12498
12499 cx.condition(|editor, _| editor.context_menu_visible())
12500 .await;
12501 cx.update_editor(|editor, _, _| {
12502 let context_menu = editor.context_menu.borrow_mut();
12503 let context_menu = context_menu
12504 .as_ref()
12505 .expect("Should have the context menu deployed");
12506 match context_menu {
12507 CodeContextMenu::Completions(completions_menu) => {
12508 let completions = completions_menu.completions.borrow_mut();
12509 assert_eq!(
12510 completions
12511 .iter()
12512 .map(|completion| &completion.label.text)
12513 .collect::<Vec<_>>(),
12514 vec!["method id()", "other"]
12515 )
12516 }
12517 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
12518 }
12519 });
12520
12521 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
12522 let item1 = item1.clone();
12523 move |_, item_to_resolve, _| {
12524 let item1 = item1.clone();
12525 async move {
12526 if item1 == item_to_resolve {
12527 Ok(lsp::CompletionItem {
12528 label: "method id()".to_string(),
12529 filter_text: Some("id".to_string()),
12530 detail: Some("Now resolved!".to_string()),
12531 documentation: Some(lsp::Documentation::String("Docs".to_string())),
12532 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12533 range: lsp::Range::new(
12534 lsp::Position::new(0, 22),
12535 lsp::Position::new(0, 22),
12536 ),
12537 new_text: ".id".to_string(),
12538 })),
12539 ..lsp::CompletionItem::default()
12540 })
12541 } else {
12542 Ok(item_to_resolve)
12543 }
12544 }
12545 }
12546 })
12547 .next()
12548 .await
12549 .unwrap();
12550 cx.run_until_parked();
12551
12552 cx.update_editor(|editor, window, cx| {
12553 editor.context_menu_next(&Default::default(), window, cx);
12554 });
12555
12556 cx.update_editor(|editor, _, _| {
12557 let context_menu = editor.context_menu.borrow_mut();
12558 let context_menu = context_menu
12559 .as_ref()
12560 .expect("Should have the context menu deployed");
12561 match context_menu {
12562 CodeContextMenu::Completions(completions_menu) => {
12563 let completions = completions_menu.completions.borrow_mut();
12564 assert_eq!(
12565 completions
12566 .iter()
12567 .map(|completion| &completion.label.text)
12568 .collect::<Vec<_>>(),
12569 vec!["method id() Now resolved!", "other"],
12570 "Should update first completion label, but not second as the filter text did not match."
12571 );
12572 }
12573 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
12574 }
12575 });
12576}
12577
12578#[gpui::test]
12579async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
12580 init_test(cx, |_| {});
12581
12582 let mut cx = EditorLspTestContext::new_rust(
12583 lsp::ServerCapabilities {
12584 completion_provider: Some(lsp::CompletionOptions {
12585 trigger_characters: Some(vec![".".to_string()]),
12586 resolve_provider: Some(true),
12587 ..Default::default()
12588 }),
12589 ..Default::default()
12590 },
12591 cx,
12592 )
12593 .await;
12594
12595 cx.set_state("fn main() { let a = 2ˇ; }");
12596 cx.simulate_keystroke(".");
12597
12598 let unresolved_item_1 = lsp::CompletionItem {
12599 label: "id".to_string(),
12600 filter_text: Some("id".to_string()),
12601 detail: None,
12602 documentation: None,
12603 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12604 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12605 new_text: ".id".to_string(),
12606 })),
12607 ..lsp::CompletionItem::default()
12608 };
12609 let resolved_item_1 = lsp::CompletionItem {
12610 additional_text_edits: Some(vec![lsp::TextEdit {
12611 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
12612 new_text: "!!".to_string(),
12613 }]),
12614 ..unresolved_item_1.clone()
12615 };
12616 let unresolved_item_2 = lsp::CompletionItem {
12617 label: "other".to_string(),
12618 filter_text: Some("other".to_string()),
12619 detail: None,
12620 documentation: None,
12621 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12622 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12623 new_text: ".other".to_string(),
12624 })),
12625 ..lsp::CompletionItem::default()
12626 };
12627 let resolved_item_2 = lsp::CompletionItem {
12628 additional_text_edits: Some(vec![lsp::TextEdit {
12629 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
12630 new_text: "??".to_string(),
12631 }]),
12632 ..unresolved_item_2.clone()
12633 };
12634
12635 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
12636 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
12637 cx.lsp
12638 .server
12639 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
12640 let unresolved_item_1 = unresolved_item_1.clone();
12641 let resolved_item_1 = resolved_item_1.clone();
12642 let unresolved_item_2 = unresolved_item_2.clone();
12643 let resolved_item_2 = resolved_item_2.clone();
12644 let resolve_requests_1 = resolve_requests_1.clone();
12645 let resolve_requests_2 = resolve_requests_2.clone();
12646 move |unresolved_request, _| {
12647 let unresolved_item_1 = unresolved_item_1.clone();
12648 let resolved_item_1 = resolved_item_1.clone();
12649 let unresolved_item_2 = unresolved_item_2.clone();
12650 let resolved_item_2 = resolved_item_2.clone();
12651 let resolve_requests_1 = resolve_requests_1.clone();
12652 let resolve_requests_2 = resolve_requests_2.clone();
12653 async move {
12654 if unresolved_request == unresolved_item_1 {
12655 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
12656 Ok(resolved_item_1.clone())
12657 } else if unresolved_request == unresolved_item_2 {
12658 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
12659 Ok(resolved_item_2.clone())
12660 } else {
12661 panic!("Unexpected completion item {unresolved_request:?}")
12662 }
12663 }
12664 }
12665 })
12666 .detach();
12667
12668 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
12669 let unresolved_item_1 = unresolved_item_1.clone();
12670 let unresolved_item_2 = unresolved_item_2.clone();
12671 async move {
12672 Ok(Some(lsp::CompletionResponse::Array(vec![
12673 unresolved_item_1,
12674 unresolved_item_2,
12675 ])))
12676 }
12677 })
12678 .next()
12679 .await;
12680
12681 cx.condition(|editor, _| editor.context_menu_visible())
12682 .await;
12683 cx.update_editor(|editor, _, _| {
12684 let context_menu = editor.context_menu.borrow_mut();
12685 let context_menu = context_menu
12686 .as_ref()
12687 .expect("Should have the context menu deployed");
12688 match context_menu {
12689 CodeContextMenu::Completions(completions_menu) => {
12690 let completions = completions_menu.completions.borrow_mut();
12691 assert_eq!(
12692 completions
12693 .iter()
12694 .map(|completion| &completion.label.text)
12695 .collect::<Vec<_>>(),
12696 vec!["id", "other"]
12697 )
12698 }
12699 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
12700 }
12701 });
12702 cx.run_until_parked();
12703
12704 cx.update_editor(|editor, window, cx| {
12705 editor.context_menu_next(&ContextMenuNext, window, cx);
12706 });
12707 cx.run_until_parked();
12708 cx.update_editor(|editor, window, cx| {
12709 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
12710 });
12711 cx.run_until_parked();
12712 cx.update_editor(|editor, window, cx| {
12713 editor.context_menu_next(&ContextMenuNext, window, cx);
12714 });
12715 cx.run_until_parked();
12716 cx.update_editor(|editor, window, cx| {
12717 editor
12718 .compose_completion(&ComposeCompletion::default(), window, cx)
12719 .expect("No task returned")
12720 })
12721 .await
12722 .expect("Completion failed");
12723 cx.run_until_parked();
12724
12725 cx.update_editor(|editor, _, cx| {
12726 assert_eq!(
12727 resolve_requests_1.load(atomic::Ordering::Acquire),
12728 1,
12729 "Should always resolve once despite multiple selections"
12730 );
12731 assert_eq!(
12732 resolve_requests_2.load(atomic::Ordering::Acquire),
12733 1,
12734 "Should always resolve once after multiple selections and applying the completion"
12735 );
12736 assert_eq!(
12737 editor.text(cx),
12738 "fn main() { let a = ??.other; }",
12739 "Should use resolved data when applying the completion"
12740 );
12741 });
12742}
12743
12744#[gpui::test]
12745async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
12746 init_test(cx, |_| {});
12747
12748 let item_0 = lsp::CompletionItem {
12749 label: "abs".into(),
12750 insert_text: Some("abs".into()),
12751 data: Some(json!({ "very": "special"})),
12752 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
12753 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
12754 lsp::InsertReplaceEdit {
12755 new_text: "abs".to_string(),
12756 insert: lsp::Range::default(),
12757 replace: lsp::Range::default(),
12758 },
12759 )),
12760 ..lsp::CompletionItem::default()
12761 };
12762 let items = iter::once(item_0.clone())
12763 .chain((11..51).map(|i| lsp::CompletionItem {
12764 label: format!("item_{}", i),
12765 insert_text: Some(format!("item_{}", i)),
12766 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
12767 ..lsp::CompletionItem::default()
12768 }))
12769 .collect::<Vec<_>>();
12770
12771 let default_commit_characters = vec!["?".to_string()];
12772 let default_data = json!({ "default": "data"});
12773 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
12774 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
12775 let default_edit_range = lsp::Range {
12776 start: lsp::Position {
12777 line: 0,
12778 character: 5,
12779 },
12780 end: lsp::Position {
12781 line: 0,
12782 character: 5,
12783 },
12784 };
12785
12786 let mut cx = EditorLspTestContext::new_rust(
12787 lsp::ServerCapabilities {
12788 completion_provider: Some(lsp::CompletionOptions {
12789 trigger_characters: Some(vec![".".to_string()]),
12790 resolve_provider: Some(true),
12791 ..Default::default()
12792 }),
12793 ..Default::default()
12794 },
12795 cx,
12796 )
12797 .await;
12798
12799 cx.set_state("fn main() { let a = 2ˇ; }");
12800 cx.simulate_keystroke(".");
12801
12802 let completion_data = default_data.clone();
12803 let completion_characters = default_commit_characters.clone();
12804 let completion_items = items.clone();
12805 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
12806 let default_data = completion_data.clone();
12807 let default_commit_characters = completion_characters.clone();
12808 let items = completion_items.clone();
12809 async move {
12810 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
12811 items,
12812 item_defaults: Some(lsp::CompletionListItemDefaults {
12813 data: Some(default_data.clone()),
12814 commit_characters: Some(default_commit_characters.clone()),
12815 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
12816 default_edit_range,
12817 )),
12818 insert_text_format: Some(default_insert_text_format),
12819 insert_text_mode: Some(default_insert_text_mode),
12820 }),
12821 ..lsp::CompletionList::default()
12822 })))
12823 }
12824 })
12825 .next()
12826 .await;
12827
12828 let resolved_items = Arc::new(Mutex::new(Vec::new()));
12829 cx.lsp
12830 .server
12831 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
12832 let closure_resolved_items = resolved_items.clone();
12833 move |item_to_resolve, _| {
12834 let closure_resolved_items = closure_resolved_items.clone();
12835 async move {
12836 closure_resolved_items.lock().push(item_to_resolve.clone());
12837 Ok(item_to_resolve)
12838 }
12839 }
12840 })
12841 .detach();
12842
12843 cx.condition(|editor, _| editor.context_menu_visible())
12844 .await;
12845 cx.run_until_parked();
12846 cx.update_editor(|editor, _, _| {
12847 let menu = editor.context_menu.borrow_mut();
12848 match menu.as_ref().expect("should have the completions menu") {
12849 CodeContextMenu::Completions(completions_menu) => {
12850 assert_eq!(
12851 completions_menu
12852 .entries
12853 .borrow()
12854 .iter()
12855 .map(|mat| mat.string.clone())
12856 .collect::<Vec<String>>(),
12857 items
12858 .iter()
12859 .map(|completion| completion.label.clone())
12860 .collect::<Vec<String>>()
12861 );
12862 }
12863 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
12864 }
12865 });
12866 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
12867 // with 4 from the end.
12868 assert_eq!(
12869 *resolved_items.lock(),
12870 [&items[0..16], &items[items.len() - 4..items.len()]]
12871 .concat()
12872 .iter()
12873 .cloned()
12874 .map(|mut item| {
12875 if item.data.is_none() {
12876 item.data = Some(default_data.clone());
12877 }
12878 item
12879 })
12880 .collect::<Vec<lsp::CompletionItem>>(),
12881 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
12882 );
12883 resolved_items.lock().clear();
12884
12885 cx.update_editor(|editor, window, cx| {
12886 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
12887 });
12888 cx.run_until_parked();
12889 // Completions that have already been resolved are skipped.
12890 assert_eq!(
12891 *resolved_items.lock(),
12892 items[items.len() - 16..items.len() - 4]
12893 .iter()
12894 .cloned()
12895 .map(|mut item| {
12896 if item.data.is_none() {
12897 item.data = Some(default_data.clone());
12898 }
12899 item
12900 })
12901 .collect::<Vec<lsp::CompletionItem>>()
12902 );
12903 resolved_items.lock().clear();
12904}
12905
12906#[gpui::test]
12907async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
12908 init_test(cx, |_| {});
12909
12910 let mut cx = EditorLspTestContext::new(
12911 Language::new(
12912 LanguageConfig {
12913 matcher: LanguageMatcher {
12914 path_suffixes: vec!["jsx".into()],
12915 ..Default::default()
12916 },
12917 overrides: [(
12918 "element".into(),
12919 LanguageConfigOverride {
12920 completion_query_characters: Override::Set(['-'].into_iter().collect()),
12921 ..Default::default()
12922 },
12923 )]
12924 .into_iter()
12925 .collect(),
12926 ..Default::default()
12927 },
12928 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
12929 )
12930 .with_override_query("(jsx_self_closing_element) @element")
12931 .unwrap(),
12932 lsp::ServerCapabilities {
12933 completion_provider: Some(lsp::CompletionOptions {
12934 trigger_characters: Some(vec![":".to_string()]),
12935 ..Default::default()
12936 }),
12937 ..Default::default()
12938 },
12939 cx,
12940 )
12941 .await;
12942
12943 cx.lsp
12944 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12945 Ok(Some(lsp::CompletionResponse::Array(vec![
12946 lsp::CompletionItem {
12947 label: "bg-blue".into(),
12948 ..Default::default()
12949 },
12950 lsp::CompletionItem {
12951 label: "bg-red".into(),
12952 ..Default::default()
12953 },
12954 lsp::CompletionItem {
12955 label: "bg-yellow".into(),
12956 ..Default::default()
12957 },
12958 ])))
12959 });
12960
12961 cx.set_state(r#"<p class="bgˇ" />"#);
12962
12963 // Trigger completion when typing a dash, because the dash is an extra
12964 // word character in the 'element' scope, which contains the cursor.
12965 cx.simulate_keystroke("-");
12966 cx.executor().run_until_parked();
12967 cx.update_editor(|editor, _, _| {
12968 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12969 {
12970 assert_eq!(
12971 completion_menu_entries(&menu),
12972 &["bg-red", "bg-blue", "bg-yellow"]
12973 );
12974 } else {
12975 panic!("expected completion menu to be open");
12976 }
12977 });
12978
12979 cx.simulate_keystroke("l");
12980 cx.executor().run_until_parked();
12981 cx.update_editor(|editor, _, _| {
12982 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12983 {
12984 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
12985 } else {
12986 panic!("expected completion menu to be open");
12987 }
12988 });
12989
12990 // When filtering completions, consider the character after the '-' to
12991 // be the start of a subword.
12992 cx.set_state(r#"<p class="yelˇ" />"#);
12993 cx.simulate_keystroke("l");
12994 cx.executor().run_until_parked();
12995 cx.update_editor(|editor, _, _| {
12996 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12997 {
12998 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
12999 } else {
13000 panic!("expected completion menu to be open");
13001 }
13002 });
13003}
13004
13005fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
13006 let entries = menu.entries.borrow();
13007 entries.iter().map(|mat| mat.string.clone()).collect()
13008}
13009
13010#[gpui::test]
13011async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
13012 init_test(cx, |settings| {
13013 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
13014 FormatterList(vec![Formatter::Prettier].into()),
13015 ))
13016 });
13017
13018 let fs = FakeFs::new(cx.executor());
13019 fs.insert_file(path!("/file.ts"), Default::default()).await;
13020
13021 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
13022 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13023
13024 language_registry.add(Arc::new(Language::new(
13025 LanguageConfig {
13026 name: "TypeScript".into(),
13027 matcher: LanguageMatcher {
13028 path_suffixes: vec!["ts".to_string()],
13029 ..Default::default()
13030 },
13031 ..Default::default()
13032 },
13033 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
13034 )));
13035 update_test_language_settings(cx, |settings| {
13036 settings.defaults.prettier = Some(PrettierSettings {
13037 allowed: true,
13038 ..PrettierSettings::default()
13039 });
13040 });
13041
13042 let test_plugin = "test_plugin";
13043 let _ = language_registry.register_fake_lsp(
13044 "TypeScript",
13045 FakeLspAdapter {
13046 prettier_plugins: vec![test_plugin],
13047 ..Default::default()
13048 },
13049 );
13050
13051 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
13052 let buffer = project
13053 .update(cx, |project, cx| {
13054 project.open_local_buffer(path!("/file.ts"), cx)
13055 })
13056 .await
13057 .unwrap();
13058
13059 let buffer_text = "one\ntwo\nthree\n";
13060 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13061 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
13062 editor.update_in(cx, |editor, window, cx| {
13063 editor.set_text(buffer_text, window, cx)
13064 });
13065
13066 editor
13067 .update_in(cx, |editor, window, cx| {
13068 editor.perform_format(
13069 project.clone(),
13070 FormatTrigger::Manual,
13071 FormatTarget::Buffers,
13072 window,
13073 cx,
13074 )
13075 })
13076 .unwrap()
13077 .await;
13078 assert_eq!(
13079 editor.update(cx, |editor, cx| editor.text(cx)),
13080 buffer_text.to_string() + prettier_format_suffix,
13081 "Test prettier formatting was not applied to the original buffer text",
13082 );
13083
13084 update_test_language_settings(cx, |settings| {
13085 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
13086 });
13087 let format = editor.update_in(cx, |editor, window, cx| {
13088 editor.perform_format(
13089 project.clone(),
13090 FormatTrigger::Manual,
13091 FormatTarget::Buffers,
13092 window,
13093 cx,
13094 )
13095 });
13096 format.await.unwrap();
13097 assert_eq!(
13098 editor.update(cx, |editor, cx| editor.text(cx)),
13099 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
13100 "Autoformatting (via test prettier) was not applied to the original buffer text",
13101 );
13102}
13103
13104#[gpui::test]
13105async fn test_addition_reverts(cx: &mut TestAppContext) {
13106 init_test(cx, |_| {});
13107 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13108 let base_text = indoc! {r#"
13109 struct Row;
13110 struct Row1;
13111 struct Row2;
13112
13113 struct Row4;
13114 struct Row5;
13115 struct Row6;
13116
13117 struct Row8;
13118 struct Row9;
13119 struct Row10;"#};
13120
13121 // When addition hunks are not adjacent to carets, no hunk revert is performed
13122 assert_hunk_revert(
13123 indoc! {r#"struct Row;
13124 struct Row1;
13125 struct Row1.1;
13126 struct Row1.2;
13127 struct Row2;ˇ
13128
13129 struct Row4;
13130 struct Row5;
13131 struct Row6;
13132
13133 struct Row8;
13134 ˇstruct Row9;
13135 struct Row9.1;
13136 struct Row9.2;
13137 struct Row9.3;
13138 struct Row10;"#},
13139 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
13140 indoc! {r#"struct Row;
13141 struct Row1;
13142 struct Row1.1;
13143 struct Row1.2;
13144 struct Row2;ˇ
13145
13146 struct Row4;
13147 struct Row5;
13148 struct Row6;
13149
13150 struct Row8;
13151 ˇstruct Row9;
13152 struct Row9.1;
13153 struct Row9.2;
13154 struct Row9.3;
13155 struct Row10;"#},
13156 base_text,
13157 &mut cx,
13158 );
13159 // Same for selections
13160 assert_hunk_revert(
13161 indoc! {r#"struct Row;
13162 struct Row1;
13163 struct Row2;
13164 struct Row2.1;
13165 struct Row2.2;
13166 «ˇ
13167 struct Row4;
13168 struct» Row5;
13169 «struct Row6;
13170 ˇ»
13171 struct Row9.1;
13172 struct Row9.2;
13173 struct Row9.3;
13174 struct Row8;
13175 struct Row9;
13176 struct Row10;"#},
13177 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
13178 indoc! {r#"struct Row;
13179 struct Row1;
13180 struct Row2;
13181 struct Row2.1;
13182 struct Row2.2;
13183 «ˇ
13184 struct Row4;
13185 struct» Row5;
13186 «struct Row6;
13187 ˇ»
13188 struct Row9.1;
13189 struct Row9.2;
13190 struct Row9.3;
13191 struct Row8;
13192 struct Row9;
13193 struct Row10;"#},
13194 base_text,
13195 &mut cx,
13196 );
13197
13198 // When carets and selections intersect the addition hunks, those are reverted.
13199 // Adjacent carets got merged.
13200 assert_hunk_revert(
13201 indoc! {r#"struct Row;
13202 ˇ// something on the top
13203 struct Row1;
13204 struct Row2;
13205 struct Roˇw3.1;
13206 struct Row2.2;
13207 struct Row2.3;ˇ
13208
13209 struct Row4;
13210 struct ˇRow5.1;
13211 struct Row5.2;
13212 struct «Rowˇ»5.3;
13213 struct Row5;
13214 struct Row6;
13215 ˇ
13216 struct Row9.1;
13217 struct «Rowˇ»9.2;
13218 struct «ˇRow»9.3;
13219 struct Row8;
13220 struct Row9;
13221 «ˇ// something on bottom»
13222 struct Row10;"#},
13223 vec![
13224 DiffHunkStatusKind::Added,
13225 DiffHunkStatusKind::Added,
13226 DiffHunkStatusKind::Added,
13227 DiffHunkStatusKind::Added,
13228 DiffHunkStatusKind::Added,
13229 ],
13230 indoc! {r#"struct Row;
13231 ˇstruct Row1;
13232 struct Row2;
13233 ˇ
13234 struct Row4;
13235 ˇstruct Row5;
13236 struct Row6;
13237 ˇ
13238 ˇstruct Row8;
13239 struct Row9;
13240 ˇstruct Row10;"#},
13241 base_text,
13242 &mut cx,
13243 );
13244}
13245
13246#[gpui::test]
13247async fn test_modification_reverts(cx: &mut TestAppContext) {
13248 init_test(cx, |_| {});
13249 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13250 let base_text = indoc! {r#"
13251 struct Row;
13252 struct Row1;
13253 struct Row2;
13254
13255 struct Row4;
13256 struct Row5;
13257 struct Row6;
13258
13259 struct Row8;
13260 struct Row9;
13261 struct Row10;"#};
13262
13263 // Modification hunks behave the same as the addition ones.
13264 assert_hunk_revert(
13265 indoc! {r#"struct Row;
13266 struct Row1;
13267 struct Row33;
13268 ˇ
13269 struct Row4;
13270 struct Row5;
13271 struct Row6;
13272 ˇ
13273 struct Row99;
13274 struct Row9;
13275 struct Row10;"#},
13276 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
13277 indoc! {r#"struct Row;
13278 struct Row1;
13279 struct Row33;
13280 ˇ
13281 struct Row4;
13282 struct Row5;
13283 struct Row6;
13284 ˇ
13285 struct Row99;
13286 struct Row9;
13287 struct Row10;"#},
13288 base_text,
13289 &mut cx,
13290 );
13291 assert_hunk_revert(
13292 indoc! {r#"struct Row;
13293 struct Row1;
13294 struct Row33;
13295 «ˇ
13296 struct Row4;
13297 struct» Row5;
13298 «struct Row6;
13299 ˇ»
13300 struct Row99;
13301 struct Row9;
13302 struct Row10;"#},
13303 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
13304 indoc! {r#"struct Row;
13305 struct Row1;
13306 struct Row33;
13307 «ˇ
13308 struct Row4;
13309 struct» Row5;
13310 «struct Row6;
13311 ˇ»
13312 struct Row99;
13313 struct Row9;
13314 struct Row10;"#},
13315 base_text,
13316 &mut cx,
13317 );
13318
13319 assert_hunk_revert(
13320 indoc! {r#"ˇstruct Row1.1;
13321 struct Row1;
13322 «ˇstr»uct Row22;
13323
13324 struct ˇRow44;
13325 struct Row5;
13326 struct «Rˇ»ow66;ˇ
13327
13328 «struˇ»ct Row88;
13329 struct Row9;
13330 struct Row1011;ˇ"#},
13331 vec![
13332 DiffHunkStatusKind::Modified,
13333 DiffHunkStatusKind::Modified,
13334 DiffHunkStatusKind::Modified,
13335 DiffHunkStatusKind::Modified,
13336 DiffHunkStatusKind::Modified,
13337 DiffHunkStatusKind::Modified,
13338 ],
13339 indoc! {r#"struct Row;
13340 ˇstruct Row1;
13341 struct Row2;
13342 ˇ
13343 struct Row4;
13344 ˇstruct Row5;
13345 struct Row6;
13346 ˇ
13347 struct Row8;
13348 ˇstruct Row9;
13349 struct Row10;ˇ"#},
13350 base_text,
13351 &mut cx,
13352 );
13353}
13354
13355#[gpui::test]
13356async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
13357 init_test(cx, |_| {});
13358 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13359 let base_text = indoc! {r#"
13360 one
13361
13362 two
13363 three
13364 "#};
13365
13366 cx.set_head_text(base_text);
13367 cx.set_state("\nˇ\n");
13368 cx.executor().run_until_parked();
13369 cx.update_editor(|editor, _window, cx| {
13370 editor.expand_selected_diff_hunks(cx);
13371 });
13372 cx.executor().run_until_parked();
13373 cx.update_editor(|editor, window, cx| {
13374 editor.backspace(&Default::default(), window, cx);
13375 });
13376 cx.run_until_parked();
13377 cx.assert_state_with_diff(
13378 indoc! {r#"
13379
13380 - two
13381 - threeˇ
13382 +
13383 "#}
13384 .to_string(),
13385 );
13386}
13387
13388#[gpui::test]
13389async fn test_deletion_reverts(cx: &mut TestAppContext) {
13390 init_test(cx, |_| {});
13391 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13392 let base_text = indoc! {r#"struct Row;
13393struct Row1;
13394struct Row2;
13395
13396struct Row4;
13397struct Row5;
13398struct Row6;
13399
13400struct Row8;
13401struct Row9;
13402struct Row10;"#};
13403
13404 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
13405 assert_hunk_revert(
13406 indoc! {r#"struct Row;
13407 struct Row2;
13408
13409 ˇstruct Row4;
13410 struct Row5;
13411 struct Row6;
13412 ˇ
13413 struct Row8;
13414 struct Row10;"#},
13415 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
13416 indoc! {r#"struct Row;
13417 struct Row2;
13418
13419 ˇstruct Row4;
13420 struct Row5;
13421 struct Row6;
13422 ˇ
13423 struct Row8;
13424 struct Row10;"#},
13425 base_text,
13426 &mut cx,
13427 );
13428 assert_hunk_revert(
13429 indoc! {r#"struct Row;
13430 struct Row2;
13431
13432 «ˇstruct Row4;
13433 struct» Row5;
13434 «struct Row6;
13435 ˇ»
13436 struct Row8;
13437 struct Row10;"#},
13438 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
13439 indoc! {r#"struct Row;
13440 struct Row2;
13441
13442 «ˇstruct Row4;
13443 struct» Row5;
13444 «struct Row6;
13445 ˇ»
13446 struct Row8;
13447 struct Row10;"#},
13448 base_text,
13449 &mut cx,
13450 );
13451
13452 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
13453 assert_hunk_revert(
13454 indoc! {r#"struct Row;
13455 ˇstruct Row2;
13456
13457 struct Row4;
13458 struct Row5;
13459 struct Row6;
13460
13461 struct Row8;ˇ
13462 struct Row10;"#},
13463 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
13464 indoc! {r#"struct Row;
13465 struct Row1;
13466 ˇstruct Row2;
13467
13468 struct Row4;
13469 struct Row5;
13470 struct Row6;
13471
13472 struct Row8;ˇ
13473 struct Row9;
13474 struct Row10;"#},
13475 base_text,
13476 &mut cx,
13477 );
13478 assert_hunk_revert(
13479 indoc! {r#"struct Row;
13480 struct Row2«ˇ;
13481 struct Row4;
13482 struct» Row5;
13483 «struct Row6;
13484
13485 struct Row8;ˇ»
13486 struct Row10;"#},
13487 vec![
13488 DiffHunkStatusKind::Deleted,
13489 DiffHunkStatusKind::Deleted,
13490 DiffHunkStatusKind::Deleted,
13491 ],
13492 indoc! {r#"struct Row;
13493 struct Row1;
13494 struct Row2«ˇ;
13495
13496 struct Row4;
13497 struct» Row5;
13498 «struct Row6;
13499
13500 struct Row8;ˇ»
13501 struct Row9;
13502 struct Row10;"#},
13503 base_text,
13504 &mut cx,
13505 );
13506}
13507
13508#[gpui::test]
13509async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
13510 init_test(cx, |_| {});
13511
13512 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
13513 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
13514 let base_text_3 =
13515 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
13516
13517 let text_1 = edit_first_char_of_every_line(base_text_1);
13518 let text_2 = edit_first_char_of_every_line(base_text_2);
13519 let text_3 = edit_first_char_of_every_line(base_text_3);
13520
13521 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
13522 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
13523 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
13524
13525 let multibuffer = cx.new(|cx| {
13526 let mut multibuffer = MultiBuffer::new(ReadWrite);
13527 multibuffer.push_excerpts(
13528 buffer_1.clone(),
13529 [
13530 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
13531 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
13532 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
13533 ],
13534 cx,
13535 );
13536 multibuffer.push_excerpts(
13537 buffer_2.clone(),
13538 [
13539 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
13540 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
13541 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
13542 ],
13543 cx,
13544 );
13545 multibuffer.push_excerpts(
13546 buffer_3.clone(),
13547 [
13548 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
13549 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
13550 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
13551 ],
13552 cx,
13553 );
13554 multibuffer
13555 });
13556
13557 let fs = FakeFs::new(cx.executor());
13558 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
13559 let (editor, cx) = cx
13560 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
13561 editor.update_in(cx, |editor, _window, cx| {
13562 for (buffer, diff_base) in [
13563 (buffer_1.clone(), base_text_1),
13564 (buffer_2.clone(), base_text_2),
13565 (buffer_3.clone(), base_text_3),
13566 ] {
13567 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
13568 editor
13569 .buffer
13570 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
13571 }
13572 });
13573 cx.executor().run_until_parked();
13574
13575 editor.update_in(cx, |editor, window, cx| {
13576 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}");
13577 editor.select_all(&SelectAll, window, cx);
13578 editor.git_restore(&Default::default(), window, cx);
13579 });
13580 cx.executor().run_until_parked();
13581
13582 // When all ranges are selected, all buffer hunks are reverted.
13583 editor.update(cx, |editor, cx| {
13584 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");
13585 });
13586 buffer_1.update(cx, |buffer, _| {
13587 assert_eq!(buffer.text(), base_text_1);
13588 });
13589 buffer_2.update(cx, |buffer, _| {
13590 assert_eq!(buffer.text(), base_text_2);
13591 });
13592 buffer_3.update(cx, |buffer, _| {
13593 assert_eq!(buffer.text(), base_text_3);
13594 });
13595
13596 editor.update_in(cx, |editor, window, cx| {
13597 editor.undo(&Default::default(), window, cx);
13598 });
13599
13600 editor.update_in(cx, |editor, window, cx| {
13601 editor.change_selections(None, window, cx, |s| {
13602 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
13603 });
13604 editor.git_restore(&Default::default(), window, cx);
13605 });
13606
13607 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
13608 // but not affect buffer_2 and its related excerpts.
13609 editor.update(cx, |editor, cx| {
13610 assert_eq!(
13611 editor.text(cx),
13612 "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}"
13613 );
13614 });
13615 buffer_1.update(cx, |buffer, _| {
13616 assert_eq!(buffer.text(), base_text_1);
13617 });
13618 buffer_2.update(cx, |buffer, _| {
13619 assert_eq!(
13620 buffer.text(),
13621 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
13622 );
13623 });
13624 buffer_3.update(cx, |buffer, _| {
13625 assert_eq!(
13626 buffer.text(),
13627 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
13628 );
13629 });
13630
13631 fn edit_first_char_of_every_line(text: &str) -> String {
13632 text.split('\n')
13633 .map(|line| format!("X{}", &line[1..]))
13634 .collect::<Vec<_>>()
13635 .join("\n")
13636 }
13637}
13638
13639#[gpui::test]
13640async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
13641 init_test(cx, |_| {});
13642
13643 let cols = 4;
13644 let rows = 10;
13645 let sample_text_1 = sample_text(rows, cols, 'a');
13646 assert_eq!(
13647 sample_text_1,
13648 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
13649 );
13650 let sample_text_2 = sample_text(rows, cols, 'l');
13651 assert_eq!(
13652 sample_text_2,
13653 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
13654 );
13655 let sample_text_3 = sample_text(rows, cols, 'v');
13656 assert_eq!(
13657 sample_text_3,
13658 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
13659 );
13660
13661 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
13662 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
13663 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
13664
13665 let multi_buffer = cx.new(|cx| {
13666 let mut multibuffer = MultiBuffer::new(ReadWrite);
13667 multibuffer.push_excerpts(
13668 buffer_1.clone(),
13669 [
13670 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
13671 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
13672 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
13673 ],
13674 cx,
13675 );
13676 multibuffer.push_excerpts(
13677 buffer_2.clone(),
13678 [
13679 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
13680 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
13681 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
13682 ],
13683 cx,
13684 );
13685 multibuffer.push_excerpts(
13686 buffer_3.clone(),
13687 [
13688 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
13689 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
13690 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
13691 ],
13692 cx,
13693 );
13694 multibuffer
13695 });
13696
13697 let fs = FakeFs::new(cx.executor());
13698 fs.insert_tree(
13699 "/a",
13700 json!({
13701 "main.rs": sample_text_1,
13702 "other.rs": sample_text_2,
13703 "lib.rs": sample_text_3,
13704 }),
13705 )
13706 .await;
13707 let project = Project::test(fs, ["/a".as_ref()], cx).await;
13708 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13709 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
13710 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
13711 Editor::new(
13712 EditorMode::Full,
13713 multi_buffer,
13714 Some(project.clone()),
13715 window,
13716 cx,
13717 )
13718 });
13719 let multibuffer_item_id = workspace
13720 .update(cx, |workspace, window, cx| {
13721 assert!(
13722 workspace.active_item(cx).is_none(),
13723 "active item should be None before the first item is added"
13724 );
13725 workspace.add_item_to_active_pane(
13726 Box::new(multi_buffer_editor.clone()),
13727 None,
13728 true,
13729 window,
13730 cx,
13731 );
13732 let active_item = workspace
13733 .active_item(cx)
13734 .expect("should have an active item after adding the multi buffer");
13735 assert!(
13736 !active_item.is_singleton(cx),
13737 "A multi buffer was expected to active after adding"
13738 );
13739 active_item.item_id()
13740 })
13741 .unwrap();
13742 cx.executor().run_until_parked();
13743
13744 multi_buffer_editor.update_in(cx, |editor, window, cx| {
13745 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
13746 s.select_ranges(Some(1..2))
13747 });
13748 editor.open_excerpts(&OpenExcerpts, window, cx);
13749 });
13750 cx.executor().run_until_parked();
13751 let first_item_id = workspace
13752 .update(cx, |workspace, window, cx| {
13753 let active_item = workspace
13754 .active_item(cx)
13755 .expect("should have an active item after navigating into the 1st buffer");
13756 let first_item_id = active_item.item_id();
13757 assert_ne!(
13758 first_item_id, multibuffer_item_id,
13759 "Should navigate into the 1st buffer and activate it"
13760 );
13761 assert!(
13762 active_item.is_singleton(cx),
13763 "New active item should be a singleton buffer"
13764 );
13765 assert_eq!(
13766 active_item
13767 .act_as::<Editor>(cx)
13768 .expect("should have navigated into an editor for the 1st buffer")
13769 .read(cx)
13770 .text(cx),
13771 sample_text_1
13772 );
13773
13774 workspace
13775 .go_back(workspace.active_pane().downgrade(), window, cx)
13776 .detach_and_log_err(cx);
13777
13778 first_item_id
13779 })
13780 .unwrap();
13781 cx.executor().run_until_parked();
13782 workspace
13783 .update(cx, |workspace, _, cx| {
13784 let active_item = workspace
13785 .active_item(cx)
13786 .expect("should have an active item after navigating back");
13787 assert_eq!(
13788 active_item.item_id(),
13789 multibuffer_item_id,
13790 "Should navigate back to the multi buffer"
13791 );
13792 assert!(!active_item.is_singleton(cx));
13793 })
13794 .unwrap();
13795
13796 multi_buffer_editor.update_in(cx, |editor, window, cx| {
13797 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
13798 s.select_ranges(Some(39..40))
13799 });
13800 editor.open_excerpts(&OpenExcerpts, window, cx);
13801 });
13802 cx.executor().run_until_parked();
13803 let second_item_id = workspace
13804 .update(cx, |workspace, window, cx| {
13805 let active_item = workspace
13806 .active_item(cx)
13807 .expect("should have an active item after navigating into the 2nd buffer");
13808 let second_item_id = active_item.item_id();
13809 assert_ne!(
13810 second_item_id, multibuffer_item_id,
13811 "Should navigate away from the multibuffer"
13812 );
13813 assert_ne!(
13814 second_item_id, first_item_id,
13815 "Should navigate into the 2nd buffer and activate it"
13816 );
13817 assert!(
13818 active_item.is_singleton(cx),
13819 "New active item should be a singleton buffer"
13820 );
13821 assert_eq!(
13822 active_item
13823 .act_as::<Editor>(cx)
13824 .expect("should have navigated into an editor")
13825 .read(cx)
13826 .text(cx),
13827 sample_text_2
13828 );
13829
13830 workspace
13831 .go_back(workspace.active_pane().downgrade(), window, cx)
13832 .detach_and_log_err(cx);
13833
13834 second_item_id
13835 })
13836 .unwrap();
13837 cx.executor().run_until_parked();
13838 workspace
13839 .update(cx, |workspace, _, cx| {
13840 let active_item = workspace
13841 .active_item(cx)
13842 .expect("should have an active item after navigating back from the 2nd buffer");
13843 assert_eq!(
13844 active_item.item_id(),
13845 multibuffer_item_id,
13846 "Should navigate back from the 2nd buffer to the multi buffer"
13847 );
13848 assert!(!active_item.is_singleton(cx));
13849 })
13850 .unwrap();
13851
13852 multi_buffer_editor.update_in(cx, |editor, window, cx| {
13853 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
13854 s.select_ranges(Some(70..70))
13855 });
13856 editor.open_excerpts(&OpenExcerpts, window, cx);
13857 });
13858 cx.executor().run_until_parked();
13859 workspace
13860 .update(cx, |workspace, window, cx| {
13861 let active_item = workspace
13862 .active_item(cx)
13863 .expect("should have an active item after navigating into the 3rd buffer");
13864 let third_item_id = active_item.item_id();
13865 assert_ne!(
13866 third_item_id, multibuffer_item_id,
13867 "Should navigate into the 3rd buffer and activate it"
13868 );
13869 assert_ne!(third_item_id, first_item_id);
13870 assert_ne!(third_item_id, second_item_id);
13871 assert!(
13872 active_item.is_singleton(cx),
13873 "New active item should be a singleton buffer"
13874 );
13875 assert_eq!(
13876 active_item
13877 .act_as::<Editor>(cx)
13878 .expect("should have navigated into an editor")
13879 .read(cx)
13880 .text(cx),
13881 sample_text_3
13882 );
13883
13884 workspace
13885 .go_back(workspace.active_pane().downgrade(), window, cx)
13886 .detach_and_log_err(cx);
13887 })
13888 .unwrap();
13889 cx.executor().run_until_parked();
13890 workspace
13891 .update(cx, |workspace, _, cx| {
13892 let active_item = workspace
13893 .active_item(cx)
13894 .expect("should have an active item after navigating back from the 3rd buffer");
13895 assert_eq!(
13896 active_item.item_id(),
13897 multibuffer_item_id,
13898 "Should navigate back from the 3rd buffer to the multi buffer"
13899 );
13900 assert!(!active_item.is_singleton(cx));
13901 })
13902 .unwrap();
13903}
13904
13905#[gpui::test]
13906async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13907 init_test(cx, |_| {});
13908
13909 let mut cx = EditorTestContext::new(cx).await;
13910
13911 let diff_base = r#"
13912 use some::mod;
13913
13914 const A: u32 = 42;
13915
13916 fn main() {
13917 println!("hello");
13918
13919 println!("world");
13920 }
13921 "#
13922 .unindent();
13923
13924 cx.set_state(
13925 &r#"
13926 use some::modified;
13927
13928 ˇ
13929 fn main() {
13930 println!("hello there");
13931
13932 println!("around the");
13933 println!("world");
13934 }
13935 "#
13936 .unindent(),
13937 );
13938
13939 cx.set_head_text(&diff_base);
13940 executor.run_until_parked();
13941
13942 cx.update_editor(|editor, window, cx| {
13943 editor.go_to_next_hunk(&GoToHunk, window, cx);
13944 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
13945 });
13946 executor.run_until_parked();
13947 cx.assert_state_with_diff(
13948 r#"
13949 use some::modified;
13950
13951
13952 fn main() {
13953 - println!("hello");
13954 + ˇ println!("hello there");
13955
13956 println!("around the");
13957 println!("world");
13958 }
13959 "#
13960 .unindent(),
13961 );
13962
13963 cx.update_editor(|editor, window, cx| {
13964 for _ in 0..2 {
13965 editor.go_to_next_hunk(&GoToHunk, window, cx);
13966 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
13967 }
13968 });
13969 executor.run_until_parked();
13970 cx.assert_state_with_diff(
13971 r#"
13972 - use some::mod;
13973 + ˇuse some::modified;
13974
13975
13976 fn main() {
13977 - println!("hello");
13978 + println!("hello there");
13979
13980 + println!("around the");
13981 println!("world");
13982 }
13983 "#
13984 .unindent(),
13985 );
13986
13987 cx.update_editor(|editor, window, cx| {
13988 editor.go_to_next_hunk(&GoToHunk, window, cx);
13989 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
13990 });
13991 executor.run_until_parked();
13992 cx.assert_state_with_diff(
13993 r#"
13994 - use some::mod;
13995 + use some::modified;
13996
13997 - const A: u32 = 42;
13998 ˇ
13999 fn main() {
14000 - println!("hello");
14001 + println!("hello there");
14002
14003 + println!("around the");
14004 println!("world");
14005 }
14006 "#
14007 .unindent(),
14008 );
14009
14010 cx.update_editor(|editor, window, cx| {
14011 editor.cancel(&Cancel, window, cx);
14012 });
14013
14014 cx.assert_state_with_diff(
14015 r#"
14016 use some::modified;
14017
14018 ˇ
14019 fn main() {
14020 println!("hello there");
14021
14022 println!("around the");
14023 println!("world");
14024 }
14025 "#
14026 .unindent(),
14027 );
14028}
14029
14030#[gpui::test]
14031async fn test_diff_base_change_with_expanded_diff_hunks(
14032 executor: BackgroundExecutor,
14033 cx: &mut TestAppContext,
14034) {
14035 init_test(cx, |_| {});
14036
14037 let mut cx = EditorTestContext::new(cx).await;
14038
14039 let diff_base = r#"
14040 use some::mod1;
14041 use some::mod2;
14042
14043 const A: u32 = 42;
14044 const B: u32 = 42;
14045 const C: u32 = 42;
14046
14047 fn main() {
14048 println!("hello");
14049
14050 println!("world");
14051 }
14052 "#
14053 .unindent();
14054
14055 cx.set_state(
14056 &r#"
14057 use some::mod2;
14058
14059 const A: u32 = 42;
14060 const C: u32 = 42;
14061
14062 fn main(ˇ) {
14063 //println!("hello");
14064
14065 println!("world");
14066 //
14067 //
14068 }
14069 "#
14070 .unindent(),
14071 );
14072
14073 cx.set_head_text(&diff_base);
14074 executor.run_until_parked();
14075
14076 cx.update_editor(|editor, window, cx| {
14077 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14078 });
14079 executor.run_until_parked();
14080 cx.assert_state_with_diff(
14081 r#"
14082 - use some::mod1;
14083 use some::mod2;
14084
14085 const A: u32 = 42;
14086 - const B: u32 = 42;
14087 const C: u32 = 42;
14088
14089 fn main(ˇ) {
14090 - println!("hello");
14091 + //println!("hello");
14092
14093 println!("world");
14094 + //
14095 + //
14096 }
14097 "#
14098 .unindent(),
14099 );
14100
14101 cx.set_head_text("new diff base!");
14102 executor.run_until_parked();
14103 cx.assert_state_with_diff(
14104 r#"
14105 - new diff base!
14106 + use some::mod2;
14107 +
14108 + const A: u32 = 42;
14109 + const C: u32 = 42;
14110 +
14111 + fn main(ˇ) {
14112 + //println!("hello");
14113 +
14114 + println!("world");
14115 + //
14116 + //
14117 + }
14118 "#
14119 .unindent(),
14120 );
14121}
14122
14123#[gpui::test]
14124async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
14125 init_test(cx, |_| {});
14126
14127 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
14128 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
14129 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
14130 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
14131 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
14132 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
14133
14134 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
14135 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
14136 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
14137
14138 let multi_buffer = cx.new(|cx| {
14139 let mut multibuffer = MultiBuffer::new(ReadWrite);
14140 multibuffer.push_excerpts(
14141 buffer_1.clone(),
14142 [
14143 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14144 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14145 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
14146 ],
14147 cx,
14148 );
14149 multibuffer.push_excerpts(
14150 buffer_2.clone(),
14151 [
14152 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14153 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14154 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
14155 ],
14156 cx,
14157 );
14158 multibuffer.push_excerpts(
14159 buffer_3.clone(),
14160 [
14161 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14162 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14163 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
14164 ],
14165 cx,
14166 );
14167 multibuffer
14168 });
14169
14170 let editor =
14171 cx.add_window(|window, cx| Editor::new(EditorMode::Full, multi_buffer, None, window, cx));
14172 editor
14173 .update(cx, |editor, _window, cx| {
14174 for (buffer, diff_base) in [
14175 (buffer_1.clone(), file_1_old),
14176 (buffer_2.clone(), file_2_old),
14177 (buffer_3.clone(), file_3_old),
14178 ] {
14179 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
14180 editor
14181 .buffer
14182 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
14183 }
14184 })
14185 .unwrap();
14186
14187 let mut cx = EditorTestContext::for_editor(editor, cx).await;
14188 cx.run_until_parked();
14189
14190 cx.assert_editor_state(
14191 &"
14192 ˇaaa
14193 ccc
14194 ddd
14195
14196 ggg
14197 hhh
14198
14199
14200 lll
14201 mmm
14202 NNN
14203
14204 qqq
14205 rrr
14206
14207 uuu
14208 111
14209 222
14210 333
14211
14212 666
14213 777
14214
14215 000
14216 !!!"
14217 .unindent(),
14218 );
14219
14220 cx.update_editor(|editor, window, cx| {
14221 editor.select_all(&SelectAll, window, cx);
14222 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14223 });
14224 cx.executor().run_until_parked();
14225
14226 cx.assert_state_with_diff(
14227 "
14228 «aaa
14229 - bbb
14230 ccc
14231 ddd
14232
14233 ggg
14234 hhh
14235
14236
14237 lll
14238 mmm
14239 - nnn
14240 + NNN
14241
14242 qqq
14243 rrr
14244
14245 uuu
14246 111
14247 222
14248 333
14249
14250 + 666
14251 777
14252
14253 000
14254 !!!ˇ»"
14255 .unindent(),
14256 );
14257}
14258
14259#[gpui::test]
14260async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
14261 init_test(cx, |_| {});
14262
14263 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
14264 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
14265
14266 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
14267 let multi_buffer = cx.new(|cx| {
14268 let mut multibuffer = MultiBuffer::new(ReadWrite);
14269 multibuffer.push_excerpts(
14270 buffer.clone(),
14271 [
14272 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
14273 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
14274 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
14275 ],
14276 cx,
14277 );
14278 multibuffer
14279 });
14280
14281 let editor =
14282 cx.add_window(|window, cx| Editor::new(EditorMode::Full, multi_buffer, None, window, cx));
14283 editor
14284 .update(cx, |editor, _window, cx| {
14285 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
14286 editor
14287 .buffer
14288 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
14289 })
14290 .unwrap();
14291
14292 let mut cx = EditorTestContext::for_editor(editor, cx).await;
14293 cx.run_until_parked();
14294
14295 cx.update_editor(|editor, window, cx| {
14296 editor.expand_all_diff_hunks(&Default::default(), window, cx)
14297 });
14298 cx.executor().run_until_parked();
14299
14300 // When the start of a hunk coincides with the start of its excerpt,
14301 // the hunk is expanded. When the start of a a hunk is earlier than
14302 // the start of its excerpt, the hunk is not expanded.
14303 cx.assert_state_with_diff(
14304 "
14305 ˇaaa
14306 - bbb
14307 + BBB
14308
14309 - ddd
14310 - eee
14311 + DDD
14312 + EEE
14313 fff
14314
14315 iii
14316 "
14317 .unindent(),
14318 );
14319}
14320
14321#[gpui::test]
14322async fn test_edits_around_expanded_insertion_hunks(
14323 executor: BackgroundExecutor,
14324 cx: &mut TestAppContext,
14325) {
14326 init_test(cx, |_| {});
14327
14328 let mut cx = EditorTestContext::new(cx).await;
14329
14330 let diff_base = r#"
14331 use some::mod1;
14332 use some::mod2;
14333
14334 const A: u32 = 42;
14335
14336 fn main() {
14337 println!("hello");
14338
14339 println!("world");
14340 }
14341 "#
14342 .unindent();
14343 executor.run_until_parked();
14344 cx.set_state(
14345 &r#"
14346 use some::mod1;
14347 use some::mod2;
14348
14349 const A: u32 = 42;
14350 const B: u32 = 42;
14351 const C: u32 = 42;
14352 ˇ
14353
14354 fn main() {
14355 println!("hello");
14356
14357 println!("world");
14358 }
14359 "#
14360 .unindent(),
14361 );
14362
14363 cx.set_head_text(&diff_base);
14364 executor.run_until_parked();
14365
14366 cx.update_editor(|editor, window, cx| {
14367 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14368 });
14369 executor.run_until_parked();
14370
14371 cx.assert_state_with_diff(
14372 r#"
14373 use some::mod1;
14374 use some::mod2;
14375
14376 const A: u32 = 42;
14377 + const B: u32 = 42;
14378 + const C: u32 = 42;
14379 + ˇ
14380
14381 fn main() {
14382 println!("hello");
14383
14384 println!("world");
14385 }
14386 "#
14387 .unindent(),
14388 );
14389
14390 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
14391 executor.run_until_parked();
14392
14393 cx.assert_state_with_diff(
14394 r#"
14395 use some::mod1;
14396 use some::mod2;
14397
14398 const A: u32 = 42;
14399 + const B: u32 = 42;
14400 + const C: u32 = 42;
14401 + const D: u32 = 42;
14402 + ˇ
14403
14404 fn main() {
14405 println!("hello");
14406
14407 println!("world");
14408 }
14409 "#
14410 .unindent(),
14411 );
14412
14413 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
14414 executor.run_until_parked();
14415
14416 cx.assert_state_with_diff(
14417 r#"
14418 use some::mod1;
14419 use some::mod2;
14420
14421 const A: u32 = 42;
14422 + const B: u32 = 42;
14423 + const C: u32 = 42;
14424 + const D: u32 = 42;
14425 + const E: u32 = 42;
14426 + ˇ
14427
14428 fn main() {
14429 println!("hello");
14430
14431 println!("world");
14432 }
14433 "#
14434 .unindent(),
14435 );
14436
14437 cx.update_editor(|editor, window, cx| {
14438 editor.delete_line(&DeleteLine, window, cx);
14439 });
14440 executor.run_until_parked();
14441
14442 cx.assert_state_with_diff(
14443 r#"
14444 use some::mod1;
14445 use some::mod2;
14446
14447 const A: u32 = 42;
14448 + const B: u32 = 42;
14449 + const C: u32 = 42;
14450 + const D: u32 = 42;
14451 + const E: u32 = 42;
14452 ˇ
14453 fn main() {
14454 println!("hello");
14455
14456 println!("world");
14457 }
14458 "#
14459 .unindent(),
14460 );
14461
14462 cx.update_editor(|editor, window, cx| {
14463 editor.move_up(&MoveUp, window, cx);
14464 editor.delete_line(&DeleteLine, window, cx);
14465 editor.move_up(&MoveUp, window, cx);
14466 editor.delete_line(&DeleteLine, window, cx);
14467 editor.move_up(&MoveUp, window, cx);
14468 editor.delete_line(&DeleteLine, window, cx);
14469 });
14470 executor.run_until_parked();
14471 cx.assert_state_with_diff(
14472 r#"
14473 use some::mod1;
14474 use some::mod2;
14475
14476 const A: u32 = 42;
14477 + const B: u32 = 42;
14478 ˇ
14479 fn main() {
14480 println!("hello");
14481
14482 println!("world");
14483 }
14484 "#
14485 .unindent(),
14486 );
14487
14488 cx.update_editor(|editor, window, cx| {
14489 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
14490 editor.delete_line(&DeleteLine, window, cx);
14491 });
14492 executor.run_until_parked();
14493 cx.assert_state_with_diff(
14494 r#"
14495 ˇ
14496 fn main() {
14497 println!("hello");
14498
14499 println!("world");
14500 }
14501 "#
14502 .unindent(),
14503 );
14504}
14505
14506#[gpui::test]
14507async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
14508 init_test(cx, |_| {});
14509
14510 let mut cx = EditorTestContext::new(cx).await;
14511 cx.set_head_text(indoc! { "
14512 one
14513 two
14514 three
14515 four
14516 five
14517 "
14518 });
14519 cx.set_state(indoc! { "
14520 one
14521 ˇthree
14522 five
14523 "});
14524 cx.run_until_parked();
14525 cx.update_editor(|editor, window, cx| {
14526 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14527 });
14528 cx.assert_state_with_diff(
14529 indoc! { "
14530 one
14531 - two
14532 ˇthree
14533 - four
14534 five
14535 "}
14536 .to_string(),
14537 );
14538 cx.update_editor(|editor, window, cx| {
14539 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14540 });
14541
14542 cx.assert_state_with_diff(
14543 indoc! { "
14544 one
14545 ˇthree
14546 five
14547 "}
14548 .to_string(),
14549 );
14550
14551 cx.set_state(indoc! { "
14552 one
14553 ˇTWO
14554 three
14555 four
14556 five
14557 "});
14558 cx.run_until_parked();
14559 cx.update_editor(|editor, window, cx| {
14560 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14561 });
14562
14563 cx.assert_state_with_diff(
14564 indoc! { "
14565 one
14566 - two
14567 + ˇTWO
14568 three
14569 four
14570 five
14571 "}
14572 .to_string(),
14573 );
14574 cx.update_editor(|editor, window, cx| {
14575 editor.move_up(&Default::default(), window, cx);
14576 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14577 });
14578 cx.assert_state_with_diff(
14579 indoc! { "
14580 one
14581 ˇTWO
14582 three
14583 four
14584 five
14585 "}
14586 .to_string(),
14587 );
14588}
14589
14590#[gpui::test]
14591async fn test_edits_around_expanded_deletion_hunks(
14592 executor: BackgroundExecutor,
14593 cx: &mut TestAppContext,
14594) {
14595 init_test(cx, |_| {});
14596
14597 let mut cx = EditorTestContext::new(cx).await;
14598
14599 let diff_base = r#"
14600 use some::mod1;
14601 use some::mod2;
14602
14603 const A: u32 = 42;
14604 const B: u32 = 42;
14605 const C: u32 = 42;
14606
14607
14608 fn main() {
14609 println!("hello");
14610
14611 println!("world");
14612 }
14613 "#
14614 .unindent();
14615 executor.run_until_parked();
14616 cx.set_state(
14617 &r#"
14618 use some::mod1;
14619 use some::mod2;
14620
14621 ˇconst B: u32 = 42;
14622 const C: u32 = 42;
14623
14624
14625 fn main() {
14626 println!("hello");
14627
14628 println!("world");
14629 }
14630 "#
14631 .unindent(),
14632 );
14633
14634 cx.set_head_text(&diff_base);
14635 executor.run_until_parked();
14636
14637 cx.update_editor(|editor, window, cx| {
14638 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14639 });
14640 executor.run_until_parked();
14641
14642 cx.assert_state_with_diff(
14643 r#"
14644 use some::mod1;
14645 use some::mod2;
14646
14647 - const A: u32 = 42;
14648 ˇconst B: u32 = 42;
14649 const C: u32 = 42;
14650
14651
14652 fn main() {
14653 println!("hello");
14654
14655 println!("world");
14656 }
14657 "#
14658 .unindent(),
14659 );
14660
14661 cx.update_editor(|editor, window, cx| {
14662 editor.delete_line(&DeleteLine, window, cx);
14663 });
14664 executor.run_until_parked();
14665 cx.assert_state_with_diff(
14666 r#"
14667 use some::mod1;
14668 use some::mod2;
14669
14670 - const A: u32 = 42;
14671 - const B: u32 = 42;
14672 ˇconst C: u32 = 42;
14673
14674
14675 fn main() {
14676 println!("hello");
14677
14678 println!("world");
14679 }
14680 "#
14681 .unindent(),
14682 );
14683
14684 cx.update_editor(|editor, window, cx| {
14685 editor.delete_line(&DeleteLine, window, cx);
14686 });
14687 executor.run_until_parked();
14688 cx.assert_state_with_diff(
14689 r#"
14690 use some::mod1;
14691 use some::mod2;
14692
14693 - const A: u32 = 42;
14694 - const B: u32 = 42;
14695 - const C: u32 = 42;
14696 ˇ
14697
14698 fn main() {
14699 println!("hello");
14700
14701 println!("world");
14702 }
14703 "#
14704 .unindent(),
14705 );
14706
14707 cx.update_editor(|editor, window, cx| {
14708 editor.handle_input("replacement", window, cx);
14709 });
14710 executor.run_until_parked();
14711 cx.assert_state_with_diff(
14712 r#"
14713 use some::mod1;
14714 use some::mod2;
14715
14716 - const A: u32 = 42;
14717 - const B: u32 = 42;
14718 - const C: u32 = 42;
14719 -
14720 + replacementˇ
14721
14722 fn main() {
14723 println!("hello");
14724
14725 println!("world");
14726 }
14727 "#
14728 .unindent(),
14729 );
14730}
14731
14732#[gpui::test]
14733async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14734 init_test(cx, |_| {});
14735
14736 let mut cx = EditorTestContext::new(cx).await;
14737
14738 let base_text = r#"
14739 one
14740 two
14741 three
14742 four
14743 five
14744 "#
14745 .unindent();
14746 executor.run_until_parked();
14747 cx.set_state(
14748 &r#"
14749 one
14750 two
14751 fˇour
14752 five
14753 "#
14754 .unindent(),
14755 );
14756
14757 cx.set_head_text(&base_text);
14758 executor.run_until_parked();
14759
14760 cx.update_editor(|editor, window, cx| {
14761 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14762 });
14763 executor.run_until_parked();
14764
14765 cx.assert_state_with_diff(
14766 r#"
14767 one
14768 two
14769 - three
14770 fˇour
14771 five
14772 "#
14773 .unindent(),
14774 );
14775
14776 cx.update_editor(|editor, window, cx| {
14777 editor.backspace(&Backspace, window, cx);
14778 editor.backspace(&Backspace, window, cx);
14779 });
14780 executor.run_until_parked();
14781 cx.assert_state_with_diff(
14782 r#"
14783 one
14784 two
14785 - threeˇ
14786 - four
14787 + our
14788 five
14789 "#
14790 .unindent(),
14791 );
14792}
14793
14794#[gpui::test]
14795async fn test_edit_after_expanded_modification_hunk(
14796 executor: BackgroundExecutor,
14797 cx: &mut TestAppContext,
14798) {
14799 init_test(cx, |_| {});
14800
14801 let mut cx = EditorTestContext::new(cx).await;
14802
14803 let diff_base = r#"
14804 use some::mod1;
14805 use some::mod2;
14806
14807 const A: u32 = 42;
14808 const B: u32 = 42;
14809 const C: u32 = 42;
14810 const D: u32 = 42;
14811
14812
14813 fn main() {
14814 println!("hello");
14815
14816 println!("world");
14817 }"#
14818 .unindent();
14819
14820 cx.set_state(
14821 &r#"
14822 use some::mod1;
14823 use some::mod2;
14824
14825 const A: u32 = 42;
14826 const B: u32 = 42;
14827 const C: u32 = 43ˇ
14828 const D: u32 = 42;
14829
14830
14831 fn main() {
14832 println!("hello");
14833
14834 println!("world");
14835 }"#
14836 .unindent(),
14837 );
14838
14839 cx.set_head_text(&diff_base);
14840 executor.run_until_parked();
14841 cx.update_editor(|editor, window, cx| {
14842 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14843 });
14844 executor.run_until_parked();
14845
14846 cx.assert_state_with_diff(
14847 r#"
14848 use some::mod1;
14849 use some::mod2;
14850
14851 const A: u32 = 42;
14852 const B: u32 = 42;
14853 - const C: u32 = 42;
14854 + const C: u32 = 43ˇ
14855 const D: u32 = 42;
14856
14857
14858 fn main() {
14859 println!("hello");
14860
14861 println!("world");
14862 }"#
14863 .unindent(),
14864 );
14865
14866 cx.update_editor(|editor, window, cx| {
14867 editor.handle_input("\nnew_line\n", window, cx);
14868 });
14869 executor.run_until_parked();
14870
14871 cx.assert_state_with_diff(
14872 r#"
14873 use some::mod1;
14874 use some::mod2;
14875
14876 const A: u32 = 42;
14877 const B: u32 = 42;
14878 - const C: u32 = 42;
14879 + const C: u32 = 43
14880 + new_line
14881 + ˇ
14882 const D: u32 = 42;
14883
14884
14885 fn main() {
14886 println!("hello");
14887
14888 println!("world");
14889 }"#
14890 .unindent(),
14891 );
14892}
14893
14894#[gpui::test]
14895async fn test_stage_and_unstage_added_file_hunk(
14896 executor: BackgroundExecutor,
14897 cx: &mut TestAppContext,
14898) {
14899 init_test(cx, |_| {});
14900
14901 let mut cx = EditorTestContext::new(cx).await;
14902 cx.update_editor(|editor, _, cx| {
14903 editor.set_expand_all_diff_hunks(cx);
14904 });
14905
14906 let working_copy = r#"
14907 ˇfn main() {
14908 println!("hello, world!");
14909 }
14910 "#
14911 .unindent();
14912
14913 cx.set_state(&working_copy);
14914 executor.run_until_parked();
14915
14916 cx.assert_state_with_diff(
14917 r#"
14918 + ˇfn main() {
14919 + println!("hello, world!");
14920 + }
14921 "#
14922 .unindent(),
14923 );
14924 cx.assert_index_text(None);
14925
14926 cx.update_editor(|editor, window, cx| {
14927 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
14928 });
14929 executor.run_until_parked();
14930 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
14931 cx.assert_state_with_diff(
14932 r#"
14933 + ˇfn main() {
14934 + println!("hello, world!");
14935 + }
14936 "#
14937 .unindent(),
14938 );
14939
14940 cx.update_editor(|editor, window, cx| {
14941 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
14942 });
14943 executor.run_until_parked();
14944 cx.assert_index_text(None);
14945}
14946
14947async fn setup_indent_guides_editor(
14948 text: &str,
14949 cx: &mut TestAppContext,
14950) -> (BufferId, EditorTestContext) {
14951 init_test(cx, |_| {});
14952
14953 let mut cx = EditorTestContext::new(cx).await;
14954
14955 let buffer_id = cx.update_editor(|editor, window, cx| {
14956 editor.set_text(text, window, cx);
14957 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
14958
14959 buffer_ids[0]
14960 });
14961
14962 (buffer_id, cx)
14963}
14964
14965fn assert_indent_guides(
14966 range: Range<u32>,
14967 expected: Vec<IndentGuide>,
14968 active_indices: Option<Vec<usize>>,
14969 cx: &mut EditorTestContext,
14970) {
14971 let indent_guides = cx.update_editor(|editor, window, cx| {
14972 let snapshot = editor.snapshot(window, cx).display_snapshot;
14973 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
14974 editor,
14975 MultiBufferRow(range.start)..MultiBufferRow(range.end),
14976 true,
14977 &snapshot,
14978 cx,
14979 );
14980
14981 indent_guides.sort_by(|a, b| {
14982 a.depth.cmp(&b.depth).then(
14983 a.start_row
14984 .cmp(&b.start_row)
14985 .then(a.end_row.cmp(&b.end_row)),
14986 )
14987 });
14988 indent_guides
14989 });
14990
14991 if let Some(expected) = active_indices {
14992 let active_indices = cx.update_editor(|editor, window, cx| {
14993 let snapshot = editor.snapshot(window, cx).display_snapshot;
14994 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
14995 });
14996
14997 assert_eq!(
14998 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
14999 expected,
15000 "Active indent guide indices do not match"
15001 );
15002 }
15003
15004 assert_eq!(indent_guides, expected, "Indent guides do not match");
15005}
15006
15007fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
15008 IndentGuide {
15009 buffer_id,
15010 start_row: MultiBufferRow(start_row),
15011 end_row: MultiBufferRow(end_row),
15012 depth,
15013 tab_size: 4,
15014 settings: IndentGuideSettings {
15015 enabled: true,
15016 line_width: 1,
15017 active_line_width: 1,
15018 ..Default::default()
15019 },
15020 }
15021}
15022
15023#[gpui::test]
15024async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
15025 let (buffer_id, mut cx) = setup_indent_guides_editor(
15026 &"
15027 fn main() {
15028 let a = 1;
15029 }"
15030 .unindent(),
15031 cx,
15032 )
15033 .await;
15034
15035 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
15036}
15037
15038#[gpui::test]
15039async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
15040 let (buffer_id, mut cx) = setup_indent_guides_editor(
15041 &"
15042 fn main() {
15043 let a = 1;
15044 let b = 2;
15045 }"
15046 .unindent(),
15047 cx,
15048 )
15049 .await;
15050
15051 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
15052}
15053
15054#[gpui::test]
15055async fn test_indent_guide_nested(cx: &mut TestAppContext) {
15056 let (buffer_id, mut cx) = setup_indent_guides_editor(
15057 &"
15058 fn main() {
15059 let a = 1;
15060 if a == 3 {
15061 let b = 2;
15062 } else {
15063 let c = 3;
15064 }
15065 }"
15066 .unindent(),
15067 cx,
15068 )
15069 .await;
15070
15071 assert_indent_guides(
15072 0..8,
15073 vec![
15074 indent_guide(buffer_id, 1, 6, 0),
15075 indent_guide(buffer_id, 3, 3, 1),
15076 indent_guide(buffer_id, 5, 5, 1),
15077 ],
15078 None,
15079 &mut cx,
15080 );
15081}
15082
15083#[gpui::test]
15084async fn test_indent_guide_tab(cx: &mut TestAppContext) {
15085 let (buffer_id, mut cx) = setup_indent_guides_editor(
15086 &"
15087 fn main() {
15088 let a = 1;
15089 let b = 2;
15090 let c = 3;
15091 }"
15092 .unindent(),
15093 cx,
15094 )
15095 .await;
15096
15097 assert_indent_guides(
15098 0..5,
15099 vec![
15100 indent_guide(buffer_id, 1, 3, 0),
15101 indent_guide(buffer_id, 2, 2, 1),
15102 ],
15103 None,
15104 &mut cx,
15105 );
15106}
15107
15108#[gpui::test]
15109async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
15110 let (buffer_id, mut cx) = setup_indent_guides_editor(
15111 &"
15112 fn main() {
15113 let a = 1;
15114
15115 let c = 3;
15116 }"
15117 .unindent(),
15118 cx,
15119 )
15120 .await;
15121
15122 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
15123}
15124
15125#[gpui::test]
15126async fn test_indent_guide_complex(cx: &mut TestAppContext) {
15127 let (buffer_id, mut cx) = setup_indent_guides_editor(
15128 &"
15129 fn main() {
15130 let a = 1;
15131
15132 let c = 3;
15133
15134 if a == 3 {
15135 let b = 2;
15136 } else {
15137 let c = 3;
15138 }
15139 }"
15140 .unindent(),
15141 cx,
15142 )
15143 .await;
15144
15145 assert_indent_guides(
15146 0..11,
15147 vec![
15148 indent_guide(buffer_id, 1, 9, 0),
15149 indent_guide(buffer_id, 6, 6, 1),
15150 indent_guide(buffer_id, 8, 8, 1),
15151 ],
15152 None,
15153 &mut cx,
15154 );
15155}
15156
15157#[gpui::test]
15158async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
15159 let (buffer_id, mut cx) = setup_indent_guides_editor(
15160 &"
15161 fn main() {
15162 let a = 1;
15163
15164 let c = 3;
15165
15166 if a == 3 {
15167 let b = 2;
15168 } else {
15169 let c = 3;
15170 }
15171 }"
15172 .unindent(),
15173 cx,
15174 )
15175 .await;
15176
15177 assert_indent_guides(
15178 1..11,
15179 vec![
15180 indent_guide(buffer_id, 1, 9, 0),
15181 indent_guide(buffer_id, 6, 6, 1),
15182 indent_guide(buffer_id, 8, 8, 1),
15183 ],
15184 None,
15185 &mut cx,
15186 );
15187}
15188
15189#[gpui::test]
15190async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
15191 let (buffer_id, mut cx) = setup_indent_guides_editor(
15192 &"
15193 fn main() {
15194 let a = 1;
15195
15196 let c = 3;
15197
15198 if a == 3 {
15199 let b = 2;
15200 } else {
15201 let c = 3;
15202 }
15203 }"
15204 .unindent(),
15205 cx,
15206 )
15207 .await;
15208
15209 assert_indent_guides(
15210 1..10,
15211 vec![
15212 indent_guide(buffer_id, 1, 9, 0),
15213 indent_guide(buffer_id, 6, 6, 1),
15214 indent_guide(buffer_id, 8, 8, 1),
15215 ],
15216 None,
15217 &mut cx,
15218 );
15219}
15220
15221#[gpui::test]
15222async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
15223 let (buffer_id, mut cx) = setup_indent_guides_editor(
15224 &"
15225 block1
15226 block2
15227 block3
15228 block4
15229 block2
15230 block1
15231 block1"
15232 .unindent(),
15233 cx,
15234 )
15235 .await;
15236
15237 assert_indent_guides(
15238 1..10,
15239 vec![
15240 indent_guide(buffer_id, 1, 4, 0),
15241 indent_guide(buffer_id, 2, 3, 1),
15242 indent_guide(buffer_id, 3, 3, 2),
15243 ],
15244 None,
15245 &mut cx,
15246 );
15247}
15248
15249#[gpui::test]
15250async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
15251 let (buffer_id, mut cx) = setup_indent_guides_editor(
15252 &"
15253 block1
15254 block2
15255 block3
15256
15257 block1
15258 block1"
15259 .unindent(),
15260 cx,
15261 )
15262 .await;
15263
15264 assert_indent_guides(
15265 0..6,
15266 vec![
15267 indent_guide(buffer_id, 1, 2, 0),
15268 indent_guide(buffer_id, 2, 2, 1),
15269 ],
15270 None,
15271 &mut cx,
15272 );
15273}
15274
15275#[gpui::test]
15276async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
15277 let (buffer_id, mut cx) = setup_indent_guides_editor(
15278 &"
15279 block1
15280
15281
15282
15283 block2
15284 "
15285 .unindent(),
15286 cx,
15287 )
15288 .await;
15289
15290 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
15291}
15292
15293#[gpui::test]
15294async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
15295 let (buffer_id, mut cx) = setup_indent_guides_editor(
15296 &"
15297 def a:
15298 \tb = 3
15299 \tif True:
15300 \t\tc = 4
15301 \t\td = 5
15302 \tprint(b)
15303 "
15304 .unindent(),
15305 cx,
15306 )
15307 .await;
15308
15309 assert_indent_guides(
15310 0..6,
15311 vec![
15312 indent_guide(buffer_id, 1, 6, 0),
15313 indent_guide(buffer_id, 3, 4, 1),
15314 ],
15315 None,
15316 &mut cx,
15317 );
15318}
15319
15320#[gpui::test]
15321async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
15322 let (buffer_id, mut cx) = setup_indent_guides_editor(
15323 &"
15324 fn main() {
15325 let a = 1;
15326 }"
15327 .unindent(),
15328 cx,
15329 )
15330 .await;
15331
15332 cx.update_editor(|editor, window, cx| {
15333 editor.change_selections(None, window, cx, |s| {
15334 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
15335 });
15336 });
15337
15338 assert_indent_guides(
15339 0..3,
15340 vec![indent_guide(buffer_id, 1, 1, 0)],
15341 Some(vec![0]),
15342 &mut cx,
15343 );
15344}
15345
15346#[gpui::test]
15347async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
15348 let (buffer_id, mut cx) = setup_indent_guides_editor(
15349 &"
15350 fn main() {
15351 if 1 == 2 {
15352 let a = 1;
15353 }
15354 }"
15355 .unindent(),
15356 cx,
15357 )
15358 .await;
15359
15360 cx.update_editor(|editor, window, cx| {
15361 editor.change_selections(None, window, cx, |s| {
15362 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
15363 });
15364 });
15365
15366 assert_indent_guides(
15367 0..4,
15368 vec![
15369 indent_guide(buffer_id, 1, 3, 0),
15370 indent_guide(buffer_id, 2, 2, 1),
15371 ],
15372 Some(vec![1]),
15373 &mut cx,
15374 );
15375
15376 cx.update_editor(|editor, window, cx| {
15377 editor.change_selections(None, window, cx, |s| {
15378 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
15379 });
15380 });
15381
15382 assert_indent_guides(
15383 0..4,
15384 vec![
15385 indent_guide(buffer_id, 1, 3, 0),
15386 indent_guide(buffer_id, 2, 2, 1),
15387 ],
15388 Some(vec![1]),
15389 &mut cx,
15390 );
15391
15392 cx.update_editor(|editor, window, cx| {
15393 editor.change_selections(None, window, cx, |s| {
15394 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
15395 });
15396 });
15397
15398 assert_indent_guides(
15399 0..4,
15400 vec![
15401 indent_guide(buffer_id, 1, 3, 0),
15402 indent_guide(buffer_id, 2, 2, 1),
15403 ],
15404 Some(vec![0]),
15405 &mut cx,
15406 );
15407}
15408
15409#[gpui::test]
15410async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
15411 let (buffer_id, mut cx) = setup_indent_guides_editor(
15412 &"
15413 fn main() {
15414 let a = 1;
15415
15416 let b = 2;
15417 }"
15418 .unindent(),
15419 cx,
15420 )
15421 .await;
15422
15423 cx.update_editor(|editor, window, cx| {
15424 editor.change_selections(None, window, cx, |s| {
15425 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
15426 });
15427 });
15428
15429 assert_indent_guides(
15430 0..5,
15431 vec![indent_guide(buffer_id, 1, 3, 0)],
15432 Some(vec![0]),
15433 &mut cx,
15434 );
15435}
15436
15437#[gpui::test]
15438async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
15439 let (buffer_id, mut cx) = setup_indent_guides_editor(
15440 &"
15441 def m:
15442 a = 1
15443 pass"
15444 .unindent(),
15445 cx,
15446 )
15447 .await;
15448
15449 cx.update_editor(|editor, window, cx| {
15450 editor.change_selections(None, window, cx, |s| {
15451 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
15452 });
15453 });
15454
15455 assert_indent_guides(
15456 0..3,
15457 vec![indent_guide(buffer_id, 1, 2, 0)],
15458 Some(vec![0]),
15459 &mut cx,
15460 );
15461}
15462
15463#[gpui::test]
15464async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
15465 init_test(cx, |_| {});
15466 let mut cx = EditorTestContext::new(cx).await;
15467 let text = indoc! {
15468 "
15469 impl A {
15470 fn b() {
15471 0;
15472 3;
15473 5;
15474 6;
15475 7;
15476 }
15477 }
15478 "
15479 };
15480 let base_text = indoc! {
15481 "
15482 impl A {
15483 fn b() {
15484 0;
15485 1;
15486 2;
15487 3;
15488 4;
15489 }
15490 fn c() {
15491 5;
15492 6;
15493 7;
15494 }
15495 }
15496 "
15497 };
15498
15499 cx.update_editor(|editor, window, cx| {
15500 editor.set_text(text, window, cx);
15501
15502 editor.buffer().update(cx, |multibuffer, cx| {
15503 let buffer = multibuffer.as_singleton().unwrap();
15504 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
15505
15506 multibuffer.set_all_diff_hunks_expanded(cx);
15507 multibuffer.add_diff(diff, cx);
15508
15509 buffer.read(cx).remote_id()
15510 })
15511 });
15512 cx.run_until_parked();
15513
15514 cx.assert_state_with_diff(
15515 indoc! { "
15516 impl A {
15517 fn b() {
15518 0;
15519 - 1;
15520 - 2;
15521 3;
15522 - 4;
15523 - }
15524 - fn c() {
15525 5;
15526 6;
15527 7;
15528 }
15529 }
15530 ˇ"
15531 }
15532 .to_string(),
15533 );
15534
15535 let mut actual_guides = cx.update_editor(|editor, window, cx| {
15536 editor
15537 .snapshot(window, cx)
15538 .buffer_snapshot
15539 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
15540 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
15541 .collect::<Vec<_>>()
15542 });
15543 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
15544 assert_eq!(
15545 actual_guides,
15546 vec![
15547 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
15548 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
15549 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
15550 ]
15551 );
15552}
15553
15554#[gpui::test]
15555async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15556 init_test(cx, |_| {});
15557 let mut cx = EditorTestContext::new(cx).await;
15558
15559 let diff_base = r#"
15560 a
15561 b
15562 c
15563 "#
15564 .unindent();
15565
15566 cx.set_state(
15567 &r#"
15568 ˇA
15569 b
15570 C
15571 "#
15572 .unindent(),
15573 );
15574 cx.set_head_text(&diff_base);
15575 cx.update_editor(|editor, window, cx| {
15576 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15577 });
15578 executor.run_until_parked();
15579
15580 let both_hunks_expanded = r#"
15581 - a
15582 + ˇA
15583 b
15584 - c
15585 + C
15586 "#
15587 .unindent();
15588
15589 cx.assert_state_with_diff(both_hunks_expanded.clone());
15590
15591 let hunk_ranges = cx.update_editor(|editor, window, cx| {
15592 let snapshot = editor.snapshot(window, cx);
15593 let hunks = editor
15594 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
15595 .collect::<Vec<_>>();
15596 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
15597 let buffer_id = hunks[0].buffer_id;
15598 hunks
15599 .into_iter()
15600 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
15601 .collect::<Vec<_>>()
15602 });
15603 assert_eq!(hunk_ranges.len(), 2);
15604
15605 cx.update_editor(|editor, _, cx| {
15606 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
15607 });
15608 executor.run_until_parked();
15609
15610 let second_hunk_expanded = r#"
15611 ˇA
15612 b
15613 - c
15614 + C
15615 "#
15616 .unindent();
15617
15618 cx.assert_state_with_diff(second_hunk_expanded);
15619
15620 cx.update_editor(|editor, _, cx| {
15621 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
15622 });
15623 executor.run_until_parked();
15624
15625 cx.assert_state_with_diff(both_hunks_expanded.clone());
15626
15627 cx.update_editor(|editor, _, cx| {
15628 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
15629 });
15630 executor.run_until_parked();
15631
15632 let first_hunk_expanded = r#"
15633 - a
15634 + ˇA
15635 b
15636 C
15637 "#
15638 .unindent();
15639
15640 cx.assert_state_with_diff(first_hunk_expanded);
15641
15642 cx.update_editor(|editor, _, cx| {
15643 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
15644 });
15645 executor.run_until_parked();
15646
15647 cx.assert_state_with_diff(both_hunks_expanded);
15648
15649 cx.set_state(
15650 &r#"
15651 ˇA
15652 b
15653 "#
15654 .unindent(),
15655 );
15656 cx.run_until_parked();
15657
15658 // TODO this cursor position seems bad
15659 cx.assert_state_with_diff(
15660 r#"
15661 - ˇa
15662 + A
15663 b
15664 "#
15665 .unindent(),
15666 );
15667
15668 cx.update_editor(|editor, window, cx| {
15669 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15670 });
15671
15672 cx.assert_state_with_diff(
15673 r#"
15674 - ˇa
15675 + A
15676 b
15677 - c
15678 "#
15679 .unindent(),
15680 );
15681
15682 let hunk_ranges = cx.update_editor(|editor, window, cx| {
15683 let snapshot = editor.snapshot(window, cx);
15684 let hunks = editor
15685 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
15686 .collect::<Vec<_>>();
15687 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
15688 let buffer_id = hunks[0].buffer_id;
15689 hunks
15690 .into_iter()
15691 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
15692 .collect::<Vec<_>>()
15693 });
15694 assert_eq!(hunk_ranges.len(), 2);
15695
15696 cx.update_editor(|editor, _, cx| {
15697 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
15698 });
15699 executor.run_until_parked();
15700
15701 cx.assert_state_with_diff(
15702 r#"
15703 - ˇa
15704 + A
15705 b
15706 "#
15707 .unindent(),
15708 );
15709}
15710
15711#[gpui::test]
15712async fn test_toggle_deletion_hunk_at_start_of_file(
15713 executor: BackgroundExecutor,
15714 cx: &mut TestAppContext,
15715) {
15716 init_test(cx, |_| {});
15717 let mut cx = EditorTestContext::new(cx).await;
15718
15719 let diff_base = r#"
15720 a
15721 b
15722 c
15723 "#
15724 .unindent();
15725
15726 cx.set_state(
15727 &r#"
15728 ˇb
15729 c
15730 "#
15731 .unindent(),
15732 );
15733 cx.set_head_text(&diff_base);
15734 cx.update_editor(|editor, window, cx| {
15735 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15736 });
15737 executor.run_until_parked();
15738
15739 let hunk_expanded = r#"
15740 - a
15741 ˇb
15742 c
15743 "#
15744 .unindent();
15745
15746 cx.assert_state_with_diff(hunk_expanded.clone());
15747
15748 let hunk_ranges = cx.update_editor(|editor, window, cx| {
15749 let snapshot = editor.snapshot(window, cx);
15750 let hunks = editor
15751 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
15752 .collect::<Vec<_>>();
15753 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
15754 let buffer_id = hunks[0].buffer_id;
15755 hunks
15756 .into_iter()
15757 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
15758 .collect::<Vec<_>>()
15759 });
15760 assert_eq!(hunk_ranges.len(), 1);
15761
15762 cx.update_editor(|editor, _, cx| {
15763 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
15764 });
15765 executor.run_until_parked();
15766
15767 let hunk_collapsed = r#"
15768 ˇb
15769 c
15770 "#
15771 .unindent();
15772
15773 cx.assert_state_with_diff(hunk_collapsed);
15774
15775 cx.update_editor(|editor, _, cx| {
15776 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
15777 });
15778 executor.run_until_parked();
15779
15780 cx.assert_state_with_diff(hunk_expanded.clone());
15781}
15782
15783#[gpui::test]
15784async fn test_display_diff_hunks(cx: &mut TestAppContext) {
15785 init_test(cx, |_| {});
15786
15787 let fs = FakeFs::new(cx.executor());
15788 fs.insert_tree(
15789 path!("/test"),
15790 json!({
15791 ".git": {},
15792 "file-1": "ONE\n",
15793 "file-2": "TWO\n",
15794 "file-3": "THREE\n",
15795 }),
15796 )
15797 .await;
15798
15799 fs.set_head_for_repo(
15800 path!("/test/.git").as_ref(),
15801 &[
15802 ("file-1".into(), "one\n".into()),
15803 ("file-2".into(), "two\n".into()),
15804 ("file-3".into(), "three\n".into()),
15805 ],
15806 );
15807
15808 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
15809 let mut buffers = vec![];
15810 for i in 1..=3 {
15811 let buffer = project
15812 .update(cx, |project, cx| {
15813 let path = format!(path!("/test/file-{}"), i);
15814 project.open_local_buffer(path, cx)
15815 })
15816 .await
15817 .unwrap();
15818 buffers.push(buffer);
15819 }
15820
15821 let multibuffer = cx.new(|cx| {
15822 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
15823 multibuffer.set_all_diff_hunks_expanded(cx);
15824 for buffer in &buffers {
15825 let snapshot = buffer.read(cx).snapshot();
15826 multibuffer.set_excerpts_for_path(
15827 PathKey::namespaced("", buffer.read(cx).file().unwrap().path().clone()),
15828 buffer.clone(),
15829 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
15830 DEFAULT_MULTIBUFFER_CONTEXT,
15831 cx,
15832 );
15833 }
15834 multibuffer
15835 });
15836
15837 let editor = cx.add_window(|window, cx| {
15838 Editor::new(EditorMode::Full, multibuffer, Some(project), window, cx)
15839 });
15840 cx.run_until_parked();
15841
15842 let snapshot = editor
15843 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
15844 .unwrap();
15845 let hunks = snapshot
15846 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
15847 .map(|hunk| match hunk {
15848 DisplayDiffHunk::Unfolded {
15849 display_row_range, ..
15850 } => display_row_range,
15851 DisplayDiffHunk::Folded { .. } => unreachable!(),
15852 })
15853 .collect::<Vec<_>>();
15854 assert_eq!(
15855 hunks,
15856 [
15857 DisplayRow(2)..DisplayRow(4),
15858 DisplayRow(7)..DisplayRow(9),
15859 DisplayRow(12)..DisplayRow(14),
15860 ]
15861 );
15862}
15863
15864#[gpui::test]
15865async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
15866 init_test(cx, |_| {});
15867
15868 let mut cx = EditorTestContext::new(cx).await;
15869 cx.set_head_text(indoc! { "
15870 one
15871 two
15872 three
15873 four
15874 five
15875 "
15876 });
15877 cx.set_index_text(indoc! { "
15878 one
15879 two
15880 three
15881 four
15882 five
15883 "
15884 });
15885 cx.set_state(indoc! {"
15886 one
15887 TWO
15888 ˇTHREE
15889 FOUR
15890 five
15891 "});
15892 cx.run_until_parked();
15893 cx.update_editor(|editor, window, cx| {
15894 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
15895 });
15896 cx.run_until_parked();
15897 cx.assert_index_text(Some(indoc! {"
15898 one
15899 TWO
15900 THREE
15901 FOUR
15902 five
15903 "}));
15904 cx.set_state(indoc! { "
15905 one
15906 TWO
15907 ˇTHREE-HUNDRED
15908 FOUR
15909 five
15910 "});
15911 cx.run_until_parked();
15912 cx.update_editor(|editor, window, cx| {
15913 let snapshot = editor.snapshot(window, cx);
15914 let hunks = editor
15915 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
15916 .collect::<Vec<_>>();
15917 assert_eq!(hunks.len(), 1);
15918 assert_eq!(
15919 hunks[0].status(),
15920 DiffHunkStatus {
15921 kind: DiffHunkStatusKind::Modified,
15922 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
15923 }
15924 );
15925
15926 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
15927 });
15928 cx.run_until_parked();
15929 cx.assert_index_text(Some(indoc! {"
15930 one
15931 TWO
15932 THREE-HUNDRED
15933 FOUR
15934 five
15935 "}));
15936}
15937
15938#[gpui::test]
15939fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
15940 init_test(cx, |_| {});
15941
15942 let editor = cx.add_window(|window, cx| {
15943 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
15944 build_editor(buffer, window, cx)
15945 });
15946
15947 let render_args = Arc::new(Mutex::new(None));
15948 let snapshot = editor
15949 .update(cx, |editor, window, cx| {
15950 let snapshot = editor.buffer().read(cx).snapshot(cx);
15951 let range =
15952 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
15953
15954 struct RenderArgs {
15955 row: MultiBufferRow,
15956 folded: bool,
15957 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
15958 }
15959
15960 let crease = Crease::inline(
15961 range,
15962 FoldPlaceholder::test(),
15963 {
15964 let toggle_callback = render_args.clone();
15965 move |row, folded, callback, _window, _cx| {
15966 *toggle_callback.lock() = Some(RenderArgs {
15967 row,
15968 folded,
15969 callback,
15970 });
15971 div()
15972 }
15973 },
15974 |_row, _folded, _window, _cx| div(),
15975 );
15976
15977 editor.insert_creases(Some(crease), cx);
15978 let snapshot = editor.snapshot(window, cx);
15979 let _div = snapshot.render_crease_toggle(
15980 MultiBufferRow(1),
15981 false,
15982 cx.entity().clone(),
15983 window,
15984 cx,
15985 );
15986 snapshot
15987 })
15988 .unwrap();
15989
15990 let render_args = render_args.lock().take().unwrap();
15991 assert_eq!(render_args.row, MultiBufferRow(1));
15992 assert!(!render_args.folded);
15993 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
15994
15995 cx.update_window(*editor, |_, window, cx| {
15996 (render_args.callback)(true, window, cx)
15997 })
15998 .unwrap();
15999 let snapshot = editor
16000 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16001 .unwrap();
16002 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
16003
16004 cx.update_window(*editor, |_, window, cx| {
16005 (render_args.callback)(false, window, cx)
16006 })
16007 .unwrap();
16008 let snapshot = editor
16009 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16010 .unwrap();
16011 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
16012}
16013
16014#[gpui::test]
16015async fn test_input_text(cx: &mut TestAppContext) {
16016 init_test(cx, |_| {});
16017 let mut cx = EditorTestContext::new(cx).await;
16018
16019 cx.set_state(
16020 &r#"ˇone
16021 two
16022
16023 three
16024 fourˇ
16025 five
16026
16027 siˇx"#
16028 .unindent(),
16029 );
16030
16031 cx.dispatch_action(HandleInput(String::new()));
16032 cx.assert_editor_state(
16033 &r#"ˇone
16034 two
16035
16036 three
16037 fourˇ
16038 five
16039
16040 siˇx"#
16041 .unindent(),
16042 );
16043
16044 cx.dispatch_action(HandleInput("AAAA".to_string()));
16045 cx.assert_editor_state(
16046 &r#"AAAAˇone
16047 two
16048
16049 three
16050 fourAAAAˇ
16051 five
16052
16053 siAAAAˇx"#
16054 .unindent(),
16055 );
16056}
16057
16058#[gpui::test]
16059async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
16060 init_test(cx, |_| {});
16061
16062 let mut cx = EditorTestContext::new(cx).await;
16063 cx.set_state(
16064 r#"let foo = 1;
16065let foo = 2;
16066let foo = 3;
16067let fooˇ = 4;
16068let foo = 5;
16069let foo = 6;
16070let foo = 7;
16071let foo = 8;
16072let foo = 9;
16073let foo = 10;
16074let foo = 11;
16075let foo = 12;
16076let foo = 13;
16077let foo = 14;
16078let foo = 15;"#,
16079 );
16080
16081 cx.update_editor(|e, window, cx| {
16082 assert_eq!(
16083 e.next_scroll_position,
16084 NextScrollCursorCenterTopBottom::Center,
16085 "Default next scroll direction is center",
16086 );
16087
16088 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16089 assert_eq!(
16090 e.next_scroll_position,
16091 NextScrollCursorCenterTopBottom::Top,
16092 "After center, next scroll direction should be top",
16093 );
16094
16095 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16096 assert_eq!(
16097 e.next_scroll_position,
16098 NextScrollCursorCenterTopBottom::Bottom,
16099 "After top, next scroll direction should be bottom",
16100 );
16101
16102 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16103 assert_eq!(
16104 e.next_scroll_position,
16105 NextScrollCursorCenterTopBottom::Center,
16106 "After bottom, scrolling should start over",
16107 );
16108
16109 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16110 assert_eq!(
16111 e.next_scroll_position,
16112 NextScrollCursorCenterTopBottom::Top,
16113 "Scrolling continues if retriggered fast enough"
16114 );
16115 });
16116
16117 cx.executor()
16118 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
16119 cx.executor().run_until_parked();
16120 cx.update_editor(|e, _, _| {
16121 assert_eq!(
16122 e.next_scroll_position,
16123 NextScrollCursorCenterTopBottom::Center,
16124 "If scrolling is not triggered fast enough, it should reset"
16125 );
16126 });
16127}
16128
16129#[gpui::test]
16130async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
16131 init_test(cx, |_| {});
16132 let mut cx = EditorLspTestContext::new_rust(
16133 lsp::ServerCapabilities {
16134 definition_provider: Some(lsp::OneOf::Left(true)),
16135 references_provider: Some(lsp::OneOf::Left(true)),
16136 ..lsp::ServerCapabilities::default()
16137 },
16138 cx,
16139 )
16140 .await;
16141
16142 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
16143 let go_to_definition = cx
16144 .lsp
16145 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
16146 move |params, _| async move {
16147 if empty_go_to_definition {
16148 Ok(None)
16149 } else {
16150 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
16151 uri: params.text_document_position_params.text_document.uri,
16152 range: lsp::Range::new(
16153 lsp::Position::new(4, 3),
16154 lsp::Position::new(4, 6),
16155 ),
16156 })))
16157 }
16158 },
16159 );
16160 let references = cx
16161 .lsp
16162 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
16163 Ok(Some(vec![lsp::Location {
16164 uri: params.text_document_position.text_document.uri,
16165 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
16166 }]))
16167 });
16168 (go_to_definition, references)
16169 };
16170
16171 cx.set_state(
16172 &r#"fn one() {
16173 let mut a = ˇtwo();
16174 }
16175
16176 fn two() {}"#
16177 .unindent(),
16178 );
16179 set_up_lsp_handlers(false, &mut cx);
16180 let navigated = cx
16181 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
16182 .await
16183 .expect("Failed to navigate to definition");
16184 assert_eq!(
16185 navigated,
16186 Navigated::Yes,
16187 "Should have navigated to definition from the GetDefinition response"
16188 );
16189 cx.assert_editor_state(
16190 &r#"fn one() {
16191 let mut a = two();
16192 }
16193
16194 fn «twoˇ»() {}"#
16195 .unindent(),
16196 );
16197
16198 let editors = cx.update_workspace(|workspace, _, cx| {
16199 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
16200 });
16201 cx.update_editor(|_, _, test_editor_cx| {
16202 assert_eq!(
16203 editors.len(),
16204 1,
16205 "Initially, only one, test, editor should be open in the workspace"
16206 );
16207 assert_eq!(
16208 test_editor_cx.entity(),
16209 editors.last().expect("Asserted len is 1").clone()
16210 );
16211 });
16212
16213 set_up_lsp_handlers(true, &mut cx);
16214 let navigated = cx
16215 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
16216 .await
16217 .expect("Failed to navigate to lookup references");
16218 assert_eq!(
16219 navigated,
16220 Navigated::Yes,
16221 "Should have navigated to references as a fallback after empty GoToDefinition response"
16222 );
16223 // We should not change the selections in the existing file,
16224 // if opening another milti buffer with the references
16225 cx.assert_editor_state(
16226 &r#"fn one() {
16227 let mut a = two();
16228 }
16229
16230 fn «twoˇ»() {}"#
16231 .unindent(),
16232 );
16233 let editors = cx.update_workspace(|workspace, _, cx| {
16234 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
16235 });
16236 cx.update_editor(|_, _, test_editor_cx| {
16237 assert_eq!(
16238 editors.len(),
16239 2,
16240 "After falling back to references search, we open a new editor with the results"
16241 );
16242 let references_fallback_text = editors
16243 .into_iter()
16244 .find(|new_editor| *new_editor != test_editor_cx.entity())
16245 .expect("Should have one non-test editor now")
16246 .read(test_editor_cx)
16247 .text(test_editor_cx);
16248 assert_eq!(
16249 references_fallback_text, "fn one() {\n let mut a = two();\n}",
16250 "Should use the range from the references response and not the GoToDefinition one"
16251 );
16252 });
16253}
16254
16255#[gpui::test]
16256async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
16257 init_test(cx, |_| {});
16258 cx.update(|cx| {
16259 let mut editor_settings = EditorSettings::get_global(cx).clone();
16260 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
16261 EditorSettings::override_global(editor_settings, cx);
16262 });
16263 let mut cx = EditorLspTestContext::new_rust(
16264 lsp::ServerCapabilities {
16265 definition_provider: Some(lsp::OneOf::Left(true)),
16266 references_provider: Some(lsp::OneOf::Left(true)),
16267 ..lsp::ServerCapabilities::default()
16268 },
16269 cx,
16270 )
16271 .await;
16272 let original_state = r#"fn one() {
16273 let mut a = ˇtwo();
16274 }
16275
16276 fn two() {}"#
16277 .unindent();
16278 cx.set_state(&original_state);
16279
16280 let mut go_to_definition = cx
16281 .lsp
16282 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
16283 move |_, _| async move { Ok(None) },
16284 );
16285 let _references = cx
16286 .lsp
16287 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
16288 panic!("Should not call for references with no go to definition fallback")
16289 });
16290
16291 let navigated = cx
16292 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
16293 .await
16294 .expect("Failed to navigate to lookup references");
16295 go_to_definition
16296 .next()
16297 .await
16298 .expect("Should have called the go_to_definition handler");
16299
16300 assert_eq!(
16301 navigated,
16302 Navigated::No,
16303 "Should have navigated to references as a fallback after empty GoToDefinition response"
16304 );
16305 cx.assert_editor_state(&original_state);
16306 let editors = cx.update_workspace(|workspace, _, cx| {
16307 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
16308 });
16309 cx.update_editor(|_, _, _| {
16310 assert_eq!(
16311 editors.len(),
16312 1,
16313 "After unsuccessful fallback, no other editor should have been opened"
16314 );
16315 });
16316}
16317
16318#[gpui::test]
16319async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
16320 init_test(cx, |_| {});
16321
16322 let language = Arc::new(Language::new(
16323 LanguageConfig::default(),
16324 Some(tree_sitter_rust::LANGUAGE.into()),
16325 ));
16326
16327 let text = r#"
16328 #[cfg(test)]
16329 mod tests() {
16330 #[test]
16331 fn runnable_1() {
16332 let a = 1;
16333 }
16334
16335 #[test]
16336 fn runnable_2() {
16337 let a = 1;
16338 let b = 2;
16339 }
16340 }
16341 "#
16342 .unindent();
16343
16344 let fs = FakeFs::new(cx.executor());
16345 fs.insert_file("/file.rs", Default::default()).await;
16346
16347 let project = Project::test(fs, ["/a".as_ref()], cx).await;
16348 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16349 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16350 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16351 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
16352
16353 let editor = cx.new_window_entity(|window, cx| {
16354 Editor::new(
16355 EditorMode::Full,
16356 multi_buffer,
16357 Some(project.clone()),
16358 window,
16359 cx,
16360 )
16361 });
16362
16363 editor.update_in(cx, |editor, window, cx| {
16364 let snapshot = editor.buffer().read(cx).snapshot(cx);
16365 editor.tasks.insert(
16366 (buffer.read(cx).remote_id(), 3),
16367 RunnableTasks {
16368 templates: vec![],
16369 offset: snapshot.anchor_before(43),
16370 column: 0,
16371 extra_variables: HashMap::default(),
16372 context_range: BufferOffset(43)..BufferOffset(85),
16373 },
16374 );
16375 editor.tasks.insert(
16376 (buffer.read(cx).remote_id(), 8),
16377 RunnableTasks {
16378 templates: vec![],
16379 offset: snapshot.anchor_before(86),
16380 column: 0,
16381 extra_variables: HashMap::default(),
16382 context_range: BufferOffset(86)..BufferOffset(191),
16383 },
16384 );
16385
16386 // Test finding task when cursor is inside function body
16387 editor.change_selections(None, window, cx, |s| {
16388 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
16389 });
16390 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
16391 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
16392
16393 // Test finding task when cursor is on function name
16394 editor.change_selections(None, window, cx, |s| {
16395 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
16396 });
16397 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
16398 assert_eq!(row, 8, "Should find task when cursor is on function name");
16399 });
16400}
16401
16402#[gpui::test]
16403async fn test_folding_buffers(cx: &mut TestAppContext) {
16404 init_test(cx, |_| {});
16405
16406 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
16407 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
16408 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
16409
16410 let fs = FakeFs::new(cx.executor());
16411 fs.insert_tree(
16412 path!("/a"),
16413 json!({
16414 "first.rs": sample_text_1,
16415 "second.rs": sample_text_2,
16416 "third.rs": sample_text_3,
16417 }),
16418 )
16419 .await;
16420 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16421 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16422 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16423 let worktree = project.update(cx, |project, cx| {
16424 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
16425 assert_eq!(worktrees.len(), 1);
16426 worktrees.pop().unwrap()
16427 });
16428 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
16429
16430 let buffer_1 = project
16431 .update(cx, |project, cx| {
16432 project.open_buffer((worktree_id, "first.rs"), cx)
16433 })
16434 .await
16435 .unwrap();
16436 let buffer_2 = project
16437 .update(cx, |project, cx| {
16438 project.open_buffer((worktree_id, "second.rs"), cx)
16439 })
16440 .await
16441 .unwrap();
16442 let buffer_3 = project
16443 .update(cx, |project, cx| {
16444 project.open_buffer((worktree_id, "third.rs"), cx)
16445 })
16446 .await
16447 .unwrap();
16448
16449 let multi_buffer = cx.new(|cx| {
16450 let mut multi_buffer = MultiBuffer::new(ReadWrite);
16451 multi_buffer.push_excerpts(
16452 buffer_1.clone(),
16453 [
16454 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16455 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16456 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16457 ],
16458 cx,
16459 );
16460 multi_buffer.push_excerpts(
16461 buffer_2.clone(),
16462 [
16463 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16464 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16465 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16466 ],
16467 cx,
16468 );
16469 multi_buffer.push_excerpts(
16470 buffer_3.clone(),
16471 [
16472 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16473 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16474 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16475 ],
16476 cx,
16477 );
16478 multi_buffer
16479 });
16480 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16481 Editor::new(
16482 EditorMode::Full,
16483 multi_buffer.clone(),
16484 Some(project.clone()),
16485 window,
16486 cx,
16487 )
16488 });
16489
16490 assert_eq!(
16491 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16492 "\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",
16493 );
16494
16495 multi_buffer_editor.update(cx, |editor, cx| {
16496 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
16497 });
16498 assert_eq!(
16499 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16500 "\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",
16501 "After folding the first buffer, its text should not be displayed"
16502 );
16503
16504 multi_buffer_editor.update(cx, |editor, cx| {
16505 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
16506 });
16507 assert_eq!(
16508 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16509 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
16510 "After folding the second buffer, its text should not be displayed"
16511 );
16512
16513 multi_buffer_editor.update(cx, |editor, cx| {
16514 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
16515 });
16516 assert_eq!(
16517 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16518 "\n\n\n\n\n",
16519 "After folding the third buffer, its text should not be displayed"
16520 );
16521
16522 // Emulate selection inside the fold logic, that should work
16523 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16524 editor
16525 .snapshot(window, cx)
16526 .next_line_boundary(Point::new(0, 4));
16527 });
16528
16529 multi_buffer_editor.update(cx, |editor, cx| {
16530 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
16531 });
16532 assert_eq!(
16533 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16534 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
16535 "After unfolding the second buffer, its text should be displayed"
16536 );
16537
16538 // Typing inside of buffer 1 causes that buffer to be unfolded.
16539 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16540 assert_eq!(
16541 multi_buffer
16542 .read(cx)
16543 .snapshot(cx)
16544 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
16545 .collect::<String>(),
16546 "bbbb"
16547 );
16548 editor.change_selections(None, window, cx, |selections| {
16549 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
16550 });
16551 editor.handle_input("B", window, cx);
16552 });
16553
16554 assert_eq!(
16555 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16556 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
16557 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
16558 );
16559
16560 multi_buffer_editor.update(cx, |editor, cx| {
16561 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
16562 });
16563 assert_eq!(
16564 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16565 "\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",
16566 "After unfolding the all buffers, all original text should be displayed"
16567 );
16568}
16569
16570#[gpui::test]
16571async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
16572 init_test(cx, |_| {});
16573
16574 let sample_text_1 = "1111\n2222\n3333".to_string();
16575 let sample_text_2 = "4444\n5555\n6666".to_string();
16576 let sample_text_3 = "7777\n8888\n9999".to_string();
16577
16578 let fs = FakeFs::new(cx.executor());
16579 fs.insert_tree(
16580 path!("/a"),
16581 json!({
16582 "first.rs": sample_text_1,
16583 "second.rs": sample_text_2,
16584 "third.rs": sample_text_3,
16585 }),
16586 )
16587 .await;
16588 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16589 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16590 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16591 let worktree = project.update(cx, |project, cx| {
16592 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
16593 assert_eq!(worktrees.len(), 1);
16594 worktrees.pop().unwrap()
16595 });
16596 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
16597
16598 let buffer_1 = project
16599 .update(cx, |project, cx| {
16600 project.open_buffer((worktree_id, "first.rs"), cx)
16601 })
16602 .await
16603 .unwrap();
16604 let buffer_2 = project
16605 .update(cx, |project, cx| {
16606 project.open_buffer((worktree_id, "second.rs"), cx)
16607 })
16608 .await
16609 .unwrap();
16610 let buffer_3 = project
16611 .update(cx, |project, cx| {
16612 project.open_buffer((worktree_id, "third.rs"), cx)
16613 })
16614 .await
16615 .unwrap();
16616
16617 let multi_buffer = cx.new(|cx| {
16618 let mut multi_buffer = MultiBuffer::new(ReadWrite);
16619 multi_buffer.push_excerpts(
16620 buffer_1.clone(),
16621 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
16622 cx,
16623 );
16624 multi_buffer.push_excerpts(
16625 buffer_2.clone(),
16626 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
16627 cx,
16628 );
16629 multi_buffer.push_excerpts(
16630 buffer_3.clone(),
16631 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
16632 cx,
16633 );
16634 multi_buffer
16635 });
16636
16637 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16638 Editor::new(
16639 EditorMode::Full,
16640 multi_buffer,
16641 Some(project.clone()),
16642 window,
16643 cx,
16644 )
16645 });
16646
16647 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
16648 assert_eq!(
16649 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16650 full_text,
16651 );
16652
16653 multi_buffer_editor.update(cx, |editor, cx| {
16654 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
16655 });
16656 assert_eq!(
16657 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16658 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
16659 "After folding the first buffer, its text should not be displayed"
16660 );
16661
16662 multi_buffer_editor.update(cx, |editor, cx| {
16663 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
16664 });
16665
16666 assert_eq!(
16667 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16668 "\n\n\n\n\n\n7777\n8888\n9999",
16669 "After folding the second buffer, its text should not be displayed"
16670 );
16671
16672 multi_buffer_editor.update(cx, |editor, cx| {
16673 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
16674 });
16675 assert_eq!(
16676 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16677 "\n\n\n\n\n",
16678 "After folding the third buffer, its text should not be displayed"
16679 );
16680
16681 multi_buffer_editor.update(cx, |editor, cx| {
16682 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
16683 });
16684 assert_eq!(
16685 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16686 "\n\n\n\n4444\n5555\n6666\n\n",
16687 "After unfolding the second buffer, its text should be displayed"
16688 );
16689
16690 multi_buffer_editor.update(cx, |editor, cx| {
16691 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
16692 });
16693 assert_eq!(
16694 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16695 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
16696 "After unfolding the first buffer, its text should be displayed"
16697 );
16698
16699 multi_buffer_editor.update(cx, |editor, cx| {
16700 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
16701 });
16702 assert_eq!(
16703 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16704 full_text,
16705 "After unfolding all buffers, all original text should be displayed"
16706 );
16707}
16708
16709#[gpui::test]
16710async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
16711 init_test(cx, |_| {});
16712
16713 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
16714
16715 let fs = FakeFs::new(cx.executor());
16716 fs.insert_tree(
16717 path!("/a"),
16718 json!({
16719 "main.rs": sample_text,
16720 }),
16721 )
16722 .await;
16723 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16724 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16725 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16726 let worktree = project.update(cx, |project, cx| {
16727 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
16728 assert_eq!(worktrees.len(), 1);
16729 worktrees.pop().unwrap()
16730 });
16731 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
16732
16733 let buffer_1 = project
16734 .update(cx, |project, cx| {
16735 project.open_buffer((worktree_id, "main.rs"), cx)
16736 })
16737 .await
16738 .unwrap();
16739
16740 let multi_buffer = cx.new(|cx| {
16741 let mut multi_buffer = MultiBuffer::new(ReadWrite);
16742 multi_buffer.push_excerpts(
16743 buffer_1.clone(),
16744 [ExcerptRange::new(
16745 Point::new(0, 0)
16746 ..Point::new(
16747 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
16748 0,
16749 ),
16750 )],
16751 cx,
16752 );
16753 multi_buffer
16754 });
16755 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16756 Editor::new(
16757 EditorMode::Full,
16758 multi_buffer,
16759 Some(project.clone()),
16760 window,
16761 cx,
16762 )
16763 });
16764
16765 let selection_range = Point::new(1, 0)..Point::new(2, 0);
16766 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16767 enum TestHighlight {}
16768 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
16769 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
16770 editor.highlight_text::<TestHighlight>(
16771 vec![highlight_range.clone()],
16772 HighlightStyle::color(Hsla::green()),
16773 cx,
16774 );
16775 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
16776 });
16777
16778 let full_text = format!("\n\n{sample_text}");
16779 assert_eq!(
16780 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16781 full_text,
16782 );
16783}
16784
16785#[gpui::test]
16786async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
16787 init_test(cx, |_| {});
16788 cx.update(|cx| {
16789 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
16790 "keymaps/default-linux.json",
16791 cx,
16792 )
16793 .unwrap();
16794 cx.bind_keys(default_key_bindings);
16795 });
16796
16797 let (editor, cx) = cx.add_window_view(|window, cx| {
16798 let multi_buffer = MultiBuffer::build_multi(
16799 [
16800 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
16801 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
16802 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
16803 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
16804 ],
16805 cx,
16806 );
16807 let mut editor = Editor::new(EditorMode::Full, multi_buffer.clone(), None, window, cx);
16808
16809 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
16810 // fold all but the second buffer, so that we test navigating between two
16811 // adjacent folded buffers, as well as folded buffers at the start and
16812 // end the multibuffer
16813 editor.fold_buffer(buffer_ids[0], cx);
16814 editor.fold_buffer(buffer_ids[2], cx);
16815 editor.fold_buffer(buffer_ids[3], cx);
16816
16817 editor
16818 });
16819 cx.simulate_resize(size(px(1000.), px(1000.)));
16820
16821 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
16822 cx.assert_excerpts_with_selections(indoc! {"
16823 [EXCERPT]
16824 ˇ[FOLDED]
16825 [EXCERPT]
16826 a1
16827 b1
16828 [EXCERPT]
16829 [FOLDED]
16830 [EXCERPT]
16831 [FOLDED]
16832 "
16833 });
16834 cx.simulate_keystroke("down");
16835 cx.assert_excerpts_with_selections(indoc! {"
16836 [EXCERPT]
16837 [FOLDED]
16838 [EXCERPT]
16839 ˇa1
16840 b1
16841 [EXCERPT]
16842 [FOLDED]
16843 [EXCERPT]
16844 [FOLDED]
16845 "
16846 });
16847 cx.simulate_keystroke("down");
16848 cx.assert_excerpts_with_selections(indoc! {"
16849 [EXCERPT]
16850 [FOLDED]
16851 [EXCERPT]
16852 a1
16853 ˇb1
16854 [EXCERPT]
16855 [FOLDED]
16856 [EXCERPT]
16857 [FOLDED]
16858 "
16859 });
16860 cx.simulate_keystroke("down");
16861 cx.assert_excerpts_with_selections(indoc! {"
16862 [EXCERPT]
16863 [FOLDED]
16864 [EXCERPT]
16865 a1
16866 b1
16867 ˇ[EXCERPT]
16868 [FOLDED]
16869 [EXCERPT]
16870 [FOLDED]
16871 "
16872 });
16873 cx.simulate_keystroke("down");
16874 cx.assert_excerpts_with_selections(indoc! {"
16875 [EXCERPT]
16876 [FOLDED]
16877 [EXCERPT]
16878 a1
16879 b1
16880 [EXCERPT]
16881 ˇ[FOLDED]
16882 [EXCERPT]
16883 [FOLDED]
16884 "
16885 });
16886 for _ in 0..5 {
16887 cx.simulate_keystroke("down");
16888 cx.assert_excerpts_with_selections(indoc! {"
16889 [EXCERPT]
16890 [FOLDED]
16891 [EXCERPT]
16892 a1
16893 b1
16894 [EXCERPT]
16895 [FOLDED]
16896 [EXCERPT]
16897 ˇ[FOLDED]
16898 "
16899 });
16900 }
16901
16902 cx.simulate_keystroke("up");
16903 cx.assert_excerpts_with_selections(indoc! {"
16904 [EXCERPT]
16905 [FOLDED]
16906 [EXCERPT]
16907 a1
16908 b1
16909 [EXCERPT]
16910 ˇ[FOLDED]
16911 [EXCERPT]
16912 [FOLDED]
16913 "
16914 });
16915 cx.simulate_keystroke("up");
16916 cx.assert_excerpts_with_selections(indoc! {"
16917 [EXCERPT]
16918 [FOLDED]
16919 [EXCERPT]
16920 a1
16921 b1
16922 ˇ[EXCERPT]
16923 [FOLDED]
16924 [EXCERPT]
16925 [FOLDED]
16926 "
16927 });
16928 cx.simulate_keystroke("up");
16929 cx.assert_excerpts_with_selections(indoc! {"
16930 [EXCERPT]
16931 [FOLDED]
16932 [EXCERPT]
16933 a1
16934 ˇb1
16935 [EXCERPT]
16936 [FOLDED]
16937 [EXCERPT]
16938 [FOLDED]
16939 "
16940 });
16941 cx.simulate_keystroke("up");
16942 cx.assert_excerpts_with_selections(indoc! {"
16943 [EXCERPT]
16944 [FOLDED]
16945 [EXCERPT]
16946 ˇa1
16947 b1
16948 [EXCERPT]
16949 [FOLDED]
16950 [EXCERPT]
16951 [FOLDED]
16952 "
16953 });
16954 for _ in 0..5 {
16955 cx.simulate_keystroke("up");
16956 cx.assert_excerpts_with_selections(indoc! {"
16957 [EXCERPT]
16958 ˇ[FOLDED]
16959 [EXCERPT]
16960 a1
16961 b1
16962 [EXCERPT]
16963 [FOLDED]
16964 [EXCERPT]
16965 [FOLDED]
16966 "
16967 });
16968 }
16969}
16970
16971#[gpui::test]
16972async fn test_inline_completion_text(cx: &mut TestAppContext) {
16973 init_test(cx, |_| {});
16974
16975 // Simple insertion
16976 assert_highlighted_edits(
16977 "Hello, world!",
16978 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
16979 true,
16980 cx,
16981 |highlighted_edits, cx| {
16982 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
16983 assert_eq!(highlighted_edits.highlights.len(), 1);
16984 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
16985 assert_eq!(
16986 highlighted_edits.highlights[0].1.background_color,
16987 Some(cx.theme().status().created_background)
16988 );
16989 },
16990 )
16991 .await;
16992
16993 // Replacement
16994 assert_highlighted_edits(
16995 "This is a test.",
16996 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
16997 false,
16998 cx,
16999 |highlighted_edits, cx| {
17000 assert_eq!(highlighted_edits.text, "That is a test.");
17001 assert_eq!(highlighted_edits.highlights.len(), 1);
17002 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
17003 assert_eq!(
17004 highlighted_edits.highlights[0].1.background_color,
17005 Some(cx.theme().status().created_background)
17006 );
17007 },
17008 )
17009 .await;
17010
17011 // Multiple edits
17012 assert_highlighted_edits(
17013 "Hello, world!",
17014 vec![
17015 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
17016 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
17017 ],
17018 false,
17019 cx,
17020 |highlighted_edits, cx| {
17021 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
17022 assert_eq!(highlighted_edits.highlights.len(), 2);
17023 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
17024 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
17025 assert_eq!(
17026 highlighted_edits.highlights[0].1.background_color,
17027 Some(cx.theme().status().created_background)
17028 );
17029 assert_eq!(
17030 highlighted_edits.highlights[1].1.background_color,
17031 Some(cx.theme().status().created_background)
17032 );
17033 },
17034 )
17035 .await;
17036
17037 // Multiple lines with edits
17038 assert_highlighted_edits(
17039 "First line\nSecond line\nThird line\nFourth line",
17040 vec![
17041 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
17042 (
17043 Point::new(2, 0)..Point::new(2, 10),
17044 "New third line".to_string(),
17045 ),
17046 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
17047 ],
17048 false,
17049 cx,
17050 |highlighted_edits, cx| {
17051 assert_eq!(
17052 highlighted_edits.text,
17053 "Second modified\nNew third line\nFourth updated line"
17054 );
17055 assert_eq!(highlighted_edits.highlights.len(), 3);
17056 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
17057 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
17058 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
17059 for highlight in &highlighted_edits.highlights {
17060 assert_eq!(
17061 highlight.1.background_color,
17062 Some(cx.theme().status().created_background)
17063 );
17064 }
17065 },
17066 )
17067 .await;
17068}
17069
17070#[gpui::test]
17071async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
17072 init_test(cx, |_| {});
17073
17074 // Deletion
17075 assert_highlighted_edits(
17076 "Hello, world!",
17077 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
17078 true,
17079 cx,
17080 |highlighted_edits, cx| {
17081 assert_eq!(highlighted_edits.text, "Hello, world!");
17082 assert_eq!(highlighted_edits.highlights.len(), 1);
17083 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
17084 assert_eq!(
17085 highlighted_edits.highlights[0].1.background_color,
17086 Some(cx.theme().status().deleted_background)
17087 );
17088 },
17089 )
17090 .await;
17091
17092 // Insertion
17093 assert_highlighted_edits(
17094 "Hello, world!",
17095 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
17096 true,
17097 cx,
17098 |highlighted_edits, cx| {
17099 assert_eq!(highlighted_edits.highlights.len(), 1);
17100 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
17101 assert_eq!(
17102 highlighted_edits.highlights[0].1.background_color,
17103 Some(cx.theme().status().created_background)
17104 );
17105 },
17106 )
17107 .await;
17108}
17109
17110async fn assert_highlighted_edits(
17111 text: &str,
17112 edits: Vec<(Range<Point>, String)>,
17113 include_deletions: bool,
17114 cx: &mut TestAppContext,
17115 assertion_fn: impl Fn(HighlightedText, &App),
17116) {
17117 let window = cx.add_window(|window, cx| {
17118 let buffer = MultiBuffer::build_simple(text, cx);
17119 Editor::new(EditorMode::Full, buffer, None, window, cx)
17120 });
17121 let cx = &mut VisualTestContext::from_window(*window, cx);
17122
17123 let (buffer, snapshot) = window
17124 .update(cx, |editor, _window, cx| {
17125 (
17126 editor.buffer().clone(),
17127 editor.buffer().read(cx).snapshot(cx),
17128 )
17129 })
17130 .unwrap();
17131
17132 let edits = edits
17133 .into_iter()
17134 .map(|(range, edit)| {
17135 (
17136 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
17137 edit,
17138 )
17139 })
17140 .collect::<Vec<_>>();
17141
17142 let text_anchor_edits = edits
17143 .clone()
17144 .into_iter()
17145 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
17146 .collect::<Vec<_>>();
17147
17148 let edit_preview = window
17149 .update(cx, |_, _window, cx| {
17150 buffer
17151 .read(cx)
17152 .as_singleton()
17153 .unwrap()
17154 .read(cx)
17155 .preview_edits(text_anchor_edits.into(), cx)
17156 })
17157 .unwrap()
17158 .await;
17159
17160 cx.update(|_window, cx| {
17161 let highlighted_edits = inline_completion_edit_text(
17162 &snapshot.as_singleton().unwrap().2,
17163 &edits,
17164 &edit_preview,
17165 include_deletions,
17166 cx,
17167 );
17168 assertion_fn(highlighted_edits, cx)
17169 });
17170}
17171
17172#[track_caller]
17173fn assert_breakpoint(
17174 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
17175 path: &Arc<Path>,
17176 expected: Vec<(u32, Breakpoint)>,
17177) {
17178 if expected.len() == 0usize {
17179 assert!(!breakpoints.contains_key(path), "{}", path.display());
17180 } else {
17181 let mut breakpoint = breakpoints
17182 .get(path)
17183 .unwrap()
17184 .into_iter()
17185 .map(|breakpoint| {
17186 (
17187 breakpoint.row,
17188 Breakpoint {
17189 message: breakpoint.message.clone(),
17190 state: breakpoint.state,
17191 condition: breakpoint.condition.clone(),
17192 hit_condition: breakpoint.hit_condition.clone(),
17193 },
17194 )
17195 })
17196 .collect::<Vec<_>>();
17197
17198 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
17199
17200 assert_eq!(expected, breakpoint);
17201 }
17202}
17203
17204fn add_log_breakpoint_at_cursor(
17205 editor: &mut Editor,
17206 log_message: &str,
17207 window: &mut Window,
17208 cx: &mut Context<Editor>,
17209) {
17210 let (anchor, bp) = editor
17211 .breakpoint_at_cursor_head(window, cx)
17212 .unwrap_or_else(|| {
17213 let cursor_position: Point = editor.selections.newest(cx).head();
17214
17215 let breakpoint_position = editor
17216 .snapshot(window, cx)
17217 .display_snapshot
17218 .buffer_snapshot
17219 .anchor_before(Point::new(cursor_position.row, 0));
17220
17221 (breakpoint_position, Breakpoint::new_log(&log_message))
17222 });
17223
17224 editor.edit_breakpoint_at_anchor(
17225 anchor,
17226 bp,
17227 BreakpointEditAction::EditLogMessage(log_message.into()),
17228 cx,
17229 );
17230}
17231
17232#[gpui::test]
17233async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
17234 init_test(cx, |_| {});
17235
17236 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
17237 let fs = FakeFs::new(cx.executor());
17238 fs.insert_tree(
17239 path!("/a"),
17240 json!({
17241 "main.rs": sample_text,
17242 }),
17243 )
17244 .await;
17245 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17246 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17247 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17248
17249 let fs = FakeFs::new(cx.executor());
17250 fs.insert_tree(
17251 path!("/a"),
17252 json!({
17253 "main.rs": sample_text,
17254 }),
17255 )
17256 .await;
17257 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17258 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17259 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17260 let worktree_id = workspace
17261 .update(cx, |workspace, _window, cx| {
17262 workspace.project().update(cx, |project, cx| {
17263 project.worktrees(cx).next().unwrap().read(cx).id()
17264 })
17265 })
17266 .unwrap();
17267
17268 let buffer = project
17269 .update(cx, |project, cx| {
17270 project.open_buffer((worktree_id, "main.rs"), cx)
17271 })
17272 .await
17273 .unwrap();
17274
17275 let (editor, cx) = cx.add_window_view(|window, cx| {
17276 Editor::new(
17277 EditorMode::Full,
17278 MultiBuffer::build_from_buffer(buffer, cx),
17279 Some(project.clone()),
17280 window,
17281 cx,
17282 )
17283 });
17284
17285 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
17286 let abs_path = project.read_with(cx, |project, cx| {
17287 project
17288 .absolute_path(&project_path, cx)
17289 .map(|path_buf| Arc::from(path_buf.to_owned()))
17290 .unwrap()
17291 });
17292
17293 // assert we can add breakpoint on the first line
17294 editor.update_in(cx, |editor, window, cx| {
17295 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17296 editor.move_to_end(&MoveToEnd, window, cx);
17297 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17298 });
17299
17300 let breakpoints = editor.update(cx, |editor, cx| {
17301 editor
17302 .breakpoint_store()
17303 .as_ref()
17304 .unwrap()
17305 .read(cx)
17306 .all_breakpoints(cx)
17307 .clone()
17308 });
17309
17310 assert_eq!(1, breakpoints.len());
17311 assert_breakpoint(
17312 &breakpoints,
17313 &abs_path,
17314 vec![
17315 (0, Breakpoint::new_standard()),
17316 (3, Breakpoint::new_standard()),
17317 ],
17318 );
17319
17320 editor.update_in(cx, |editor, window, cx| {
17321 editor.move_to_beginning(&MoveToBeginning, window, cx);
17322 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17323 });
17324
17325 let breakpoints = editor.update(cx, |editor, cx| {
17326 editor
17327 .breakpoint_store()
17328 .as_ref()
17329 .unwrap()
17330 .read(cx)
17331 .all_breakpoints(cx)
17332 .clone()
17333 });
17334
17335 assert_eq!(1, breakpoints.len());
17336 assert_breakpoint(
17337 &breakpoints,
17338 &abs_path,
17339 vec![(3, Breakpoint::new_standard())],
17340 );
17341
17342 editor.update_in(cx, |editor, window, cx| {
17343 editor.move_to_end(&MoveToEnd, window, cx);
17344 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17345 });
17346
17347 let breakpoints = editor.update(cx, |editor, cx| {
17348 editor
17349 .breakpoint_store()
17350 .as_ref()
17351 .unwrap()
17352 .read(cx)
17353 .all_breakpoints(cx)
17354 .clone()
17355 });
17356
17357 assert_eq!(0, breakpoints.len());
17358 assert_breakpoint(&breakpoints, &abs_path, vec![]);
17359}
17360
17361#[gpui::test]
17362async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
17363 init_test(cx, |_| {});
17364
17365 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
17366
17367 let fs = FakeFs::new(cx.executor());
17368 fs.insert_tree(
17369 path!("/a"),
17370 json!({
17371 "main.rs": sample_text,
17372 }),
17373 )
17374 .await;
17375 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17376 let (workspace, cx) =
17377 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
17378
17379 let worktree_id = workspace.update(cx, |workspace, cx| {
17380 workspace.project().update(cx, |project, cx| {
17381 project.worktrees(cx).next().unwrap().read(cx).id()
17382 })
17383 });
17384
17385 let buffer = project
17386 .update(cx, |project, cx| {
17387 project.open_buffer((worktree_id, "main.rs"), cx)
17388 })
17389 .await
17390 .unwrap();
17391
17392 let (editor, cx) = cx.add_window_view(|window, cx| {
17393 Editor::new(
17394 EditorMode::Full,
17395 MultiBuffer::build_from_buffer(buffer, cx),
17396 Some(project.clone()),
17397 window,
17398 cx,
17399 )
17400 });
17401
17402 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
17403 let abs_path = project.read_with(cx, |project, cx| {
17404 project
17405 .absolute_path(&project_path, cx)
17406 .map(|path_buf| Arc::from(path_buf.to_owned()))
17407 .unwrap()
17408 });
17409
17410 editor.update_in(cx, |editor, window, cx| {
17411 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
17412 });
17413
17414 let breakpoints = editor.update(cx, |editor, cx| {
17415 editor
17416 .breakpoint_store()
17417 .as_ref()
17418 .unwrap()
17419 .read(cx)
17420 .all_breakpoints(cx)
17421 .clone()
17422 });
17423
17424 assert_breakpoint(
17425 &breakpoints,
17426 &abs_path,
17427 vec![(0, Breakpoint::new_log("hello world"))],
17428 );
17429
17430 // Removing a log message from a log breakpoint should remove it
17431 editor.update_in(cx, |editor, window, cx| {
17432 add_log_breakpoint_at_cursor(editor, "", window, cx);
17433 });
17434
17435 let breakpoints = editor.update(cx, |editor, cx| {
17436 editor
17437 .breakpoint_store()
17438 .as_ref()
17439 .unwrap()
17440 .read(cx)
17441 .all_breakpoints(cx)
17442 .clone()
17443 });
17444
17445 assert_breakpoint(&breakpoints, &abs_path, vec![]);
17446
17447 editor.update_in(cx, |editor, window, cx| {
17448 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17449 editor.move_to_end(&MoveToEnd, window, cx);
17450 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17451 // Not adding a log message to a standard breakpoint shouldn't remove it
17452 add_log_breakpoint_at_cursor(editor, "", window, cx);
17453 });
17454
17455 let breakpoints = editor.update(cx, |editor, cx| {
17456 editor
17457 .breakpoint_store()
17458 .as_ref()
17459 .unwrap()
17460 .read(cx)
17461 .all_breakpoints(cx)
17462 .clone()
17463 });
17464
17465 assert_breakpoint(
17466 &breakpoints,
17467 &abs_path,
17468 vec![
17469 (0, Breakpoint::new_standard()),
17470 (3, Breakpoint::new_standard()),
17471 ],
17472 );
17473
17474 editor.update_in(cx, |editor, window, cx| {
17475 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
17476 });
17477
17478 let breakpoints = editor.update(cx, |editor, cx| {
17479 editor
17480 .breakpoint_store()
17481 .as_ref()
17482 .unwrap()
17483 .read(cx)
17484 .all_breakpoints(cx)
17485 .clone()
17486 });
17487
17488 assert_breakpoint(
17489 &breakpoints,
17490 &abs_path,
17491 vec![
17492 (0, Breakpoint::new_standard()),
17493 (3, Breakpoint::new_log("hello world")),
17494 ],
17495 );
17496
17497 editor.update_in(cx, |editor, window, cx| {
17498 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
17499 });
17500
17501 let breakpoints = editor.update(cx, |editor, cx| {
17502 editor
17503 .breakpoint_store()
17504 .as_ref()
17505 .unwrap()
17506 .read(cx)
17507 .all_breakpoints(cx)
17508 .clone()
17509 });
17510
17511 assert_breakpoint(
17512 &breakpoints,
17513 &abs_path,
17514 vec![
17515 (0, Breakpoint::new_standard()),
17516 (3, Breakpoint::new_log("hello Earth!!")),
17517 ],
17518 );
17519}
17520
17521/// This also tests that Editor::breakpoint_at_cursor_head is working properly
17522/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
17523/// or when breakpoints were placed out of order. This tests for a regression too
17524#[gpui::test]
17525async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
17526 init_test(cx, |_| {});
17527
17528 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
17529 let fs = FakeFs::new(cx.executor());
17530 fs.insert_tree(
17531 path!("/a"),
17532 json!({
17533 "main.rs": sample_text,
17534 }),
17535 )
17536 .await;
17537 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17538 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17539 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17540
17541 let fs = FakeFs::new(cx.executor());
17542 fs.insert_tree(
17543 path!("/a"),
17544 json!({
17545 "main.rs": sample_text,
17546 }),
17547 )
17548 .await;
17549 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17550 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17551 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17552 let worktree_id = workspace
17553 .update(cx, |workspace, _window, cx| {
17554 workspace.project().update(cx, |project, cx| {
17555 project.worktrees(cx).next().unwrap().read(cx).id()
17556 })
17557 })
17558 .unwrap();
17559
17560 let buffer = project
17561 .update(cx, |project, cx| {
17562 project.open_buffer((worktree_id, "main.rs"), cx)
17563 })
17564 .await
17565 .unwrap();
17566
17567 let (editor, cx) = cx.add_window_view(|window, cx| {
17568 Editor::new(
17569 EditorMode::Full,
17570 MultiBuffer::build_from_buffer(buffer, cx),
17571 Some(project.clone()),
17572 window,
17573 cx,
17574 )
17575 });
17576
17577 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
17578 let abs_path = project.read_with(cx, |project, cx| {
17579 project
17580 .absolute_path(&project_path, cx)
17581 .map(|path_buf| Arc::from(path_buf.to_owned()))
17582 .unwrap()
17583 });
17584
17585 // assert we can add breakpoint on the first line
17586 editor.update_in(cx, |editor, window, cx| {
17587 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17588 editor.move_to_end(&MoveToEnd, window, cx);
17589 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17590 editor.move_up(&MoveUp, window, cx);
17591 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17592 });
17593
17594 let breakpoints = editor.update(cx, |editor, cx| {
17595 editor
17596 .breakpoint_store()
17597 .as_ref()
17598 .unwrap()
17599 .read(cx)
17600 .all_breakpoints(cx)
17601 .clone()
17602 });
17603
17604 assert_eq!(1, breakpoints.len());
17605 assert_breakpoint(
17606 &breakpoints,
17607 &abs_path,
17608 vec![
17609 (0, Breakpoint::new_standard()),
17610 (2, Breakpoint::new_standard()),
17611 (3, Breakpoint::new_standard()),
17612 ],
17613 );
17614
17615 editor.update_in(cx, |editor, window, cx| {
17616 editor.move_to_beginning(&MoveToBeginning, window, cx);
17617 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
17618 editor.move_to_end(&MoveToEnd, window, cx);
17619 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
17620 // Disabling a breakpoint that doesn't exist should do nothing
17621 editor.move_up(&MoveUp, window, cx);
17622 editor.move_up(&MoveUp, window, cx);
17623 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
17624 });
17625
17626 let breakpoints = editor.update(cx, |editor, cx| {
17627 editor
17628 .breakpoint_store()
17629 .as_ref()
17630 .unwrap()
17631 .read(cx)
17632 .all_breakpoints(cx)
17633 .clone()
17634 });
17635
17636 let disable_breakpoint = {
17637 let mut bp = Breakpoint::new_standard();
17638 bp.state = BreakpointState::Disabled;
17639 bp
17640 };
17641
17642 assert_eq!(1, breakpoints.len());
17643 assert_breakpoint(
17644 &breakpoints,
17645 &abs_path,
17646 vec![
17647 (0, disable_breakpoint.clone()),
17648 (2, Breakpoint::new_standard()),
17649 (3, disable_breakpoint.clone()),
17650 ],
17651 );
17652
17653 editor.update_in(cx, |editor, window, cx| {
17654 editor.move_to_beginning(&MoveToBeginning, window, cx);
17655 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
17656 editor.move_to_end(&MoveToEnd, window, cx);
17657 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
17658 editor.move_up(&MoveUp, window, cx);
17659 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
17660 });
17661
17662 let breakpoints = editor.update(cx, |editor, cx| {
17663 editor
17664 .breakpoint_store()
17665 .as_ref()
17666 .unwrap()
17667 .read(cx)
17668 .all_breakpoints(cx)
17669 .clone()
17670 });
17671
17672 assert_eq!(1, breakpoints.len());
17673 assert_breakpoint(
17674 &breakpoints,
17675 &abs_path,
17676 vec![
17677 (0, Breakpoint::new_standard()),
17678 (2, disable_breakpoint),
17679 (3, Breakpoint::new_standard()),
17680 ],
17681 );
17682}
17683
17684#[gpui::test]
17685async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
17686 init_test(cx, |_| {});
17687 let capabilities = lsp::ServerCapabilities {
17688 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
17689 prepare_provider: Some(true),
17690 work_done_progress_options: Default::default(),
17691 })),
17692 ..Default::default()
17693 };
17694 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
17695
17696 cx.set_state(indoc! {"
17697 struct Fˇoo {}
17698 "});
17699
17700 cx.update_editor(|editor, _, cx| {
17701 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
17702 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
17703 editor.highlight_background::<DocumentHighlightRead>(
17704 &[highlight_range],
17705 |c| c.editor_document_highlight_read_background,
17706 cx,
17707 );
17708 });
17709
17710 let mut prepare_rename_handler = cx
17711 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
17712 move |_, _, _| async move {
17713 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
17714 start: lsp::Position {
17715 line: 0,
17716 character: 7,
17717 },
17718 end: lsp::Position {
17719 line: 0,
17720 character: 10,
17721 },
17722 })))
17723 },
17724 );
17725 let prepare_rename_task = cx
17726 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
17727 .expect("Prepare rename was not started");
17728 prepare_rename_handler.next().await.unwrap();
17729 prepare_rename_task.await.expect("Prepare rename failed");
17730
17731 let mut rename_handler =
17732 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
17733 let edit = lsp::TextEdit {
17734 range: lsp::Range {
17735 start: lsp::Position {
17736 line: 0,
17737 character: 7,
17738 },
17739 end: lsp::Position {
17740 line: 0,
17741 character: 10,
17742 },
17743 },
17744 new_text: "FooRenamed".to_string(),
17745 };
17746 Ok(Some(lsp::WorkspaceEdit::new(
17747 // Specify the same edit twice
17748 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
17749 )))
17750 });
17751 let rename_task = cx
17752 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
17753 .expect("Confirm rename was not started");
17754 rename_handler.next().await.unwrap();
17755 rename_task.await.expect("Confirm rename failed");
17756 cx.run_until_parked();
17757
17758 // Despite two edits, only one is actually applied as those are identical
17759 cx.assert_editor_state(indoc! {"
17760 struct FooRenamedˇ {}
17761 "});
17762}
17763
17764#[gpui::test]
17765async fn test_rename_without_prepare(cx: &mut TestAppContext) {
17766 init_test(cx, |_| {});
17767 // These capabilities indicate that the server does not support prepare rename.
17768 let capabilities = lsp::ServerCapabilities {
17769 rename_provider: Some(lsp::OneOf::Left(true)),
17770 ..Default::default()
17771 };
17772 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
17773
17774 cx.set_state(indoc! {"
17775 struct Fˇoo {}
17776 "});
17777
17778 cx.update_editor(|editor, _window, cx| {
17779 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
17780 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
17781 editor.highlight_background::<DocumentHighlightRead>(
17782 &[highlight_range],
17783 |c| c.editor_document_highlight_read_background,
17784 cx,
17785 );
17786 });
17787
17788 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
17789 .expect("Prepare rename was not started")
17790 .await
17791 .expect("Prepare rename failed");
17792
17793 let mut rename_handler =
17794 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
17795 let edit = lsp::TextEdit {
17796 range: lsp::Range {
17797 start: lsp::Position {
17798 line: 0,
17799 character: 7,
17800 },
17801 end: lsp::Position {
17802 line: 0,
17803 character: 10,
17804 },
17805 },
17806 new_text: "FooRenamed".to_string(),
17807 };
17808 Ok(Some(lsp::WorkspaceEdit::new(
17809 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
17810 )))
17811 });
17812 let rename_task = cx
17813 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
17814 .expect("Confirm rename was not started");
17815 rename_handler.next().await.unwrap();
17816 rename_task.await.expect("Confirm rename failed");
17817 cx.run_until_parked();
17818
17819 // Correct range is renamed, as `surrounding_word` is used to find it.
17820 cx.assert_editor_state(indoc! {"
17821 struct FooRenamedˇ {}
17822 "});
17823}
17824
17825#[gpui::test]
17826async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
17827 init_test(cx, |_| {});
17828 let mut cx = EditorTestContext::new(cx).await;
17829
17830 let language = Arc::new(
17831 Language::new(
17832 LanguageConfig::default(),
17833 Some(tree_sitter_html::LANGUAGE.into()),
17834 )
17835 .with_brackets_query(
17836 r#"
17837 ("<" @open "/>" @close)
17838 ("</" @open ">" @close)
17839 ("<" @open ">" @close)
17840 ("\"" @open "\"" @close)
17841 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
17842 "#,
17843 )
17844 .unwrap(),
17845 );
17846 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
17847
17848 cx.set_state(indoc! {"
17849 <span>ˇ</span>
17850 "});
17851 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
17852 cx.assert_editor_state(indoc! {"
17853 <span>
17854 ˇ
17855 </span>
17856 "});
17857
17858 cx.set_state(indoc! {"
17859 <span><span></span>ˇ</span>
17860 "});
17861 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
17862 cx.assert_editor_state(indoc! {"
17863 <span><span></span>
17864 ˇ</span>
17865 "});
17866
17867 cx.set_state(indoc! {"
17868 <span>ˇ
17869 </span>
17870 "});
17871 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
17872 cx.assert_editor_state(indoc! {"
17873 <span>
17874 ˇ
17875 </span>
17876 "});
17877}
17878
17879#[gpui::test(iterations = 10)]
17880async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
17881 init_test(cx, |_| {});
17882
17883 let fs = FakeFs::new(cx.executor());
17884 fs.insert_tree(
17885 path!("/dir"),
17886 json!({
17887 "a.ts": "a",
17888 }),
17889 )
17890 .await;
17891
17892 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
17893 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17894 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17895
17896 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17897 language_registry.add(Arc::new(Language::new(
17898 LanguageConfig {
17899 name: "TypeScript".into(),
17900 matcher: LanguageMatcher {
17901 path_suffixes: vec!["ts".to_string()],
17902 ..Default::default()
17903 },
17904 ..Default::default()
17905 },
17906 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
17907 )));
17908 let mut fake_language_servers = language_registry.register_fake_lsp(
17909 "TypeScript",
17910 FakeLspAdapter {
17911 capabilities: lsp::ServerCapabilities {
17912 code_lens_provider: Some(lsp::CodeLensOptions {
17913 resolve_provider: Some(true),
17914 }),
17915 execute_command_provider: Some(lsp::ExecuteCommandOptions {
17916 commands: vec!["_the/command".to_string()],
17917 ..lsp::ExecuteCommandOptions::default()
17918 }),
17919 ..lsp::ServerCapabilities::default()
17920 },
17921 ..FakeLspAdapter::default()
17922 },
17923 );
17924
17925 let (buffer, _handle) = project
17926 .update(cx, |p, cx| {
17927 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
17928 })
17929 .await
17930 .unwrap();
17931 cx.executor().run_until_parked();
17932
17933 let fake_server = fake_language_servers.next().await.unwrap();
17934
17935 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
17936 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
17937 drop(buffer_snapshot);
17938 let actions = cx
17939 .update_window(*workspace, |_, window, cx| {
17940 project.code_actions(&buffer, anchor..anchor, window, cx)
17941 })
17942 .unwrap();
17943
17944 fake_server
17945 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
17946 Ok(Some(vec![
17947 lsp::CodeLens {
17948 range: lsp::Range::default(),
17949 command: Some(lsp::Command {
17950 title: "Code lens command".to_owned(),
17951 command: "_the/command".to_owned(),
17952 arguments: None,
17953 }),
17954 data: None,
17955 },
17956 lsp::CodeLens {
17957 range: lsp::Range::default(),
17958 command: Some(lsp::Command {
17959 title: "Command not in capabilities".to_owned(),
17960 command: "not in capabilities".to_owned(),
17961 arguments: None,
17962 }),
17963 data: None,
17964 },
17965 lsp::CodeLens {
17966 range: lsp::Range {
17967 start: lsp::Position {
17968 line: 1,
17969 character: 1,
17970 },
17971 end: lsp::Position {
17972 line: 1,
17973 character: 1,
17974 },
17975 },
17976 command: Some(lsp::Command {
17977 title: "Command not in range".to_owned(),
17978 command: "_the/command".to_owned(),
17979 arguments: None,
17980 }),
17981 data: None,
17982 },
17983 ]))
17984 })
17985 .next()
17986 .await;
17987
17988 let actions = actions.await.unwrap();
17989 assert_eq!(
17990 actions.len(),
17991 1,
17992 "Should have only one valid action for the 0..0 range"
17993 );
17994 let action = actions[0].clone();
17995 let apply = project.update(cx, |project, cx| {
17996 project.apply_code_action(buffer.clone(), action, true, cx)
17997 });
17998
17999 // Resolving the code action does not populate its edits. In absence of
18000 // edits, we must execute the given command.
18001 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
18002 |mut lens, _| async move {
18003 let lens_command = lens.command.as_mut().expect("should have a command");
18004 assert_eq!(lens_command.title, "Code lens command");
18005 lens_command.arguments = Some(vec![json!("the-argument")]);
18006 Ok(lens)
18007 },
18008 );
18009
18010 // While executing the command, the language server sends the editor
18011 // a `workspaceEdit` request.
18012 fake_server
18013 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
18014 let fake = fake_server.clone();
18015 move |params, _| {
18016 assert_eq!(params.command, "_the/command");
18017 let fake = fake.clone();
18018 async move {
18019 fake.server
18020 .request::<lsp::request::ApplyWorkspaceEdit>(
18021 lsp::ApplyWorkspaceEditParams {
18022 label: None,
18023 edit: lsp::WorkspaceEdit {
18024 changes: Some(
18025 [(
18026 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
18027 vec![lsp::TextEdit {
18028 range: lsp::Range::new(
18029 lsp::Position::new(0, 0),
18030 lsp::Position::new(0, 0),
18031 ),
18032 new_text: "X".into(),
18033 }],
18034 )]
18035 .into_iter()
18036 .collect(),
18037 ),
18038 ..Default::default()
18039 },
18040 },
18041 )
18042 .await
18043 .unwrap();
18044 Ok(Some(json!(null)))
18045 }
18046 }
18047 })
18048 .next()
18049 .await;
18050
18051 // Applying the code lens command returns a project transaction containing the edits
18052 // sent by the language server in its `workspaceEdit` request.
18053 let transaction = apply.await.unwrap();
18054 assert!(transaction.0.contains_key(&buffer));
18055 buffer.update(cx, |buffer, cx| {
18056 assert_eq!(buffer.text(), "Xa");
18057 buffer.undo(cx);
18058 assert_eq!(buffer.text(), "a");
18059 });
18060}
18061
18062#[gpui::test]
18063async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
18064 init_test(cx, |_| {});
18065
18066 let fs = FakeFs::new(cx.executor());
18067 let main_text = r#"fn main() {
18068println!("1");
18069println!("2");
18070println!("3");
18071println!("4");
18072println!("5");
18073}"#;
18074 let lib_text = "mod foo {}";
18075 fs.insert_tree(
18076 path!("/a"),
18077 json!({
18078 "lib.rs": lib_text,
18079 "main.rs": main_text,
18080 }),
18081 )
18082 .await;
18083
18084 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18085 let (workspace, cx) =
18086 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
18087 let worktree_id = workspace.update(cx, |workspace, cx| {
18088 workspace.project().update(cx, |project, cx| {
18089 project.worktrees(cx).next().unwrap().read(cx).id()
18090 })
18091 });
18092
18093 let expected_ranges = vec![
18094 Point::new(0, 0)..Point::new(0, 0),
18095 Point::new(1, 0)..Point::new(1, 1),
18096 Point::new(2, 0)..Point::new(2, 2),
18097 Point::new(3, 0)..Point::new(3, 3),
18098 ];
18099
18100 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
18101 let editor_1 = workspace
18102 .update_in(cx, |workspace, window, cx| {
18103 workspace.open_path(
18104 (worktree_id, "main.rs"),
18105 Some(pane_1.downgrade()),
18106 true,
18107 window,
18108 cx,
18109 )
18110 })
18111 .unwrap()
18112 .await
18113 .downcast::<Editor>()
18114 .unwrap();
18115 pane_1.update(cx, |pane, cx| {
18116 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18117 open_editor.update(cx, |editor, cx| {
18118 assert_eq!(
18119 editor.display_text(cx),
18120 main_text,
18121 "Original main.rs text on initial open",
18122 );
18123 assert_eq!(
18124 editor
18125 .selections
18126 .all::<Point>(cx)
18127 .into_iter()
18128 .map(|s| s.range())
18129 .collect::<Vec<_>>(),
18130 vec![Point::zero()..Point::zero()],
18131 "Default selections on initial open",
18132 );
18133 })
18134 });
18135 editor_1.update_in(cx, |editor, window, cx| {
18136 editor.change_selections(None, window, cx, |s| {
18137 s.select_ranges(expected_ranges.clone());
18138 });
18139 });
18140
18141 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
18142 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
18143 });
18144 let editor_2 = workspace
18145 .update_in(cx, |workspace, window, cx| {
18146 workspace.open_path(
18147 (worktree_id, "main.rs"),
18148 Some(pane_2.downgrade()),
18149 true,
18150 window,
18151 cx,
18152 )
18153 })
18154 .unwrap()
18155 .await
18156 .downcast::<Editor>()
18157 .unwrap();
18158 pane_2.update(cx, |pane, cx| {
18159 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18160 open_editor.update(cx, |editor, cx| {
18161 assert_eq!(
18162 editor.display_text(cx),
18163 main_text,
18164 "Original main.rs text on initial open in another panel",
18165 );
18166 assert_eq!(
18167 editor
18168 .selections
18169 .all::<Point>(cx)
18170 .into_iter()
18171 .map(|s| s.range())
18172 .collect::<Vec<_>>(),
18173 vec![Point::zero()..Point::zero()],
18174 "Default selections on initial open in another panel",
18175 );
18176 })
18177 });
18178
18179 editor_2.update_in(cx, |editor, window, cx| {
18180 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
18181 });
18182
18183 let _other_editor_1 = workspace
18184 .update_in(cx, |workspace, window, cx| {
18185 workspace.open_path(
18186 (worktree_id, "lib.rs"),
18187 Some(pane_1.downgrade()),
18188 true,
18189 window,
18190 cx,
18191 )
18192 })
18193 .unwrap()
18194 .await
18195 .downcast::<Editor>()
18196 .unwrap();
18197 pane_1
18198 .update_in(cx, |pane, window, cx| {
18199 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
18200 .unwrap()
18201 })
18202 .await
18203 .unwrap();
18204 drop(editor_1);
18205 pane_1.update(cx, |pane, cx| {
18206 pane.active_item()
18207 .unwrap()
18208 .downcast::<Editor>()
18209 .unwrap()
18210 .update(cx, |editor, cx| {
18211 assert_eq!(
18212 editor.display_text(cx),
18213 lib_text,
18214 "Other file should be open and active",
18215 );
18216 });
18217 assert_eq!(pane.items().count(), 1, "No other editors should be open");
18218 });
18219
18220 let _other_editor_2 = workspace
18221 .update_in(cx, |workspace, window, cx| {
18222 workspace.open_path(
18223 (worktree_id, "lib.rs"),
18224 Some(pane_2.downgrade()),
18225 true,
18226 window,
18227 cx,
18228 )
18229 })
18230 .unwrap()
18231 .await
18232 .downcast::<Editor>()
18233 .unwrap();
18234 pane_2
18235 .update_in(cx, |pane, window, cx| {
18236 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
18237 .unwrap()
18238 })
18239 .await
18240 .unwrap();
18241 drop(editor_2);
18242 pane_2.update(cx, |pane, cx| {
18243 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18244 open_editor.update(cx, |editor, cx| {
18245 assert_eq!(
18246 editor.display_text(cx),
18247 lib_text,
18248 "Other file should be open and active in another panel too",
18249 );
18250 });
18251 assert_eq!(
18252 pane.items().count(),
18253 1,
18254 "No other editors should be open in another pane",
18255 );
18256 });
18257
18258 let _editor_1_reopened = workspace
18259 .update_in(cx, |workspace, window, cx| {
18260 workspace.open_path(
18261 (worktree_id, "main.rs"),
18262 Some(pane_1.downgrade()),
18263 true,
18264 window,
18265 cx,
18266 )
18267 })
18268 .unwrap()
18269 .await
18270 .downcast::<Editor>()
18271 .unwrap();
18272 let _editor_2_reopened = workspace
18273 .update_in(cx, |workspace, window, cx| {
18274 workspace.open_path(
18275 (worktree_id, "main.rs"),
18276 Some(pane_2.downgrade()),
18277 true,
18278 window,
18279 cx,
18280 )
18281 })
18282 .unwrap()
18283 .await
18284 .downcast::<Editor>()
18285 .unwrap();
18286 pane_1.update(cx, |pane, cx| {
18287 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18288 open_editor.update(cx, |editor, cx| {
18289 assert_eq!(
18290 editor.display_text(cx),
18291 main_text,
18292 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
18293 );
18294 assert_eq!(
18295 editor
18296 .selections
18297 .all::<Point>(cx)
18298 .into_iter()
18299 .map(|s| s.range())
18300 .collect::<Vec<_>>(),
18301 expected_ranges,
18302 "Previous editor in the 1st panel had selections and should get them restored on reopen",
18303 );
18304 })
18305 });
18306 pane_2.update(cx, |pane, cx| {
18307 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18308 open_editor.update(cx, |editor, cx| {
18309 assert_eq!(
18310 editor.display_text(cx),
18311 r#"fn main() {
18312⋯rintln!("1");
18313⋯intln!("2");
18314⋯ntln!("3");
18315println!("4");
18316println!("5");
18317}"#,
18318 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
18319 );
18320 assert_eq!(
18321 editor
18322 .selections
18323 .all::<Point>(cx)
18324 .into_iter()
18325 .map(|s| s.range())
18326 .collect::<Vec<_>>(),
18327 vec![Point::zero()..Point::zero()],
18328 "Previous editor in the 2nd pane had no selections changed hence should restore none",
18329 );
18330 })
18331 });
18332}
18333
18334#[gpui::test]
18335async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
18336 init_test(cx, |_| {});
18337
18338 let fs = FakeFs::new(cx.executor());
18339 let main_text = r#"fn main() {
18340println!("1");
18341println!("2");
18342println!("3");
18343println!("4");
18344println!("5");
18345}"#;
18346 let lib_text = "mod foo {}";
18347 fs.insert_tree(
18348 path!("/a"),
18349 json!({
18350 "lib.rs": lib_text,
18351 "main.rs": main_text,
18352 }),
18353 )
18354 .await;
18355
18356 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18357 let (workspace, cx) =
18358 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
18359 let worktree_id = workspace.update(cx, |workspace, cx| {
18360 workspace.project().update(cx, |project, cx| {
18361 project.worktrees(cx).next().unwrap().read(cx).id()
18362 })
18363 });
18364
18365 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
18366 let editor = workspace
18367 .update_in(cx, |workspace, window, cx| {
18368 workspace.open_path(
18369 (worktree_id, "main.rs"),
18370 Some(pane.downgrade()),
18371 true,
18372 window,
18373 cx,
18374 )
18375 })
18376 .unwrap()
18377 .await
18378 .downcast::<Editor>()
18379 .unwrap();
18380 pane.update(cx, |pane, cx| {
18381 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18382 open_editor.update(cx, |editor, cx| {
18383 assert_eq!(
18384 editor.display_text(cx),
18385 main_text,
18386 "Original main.rs text on initial open",
18387 );
18388 })
18389 });
18390 editor.update_in(cx, |editor, window, cx| {
18391 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
18392 });
18393
18394 cx.update_global(|store: &mut SettingsStore, cx| {
18395 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
18396 s.restore_on_file_reopen = Some(false);
18397 });
18398 });
18399 editor.update_in(cx, |editor, window, cx| {
18400 editor.fold_ranges(
18401 vec![
18402 Point::new(1, 0)..Point::new(1, 1),
18403 Point::new(2, 0)..Point::new(2, 2),
18404 Point::new(3, 0)..Point::new(3, 3),
18405 ],
18406 false,
18407 window,
18408 cx,
18409 );
18410 });
18411 pane.update_in(cx, |pane, window, cx| {
18412 pane.close_all_items(&CloseAllItems::default(), window, cx)
18413 .unwrap()
18414 })
18415 .await
18416 .unwrap();
18417 pane.update(cx, |pane, _| {
18418 assert!(pane.active_item().is_none());
18419 });
18420 cx.update_global(|store: &mut SettingsStore, cx| {
18421 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
18422 s.restore_on_file_reopen = Some(true);
18423 });
18424 });
18425
18426 let _editor_reopened = workspace
18427 .update_in(cx, |workspace, window, cx| {
18428 workspace.open_path(
18429 (worktree_id, "main.rs"),
18430 Some(pane.downgrade()),
18431 true,
18432 window,
18433 cx,
18434 )
18435 })
18436 .unwrap()
18437 .await
18438 .downcast::<Editor>()
18439 .unwrap();
18440 pane.update(cx, |pane, cx| {
18441 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18442 open_editor.update(cx, |editor, cx| {
18443 assert_eq!(
18444 editor.display_text(cx),
18445 main_text,
18446 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
18447 );
18448 })
18449 });
18450}
18451
18452fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
18453 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
18454 point..point
18455}
18456
18457fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
18458 let (text, ranges) = marked_text_ranges(marked_text, true);
18459 assert_eq!(editor.text(cx), text);
18460 assert_eq!(
18461 editor.selections.ranges(cx),
18462 ranges,
18463 "Assert selections are {}",
18464 marked_text
18465 );
18466}
18467
18468pub fn handle_signature_help_request(
18469 cx: &mut EditorLspTestContext,
18470 mocked_response: lsp::SignatureHelp,
18471) -> impl Future<Output = ()> + use<> {
18472 let mut request =
18473 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
18474 let mocked_response = mocked_response.clone();
18475 async move { Ok(Some(mocked_response)) }
18476 });
18477
18478 async move {
18479 request.next().await;
18480 }
18481}
18482
18483/// Handle completion request passing a marked string specifying where the completion
18484/// should be triggered from using '|' character, what range should be replaced, and what completions
18485/// should be returned using '<' and '>' to delimit the range
18486pub fn handle_completion_request(
18487 cx: &mut EditorLspTestContext,
18488 marked_string: &str,
18489 completions: Vec<&'static str>,
18490 counter: Arc<AtomicUsize>,
18491) -> impl Future<Output = ()> {
18492 let complete_from_marker: TextRangeMarker = '|'.into();
18493 let replace_range_marker: TextRangeMarker = ('<', '>').into();
18494 let (_, mut marked_ranges) = marked_text_ranges_by(
18495 marked_string,
18496 vec![complete_from_marker.clone(), replace_range_marker.clone()],
18497 );
18498
18499 let complete_from_position =
18500 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
18501 let replace_range =
18502 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
18503
18504 let mut request =
18505 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
18506 let completions = completions.clone();
18507 counter.fetch_add(1, atomic::Ordering::Release);
18508 async move {
18509 assert_eq!(params.text_document_position.text_document.uri, url.clone());
18510 assert_eq!(
18511 params.text_document_position.position,
18512 complete_from_position
18513 );
18514 Ok(Some(lsp::CompletionResponse::Array(
18515 completions
18516 .iter()
18517 .map(|completion_text| lsp::CompletionItem {
18518 label: completion_text.to_string(),
18519 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18520 range: replace_range,
18521 new_text: completion_text.to_string(),
18522 })),
18523 ..Default::default()
18524 })
18525 .collect(),
18526 )))
18527 }
18528 });
18529
18530 async move {
18531 request.next().await;
18532 }
18533}
18534
18535fn handle_resolve_completion_request(
18536 cx: &mut EditorLspTestContext,
18537 edits: Option<Vec<(&'static str, &'static str)>>,
18538) -> impl Future<Output = ()> {
18539 let edits = edits.map(|edits| {
18540 edits
18541 .iter()
18542 .map(|(marked_string, new_text)| {
18543 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
18544 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
18545 lsp::TextEdit::new(replace_range, new_text.to_string())
18546 })
18547 .collect::<Vec<_>>()
18548 });
18549
18550 let mut request =
18551 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
18552 let edits = edits.clone();
18553 async move {
18554 Ok(lsp::CompletionItem {
18555 additional_text_edits: edits,
18556 ..Default::default()
18557 })
18558 }
18559 });
18560
18561 async move {
18562 request.next().await;
18563 }
18564}
18565
18566pub(crate) fn update_test_language_settings(
18567 cx: &mut TestAppContext,
18568 f: impl Fn(&mut AllLanguageSettingsContent),
18569) {
18570 cx.update(|cx| {
18571 SettingsStore::update_global(cx, |store, cx| {
18572 store.update_user_settings::<AllLanguageSettings>(cx, f);
18573 });
18574 });
18575}
18576
18577pub(crate) fn update_test_project_settings(
18578 cx: &mut TestAppContext,
18579 f: impl Fn(&mut ProjectSettings),
18580) {
18581 cx.update(|cx| {
18582 SettingsStore::update_global(cx, |store, cx| {
18583 store.update_user_settings::<ProjectSettings>(cx, f);
18584 });
18585 });
18586}
18587
18588pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
18589 cx.update(|cx| {
18590 assets::Assets.load_test_fonts(cx);
18591 let store = SettingsStore::test(cx);
18592 cx.set_global(store);
18593 theme::init(theme::LoadThemes::JustBase, cx);
18594 release_channel::init(SemanticVersion::default(), cx);
18595 client::init_settings(cx);
18596 language::init(cx);
18597 Project::init_settings(cx);
18598 workspace::init_settings(cx);
18599 crate::init(cx);
18600 });
18601
18602 update_test_language_settings(cx, f);
18603}
18604
18605#[track_caller]
18606fn assert_hunk_revert(
18607 not_reverted_text_with_selections: &str,
18608 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
18609 expected_reverted_text_with_selections: &str,
18610 base_text: &str,
18611 cx: &mut EditorLspTestContext,
18612) {
18613 cx.set_state(not_reverted_text_with_selections);
18614 cx.set_head_text(base_text);
18615 cx.executor().run_until_parked();
18616
18617 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
18618 let snapshot = editor.snapshot(window, cx);
18619 let reverted_hunk_statuses = snapshot
18620 .buffer_snapshot
18621 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
18622 .map(|hunk| hunk.status().kind)
18623 .collect::<Vec<_>>();
18624
18625 editor.git_restore(&Default::default(), window, cx);
18626 reverted_hunk_statuses
18627 });
18628 cx.executor().run_until_parked();
18629 cx.assert_editor_state(expected_reverted_text_with_selections);
18630 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
18631}