1use super::*;
2use crate::{
3 scroll::scroll_amount::ScrollAmount,
4 test::{
5 assert_text_with_selections, build_editor,
6 editor_lsp_test_context::{git_commit_lang, EditorLspTestContext},
7 editor_test_context::EditorTestContext,
8 select_ranges,
9 },
10 JoinLines,
11};
12use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
13use futures::StreamExt;
14use gpui::{
15 div, BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext,
16 WindowBounds, WindowOptions,
17};
18use indoc::indoc;
19use language::{
20 language_settings::{
21 AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
22 LanguageSettingsContent, PrettierSettings,
23 },
24 BracketPairConfig,
25 Capability::ReadWrite,
26 FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName,
27 Override, Point,
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 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
36 project_settings::{LspSettings, ProjectSettings},
37 FakeFs,
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::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker},
51 uri,
52};
53use workspace::{
54 item::{FollowEvent, FollowableItem, Item, ItemHandle},
55 CloseAllItems, CloseInactiveItems, NavigationEntry, ViewId,
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 {
3169 context: Point::new(0, 0)..Point::new(2, 0),
3170 primary: None,
3171 }],
3172 cx,
3173 );
3174 multibuffer.push_excerpts(
3175 rust_buffer.clone(),
3176 [ExcerptRange {
3177 context: Point::new(0, 0)..Point::new(1, 0),
3178 primary: None,
3179 }],
3180 cx,
3181 );
3182 multibuffer
3183 });
3184
3185 cx.add_window(|window, cx| {
3186 let mut editor = build_editor(multibuffer, window, cx);
3187
3188 assert_eq!(
3189 editor.text(cx),
3190 indoc! {"
3191 a = 1
3192 b = 2
3193
3194 const c: usize = 3;
3195 "}
3196 );
3197
3198 select_ranges(
3199 &mut editor,
3200 indoc! {"
3201 «aˇ» = 1
3202 b = 2
3203
3204 «const c:ˇ» usize = 3;
3205 "},
3206 window,
3207 cx,
3208 );
3209
3210 editor.tab(&Tab, window, cx);
3211 assert_text_with_selections(
3212 &mut editor,
3213 indoc! {"
3214 «aˇ» = 1
3215 b = 2
3216
3217 «const c:ˇ» usize = 3;
3218 "},
3219 cx,
3220 );
3221 editor.backtab(&Backtab, window, cx);
3222 assert_text_with_selections(
3223 &mut editor,
3224 indoc! {"
3225 «aˇ» = 1
3226 b = 2
3227
3228 «const c:ˇ» usize = 3;
3229 "},
3230 cx,
3231 );
3232
3233 editor
3234 });
3235}
3236
3237#[gpui::test]
3238async fn test_backspace(cx: &mut TestAppContext) {
3239 init_test(cx, |_| {});
3240
3241 let mut cx = EditorTestContext::new(cx).await;
3242
3243 // Basic backspace
3244 cx.set_state(indoc! {"
3245 onˇe two three
3246 fou«rˇ» five six
3247 seven «ˇeight nine
3248 »ten
3249 "});
3250 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3251 cx.assert_editor_state(indoc! {"
3252 oˇe two three
3253 fouˇ five six
3254 seven ˇten
3255 "});
3256
3257 // Test backspace inside and around indents
3258 cx.set_state(indoc! {"
3259 zero
3260 ˇone
3261 ˇtwo
3262 ˇ ˇ ˇ three
3263 ˇ ˇ four
3264 "});
3265 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3266 cx.assert_editor_state(indoc! {"
3267 zero
3268 ˇone
3269 ˇtwo
3270 ˇ threeˇ four
3271 "});
3272}
3273
3274#[gpui::test]
3275async fn test_delete(cx: &mut TestAppContext) {
3276 init_test(cx, |_| {});
3277
3278 let mut cx = EditorTestContext::new(cx).await;
3279 cx.set_state(indoc! {"
3280 onˇe two three
3281 fou«rˇ» five six
3282 seven «ˇeight nine
3283 »ten
3284 "});
3285 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3286 cx.assert_editor_state(indoc! {"
3287 onˇ two three
3288 fouˇ five six
3289 seven ˇten
3290 "});
3291}
3292
3293#[gpui::test]
3294fn test_delete_line(cx: &mut TestAppContext) {
3295 init_test(cx, |_| {});
3296
3297 let editor = cx.add_window(|window, cx| {
3298 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3299 build_editor(buffer, window, cx)
3300 });
3301 _ = editor.update(cx, |editor, window, cx| {
3302 editor.change_selections(None, window, cx, |s| {
3303 s.select_display_ranges([
3304 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3305 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3306 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3307 ])
3308 });
3309 editor.delete_line(&DeleteLine, window, cx);
3310 assert_eq!(editor.display_text(cx), "ghi");
3311 assert_eq!(
3312 editor.selections.display_ranges(cx),
3313 vec![
3314 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3315 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3316 ]
3317 );
3318 });
3319
3320 let editor = cx.add_window(|window, cx| {
3321 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3322 build_editor(buffer, window, cx)
3323 });
3324 _ = editor.update(cx, |editor, window, cx| {
3325 editor.change_selections(None, window, cx, |s| {
3326 s.select_display_ranges([
3327 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3328 ])
3329 });
3330 editor.delete_line(&DeleteLine, window, cx);
3331 assert_eq!(editor.display_text(cx), "ghi\n");
3332 assert_eq!(
3333 editor.selections.display_ranges(cx),
3334 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3335 );
3336 });
3337}
3338
3339#[gpui::test]
3340fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3341 init_test(cx, |_| {});
3342
3343 cx.add_window(|window, cx| {
3344 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3345 let mut editor = build_editor(buffer.clone(), window, cx);
3346 let buffer = buffer.read(cx).as_singleton().unwrap();
3347
3348 assert_eq!(
3349 editor.selections.ranges::<Point>(cx),
3350 &[Point::new(0, 0)..Point::new(0, 0)]
3351 );
3352
3353 // When on single line, replace newline at end by space
3354 editor.join_lines(&JoinLines, window, cx);
3355 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3356 assert_eq!(
3357 editor.selections.ranges::<Point>(cx),
3358 &[Point::new(0, 3)..Point::new(0, 3)]
3359 );
3360
3361 // When multiple lines are selected, remove newlines that are spanned by the selection
3362 editor.change_selections(None, window, cx, |s| {
3363 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3364 });
3365 editor.join_lines(&JoinLines, window, cx);
3366 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3367 assert_eq!(
3368 editor.selections.ranges::<Point>(cx),
3369 &[Point::new(0, 11)..Point::new(0, 11)]
3370 );
3371
3372 // Undo should be transactional
3373 editor.undo(&Undo, window, cx);
3374 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3375 assert_eq!(
3376 editor.selections.ranges::<Point>(cx),
3377 &[Point::new(0, 5)..Point::new(2, 2)]
3378 );
3379
3380 // When joining an empty line don't insert a space
3381 editor.change_selections(None, window, cx, |s| {
3382 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3383 });
3384 editor.join_lines(&JoinLines, window, cx);
3385 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3386 assert_eq!(
3387 editor.selections.ranges::<Point>(cx),
3388 [Point::new(2, 3)..Point::new(2, 3)]
3389 );
3390
3391 // We can remove trailing newlines
3392 editor.join_lines(&JoinLines, window, cx);
3393 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3394 assert_eq!(
3395 editor.selections.ranges::<Point>(cx),
3396 [Point::new(2, 3)..Point::new(2, 3)]
3397 );
3398
3399 // We don't blow up on the last line
3400 editor.join_lines(&JoinLines, window, cx);
3401 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3402 assert_eq!(
3403 editor.selections.ranges::<Point>(cx),
3404 [Point::new(2, 3)..Point::new(2, 3)]
3405 );
3406
3407 // reset to test indentation
3408 editor.buffer.update(cx, |buffer, cx| {
3409 buffer.edit(
3410 [
3411 (Point::new(1, 0)..Point::new(1, 2), " "),
3412 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3413 ],
3414 None,
3415 cx,
3416 )
3417 });
3418
3419 // We remove any leading spaces
3420 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3421 editor.change_selections(None, window, cx, |s| {
3422 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3423 });
3424 editor.join_lines(&JoinLines, window, cx);
3425 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3426
3427 // We don't insert a space for a line containing only spaces
3428 editor.join_lines(&JoinLines, window, cx);
3429 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3430
3431 // We ignore any leading tabs
3432 editor.join_lines(&JoinLines, window, cx);
3433 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3434
3435 editor
3436 });
3437}
3438
3439#[gpui::test]
3440fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3441 init_test(cx, |_| {});
3442
3443 cx.add_window(|window, cx| {
3444 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3445 let mut editor = build_editor(buffer.clone(), window, cx);
3446 let buffer = buffer.read(cx).as_singleton().unwrap();
3447
3448 editor.change_selections(None, window, cx, |s| {
3449 s.select_ranges([
3450 Point::new(0, 2)..Point::new(1, 1),
3451 Point::new(1, 2)..Point::new(1, 2),
3452 Point::new(3, 1)..Point::new(3, 2),
3453 ])
3454 });
3455
3456 editor.join_lines(&JoinLines, window, cx);
3457 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3458
3459 assert_eq!(
3460 editor.selections.ranges::<Point>(cx),
3461 [
3462 Point::new(0, 7)..Point::new(0, 7),
3463 Point::new(1, 3)..Point::new(1, 3)
3464 ]
3465 );
3466 editor
3467 });
3468}
3469
3470#[gpui::test]
3471async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3472 init_test(cx, |_| {});
3473
3474 let mut cx = EditorTestContext::new(cx).await;
3475
3476 let diff_base = r#"
3477 Line 0
3478 Line 1
3479 Line 2
3480 Line 3
3481 "#
3482 .unindent();
3483
3484 cx.set_state(
3485 &r#"
3486 ˇLine 0
3487 Line 1
3488 Line 2
3489 Line 3
3490 "#
3491 .unindent(),
3492 );
3493
3494 cx.set_head_text(&diff_base);
3495 executor.run_until_parked();
3496
3497 // Join lines
3498 cx.update_editor(|editor, window, cx| {
3499 editor.join_lines(&JoinLines, window, cx);
3500 });
3501 executor.run_until_parked();
3502
3503 cx.assert_editor_state(
3504 &r#"
3505 Line 0ˇ Line 1
3506 Line 2
3507 Line 3
3508 "#
3509 .unindent(),
3510 );
3511 // Join again
3512 cx.update_editor(|editor, window, cx| {
3513 editor.join_lines(&JoinLines, window, cx);
3514 });
3515 executor.run_until_parked();
3516
3517 cx.assert_editor_state(
3518 &r#"
3519 Line 0 Line 1ˇ Line 2
3520 Line 3
3521 "#
3522 .unindent(),
3523 );
3524}
3525
3526#[gpui::test]
3527async fn test_custom_newlines_cause_no_false_positive_diffs(
3528 executor: BackgroundExecutor,
3529 cx: &mut TestAppContext,
3530) {
3531 init_test(cx, |_| {});
3532 let mut cx = EditorTestContext::new(cx).await;
3533 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3534 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3535 executor.run_until_parked();
3536
3537 cx.update_editor(|editor, window, cx| {
3538 let snapshot = editor.snapshot(window, cx);
3539 assert_eq!(
3540 snapshot
3541 .buffer_snapshot
3542 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3543 .collect::<Vec<_>>(),
3544 Vec::new(),
3545 "Should not have any diffs for files with custom newlines"
3546 );
3547 });
3548}
3549
3550#[gpui::test]
3551async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3552 init_test(cx, |_| {});
3553
3554 let mut cx = EditorTestContext::new(cx).await;
3555
3556 // Test sort_lines_case_insensitive()
3557 cx.set_state(indoc! {"
3558 «z
3559 y
3560 x
3561 Z
3562 Y
3563 Xˇ»
3564 "});
3565 cx.update_editor(|e, window, cx| {
3566 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3567 });
3568 cx.assert_editor_state(indoc! {"
3569 «x
3570 X
3571 y
3572 Y
3573 z
3574 Zˇ»
3575 "});
3576
3577 // Test reverse_lines()
3578 cx.set_state(indoc! {"
3579 «5
3580 4
3581 3
3582 2
3583 1ˇ»
3584 "});
3585 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
3586 cx.assert_editor_state(indoc! {"
3587 «1
3588 2
3589 3
3590 4
3591 5ˇ»
3592 "});
3593
3594 // Skip testing shuffle_line()
3595
3596 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
3597 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
3598
3599 // Don't manipulate when cursor is on single line, but expand the selection
3600 cx.set_state(indoc! {"
3601 ddˇdd
3602 ccc
3603 bb
3604 a
3605 "});
3606 cx.update_editor(|e, window, cx| {
3607 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3608 });
3609 cx.assert_editor_state(indoc! {"
3610 «ddddˇ»
3611 ccc
3612 bb
3613 a
3614 "});
3615
3616 // Basic manipulate case
3617 // Start selection moves to column 0
3618 // End of selection shrinks to fit shorter line
3619 cx.set_state(indoc! {"
3620 dd«d
3621 ccc
3622 bb
3623 aaaaaˇ»
3624 "});
3625 cx.update_editor(|e, window, cx| {
3626 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3627 });
3628 cx.assert_editor_state(indoc! {"
3629 «aaaaa
3630 bb
3631 ccc
3632 dddˇ»
3633 "});
3634
3635 // Manipulate case with newlines
3636 cx.set_state(indoc! {"
3637 dd«d
3638 ccc
3639
3640 bb
3641 aaaaa
3642
3643 ˇ»
3644 "});
3645 cx.update_editor(|e, window, cx| {
3646 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3647 });
3648 cx.assert_editor_state(indoc! {"
3649 «
3650
3651 aaaaa
3652 bb
3653 ccc
3654 dddˇ»
3655
3656 "});
3657
3658 // Adding new line
3659 cx.set_state(indoc! {"
3660 aa«a
3661 bbˇ»b
3662 "});
3663 cx.update_editor(|e, window, cx| {
3664 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
3665 });
3666 cx.assert_editor_state(indoc! {"
3667 «aaa
3668 bbb
3669 added_lineˇ»
3670 "});
3671
3672 // Removing line
3673 cx.set_state(indoc! {"
3674 aa«a
3675 bbbˇ»
3676 "});
3677 cx.update_editor(|e, window, cx| {
3678 e.manipulate_lines(window, cx, |lines| {
3679 lines.pop();
3680 })
3681 });
3682 cx.assert_editor_state(indoc! {"
3683 «aaaˇ»
3684 "});
3685
3686 // Removing all lines
3687 cx.set_state(indoc! {"
3688 aa«a
3689 bbbˇ»
3690 "});
3691 cx.update_editor(|e, window, cx| {
3692 e.manipulate_lines(window, cx, |lines| {
3693 lines.drain(..);
3694 })
3695 });
3696 cx.assert_editor_state(indoc! {"
3697 ˇ
3698 "});
3699}
3700
3701#[gpui::test]
3702async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
3703 init_test(cx, |_| {});
3704
3705 let mut cx = EditorTestContext::new(cx).await;
3706
3707 // Consider continuous selection as single selection
3708 cx.set_state(indoc! {"
3709 Aaa«aa
3710 cˇ»c«c
3711 bb
3712 aaaˇ»aa
3713 "});
3714 cx.update_editor(|e, window, cx| {
3715 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3716 });
3717 cx.assert_editor_state(indoc! {"
3718 «Aaaaa
3719 ccc
3720 bb
3721 aaaaaˇ»
3722 "});
3723
3724 cx.set_state(indoc! {"
3725 Aaa«aa
3726 cˇ»c«c
3727 bb
3728 aaaˇ»aa
3729 "});
3730 cx.update_editor(|e, window, cx| {
3731 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3732 });
3733 cx.assert_editor_state(indoc! {"
3734 «Aaaaa
3735 ccc
3736 bbˇ»
3737 "});
3738
3739 // Consider non continuous selection as distinct dedup operations
3740 cx.set_state(indoc! {"
3741 «aaaaa
3742 bb
3743 aaaaa
3744 aaaaaˇ»
3745
3746 aaa«aaˇ»
3747 "});
3748 cx.update_editor(|e, window, cx| {
3749 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3750 });
3751 cx.assert_editor_state(indoc! {"
3752 «aaaaa
3753 bbˇ»
3754
3755 «aaaaaˇ»
3756 "});
3757}
3758
3759#[gpui::test]
3760async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
3761 init_test(cx, |_| {});
3762
3763 let mut cx = EditorTestContext::new(cx).await;
3764
3765 cx.set_state(indoc! {"
3766 «Aaa
3767 aAa
3768 Aaaˇ»
3769 "});
3770 cx.update_editor(|e, window, cx| {
3771 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3772 });
3773 cx.assert_editor_state(indoc! {"
3774 «Aaa
3775 aAaˇ»
3776 "});
3777
3778 cx.set_state(indoc! {"
3779 «Aaa
3780 aAa
3781 aaAˇ»
3782 "});
3783 cx.update_editor(|e, window, cx| {
3784 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3785 });
3786 cx.assert_editor_state(indoc! {"
3787 «Aaaˇ»
3788 "});
3789}
3790
3791#[gpui::test]
3792async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
3793 init_test(cx, |_| {});
3794
3795 let mut cx = EditorTestContext::new(cx).await;
3796
3797 // Manipulate with multiple selections on a single line
3798 cx.set_state(indoc! {"
3799 dd«dd
3800 cˇ»c«c
3801 bb
3802 aaaˇ»aa
3803 "});
3804 cx.update_editor(|e, window, cx| {
3805 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3806 });
3807 cx.assert_editor_state(indoc! {"
3808 «aaaaa
3809 bb
3810 ccc
3811 ddddˇ»
3812 "});
3813
3814 // Manipulate with multiple disjoin selections
3815 cx.set_state(indoc! {"
3816 5«
3817 4
3818 3
3819 2
3820 1ˇ»
3821
3822 dd«dd
3823 ccc
3824 bb
3825 aaaˇ»aa
3826 "});
3827 cx.update_editor(|e, window, cx| {
3828 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3829 });
3830 cx.assert_editor_state(indoc! {"
3831 «1
3832 2
3833 3
3834 4
3835 5ˇ»
3836
3837 «aaaaa
3838 bb
3839 ccc
3840 ddddˇ»
3841 "});
3842
3843 // Adding lines on each selection
3844 cx.set_state(indoc! {"
3845 2«
3846 1ˇ»
3847
3848 bb«bb
3849 aaaˇ»aa
3850 "});
3851 cx.update_editor(|e, window, cx| {
3852 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
3853 });
3854 cx.assert_editor_state(indoc! {"
3855 «2
3856 1
3857 added lineˇ»
3858
3859 «bbbb
3860 aaaaa
3861 added lineˇ»
3862 "});
3863
3864 // Removing lines on each selection
3865 cx.set_state(indoc! {"
3866 2«
3867 1ˇ»
3868
3869 bb«bb
3870 aaaˇ»aa
3871 "});
3872 cx.update_editor(|e, window, cx| {
3873 e.manipulate_lines(window, cx, |lines| {
3874 lines.pop();
3875 })
3876 });
3877 cx.assert_editor_state(indoc! {"
3878 «2ˇ»
3879
3880 «bbbbˇ»
3881 "});
3882}
3883
3884#[gpui::test]
3885async fn test_manipulate_text(cx: &mut TestAppContext) {
3886 init_test(cx, |_| {});
3887
3888 let mut cx = EditorTestContext::new(cx).await;
3889
3890 // Test convert_to_upper_case()
3891 cx.set_state(indoc! {"
3892 «hello worldˇ»
3893 "});
3894 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3895 cx.assert_editor_state(indoc! {"
3896 «HELLO WORLDˇ»
3897 "});
3898
3899 // Test convert_to_lower_case()
3900 cx.set_state(indoc! {"
3901 «HELLO WORLDˇ»
3902 "});
3903 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
3904 cx.assert_editor_state(indoc! {"
3905 «hello worldˇ»
3906 "});
3907
3908 // Test multiple line, single selection case
3909 cx.set_state(indoc! {"
3910 «The quick brown
3911 fox jumps over
3912 the lazy dogˇ»
3913 "});
3914 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
3915 cx.assert_editor_state(indoc! {"
3916 «The Quick Brown
3917 Fox Jumps Over
3918 The Lazy Dogˇ»
3919 "});
3920
3921 // Test multiple line, single selection case
3922 cx.set_state(indoc! {"
3923 «The quick brown
3924 fox jumps over
3925 the lazy dogˇ»
3926 "});
3927 cx.update_editor(|e, window, cx| {
3928 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
3929 });
3930 cx.assert_editor_state(indoc! {"
3931 «TheQuickBrown
3932 FoxJumpsOver
3933 TheLazyDogˇ»
3934 "});
3935
3936 // From here on out, test more complex cases of manipulate_text()
3937
3938 // Test no selection case - should affect words cursors are in
3939 // Cursor at beginning, middle, and end of word
3940 cx.set_state(indoc! {"
3941 ˇhello big beauˇtiful worldˇ
3942 "});
3943 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3944 cx.assert_editor_state(indoc! {"
3945 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
3946 "});
3947
3948 // Test multiple selections on a single line and across multiple lines
3949 cx.set_state(indoc! {"
3950 «Theˇ» quick «brown
3951 foxˇ» jumps «overˇ»
3952 the «lazyˇ» dog
3953 "});
3954 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3955 cx.assert_editor_state(indoc! {"
3956 «THEˇ» quick «BROWN
3957 FOXˇ» jumps «OVERˇ»
3958 the «LAZYˇ» dog
3959 "});
3960
3961 // Test case where text length grows
3962 cx.set_state(indoc! {"
3963 «tschüߡ»
3964 "});
3965 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3966 cx.assert_editor_state(indoc! {"
3967 «TSCHÜSSˇ»
3968 "});
3969
3970 // Test to make sure we don't crash when text shrinks
3971 cx.set_state(indoc! {"
3972 aaa_bbbˇ
3973 "});
3974 cx.update_editor(|e, window, cx| {
3975 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
3976 });
3977 cx.assert_editor_state(indoc! {"
3978 «aaaBbbˇ»
3979 "});
3980
3981 // Test to make sure we all aware of the fact that each word can grow and shrink
3982 // Final selections should be aware of this fact
3983 cx.set_state(indoc! {"
3984 aaa_bˇbb bbˇb_ccc ˇccc_ddd
3985 "});
3986 cx.update_editor(|e, window, cx| {
3987 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
3988 });
3989 cx.assert_editor_state(indoc! {"
3990 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
3991 "});
3992
3993 cx.set_state(indoc! {"
3994 «hElLo, WoRld!ˇ»
3995 "});
3996 cx.update_editor(|e, window, cx| {
3997 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
3998 });
3999 cx.assert_editor_state(indoc! {"
4000 «HeLlO, wOrLD!ˇ»
4001 "});
4002}
4003
4004#[gpui::test]
4005fn test_duplicate_line(cx: &mut TestAppContext) {
4006 init_test(cx, |_| {});
4007
4008 let editor = cx.add_window(|window, cx| {
4009 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4010 build_editor(buffer, window, cx)
4011 });
4012 _ = editor.update(cx, |editor, window, cx| {
4013 editor.change_selections(None, window, cx, |s| {
4014 s.select_display_ranges([
4015 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4016 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4017 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4018 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4019 ])
4020 });
4021 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4022 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4023 assert_eq!(
4024 editor.selections.display_ranges(cx),
4025 vec![
4026 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4027 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4028 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4029 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4030 ]
4031 );
4032 });
4033
4034 let editor = cx.add_window(|window, cx| {
4035 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4036 build_editor(buffer, window, cx)
4037 });
4038 _ = editor.update(cx, |editor, window, cx| {
4039 editor.change_selections(None, window, cx, |s| {
4040 s.select_display_ranges([
4041 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4042 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4043 ])
4044 });
4045 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4046 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4047 assert_eq!(
4048 editor.selections.display_ranges(cx),
4049 vec![
4050 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4051 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4052 ]
4053 );
4054 });
4055
4056 // With `move_upwards` the selections stay in place, except for
4057 // the lines inserted above them
4058 let editor = cx.add_window(|window, cx| {
4059 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4060 build_editor(buffer, window, cx)
4061 });
4062 _ = editor.update(cx, |editor, window, cx| {
4063 editor.change_selections(None, window, cx, |s| {
4064 s.select_display_ranges([
4065 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4066 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4067 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4068 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4069 ])
4070 });
4071 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4072 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4073 assert_eq!(
4074 editor.selections.display_ranges(cx),
4075 vec![
4076 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4077 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4078 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4079 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4080 ]
4081 );
4082 });
4083
4084 let editor = cx.add_window(|window, cx| {
4085 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4086 build_editor(buffer, window, cx)
4087 });
4088 _ = editor.update(cx, |editor, window, cx| {
4089 editor.change_selections(None, window, cx, |s| {
4090 s.select_display_ranges([
4091 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4092 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4093 ])
4094 });
4095 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4096 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4097 assert_eq!(
4098 editor.selections.display_ranges(cx),
4099 vec![
4100 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4101 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4102 ]
4103 );
4104 });
4105
4106 let editor = cx.add_window(|window, cx| {
4107 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4108 build_editor(buffer, window, cx)
4109 });
4110 _ = editor.update(cx, |editor, window, cx| {
4111 editor.change_selections(None, window, cx, |s| {
4112 s.select_display_ranges([
4113 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4114 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4115 ])
4116 });
4117 editor.duplicate_selection(&DuplicateSelection, window, cx);
4118 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4119 assert_eq!(
4120 editor.selections.display_ranges(cx),
4121 vec![
4122 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4123 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4124 ]
4125 );
4126 });
4127}
4128
4129#[gpui::test]
4130fn test_move_line_up_down(cx: &mut TestAppContext) {
4131 init_test(cx, |_| {});
4132
4133 let editor = cx.add_window(|window, cx| {
4134 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4135 build_editor(buffer, window, cx)
4136 });
4137 _ = editor.update(cx, |editor, window, cx| {
4138 editor.fold_creases(
4139 vec![
4140 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4141 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4142 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4143 ],
4144 true,
4145 window,
4146 cx,
4147 );
4148 editor.change_selections(None, window, cx, |s| {
4149 s.select_display_ranges([
4150 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4151 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4152 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4153 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4154 ])
4155 });
4156 assert_eq!(
4157 editor.display_text(cx),
4158 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4159 );
4160
4161 editor.move_line_up(&MoveLineUp, window, cx);
4162 assert_eq!(
4163 editor.display_text(cx),
4164 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4165 );
4166 assert_eq!(
4167 editor.selections.display_ranges(cx),
4168 vec![
4169 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4170 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4171 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4172 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4173 ]
4174 );
4175 });
4176
4177 _ = editor.update(cx, |editor, window, cx| {
4178 editor.move_line_down(&MoveLineDown, window, cx);
4179 assert_eq!(
4180 editor.display_text(cx),
4181 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4182 );
4183 assert_eq!(
4184 editor.selections.display_ranges(cx),
4185 vec![
4186 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4187 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4188 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4189 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4190 ]
4191 );
4192 });
4193
4194 _ = editor.update(cx, |editor, window, cx| {
4195 editor.move_line_down(&MoveLineDown, window, cx);
4196 assert_eq!(
4197 editor.display_text(cx),
4198 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4199 );
4200 assert_eq!(
4201 editor.selections.display_ranges(cx),
4202 vec![
4203 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4204 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4205 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4206 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4207 ]
4208 );
4209 });
4210
4211 _ = editor.update(cx, |editor, window, cx| {
4212 editor.move_line_up(&MoveLineUp, window, cx);
4213 assert_eq!(
4214 editor.display_text(cx),
4215 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4216 );
4217 assert_eq!(
4218 editor.selections.display_ranges(cx),
4219 vec![
4220 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4221 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4222 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4223 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4224 ]
4225 );
4226 });
4227}
4228
4229#[gpui::test]
4230fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4231 init_test(cx, |_| {});
4232
4233 let editor = cx.add_window(|window, cx| {
4234 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4235 build_editor(buffer, window, cx)
4236 });
4237 _ = editor.update(cx, |editor, window, cx| {
4238 let snapshot = editor.buffer.read(cx).snapshot(cx);
4239 editor.insert_blocks(
4240 [BlockProperties {
4241 style: BlockStyle::Fixed,
4242 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4243 height: 1,
4244 render: Arc::new(|_| div().into_any()),
4245 priority: 0,
4246 }],
4247 Some(Autoscroll::fit()),
4248 cx,
4249 );
4250 editor.change_selections(None, window, cx, |s| {
4251 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4252 });
4253 editor.move_line_down(&MoveLineDown, window, cx);
4254 });
4255}
4256
4257#[gpui::test]
4258async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4259 init_test(cx, |_| {});
4260
4261 let mut cx = EditorTestContext::new(cx).await;
4262 cx.set_state(
4263 &"
4264 ˇzero
4265 one
4266 two
4267 three
4268 four
4269 five
4270 "
4271 .unindent(),
4272 );
4273
4274 // Create a four-line block that replaces three lines of text.
4275 cx.update_editor(|editor, window, cx| {
4276 let snapshot = editor.snapshot(window, cx);
4277 let snapshot = &snapshot.buffer_snapshot;
4278 let placement = BlockPlacement::Replace(
4279 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4280 );
4281 editor.insert_blocks(
4282 [BlockProperties {
4283 placement,
4284 height: 4,
4285 style: BlockStyle::Sticky,
4286 render: Arc::new(|_| gpui::div().into_any_element()),
4287 priority: 0,
4288 }],
4289 None,
4290 cx,
4291 );
4292 });
4293
4294 // Move down so that the cursor touches the block.
4295 cx.update_editor(|editor, window, cx| {
4296 editor.move_down(&Default::default(), window, cx);
4297 });
4298 cx.assert_editor_state(
4299 &"
4300 zero
4301 «one
4302 two
4303 threeˇ»
4304 four
4305 five
4306 "
4307 .unindent(),
4308 );
4309
4310 // Move down past the block.
4311 cx.update_editor(|editor, window, cx| {
4312 editor.move_down(&Default::default(), window, cx);
4313 });
4314 cx.assert_editor_state(
4315 &"
4316 zero
4317 one
4318 two
4319 three
4320 ˇfour
4321 five
4322 "
4323 .unindent(),
4324 );
4325}
4326
4327#[gpui::test]
4328fn test_transpose(cx: &mut TestAppContext) {
4329 init_test(cx, |_| {});
4330
4331 _ = cx.add_window(|window, cx| {
4332 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4333 editor.set_style(EditorStyle::default(), window, cx);
4334 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4335 editor.transpose(&Default::default(), window, cx);
4336 assert_eq!(editor.text(cx), "bac");
4337 assert_eq!(editor.selections.ranges(cx), [2..2]);
4338
4339 editor.transpose(&Default::default(), window, cx);
4340 assert_eq!(editor.text(cx), "bca");
4341 assert_eq!(editor.selections.ranges(cx), [3..3]);
4342
4343 editor.transpose(&Default::default(), window, cx);
4344 assert_eq!(editor.text(cx), "bac");
4345 assert_eq!(editor.selections.ranges(cx), [3..3]);
4346
4347 editor
4348 });
4349
4350 _ = cx.add_window(|window, cx| {
4351 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4352 editor.set_style(EditorStyle::default(), window, cx);
4353 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4354 editor.transpose(&Default::default(), window, cx);
4355 assert_eq!(editor.text(cx), "acb\nde");
4356 assert_eq!(editor.selections.ranges(cx), [3..3]);
4357
4358 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4359 editor.transpose(&Default::default(), window, cx);
4360 assert_eq!(editor.text(cx), "acbd\ne");
4361 assert_eq!(editor.selections.ranges(cx), [5..5]);
4362
4363 editor.transpose(&Default::default(), window, cx);
4364 assert_eq!(editor.text(cx), "acbde\n");
4365 assert_eq!(editor.selections.ranges(cx), [6..6]);
4366
4367 editor.transpose(&Default::default(), window, cx);
4368 assert_eq!(editor.text(cx), "acbd\ne");
4369 assert_eq!(editor.selections.ranges(cx), [6..6]);
4370
4371 editor
4372 });
4373
4374 _ = cx.add_window(|window, cx| {
4375 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4376 editor.set_style(EditorStyle::default(), window, cx);
4377 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4378 editor.transpose(&Default::default(), window, cx);
4379 assert_eq!(editor.text(cx), "bacd\ne");
4380 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4381
4382 editor.transpose(&Default::default(), window, cx);
4383 assert_eq!(editor.text(cx), "bcade\n");
4384 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4385
4386 editor.transpose(&Default::default(), window, cx);
4387 assert_eq!(editor.text(cx), "bcda\ne");
4388 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4389
4390 editor.transpose(&Default::default(), window, cx);
4391 assert_eq!(editor.text(cx), "bcade\n");
4392 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4393
4394 editor.transpose(&Default::default(), window, cx);
4395 assert_eq!(editor.text(cx), "bcaed\n");
4396 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4397
4398 editor
4399 });
4400
4401 _ = cx.add_window(|window, cx| {
4402 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4403 editor.set_style(EditorStyle::default(), window, cx);
4404 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4405 editor.transpose(&Default::default(), window, cx);
4406 assert_eq!(editor.text(cx), "🏀🍐✋");
4407 assert_eq!(editor.selections.ranges(cx), [8..8]);
4408
4409 editor.transpose(&Default::default(), window, cx);
4410 assert_eq!(editor.text(cx), "🏀✋🍐");
4411 assert_eq!(editor.selections.ranges(cx), [11..11]);
4412
4413 editor.transpose(&Default::default(), window, cx);
4414 assert_eq!(editor.text(cx), "🏀🍐✋");
4415 assert_eq!(editor.selections.ranges(cx), [11..11]);
4416
4417 editor
4418 });
4419}
4420
4421#[gpui::test]
4422async fn test_rewrap(cx: &mut TestAppContext) {
4423 init_test(cx, |settings| {
4424 settings.languages.extend([
4425 (
4426 "Markdown".into(),
4427 LanguageSettingsContent {
4428 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4429 ..Default::default()
4430 },
4431 ),
4432 (
4433 "Plain Text".into(),
4434 LanguageSettingsContent {
4435 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4436 ..Default::default()
4437 },
4438 ),
4439 ])
4440 });
4441
4442 let mut cx = EditorTestContext::new(cx).await;
4443
4444 let language_with_c_comments = Arc::new(Language::new(
4445 LanguageConfig {
4446 line_comments: vec!["// ".into()],
4447 ..LanguageConfig::default()
4448 },
4449 None,
4450 ));
4451 let language_with_pound_comments = Arc::new(Language::new(
4452 LanguageConfig {
4453 line_comments: vec!["# ".into()],
4454 ..LanguageConfig::default()
4455 },
4456 None,
4457 ));
4458 let markdown_language = Arc::new(Language::new(
4459 LanguageConfig {
4460 name: "Markdown".into(),
4461 ..LanguageConfig::default()
4462 },
4463 None,
4464 ));
4465 let language_with_doc_comments = Arc::new(Language::new(
4466 LanguageConfig {
4467 line_comments: vec!["// ".into(), "/// ".into()],
4468 ..LanguageConfig::default()
4469 },
4470 Some(tree_sitter_rust::LANGUAGE.into()),
4471 ));
4472
4473 let plaintext_language = Arc::new(Language::new(
4474 LanguageConfig {
4475 name: "Plain Text".into(),
4476 ..LanguageConfig::default()
4477 },
4478 None,
4479 ));
4480
4481 assert_rewrap(
4482 indoc! {"
4483 // ˇ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.
4484 "},
4485 indoc! {"
4486 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4487 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4488 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4489 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4490 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4491 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4492 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4493 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4494 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4495 // porttitor id. Aliquam id accumsan eros.
4496 "},
4497 language_with_c_comments.clone(),
4498 &mut cx,
4499 );
4500
4501 // Test that rewrapping works inside of a selection
4502 assert_rewrap(
4503 indoc! {"
4504 «// 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.ˇ»
4505 "},
4506 indoc! {"
4507 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4508 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4509 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4510 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4511 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4512 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4513 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4514 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4515 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4516 // porttitor id. Aliquam id accumsan eros.ˇ»
4517 "},
4518 language_with_c_comments.clone(),
4519 &mut cx,
4520 );
4521
4522 // Test that cursors that expand to the same region are collapsed.
4523 assert_rewrap(
4524 indoc! {"
4525 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4526 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4527 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4528 // ˇ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.
4529 "},
4530 indoc! {"
4531 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4532 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4533 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
4534 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4535 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
4536 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4537 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4538 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4539 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4540 // porttitor id. Aliquam id accumsan eros.
4541 "},
4542 language_with_c_comments.clone(),
4543 &mut cx,
4544 );
4545
4546 // Test that non-contiguous selections are treated separately.
4547 assert_rewrap(
4548 indoc! {"
4549 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4550 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4551 //
4552 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4553 // ˇ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.
4554 "},
4555 indoc! {"
4556 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4557 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4558 // auctor, eu lacinia sapien scelerisque.
4559 //
4560 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
4561 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4562 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
4563 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
4564 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
4565 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
4566 // vulputate turpis porttitor id. Aliquam id accumsan eros.
4567 "},
4568 language_with_c_comments.clone(),
4569 &mut cx,
4570 );
4571
4572 // Test that different comment prefixes are supported.
4573 assert_rewrap(
4574 indoc! {"
4575 # ˇ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.
4576 "},
4577 indoc! {"
4578 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4579 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4580 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4581 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4582 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
4583 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
4584 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
4585 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
4586 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
4587 # accumsan eros.
4588 "},
4589 language_with_pound_comments.clone(),
4590 &mut cx,
4591 );
4592
4593 // Test that rewrapping is ignored outside of comments in most languages.
4594 assert_rewrap(
4595 indoc! {"
4596 /// Adds two numbers.
4597 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4598 fn add(a: u32, b: u32) -> u32 {
4599 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ˇ
4600 }
4601 "},
4602 indoc! {"
4603 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
4604 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4605 fn add(a: u32, b: u32) -> u32 {
4606 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ˇ
4607 }
4608 "},
4609 language_with_doc_comments.clone(),
4610 &mut cx,
4611 );
4612
4613 // Test that rewrapping works in Markdown and Plain Text languages.
4614 assert_rewrap(
4615 indoc! {"
4616 # Hello
4617
4618 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.
4619 "},
4620 indoc! {"
4621 # Hello
4622
4623 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4624 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4625 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4626 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4627 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4628 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4629 Integer sit amet scelerisque nisi.
4630 "},
4631 markdown_language,
4632 &mut cx,
4633 );
4634
4635 assert_rewrap(
4636 indoc! {"
4637 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.
4638 "},
4639 indoc! {"
4640 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4641 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4642 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4643 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4644 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4645 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4646 Integer sit amet scelerisque nisi.
4647 "},
4648 plaintext_language,
4649 &mut cx,
4650 );
4651
4652 // Test rewrapping unaligned comments in a selection.
4653 assert_rewrap(
4654 indoc! {"
4655 fn foo() {
4656 if true {
4657 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4658 // Praesent semper egestas tellus id dignissim.ˇ»
4659 do_something();
4660 } else {
4661 //
4662 }
4663 }
4664 "},
4665 indoc! {"
4666 fn foo() {
4667 if true {
4668 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4669 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4670 // egestas tellus id dignissim.ˇ»
4671 do_something();
4672 } else {
4673 //
4674 }
4675 }
4676 "},
4677 language_with_doc_comments.clone(),
4678 &mut cx,
4679 );
4680
4681 assert_rewrap(
4682 indoc! {"
4683 fn foo() {
4684 if true {
4685 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4686 // Praesent semper egestas tellus id dignissim.»
4687 do_something();
4688 } else {
4689 //
4690 }
4691
4692 }
4693 "},
4694 indoc! {"
4695 fn foo() {
4696 if true {
4697 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4698 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4699 // egestas tellus id dignissim.»
4700 do_something();
4701 } else {
4702 //
4703 }
4704
4705 }
4706 "},
4707 language_with_doc_comments.clone(),
4708 &mut cx,
4709 );
4710
4711 #[track_caller]
4712 fn assert_rewrap(
4713 unwrapped_text: &str,
4714 wrapped_text: &str,
4715 language: Arc<Language>,
4716 cx: &mut EditorTestContext,
4717 ) {
4718 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4719 cx.set_state(unwrapped_text);
4720 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
4721 cx.assert_editor_state(wrapped_text);
4722 }
4723}
4724
4725#[gpui::test]
4726async fn test_hard_wrap(cx: &mut TestAppContext) {
4727 init_test(cx, |_| {});
4728 let mut cx = EditorTestContext::new(cx).await;
4729
4730 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
4731 cx.update_editor(|editor, _, cx| {
4732 editor.set_hard_wrap(Some(14), cx);
4733 });
4734
4735 cx.set_state(indoc!(
4736 "
4737 one two three ˇ
4738 "
4739 ));
4740 cx.simulate_input("four");
4741 cx.run_until_parked();
4742
4743 cx.assert_editor_state(indoc!(
4744 "
4745 one two three
4746 fourˇ
4747 "
4748 ));
4749
4750 cx.update_editor(|editor, window, cx| {
4751 editor.newline(&Default::default(), window, cx);
4752 });
4753 cx.run_until_parked();
4754 cx.assert_editor_state(indoc!(
4755 "
4756 one two three
4757 four
4758 ˇ
4759 "
4760 ));
4761
4762 cx.simulate_input("five");
4763 cx.run_until_parked();
4764 cx.assert_editor_state(indoc!(
4765 "
4766 one two three
4767 four
4768 fiveˇ
4769 "
4770 ));
4771
4772 cx.update_editor(|editor, window, cx| {
4773 editor.newline(&Default::default(), window, cx);
4774 });
4775 cx.run_until_parked();
4776 cx.simulate_input("# ");
4777 cx.run_until_parked();
4778 cx.assert_editor_state(indoc!(
4779 "
4780 one two three
4781 four
4782 five
4783 # ˇ
4784 "
4785 ));
4786
4787 cx.update_editor(|editor, window, cx| {
4788 editor.newline(&Default::default(), window, cx);
4789 });
4790 cx.run_until_parked();
4791 cx.assert_editor_state(indoc!(
4792 "
4793 one two three
4794 four
4795 five
4796 #\x20
4797 #ˇ
4798 "
4799 ));
4800
4801 cx.simulate_input(" 6");
4802 cx.run_until_parked();
4803 cx.assert_editor_state(indoc!(
4804 "
4805 one two three
4806 four
4807 five
4808 #
4809 # 6ˇ
4810 "
4811 ));
4812}
4813
4814#[gpui::test]
4815async fn test_clipboard(cx: &mut TestAppContext) {
4816 init_test(cx, |_| {});
4817
4818 let mut cx = EditorTestContext::new(cx).await;
4819
4820 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
4821 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4822 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
4823
4824 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
4825 cx.set_state("two ˇfour ˇsix ˇ");
4826 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4827 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
4828
4829 // Paste again but with only two cursors. Since the number of cursors doesn't
4830 // match the number of slices in the clipboard, the entire clipboard text
4831 // is pasted at each cursor.
4832 cx.set_state("ˇtwo one✅ four three six five ˇ");
4833 cx.update_editor(|e, window, cx| {
4834 e.handle_input("( ", window, cx);
4835 e.paste(&Paste, window, cx);
4836 e.handle_input(") ", window, cx);
4837 });
4838 cx.assert_editor_state(
4839 &([
4840 "( one✅ ",
4841 "three ",
4842 "five ) ˇtwo one✅ four three six five ( one✅ ",
4843 "three ",
4844 "five ) ˇ",
4845 ]
4846 .join("\n")),
4847 );
4848
4849 // Cut with three selections, one of which is full-line.
4850 cx.set_state(indoc! {"
4851 1«2ˇ»3
4852 4ˇ567
4853 «8ˇ»9"});
4854 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4855 cx.assert_editor_state(indoc! {"
4856 1ˇ3
4857 ˇ9"});
4858
4859 // Paste with three selections, noticing how the copied selection that was full-line
4860 // gets inserted before the second cursor.
4861 cx.set_state(indoc! {"
4862 1ˇ3
4863 9ˇ
4864 «oˇ»ne"});
4865 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4866 cx.assert_editor_state(indoc! {"
4867 12ˇ3
4868 4567
4869 9ˇ
4870 8ˇne"});
4871
4872 // Copy with a single cursor only, which writes the whole line into the clipboard.
4873 cx.set_state(indoc! {"
4874 The quick brown
4875 fox juˇmps over
4876 the lazy dog"});
4877 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4878 assert_eq!(
4879 cx.read_from_clipboard()
4880 .and_then(|item| item.text().as_deref().map(str::to_string)),
4881 Some("fox jumps over\n".to_string())
4882 );
4883
4884 // Paste with three selections, noticing how the copied full-line selection is inserted
4885 // before the empty selections but replaces the selection that is non-empty.
4886 cx.set_state(indoc! {"
4887 Tˇhe quick brown
4888 «foˇ»x jumps over
4889 tˇhe lazy dog"});
4890 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4891 cx.assert_editor_state(indoc! {"
4892 fox jumps over
4893 Tˇhe quick brown
4894 fox jumps over
4895 ˇx jumps over
4896 fox jumps over
4897 tˇhe lazy dog"});
4898}
4899
4900#[gpui::test]
4901async fn test_copy_trim(cx: &mut TestAppContext) {
4902 init_test(cx, |_| {});
4903
4904 let mut cx = EditorTestContext::new(cx).await;
4905 cx.set_state(
4906 r#" «for selection in selections.iter() {
4907 let mut start = selection.start;
4908 let mut end = selection.end;
4909 let is_entire_line = selection.is_empty();
4910 if is_entire_line {
4911 start = Point::new(start.row, 0);ˇ»
4912 end = cmp::min(max_point, Point::new(end.row + 1, 0));
4913 }
4914 "#,
4915 );
4916 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4917 assert_eq!(
4918 cx.read_from_clipboard()
4919 .and_then(|item| item.text().as_deref().map(str::to_string)),
4920 Some(
4921 "for selection in selections.iter() {
4922 let mut start = selection.start;
4923 let mut end = selection.end;
4924 let is_entire_line = selection.is_empty();
4925 if is_entire_line {
4926 start = Point::new(start.row, 0);"
4927 .to_string()
4928 ),
4929 "Regular copying preserves all indentation selected",
4930 );
4931 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
4932 assert_eq!(
4933 cx.read_from_clipboard()
4934 .and_then(|item| item.text().as_deref().map(str::to_string)),
4935 Some(
4936 "for selection in selections.iter() {
4937let mut start = selection.start;
4938let mut end = selection.end;
4939let is_entire_line = selection.is_empty();
4940if is_entire_line {
4941 start = Point::new(start.row, 0);"
4942 .to_string()
4943 ),
4944 "Copying with stripping should strip all leading whitespaces"
4945 );
4946
4947 cx.set_state(
4948 r#" « for selection in selections.iter() {
4949 let mut start = selection.start;
4950 let mut end = selection.end;
4951 let is_entire_line = selection.is_empty();
4952 if is_entire_line {
4953 start = Point::new(start.row, 0);ˇ»
4954 end = cmp::min(max_point, Point::new(end.row + 1, 0));
4955 }
4956 "#,
4957 );
4958 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4959 assert_eq!(
4960 cx.read_from_clipboard()
4961 .and_then(|item| item.text().as_deref().map(str::to_string)),
4962 Some(
4963 " for selection in selections.iter() {
4964 let mut start = selection.start;
4965 let mut end = selection.end;
4966 let is_entire_line = selection.is_empty();
4967 if is_entire_line {
4968 start = Point::new(start.row, 0);"
4969 .to_string()
4970 ),
4971 "Regular copying preserves all indentation selected",
4972 );
4973 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
4974 assert_eq!(
4975 cx.read_from_clipboard()
4976 .and_then(|item| item.text().as_deref().map(str::to_string)),
4977 Some(
4978 "for selection in selections.iter() {
4979let mut start = selection.start;
4980let mut end = selection.end;
4981let is_entire_line = selection.is_empty();
4982if is_entire_line {
4983 start = Point::new(start.row, 0);"
4984 .to_string()
4985 ),
4986 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
4987 );
4988
4989 cx.set_state(
4990 r#" «ˇ for selection in selections.iter() {
4991 let mut start = selection.start;
4992 let mut end = selection.end;
4993 let is_entire_line = selection.is_empty();
4994 if is_entire_line {
4995 start = Point::new(start.row, 0);»
4996 end = cmp::min(max_point, Point::new(end.row + 1, 0));
4997 }
4998 "#,
4999 );
5000 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5001 assert_eq!(
5002 cx.read_from_clipboard()
5003 .and_then(|item| item.text().as_deref().map(str::to_string)),
5004 Some(
5005 " for selection in selections.iter() {
5006 let mut start = selection.start;
5007 let mut end = selection.end;
5008 let is_entire_line = selection.is_empty();
5009 if is_entire_line {
5010 start = Point::new(start.row, 0);"
5011 .to_string()
5012 ),
5013 "Regular copying for reverse selection works the same",
5014 );
5015 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5016 assert_eq!(
5017 cx.read_from_clipboard()
5018 .and_then(|item| item.text().as_deref().map(str::to_string)),
5019 Some(
5020 "for selection in selections.iter() {
5021let mut start = selection.start;
5022let mut end = selection.end;
5023let is_entire_line = selection.is_empty();
5024if is_entire_line {
5025 start = Point::new(start.row, 0);"
5026 .to_string()
5027 ),
5028 "Copying with stripping for reverse selection works the same"
5029 );
5030
5031 cx.set_state(
5032 r#" for selection «in selections.iter() {
5033 let mut start = selection.start;
5034 let mut end = selection.end;
5035 let is_entire_line = selection.is_empty();
5036 if is_entire_line {
5037 start = Point::new(start.row, 0);ˇ»
5038 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5039 }
5040 "#,
5041 );
5042 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5043 assert_eq!(
5044 cx.read_from_clipboard()
5045 .and_then(|item| item.text().as_deref().map(str::to_string)),
5046 Some(
5047 "in selections.iter() {
5048 let mut start = selection.start;
5049 let mut end = selection.end;
5050 let is_entire_line = selection.is_empty();
5051 if is_entire_line {
5052 start = Point::new(start.row, 0);"
5053 .to_string()
5054 ),
5055 "When selecting past the indent, the copying works as usual",
5056 );
5057 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5058 assert_eq!(
5059 cx.read_from_clipboard()
5060 .and_then(|item| item.text().as_deref().map(str::to_string)),
5061 Some(
5062 "in selections.iter() {
5063 let mut start = selection.start;
5064 let mut end = selection.end;
5065 let is_entire_line = selection.is_empty();
5066 if is_entire_line {
5067 start = Point::new(start.row, 0);"
5068 .to_string()
5069 ),
5070 "When selecting past the indent, nothing is trimmed"
5071 );
5072}
5073
5074#[gpui::test]
5075async fn test_paste_multiline(cx: &mut TestAppContext) {
5076 init_test(cx, |_| {});
5077
5078 let mut cx = EditorTestContext::new(cx).await;
5079 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5080
5081 // Cut an indented block, without the leading whitespace.
5082 cx.set_state(indoc! {"
5083 const a: B = (
5084 c(),
5085 «d(
5086 e,
5087 f
5088 )ˇ»
5089 );
5090 "});
5091 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5092 cx.assert_editor_state(indoc! {"
5093 const a: B = (
5094 c(),
5095 ˇ
5096 );
5097 "});
5098
5099 // Paste it at the same position.
5100 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5101 cx.assert_editor_state(indoc! {"
5102 const a: B = (
5103 c(),
5104 d(
5105 e,
5106 f
5107 )ˇ
5108 );
5109 "});
5110
5111 // Paste it at a line with a lower indent level.
5112 cx.set_state(indoc! {"
5113 ˇ
5114 const a: B = (
5115 c(),
5116 );
5117 "});
5118 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5119 cx.assert_editor_state(indoc! {"
5120 d(
5121 e,
5122 f
5123 )ˇ
5124 const a: B = (
5125 c(),
5126 );
5127 "});
5128
5129 // Cut an indented block, with the leading whitespace.
5130 cx.set_state(indoc! {"
5131 const a: B = (
5132 c(),
5133 « d(
5134 e,
5135 f
5136 )
5137 ˇ»);
5138 "});
5139 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5140 cx.assert_editor_state(indoc! {"
5141 const a: B = (
5142 c(),
5143 ˇ);
5144 "});
5145
5146 // Paste it at the same position.
5147 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5148 cx.assert_editor_state(indoc! {"
5149 const a: B = (
5150 c(),
5151 d(
5152 e,
5153 f
5154 )
5155 ˇ);
5156 "});
5157
5158 // Paste it at a line with a higher indent level.
5159 cx.set_state(indoc! {"
5160 const a: B = (
5161 c(),
5162 d(
5163 e,
5164 fˇ
5165 )
5166 );
5167 "});
5168 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5169 cx.assert_editor_state(indoc! {"
5170 const a: B = (
5171 c(),
5172 d(
5173 e,
5174 f d(
5175 e,
5176 f
5177 )
5178 ˇ
5179 )
5180 );
5181 "});
5182
5183 // Copy an indented block, starting mid-line
5184 cx.set_state(indoc! {"
5185 const a: B = (
5186 c(),
5187 somethin«g(
5188 e,
5189 f
5190 )ˇ»
5191 );
5192 "});
5193 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5194
5195 // Paste it on a line with a lower indent level
5196 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
5197 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5198 cx.assert_editor_state(indoc! {"
5199 const a: B = (
5200 c(),
5201 something(
5202 e,
5203 f
5204 )
5205 );
5206 g(
5207 e,
5208 f
5209 )ˇ"});
5210}
5211
5212#[gpui::test]
5213async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
5214 init_test(cx, |_| {});
5215
5216 cx.write_to_clipboard(ClipboardItem::new_string(
5217 " d(\n e\n );\n".into(),
5218 ));
5219
5220 let mut cx = EditorTestContext::new(cx).await;
5221 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5222
5223 cx.set_state(indoc! {"
5224 fn a() {
5225 b();
5226 if c() {
5227 ˇ
5228 }
5229 }
5230 "});
5231
5232 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5233 cx.assert_editor_state(indoc! {"
5234 fn a() {
5235 b();
5236 if c() {
5237 d(
5238 e
5239 );
5240 ˇ
5241 }
5242 }
5243 "});
5244
5245 cx.set_state(indoc! {"
5246 fn a() {
5247 b();
5248 ˇ
5249 }
5250 "});
5251
5252 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5253 cx.assert_editor_state(indoc! {"
5254 fn a() {
5255 b();
5256 d(
5257 e
5258 );
5259 ˇ
5260 }
5261 "});
5262}
5263
5264#[gpui::test]
5265fn test_select_all(cx: &mut TestAppContext) {
5266 init_test(cx, |_| {});
5267
5268 let editor = cx.add_window(|window, cx| {
5269 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
5270 build_editor(buffer, window, cx)
5271 });
5272 _ = editor.update(cx, |editor, window, cx| {
5273 editor.select_all(&SelectAll, window, cx);
5274 assert_eq!(
5275 editor.selections.display_ranges(cx),
5276 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
5277 );
5278 });
5279}
5280
5281#[gpui::test]
5282fn test_select_line(cx: &mut TestAppContext) {
5283 init_test(cx, |_| {});
5284
5285 let editor = cx.add_window(|window, cx| {
5286 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
5287 build_editor(buffer, window, cx)
5288 });
5289 _ = editor.update(cx, |editor, window, cx| {
5290 editor.change_selections(None, window, cx, |s| {
5291 s.select_display_ranges([
5292 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5293 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5294 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5295 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
5296 ])
5297 });
5298 editor.select_line(&SelectLine, window, cx);
5299 assert_eq!(
5300 editor.selections.display_ranges(cx),
5301 vec![
5302 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
5303 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
5304 ]
5305 );
5306 });
5307
5308 _ = editor.update(cx, |editor, window, cx| {
5309 editor.select_line(&SelectLine, window, cx);
5310 assert_eq!(
5311 editor.selections.display_ranges(cx),
5312 vec![
5313 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
5314 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
5315 ]
5316 );
5317 });
5318
5319 _ = editor.update(cx, |editor, window, cx| {
5320 editor.select_line(&SelectLine, window, cx);
5321 assert_eq!(
5322 editor.selections.display_ranges(cx),
5323 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
5324 );
5325 });
5326}
5327
5328#[gpui::test]
5329async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
5330 init_test(cx, |_| {});
5331 let mut cx = EditorTestContext::new(cx).await;
5332
5333 #[track_caller]
5334 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
5335 cx.set_state(initial_state);
5336 cx.update_editor(|e, window, cx| {
5337 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
5338 });
5339 cx.assert_editor_state(expected_state);
5340 }
5341
5342 // Selection starts and ends at the middle of lines, left-to-right
5343 test(
5344 &mut cx,
5345 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
5346 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5347 );
5348 // Same thing, right-to-left
5349 test(
5350 &mut cx,
5351 "aa\nb«b\ncc\ndd\neˇ»e\nff",
5352 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5353 );
5354
5355 // Whole buffer, left-to-right, last line *doesn't* end with newline
5356 test(
5357 &mut cx,
5358 "«ˇaa\nbb\ncc\ndd\nee\nff»",
5359 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5360 );
5361 // Same thing, right-to-left
5362 test(
5363 &mut cx,
5364 "«aa\nbb\ncc\ndd\nee\nffˇ»",
5365 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5366 );
5367
5368 // Whole buffer, left-to-right, last line ends with newline
5369 test(
5370 &mut cx,
5371 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
5372 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5373 );
5374 // Same thing, right-to-left
5375 test(
5376 &mut cx,
5377 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
5378 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5379 );
5380
5381 // Starts at the end of a line, ends at the start of another
5382 test(
5383 &mut cx,
5384 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
5385 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
5386 );
5387}
5388
5389#[gpui::test]
5390async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
5391 init_test(cx, |_| {});
5392
5393 let editor = cx.add_window(|window, cx| {
5394 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
5395 build_editor(buffer, window, cx)
5396 });
5397
5398 // setup
5399 _ = editor.update(cx, |editor, window, cx| {
5400 editor.fold_creases(
5401 vec![
5402 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5403 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5404 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5405 ],
5406 true,
5407 window,
5408 cx,
5409 );
5410 assert_eq!(
5411 editor.display_text(cx),
5412 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5413 );
5414 });
5415
5416 _ = editor.update(cx, |editor, window, cx| {
5417 editor.change_selections(None, window, cx, |s| {
5418 s.select_display_ranges([
5419 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5420 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5421 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5422 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
5423 ])
5424 });
5425 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5426 assert_eq!(
5427 editor.display_text(cx),
5428 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5429 );
5430 });
5431 EditorTestContext::for_editor(editor, cx)
5432 .await
5433 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
5434
5435 _ = editor.update(cx, |editor, window, cx| {
5436 editor.change_selections(None, window, cx, |s| {
5437 s.select_display_ranges([
5438 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
5439 ])
5440 });
5441 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5442 assert_eq!(
5443 editor.display_text(cx),
5444 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
5445 );
5446 assert_eq!(
5447 editor.selections.display_ranges(cx),
5448 [
5449 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
5450 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
5451 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
5452 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
5453 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
5454 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
5455 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
5456 ]
5457 );
5458 });
5459 EditorTestContext::for_editor(editor, cx)
5460 .await
5461 .assert_editor_state(
5462 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
5463 );
5464}
5465
5466#[gpui::test]
5467async fn test_add_selection_above_below(cx: &mut TestAppContext) {
5468 init_test(cx, |_| {});
5469
5470 let mut cx = EditorTestContext::new(cx).await;
5471
5472 cx.set_state(indoc!(
5473 r#"abc
5474 defˇghi
5475
5476 jk
5477 nlmo
5478 "#
5479 ));
5480
5481 cx.update_editor(|editor, window, cx| {
5482 editor.add_selection_above(&Default::default(), window, cx);
5483 });
5484
5485 cx.assert_editor_state(indoc!(
5486 r#"abcˇ
5487 defˇghi
5488
5489 jk
5490 nlmo
5491 "#
5492 ));
5493
5494 cx.update_editor(|editor, window, cx| {
5495 editor.add_selection_above(&Default::default(), window, cx);
5496 });
5497
5498 cx.assert_editor_state(indoc!(
5499 r#"abcˇ
5500 defˇghi
5501
5502 jk
5503 nlmo
5504 "#
5505 ));
5506
5507 cx.update_editor(|editor, window, cx| {
5508 editor.add_selection_below(&Default::default(), window, cx);
5509 });
5510
5511 cx.assert_editor_state(indoc!(
5512 r#"abc
5513 defˇghi
5514
5515 jk
5516 nlmo
5517 "#
5518 ));
5519
5520 cx.update_editor(|editor, window, cx| {
5521 editor.undo_selection(&Default::default(), window, cx);
5522 });
5523
5524 cx.assert_editor_state(indoc!(
5525 r#"abcˇ
5526 defˇghi
5527
5528 jk
5529 nlmo
5530 "#
5531 ));
5532
5533 cx.update_editor(|editor, window, cx| {
5534 editor.redo_selection(&Default::default(), window, cx);
5535 });
5536
5537 cx.assert_editor_state(indoc!(
5538 r#"abc
5539 defˇghi
5540
5541 jk
5542 nlmo
5543 "#
5544 ));
5545
5546 cx.update_editor(|editor, window, cx| {
5547 editor.add_selection_below(&Default::default(), window, cx);
5548 });
5549
5550 cx.assert_editor_state(indoc!(
5551 r#"abc
5552 defˇghi
5553
5554 jk
5555 nlmˇo
5556 "#
5557 ));
5558
5559 cx.update_editor(|editor, window, cx| {
5560 editor.add_selection_below(&Default::default(), window, cx);
5561 });
5562
5563 cx.assert_editor_state(indoc!(
5564 r#"abc
5565 defˇghi
5566
5567 jk
5568 nlmˇo
5569 "#
5570 ));
5571
5572 // change selections
5573 cx.set_state(indoc!(
5574 r#"abc
5575 def«ˇg»hi
5576
5577 jk
5578 nlmo
5579 "#
5580 ));
5581
5582 cx.update_editor(|editor, window, cx| {
5583 editor.add_selection_below(&Default::default(), window, cx);
5584 });
5585
5586 cx.assert_editor_state(indoc!(
5587 r#"abc
5588 def«ˇg»hi
5589
5590 jk
5591 nlm«ˇo»
5592 "#
5593 ));
5594
5595 cx.update_editor(|editor, window, cx| {
5596 editor.add_selection_below(&Default::default(), window, cx);
5597 });
5598
5599 cx.assert_editor_state(indoc!(
5600 r#"abc
5601 def«ˇg»hi
5602
5603 jk
5604 nlm«ˇo»
5605 "#
5606 ));
5607
5608 cx.update_editor(|editor, window, cx| {
5609 editor.add_selection_above(&Default::default(), window, cx);
5610 });
5611
5612 cx.assert_editor_state(indoc!(
5613 r#"abc
5614 def«ˇg»hi
5615
5616 jk
5617 nlmo
5618 "#
5619 ));
5620
5621 cx.update_editor(|editor, window, cx| {
5622 editor.add_selection_above(&Default::default(), window, cx);
5623 });
5624
5625 cx.assert_editor_state(indoc!(
5626 r#"abc
5627 def«ˇg»hi
5628
5629 jk
5630 nlmo
5631 "#
5632 ));
5633
5634 // Change selections again
5635 cx.set_state(indoc!(
5636 r#"a«bc
5637 defgˇ»hi
5638
5639 jk
5640 nlmo
5641 "#
5642 ));
5643
5644 cx.update_editor(|editor, window, cx| {
5645 editor.add_selection_below(&Default::default(), window, cx);
5646 });
5647
5648 cx.assert_editor_state(indoc!(
5649 r#"a«bcˇ»
5650 d«efgˇ»hi
5651
5652 j«kˇ»
5653 nlmo
5654 "#
5655 ));
5656
5657 cx.update_editor(|editor, window, cx| {
5658 editor.add_selection_below(&Default::default(), window, cx);
5659 });
5660 cx.assert_editor_state(indoc!(
5661 r#"a«bcˇ»
5662 d«efgˇ»hi
5663
5664 j«kˇ»
5665 n«lmoˇ»
5666 "#
5667 ));
5668 cx.update_editor(|editor, window, cx| {
5669 editor.add_selection_above(&Default::default(), window, cx);
5670 });
5671
5672 cx.assert_editor_state(indoc!(
5673 r#"a«bcˇ»
5674 d«efgˇ»hi
5675
5676 j«kˇ»
5677 nlmo
5678 "#
5679 ));
5680
5681 // Change selections again
5682 cx.set_state(indoc!(
5683 r#"abc
5684 d«ˇefghi
5685
5686 jk
5687 nlm»o
5688 "#
5689 ));
5690
5691 cx.update_editor(|editor, window, cx| {
5692 editor.add_selection_above(&Default::default(), window, cx);
5693 });
5694
5695 cx.assert_editor_state(indoc!(
5696 r#"a«ˇbc»
5697 d«ˇef»ghi
5698
5699 j«ˇk»
5700 n«ˇlm»o
5701 "#
5702 ));
5703
5704 cx.update_editor(|editor, window, cx| {
5705 editor.add_selection_below(&Default::default(), window, cx);
5706 });
5707
5708 cx.assert_editor_state(indoc!(
5709 r#"abc
5710 d«ˇef»ghi
5711
5712 j«ˇk»
5713 n«ˇlm»o
5714 "#
5715 ));
5716}
5717
5718#[gpui::test]
5719async fn test_select_next(cx: &mut TestAppContext) {
5720 init_test(cx, |_| {});
5721
5722 let mut cx = EditorTestContext::new(cx).await;
5723 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5724
5725 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5726 .unwrap();
5727 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5728
5729 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5730 .unwrap();
5731 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5732
5733 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5734 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5735
5736 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5737 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5738
5739 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5740 .unwrap();
5741 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5742
5743 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5744 .unwrap();
5745 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5746}
5747
5748#[gpui::test]
5749async fn test_select_all_matches(cx: &mut TestAppContext) {
5750 init_test(cx, |_| {});
5751
5752 let mut cx = EditorTestContext::new(cx).await;
5753
5754 // Test caret-only selections
5755 cx.set_state("abc\nˇabc abc\ndefabc\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ˇ» «abcˇ»\ndefabc\n«abcˇ»");
5759
5760 // Test left-to-right 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 right-to-left selections
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»\n«ˇabc»");
5771
5772 // Test selecting whitespace with caret 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 selecting whitespace with left-to-right 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 // Test no matches with right-to-left selection
5785 cx.set_state("abc\n« ˇ»abc\nabc");
5786 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5787 .unwrap();
5788 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
5789}
5790
5791#[gpui::test]
5792async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
5793 init_test(cx, |_| {});
5794
5795 let mut cx = EditorTestContext::new(cx).await;
5796 cx.set_state(
5797 r#"let foo = 2;
5798lˇet foo = 2;
5799let fooˇ = 2;
5800let foo = 2;
5801let foo = ˇ2;"#,
5802 );
5803
5804 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5805 .unwrap();
5806 cx.assert_editor_state(
5807 r#"let foo = 2;
5808«letˇ» foo = 2;
5809let «fooˇ» = 2;
5810let foo = 2;
5811let foo = «2ˇ»;"#,
5812 );
5813
5814 // noop for multiple selections with different contents
5815 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5816 .unwrap();
5817 cx.assert_editor_state(
5818 r#"let foo = 2;
5819«letˇ» foo = 2;
5820let «fooˇ» = 2;
5821let foo = 2;
5822let foo = «2ˇ»;"#,
5823 );
5824}
5825
5826#[gpui::test]
5827async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
5828 init_test(cx, |_| {});
5829
5830 let mut cx =
5831 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
5832
5833 cx.assert_editor_state(indoc! {"
5834 ˇbbb
5835 ccc
5836
5837 bbb
5838 ccc
5839 "});
5840 cx.dispatch_action(SelectPrevious::default());
5841 cx.assert_editor_state(indoc! {"
5842 «bbbˇ»
5843 ccc
5844
5845 bbb
5846 ccc
5847 "});
5848 cx.dispatch_action(SelectPrevious::default());
5849 cx.assert_editor_state(indoc! {"
5850 «bbbˇ»
5851 ccc
5852
5853 «bbbˇ»
5854 ccc
5855 "});
5856}
5857
5858#[gpui::test]
5859async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
5860 init_test(cx, |_| {});
5861
5862 let mut cx = EditorTestContext::new(cx).await;
5863 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5864
5865 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5866 .unwrap();
5867 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5868
5869 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5870 .unwrap();
5871 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
5872
5873 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5874 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5875
5876 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5877 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
5878
5879 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5880 .unwrap();
5881 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
5882
5883 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5884 .unwrap();
5885 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndef«abcˇ»\n«abcˇ»");
5886
5887 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5888 .unwrap();
5889 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
5890}
5891
5892#[gpui::test]
5893async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
5894 init_test(cx, |_| {});
5895
5896 let mut cx = EditorTestContext::new(cx).await;
5897 cx.set_state("aˇ");
5898
5899 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5900 .unwrap();
5901 cx.assert_editor_state("«aˇ»");
5902 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5903 .unwrap();
5904 cx.assert_editor_state("«aˇ»");
5905}
5906
5907#[gpui::test]
5908async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
5909 init_test(cx, |_| {});
5910
5911 let mut cx = EditorTestContext::new(cx).await;
5912 cx.set_state(
5913 r#"let foo = 2;
5914lˇet foo = 2;
5915let fooˇ = 2;
5916let foo = 2;
5917let foo = ˇ2;"#,
5918 );
5919
5920 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5921 .unwrap();
5922 cx.assert_editor_state(
5923 r#"let foo = 2;
5924«letˇ» foo = 2;
5925let «fooˇ» = 2;
5926let foo = 2;
5927let foo = «2ˇ»;"#,
5928 );
5929
5930 // noop for multiple selections with different contents
5931 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5932 .unwrap();
5933 cx.assert_editor_state(
5934 r#"let foo = 2;
5935«letˇ» foo = 2;
5936let «fooˇ» = 2;
5937let foo = 2;
5938let foo = «2ˇ»;"#,
5939 );
5940}
5941
5942#[gpui::test]
5943async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
5944 init_test(cx, |_| {});
5945
5946 let mut cx = EditorTestContext::new(cx).await;
5947 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
5948
5949 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5950 .unwrap();
5951 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
5952
5953 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5954 .unwrap();
5955 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
5956
5957 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5958 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
5959
5960 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5961 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
5962
5963 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5964 .unwrap();
5965 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
5966
5967 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5968 .unwrap();
5969 cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
5970}
5971
5972#[gpui::test]
5973async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
5974 init_test(cx, |_| {});
5975
5976 let language = Arc::new(Language::new(
5977 LanguageConfig::default(),
5978 Some(tree_sitter_rust::LANGUAGE.into()),
5979 ));
5980
5981 let text = r#"
5982 use mod1::mod2::{mod3, mod4};
5983
5984 fn fn_1(param1: bool, param2: &str) {
5985 let var1 = "text";
5986 }
5987 "#
5988 .unindent();
5989
5990 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
5991 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
5992 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
5993
5994 editor
5995 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
5996 .await;
5997
5998 editor.update_in(cx, |editor, window, cx| {
5999 editor.change_selections(None, window, cx, |s| {
6000 s.select_display_ranges([
6001 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
6002 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
6003 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
6004 ]);
6005 });
6006 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6007 });
6008 editor.update(cx, |editor, cx| {
6009 assert_text_with_selections(
6010 editor,
6011 indoc! {r#"
6012 use mod1::mod2::{mod3, «mod4ˇ»};
6013
6014 fn fn_1«ˇ(param1: bool, param2: &str)» {
6015 let var1 = "«ˇtext»";
6016 }
6017 "#},
6018 cx,
6019 );
6020 });
6021
6022 editor.update_in(cx, |editor, window, cx| {
6023 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6024 });
6025 editor.update(cx, |editor, cx| {
6026 assert_text_with_selections(
6027 editor,
6028 indoc! {r#"
6029 use mod1::mod2::«{mod3, mod4}ˇ»;
6030
6031 «ˇfn fn_1(param1: bool, param2: &str) {
6032 let var1 = "text";
6033 }»
6034 "#},
6035 cx,
6036 );
6037 });
6038
6039 editor.update_in(cx, |editor, window, cx| {
6040 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6041 });
6042 assert_eq!(
6043 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6044 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6045 );
6046
6047 // Trying to expand the selected syntax node one more time has no effect.
6048 editor.update_in(cx, |editor, window, cx| {
6049 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6050 });
6051 assert_eq!(
6052 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6053 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6054 );
6055
6056 editor.update_in(cx, |editor, window, cx| {
6057 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6058 });
6059 editor.update(cx, |editor, cx| {
6060 assert_text_with_selections(
6061 editor,
6062 indoc! {r#"
6063 use mod1::mod2::«{mod3, mod4}ˇ»;
6064
6065 «ˇfn fn_1(param1: bool, param2: &str) {
6066 let var1 = "text";
6067 }»
6068 "#},
6069 cx,
6070 );
6071 });
6072
6073 editor.update_in(cx, |editor, window, cx| {
6074 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6075 });
6076 editor.update(cx, |editor, cx| {
6077 assert_text_with_selections(
6078 editor,
6079 indoc! {r#"
6080 use mod1::mod2::{mod3, «mod4ˇ»};
6081
6082 fn fn_1«ˇ(param1: bool, param2: &str)» {
6083 let var1 = "«ˇtext»";
6084 }
6085 "#},
6086 cx,
6087 );
6088 });
6089
6090 editor.update_in(cx, |editor, window, cx| {
6091 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6092 });
6093 editor.update(cx, |editor, cx| {
6094 assert_text_with_selections(
6095 editor,
6096 indoc! {r#"
6097 use mod1::mod2::{mod3, mo«ˇ»d4};
6098
6099 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6100 let var1 = "te«ˇ»xt";
6101 }
6102 "#},
6103 cx,
6104 );
6105 });
6106
6107 // Trying to shrink the selected syntax node one more time has no effect.
6108 editor.update_in(cx, |editor, window, cx| {
6109 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6110 });
6111 editor.update_in(cx, |editor, _, cx| {
6112 assert_text_with_selections(
6113 editor,
6114 indoc! {r#"
6115 use mod1::mod2::{mod3, mo«ˇ»d4};
6116
6117 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6118 let var1 = "te«ˇ»xt";
6119 }
6120 "#},
6121 cx,
6122 );
6123 });
6124
6125 // Ensure that we keep expanding the selection if the larger selection starts or ends within
6126 // a fold.
6127 editor.update_in(cx, |editor, window, cx| {
6128 editor.fold_creases(
6129 vec![
6130 Crease::simple(
6131 Point::new(0, 21)..Point::new(0, 24),
6132 FoldPlaceholder::test(),
6133 ),
6134 Crease::simple(
6135 Point::new(3, 20)..Point::new(3, 22),
6136 FoldPlaceholder::test(),
6137 ),
6138 ],
6139 true,
6140 window,
6141 cx,
6142 );
6143 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6144 });
6145 editor.update(cx, |editor, cx| {
6146 assert_text_with_selections(
6147 editor,
6148 indoc! {r#"
6149 use mod1::mod2::«{mod3, mod4}ˇ»;
6150
6151 fn fn_1«ˇ(param1: bool, param2: &str)» {
6152 «ˇlet var1 = "text";»
6153 }
6154 "#},
6155 cx,
6156 );
6157 });
6158}
6159
6160#[gpui::test]
6161async fn test_fold_function_bodies(cx: &mut TestAppContext) {
6162 init_test(cx, |_| {});
6163
6164 let base_text = r#"
6165 impl A {
6166 // this is an uncommitted comment
6167
6168 fn b() {
6169 c();
6170 }
6171
6172 // this is another uncommitted comment
6173
6174 fn d() {
6175 // e
6176 // f
6177 }
6178 }
6179
6180 fn g() {
6181 // h
6182 }
6183 "#
6184 .unindent();
6185
6186 let text = r#"
6187 ˇimpl A {
6188
6189 fn b() {
6190 c();
6191 }
6192
6193 fn d() {
6194 // e
6195 // f
6196 }
6197 }
6198
6199 fn g() {
6200 // h
6201 }
6202 "#
6203 .unindent();
6204
6205 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6206 cx.set_state(&text);
6207 cx.set_head_text(&base_text);
6208 cx.update_editor(|editor, window, cx| {
6209 editor.expand_all_diff_hunks(&Default::default(), window, cx);
6210 });
6211
6212 cx.assert_state_with_diff(
6213 "
6214 ˇimpl A {
6215 - // this is an uncommitted comment
6216
6217 fn b() {
6218 c();
6219 }
6220
6221 - // this is another uncommitted comment
6222 -
6223 fn d() {
6224 // e
6225 // f
6226 }
6227 }
6228
6229 fn g() {
6230 // h
6231 }
6232 "
6233 .unindent(),
6234 );
6235
6236 let expected_display_text = "
6237 impl A {
6238 // this is an uncommitted comment
6239
6240 fn b() {
6241 ⋯
6242 }
6243
6244 // this is another uncommitted comment
6245
6246 fn d() {
6247 ⋯
6248 }
6249 }
6250
6251 fn g() {
6252 ⋯
6253 }
6254 "
6255 .unindent();
6256
6257 cx.update_editor(|editor, window, cx| {
6258 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
6259 assert_eq!(editor.display_text(cx), expected_display_text);
6260 });
6261}
6262
6263#[gpui::test]
6264async fn test_autoindent(cx: &mut TestAppContext) {
6265 init_test(cx, |_| {});
6266
6267 let language = Arc::new(
6268 Language::new(
6269 LanguageConfig {
6270 brackets: BracketPairConfig {
6271 pairs: vec![
6272 BracketPair {
6273 start: "{".to_string(),
6274 end: "}".to_string(),
6275 close: false,
6276 surround: false,
6277 newline: true,
6278 },
6279 BracketPair {
6280 start: "(".to_string(),
6281 end: ")".to_string(),
6282 close: false,
6283 surround: false,
6284 newline: true,
6285 },
6286 ],
6287 ..Default::default()
6288 },
6289 ..Default::default()
6290 },
6291 Some(tree_sitter_rust::LANGUAGE.into()),
6292 )
6293 .with_indents_query(
6294 r#"
6295 (_ "(" ")" @end) @indent
6296 (_ "{" "}" @end) @indent
6297 "#,
6298 )
6299 .unwrap(),
6300 );
6301
6302 let text = "fn a() {}";
6303
6304 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6305 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6306 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6307 editor
6308 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6309 .await;
6310
6311 editor.update_in(cx, |editor, window, cx| {
6312 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
6313 editor.newline(&Newline, window, cx);
6314 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
6315 assert_eq!(
6316 editor.selections.ranges(cx),
6317 &[
6318 Point::new(1, 4)..Point::new(1, 4),
6319 Point::new(3, 4)..Point::new(3, 4),
6320 Point::new(5, 0)..Point::new(5, 0)
6321 ]
6322 );
6323 });
6324}
6325
6326#[gpui::test]
6327async fn test_autoindent_selections(cx: &mut TestAppContext) {
6328 init_test(cx, |_| {});
6329
6330 {
6331 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6332 cx.set_state(indoc! {"
6333 impl A {
6334
6335 fn b() {}
6336
6337 «fn c() {
6338
6339 }ˇ»
6340 }
6341 "});
6342
6343 cx.update_editor(|editor, window, cx| {
6344 editor.autoindent(&Default::default(), window, cx);
6345 });
6346
6347 cx.assert_editor_state(indoc! {"
6348 impl A {
6349
6350 fn b() {}
6351
6352 «fn c() {
6353
6354 }ˇ»
6355 }
6356 "});
6357 }
6358
6359 {
6360 let mut cx = EditorTestContext::new_multibuffer(
6361 cx,
6362 [indoc! { "
6363 impl A {
6364 «
6365 // a
6366 fn b(){}
6367 »
6368 «
6369 }
6370 fn c(){}
6371 »
6372 "}],
6373 );
6374
6375 let buffer = cx.update_editor(|editor, _, cx| {
6376 let buffer = editor.buffer().update(cx, |buffer, _| {
6377 buffer.all_buffers().iter().next().unwrap().clone()
6378 });
6379 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6380 buffer
6381 });
6382
6383 cx.run_until_parked();
6384 cx.update_editor(|editor, window, cx| {
6385 editor.select_all(&Default::default(), window, cx);
6386 editor.autoindent(&Default::default(), window, cx)
6387 });
6388 cx.run_until_parked();
6389
6390 cx.update(|_, cx| {
6391 pretty_assertions::assert_eq!(
6392 buffer.read(cx).text(),
6393 indoc! { "
6394 impl A {
6395
6396 // a
6397 fn b(){}
6398
6399
6400 }
6401 fn c(){}
6402
6403 " }
6404 )
6405 });
6406 }
6407}
6408
6409#[gpui::test]
6410async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
6411 init_test(cx, |_| {});
6412
6413 let mut cx = EditorTestContext::new(cx).await;
6414
6415 let language = Arc::new(Language::new(
6416 LanguageConfig {
6417 brackets: BracketPairConfig {
6418 pairs: vec![
6419 BracketPair {
6420 start: "{".to_string(),
6421 end: "}".to_string(),
6422 close: true,
6423 surround: true,
6424 newline: true,
6425 },
6426 BracketPair {
6427 start: "(".to_string(),
6428 end: ")".to_string(),
6429 close: true,
6430 surround: true,
6431 newline: true,
6432 },
6433 BracketPair {
6434 start: "/*".to_string(),
6435 end: " */".to_string(),
6436 close: true,
6437 surround: true,
6438 newline: true,
6439 },
6440 BracketPair {
6441 start: "[".to_string(),
6442 end: "]".to_string(),
6443 close: false,
6444 surround: false,
6445 newline: true,
6446 },
6447 BracketPair {
6448 start: "\"".to_string(),
6449 end: "\"".to_string(),
6450 close: true,
6451 surround: true,
6452 newline: false,
6453 },
6454 BracketPair {
6455 start: "<".to_string(),
6456 end: ">".to_string(),
6457 close: false,
6458 surround: true,
6459 newline: true,
6460 },
6461 ],
6462 ..Default::default()
6463 },
6464 autoclose_before: "})]".to_string(),
6465 ..Default::default()
6466 },
6467 Some(tree_sitter_rust::LANGUAGE.into()),
6468 ));
6469
6470 cx.language_registry().add(language.clone());
6471 cx.update_buffer(|buffer, cx| {
6472 buffer.set_language(Some(language), cx);
6473 });
6474
6475 cx.set_state(
6476 &r#"
6477 🏀ˇ
6478 εˇ
6479 ❤️ˇ
6480 "#
6481 .unindent(),
6482 );
6483
6484 // autoclose multiple nested brackets at multiple cursors
6485 cx.update_editor(|editor, window, cx| {
6486 editor.handle_input("{", window, cx);
6487 editor.handle_input("{", window, cx);
6488 editor.handle_input("{", window, cx);
6489 });
6490 cx.assert_editor_state(
6491 &"
6492 🏀{{{ˇ}}}
6493 ε{{{ˇ}}}
6494 ❤️{{{ˇ}}}
6495 "
6496 .unindent(),
6497 );
6498
6499 // insert a different closing bracket
6500 cx.update_editor(|editor, window, cx| {
6501 editor.handle_input(")", window, cx);
6502 });
6503 cx.assert_editor_state(
6504 &"
6505 🏀{{{)ˇ}}}
6506 ε{{{)ˇ}}}
6507 ❤️{{{)ˇ}}}
6508 "
6509 .unindent(),
6510 );
6511
6512 // skip over the auto-closed brackets when typing a closing bracket
6513 cx.update_editor(|editor, window, cx| {
6514 editor.move_right(&MoveRight, window, cx);
6515 editor.handle_input("}", window, cx);
6516 editor.handle_input("}", window, cx);
6517 editor.handle_input("}", window, cx);
6518 });
6519 cx.assert_editor_state(
6520 &"
6521 🏀{{{)}}}}ˇ
6522 ε{{{)}}}}ˇ
6523 ❤️{{{)}}}}ˇ
6524 "
6525 .unindent(),
6526 );
6527
6528 // autoclose multi-character pairs
6529 cx.set_state(
6530 &"
6531 ˇ
6532 ˇ
6533 "
6534 .unindent(),
6535 );
6536 cx.update_editor(|editor, window, cx| {
6537 editor.handle_input("/", window, cx);
6538 editor.handle_input("*", window, cx);
6539 });
6540 cx.assert_editor_state(
6541 &"
6542 /*ˇ */
6543 /*ˇ */
6544 "
6545 .unindent(),
6546 );
6547
6548 // one cursor autocloses a multi-character pair, one cursor
6549 // does not autoclose.
6550 cx.set_state(
6551 &"
6552 /ˇ
6553 ˇ
6554 "
6555 .unindent(),
6556 );
6557 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
6558 cx.assert_editor_state(
6559 &"
6560 /*ˇ */
6561 *ˇ
6562 "
6563 .unindent(),
6564 );
6565
6566 // Don't autoclose if the next character isn't whitespace and isn't
6567 // listed in the language's "autoclose_before" section.
6568 cx.set_state("ˇa b");
6569 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6570 cx.assert_editor_state("{ˇa b");
6571
6572 // Don't autoclose if `close` is false for the bracket pair
6573 cx.set_state("ˇ");
6574 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
6575 cx.assert_editor_state("[ˇ");
6576
6577 // Surround with brackets if text is selected
6578 cx.set_state("«aˇ» b");
6579 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6580 cx.assert_editor_state("{«aˇ»} b");
6581
6582 // Autoclose when not immediately after a word character
6583 cx.set_state("a ˇ");
6584 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6585 cx.assert_editor_state("a \"ˇ\"");
6586
6587 // Autoclose pair where the start and end characters are the same
6588 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6589 cx.assert_editor_state("a \"\"ˇ");
6590
6591 // Don't autoclose when immediately after a word character
6592 cx.set_state("aˇ");
6593 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6594 cx.assert_editor_state("a\"ˇ");
6595
6596 // Do autoclose when after a non-word character
6597 cx.set_state("{ˇ");
6598 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6599 cx.assert_editor_state("{\"ˇ\"");
6600
6601 // Non identical pairs autoclose regardless of preceding character
6602 cx.set_state("aˇ");
6603 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6604 cx.assert_editor_state("a{ˇ}");
6605
6606 // Don't autoclose pair if autoclose is disabled
6607 cx.set_state("ˇ");
6608 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6609 cx.assert_editor_state("<ˇ");
6610
6611 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
6612 cx.set_state("«aˇ» b");
6613 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6614 cx.assert_editor_state("<«aˇ»> b");
6615}
6616
6617#[gpui::test]
6618async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
6619 init_test(cx, |settings| {
6620 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
6621 });
6622
6623 let mut cx = EditorTestContext::new(cx).await;
6624
6625 let language = Arc::new(Language::new(
6626 LanguageConfig {
6627 brackets: BracketPairConfig {
6628 pairs: vec![
6629 BracketPair {
6630 start: "{".to_string(),
6631 end: "}".to_string(),
6632 close: true,
6633 surround: true,
6634 newline: true,
6635 },
6636 BracketPair {
6637 start: "(".to_string(),
6638 end: ")".to_string(),
6639 close: true,
6640 surround: true,
6641 newline: true,
6642 },
6643 BracketPair {
6644 start: "[".to_string(),
6645 end: "]".to_string(),
6646 close: false,
6647 surround: false,
6648 newline: true,
6649 },
6650 ],
6651 ..Default::default()
6652 },
6653 autoclose_before: "})]".to_string(),
6654 ..Default::default()
6655 },
6656 Some(tree_sitter_rust::LANGUAGE.into()),
6657 ));
6658
6659 cx.language_registry().add(language.clone());
6660 cx.update_buffer(|buffer, cx| {
6661 buffer.set_language(Some(language), cx);
6662 });
6663
6664 cx.set_state(
6665 &"
6666 ˇ
6667 ˇ
6668 ˇ
6669 "
6670 .unindent(),
6671 );
6672
6673 // ensure only matching closing brackets are skipped over
6674 cx.update_editor(|editor, window, cx| {
6675 editor.handle_input("}", window, cx);
6676 editor.move_left(&MoveLeft, window, cx);
6677 editor.handle_input(")", window, cx);
6678 editor.move_left(&MoveLeft, window, cx);
6679 });
6680 cx.assert_editor_state(
6681 &"
6682 ˇ)}
6683 ˇ)}
6684 ˇ)}
6685 "
6686 .unindent(),
6687 );
6688
6689 // skip-over closing brackets at multiple cursors
6690 cx.update_editor(|editor, window, cx| {
6691 editor.handle_input(")", window, cx);
6692 editor.handle_input("}", window, cx);
6693 });
6694 cx.assert_editor_state(
6695 &"
6696 )}ˇ
6697 )}ˇ
6698 )}ˇ
6699 "
6700 .unindent(),
6701 );
6702
6703 // ignore non-close brackets
6704 cx.update_editor(|editor, window, cx| {
6705 editor.handle_input("]", window, cx);
6706 editor.move_left(&MoveLeft, window, cx);
6707 editor.handle_input("]", window, cx);
6708 });
6709 cx.assert_editor_state(
6710 &"
6711 )}]ˇ]
6712 )}]ˇ]
6713 )}]ˇ]
6714 "
6715 .unindent(),
6716 );
6717}
6718
6719#[gpui::test]
6720async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
6721 init_test(cx, |_| {});
6722
6723 let mut cx = EditorTestContext::new(cx).await;
6724
6725 let html_language = Arc::new(
6726 Language::new(
6727 LanguageConfig {
6728 name: "HTML".into(),
6729 brackets: BracketPairConfig {
6730 pairs: vec![
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 BracketPair {
6744 start: "(".into(),
6745 end: ")".into(),
6746 close: true,
6747 ..Default::default()
6748 },
6749 ],
6750 ..Default::default()
6751 },
6752 autoclose_before: "})]>".into(),
6753 ..Default::default()
6754 },
6755 Some(tree_sitter_html::LANGUAGE.into()),
6756 )
6757 .with_injection_query(
6758 r#"
6759 (script_element
6760 (raw_text) @injection.content
6761 (#set! injection.language "javascript"))
6762 "#,
6763 )
6764 .unwrap(),
6765 );
6766
6767 let javascript_language = Arc::new(Language::new(
6768 LanguageConfig {
6769 name: "JavaScript".into(),
6770 brackets: BracketPairConfig {
6771 pairs: vec![
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 BracketPair {
6785 start: "(".into(),
6786 end: ")".into(),
6787 close: true,
6788 ..Default::default()
6789 },
6790 ],
6791 ..Default::default()
6792 },
6793 autoclose_before: "})]>".into(),
6794 ..Default::default()
6795 },
6796 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
6797 ));
6798
6799 cx.language_registry().add(html_language.clone());
6800 cx.language_registry().add(javascript_language.clone());
6801
6802 cx.update_buffer(|buffer, cx| {
6803 buffer.set_language(Some(html_language), cx);
6804 });
6805
6806 cx.set_state(
6807 &r#"
6808 <body>ˇ
6809 <script>
6810 var x = 1;ˇ
6811 </script>
6812 </body>ˇ
6813 "#
6814 .unindent(),
6815 );
6816
6817 // Precondition: different languages are active at different locations.
6818 cx.update_editor(|editor, window, cx| {
6819 let snapshot = editor.snapshot(window, cx);
6820 let cursors = editor.selections.ranges::<usize>(cx);
6821 let languages = cursors
6822 .iter()
6823 .map(|c| snapshot.language_at(c.start).unwrap().name())
6824 .collect::<Vec<_>>();
6825 assert_eq!(
6826 languages,
6827 &["HTML".into(), "JavaScript".into(), "HTML".into()]
6828 );
6829 });
6830
6831 // Angle brackets autoclose in HTML, but not JavaScript.
6832 cx.update_editor(|editor, window, cx| {
6833 editor.handle_input("<", window, cx);
6834 editor.handle_input("a", window, cx);
6835 });
6836 cx.assert_editor_state(
6837 &r#"
6838 <body><aˇ>
6839 <script>
6840 var x = 1;<aˇ
6841 </script>
6842 </body><aˇ>
6843 "#
6844 .unindent(),
6845 );
6846
6847 // Curly braces and parens autoclose in both HTML and JavaScript.
6848 cx.update_editor(|editor, window, cx| {
6849 editor.handle_input(" b=", window, cx);
6850 editor.handle_input("{", window, cx);
6851 editor.handle_input("c", window, cx);
6852 editor.handle_input("(", window, cx);
6853 });
6854 cx.assert_editor_state(
6855 &r#"
6856 <body><a b={c(ˇ)}>
6857 <script>
6858 var x = 1;<a b={c(ˇ)}
6859 </script>
6860 </body><a b={c(ˇ)}>
6861 "#
6862 .unindent(),
6863 );
6864
6865 // Brackets that were already autoclosed are skipped.
6866 cx.update_editor(|editor, window, cx| {
6867 editor.handle_input(")", window, cx);
6868 editor.handle_input("d", window, cx);
6869 editor.handle_input("}", window, cx);
6870 });
6871 cx.assert_editor_state(
6872 &r#"
6873 <body><a b={c()d}ˇ>
6874 <script>
6875 var x = 1;<a b={c()d}ˇ
6876 </script>
6877 </body><a b={c()d}ˇ>
6878 "#
6879 .unindent(),
6880 );
6881 cx.update_editor(|editor, window, cx| {
6882 editor.handle_input(">", window, cx);
6883 });
6884 cx.assert_editor_state(
6885 &r#"
6886 <body><a b={c()d}>ˇ
6887 <script>
6888 var x = 1;<a b={c()d}>ˇ
6889 </script>
6890 </body><a b={c()d}>ˇ
6891 "#
6892 .unindent(),
6893 );
6894
6895 // Reset
6896 cx.set_state(
6897 &r#"
6898 <body>ˇ
6899 <script>
6900 var x = 1;ˇ
6901 </script>
6902 </body>ˇ
6903 "#
6904 .unindent(),
6905 );
6906
6907 cx.update_editor(|editor, window, cx| {
6908 editor.handle_input("<", window, cx);
6909 });
6910 cx.assert_editor_state(
6911 &r#"
6912 <body><ˇ>
6913 <script>
6914 var x = 1;<ˇ
6915 </script>
6916 </body><ˇ>
6917 "#
6918 .unindent(),
6919 );
6920
6921 // When backspacing, the closing angle brackets are removed.
6922 cx.update_editor(|editor, window, cx| {
6923 editor.backspace(&Backspace, window, cx);
6924 });
6925 cx.assert_editor_state(
6926 &r#"
6927 <body>ˇ
6928 <script>
6929 var x = 1;ˇ
6930 </script>
6931 </body>ˇ
6932 "#
6933 .unindent(),
6934 );
6935
6936 // Block comments autoclose in JavaScript, but not HTML.
6937 cx.update_editor(|editor, window, cx| {
6938 editor.handle_input("/", window, cx);
6939 editor.handle_input("*", window, cx);
6940 });
6941 cx.assert_editor_state(
6942 &r#"
6943 <body>/*ˇ
6944 <script>
6945 var x = 1;/*ˇ */
6946 </script>
6947 </body>/*ˇ
6948 "#
6949 .unindent(),
6950 );
6951}
6952
6953#[gpui::test]
6954async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
6955 init_test(cx, |_| {});
6956
6957 let mut cx = EditorTestContext::new(cx).await;
6958
6959 let rust_language = Arc::new(
6960 Language::new(
6961 LanguageConfig {
6962 name: "Rust".into(),
6963 brackets: serde_json::from_value(json!([
6964 { "start": "{", "end": "}", "close": true, "newline": true },
6965 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
6966 ]))
6967 .unwrap(),
6968 autoclose_before: "})]>".into(),
6969 ..Default::default()
6970 },
6971 Some(tree_sitter_rust::LANGUAGE.into()),
6972 )
6973 .with_override_query("(string_literal) @string")
6974 .unwrap(),
6975 );
6976
6977 cx.language_registry().add(rust_language.clone());
6978 cx.update_buffer(|buffer, cx| {
6979 buffer.set_language(Some(rust_language), cx);
6980 });
6981
6982 cx.set_state(
6983 &r#"
6984 let x = ˇ
6985 "#
6986 .unindent(),
6987 );
6988
6989 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
6990 cx.update_editor(|editor, window, cx| {
6991 editor.handle_input("\"", window, cx);
6992 });
6993 cx.assert_editor_state(
6994 &r#"
6995 let x = "ˇ"
6996 "#
6997 .unindent(),
6998 );
6999
7000 // Inserting another quotation mark. The cursor moves across the existing
7001 // automatically-inserted quotation mark.
7002 cx.update_editor(|editor, window, cx| {
7003 editor.handle_input("\"", window, cx);
7004 });
7005 cx.assert_editor_state(
7006 &r#"
7007 let x = ""ˇ
7008 "#
7009 .unindent(),
7010 );
7011
7012 // Reset
7013 cx.set_state(
7014 &r#"
7015 let x = ˇ
7016 "#
7017 .unindent(),
7018 );
7019
7020 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
7021 cx.update_editor(|editor, window, cx| {
7022 editor.handle_input("\"", window, cx);
7023 editor.handle_input(" ", window, cx);
7024 editor.move_left(&Default::default(), window, cx);
7025 editor.handle_input("\\", window, cx);
7026 editor.handle_input("\"", window, cx);
7027 });
7028 cx.assert_editor_state(
7029 &r#"
7030 let x = "\"ˇ "
7031 "#
7032 .unindent(),
7033 );
7034
7035 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
7036 // mark. Nothing is inserted.
7037 cx.update_editor(|editor, window, cx| {
7038 editor.move_right(&Default::default(), window, cx);
7039 editor.handle_input("\"", window, cx);
7040 });
7041 cx.assert_editor_state(
7042 &r#"
7043 let x = "\" "ˇ
7044 "#
7045 .unindent(),
7046 );
7047}
7048
7049#[gpui::test]
7050async fn test_surround_with_pair(cx: &mut TestAppContext) {
7051 init_test(cx, |_| {});
7052
7053 let language = Arc::new(Language::new(
7054 LanguageConfig {
7055 brackets: BracketPairConfig {
7056 pairs: vec![
7057 BracketPair {
7058 start: "{".to_string(),
7059 end: "}".to_string(),
7060 close: true,
7061 surround: true,
7062 newline: true,
7063 },
7064 BracketPair {
7065 start: "/* ".to_string(),
7066 end: "*/".to_string(),
7067 close: true,
7068 surround: true,
7069 ..Default::default()
7070 },
7071 ],
7072 ..Default::default()
7073 },
7074 ..Default::default()
7075 },
7076 Some(tree_sitter_rust::LANGUAGE.into()),
7077 ));
7078
7079 let text = r#"
7080 a
7081 b
7082 c
7083 "#
7084 .unindent();
7085
7086 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7087 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7088 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7089 editor
7090 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7091 .await;
7092
7093 editor.update_in(cx, |editor, window, cx| {
7094 editor.change_selections(None, window, cx, |s| {
7095 s.select_display_ranges([
7096 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7097 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7098 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
7099 ])
7100 });
7101
7102 editor.handle_input("{", window, cx);
7103 editor.handle_input("{", window, cx);
7104 editor.handle_input("{", window, cx);
7105 assert_eq!(
7106 editor.text(cx),
7107 "
7108 {{{a}}}
7109 {{{b}}}
7110 {{{c}}}
7111 "
7112 .unindent()
7113 );
7114 assert_eq!(
7115 editor.selections.display_ranges(cx),
7116 [
7117 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
7118 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
7119 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
7120 ]
7121 );
7122
7123 editor.undo(&Undo, window, cx);
7124 editor.undo(&Undo, window, cx);
7125 editor.undo(&Undo, window, cx);
7126 assert_eq!(
7127 editor.text(cx),
7128 "
7129 a
7130 b
7131 c
7132 "
7133 .unindent()
7134 );
7135 assert_eq!(
7136 editor.selections.display_ranges(cx),
7137 [
7138 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7139 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7140 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7141 ]
7142 );
7143
7144 // Ensure inserting the first character of a multi-byte bracket pair
7145 // doesn't surround the selections with the bracket.
7146 editor.handle_input("/", window, cx);
7147 assert_eq!(
7148 editor.text(cx),
7149 "
7150 /
7151 /
7152 /
7153 "
7154 .unindent()
7155 );
7156 assert_eq!(
7157 editor.selections.display_ranges(cx),
7158 [
7159 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7160 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7161 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7162 ]
7163 );
7164
7165 editor.undo(&Undo, window, cx);
7166 assert_eq!(
7167 editor.text(cx),
7168 "
7169 a
7170 b
7171 c
7172 "
7173 .unindent()
7174 );
7175 assert_eq!(
7176 editor.selections.display_ranges(cx),
7177 [
7178 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7179 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7180 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7181 ]
7182 );
7183
7184 // Ensure inserting the last character of a multi-byte bracket pair
7185 // doesn't surround the selections with the bracket.
7186 editor.handle_input("*", window, cx);
7187 assert_eq!(
7188 editor.text(cx),
7189 "
7190 *
7191 *
7192 *
7193 "
7194 .unindent()
7195 );
7196 assert_eq!(
7197 editor.selections.display_ranges(cx),
7198 [
7199 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7200 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7201 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7202 ]
7203 );
7204 });
7205}
7206
7207#[gpui::test]
7208async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
7209 init_test(cx, |_| {});
7210
7211 let language = Arc::new(Language::new(
7212 LanguageConfig {
7213 brackets: BracketPairConfig {
7214 pairs: vec![BracketPair {
7215 start: "{".to_string(),
7216 end: "}".to_string(),
7217 close: true,
7218 surround: true,
7219 newline: true,
7220 }],
7221 ..Default::default()
7222 },
7223 autoclose_before: "}".to_string(),
7224 ..Default::default()
7225 },
7226 Some(tree_sitter_rust::LANGUAGE.into()),
7227 ));
7228
7229 let text = r#"
7230 a
7231 b
7232 c
7233 "#
7234 .unindent();
7235
7236 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7237 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7238 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7239 editor
7240 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7241 .await;
7242
7243 editor.update_in(cx, |editor, window, cx| {
7244 editor.change_selections(None, window, cx, |s| {
7245 s.select_ranges([
7246 Point::new(0, 1)..Point::new(0, 1),
7247 Point::new(1, 1)..Point::new(1, 1),
7248 Point::new(2, 1)..Point::new(2, 1),
7249 ])
7250 });
7251
7252 editor.handle_input("{", window, cx);
7253 editor.handle_input("{", window, cx);
7254 editor.handle_input("_", window, cx);
7255 assert_eq!(
7256 editor.text(cx),
7257 "
7258 a{{_}}
7259 b{{_}}
7260 c{{_}}
7261 "
7262 .unindent()
7263 );
7264 assert_eq!(
7265 editor.selections.ranges::<Point>(cx),
7266 [
7267 Point::new(0, 4)..Point::new(0, 4),
7268 Point::new(1, 4)..Point::new(1, 4),
7269 Point::new(2, 4)..Point::new(2, 4)
7270 ]
7271 );
7272
7273 editor.backspace(&Default::default(), window, cx);
7274 editor.backspace(&Default::default(), window, cx);
7275 assert_eq!(
7276 editor.text(cx),
7277 "
7278 a{}
7279 b{}
7280 c{}
7281 "
7282 .unindent()
7283 );
7284 assert_eq!(
7285 editor.selections.ranges::<Point>(cx),
7286 [
7287 Point::new(0, 2)..Point::new(0, 2),
7288 Point::new(1, 2)..Point::new(1, 2),
7289 Point::new(2, 2)..Point::new(2, 2)
7290 ]
7291 );
7292
7293 editor.delete_to_previous_word_start(&Default::default(), window, cx);
7294 assert_eq!(
7295 editor.text(cx),
7296 "
7297 a
7298 b
7299 c
7300 "
7301 .unindent()
7302 );
7303 assert_eq!(
7304 editor.selections.ranges::<Point>(cx),
7305 [
7306 Point::new(0, 1)..Point::new(0, 1),
7307 Point::new(1, 1)..Point::new(1, 1),
7308 Point::new(2, 1)..Point::new(2, 1)
7309 ]
7310 );
7311 });
7312}
7313
7314#[gpui::test]
7315async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
7316 init_test(cx, |settings| {
7317 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7318 });
7319
7320 let mut cx = EditorTestContext::new(cx).await;
7321
7322 let language = Arc::new(Language::new(
7323 LanguageConfig {
7324 brackets: BracketPairConfig {
7325 pairs: vec![
7326 BracketPair {
7327 start: "{".to_string(),
7328 end: "}".to_string(),
7329 close: true,
7330 surround: true,
7331 newline: true,
7332 },
7333 BracketPair {
7334 start: "(".to_string(),
7335 end: ")".to_string(),
7336 close: true,
7337 surround: true,
7338 newline: true,
7339 },
7340 BracketPair {
7341 start: "[".to_string(),
7342 end: "]".to_string(),
7343 close: false,
7344 surround: true,
7345 newline: true,
7346 },
7347 ],
7348 ..Default::default()
7349 },
7350 autoclose_before: "})]".to_string(),
7351 ..Default::default()
7352 },
7353 Some(tree_sitter_rust::LANGUAGE.into()),
7354 ));
7355
7356 cx.language_registry().add(language.clone());
7357 cx.update_buffer(|buffer, cx| {
7358 buffer.set_language(Some(language), cx);
7359 });
7360
7361 cx.set_state(
7362 &"
7363 {(ˇ)}
7364 [[ˇ]]
7365 {(ˇ)}
7366 "
7367 .unindent(),
7368 );
7369
7370 cx.update_editor(|editor, window, cx| {
7371 editor.backspace(&Default::default(), window, cx);
7372 editor.backspace(&Default::default(), window, cx);
7373 });
7374
7375 cx.assert_editor_state(
7376 &"
7377 ˇ
7378 ˇ]]
7379 ˇ
7380 "
7381 .unindent(),
7382 );
7383
7384 cx.update_editor(|editor, window, cx| {
7385 editor.handle_input("{", window, cx);
7386 editor.handle_input("{", window, cx);
7387 editor.move_right(&MoveRight, window, cx);
7388 editor.move_right(&MoveRight, window, cx);
7389 editor.move_left(&MoveLeft, window, cx);
7390 editor.move_left(&MoveLeft, window, cx);
7391 editor.backspace(&Default::default(), window, cx);
7392 });
7393
7394 cx.assert_editor_state(
7395 &"
7396 {ˇ}
7397 {ˇ}]]
7398 {ˇ}
7399 "
7400 .unindent(),
7401 );
7402
7403 cx.update_editor(|editor, window, cx| {
7404 editor.backspace(&Default::default(), window, cx);
7405 });
7406
7407 cx.assert_editor_state(
7408 &"
7409 ˇ
7410 ˇ]]
7411 ˇ
7412 "
7413 .unindent(),
7414 );
7415}
7416
7417#[gpui::test]
7418async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
7419 init_test(cx, |_| {});
7420
7421 let language = Arc::new(Language::new(
7422 LanguageConfig::default(),
7423 Some(tree_sitter_rust::LANGUAGE.into()),
7424 ));
7425
7426 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
7427 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7428 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7429 editor
7430 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7431 .await;
7432
7433 editor.update_in(cx, |editor, window, cx| {
7434 editor.set_auto_replace_emoji_shortcode(true);
7435
7436 editor.handle_input("Hello ", window, cx);
7437 editor.handle_input(":wave", window, cx);
7438 assert_eq!(editor.text(cx), "Hello :wave".unindent());
7439
7440 editor.handle_input(":", window, cx);
7441 assert_eq!(editor.text(cx), "Hello 👋".unindent());
7442
7443 editor.handle_input(" :smile", window, cx);
7444 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
7445
7446 editor.handle_input(":", window, cx);
7447 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
7448
7449 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
7450 editor.handle_input(":wave", window, cx);
7451 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
7452
7453 editor.handle_input(":", window, cx);
7454 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
7455
7456 editor.handle_input(":1", window, cx);
7457 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
7458
7459 editor.handle_input(":", window, cx);
7460 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
7461
7462 // Ensure shortcode does not get replaced when it is part of a word
7463 editor.handle_input(" Test:wave", window, cx);
7464 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
7465
7466 editor.handle_input(":", window, cx);
7467 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
7468
7469 editor.set_auto_replace_emoji_shortcode(false);
7470
7471 // Ensure shortcode does not get replaced when auto replace is off
7472 editor.handle_input(" :wave", window, cx);
7473 assert_eq!(
7474 editor.text(cx),
7475 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
7476 );
7477
7478 editor.handle_input(":", window, cx);
7479 assert_eq!(
7480 editor.text(cx),
7481 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
7482 );
7483 });
7484}
7485
7486#[gpui::test]
7487async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
7488 init_test(cx, |_| {});
7489
7490 let (text, insertion_ranges) = marked_text_ranges(
7491 indoc! {"
7492 ˇ
7493 "},
7494 false,
7495 );
7496
7497 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7498 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7499
7500 _ = editor.update_in(cx, |editor, window, cx| {
7501 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
7502
7503 editor
7504 .insert_snippet(&insertion_ranges, snippet, window, cx)
7505 .unwrap();
7506
7507 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7508 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7509 assert_eq!(editor.text(cx), expected_text);
7510 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7511 }
7512
7513 assert(
7514 editor,
7515 cx,
7516 indoc! {"
7517 type «» =•
7518 "},
7519 );
7520
7521 assert!(editor.context_menu_visible(), "There should be a matches");
7522 });
7523}
7524
7525#[gpui::test]
7526async fn test_snippets(cx: &mut TestAppContext) {
7527 init_test(cx, |_| {});
7528
7529 let (text, insertion_ranges) = marked_text_ranges(
7530 indoc! {"
7531 a.ˇ b
7532 a.ˇ b
7533 a.ˇ b
7534 "},
7535 false,
7536 );
7537
7538 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7539 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7540
7541 editor.update_in(cx, |editor, window, cx| {
7542 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
7543
7544 editor
7545 .insert_snippet(&insertion_ranges, snippet, window, cx)
7546 .unwrap();
7547
7548 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7549 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7550 assert_eq!(editor.text(cx), expected_text);
7551 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7552 }
7553
7554 assert(
7555 editor,
7556 cx,
7557 indoc! {"
7558 a.f(«one», two, «three») b
7559 a.f(«one», two, «three») b
7560 a.f(«one», two, «three») b
7561 "},
7562 );
7563
7564 // Can't move earlier than the first tab stop
7565 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
7566 assert(
7567 editor,
7568 cx,
7569 indoc! {"
7570 a.f(«one», two, «three») b
7571 a.f(«one», two, «three») b
7572 a.f(«one», two, «three») b
7573 "},
7574 );
7575
7576 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7577 assert(
7578 editor,
7579 cx,
7580 indoc! {"
7581 a.f(one, «two», three) b
7582 a.f(one, «two», three) b
7583 a.f(one, «two», three) b
7584 "},
7585 );
7586
7587 editor.move_to_prev_snippet_tabstop(window, cx);
7588 assert(
7589 editor,
7590 cx,
7591 indoc! {"
7592 a.f(«one», two, «three») b
7593 a.f(«one», two, «three») b
7594 a.f(«one», two, «three») b
7595 "},
7596 );
7597
7598 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7599 assert(
7600 editor,
7601 cx,
7602 indoc! {"
7603 a.f(one, «two», three) b
7604 a.f(one, «two», three) b
7605 a.f(one, «two», three) b
7606 "},
7607 );
7608 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7609 assert(
7610 editor,
7611 cx,
7612 indoc! {"
7613 a.f(one, two, three)ˇ b
7614 a.f(one, two, three)ˇ b
7615 a.f(one, two, three)ˇ b
7616 "},
7617 );
7618
7619 // As soon as the last tab stop is reached, snippet state is gone
7620 editor.move_to_prev_snippet_tabstop(window, cx);
7621 assert(
7622 editor,
7623 cx,
7624 indoc! {"
7625 a.f(one, two, three)ˇ b
7626 a.f(one, two, three)ˇ b
7627 a.f(one, two, three)ˇ b
7628 "},
7629 );
7630 });
7631}
7632
7633#[gpui::test]
7634async fn test_document_format_during_save(cx: &mut TestAppContext) {
7635 init_test(cx, |_| {});
7636
7637 let fs = FakeFs::new(cx.executor());
7638 fs.insert_file(path!("/file.rs"), Default::default()).await;
7639
7640 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
7641
7642 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7643 language_registry.add(rust_lang());
7644 let mut fake_servers = language_registry.register_fake_lsp(
7645 "Rust",
7646 FakeLspAdapter {
7647 capabilities: lsp::ServerCapabilities {
7648 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7649 ..Default::default()
7650 },
7651 ..Default::default()
7652 },
7653 );
7654
7655 let buffer = project
7656 .update(cx, |project, cx| {
7657 project.open_local_buffer(path!("/file.rs"), cx)
7658 })
7659 .await
7660 .unwrap();
7661
7662 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7663 let (editor, cx) = cx.add_window_view(|window, cx| {
7664 build_editor_with_project(project.clone(), buffer, window, cx)
7665 });
7666 editor.update_in(cx, |editor, window, cx| {
7667 editor.set_text("one\ntwo\nthree\n", window, cx)
7668 });
7669 assert!(cx.read(|cx| editor.is_dirty(cx)));
7670
7671 cx.executor().start_waiting();
7672 let fake_server = fake_servers.next().await.unwrap();
7673
7674 let save = editor
7675 .update_in(cx, |editor, window, cx| {
7676 editor.save(true, project.clone(), window, cx)
7677 })
7678 .unwrap();
7679 fake_server
7680 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
7681 assert_eq!(
7682 params.text_document.uri,
7683 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7684 );
7685 assert_eq!(params.options.tab_size, 4);
7686 Ok(Some(vec![lsp::TextEdit::new(
7687 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7688 ", ".to_string(),
7689 )]))
7690 })
7691 .next()
7692 .await;
7693 cx.executor().start_waiting();
7694 save.await;
7695
7696 assert_eq!(
7697 editor.update(cx, |editor, cx| editor.text(cx)),
7698 "one, two\nthree\n"
7699 );
7700 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7701
7702 editor.update_in(cx, |editor, window, cx| {
7703 editor.set_text("one\ntwo\nthree\n", window, cx)
7704 });
7705 assert!(cx.read(|cx| editor.is_dirty(cx)));
7706
7707 // Ensure we can still save even if formatting hangs.
7708 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
7709 move |params, _| async move {
7710 assert_eq!(
7711 params.text_document.uri,
7712 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7713 );
7714 futures::future::pending::<()>().await;
7715 unreachable!()
7716 },
7717 );
7718 let save = editor
7719 .update_in(cx, |editor, window, cx| {
7720 editor.save(true, project.clone(), window, cx)
7721 })
7722 .unwrap();
7723 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7724 cx.executor().start_waiting();
7725 save.await;
7726 assert_eq!(
7727 editor.update(cx, |editor, cx| editor.text(cx)),
7728 "one\ntwo\nthree\n"
7729 );
7730 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7731
7732 // For non-dirty buffer, no formatting request should be sent
7733 let save = editor
7734 .update_in(cx, |editor, window, cx| {
7735 editor.save(true, project.clone(), window, cx)
7736 })
7737 .unwrap();
7738 let _pending_format_request = fake_server
7739 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
7740 panic!("Should not be invoked on non-dirty buffer");
7741 })
7742 .next();
7743 cx.executor().start_waiting();
7744 save.await;
7745
7746 // Set rust language override and assert overridden tabsize is sent to language server
7747 update_test_language_settings(cx, |settings| {
7748 settings.languages.insert(
7749 "Rust".into(),
7750 LanguageSettingsContent {
7751 tab_size: NonZeroU32::new(8),
7752 ..Default::default()
7753 },
7754 );
7755 });
7756
7757 editor.update_in(cx, |editor, window, cx| {
7758 editor.set_text("somehting_new\n", window, cx)
7759 });
7760 assert!(cx.read(|cx| editor.is_dirty(cx)));
7761 let save = editor
7762 .update_in(cx, |editor, window, cx| {
7763 editor.save(true, project.clone(), window, cx)
7764 })
7765 .unwrap();
7766 fake_server
7767 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
7768 assert_eq!(
7769 params.text_document.uri,
7770 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7771 );
7772 assert_eq!(params.options.tab_size, 8);
7773 Ok(Some(vec![]))
7774 })
7775 .next()
7776 .await;
7777 cx.executor().start_waiting();
7778 save.await;
7779}
7780
7781#[gpui::test]
7782async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
7783 init_test(cx, |_| {});
7784
7785 let cols = 4;
7786 let rows = 10;
7787 let sample_text_1 = sample_text(rows, cols, 'a');
7788 assert_eq!(
7789 sample_text_1,
7790 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
7791 );
7792 let sample_text_2 = sample_text(rows, cols, 'l');
7793 assert_eq!(
7794 sample_text_2,
7795 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
7796 );
7797 let sample_text_3 = sample_text(rows, cols, 'v');
7798 assert_eq!(
7799 sample_text_3,
7800 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
7801 );
7802
7803 let fs = FakeFs::new(cx.executor());
7804 fs.insert_tree(
7805 path!("/a"),
7806 json!({
7807 "main.rs": sample_text_1,
7808 "other.rs": sample_text_2,
7809 "lib.rs": sample_text_3,
7810 }),
7811 )
7812 .await;
7813
7814 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
7815 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7816 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
7817
7818 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7819 language_registry.add(rust_lang());
7820 let mut fake_servers = language_registry.register_fake_lsp(
7821 "Rust",
7822 FakeLspAdapter {
7823 capabilities: lsp::ServerCapabilities {
7824 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7825 ..Default::default()
7826 },
7827 ..Default::default()
7828 },
7829 );
7830
7831 let worktree = project.update(cx, |project, cx| {
7832 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
7833 assert_eq!(worktrees.len(), 1);
7834 worktrees.pop().unwrap()
7835 });
7836 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
7837
7838 let buffer_1 = project
7839 .update(cx, |project, cx| {
7840 project.open_buffer((worktree_id, "main.rs"), cx)
7841 })
7842 .await
7843 .unwrap();
7844 let buffer_2 = project
7845 .update(cx, |project, cx| {
7846 project.open_buffer((worktree_id, "other.rs"), cx)
7847 })
7848 .await
7849 .unwrap();
7850 let buffer_3 = project
7851 .update(cx, |project, cx| {
7852 project.open_buffer((worktree_id, "lib.rs"), cx)
7853 })
7854 .await
7855 .unwrap();
7856
7857 let multi_buffer = cx.new(|cx| {
7858 let mut multi_buffer = MultiBuffer::new(ReadWrite);
7859 multi_buffer.push_excerpts(
7860 buffer_1.clone(),
7861 [
7862 ExcerptRange {
7863 context: Point::new(0, 0)..Point::new(3, 0),
7864 primary: None,
7865 },
7866 ExcerptRange {
7867 context: Point::new(5, 0)..Point::new(7, 0),
7868 primary: None,
7869 },
7870 ExcerptRange {
7871 context: Point::new(9, 0)..Point::new(10, 4),
7872 primary: None,
7873 },
7874 ],
7875 cx,
7876 );
7877 multi_buffer.push_excerpts(
7878 buffer_2.clone(),
7879 [
7880 ExcerptRange {
7881 context: Point::new(0, 0)..Point::new(3, 0),
7882 primary: None,
7883 },
7884 ExcerptRange {
7885 context: Point::new(5, 0)..Point::new(7, 0),
7886 primary: None,
7887 },
7888 ExcerptRange {
7889 context: Point::new(9, 0)..Point::new(10, 4),
7890 primary: None,
7891 },
7892 ],
7893 cx,
7894 );
7895 multi_buffer.push_excerpts(
7896 buffer_3.clone(),
7897 [
7898 ExcerptRange {
7899 context: Point::new(0, 0)..Point::new(3, 0),
7900 primary: None,
7901 },
7902 ExcerptRange {
7903 context: Point::new(5, 0)..Point::new(7, 0),
7904 primary: None,
7905 },
7906 ExcerptRange {
7907 context: Point::new(9, 0)..Point::new(10, 4),
7908 primary: None,
7909 },
7910 ],
7911 cx,
7912 );
7913 multi_buffer
7914 });
7915 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
7916 Editor::new(
7917 EditorMode::Full,
7918 multi_buffer,
7919 Some(project.clone()),
7920 window,
7921 cx,
7922 )
7923 });
7924
7925 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7926 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7927 s.select_ranges(Some(1..2))
7928 });
7929 editor.insert("|one|two|three|", window, cx);
7930 });
7931 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7932 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7933 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7934 s.select_ranges(Some(60..70))
7935 });
7936 editor.insert("|four|five|six|", window, cx);
7937 });
7938 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7939
7940 // First two buffers should be edited, but not the third one.
7941 assert_eq!(
7942 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
7943 "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}",
7944 );
7945 buffer_1.update(cx, |buffer, _| {
7946 assert!(buffer.is_dirty());
7947 assert_eq!(
7948 buffer.text(),
7949 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
7950 )
7951 });
7952 buffer_2.update(cx, |buffer, _| {
7953 assert!(buffer.is_dirty());
7954 assert_eq!(
7955 buffer.text(),
7956 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
7957 )
7958 });
7959 buffer_3.update(cx, |buffer, _| {
7960 assert!(!buffer.is_dirty());
7961 assert_eq!(buffer.text(), sample_text_3,)
7962 });
7963 cx.executor().run_until_parked();
7964
7965 cx.executor().start_waiting();
7966 let save = multi_buffer_editor
7967 .update_in(cx, |editor, window, cx| {
7968 editor.save(true, project.clone(), window, cx)
7969 })
7970 .unwrap();
7971
7972 let fake_server = fake_servers.next().await.unwrap();
7973 fake_server
7974 .server
7975 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7976 Ok(Some(vec![lsp::TextEdit::new(
7977 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7978 format!("[{} formatted]", params.text_document.uri),
7979 )]))
7980 })
7981 .detach();
7982 save.await;
7983
7984 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
7985 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
7986 assert_eq!(
7987 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
7988 uri!("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}"),
7989 );
7990 buffer_1.update(cx, |buffer, _| {
7991 assert!(!buffer.is_dirty());
7992 assert_eq!(
7993 buffer.text(),
7994 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
7995 )
7996 });
7997 buffer_2.update(cx, |buffer, _| {
7998 assert!(!buffer.is_dirty());
7999 assert_eq!(
8000 buffer.text(),
8001 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
8002 )
8003 });
8004 buffer_3.update(cx, |buffer, _| {
8005 assert!(!buffer.is_dirty());
8006 assert_eq!(buffer.text(), sample_text_3,)
8007 });
8008}
8009
8010#[gpui::test]
8011async fn test_range_format_during_save(cx: &mut TestAppContext) {
8012 init_test(cx, |_| {});
8013
8014 let fs = FakeFs::new(cx.executor());
8015 fs.insert_file(path!("/file.rs"), Default::default()).await;
8016
8017 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8018
8019 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8020 language_registry.add(rust_lang());
8021 let mut fake_servers = language_registry.register_fake_lsp(
8022 "Rust",
8023 FakeLspAdapter {
8024 capabilities: lsp::ServerCapabilities {
8025 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
8026 ..Default::default()
8027 },
8028 ..Default::default()
8029 },
8030 );
8031
8032 let buffer = project
8033 .update(cx, |project, cx| {
8034 project.open_local_buffer(path!("/file.rs"), cx)
8035 })
8036 .await
8037 .unwrap();
8038
8039 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8040 let (editor, cx) = cx.add_window_view(|window, cx| {
8041 build_editor_with_project(project.clone(), buffer, window, cx)
8042 });
8043 editor.update_in(cx, |editor, window, cx| {
8044 editor.set_text("one\ntwo\nthree\n", window, cx)
8045 });
8046 assert!(cx.read(|cx| editor.is_dirty(cx)));
8047
8048 cx.executor().start_waiting();
8049 let fake_server = fake_servers.next().await.unwrap();
8050
8051 let save = editor
8052 .update_in(cx, |editor, window, cx| {
8053 editor.save(true, project.clone(), window, cx)
8054 })
8055 .unwrap();
8056 fake_server
8057 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8058 assert_eq!(
8059 params.text_document.uri,
8060 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8061 );
8062 assert_eq!(params.options.tab_size, 4);
8063 Ok(Some(vec![lsp::TextEdit::new(
8064 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8065 ", ".to_string(),
8066 )]))
8067 })
8068 .next()
8069 .await;
8070 cx.executor().start_waiting();
8071 save.await;
8072 assert_eq!(
8073 editor.update(cx, |editor, cx| editor.text(cx)),
8074 "one, two\nthree\n"
8075 );
8076 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8077
8078 editor.update_in(cx, |editor, window, cx| {
8079 editor.set_text("one\ntwo\nthree\n", window, cx)
8080 });
8081 assert!(cx.read(|cx| editor.is_dirty(cx)));
8082
8083 // Ensure we can still save even if formatting hangs.
8084 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
8085 move |params, _| async move {
8086 assert_eq!(
8087 params.text_document.uri,
8088 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8089 );
8090 futures::future::pending::<()>().await;
8091 unreachable!()
8092 },
8093 );
8094 let save = editor
8095 .update_in(cx, |editor, window, cx| {
8096 editor.save(true, project.clone(), window, cx)
8097 })
8098 .unwrap();
8099 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8100 cx.executor().start_waiting();
8101 save.await;
8102 assert_eq!(
8103 editor.update(cx, |editor, cx| editor.text(cx)),
8104 "one\ntwo\nthree\n"
8105 );
8106 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8107
8108 // For non-dirty buffer, no formatting request should be sent
8109 let save = editor
8110 .update_in(cx, |editor, window, cx| {
8111 editor.save(true, project.clone(), window, cx)
8112 })
8113 .unwrap();
8114 let _pending_format_request = fake_server
8115 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
8116 panic!("Should not be invoked on non-dirty buffer");
8117 })
8118 .next();
8119 cx.executor().start_waiting();
8120 save.await;
8121
8122 // Set Rust language override and assert overridden tabsize is sent to language server
8123 update_test_language_settings(cx, |settings| {
8124 settings.languages.insert(
8125 "Rust".into(),
8126 LanguageSettingsContent {
8127 tab_size: NonZeroU32::new(8),
8128 ..Default::default()
8129 },
8130 );
8131 });
8132
8133 editor.update_in(cx, |editor, window, cx| {
8134 editor.set_text("somehting_new\n", window, cx)
8135 });
8136 assert!(cx.read(|cx| editor.is_dirty(cx)));
8137 let save = editor
8138 .update_in(cx, |editor, window, cx| {
8139 editor.save(true, project.clone(), window, cx)
8140 })
8141 .unwrap();
8142 fake_server
8143 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8144 assert_eq!(
8145 params.text_document.uri,
8146 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8147 );
8148 assert_eq!(params.options.tab_size, 8);
8149 Ok(Some(vec![]))
8150 })
8151 .next()
8152 .await;
8153 cx.executor().start_waiting();
8154 save.await;
8155}
8156
8157#[gpui::test]
8158async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
8159 init_test(cx, |settings| {
8160 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8161 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8162 ))
8163 });
8164
8165 let fs = FakeFs::new(cx.executor());
8166 fs.insert_file(path!("/file.rs"), Default::default()).await;
8167
8168 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8169
8170 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8171 language_registry.add(Arc::new(Language::new(
8172 LanguageConfig {
8173 name: "Rust".into(),
8174 matcher: LanguageMatcher {
8175 path_suffixes: vec!["rs".to_string()],
8176 ..Default::default()
8177 },
8178 ..LanguageConfig::default()
8179 },
8180 Some(tree_sitter_rust::LANGUAGE.into()),
8181 )));
8182 update_test_language_settings(cx, |settings| {
8183 // Enable Prettier formatting for the same buffer, and ensure
8184 // LSP is called instead of Prettier.
8185 settings.defaults.prettier = Some(PrettierSettings {
8186 allowed: true,
8187 ..PrettierSettings::default()
8188 });
8189 });
8190 let mut fake_servers = language_registry.register_fake_lsp(
8191 "Rust",
8192 FakeLspAdapter {
8193 capabilities: lsp::ServerCapabilities {
8194 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8195 ..Default::default()
8196 },
8197 ..Default::default()
8198 },
8199 );
8200
8201 let buffer = project
8202 .update(cx, |project, cx| {
8203 project.open_local_buffer(path!("/file.rs"), cx)
8204 })
8205 .await
8206 .unwrap();
8207
8208 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8209 let (editor, cx) = cx.add_window_view(|window, cx| {
8210 build_editor_with_project(project.clone(), buffer, window, cx)
8211 });
8212 editor.update_in(cx, |editor, window, cx| {
8213 editor.set_text("one\ntwo\nthree\n", window, cx)
8214 });
8215
8216 cx.executor().start_waiting();
8217 let fake_server = fake_servers.next().await.unwrap();
8218
8219 let format = editor
8220 .update_in(cx, |editor, window, cx| {
8221 editor.perform_format(
8222 project.clone(),
8223 FormatTrigger::Manual,
8224 FormatTarget::Buffers,
8225 window,
8226 cx,
8227 )
8228 })
8229 .unwrap();
8230 fake_server
8231 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8232 assert_eq!(
8233 params.text_document.uri,
8234 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8235 );
8236 assert_eq!(params.options.tab_size, 4);
8237 Ok(Some(vec![lsp::TextEdit::new(
8238 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8239 ", ".to_string(),
8240 )]))
8241 })
8242 .next()
8243 .await;
8244 cx.executor().start_waiting();
8245 format.await;
8246 assert_eq!(
8247 editor.update(cx, |editor, cx| editor.text(cx)),
8248 "one, two\nthree\n"
8249 );
8250
8251 editor.update_in(cx, |editor, window, cx| {
8252 editor.set_text("one\ntwo\nthree\n", window, cx)
8253 });
8254 // Ensure we don't lock if formatting hangs.
8255 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8256 move |params, _| async move {
8257 assert_eq!(
8258 params.text_document.uri,
8259 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8260 );
8261 futures::future::pending::<()>().await;
8262 unreachable!()
8263 },
8264 );
8265 let format = editor
8266 .update_in(cx, |editor, window, cx| {
8267 editor.perform_format(
8268 project,
8269 FormatTrigger::Manual,
8270 FormatTarget::Buffers,
8271 window,
8272 cx,
8273 )
8274 })
8275 .unwrap();
8276 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8277 cx.executor().start_waiting();
8278 format.await;
8279 assert_eq!(
8280 editor.update(cx, |editor, cx| editor.text(cx)),
8281 "one\ntwo\nthree\n"
8282 );
8283}
8284
8285#[gpui::test]
8286async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
8287 init_test(cx, |settings| {
8288 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8289 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8290 ))
8291 });
8292
8293 let fs = FakeFs::new(cx.executor());
8294 fs.insert_file(path!("/file.ts"), Default::default()).await;
8295
8296 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8297
8298 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8299 language_registry.add(Arc::new(Language::new(
8300 LanguageConfig {
8301 name: "TypeScript".into(),
8302 matcher: LanguageMatcher {
8303 path_suffixes: vec!["ts".to_string()],
8304 ..Default::default()
8305 },
8306 ..LanguageConfig::default()
8307 },
8308 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
8309 )));
8310 update_test_language_settings(cx, |settings| {
8311 settings.defaults.prettier = Some(PrettierSettings {
8312 allowed: true,
8313 ..PrettierSettings::default()
8314 });
8315 });
8316 let mut fake_servers = language_registry.register_fake_lsp(
8317 "TypeScript",
8318 FakeLspAdapter {
8319 capabilities: lsp::ServerCapabilities {
8320 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
8321 ..Default::default()
8322 },
8323 ..Default::default()
8324 },
8325 );
8326
8327 let buffer = project
8328 .update(cx, |project, cx| {
8329 project.open_local_buffer(path!("/file.ts"), cx)
8330 })
8331 .await
8332 .unwrap();
8333
8334 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8335 let (editor, cx) = cx.add_window_view(|window, cx| {
8336 build_editor_with_project(project.clone(), buffer, window, cx)
8337 });
8338 editor.update_in(cx, |editor, window, cx| {
8339 editor.set_text(
8340 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
8341 window,
8342 cx,
8343 )
8344 });
8345
8346 cx.executor().start_waiting();
8347 let fake_server = fake_servers.next().await.unwrap();
8348
8349 let format = editor
8350 .update_in(cx, |editor, window, cx| {
8351 editor.perform_code_action_kind(
8352 project.clone(),
8353 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
8354 window,
8355 cx,
8356 )
8357 })
8358 .unwrap();
8359 fake_server
8360 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
8361 assert_eq!(
8362 params.text_document.uri,
8363 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
8364 );
8365 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
8366 lsp::CodeAction {
8367 title: "Organize Imports".to_string(),
8368 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
8369 edit: Some(lsp::WorkspaceEdit {
8370 changes: Some(
8371 [(
8372 params.text_document.uri.clone(),
8373 vec![lsp::TextEdit::new(
8374 lsp::Range::new(
8375 lsp::Position::new(1, 0),
8376 lsp::Position::new(2, 0),
8377 ),
8378 "".to_string(),
8379 )],
8380 )]
8381 .into_iter()
8382 .collect(),
8383 ),
8384 ..Default::default()
8385 }),
8386 ..Default::default()
8387 },
8388 )]))
8389 })
8390 .next()
8391 .await;
8392 cx.executor().start_waiting();
8393 format.await;
8394 assert_eq!(
8395 editor.update(cx, |editor, cx| editor.text(cx)),
8396 "import { a } from 'module';\n\nconst x = a;\n"
8397 );
8398
8399 editor.update_in(cx, |editor, window, cx| {
8400 editor.set_text(
8401 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
8402 window,
8403 cx,
8404 )
8405 });
8406 // Ensure we don't lock if code action hangs.
8407 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
8408 move |params, _| async move {
8409 assert_eq!(
8410 params.text_document.uri,
8411 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
8412 );
8413 futures::future::pending::<()>().await;
8414 unreachable!()
8415 },
8416 );
8417 let format = editor
8418 .update_in(cx, |editor, window, cx| {
8419 editor.perform_code_action_kind(
8420 project,
8421 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
8422 window,
8423 cx,
8424 )
8425 })
8426 .unwrap();
8427 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
8428 cx.executor().start_waiting();
8429 format.await;
8430 assert_eq!(
8431 editor.update(cx, |editor, cx| editor.text(cx)),
8432 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
8433 );
8434}
8435
8436#[gpui::test]
8437async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
8438 init_test(cx, |_| {});
8439
8440 let mut cx = EditorLspTestContext::new_rust(
8441 lsp::ServerCapabilities {
8442 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8443 ..Default::default()
8444 },
8445 cx,
8446 )
8447 .await;
8448
8449 cx.set_state(indoc! {"
8450 one.twoˇ
8451 "});
8452
8453 // The format request takes a long time. When it completes, it inserts
8454 // a newline and an indent before the `.`
8455 cx.lsp
8456 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
8457 let executor = cx.background_executor().clone();
8458 async move {
8459 executor.timer(Duration::from_millis(100)).await;
8460 Ok(Some(vec![lsp::TextEdit {
8461 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
8462 new_text: "\n ".into(),
8463 }]))
8464 }
8465 });
8466
8467 // Submit a format request.
8468 let format_1 = cx
8469 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8470 .unwrap();
8471 cx.executor().run_until_parked();
8472
8473 // Submit a second format request.
8474 let format_2 = cx
8475 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8476 .unwrap();
8477 cx.executor().run_until_parked();
8478
8479 // Wait for both format requests to complete
8480 cx.executor().advance_clock(Duration::from_millis(200));
8481 cx.executor().start_waiting();
8482 format_1.await.unwrap();
8483 cx.executor().start_waiting();
8484 format_2.await.unwrap();
8485
8486 // The formatting edits only happens once.
8487 cx.assert_editor_state(indoc! {"
8488 one
8489 .twoˇ
8490 "});
8491}
8492
8493#[gpui::test]
8494async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
8495 init_test(cx, |settings| {
8496 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
8497 });
8498
8499 let mut cx = EditorLspTestContext::new_rust(
8500 lsp::ServerCapabilities {
8501 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8502 ..Default::default()
8503 },
8504 cx,
8505 )
8506 .await;
8507
8508 // Set up a buffer white some trailing whitespace and no trailing newline.
8509 cx.set_state(
8510 &[
8511 "one ", //
8512 "twoˇ", //
8513 "three ", //
8514 "four", //
8515 ]
8516 .join("\n"),
8517 );
8518
8519 // Submit a format request.
8520 let format = cx
8521 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8522 .unwrap();
8523
8524 // Record which buffer changes have been sent to the language server
8525 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
8526 cx.lsp
8527 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
8528 let buffer_changes = buffer_changes.clone();
8529 move |params, _| {
8530 buffer_changes.lock().extend(
8531 params
8532 .content_changes
8533 .into_iter()
8534 .map(|e| (e.range.unwrap(), e.text)),
8535 );
8536 }
8537 });
8538
8539 // Handle formatting requests to the language server.
8540 cx.lsp
8541 .set_request_handler::<lsp::request::Formatting, _, _>({
8542 let buffer_changes = buffer_changes.clone();
8543 move |_, _| {
8544 // When formatting is requested, trailing whitespace has already been stripped,
8545 // and the trailing newline has already been added.
8546 assert_eq!(
8547 &buffer_changes.lock()[1..],
8548 &[
8549 (
8550 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
8551 "".into()
8552 ),
8553 (
8554 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
8555 "".into()
8556 ),
8557 (
8558 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
8559 "\n".into()
8560 ),
8561 ]
8562 );
8563
8564 // Insert blank lines between each line of the buffer.
8565 async move {
8566 Ok(Some(vec![
8567 lsp::TextEdit {
8568 range: lsp::Range::new(
8569 lsp::Position::new(1, 0),
8570 lsp::Position::new(1, 0),
8571 ),
8572 new_text: "\n".into(),
8573 },
8574 lsp::TextEdit {
8575 range: lsp::Range::new(
8576 lsp::Position::new(2, 0),
8577 lsp::Position::new(2, 0),
8578 ),
8579 new_text: "\n".into(),
8580 },
8581 ]))
8582 }
8583 }
8584 });
8585
8586 // After formatting the buffer, the trailing whitespace is stripped,
8587 // a newline is appended, and the edits provided by the language server
8588 // have been applied.
8589 format.await.unwrap();
8590 cx.assert_editor_state(
8591 &[
8592 "one", //
8593 "", //
8594 "twoˇ", //
8595 "", //
8596 "three", //
8597 "four", //
8598 "", //
8599 ]
8600 .join("\n"),
8601 );
8602
8603 // Undoing the formatting undoes the trailing whitespace removal, the
8604 // trailing newline, and the LSP edits.
8605 cx.update_buffer(|buffer, cx| buffer.undo(cx));
8606 cx.assert_editor_state(
8607 &[
8608 "one ", //
8609 "twoˇ", //
8610 "three ", //
8611 "four", //
8612 ]
8613 .join("\n"),
8614 );
8615}
8616
8617#[gpui::test]
8618async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
8619 cx: &mut TestAppContext,
8620) {
8621 init_test(cx, |_| {});
8622
8623 cx.update(|cx| {
8624 cx.update_global::<SettingsStore, _>(|settings, cx| {
8625 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8626 settings.auto_signature_help = Some(true);
8627 });
8628 });
8629 });
8630
8631 let mut cx = EditorLspTestContext::new_rust(
8632 lsp::ServerCapabilities {
8633 signature_help_provider: Some(lsp::SignatureHelpOptions {
8634 ..Default::default()
8635 }),
8636 ..Default::default()
8637 },
8638 cx,
8639 )
8640 .await;
8641
8642 let language = Language::new(
8643 LanguageConfig {
8644 name: "Rust".into(),
8645 brackets: BracketPairConfig {
8646 pairs: vec![
8647 BracketPair {
8648 start: "{".to_string(),
8649 end: "}".to_string(),
8650 close: true,
8651 surround: true,
8652 newline: true,
8653 },
8654 BracketPair {
8655 start: "(".to_string(),
8656 end: ")".to_string(),
8657 close: true,
8658 surround: true,
8659 newline: true,
8660 },
8661 BracketPair {
8662 start: "/*".to_string(),
8663 end: " */".to_string(),
8664 close: true,
8665 surround: true,
8666 newline: true,
8667 },
8668 BracketPair {
8669 start: "[".to_string(),
8670 end: "]".to_string(),
8671 close: false,
8672 surround: false,
8673 newline: true,
8674 },
8675 BracketPair {
8676 start: "\"".to_string(),
8677 end: "\"".to_string(),
8678 close: true,
8679 surround: true,
8680 newline: false,
8681 },
8682 BracketPair {
8683 start: "<".to_string(),
8684 end: ">".to_string(),
8685 close: false,
8686 surround: true,
8687 newline: true,
8688 },
8689 ],
8690 ..Default::default()
8691 },
8692 autoclose_before: "})]".to_string(),
8693 ..Default::default()
8694 },
8695 Some(tree_sitter_rust::LANGUAGE.into()),
8696 );
8697 let language = Arc::new(language);
8698
8699 cx.language_registry().add(language.clone());
8700 cx.update_buffer(|buffer, cx| {
8701 buffer.set_language(Some(language), cx);
8702 });
8703
8704 cx.set_state(
8705 &r#"
8706 fn main() {
8707 sampleˇ
8708 }
8709 "#
8710 .unindent(),
8711 );
8712
8713 cx.update_editor(|editor, window, cx| {
8714 editor.handle_input("(", window, cx);
8715 });
8716 cx.assert_editor_state(
8717 &"
8718 fn main() {
8719 sample(ˇ)
8720 }
8721 "
8722 .unindent(),
8723 );
8724
8725 let mocked_response = lsp::SignatureHelp {
8726 signatures: vec![lsp::SignatureInformation {
8727 label: "fn sample(param1: u8, param2: u8)".to_string(),
8728 documentation: None,
8729 parameters: Some(vec![
8730 lsp::ParameterInformation {
8731 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8732 documentation: None,
8733 },
8734 lsp::ParameterInformation {
8735 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8736 documentation: None,
8737 },
8738 ]),
8739 active_parameter: None,
8740 }],
8741 active_signature: Some(0),
8742 active_parameter: Some(0),
8743 };
8744 handle_signature_help_request(&mut cx, mocked_response).await;
8745
8746 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8747 .await;
8748
8749 cx.editor(|editor, _, _| {
8750 let signature_help_state = editor.signature_help_state.popover().cloned();
8751 assert_eq!(
8752 signature_help_state.unwrap().label,
8753 "param1: u8, param2: u8"
8754 );
8755 });
8756}
8757
8758#[gpui::test]
8759async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
8760 init_test(cx, |_| {});
8761
8762 cx.update(|cx| {
8763 cx.update_global::<SettingsStore, _>(|settings, cx| {
8764 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8765 settings.auto_signature_help = Some(false);
8766 settings.show_signature_help_after_edits = Some(false);
8767 });
8768 });
8769 });
8770
8771 let mut cx = EditorLspTestContext::new_rust(
8772 lsp::ServerCapabilities {
8773 signature_help_provider: Some(lsp::SignatureHelpOptions {
8774 ..Default::default()
8775 }),
8776 ..Default::default()
8777 },
8778 cx,
8779 )
8780 .await;
8781
8782 let language = Language::new(
8783 LanguageConfig {
8784 name: "Rust".into(),
8785 brackets: BracketPairConfig {
8786 pairs: vec![
8787 BracketPair {
8788 start: "{".to_string(),
8789 end: "}".to_string(),
8790 close: true,
8791 surround: true,
8792 newline: true,
8793 },
8794 BracketPair {
8795 start: "(".to_string(),
8796 end: ")".to_string(),
8797 close: true,
8798 surround: true,
8799 newline: true,
8800 },
8801 BracketPair {
8802 start: "/*".to_string(),
8803 end: " */".to_string(),
8804 close: true,
8805 surround: true,
8806 newline: true,
8807 },
8808 BracketPair {
8809 start: "[".to_string(),
8810 end: "]".to_string(),
8811 close: false,
8812 surround: false,
8813 newline: true,
8814 },
8815 BracketPair {
8816 start: "\"".to_string(),
8817 end: "\"".to_string(),
8818 close: true,
8819 surround: true,
8820 newline: false,
8821 },
8822 BracketPair {
8823 start: "<".to_string(),
8824 end: ">".to_string(),
8825 close: false,
8826 surround: true,
8827 newline: true,
8828 },
8829 ],
8830 ..Default::default()
8831 },
8832 autoclose_before: "})]".to_string(),
8833 ..Default::default()
8834 },
8835 Some(tree_sitter_rust::LANGUAGE.into()),
8836 );
8837 let language = Arc::new(language);
8838
8839 cx.language_registry().add(language.clone());
8840 cx.update_buffer(|buffer, cx| {
8841 buffer.set_language(Some(language), cx);
8842 });
8843
8844 // Ensure that signature_help is not called when no signature help is enabled.
8845 cx.set_state(
8846 &r#"
8847 fn main() {
8848 sampleˇ
8849 }
8850 "#
8851 .unindent(),
8852 );
8853 cx.update_editor(|editor, window, cx| {
8854 editor.handle_input("(", window, cx);
8855 });
8856 cx.assert_editor_state(
8857 &"
8858 fn main() {
8859 sample(ˇ)
8860 }
8861 "
8862 .unindent(),
8863 );
8864 cx.editor(|editor, _, _| {
8865 assert!(editor.signature_help_state.task().is_none());
8866 });
8867
8868 let mocked_response = lsp::SignatureHelp {
8869 signatures: vec![lsp::SignatureInformation {
8870 label: "fn sample(param1: u8, param2: u8)".to_string(),
8871 documentation: None,
8872 parameters: Some(vec![
8873 lsp::ParameterInformation {
8874 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8875 documentation: None,
8876 },
8877 lsp::ParameterInformation {
8878 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8879 documentation: None,
8880 },
8881 ]),
8882 active_parameter: None,
8883 }],
8884 active_signature: Some(0),
8885 active_parameter: Some(0),
8886 };
8887
8888 // Ensure that signature_help is called when enabled afte edits
8889 cx.update(|_, cx| {
8890 cx.update_global::<SettingsStore, _>(|settings, cx| {
8891 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8892 settings.auto_signature_help = Some(false);
8893 settings.show_signature_help_after_edits = Some(true);
8894 });
8895 });
8896 });
8897 cx.set_state(
8898 &r#"
8899 fn main() {
8900 sampleˇ
8901 }
8902 "#
8903 .unindent(),
8904 );
8905 cx.update_editor(|editor, window, cx| {
8906 editor.handle_input("(", window, cx);
8907 });
8908 cx.assert_editor_state(
8909 &"
8910 fn main() {
8911 sample(ˇ)
8912 }
8913 "
8914 .unindent(),
8915 );
8916 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8917 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8918 .await;
8919 cx.update_editor(|editor, _, _| {
8920 let signature_help_state = editor.signature_help_state.popover().cloned();
8921 assert!(signature_help_state.is_some());
8922 assert_eq!(
8923 signature_help_state.unwrap().label,
8924 "param1: u8, param2: u8"
8925 );
8926 editor.signature_help_state = SignatureHelpState::default();
8927 });
8928
8929 // Ensure that signature_help is called when auto signature help override is enabled
8930 cx.update(|_, cx| {
8931 cx.update_global::<SettingsStore, _>(|settings, cx| {
8932 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8933 settings.auto_signature_help = Some(true);
8934 settings.show_signature_help_after_edits = Some(false);
8935 });
8936 });
8937 });
8938 cx.set_state(
8939 &r#"
8940 fn main() {
8941 sampleˇ
8942 }
8943 "#
8944 .unindent(),
8945 );
8946 cx.update_editor(|editor, window, cx| {
8947 editor.handle_input("(", window, cx);
8948 });
8949 cx.assert_editor_state(
8950 &"
8951 fn main() {
8952 sample(ˇ)
8953 }
8954 "
8955 .unindent(),
8956 );
8957 handle_signature_help_request(&mut cx, mocked_response).await;
8958 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8959 .await;
8960 cx.editor(|editor, _, _| {
8961 let signature_help_state = editor.signature_help_state.popover().cloned();
8962 assert!(signature_help_state.is_some());
8963 assert_eq!(
8964 signature_help_state.unwrap().label,
8965 "param1: u8, param2: u8"
8966 );
8967 });
8968}
8969
8970#[gpui::test]
8971async fn test_signature_help(cx: &mut TestAppContext) {
8972 init_test(cx, |_| {});
8973 cx.update(|cx| {
8974 cx.update_global::<SettingsStore, _>(|settings, cx| {
8975 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8976 settings.auto_signature_help = Some(true);
8977 });
8978 });
8979 });
8980
8981 let mut cx = EditorLspTestContext::new_rust(
8982 lsp::ServerCapabilities {
8983 signature_help_provider: Some(lsp::SignatureHelpOptions {
8984 ..Default::default()
8985 }),
8986 ..Default::default()
8987 },
8988 cx,
8989 )
8990 .await;
8991
8992 // A test that directly calls `show_signature_help`
8993 cx.update_editor(|editor, window, cx| {
8994 editor.show_signature_help(&ShowSignatureHelp, window, cx);
8995 });
8996
8997 let mocked_response = lsp::SignatureHelp {
8998 signatures: vec![lsp::SignatureInformation {
8999 label: "fn sample(param1: u8, param2: u8)".to_string(),
9000 documentation: None,
9001 parameters: Some(vec![
9002 lsp::ParameterInformation {
9003 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9004 documentation: None,
9005 },
9006 lsp::ParameterInformation {
9007 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9008 documentation: None,
9009 },
9010 ]),
9011 active_parameter: None,
9012 }],
9013 active_signature: Some(0),
9014 active_parameter: Some(0),
9015 };
9016 handle_signature_help_request(&mut cx, mocked_response).await;
9017
9018 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9019 .await;
9020
9021 cx.editor(|editor, _, _| {
9022 let signature_help_state = editor.signature_help_state.popover().cloned();
9023 assert!(signature_help_state.is_some());
9024 assert_eq!(
9025 signature_help_state.unwrap().label,
9026 "param1: u8, param2: u8"
9027 );
9028 });
9029
9030 // When exiting outside from inside the brackets, `signature_help` is closed.
9031 cx.set_state(indoc! {"
9032 fn main() {
9033 sample(ˇ);
9034 }
9035
9036 fn sample(param1: u8, param2: u8) {}
9037 "});
9038
9039 cx.update_editor(|editor, window, cx| {
9040 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
9041 });
9042
9043 let mocked_response = lsp::SignatureHelp {
9044 signatures: Vec::new(),
9045 active_signature: None,
9046 active_parameter: None,
9047 };
9048 handle_signature_help_request(&mut cx, mocked_response).await;
9049
9050 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
9051 .await;
9052
9053 cx.editor(|editor, _, _| {
9054 assert!(!editor.signature_help_state.is_shown());
9055 });
9056
9057 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
9058 cx.set_state(indoc! {"
9059 fn main() {
9060 sample(ˇ);
9061 }
9062
9063 fn sample(param1: u8, param2: u8) {}
9064 "});
9065
9066 let mocked_response = lsp::SignatureHelp {
9067 signatures: vec![lsp::SignatureInformation {
9068 label: "fn sample(param1: u8, param2: u8)".to_string(),
9069 documentation: None,
9070 parameters: Some(vec![
9071 lsp::ParameterInformation {
9072 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9073 documentation: None,
9074 },
9075 lsp::ParameterInformation {
9076 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9077 documentation: None,
9078 },
9079 ]),
9080 active_parameter: None,
9081 }],
9082 active_signature: Some(0),
9083 active_parameter: Some(0),
9084 };
9085 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9086 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9087 .await;
9088 cx.editor(|editor, _, _| {
9089 assert!(editor.signature_help_state.is_shown());
9090 });
9091
9092 // Restore the popover with more parameter input
9093 cx.set_state(indoc! {"
9094 fn main() {
9095 sample(param1, param2ˇ);
9096 }
9097
9098 fn sample(param1: u8, param2: u8) {}
9099 "});
9100
9101 let mocked_response = lsp::SignatureHelp {
9102 signatures: vec![lsp::SignatureInformation {
9103 label: "fn sample(param1: u8, param2: u8)".to_string(),
9104 documentation: None,
9105 parameters: Some(vec![
9106 lsp::ParameterInformation {
9107 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9108 documentation: None,
9109 },
9110 lsp::ParameterInformation {
9111 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9112 documentation: None,
9113 },
9114 ]),
9115 active_parameter: None,
9116 }],
9117 active_signature: Some(0),
9118 active_parameter: Some(1),
9119 };
9120 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9121 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9122 .await;
9123
9124 // When selecting a range, the popover is gone.
9125 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
9126 cx.update_editor(|editor, window, cx| {
9127 editor.change_selections(None, window, cx, |s| {
9128 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
9129 })
9130 });
9131 cx.assert_editor_state(indoc! {"
9132 fn main() {
9133 sample(param1, «ˇparam2»);
9134 }
9135
9136 fn sample(param1: u8, param2: u8) {}
9137 "});
9138 cx.editor(|editor, _, _| {
9139 assert!(!editor.signature_help_state.is_shown());
9140 });
9141
9142 // When unselecting again, the popover is back if within the brackets.
9143 cx.update_editor(|editor, window, cx| {
9144 editor.change_selections(None, window, cx, |s| {
9145 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9146 })
9147 });
9148 cx.assert_editor_state(indoc! {"
9149 fn main() {
9150 sample(param1, ˇparam2);
9151 }
9152
9153 fn sample(param1: u8, param2: u8) {}
9154 "});
9155 handle_signature_help_request(&mut cx, mocked_response).await;
9156 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9157 .await;
9158 cx.editor(|editor, _, _| {
9159 assert!(editor.signature_help_state.is_shown());
9160 });
9161
9162 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
9163 cx.update_editor(|editor, window, cx| {
9164 editor.change_selections(None, window, cx, |s| {
9165 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
9166 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9167 })
9168 });
9169 cx.assert_editor_state(indoc! {"
9170 fn main() {
9171 sample(param1, ˇparam2);
9172 }
9173
9174 fn sample(param1: u8, param2: u8) {}
9175 "});
9176
9177 let mocked_response = lsp::SignatureHelp {
9178 signatures: vec![lsp::SignatureInformation {
9179 label: "fn sample(param1: u8, param2: u8)".to_string(),
9180 documentation: None,
9181 parameters: Some(vec![
9182 lsp::ParameterInformation {
9183 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9184 documentation: None,
9185 },
9186 lsp::ParameterInformation {
9187 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9188 documentation: None,
9189 },
9190 ]),
9191 active_parameter: None,
9192 }],
9193 active_signature: Some(0),
9194 active_parameter: Some(1),
9195 };
9196 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9197 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9198 .await;
9199 cx.update_editor(|editor, _, cx| {
9200 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
9201 });
9202 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
9203 .await;
9204 cx.update_editor(|editor, window, cx| {
9205 editor.change_selections(None, window, cx, |s| {
9206 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
9207 })
9208 });
9209 cx.assert_editor_state(indoc! {"
9210 fn main() {
9211 sample(param1, «ˇparam2»);
9212 }
9213
9214 fn sample(param1: u8, param2: u8) {}
9215 "});
9216 cx.update_editor(|editor, window, cx| {
9217 editor.change_selections(None, window, cx, |s| {
9218 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9219 })
9220 });
9221 cx.assert_editor_state(indoc! {"
9222 fn main() {
9223 sample(param1, ˇparam2);
9224 }
9225
9226 fn sample(param1: u8, param2: u8) {}
9227 "});
9228 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
9229 .await;
9230}
9231
9232#[gpui::test]
9233async fn test_completion(cx: &mut TestAppContext) {
9234 init_test(cx, |_| {});
9235
9236 let mut cx = EditorLspTestContext::new_rust(
9237 lsp::ServerCapabilities {
9238 completion_provider: Some(lsp::CompletionOptions {
9239 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9240 resolve_provider: Some(true),
9241 ..Default::default()
9242 }),
9243 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9244 ..Default::default()
9245 },
9246 cx,
9247 )
9248 .await;
9249 let counter = Arc::new(AtomicUsize::new(0));
9250
9251 cx.set_state(indoc! {"
9252 oneˇ
9253 two
9254 three
9255 "});
9256 cx.simulate_keystroke(".");
9257 handle_completion_request(
9258 &mut cx,
9259 indoc! {"
9260 one.|<>
9261 two
9262 three
9263 "},
9264 vec!["first_completion", "second_completion"],
9265 counter.clone(),
9266 )
9267 .await;
9268 cx.condition(|editor, _| editor.context_menu_visible())
9269 .await;
9270 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
9271
9272 let _handler = handle_signature_help_request(
9273 &mut cx,
9274 lsp::SignatureHelp {
9275 signatures: vec![lsp::SignatureInformation {
9276 label: "test signature".to_string(),
9277 documentation: None,
9278 parameters: Some(vec![lsp::ParameterInformation {
9279 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
9280 documentation: None,
9281 }]),
9282 active_parameter: None,
9283 }],
9284 active_signature: None,
9285 active_parameter: None,
9286 },
9287 );
9288 cx.update_editor(|editor, window, cx| {
9289 assert!(
9290 !editor.signature_help_state.is_shown(),
9291 "No signature help was called for"
9292 );
9293 editor.show_signature_help(&ShowSignatureHelp, window, cx);
9294 });
9295 cx.run_until_parked();
9296 cx.update_editor(|editor, _, _| {
9297 assert!(
9298 !editor.signature_help_state.is_shown(),
9299 "No signature help should be shown when completions menu is open"
9300 );
9301 });
9302
9303 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9304 editor.context_menu_next(&Default::default(), window, cx);
9305 editor
9306 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9307 .unwrap()
9308 });
9309 cx.assert_editor_state(indoc! {"
9310 one.second_completionˇ
9311 two
9312 three
9313 "});
9314
9315 handle_resolve_completion_request(
9316 &mut cx,
9317 Some(vec![
9318 (
9319 //This overlaps with the primary completion edit which is
9320 //misbehavior from the LSP spec, test that we filter it out
9321 indoc! {"
9322 one.second_ˇcompletion
9323 two
9324 threeˇ
9325 "},
9326 "overlapping additional edit",
9327 ),
9328 (
9329 indoc! {"
9330 one.second_completion
9331 two
9332 threeˇ
9333 "},
9334 "\nadditional edit",
9335 ),
9336 ]),
9337 )
9338 .await;
9339 apply_additional_edits.await.unwrap();
9340 cx.assert_editor_state(indoc! {"
9341 one.second_completionˇ
9342 two
9343 three
9344 additional edit
9345 "});
9346
9347 cx.set_state(indoc! {"
9348 one.second_completion
9349 twoˇ
9350 threeˇ
9351 additional edit
9352 "});
9353 cx.simulate_keystroke(" ");
9354 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9355 cx.simulate_keystroke("s");
9356 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9357
9358 cx.assert_editor_state(indoc! {"
9359 one.second_completion
9360 two sˇ
9361 three sˇ
9362 additional edit
9363 "});
9364 handle_completion_request(
9365 &mut cx,
9366 indoc! {"
9367 one.second_completion
9368 two s
9369 three <s|>
9370 additional edit
9371 "},
9372 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
9373 counter.clone(),
9374 )
9375 .await;
9376 cx.condition(|editor, _| editor.context_menu_visible())
9377 .await;
9378 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
9379
9380 cx.simulate_keystroke("i");
9381
9382 handle_completion_request(
9383 &mut cx,
9384 indoc! {"
9385 one.second_completion
9386 two si
9387 three <si|>
9388 additional edit
9389 "},
9390 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
9391 counter.clone(),
9392 )
9393 .await;
9394 cx.condition(|editor, _| editor.context_menu_visible())
9395 .await;
9396 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
9397
9398 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9399 editor
9400 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9401 .unwrap()
9402 });
9403 cx.assert_editor_state(indoc! {"
9404 one.second_completion
9405 two sixth_completionˇ
9406 three sixth_completionˇ
9407 additional edit
9408 "});
9409
9410 apply_additional_edits.await.unwrap();
9411
9412 update_test_language_settings(&mut cx, |settings| {
9413 settings.defaults.show_completions_on_input = Some(false);
9414 });
9415 cx.set_state("editorˇ");
9416 cx.simulate_keystroke(".");
9417 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9418 cx.simulate_keystrokes("c l o");
9419 cx.assert_editor_state("editor.cloˇ");
9420 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9421 cx.update_editor(|editor, window, cx| {
9422 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
9423 });
9424 handle_completion_request(
9425 &mut cx,
9426 "editor.<clo|>",
9427 vec!["close", "clobber"],
9428 counter.clone(),
9429 )
9430 .await;
9431 cx.condition(|editor, _| editor.context_menu_visible())
9432 .await;
9433 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
9434
9435 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9436 editor
9437 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9438 .unwrap()
9439 });
9440 cx.assert_editor_state("editor.closeˇ");
9441 handle_resolve_completion_request(&mut cx, None).await;
9442 apply_additional_edits.await.unwrap();
9443}
9444
9445#[gpui::test]
9446async fn test_word_completion(cx: &mut TestAppContext) {
9447 let lsp_fetch_timeout_ms = 10;
9448 init_test(cx, |language_settings| {
9449 language_settings.defaults.completions = Some(CompletionSettings {
9450 words: WordsCompletionMode::Fallback,
9451 lsp: true,
9452 lsp_fetch_timeout_ms: 10,
9453 });
9454 });
9455
9456 let mut cx = EditorLspTestContext::new_rust(
9457 lsp::ServerCapabilities {
9458 completion_provider: Some(lsp::CompletionOptions {
9459 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9460 ..lsp::CompletionOptions::default()
9461 }),
9462 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9463 ..lsp::ServerCapabilities::default()
9464 },
9465 cx,
9466 )
9467 .await;
9468
9469 let throttle_completions = Arc::new(AtomicBool::new(false));
9470
9471 let lsp_throttle_completions = throttle_completions.clone();
9472 let _completion_requests_handler =
9473 cx.lsp
9474 .server
9475 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
9476 let lsp_throttle_completions = lsp_throttle_completions.clone();
9477 let cx = cx.clone();
9478 async move {
9479 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
9480 cx.background_executor()
9481 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
9482 .await;
9483 }
9484 Ok(Some(lsp::CompletionResponse::Array(vec![
9485 lsp::CompletionItem {
9486 label: "first".into(),
9487 ..lsp::CompletionItem::default()
9488 },
9489 lsp::CompletionItem {
9490 label: "last".into(),
9491 ..lsp::CompletionItem::default()
9492 },
9493 ])))
9494 }
9495 });
9496
9497 cx.set_state(indoc! {"
9498 oneˇ
9499 two
9500 three
9501 "});
9502 cx.simulate_keystroke(".");
9503 cx.executor().run_until_parked();
9504 cx.condition(|editor, _| editor.context_menu_visible())
9505 .await;
9506 cx.update_editor(|editor, window, cx| {
9507 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9508 {
9509 assert_eq!(
9510 completion_menu_entries(&menu),
9511 &["first", "last"],
9512 "When LSP server is fast to reply, no fallback word completions are used"
9513 );
9514 } else {
9515 panic!("expected completion menu to be open");
9516 }
9517 editor.cancel(&Cancel, window, cx);
9518 });
9519 cx.executor().run_until_parked();
9520 cx.condition(|editor, _| !editor.context_menu_visible())
9521 .await;
9522
9523 throttle_completions.store(true, atomic::Ordering::Release);
9524 cx.simulate_keystroke(".");
9525 cx.executor()
9526 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
9527 cx.executor().run_until_parked();
9528 cx.condition(|editor, _| editor.context_menu_visible())
9529 .await;
9530 cx.update_editor(|editor, _, _| {
9531 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9532 {
9533 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
9534 "When LSP server is slow, document words can be shown instead, if configured accordingly");
9535 } else {
9536 panic!("expected completion menu to be open");
9537 }
9538 });
9539}
9540
9541#[gpui::test]
9542async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
9543 init_test(cx, |language_settings| {
9544 language_settings.defaults.completions = Some(CompletionSettings {
9545 words: WordsCompletionMode::Enabled,
9546 lsp: true,
9547 lsp_fetch_timeout_ms: 0,
9548 });
9549 });
9550
9551 let mut cx = EditorLspTestContext::new_rust(
9552 lsp::ServerCapabilities {
9553 completion_provider: Some(lsp::CompletionOptions {
9554 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9555 ..lsp::CompletionOptions::default()
9556 }),
9557 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9558 ..lsp::ServerCapabilities::default()
9559 },
9560 cx,
9561 )
9562 .await;
9563
9564 let _completion_requests_handler =
9565 cx.lsp
9566 .server
9567 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
9568 Ok(Some(lsp::CompletionResponse::Array(vec![
9569 lsp::CompletionItem {
9570 label: "first".into(),
9571 ..lsp::CompletionItem::default()
9572 },
9573 lsp::CompletionItem {
9574 label: "last".into(),
9575 ..lsp::CompletionItem::default()
9576 },
9577 ])))
9578 });
9579
9580 cx.set_state(indoc! {"ˇ
9581 first
9582 last
9583 second
9584 "});
9585 cx.simulate_keystroke(".");
9586 cx.executor().run_until_parked();
9587 cx.condition(|editor, _| editor.context_menu_visible())
9588 .await;
9589 cx.update_editor(|editor, _, _| {
9590 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9591 {
9592 assert_eq!(
9593 completion_menu_entries(&menu),
9594 &["first", "last", "second"],
9595 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
9596 );
9597 } else {
9598 panic!("expected completion menu to be open");
9599 }
9600 });
9601}
9602
9603#[gpui::test]
9604async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
9605 init_test(cx, |language_settings| {
9606 language_settings.defaults.completions = Some(CompletionSettings {
9607 words: WordsCompletionMode::Disabled,
9608 lsp: true,
9609 lsp_fetch_timeout_ms: 0,
9610 });
9611 });
9612
9613 let mut cx = EditorLspTestContext::new_rust(
9614 lsp::ServerCapabilities {
9615 completion_provider: Some(lsp::CompletionOptions {
9616 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9617 ..lsp::CompletionOptions::default()
9618 }),
9619 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9620 ..lsp::ServerCapabilities::default()
9621 },
9622 cx,
9623 )
9624 .await;
9625
9626 let _completion_requests_handler =
9627 cx.lsp
9628 .server
9629 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
9630 panic!("LSP completions should not be queried when dealing with word completions")
9631 });
9632
9633 cx.set_state(indoc! {"ˇ
9634 first
9635 last
9636 second
9637 "});
9638 cx.update_editor(|editor, window, cx| {
9639 editor.show_word_completions(&ShowWordCompletions, window, cx);
9640 });
9641 cx.executor().run_until_parked();
9642 cx.condition(|editor, _| editor.context_menu_visible())
9643 .await;
9644 cx.update_editor(|editor, _, _| {
9645 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9646 {
9647 assert_eq!(
9648 completion_menu_entries(&menu),
9649 &["first", "last", "second"],
9650 "`ShowWordCompletions` action should show word completions"
9651 );
9652 } else {
9653 panic!("expected completion menu to be open");
9654 }
9655 });
9656
9657 cx.simulate_keystroke("l");
9658 cx.executor().run_until_parked();
9659 cx.condition(|editor, _| editor.context_menu_visible())
9660 .await;
9661 cx.update_editor(|editor, _, _| {
9662 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9663 {
9664 assert_eq!(
9665 completion_menu_entries(&menu),
9666 &["last"],
9667 "After showing word completions, further editing should filter them and not query the LSP"
9668 );
9669 } else {
9670 panic!("expected completion menu to be open");
9671 }
9672 });
9673}
9674
9675#[gpui::test]
9676async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
9677 init_test(cx, |language_settings| {
9678 language_settings.defaults.completions = Some(CompletionSettings {
9679 words: WordsCompletionMode::Fallback,
9680 lsp: false,
9681 lsp_fetch_timeout_ms: 0,
9682 });
9683 });
9684
9685 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
9686
9687 cx.set_state(indoc! {"ˇ
9688 0_usize
9689 let
9690 33
9691 4.5f32
9692 "});
9693 cx.update_editor(|editor, window, cx| {
9694 editor.show_completions(&ShowCompletions::default(), window, cx);
9695 });
9696 cx.executor().run_until_parked();
9697 cx.condition(|editor, _| editor.context_menu_visible())
9698 .await;
9699 cx.update_editor(|editor, window, cx| {
9700 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9701 {
9702 assert_eq!(
9703 completion_menu_entries(&menu),
9704 &["let"],
9705 "With no digits in the completion query, no digits should be in the word completions"
9706 );
9707 } else {
9708 panic!("expected completion menu to be open");
9709 }
9710 editor.cancel(&Cancel, window, cx);
9711 });
9712
9713 cx.set_state(indoc! {"3ˇ
9714 0_usize
9715 let
9716 3
9717 33.35f32
9718 "});
9719 cx.update_editor(|editor, window, cx| {
9720 editor.show_completions(&ShowCompletions::default(), window, cx);
9721 });
9722 cx.executor().run_until_parked();
9723 cx.condition(|editor, _| editor.context_menu_visible())
9724 .await;
9725 cx.update_editor(|editor, _, _| {
9726 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9727 {
9728 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
9729 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
9730 } else {
9731 panic!("expected completion menu to be open");
9732 }
9733 });
9734}
9735
9736fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
9737 let position = || lsp::Position {
9738 line: params.text_document_position.position.line,
9739 character: params.text_document_position.position.character,
9740 };
9741 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
9742 range: lsp::Range {
9743 start: position(),
9744 end: position(),
9745 },
9746 new_text: text.to_string(),
9747 }))
9748}
9749
9750#[gpui::test]
9751async fn test_multiline_completion(cx: &mut TestAppContext) {
9752 init_test(cx, |_| {});
9753
9754 let fs = FakeFs::new(cx.executor());
9755 fs.insert_tree(
9756 path!("/a"),
9757 json!({
9758 "main.ts": "a",
9759 }),
9760 )
9761 .await;
9762
9763 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
9764 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9765 let typescript_language = Arc::new(Language::new(
9766 LanguageConfig {
9767 name: "TypeScript".into(),
9768 matcher: LanguageMatcher {
9769 path_suffixes: vec!["ts".to_string()],
9770 ..LanguageMatcher::default()
9771 },
9772 line_comments: vec!["// ".into()],
9773 ..LanguageConfig::default()
9774 },
9775 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
9776 ));
9777 language_registry.add(typescript_language.clone());
9778 let mut fake_servers = language_registry.register_fake_lsp(
9779 "TypeScript",
9780 FakeLspAdapter {
9781 capabilities: lsp::ServerCapabilities {
9782 completion_provider: Some(lsp::CompletionOptions {
9783 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9784 ..lsp::CompletionOptions::default()
9785 }),
9786 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9787 ..lsp::ServerCapabilities::default()
9788 },
9789 // Emulate vtsls label generation
9790 label_for_completion: Some(Box::new(|item, _| {
9791 let text = if let Some(description) = item
9792 .label_details
9793 .as_ref()
9794 .and_then(|label_details| label_details.description.as_ref())
9795 {
9796 format!("{} {}", item.label, description)
9797 } else if let Some(detail) = &item.detail {
9798 format!("{} {}", item.label, detail)
9799 } else {
9800 item.label.clone()
9801 };
9802 let len = text.len();
9803 Some(language::CodeLabel {
9804 text,
9805 runs: Vec::new(),
9806 filter_range: 0..len,
9807 })
9808 })),
9809 ..FakeLspAdapter::default()
9810 },
9811 );
9812 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9813 let cx = &mut VisualTestContext::from_window(*workspace, cx);
9814 let worktree_id = workspace
9815 .update(cx, |workspace, _window, cx| {
9816 workspace.project().update(cx, |project, cx| {
9817 project.worktrees(cx).next().unwrap().read(cx).id()
9818 })
9819 })
9820 .unwrap();
9821 let _buffer = project
9822 .update(cx, |project, cx| {
9823 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
9824 })
9825 .await
9826 .unwrap();
9827 let editor = workspace
9828 .update(cx, |workspace, window, cx| {
9829 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
9830 })
9831 .unwrap()
9832 .await
9833 .unwrap()
9834 .downcast::<Editor>()
9835 .unwrap();
9836 let fake_server = fake_servers.next().await.unwrap();
9837
9838 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
9839 let multiline_label_2 = "a\nb\nc\n";
9840 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
9841 let multiline_description = "d\ne\nf\n";
9842 let multiline_detail_2 = "g\nh\ni\n";
9843
9844 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
9845 move |params, _| async move {
9846 Ok(Some(lsp::CompletionResponse::Array(vec![
9847 lsp::CompletionItem {
9848 label: multiline_label.to_string(),
9849 text_edit: gen_text_edit(¶ms, "new_text_1"),
9850 ..lsp::CompletionItem::default()
9851 },
9852 lsp::CompletionItem {
9853 label: "single line label 1".to_string(),
9854 detail: Some(multiline_detail.to_string()),
9855 text_edit: gen_text_edit(¶ms, "new_text_2"),
9856 ..lsp::CompletionItem::default()
9857 },
9858 lsp::CompletionItem {
9859 label: "single line label 2".to_string(),
9860 label_details: Some(lsp::CompletionItemLabelDetails {
9861 description: Some(multiline_description.to_string()),
9862 detail: None,
9863 }),
9864 text_edit: gen_text_edit(¶ms, "new_text_2"),
9865 ..lsp::CompletionItem::default()
9866 },
9867 lsp::CompletionItem {
9868 label: multiline_label_2.to_string(),
9869 detail: Some(multiline_detail_2.to_string()),
9870 text_edit: gen_text_edit(¶ms, "new_text_3"),
9871 ..lsp::CompletionItem::default()
9872 },
9873 lsp::CompletionItem {
9874 label: "Label with many spaces and \t but without newlines".to_string(),
9875 detail: Some(
9876 "Details with many spaces and \t but without newlines".to_string(),
9877 ),
9878 text_edit: gen_text_edit(¶ms, "new_text_4"),
9879 ..lsp::CompletionItem::default()
9880 },
9881 ])))
9882 },
9883 );
9884
9885 editor.update_in(cx, |editor, window, cx| {
9886 cx.focus_self(window);
9887 editor.move_to_end(&MoveToEnd, window, cx);
9888 editor.handle_input(".", window, cx);
9889 });
9890 cx.run_until_parked();
9891 completion_handle.next().await.unwrap();
9892
9893 editor.update(cx, |editor, _| {
9894 assert!(editor.context_menu_visible());
9895 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9896 {
9897 let completion_labels = menu
9898 .completions
9899 .borrow()
9900 .iter()
9901 .map(|c| c.label.text.clone())
9902 .collect::<Vec<_>>();
9903 assert_eq!(
9904 completion_labels,
9905 &[
9906 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
9907 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
9908 "single line label 2 d e f ",
9909 "a b c g h i ",
9910 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
9911 ],
9912 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
9913 );
9914
9915 for completion in menu
9916 .completions
9917 .borrow()
9918 .iter() {
9919 assert_eq!(
9920 completion.label.filter_range,
9921 0..completion.label.text.len(),
9922 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
9923 );
9924 }
9925 } else {
9926 panic!("expected completion menu to be open");
9927 }
9928 });
9929}
9930
9931#[gpui::test]
9932async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
9933 init_test(cx, |_| {});
9934 let mut cx = EditorLspTestContext::new_rust(
9935 lsp::ServerCapabilities {
9936 completion_provider: Some(lsp::CompletionOptions {
9937 trigger_characters: Some(vec![".".to_string()]),
9938 ..Default::default()
9939 }),
9940 ..Default::default()
9941 },
9942 cx,
9943 )
9944 .await;
9945 cx.lsp
9946 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
9947 Ok(Some(lsp::CompletionResponse::Array(vec![
9948 lsp::CompletionItem {
9949 label: "first".into(),
9950 ..Default::default()
9951 },
9952 lsp::CompletionItem {
9953 label: "last".into(),
9954 ..Default::default()
9955 },
9956 ])))
9957 });
9958 cx.set_state("variableˇ");
9959 cx.simulate_keystroke(".");
9960 cx.executor().run_until_parked();
9961
9962 cx.update_editor(|editor, _, _| {
9963 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9964 {
9965 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
9966 } else {
9967 panic!("expected completion menu to be open");
9968 }
9969 });
9970
9971 cx.update_editor(|editor, window, cx| {
9972 editor.move_page_down(&MovePageDown::default(), window, cx);
9973 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9974 {
9975 assert!(
9976 menu.selected_item == 1,
9977 "expected PageDown to select the last item from the context menu"
9978 );
9979 } else {
9980 panic!("expected completion menu to stay open after PageDown");
9981 }
9982 });
9983
9984 cx.update_editor(|editor, window, cx| {
9985 editor.move_page_up(&MovePageUp::default(), window, cx);
9986 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9987 {
9988 assert!(
9989 menu.selected_item == 0,
9990 "expected PageUp to select the first item from the context menu"
9991 );
9992 } else {
9993 panic!("expected completion menu to stay open after PageUp");
9994 }
9995 });
9996}
9997
9998#[gpui::test]
9999async fn test_completion_sort(cx: &mut TestAppContext) {
10000 init_test(cx, |_| {});
10001 let mut cx = EditorLspTestContext::new_rust(
10002 lsp::ServerCapabilities {
10003 completion_provider: Some(lsp::CompletionOptions {
10004 trigger_characters: Some(vec![".".to_string()]),
10005 ..Default::default()
10006 }),
10007 ..Default::default()
10008 },
10009 cx,
10010 )
10011 .await;
10012 cx.lsp
10013 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
10014 Ok(Some(lsp::CompletionResponse::Array(vec![
10015 lsp::CompletionItem {
10016 label: "Range".into(),
10017 sort_text: Some("a".into()),
10018 ..Default::default()
10019 },
10020 lsp::CompletionItem {
10021 label: "r".into(),
10022 sort_text: Some("b".into()),
10023 ..Default::default()
10024 },
10025 lsp::CompletionItem {
10026 label: "ret".into(),
10027 sort_text: Some("c".into()),
10028 ..Default::default()
10029 },
10030 lsp::CompletionItem {
10031 label: "return".into(),
10032 sort_text: Some("d".into()),
10033 ..Default::default()
10034 },
10035 lsp::CompletionItem {
10036 label: "slice".into(),
10037 sort_text: Some("d".into()),
10038 ..Default::default()
10039 },
10040 ])))
10041 });
10042 cx.set_state("rˇ");
10043 cx.executor().run_until_parked();
10044 cx.update_editor(|editor, window, cx| {
10045 editor.show_completions(
10046 &ShowCompletions {
10047 trigger: Some("r".into()),
10048 },
10049 window,
10050 cx,
10051 );
10052 });
10053 cx.executor().run_until_parked();
10054
10055 cx.update_editor(|editor, _, _| {
10056 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10057 {
10058 assert_eq!(
10059 completion_menu_entries(&menu),
10060 &["r", "ret", "Range", "return"]
10061 );
10062 } else {
10063 panic!("expected completion menu to be open");
10064 }
10065 });
10066}
10067
10068#[gpui::test]
10069async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
10070 init_test(cx, |_| {});
10071
10072 let mut cx = EditorLspTestContext::new_rust(
10073 lsp::ServerCapabilities {
10074 completion_provider: Some(lsp::CompletionOptions {
10075 trigger_characters: Some(vec![".".to_string()]),
10076 resolve_provider: Some(true),
10077 ..Default::default()
10078 }),
10079 ..Default::default()
10080 },
10081 cx,
10082 )
10083 .await;
10084
10085 cx.set_state("fn main() { let a = 2ˇ; }");
10086 cx.simulate_keystroke(".");
10087 let completion_item = lsp::CompletionItem {
10088 label: "Some".into(),
10089 kind: Some(lsp::CompletionItemKind::SNIPPET),
10090 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
10091 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
10092 kind: lsp::MarkupKind::Markdown,
10093 value: "```rust\nSome(2)\n```".to_string(),
10094 })),
10095 deprecated: Some(false),
10096 sort_text: Some("Some".to_string()),
10097 filter_text: Some("Some".to_string()),
10098 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
10099 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
10100 range: lsp::Range {
10101 start: lsp::Position {
10102 line: 0,
10103 character: 22,
10104 },
10105 end: lsp::Position {
10106 line: 0,
10107 character: 22,
10108 },
10109 },
10110 new_text: "Some(2)".to_string(),
10111 })),
10112 additional_text_edits: Some(vec![lsp::TextEdit {
10113 range: lsp::Range {
10114 start: lsp::Position {
10115 line: 0,
10116 character: 20,
10117 },
10118 end: lsp::Position {
10119 line: 0,
10120 character: 22,
10121 },
10122 },
10123 new_text: "".to_string(),
10124 }]),
10125 ..Default::default()
10126 };
10127
10128 let closure_completion_item = completion_item.clone();
10129 let counter = Arc::new(AtomicUsize::new(0));
10130 let counter_clone = counter.clone();
10131 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
10132 let task_completion_item = closure_completion_item.clone();
10133 counter_clone.fetch_add(1, atomic::Ordering::Release);
10134 async move {
10135 Ok(Some(lsp::CompletionResponse::Array(vec![
10136 task_completion_item,
10137 ])))
10138 }
10139 });
10140
10141 cx.condition(|editor, _| editor.context_menu_visible())
10142 .await;
10143 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
10144 assert!(request.next().await.is_some());
10145 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10146
10147 cx.simulate_keystrokes("S o m");
10148 cx.condition(|editor, _| editor.context_menu_visible())
10149 .await;
10150 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
10151 assert!(request.next().await.is_some());
10152 assert!(request.next().await.is_some());
10153 assert!(request.next().await.is_some());
10154 request.close();
10155 assert!(request.next().await.is_none());
10156 assert_eq!(
10157 counter.load(atomic::Ordering::Acquire),
10158 4,
10159 "With the completions menu open, only one LSP request should happen per input"
10160 );
10161}
10162
10163#[gpui::test]
10164async fn test_toggle_comment(cx: &mut TestAppContext) {
10165 init_test(cx, |_| {});
10166 let mut cx = EditorTestContext::new(cx).await;
10167 let language = Arc::new(Language::new(
10168 LanguageConfig {
10169 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
10170 ..Default::default()
10171 },
10172 Some(tree_sitter_rust::LANGUAGE.into()),
10173 ));
10174 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
10175
10176 // If multiple selections intersect a line, the line is only toggled once.
10177 cx.set_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 // The comment prefix is inserted at the same column for every line in a
10196 // selection.
10197 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10198
10199 cx.assert_editor_state(indoc! {"
10200 fn a() {
10201 // «b();
10202 // c();
10203 ˇ»// d();
10204 }
10205 "});
10206
10207 // If a selection ends at the beginning of a line, that line is not toggled.
10208 cx.set_selections_state(indoc! {"
10209 fn a() {
10210 // b();
10211 «// c();
10212 ˇ» // d();
10213 }
10214 "});
10215
10216 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10217
10218 cx.assert_editor_state(indoc! {"
10219 fn a() {
10220 // b();
10221 «c();
10222 ˇ» // d();
10223 }
10224 "});
10225
10226 // If a selection span a single line and is empty, the line is toggled.
10227 cx.set_state(indoc! {"
10228 fn a() {
10229 a();
10230 b();
10231 ˇ
10232 }
10233 "});
10234
10235 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10236
10237 cx.assert_editor_state(indoc! {"
10238 fn a() {
10239 a();
10240 b();
10241 //•ˇ
10242 }
10243 "});
10244
10245 // If a selection span multiple lines, empty lines are not toggled.
10246 cx.set_state(indoc! {"
10247 fn a() {
10248 «a();
10249
10250 c();ˇ»
10251 }
10252 "});
10253
10254 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10255
10256 cx.assert_editor_state(indoc! {"
10257 fn a() {
10258 // «a();
10259
10260 // c();ˇ»
10261 }
10262 "});
10263
10264 // If a selection includes multiple comment prefixes, all lines are uncommented.
10265 cx.set_state(indoc! {"
10266 fn a() {
10267 «// a();
10268 /// b();
10269 //! c();ˇ»
10270 }
10271 "});
10272
10273 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10274
10275 cx.assert_editor_state(indoc! {"
10276 fn a() {
10277 «a();
10278 b();
10279 c();ˇ»
10280 }
10281 "});
10282}
10283
10284#[gpui::test]
10285async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
10286 init_test(cx, |_| {});
10287 let mut cx = EditorTestContext::new(cx).await;
10288 let language = Arc::new(Language::new(
10289 LanguageConfig {
10290 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
10291 ..Default::default()
10292 },
10293 Some(tree_sitter_rust::LANGUAGE.into()),
10294 ));
10295 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
10296
10297 let toggle_comments = &ToggleComments {
10298 advance_downwards: false,
10299 ignore_indent: true,
10300 };
10301
10302 // If multiple selections intersect a line, the line is only toggled once.
10303 cx.set_state(indoc! {"
10304 fn a() {
10305 // «b();
10306 // c();
10307 // ˇ» d();
10308 }
10309 "});
10310
10311 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10312
10313 cx.assert_editor_state(indoc! {"
10314 fn a() {
10315 «b();
10316 c();
10317 ˇ» d();
10318 }
10319 "});
10320
10321 // The comment prefix is inserted at the beginning of each line
10322 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10323
10324 cx.assert_editor_state(indoc! {"
10325 fn a() {
10326 // «b();
10327 // c();
10328 // ˇ» d();
10329 }
10330 "});
10331
10332 // If a selection ends at the beginning of a line, that line is not toggled.
10333 cx.set_selections_state(indoc! {"
10334 fn a() {
10335 // b();
10336 // «c();
10337 ˇ»// d();
10338 }
10339 "});
10340
10341 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10342
10343 cx.assert_editor_state(indoc! {"
10344 fn a() {
10345 // b();
10346 «c();
10347 ˇ»// d();
10348 }
10349 "});
10350
10351 // If a selection span a single line and is empty, the line is toggled.
10352 cx.set_state(indoc! {"
10353 fn a() {
10354 a();
10355 b();
10356 ˇ
10357 }
10358 "});
10359
10360 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10361
10362 cx.assert_editor_state(indoc! {"
10363 fn a() {
10364 a();
10365 b();
10366 //ˇ
10367 }
10368 "});
10369
10370 // If a selection span multiple lines, empty lines are not toggled.
10371 cx.set_state(indoc! {"
10372 fn a() {
10373 «a();
10374
10375 c();ˇ»
10376 }
10377 "});
10378
10379 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10380
10381 cx.assert_editor_state(indoc! {"
10382 fn a() {
10383 // «a();
10384
10385 // c();ˇ»
10386 }
10387 "});
10388
10389 // If a selection includes multiple comment prefixes, all lines are uncommented.
10390 cx.set_state(indoc! {"
10391 fn a() {
10392 // «a();
10393 /// b();
10394 //! c();ˇ»
10395 }
10396 "});
10397
10398 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10399
10400 cx.assert_editor_state(indoc! {"
10401 fn a() {
10402 «a();
10403 b();
10404 c();ˇ»
10405 }
10406 "});
10407}
10408
10409#[gpui::test]
10410async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
10411 init_test(cx, |_| {});
10412
10413 let language = Arc::new(Language::new(
10414 LanguageConfig {
10415 line_comments: vec!["// ".into()],
10416 ..Default::default()
10417 },
10418 Some(tree_sitter_rust::LANGUAGE.into()),
10419 ));
10420
10421 let mut cx = EditorTestContext::new(cx).await;
10422
10423 cx.language_registry().add(language.clone());
10424 cx.update_buffer(|buffer, cx| {
10425 buffer.set_language(Some(language), cx);
10426 });
10427
10428 let toggle_comments = &ToggleComments {
10429 advance_downwards: true,
10430 ignore_indent: false,
10431 };
10432
10433 // Single cursor on one line -> advance
10434 // Cursor moves horizontally 3 characters as well on non-blank line
10435 cx.set_state(indoc!(
10436 "fn a() {
10437 ˇdog();
10438 cat();
10439 }"
10440 ));
10441 cx.update_editor(|editor, window, cx| {
10442 editor.toggle_comments(toggle_comments, window, cx);
10443 });
10444 cx.assert_editor_state(indoc!(
10445 "fn a() {
10446 // dog();
10447 catˇ();
10448 }"
10449 ));
10450
10451 // Single selection on one line -> don't advance
10452 cx.set_state(indoc!(
10453 "fn a() {
10454 «dog()ˇ»;
10455 cat();
10456 }"
10457 ));
10458 cx.update_editor(|editor, window, cx| {
10459 editor.toggle_comments(toggle_comments, window, cx);
10460 });
10461 cx.assert_editor_state(indoc!(
10462 "fn a() {
10463 // «dog()ˇ»;
10464 cat();
10465 }"
10466 ));
10467
10468 // Multiple cursors on one line -> advance
10469 cx.set_state(indoc!(
10470 "fn a() {
10471 ˇdˇog();
10472 cat();
10473 }"
10474 ));
10475 cx.update_editor(|editor, window, cx| {
10476 editor.toggle_comments(toggle_comments, window, cx);
10477 });
10478 cx.assert_editor_state(indoc!(
10479 "fn a() {
10480 // dog();
10481 catˇ(ˇ);
10482 }"
10483 ));
10484
10485 // Multiple cursors on one line, with selection -> don't advance
10486 cx.set_state(indoc!(
10487 "fn a() {
10488 ˇdˇog«()ˇ»;
10489 cat();
10490 }"
10491 ));
10492 cx.update_editor(|editor, window, cx| {
10493 editor.toggle_comments(toggle_comments, window, cx);
10494 });
10495 cx.assert_editor_state(indoc!(
10496 "fn a() {
10497 // ˇdˇog«()ˇ»;
10498 cat();
10499 }"
10500 ));
10501
10502 // Single cursor on one line -> advance
10503 // Cursor moves to column 0 on blank line
10504 cx.set_state(indoc!(
10505 "fn a() {
10506 ˇdog();
10507
10508 cat();
10509 }"
10510 ));
10511 cx.update_editor(|editor, window, cx| {
10512 editor.toggle_comments(toggle_comments, window, cx);
10513 });
10514 cx.assert_editor_state(indoc!(
10515 "fn a() {
10516 // dog();
10517 ˇ
10518 cat();
10519 }"
10520 ));
10521
10522 // Single cursor on one line -> advance
10523 // Cursor starts and ends at column 0
10524 cx.set_state(indoc!(
10525 "fn a() {
10526 ˇ dog();
10527 cat();
10528 }"
10529 ));
10530 cx.update_editor(|editor, window, cx| {
10531 editor.toggle_comments(toggle_comments, window, cx);
10532 });
10533 cx.assert_editor_state(indoc!(
10534 "fn a() {
10535 // dog();
10536 ˇ cat();
10537 }"
10538 ));
10539}
10540
10541#[gpui::test]
10542async fn test_toggle_block_comment(cx: &mut TestAppContext) {
10543 init_test(cx, |_| {});
10544
10545 let mut cx = EditorTestContext::new(cx).await;
10546
10547 let html_language = Arc::new(
10548 Language::new(
10549 LanguageConfig {
10550 name: "HTML".into(),
10551 block_comment: Some(("<!-- ".into(), " -->".into())),
10552 ..Default::default()
10553 },
10554 Some(tree_sitter_html::LANGUAGE.into()),
10555 )
10556 .with_injection_query(
10557 r#"
10558 (script_element
10559 (raw_text) @injection.content
10560 (#set! injection.language "javascript"))
10561 "#,
10562 )
10563 .unwrap(),
10564 );
10565
10566 let javascript_language = Arc::new(Language::new(
10567 LanguageConfig {
10568 name: "JavaScript".into(),
10569 line_comments: vec!["// ".into()],
10570 ..Default::default()
10571 },
10572 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10573 ));
10574
10575 cx.language_registry().add(html_language.clone());
10576 cx.language_registry().add(javascript_language.clone());
10577 cx.update_buffer(|buffer, cx| {
10578 buffer.set_language(Some(html_language), cx);
10579 });
10580
10581 // Toggle comments for empty selections
10582 cx.set_state(
10583 &r#"
10584 <p>A</p>ˇ
10585 <p>B</p>ˇ
10586 <p>C</p>ˇ
10587 "#
10588 .unindent(),
10589 );
10590 cx.update_editor(|editor, window, cx| {
10591 editor.toggle_comments(&ToggleComments::default(), window, cx)
10592 });
10593 cx.assert_editor_state(
10594 &r#"
10595 <!-- <p>A</p>ˇ -->
10596 <!-- <p>B</p>ˇ -->
10597 <!-- <p>C</p>ˇ -->
10598 "#
10599 .unindent(),
10600 );
10601 cx.update_editor(|editor, window, cx| {
10602 editor.toggle_comments(&ToggleComments::default(), window, cx)
10603 });
10604 cx.assert_editor_state(
10605 &r#"
10606 <p>A</p>ˇ
10607 <p>B</p>ˇ
10608 <p>C</p>ˇ
10609 "#
10610 .unindent(),
10611 );
10612
10613 // Toggle comments for mixture of empty and non-empty selections, where
10614 // multiple selections occupy a given line.
10615 cx.set_state(
10616 &r#"
10617 <p>A«</p>
10618 <p>ˇ»B</p>ˇ
10619 <p>C«</p>
10620 <p>ˇ»D</p>ˇ
10621 "#
10622 .unindent(),
10623 );
10624
10625 cx.update_editor(|editor, window, cx| {
10626 editor.toggle_comments(&ToggleComments::default(), window, cx)
10627 });
10628 cx.assert_editor_state(
10629 &r#"
10630 <!-- <p>A«</p>
10631 <p>ˇ»B</p>ˇ -->
10632 <!-- <p>C«</p>
10633 <p>ˇ»D</p>ˇ -->
10634 "#
10635 .unindent(),
10636 );
10637 cx.update_editor(|editor, window, cx| {
10638 editor.toggle_comments(&ToggleComments::default(), window, cx)
10639 });
10640 cx.assert_editor_state(
10641 &r#"
10642 <p>A«</p>
10643 <p>ˇ»B</p>ˇ
10644 <p>C«</p>
10645 <p>ˇ»D</p>ˇ
10646 "#
10647 .unindent(),
10648 );
10649
10650 // Toggle comments when different languages are active for different
10651 // selections.
10652 cx.set_state(
10653 &r#"
10654 ˇ<script>
10655 ˇvar x = new Y();
10656 ˇ</script>
10657 "#
10658 .unindent(),
10659 );
10660 cx.executor().run_until_parked();
10661 cx.update_editor(|editor, window, cx| {
10662 editor.toggle_comments(&ToggleComments::default(), window, cx)
10663 });
10664 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
10665 // Uncommenting and commenting from this position brings in even more wrong artifacts.
10666 cx.assert_editor_state(
10667 &r#"
10668 <!-- ˇ<script> -->
10669 // ˇvar x = new Y();
10670 <!-- ˇ</script> -->
10671 "#
10672 .unindent(),
10673 );
10674}
10675
10676#[gpui::test]
10677fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
10678 init_test(cx, |_| {});
10679
10680 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
10681 let multibuffer = cx.new(|cx| {
10682 let mut multibuffer = MultiBuffer::new(ReadWrite);
10683 multibuffer.push_excerpts(
10684 buffer.clone(),
10685 [
10686 ExcerptRange {
10687 context: Point::new(0, 0)..Point::new(0, 4),
10688 primary: None,
10689 },
10690 ExcerptRange {
10691 context: Point::new(1, 0)..Point::new(1, 4),
10692 primary: None,
10693 },
10694 ],
10695 cx,
10696 );
10697 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
10698 multibuffer
10699 });
10700
10701 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
10702 editor.update_in(cx, |editor, window, cx| {
10703 assert_eq!(editor.text(cx), "aaaa\nbbbb");
10704 editor.change_selections(None, window, cx, |s| {
10705 s.select_ranges([
10706 Point::new(0, 0)..Point::new(0, 0),
10707 Point::new(1, 0)..Point::new(1, 0),
10708 ])
10709 });
10710
10711 editor.handle_input("X", window, cx);
10712 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
10713 assert_eq!(
10714 editor.selections.ranges(cx),
10715 [
10716 Point::new(0, 1)..Point::new(0, 1),
10717 Point::new(1, 1)..Point::new(1, 1),
10718 ]
10719 );
10720
10721 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
10722 editor.change_selections(None, window, cx, |s| {
10723 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
10724 });
10725 editor.backspace(&Default::default(), window, cx);
10726 assert_eq!(editor.text(cx), "Xa\nbbb");
10727 assert_eq!(
10728 editor.selections.ranges(cx),
10729 [Point::new(1, 0)..Point::new(1, 0)]
10730 );
10731
10732 editor.change_selections(None, window, cx, |s| {
10733 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
10734 });
10735 editor.backspace(&Default::default(), window, cx);
10736 assert_eq!(editor.text(cx), "X\nbb");
10737 assert_eq!(
10738 editor.selections.ranges(cx),
10739 [Point::new(0, 1)..Point::new(0, 1)]
10740 );
10741 });
10742}
10743
10744#[gpui::test]
10745fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
10746 init_test(cx, |_| {});
10747
10748 let markers = vec![('[', ']').into(), ('(', ')').into()];
10749 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
10750 indoc! {"
10751 [aaaa
10752 (bbbb]
10753 cccc)",
10754 },
10755 markers.clone(),
10756 );
10757 let excerpt_ranges = markers.into_iter().map(|marker| {
10758 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
10759 ExcerptRange {
10760 context,
10761 primary: None,
10762 }
10763 });
10764 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
10765 let multibuffer = cx.new(|cx| {
10766 let mut multibuffer = MultiBuffer::new(ReadWrite);
10767 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
10768 multibuffer
10769 });
10770
10771 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
10772 editor.update_in(cx, |editor, window, cx| {
10773 let (expected_text, selection_ranges) = marked_text_ranges(
10774 indoc! {"
10775 aaaa
10776 bˇbbb
10777 bˇbbˇb
10778 cccc"
10779 },
10780 true,
10781 );
10782 assert_eq!(editor.text(cx), expected_text);
10783 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
10784
10785 editor.handle_input("X", window, cx);
10786
10787 let (expected_text, expected_selections) = marked_text_ranges(
10788 indoc! {"
10789 aaaa
10790 bXˇbbXb
10791 bXˇbbXˇb
10792 cccc"
10793 },
10794 false,
10795 );
10796 assert_eq!(editor.text(cx), expected_text);
10797 assert_eq!(editor.selections.ranges(cx), expected_selections);
10798
10799 editor.newline(&Newline, window, cx);
10800 let (expected_text, expected_selections) = marked_text_ranges(
10801 indoc! {"
10802 aaaa
10803 bX
10804 ˇbbX
10805 b
10806 bX
10807 ˇbbX
10808 ˇb
10809 cccc"
10810 },
10811 false,
10812 );
10813 assert_eq!(editor.text(cx), expected_text);
10814 assert_eq!(editor.selections.ranges(cx), expected_selections);
10815 });
10816}
10817
10818#[gpui::test]
10819fn test_refresh_selections(cx: &mut TestAppContext) {
10820 init_test(cx, |_| {});
10821
10822 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
10823 let mut excerpt1_id = None;
10824 let multibuffer = cx.new(|cx| {
10825 let mut multibuffer = MultiBuffer::new(ReadWrite);
10826 excerpt1_id = multibuffer
10827 .push_excerpts(
10828 buffer.clone(),
10829 [
10830 ExcerptRange {
10831 context: Point::new(0, 0)..Point::new(1, 4),
10832 primary: None,
10833 },
10834 ExcerptRange {
10835 context: Point::new(1, 0)..Point::new(2, 4),
10836 primary: None,
10837 },
10838 ],
10839 cx,
10840 )
10841 .into_iter()
10842 .next();
10843 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
10844 multibuffer
10845 });
10846
10847 let editor = cx.add_window(|window, cx| {
10848 let mut editor = build_editor(multibuffer.clone(), window, cx);
10849 let snapshot = editor.snapshot(window, cx);
10850 editor.change_selections(None, window, cx, |s| {
10851 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
10852 });
10853 editor.begin_selection(
10854 Point::new(2, 1).to_display_point(&snapshot),
10855 true,
10856 1,
10857 window,
10858 cx,
10859 );
10860 assert_eq!(
10861 editor.selections.ranges(cx),
10862 [
10863 Point::new(1, 3)..Point::new(1, 3),
10864 Point::new(2, 1)..Point::new(2, 1),
10865 ]
10866 );
10867 editor
10868 });
10869
10870 // Refreshing selections is a no-op when excerpts haven't changed.
10871 _ = editor.update(cx, |editor, window, cx| {
10872 editor.change_selections(None, window, cx, |s| s.refresh());
10873 assert_eq!(
10874 editor.selections.ranges(cx),
10875 [
10876 Point::new(1, 3)..Point::new(1, 3),
10877 Point::new(2, 1)..Point::new(2, 1),
10878 ]
10879 );
10880 });
10881
10882 multibuffer.update(cx, |multibuffer, cx| {
10883 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
10884 });
10885 _ = editor.update(cx, |editor, window, cx| {
10886 // Removing an excerpt causes the first selection to become degenerate.
10887 assert_eq!(
10888 editor.selections.ranges(cx),
10889 [
10890 Point::new(0, 0)..Point::new(0, 0),
10891 Point::new(0, 1)..Point::new(0, 1)
10892 ]
10893 );
10894
10895 // Refreshing selections will relocate the first selection to the original buffer
10896 // location.
10897 editor.change_selections(None, window, cx, |s| s.refresh());
10898 assert_eq!(
10899 editor.selections.ranges(cx),
10900 [
10901 Point::new(0, 1)..Point::new(0, 1),
10902 Point::new(0, 3)..Point::new(0, 3)
10903 ]
10904 );
10905 assert!(editor.selections.pending_anchor().is_some());
10906 });
10907}
10908
10909#[gpui::test]
10910fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
10911 init_test(cx, |_| {});
10912
10913 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
10914 let mut excerpt1_id = None;
10915 let multibuffer = cx.new(|cx| {
10916 let mut multibuffer = MultiBuffer::new(ReadWrite);
10917 excerpt1_id = multibuffer
10918 .push_excerpts(
10919 buffer.clone(),
10920 [
10921 ExcerptRange {
10922 context: Point::new(0, 0)..Point::new(1, 4),
10923 primary: None,
10924 },
10925 ExcerptRange {
10926 context: Point::new(1, 0)..Point::new(2, 4),
10927 primary: None,
10928 },
10929 ],
10930 cx,
10931 )
10932 .into_iter()
10933 .next();
10934 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
10935 multibuffer
10936 });
10937
10938 let editor = cx.add_window(|window, cx| {
10939 let mut editor = build_editor(multibuffer.clone(), window, cx);
10940 let snapshot = editor.snapshot(window, cx);
10941 editor.begin_selection(
10942 Point::new(1, 3).to_display_point(&snapshot),
10943 false,
10944 1,
10945 window,
10946 cx,
10947 );
10948 assert_eq!(
10949 editor.selections.ranges(cx),
10950 [Point::new(1, 3)..Point::new(1, 3)]
10951 );
10952 editor
10953 });
10954
10955 multibuffer.update(cx, |multibuffer, cx| {
10956 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
10957 });
10958 _ = editor.update(cx, |editor, window, cx| {
10959 assert_eq!(
10960 editor.selections.ranges(cx),
10961 [Point::new(0, 0)..Point::new(0, 0)]
10962 );
10963
10964 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
10965 editor.change_selections(None, window, cx, |s| s.refresh());
10966 assert_eq!(
10967 editor.selections.ranges(cx),
10968 [Point::new(0, 3)..Point::new(0, 3)]
10969 );
10970 assert!(editor.selections.pending_anchor().is_some());
10971 });
10972}
10973
10974#[gpui::test]
10975async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
10976 init_test(cx, |_| {});
10977
10978 let language = Arc::new(
10979 Language::new(
10980 LanguageConfig {
10981 brackets: BracketPairConfig {
10982 pairs: vec![
10983 BracketPair {
10984 start: "{".to_string(),
10985 end: "}".to_string(),
10986 close: true,
10987 surround: true,
10988 newline: true,
10989 },
10990 BracketPair {
10991 start: "/* ".to_string(),
10992 end: " */".to_string(),
10993 close: true,
10994 surround: true,
10995 newline: true,
10996 },
10997 ],
10998 ..Default::default()
10999 },
11000 ..Default::default()
11001 },
11002 Some(tree_sitter_rust::LANGUAGE.into()),
11003 )
11004 .with_indents_query("")
11005 .unwrap(),
11006 );
11007
11008 let text = concat!(
11009 "{ }\n", //
11010 " x\n", //
11011 " /* */\n", //
11012 "x\n", //
11013 "{{} }\n", //
11014 );
11015
11016 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
11017 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11018 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11019 editor
11020 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11021 .await;
11022
11023 editor.update_in(cx, |editor, window, cx| {
11024 editor.change_selections(None, window, cx, |s| {
11025 s.select_display_ranges([
11026 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
11027 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
11028 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
11029 ])
11030 });
11031 editor.newline(&Newline, window, cx);
11032
11033 assert_eq!(
11034 editor.buffer().read(cx).read(cx).text(),
11035 concat!(
11036 "{ \n", // Suppress rustfmt
11037 "\n", //
11038 "}\n", //
11039 " x\n", //
11040 " /* \n", //
11041 " \n", //
11042 " */\n", //
11043 "x\n", //
11044 "{{} \n", //
11045 "}\n", //
11046 )
11047 );
11048 });
11049}
11050
11051#[gpui::test]
11052fn test_highlighted_ranges(cx: &mut TestAppContext) {
11053 init_test(cx, |_| {});
11054
11055 let editor = cx.add_window(|window, cx| {
11056 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
11057 build_editor(buffer.clone(), window, cx)
11058 });
11059
11060 _ = editor.update(cx, |editor, window, cx| {
11061 struct Type1;
11062 struct Type2;
11063
11064 let buffer = editor.buffer.read(cx).snapshot(cx);
11065
11066 let anchor_range =
11067 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
11068
11069 editor.highlight_background::<Type1>(
11070 &[
11071 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
11072 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
11073 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
11074 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
11075 ],
11076 |_| Hsla::red(),
11077 cx,
11078 );
11079 editor.highlight_background::<Type2>(
11080 &[
11081 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
11082 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
11083 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
11084 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
11085 ],
11086 |_| Hsla::green(),
11087 cx,
11088 );
11089
11090 let snapshot = editor.snapshot(window, cx);
11091 let mut highlighted_ranges = editor.background_highlights_in_range(
11092 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
11093 &snapshot,
11094 cx.theme().colors(),
11095 );
11096 // Enforce a consistent ordering based on color without relying on the ordering of the
11097 // highlight's `TypeId` which is non-executor.
11098 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
11099 assert_eq!(
11100 highlighted_ranges,
11101 &[
11102 (
11103 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
11104 Hsla::red(),
11105 ),
11106 (
11107 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
11108 Hsla::red(),
11109 ),
11110 (
11111 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
11112 Hsla::green(),
11113 ),
11114 (
11115 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
11116 Hsla::green(),
11117 ),
11118 ]
11119 );
11120 assert_eq!(
11121 editor.background_highlights_in_range(
11122 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
11123 &snapshot,
11124 cx.theme().colors(),
11125 ),
11126 &[(
11127 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
11128 Hsla::red(),
11129 )]
11130 );
11131 });
11132}
11133
11134#[gpui::test]
11135async fn test_following(cx: &mut TestAppContext) {
11136 init_test(cx, |_| {});
11137
11138 let fs = FakeFs::new(cx.executor());
11139 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
11140
11141 let buffer = project.update(cx, |project, cx| {
11142 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
11143 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
11144 });
11145 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
11146 let follower = cx.update(|cx| {
11147 cx.open_window(
11148 WindowOptions {
11149 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
11150 gpui::Point::new(px(0.), px(0.)),
11151 gpui::Point::new(px(10.), px(80.)),
11152 ))),
11153 ..Default::default()
11154 },
11155 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
11156 )
11157 .unwrap()
11158 });
11159
11160 let is_still_following = Rc::new(RefCell::new(true));
11161 let follower_edit_event_count = Rc::new(RefCell::new(0));
11162 let pending_update = Rc::new(RefCell::new(None));
11163 let leader_entity = leader.root(cx).unwrap();
11164 let follower_entity = follower.root(cx).unwrap();
11165 _ = follower.update(cx, {
11166 let update = pending_update.clone();
11167 let is_still_following = is_still_following.clone();
11168 let follower_edit_event_count = follower_edit_event_count.clone();
11169 |_, window, cx| {
11170 cx.subscribe_in(
11171 &leader_entity,
11172 window,
11173 move |_, leader, event, window, cx| {
11174 leader.read(cx).add_event_to_update_proto(
11175 event,
11176 &mut update.borrow_mut(),
11177 window,
11178 cx,
11179 );
11180 },
11181 )
11182 .detach();
11183
11184 cx.subscribe_in(
11185 &follower_entity,
11186 window,
11187 move |_, _, event: &EditorEvent, _window, _cx| {
11188 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
11189 *is_still_following.borrow_mut() = false;
11190 }
11191
11192 if let EditorEvent::BufferEdited = event {
11193 *follower_edit_event_count.borrow_mut() += 1;
11194 }
11195 },
11196 )
11197 .detach();
11198 }
11199 });
11200
11201 // Update the selections only
11202 _ = leader.update(cx, |leader, window, cx| {
11203 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
11204 });
11205 follower
11206 .update(cx, |follower, window, cx| {
11207 follower.apply_update_proto(
11208 &project,
11209 pending_update.borrow_mut().take().unwrap(),
11210 window,
11211 cx,
11212 )
11213 })
11214 .unwrap()
11215 .await
11216 .unwrap();
11217 _ = follower.update(cx, |follower, _, cx| {
11218 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
11219 });
11220 assert!(*is_still_following.borrow());
11221 assert_eq!(*follower_edit_event_count.borrow(), 0);
11222
11223 // Update the scroll position only
11224 _ = leader.update(cx, |leader, window, cx| {
11225 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
11226 });
11227 follower
11228 .update(cx, |follower, window, cx| {
11229 follower.apply_update_proto(
11230 &project,
11231 pending_update.borrow_mut().take().unwrap(),
11232 window,
11233 cx,
11234 )
11235 })
11236 .unwrap()
11237 .await
11238 .unwrap();
11239 assert_eq!(
11240 follower
11241 .update(cx, |follower, _, cx| follower.scroll_position(cx))
11242 .unwrap(),
11243 gpui::Point::new(1.5, 3.5)
11244 );
11245 assert!(*is_still_following.borrow());
11246 assert_eq!(*follower_edit_event_count.borrow(), 0);
11247
11248 // Update the selections and scroll position. The follower's scroll position is updated
11249 // via autoscroll, not via the leader's exact scroll position.
11250 _ = leader.update(cx, |leader, window, cx| {
11251 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
11252 leader.request_autoscroll(Autoscroll::newest(), cx);
11253 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
11254 });
11255 follower
11256 .update(cx, |follower, window, cx| {
11257 follower.apply_update_proto(
11258 &project,
11259 pending_update.borrow_mut().take().unwrap(),
11260 window,
11261 cx,
11262 )
11263 })
11264 .unwrap()
11265 .await
11266 .unwrap();
11267 _ = follower.update(cx, |follower, _, cx| {
11268 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
11269 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
11270 });
11271 assert!(*is_still_following.borrow());
11272
11273 // Creating a pending selection that precedes another selection
11274 _ = leader.update(cx, |leader, window, cx| {
11275 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
11276 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
11277 });
11278 follower
11279 .update(cx, |follower, window, cx| {
11280 follower.apply_update_proto(
11281 &project,
11282 pending_update.borrow_mut().take().unwrap(),
11283 window,
11284 cx,
11285 )
11286 })
11287 .unwrap()
11288 .await
11289 .unwrap();
11290 _ = follower.update(cx, |follower, _, cx| {
11291 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
11292 });
11293 assert!(*is_still_following.borrow());
11294
11295 // Extend the pending selection so that it surrounds another selection
11296 _ = leader.update(cx, |leader, window, cx| {
11297 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
11298 });
11299 follower
11300 .update(cx, |follower, window, cx| {
11301 follower.apply_update_proto(
11302 &project,
11303 pending_update.borrow_mut().take().unwrap(),
11304 window,
11305 cx,
11306 )
11307 })
11308 .unwrap()
11309 .await
11310 .unwrap();
11311 _ = follower.update(cx, |follower, _, cx| {
11312 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
11313 });
11314
11315 // Scrolling locally breaks the follow
11316 _ = follower.update(cx, |follower, window, cx| {
11317 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
11318 follower.set_scroll_anchor(
11319 ScrollAnchor {
11320 anchor: top_anchor,
11321 offset: gpui::Point::new(0.0, 0.5),
11322 },
11323 window,
11324 cx,
11325 );
11326 });
11327 assert!(!(*is_still_following.borrow()));
11328}
11329
11330#[gpui::test]
11331async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
11332 init_test(cx, |_| {});
11333
11334 let fs = FakeFs::new(cx.executor());
11335 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
11336 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11337 let pane = workspace
11338 .update(cx, |workspace, _, _| workspace.active_pane().clone())
11339 .unwrap();
11340
11341 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11342
11343 let leader = pane.update_in(cx, |_, window, cx| {
11344 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
11345 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
11346 });
11347
11348 // Start following the editor when it has no excerpts.
11349 let mut state_message =
11350 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
11351 let workspace_entity = workspace.root(cx).unwrap();
11352 let follower_1 = cx
11353 .update_window(*workspace.deref(), |_, window, cx| {
11354 Editor::from_state_proto(
11355 workspace_entity,
11356 ViewId {
11357 creator: Default::default(),
11358 id: 0,
11359 },
11360 &mut state_message,
11361 window,
11362 cx,
11363 )
11364 })
11365 .unwrap()
11366 .unwrap()
11367 .await
11368 .unwrap();
11369
11370 let update_message = Rc::new(RefCell::new(None));
11371 follower_1.update_in(cx, {
11372 let update = update_message.clone();
11373 |_, window, cx| {
11374 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
11375 leader.read(cx).add_event_to_update_proto(
11376 event,
11377 &mut update.borrow_mut(),
11378 window,
11379 cx,
11380 );
11381 })
11382 .detach();
11383 }
11384 });
11385
11386 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
11387 (
11388 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
11389 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
11390 )
11391 });
11392
11393 // Insert some excerpts.
11394 leader.update(cx, |leader, cx| {
11395 leader.buffer.update(cx, |multibuffer, cx| {
11396 let excerpt_ids = multibuffer.push_excerpts(
11397 buffer_1.clone(),
11398 [
11399 ExcerptRange {
11400 context: 1..6,
11401 primary: None,
11402 },
11403 ExcerptRange {
11404 context: 12..15,
11405 primary: None,
11406 },
11407 ExcerptRange {
11408 context: 0..3,
11409 primary: None,
11410 },
11411 ],
11412 cx,
11413 );
11414 multibuffer.insert_excerpts_after(
11415 excerpt_ids[0],
11416 buffer_2.clone(),
11417 [
11418 ExcerptRange {
11419 context: 8..12,
11420 primary: None,
11421 },
11422 ExcerptRange {
11423 context: 0..6,
11424 primary: None,
11425 },
11426 ],
11427 cx,
11428 );
11429 });
11430 });
11431
11432 // Apply the update of adding the excerpts.
11433 follower_1
11434 .update_in(cx, |follower, window, cx| {
11435 follower.apply_update_proto(
11436 &project,
11437 update_message.borrow().clone().unwrap(),
11438 window,
11439 cx,
11440 )
11441 })
11442 .await
11443 .unwrap();
11444 assert_eq!(
11445 follower_1.update(cx, |editor, cx| editor.text(cx)),
11446 leader.update(cx, |editor, cx| editor.text(cx))
11447 );
11448 update_message.borrow_mut().take();
11449
11450 // Start following separately after it already has excerpts.
11451 let mut state_message =
11452 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
11453 let workspace_entity = workspace.root(cx).unwrap();
11454 let follower_2 = cx
11455 .update_window(*workspace.deref(), |_, window, cx| {
11456 Editor::from_state_proto(
11457 workspace_entity,
11458 ViewId {
11459 creator: Default::default(),
11460 id: 0,
11461 },
11462 &mut state_message,
11463 window,
11464 cx,
11465 )
11466 })
11467 .unwrap()
11468 .unwrap()
11469 .await
11470 .unwrap();
11471 assert_eq!(
11472 follower_2.update(cx, |editor, cx| editor.text(cx)),
11473 leader.update(cx, |editor, cx| editor.text(cx))
11474 );
11475
11476 // Remove some excerpts.
11477 leader.update(cx, |leader, cx| {
11478 leader.buffer.update(cx, |multibuffer, cx| {
11479 let excerpt_ids = multibuffer.excerpt_ids();
11480 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
11481 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
11482 });
11483 });
11484
11485 // Apply the update of removing the excerpts.
11486 follower_1
11487 .update_in(cx, |follower, window, cx| {
11488 follower.apply_update_proto(
11489 &project,
11490 update_message.borrow().clone().unwrap(),
11491 window,
11492 cx,
11493 )
11494 })
11495 .await
11496 .unwrap();
11497 follower_2
11498 .update_in(cx, |follower, window, cx| {
11499 follower.apply_update_proto(
11500 &project,
11501 update_message.borrow().clone().unwrap(),
11502 window,
11503 cx,
11504 )
11505 })
11506 .await
11507 .unwrap();
11508 update_message.borrow_mut().take();
11509 assert_eq!(
11510 follower_1.update(cx, |editor, cx| editor.text(cx)),
11511 leader.update(cx, |editor, cx| editor.text(cx))
11512 );
11513}
11514
11515#[gpui::test]
11516async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
11517 init_test(cx, |_| {});
11518
11519 let mut cx = EditorTestContext::new(cx).await;
11520 let lsp_store =
11521 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
11522
11523 cx.set_state(indoc! {"
11524 ˇfn func(abc def: i32) -> u32 {
11525 }
11526 "});
11527
11528 cx.update(|_, cx| {
11529 lsp_store.update(cx, |lsp_store, cx| {
11530 lsp_store
11531 .update_diagnostics(
11532 LanguageServerId(0),
11533 lsp::PublishDiagnosticsParams {
11534 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11535 version: None,
11536 diagnostics: vec![
11537 lsp::Diagnostic {
11538 range: lsp::Range::new(
11539 lsp::Position::new(0, 11),
11540 lsp::Position::new(0, 12),
11541 ),
11542 severity: Some(lsp::DiagnosticSeverity::ERROR),
11543 ..Default::default()
11544 },
11545 lsp::Diagnostic {
11546 range: lsp::Range::new(
11547 lsp::Position::new(0, 12),
11548 lsp::Position::new(0, 15),
11549 ),
11550 severity: Some(lsp::DiagnosticSeverity::ERROR),
11551 ..Default::default()
11552 },
11553 lsp::Diagnostic {
11554 range: lsp::Range::new(
11555 lsp::Position::new(0, 25),
11556 lsp::Position::new(0, 28),
11557 ),
11558 severity: Some(lsp::DiagnosticSeverity::ERROR),
11559 ..Default::default()
11560 },
11561 ],
11562 },
11563 &[],
11564 cx,
11565 )
11566 .unwrap()
11567 });
11568 });
11569
11570 executor.run_until_parked();
11571
11572 cx.update_editor(|editor, window, cx| {
11573 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11574 });
11575
11576 cx.assert_editor_state(indoc! {"
11577 fn func(abc def: i32) -> ˇu32 {
11578 }
11579 "});
11580
11581 cx.update_editor(|editor, window, cx| {
11582 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11583 });
11584
11585 cx.assert_editor_state(indoc! {"
11586 fn func(abc ˇdef: i32) -> u32 {
11587 }
11588 "});
11589
11590 cx.update_editor(|editor, window, cx| {
11591 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11592 });
11593
11594 cx.assert_editor_state(indoc! {"
11595 fn func(abcˇ def: i32) -> u32 {
11596 }
11597 "});
11598
11599 cx.update_editor(|editor, window, cx| {
11600 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11601 });
11602
11603 cx.assert_editor_state(indoc! {"
11604 fn func(abc def: i32) -> ˇu32 {
11605 }
11606 "});
11607}
11608
11609#[gpui::test]
11610async fn cycle_through_same_place_diagnostics(
11611 executor: BackgroundExecutor,
11612 cx: &mut TestAppContext,
11613) {
11614 init_test(cx, |_| {});
11615
11616 let mut cx = EditorTestContext::new(cx).await;
11617 let lsp_store =
11618 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
11619
11620 cx.set_state(indoc! {"
11621 ˇfn func(abc def: i32) -> u32 {
11622 }
11623 "});
11624
11625 cx.update(|_, cx| {
11626 lsp_store.update(cx, |lsp_store, cx| {
11627 lsp_store
11628 .update_diagnostics(
11629 LanguageServerId(0),
11630 lsp::PublishDiagnosticsParams {
11631 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11632 version: None,
11633 diagnostics: vec![
11634 lsp::Diagnostic {
11635 range: lsp::Range::new(
11636 lsp::Position::new(0, 11),
11637 lsp::Position::new(0, 12),
11638 ),
11639 severity: Some(lsp::DiagnosticSeverity::ERROR),
11640 ..Default::default()
11641 },
11642 lsp::Diagnostic {
11643 range: lsp::Range::new(
11644 lsp::Position::new(0, 12),
11645 lsp::Position::new(0, 15),
11646 ),
11647 severity: Some(lsp::DiagnosticSeverity::ERROR),
11648 ..Default::default()
11649 },
11650 lsp::Diagnostic {
11651 range: lsp::Range::new(
11652 lsp::Position::new(0, 12),
11653 lsp::Position::new(0, 15),
11654 ),
11655 severity: Some(lsp::DiagnosticSeverity::ERROR),
11656 ..Default::default()
11657 },
11658 lsp::Diagnostic {
11659 range: lsp::Range::new(
11660 lsp::Position::new(0, 25),
11661 lsp::Position::new(0, 28),
11662 ),
11663 severity: Some(lsp::DiagnosticSeverity::ERROR),
11664 ..Default::default()
11665 },
11666 ],
11667 },
11668 &[],
11669 cx,
11670 )
11671 .unwrap()
11672 });
11673 });
11674 executor.run_until_parked();
11675
11676 //// Backward
11677
11678 // Fourth diagnostic
11679 cx.update_editor(|editor, window, cx| {
11680 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11681 });
11682 cx.assert_editor_state(indoc! {"
11683 fn func(abc def: i32) -> ˇu32 {
11684 }
11685 "});
11686
11687 // Third diagnostic
11688 cx.update_editor(|editor, window, cx| {
11689 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11690 });
11691 cx.assert_editor_state(indoc! {"
11692 fn func(abc ˇdef: i32) -> u32 {
11693 }
11694 "});
11695
11696 // Second diagnostic, same place
11697 cx.update_editor(|editor, window, cx| {
11698 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11699 });
11700 cx.assert_editor_state(indoc! {"
11701 fn func(abc ˇdef: i32) -> u32 {
11702 }
11703 "});
11704
11705 // First diagnostic
11706 cx.update_editor(|editor, window, cx| {
11707 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11708 });
11709 cx.assert_editor_state(indoc! {"
11710 fn func(abcˇ def: i32) -> u32 {
11711 }
11712 "});
11713
11714 // Wrapped over, fourth diagnostic
11715 cx.update_editor(|editor, window, cx| {
11716 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11717 });
11718 cx.assert_editor_state(indoc! {"
11719 fn func(abc def: i32) -> ˇu32 {
11720 }
11721 "});
11722
11723 cx.update_editor(|editor, window, cx| {
11724 editor.move_to_beginning(&MoveToBeginning, window, cx);
11725 });
11726 cx.assert_editor_state(indoc! {"
11727 ˇfn func(abc def: i32) -> u32 {
11728 }
11729 "});
11730
11731 //// Forward
11732
11733 // First diagnostic
11734 cx.update_editor(|editor, window, cx| {
11735 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11736 });
11737 cx.assert_editor_state(indoc! {"
11738 fn func(abcˇ def: i32) -> u32 {
11739 }
11740 "});
11741
11742 // Second diagnostic
11743 cx.update_editor(|editor, window, cx| {
11744 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11745 });
11746 cx.assert_editor_state(indoc! {"
11747 fn func(abc ˇdef: i32) -> u32 {
11748 }
11749 "});
11750
11751 // Third diagnostic, same place
11752 cx.update_editor(|editor, window, cx| {
11753 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11754 });
11755 cx.assert_editor_state(indoc! {"
11756 fn func(abc ˇdef: i32) -> u32 {
11757 }
11758 "});
11759
11760 // Fourth diagnostic
11761 cx.update_editor(|editor, window, cx| {
11762 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11763 });
11764 cx.assert_editor_state(indoc! {"
11765 fn func(abc def: i32) -> ˇu32 {
11766 }
11767 "});
11768
11769 // Wrapped around, first diagnostic
11770 cx.update_editor(|editor, window, cx| {
11771 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11772 });
11773 cx.assert_editor_state(indoc! {"
11774 fn func(abcˇ def: i32) -> u32 {
11775 }
11776 "});
11777}
11778
11779#[gpui::test]
11780async fn active_diagnostics_dismiss_after_invalidation(
11781 executor: BackgroundExecutor,
11782 cx: &mut TestAppContext,
11783) {
11784 init_test(cx, |_| {});
11785
11786 let mut cx = EditorTestContext::new(cx).await;
11787 let lsp_store =
11788 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
11789
11790 cx.set_state(indoc! {"
11791 ˇfn func(abc def: i32) -> u32 {
11792 }
11793 "});
11794
11795 let message = "Something's wrong!";
11796 cx.update(|_, cx| {
11797 lsp_store.update(cx, |lsp_store, cx| {
11798 lsp_store
11799 .update_diagnostics(
11800 LanguageServerId(0),
11801 lsp::PublishDiagnosticsParams {
11802 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11803 version: None,
11804 diagnostics: vec![lsp::Diagnostic {
11805 range: lsp::Range::new(
11806 lsp::Position::new(0, 11),
11807 lsp::Position::new(0, 12),
11808 ),
11809 severity: Some(lsp::DiagnosticSeverity::ERROR),
11810 message: message.to_string(),
11811 ..Default::default()
11812 }],
11813 },
11814 &[],
11815 cx,
11816 )
11817 .unwrap()
11818 });
11819 });
11820 executor.run_until_parked();
11821
11822 cx.update_editor(|editor, window, cx| {
11823 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11824 assert_eq!(
11825 editor
11826 .active_diagnostics
11827 .as_ref()
11828 .map(|diagnostics_group| diagnostics_group.primary_message.as_str()),
11829 Some(message),
11830 "Should have a diagnostics group activated"
11831 );
11832 });
11833 cx.assert_editor_state(indoc! {"
11834 fn func(abcˇ def: i32) -> u32 {
11835 }
11836 "});
11837
11838 cx.update(|_, cx| {
11839 lsp_store.update(cx, |lsp_store, cx| {
11840 lsp_store
11841 .update_diagnostics(
11842 LanguageServerId(0),
11843 lsp::PublishDiagnosticsParams {
11844 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11845 version: None,
11846 diagnostics: Vec::new(),
11847 },
11848 &[],
11849 cx,
11850 )
11851 .unwrap()
11852 });
11853 });
11854 executor.run_until_parked();
11855 cx.update_editor(|editor, _, _| {
11856 assert_eq!(
11857 editor.active_diagnostics, None,
11858 "After no diagnostics set to the editor, no diagnostics should be active"
11859 );
11860 });
11861 cx.assert_editor_state(indoc! {"
11862 fn func(abcˇ def: i32) -> u32 {
11863 }
11864 "});
11865
11866 cx.update_editor(|editor, window, cx| {
11867 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11868 assert_eq!(
11869 editor.active_diagnostics, None,
11870 "Should be no diagnostics to go to and activate"
11871 );
11872 });
11873 cx.assert_editor_state(indoc! {"
11874 fn func(abcˇ def: i32) -> u32 {
11875 }
11876 "});
11877}
11878
11879#[gpui::test]
11880async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
11881 init_test(cx, |_| {});
11882
11883 let mut cx = EditorTestContext::new(cx).await;
11884
11885 cx.set_state(indoc! {"
11886 fn func(abˇc def: i32) -> u32 {
11887 }
11888 "});
11889 let lsp_store =
11890 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
11891
11892 cx.update(|_, cx| {
11893 lsp_store.update(cx, |lsp_store, cx| {
11894 lsp_store.update_diagnostics(
11895 LanguageServerId(0),
11896 lsp::PublishDiagnosticsParams {
11897 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11898 version: None,
11899 diagnostics: vec![lsp::Diagnostic {
11900 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 12)),
11901 severity: Some(lsp::DiagnosticSeverity::ERROR),
11902 message: "we've had problems with <https://link.one>, and <https://link.two> is broken".to_string(),
11903 ..Default::default()
11904 }],
11905 },
11906 &[],
11907 cx,
11908 )
11909 })
11910 }).unwrap();
11911 cx.run_until_parked();
11912 cx.update_editor(|editor, window, cx| {
11913 hover_popover::hover(editor, &Default::default(), window, cx)
11914 });
11915 cx.run_until_parked();
11916 cx.update_editor(|editor, _, _| assert!(editor.hover_state.diagnostic_popover.is_some()))
11917}
11918
11919#[gpui::test]
11920async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
11921 init_test(cx, |_| {});
11922
11923 let mut cx = EditorTestContext::new(cx).await;
11924
11925 let diff_base = r#"
11926 use some::mod;
11927
11928 const A: u32 = 42;
11929
11930 fn main() {
11931 println!("hello");
11932
11933 println!("world");
11934 }
11935 "#
11936 .unindent();
11937
11938 // Edits are modified, removed, modified, added
11939 cx.set_state(
11940 &r#"
11941 use some::modified;
11942
11943 ˇ
11944 fn main() {
11945 println!("hello there");
11946
11947 println!("around the");
11948 println!("world");
11949 }
11950 "#
11951 .unindent(),
11952 );
11953
11954 cx.set_head_text(&diff_base);
11955 executor.run_until_parked();
11956
11957 cx.update_editor(|editor, window, cx| {
11958 //Wrap around the bottom of the buffer
11959 for _ in 0..3 {
11960 editor.go_to_next_hunk(&GoToHunk, window, cx);
11961 }
11962 });
11963
11964 cx.assert_editor_state(
11965 &r#"
11966 ˇuse some::modified;
11967
11968
11969 fn main() {
11970 println!("hello there");
11971
11972 println!("around the");
11973 println!("world");
11974 }
11975 "#
11976 .unindent(),
11977 );
11978
11979 cx.update_editor(|editor, window, cx| {
11980 //Wrap around the top of the buffer
11981 for _ in 0..2 {
11982 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
11983 }
11984 });
11985
11986 cx.assert_editor_state(
11987 &r#"
11988 use some::modified;
11989
11990
11991 fn main() {
11992 ˇ println!("hello there");
11993
11994 println!("around the");
11995 println!("world");
11996 }
11997 "#
11998 .unindent(),
11999 );
12000
12001 cx.update_editor(|editor, window, cx| {
12002 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12003 });
12004
12005 cx.assert_editor_state(
12006 &r#"
12007 use some::modified;
12008
12009 ˇ
12010 fn main() {
12011 println!("hello there");
12012
12013 println!("around the");
12014 println!("world");
12015 }
12016 "#
12017 .unindent(),
12018 );
12019
12020 cx.update_editor(|editor, window, cx| {
12021 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12022 });
12023
12024 cx.assert_editor_state(
12025 &r#"
12026 ˇuse some::modified;
12027
12028
12029 fn main() {
12030 println!("hello there");
12031
12032 println!("around the");
12033 println!("world");
12034 }
12035 "#
12036 .unindent(),
12037 );
12038
12039 cx.update_editor(|editor, window, cx| {
12040 for _ in 0..2 {
12041 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12042 }
12043 });
12044
12045 cx.assert_editor_state(
12046 &r#"
12047 use some::modified;
12048
12049
12050 fn main() {
12051 ˇ println!("hello there");
12052
12053 println!("around the");
12054 println!("world");
12055 }
12056 "#
12057 .unindent(),
12058 );
12059
12060 cx.update_editor(|editor, window, cx| {
12061 editor.fold(&Fold, window, cx);
12062 });
12063
12064 cx.update_editor(|editor, window, cx| {
12065 editor.go_to_next_hunk(&GoToHunk, window, cx);
12066 });
12067
12068 cx.assert_editor_state(
12069 &r#"
12070 ˇuse some::modified;
12071
12072
12073 fn main() {
12074 println!("hello there");
12075
12076 println!("around the");
12077 println!("world");
12078 }
12079 "#
12080 .unindent(),
12081 );
12082}
12083
12084#[test]
12085fn test_split_words() {
12086 fn split(text: &str) -> Vec<&str> {
12087 split_words(text).collect()
12088 }
12089
12090 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
12091 assert_eq!(split("hello_world"), &["hello_", "world"]);
12092 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
12093 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
12094 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
12095 assert_eq!(split("helloworld"), &["helloworld"]);
12096
12097 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
12098}
12099
12100#[gpui::test]
12101async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
12102 init_test(cx, |_| {});
12103
12104 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
12105 let mut assert = |before, after| {
12106 let _state_context = cx.set_state(before);
12107 cx.run_until_parked();
12108 cx.update_editor(|editor, window, cx| {
12109 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
12110 });
12111 cx.run_until_parked();
12112 cx.assert_editor_state(after);
12113 };
12114
12115 // Outside bracket jumps to outside of matching bracket
12116 assert("console.logˇ(var);", "console.log(var)ˇ;");
12117 assert("console.log(var)ˇ;", "console.logˇ(var);");
12118
12119 // Inside bracket jumps to inside of matching bracket
12120 assert("console.log(ˇvar);", "console.log(varˇ);");
12121 assert("console.log(varˇ);", "console.log(ˇvar);");
12122
12123 // When outside a bracket and inside, favor jumping to the inside bracket
12124 assert(
12125 "console.log('foo', [1, 2, 3]ˇ);",
12126 "console.log(ˇ'foo', [1, 2, 3]);",
12127 );
12128 assert(
12129 "console.log(ˇ'foo', [1, 2, 3]);",
12130 "console.log('foo', [1, 2, 3]ˇ);",
12131 );
12132
12133 // Bias forward if two options are equally likely
12134 assert(
12135 "let result = curried_fun()ˇ();",
12136 "let result = curried_fun()()ˇ;",
12137 );
12138
12139 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
12140 assert(
12141 indoc! {"
12142 function test() {
12143 console.log('test')ˇ
12144 }"},
12145 indoc! {"
12146 function test() {
12147 console.logˇ('test')
12148 }"},
12149 );
12150}
12151
12152#[gpui::test]
12153async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
12154 init_test(cx, |_| {});
12155
12156 let fs = FakeFs::new(cx.executor());
12157 fs.insert_tree(
12158 path!("/a"),
12159 json!({
12160 "main.rs": "fn main() { let a = 5; }",
12161 "other.rs": "// Test file",
12162 }),
12163 )
12164 .await;
12165 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12166
12167 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12168 language_registry.add(Arc::new(Language::new(
12169 LanguageConfig {
12170 name: "Rust".into(),
12171 matcher: LanguageMatcher {
12172 path_suffixes: vec!["rs".to_string()],
12173 ..Default::default()
12174 },
12175 brackets: BracketPairConfig {
12176 pairs: vec![BracketPair {
12177 start: "{".to_string(),
12178 end: "}".to_string(),
12179 close: true,
12180 surround: true,
12181 newline: true,
12182 }],
12183 disabled_scopes_by_bracket_ix: Vec::new(),
12184 },
12185 ..Default::default()
12186 },
12187 Some(tree_sitter_rust::LANGUAGE.into()),
12188 )));
12189 let mut fake_servers = language_registry.register_fake_lsp(
12190 "Rust",
12191 FakeLspAdapter {
12192 capabilities: lsp::ServerCapabilities {
12193 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
12194 first_trigger_character: "{".to_string(),
12195 more_trigger_character: None,
12196 }),
12197 ..Default::default()
12198 },
12199 ..Default::default()
12200 },
12201 );
12202
12203 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12204
12205 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12206
12207 let worktree_id = workspace
12208 .update(cx, |workspace, _, cx| {
12209 workspace.project().update(cx, |project, cx| {
12210 project.worktrees(cx).next().unwrap().read(cx).id()
12211 })
12212 })
12213 .unwrap();
12214
12215 let buffer = project
12216 .update(cx, |project, cx| {
12217 project.open_local_buffer(path!("/a/main.rs"), cx)
12218 })
12219 .await
12220 .unwrap();
12221 let editor_handle = workspace
12222 .update(cx, |workspace, window, cx| {
12223 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
12224 })
12225 .unwrap()
12226 .await
12227 .unwrap()
12228 .downcast::<Editor>()
12229 .unwrap();
12230
12231 cx.executor().start_waiting();
12232 let fake_server = fake_servers.next().await.unwrap();
12233
12234 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
12235 |params, _| async move {
12236 assert_eq!(
12237 params.text_document_position.text_document.uri,
12238 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
12239 );
12240 assert_eq!(
12241 params.text_document_position.position,
12242 lsp::Position::new(0, 21),
12243 );
12244
12245 Ok(Some(vec![lsp::TextEdit {
12246 new_text: "]".to_string(),
12247 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12248 }]))
12249 },
12250 );
12251
12252 editor_handle.update_in(cx, |editor, window, cx| {
12253 window.focus(&editor.focus_handle(cx));
12254 editor.change_selections(None, window, cx, |s| {
12255 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
12256 });
12257 editor.handle_input("{", window, cx);
12258 });
12259
12260 cx.executor().run_until_parked();
12261
12262 buffer.update(cx, |buffer, _| {
12263 assert_eq!(
12264 buffer.text(),
12265 "fn main() { let a = {5}; }",
12266 "No extra braces from on type formatting should appear in the buffer"
12267 )
12268 });
12269}
12270
12271#[gpui::test]
12272async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
12273 init_test(cx, |_| {});
12274
12275 let fs = FakeFs::new(cx.executor());
12276 fs.insert_tree(
12277 path!("/a"),
12278 json!({
12279 "main.rs": "fn main() { let a = 5; }",
12280 "other.rs": "// Test file",
12281 }),
12282 )
12283 .await;
12284
12285 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12286
12287 let server_restarts = Arc::new(AtomicUsize::new(0));
12288 let closure_restarts = Arc::clone(&server_restarts);
12289 let language_server_name = "test language server";
12290 let language_name: LanguageName = "Rust".into();
12291
12292 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12293 language_registry.add(Arc::new(Language::new(
12294 LanguageConfig {
12295 name: language_name.clone(),
12296 matcher: LanguageMatcher {
12297 path_suffixes: vec!["rs".to_string()],
12298 ..Default::default()
12299 },
12300 ..Default::default()
12301 },
12302 Some(tree_sitter_rust::LANGUAGE.into()),
12303 )));
12304 let mut fake_servers = language_registry.register_fake_lsp(
12305 "Rust",
12306 FakeLspAdapter {
12307 name: language_server_name,
12308 initialization_options: Some(json!({
12309 "testOptionValue": true
12310 })),
12311 initializer: Some(Box::new(move |fake_server| {
12312 let task_restarts = Arc::clone(&closure_restarts);
12313 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
12314 task_restarts.fetch_add(1, atomic::Ordering::Release);
12315 futures::future::ready(Ok(()))
12316 });
12317 })),
12318 ..Default::default()
12319 },
12320 );
12321
12322 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12323 let _buffer = project
12324 .update(cx, |project, cx| {
12325 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
12326 })
12327 .await
12328 .unwrap();
12329 let _fake_server = fake_servers.next().await.unwrap();
12330 update_test_language_settings(cx, |language_settings| {
12331 language_settings.languages.insert(
12332 language_name.clone(),
12333 LanguageSettingsContent {
12334 tab_size: NonZeroU32::new(8),
12335 ..Default::default()
12336 },
12337 );
12338 });
12339 cx.executor().run_until_parked();
12340 assert_eq!(
12341 server_restarts.load(atomic::Ordering::Acquire),
12342 0,
12343 "Should not restart LSP server on an unrelated change"
12344 );
12345
12346 update_test_project_settings(cx, |project_settings| {
12347 project_settings.lsp.insert(
12348 "Some other server name".into(),
12349 LspSettings {
12350 binary: None,
12351 settings: None,
12352 initialization_options: Some(json!({
12353 "some other init value": false
12354 })),
12355 },
12356 );
12357 });
12358 cx.executor().run_until_parked();
12359 assert_eq!(
12360 server_restarts.load(atomic::Ordering::Acquire),
12361 0,
12362 "Should not restart LSP server on an unrelated LSP settings change"
12363 );
12364
12365 update_test_project_settings(cx, |project_settings| {
12366 project_settings.lsp.insert(
12367 language_server_name.into(),
12368 LspSettings {
12369 binary: None,
12370 settings: None,
12371 initialization_options: Some(json!({
12372 "anotherInitValue": false
12373 })),
12374 },
12375 );
12376 });
12377 cx.executor().run_until_parked();
12378 assert_eq!(
12379 server_restarts.load(atomic::Ordering::Acquire),
12380 1,
12381 "Should restart LSP server on a related LSP settings change"
12382 );
12383
12384 update_test_project_settings(cx, |project_settings| {
12385 project_settings.lsp.insert(
12386 language_server_name.into(),
12387 LspSettings {
12388 binary: None,
12389 settings: None,
12390 initialization_options: Some(json!({
12391 "anotherInitValue": false
12392 })),
12393 },
12394 );
12395 });
12396 cx.executor().run_until_parked();
12397 assert_eq!(
12398 server_restarts.load(atomic::Ordering::Acquire),
12399 1,
12400 "Should not restart LSP server on a related LSP settings change that is the same"
12401 );
12402
12403 update_test_project_settings(cx, |project_settings| {
12404 project_settings.lsp.insert(
12405 language_server_name.into(),
12406 LspSettings {
12407 binary: None,
12408 settings: None,
12409 initialization_options: None,
12410 },
12411 );
12412 });
12413 cx.executor().run_until_parked();
12414 assert_eq!(
12415 server_restarts.load(atomic::Ordering::Acquire),
12416 2,
12417 "Should restart LSP server on another related LSP settings change"
12418 );
12419}
12420
12421#[gpui::test]
12422async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
12423 init_test(cx, |_| {});
12424
12425 let mut cx = EditorLspTestContext::new_rust(
12426 lsp::ServerCapabilities {
12427 completion_provider: Some(lsp::CompletionOptions {
12428 trigger_characters: Some(vec![".".to_string()]),
12429 resolve_provider: Some(true),
12430 ..Default::default()
12431 }),
12432 ..Default::default()
12433 },
12434 cx,
12435 )
12436 .await;
12437
12438 cx.set_state("fn main() { let a = 2ˇ; }");
12439 cx.simulate_keystroke(".");
12440 let completion_item = lsp::CompletionItem {
12441 label: "some".into(),
12442 kind: Some(lsp::CompletionItemKind::SNIPPET),
12443 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
12444 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
12445 kind: lsp::MarkupKind::Markdown,
12446 value: "```rust\nSome(2)\n```".to_string(),
12447 })),
12448 deprecated: Some(false),
12449 sort_text: Some("fffffff2".to_string()),
12450 filter_text: Some("some".to_string()),
12451 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
12452 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12453 range: lsp::Range {
12454 start: lsp::Position {
12455 line: 0,
12456 character: 22,
12457 },
12458 end: lsp::Position {
12459 line: 0,
12460 character: 22,
12461 },
12462 },
12463 new_text: "Some(2)".to_string(),
12464 })),
12465 additional_text_edits: Some(vec![lsp::TextEdit {
12466 range: lsp::Range {
12467 start: lsp::Position {
12468 line: 0,
12469 character: 20,
12470 },
12471 end: lsp::Position {
12472 line: 0,
12473 character: 22,
12474 },
12475 },
12476 new_text: "".to_string(),
12477 }]),
12478 ..Default::default()
12479 };
12480
12481 let closure_completion_item = completion_item.clone();
12482 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
12483 let task_completion_item = closure_completion_item.clone();
12484 async move {
12485 Ok(Some(lsp::CompletionResponse::Array(vec![
12486 task_completion_item,
12487 ])))
12488 }
12489 });
12490
12491 request.next().await;
12492
12493 cx.condition(|editor, _| editor.context_menu_visible())
12494 .await;
12495 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12496 editor
12497 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12498 .unwrap()
12499 });
12500 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
12501
12502 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
12503 let task_completion_item = completion_item.clone();
12504 async move { Ok(task_completion_item) }
12505 })
12506 .next()
12507 .await
12508 .unwrap();
12509 apply_additional_edits.await.unwrap();
12510 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
12511}
12512
12513#[gpui::test]
12514async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
12515 init_test(cx, |_| {});
12516
12517 let mut cx = EditorLspTestContext::new_rust(
12518 lsp::ServerCapabilities {
12519 completion_provider: Some(lsp::CompletionOptions {
12520 trigger_characters: Some(vec![".".to_string()]),
12521 resolve_provider: Some(true),
12522 ..Default::default()
12523 }),
12524 ..Default::default()
12525 },
12526 cx,
12527 )
12528 .await;
12529
12530 cx.set_state("fn main() { let a = 2ˇ; }");
12531 cx.simulate_keystroke(".");
12532
12533 let item1 = lsp::CompletionItem {
12534 label: "method id()".to_string(),
12535 filter_text: Some("id".to_string()),
12536 detail: None,
12537 documentation: None,
12538 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12539 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12540 new_text: ".id".to_string(),
12541 })),
12542 ..lsp::CompletionItem::default()
12543 };
12544
12545 let item2 = lsp::CompletionItem {
12546 label: "other".to_string(),
12547 filter_text: Some("other".to_string()),
12548 detail: None,
12549 documentation: None,
12550 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12551 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12552 new_text: ".other".to_string(),
12553 })),
12554 ..lsp::CompletionItem::default()
12555 };
12556
12557 let item1 = item1.clone();
12558 cx.set_request_handler::<lsp::request::Completion, _, _>({
12559 let item1 = item1.clone();
12560 move |_, _, _| {
12561 let item1 = item1.clone();
12562 let item2 = item2.clone();
12563 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
12564 }
12565 })
12566 .next()
12567 .await;
12568
12569 cx.condition(|editor, _| editor.context_menu_visible())
12570 .await;
12571 cx.update_editor(|editor, _, _| {
12572 let context_menu = editor.context_menu.borrow_mut();
12573 let context_menu = context_menu
12574 .as_ref()
12575 .expect("Should have the context menu deployed");
12576 match context_menu {
12577 CodeContextMenu::Completions(completions_menu) => {
12578 let completions = completions_menu.completions.borrow_mut();
12579 assert_eq!(
12580 completions
12581 .iter()
12582 .map(|completion| &completion.label.text)
12583 .collect::<Vec<_>>(),
12584 vec!["method id()", "other"]
12585 )
12586 }
12587 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
12588 }
12589 });
12590
12591 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
12592 let item1 = item1.clone();
12593 move |_, item_to_resolve, _| {
12594 let item1 = item1.clone();
12595 async move {
12596 if item1 == item_to_resolve {
12597 Ok(lsp::CompletionItem {
12598 label: "method id()".to_string(),
12599 filter_text: Some("id".to_string()),
12600 detail: Some("Now resolved!".to_string()),
12601 documentation: Some(lsp::Documentation::String("Docs".to_string())),
12602 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12603 range: lsp::Range::new(
12604 lsp::Position::new(0, 22),
12605 lsp::Position::new(0, 22),
12606 ),
12607 new_text: ".id".to_string(),
12608 })),
12609 ..lsp::CompletionItem::default()
12610 })
12611 } else {
12612 Ok(item_to_resolve)
12613 }
12614 }
12615 }
12616 })
12617 .next()
12618 .await
12619 .unwrap();
12620 cx.run_until_parked();
12621
12622 cx.update_editor(|editor, window, cx| {
12623 editor.context_menu_next(&Default::default(), window, cx);
12624 });
12625
12626 cx.update_editor(|editor, _, _| {
12627 let context_menu = editor.context_menu.borrow_mut();
12628 let context_menu = context_menu
12629 .as_ref()
12630 .expect("Should have the context menu deployed");
12631 match context_menu {
12632 CodeContextMenu::Completions(completions_menu) => {
12633 let completions = completions_menu.completions.borrow_mut();
12634 assert_eq!(
12635 completions
12636 .iter()
12637 .map(|completion| &completion.label.text)
12638 .collect::<Vec<_>>(),
12639 vec!["method id() Now resolved!", "other"],
12640 "Should update first completion label, but not second as the filter text did not match."
12641 );
12642 }
12643 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
12644 }
12645 });
12646}
12647
12648#[gpui::test]
12649async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
12650 init_test(cx, |_| {});
12651
12652 let mut cx = EditorLspTestContext::new_rust(
12653 lsp::ServerCapabilities {
12654 completion_provider: Some(lsp::CompletionOptions {
12655 trigger_characters: Some(vec![".".to_string()]),
12656 resolve_provider: Some(true),
12657 ..Default::default()
12658 }),
12659 ..Default::default()
12660 },
12661 cx,
12662 )
12663 .await;
12664
12665 cx.set_state("fn main() { let a = 2ˇ; }");
12666 cx.simulate_keystroke(".");
12667
12668 let unresolved_item_1 = lsp::CompletionItem {
12669 label: "id".to_string(),
12670 filter_text: Some("id".to_string()),
12671 detail: None,
12672 documentation: None,
12673 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12674 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12675 new_text: ".id".to_string(),
12676 })),
12677 ..lsp::CompletionItem::default()
12678 };
12679 let resolved_item_1 = lsp::CompletionItem {
12680 additional_text_edits: Some(vec![lsp::TextEdit {
12681 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
12682 new_text: "!!".to_string(),
12683 }]),
12684 ..unresolved_item_1.clone()
12685 };
12686 let unresolved_item_2 = lsp::CompletionItem {
12687 label: "other".to_string(),
12688 filter_text: Some("other".to_string()),
12689 detail: None,
12690 documentation: None,
12691 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12692 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12693 new_text: ".other".to_string(),
12694 })),
12695 ..lsp::CompletionItem::default()
12696 };
12697 let resolved_item_2 = lsp::CompletionItem {
12698 additional_text_edits: Some(vec![lsp::TextEdit {
12699 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
12700 new_text: "??".to_string(),
12701 }]),
12702 ..unresolved_item_2.clone()
12703 };
12704
12705 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
12706 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
12707 cx.lsp
12708 .server
12709 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
12710 let unresolved_item_1 = unresolved_item_1.clone();
12711 let resolved_item_1 = resolved_item_1.clone();
12712 let unresolved_item_2 = unresolved_item_2.clone();
12713 let resolved_item_2 = resolved_item_2.clone();
12714 let resolve_requests_1 = resolve_requests_1.clone();
12715 let resolve_requests_2 = resolve_requests_2.clone();
12716 move |unresolved_request, _| {
12717 let unresolved_item_1 = unresolved_item_1.clone();
12718 let resolved_item_1 = resolved_item_1.clone();
12719 let unresolved_item_2 = unresolved_item_2.clone();
12720 let resolved_item_2 = resolved_item_2.clone();
12721 let resolve_requests_1 = resolve_requests_1.clone();
12722 let resolve_requests_2 = resolve_requests_2.clone();
12723 async move {
12724 if unresolved_request == unresolved_item_1 {
12725 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
12726 Ok(resolved_item_1.clone())
12727 } else if unresolved_request == unresolved_item_2 {
12728 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
12729 Ok(resolved_item_2.clone())
12730 } else {
12731 panic!("Unexpected completion item {unresolved_request:?}")
12732 }
12733 }
12734 }
12735 })
12736 .detach();
12737
12738 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
12739 let unresolved_item_1 = unresolved_item_1.clone();
12740 let unresolved_item_2 = unresolved_item_2.clone();
12741 async move {
12742 Ok(Some(lsp::CompletionResponse::Array(vec![
12743 unresolved_item_1,
12744 unresolved_item_2,
12745 ])))
12746 }
12747 })
12748 .next()
12749 .await;
12750
12751 cx.condition(|editor, _| editor.context_menu_visible())
12752 .await;
12753 cx.update_editor(|editor, _, _| {
12754 let context_menu = editor.context_menu.borrow_mut();
12755 let context_menu = context_menu
12756 .as_ref()
12757 .expect("Should have the context menu deployed");
12758 match context_menu {
12759 CodeContextMenu::Completions(completions_menu) => {
12760 let completions = completions_menu.completions.borrow_mut();
12761 assert_eq!(
12762 completions
12763 .iter()
12764 .map(|completion| &completion.label.text)
12765 .collect::<Vec<_>>(),
12766 vec!["id", "other"]
12767 )
12768 }
12769 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
12770 }
12771 });
12772 cx.run_until_parked();
12773
12774 cx.update_editor(|editor, window, cx| {
12775 editor.context_menu_next(&ContextMenuNext, window, cx);
12776 });
12777 cx.run_until_parked();
12778 cx.update_editor(|editor, window, cx| {
12779 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
12780 });
12781 cx.run_until_parked();
12782 cx.update_editor(|editor, window, cx| {
12783 editor.context_menu_next(&ContextMenuNext, window, cx);
12784 });
12785 cx.run_until_parked();
12786 cx.update_editor(|editor, window, cx| {
12787 editor
12788 .compose_completion(&ComposeCompletion::default(), window, cx)
12789 .expect("No task returned")
12790 })
12791 .await
12792 .expect("Completion failed");
12793 cx.run_until_parked();
12794
12795 cx.update_editor(|editor, _, cx| {
12796 assert_eq!(
12797 resolve_requests_1.load(atomic::Ordering::Acquire),
12798 1,
12799 "Should always resolve once despite multiple selections"
12800 );
12801 assert_eq!(
12802 resolve_requests_2.load(atomic::Ordering::Acquire),
12803 1,
12804 "Should always resolve once after multiple selections and applying the completion"
12805 );
12806 assert_eq!(
12807 editor.text(cx),
12808 "fn main() { let a = ??.other; }",
12809 "Should use resolved data when applying the completion"
12810 );
12811 });
12812}
12813
12814#[gpui::test]
12815async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
12816 init_test(cx, |_| {});
12817
12818 let item_0 = lsp::CompletionItem {
12819 label: "abs".into(),
12820 insert_text: Some("abs".into()),
12821 data: Some(json!({ "very": "special"})),
12822 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
12823 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
12824 lsp::InsertReplaceEdit {
12825 new_text: "abs".to_string(),
12826 insert: lsp::Range::default(),
12827 replace: lsp::Range::default(),
12828 },
12829 )),
12830 ..lsp::CompletionItem::default()
12831 };
12832 let items = iter::once(item_0.clone())
12833 .chain((11..51).map(|i| lsp::CompletionItem {
12834 label: format!("item_{}", i),
12835 insert_text: Some(format!("item_{}", i)),
12836 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
12837 ..lsp::CompletionItem::default()
12838 }))
12839 .collect::<Vec<_>>();
12840
12841 let default_commit_characters = vec!["?".to_string()];
12842 let default_data = json!({ "default": "data"});
12843 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
12844 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
12845 let default_edit_range = lsp::Range {
12846 start: lsp::Position {
12847 line: 0,
12848 character: 5,
12849 },
12850 end: lsp::Position {
12851 line: 0,
12852 character: 5,
12853 },
12854 };
12855
12856 let mut cx = EditorLspTestContext::new_rust(
12857 lsp::ServerCapabilities {
12858 completion_provider: Some(lsp::CompletionOptions {
12859 trigger_characters: Some(vec![".".to_string()]),
12860 resolve_provider: Some(true),
12861 ..Default::default()
12862 }),
12863 ..Default::default()
12864 },
12865 cx,
12866 )
12867 .await;
12868
12869 cx.set_state("fn main() { let a = 2ˇ; }");
12870 cx.simulate_keystroke(".");
12871
12872 let completion_data = default_data.clone();
12873 let completion_characters = default_commit_characters.clone();
12874 let completion_items = items.clone();
12875 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
12876 let default_data = completion_data.clone();
12877 let default_commit_characters = completion_characters.clone();
12878 let items = completion_items.clone();
12879 async move {
12880 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
12881 items,
12882 item_defaults: Some(lsp::CompletionListItemDefaults {
12883 data: Some(default_data.clone()),
12884 commit_characters: Some(default_commit_characters.clone()),
12885 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
12886 default_edit_range,
12887 )),
12888 insert_text_format: Some(default_insert_text_format),
12889 insert_text_mode: Some(default_insert_text_mode),
12890 }),
12891 ..lsp::CompletionList::default()
12892 })))
12893 }
12894 })
12895 .next()
12896 .await;
12897
12898 let resolved_items = Arc::new(Mutex::new(Vec::new()));
12899 cx.lsp
12900 .server
12901 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
12902 let closure_resolved_items = resolved_items.clone();
12903 move |item_to_resolve, _| {
12904 let closure_resolved_items = closure_resolved_items.clone();
12905 async move {
12906 closure_resolved_items.lock().push(item_to_resolve.clone());
12907 Ok(item_to_resolve)
12908 }
12909 }
12910 })
12911 .detach();
12912
12913 cx.condition(|editor, _| editor.context_menu_visible())
12914 .await;
12915 cx.run_until_parked();
12916 cx.update_editor(|editor, _, _| {
12917 let menu = editor.context_menu.borrow_mut();
12918 match menu.as_ref().expect("should have the completions menu") {
12919 CodeContextMenu::Completions(completions_menu) => {
12920 assert_eq!(
12921 completions_menu
12922 .entries
12923 .borrow()
12924 .iter()
12925 .map(|mat| mat.string.clone())
12926 .collect::<Vec<String>>(),
12927 items
12928 .iter()
12929 .map(|completion| completion.label.clone())
12930 .collect::<Vec<String>>()
12931 );
12932 }
12933 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
12934 }
12935 });
12936 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
12937 // with 4 from the end.
12938 assert_eq!(
12939 *resolved_items.lock(),
12940 [&items[0..16], &items[items.len() - 4..items.len()]]
12941 .concat()
12942 .iter()
12943 .cloned()
12944 .map(|mut item| {
12945 if item.data.is_none() {
12946 item.data = Some(default_data.clone());
12947 }
12948 item
12949 })
12950 .collect::<Vec<lsp::CompletionItem>>(),
12951 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
12952 );
12953 resolved_items.lock().clear();
12954
12955 cx.update_editor(|editor, window, cx| {
12956 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
12957 });
12958 cx.run_until_parked();
12959 // Completions that have already been resolved are skipped.
12960 assert_eq!(
12961 *resolved_items.lock(),
12962 items[items.len() - 16..items.len() - 4]
12963 .iter()
12964 .cloned()
12965 .map(|mut item| {
12966 if item.data.is_none() {
12967 item.data = Some(default_data.clone());
12968 }
12969 item
12970 })
12971 .collect::<Vec<lsp::CompletionItem>>()
12972 );
12973 resolved_items.lock().clear();
12974}
12975
12976#[gpui::test]
12977async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
12978 init_test(cx, |_| {});
12979
12980 let mut cx = EditorLspTestContext::new(
12981 Language::new(
12982 LanguageConfig {
12983 matcher: LanguageMatcher {
12984 path_suffixes: vec!["jsx".into()],
12985 ..Default::default()
12986 },
12987 overrides: [(
12988 "element".into(),
12989 LanguageConfigOverride {
12990 completion_query_characters: Override::Set(['-'].into_iter().collect()),
12991 ..Default::default()
12992 },
12993 )]
12994 .into_iter()
12995 .collect(),
12996 ..Default::default()
12997 },
12998 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
12999 )
13000 .with_override_query("(jsx_self_closing_element) @element")
13001 .unwrap(),
13002 lsp::ServerCapabilities {
13003 completion_provider: Some(lsp::CompletionOptions {
13004 trigger_characters: Some(vec![":".to_string()]),
13005 ..Default::default()
13006 }),
13007 ..Default::default()
13008 },
13009 cx,
13010 )
13011 .await;
13012
13013 cx.lsp
13014 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13015 Ok(Some(lsp::CompletionResponse::Array(vec![
13016 lsp::CompletionItem {
13017 label: "bg-blue".into(),
13018 ..Default::default()
13019 },
13020 lsp::CompletionItem {
13021 label: "bg-red".into(),
13022 ..Default::default()
13023 },
13024 lsp::CompletionItem {
13025 label: "bg-yellow".into(),
13026 ..Default::default()
13027 },
13028 ])))
13029 });
13030
13031 cx.set_state(r#"<p class="bgˇ" />"#);
13032
13033 // Trigger completion when typing a dash, because the dash is an extra
13034 // word character in the 'element' scope, which contains the cursor.
13035 cx.simulate_keystroke("-");
13036 cx.executor().run_until_parked();
13037 cx.update_editor(|editor, _, _| {
13038 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13039 {
13040 assert_eq!(
13041 completion_menu_entries(&menu),
13042 &["bg-red", "bg-blue", "bg-yellow"]
13043 );
13044 } else {
13045 panic!("expected completion menu to be open");
13046 }
13047 });
13048
13049 cx.simulate_keystroke("l");
13050 cx.executor().run_until_parked();
13051 cx.update_editor(|editor, _, _| {
13052 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13053 {
13054 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
13055 } else {
13056 panic!("expected completion menu to be open");
13057 }
13058 });
13059
13060 // When filtering completions, consider the character after the '-' to
13061 // be the start of a subword.
13062 cx.set_state(r#"<p class="yelˇ" />"#);
13063 cx.simulate_keystroke("l");
13064 cx.executor().run_until_parked();
13065 cx.update_editor(|editor, _, _| {
13066 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13067 {
13068 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
13069 } else {
13070 panic!("expected completion menu to be open");
13071 }
13072 });
13073}
13074
13075fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
13076 let entries = menu.entries.borrow();
13077 entries.iter().map(|mat| mat.string.clone()).collect()
13078}
13079
13080#[gpui::test]
13081async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
13082 init_test(cx, |settings| {
13083 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
13084 FormatterList(vec![Formatter::Prettier].into()),
13085 ))
13086 });
13087
13088 let fs = FakeFs::new(cx.executor());
13089 fs.insert_file(path!("/file.ts"), Default::default()).await;
13090
13091 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
13092 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13093
13094 language_registry.add(Arc::new(Language::new(
13095 LanguageConfig {
13096 name: "TypeScript".into(),
13097 matcher: LanguageMatcher {
13098 path_suffixes: vec!["ts".to_string()],
13099 ..Default::default()
13100 },
13101 ..Default::default()
13102 },
13103 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
13104 )));
13105 update_test_language_settings(cx, |settings| {
13106 settings.defaults.prettier = Some(PrettierSettings {
13107 allowed: true,
13108 ..PrettierSettings::default()
13109 });
13110 });
13111
13112 let test_plugin = "test_plugin";
13113 let _ = language_registry.register_fake_lsp(
13114 "TypeScript",
13115 FakeLspAdapter {
13116 prettier_plugins: vec![test_plugin],
13117 ..Default::default()
13118 },
13119 );
13120
13121 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
13122 let buffer = project
13123 .update(cx, |project, cx| {
13124 project.open_local_buffer(path!("/file.ts"), cx)
13125 })
13126 .await
13127 .unwrap();
13128
13129 let buffer_text = "one\ntwo\nthree\n";
13130 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13131 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
13132 editor.update_in(cx, |editor, window, cx| {
13133 editor.set_text(buffer_text, window, cx)
13134 });
13135
13136 editor
13137 .update_in(cx, |editor, window, cx| {
13138 editor.perform_format(
13139 project.clone(),
13140 FormatTrigger::Manual,
13141 FormatTarget::Buffers,
13142 window,
13143 cx,
13144 )
13145 })
13146 .unwrap()
13147 .await;
13148 assert_eq!(
13149 editor.update(cx, |editor, cx| editor.text(cx)),
13150 buffer_text.to_string() + prettier_format_suffix,
13151 "Test prettier formatting was not applied to the original buffer text",
13152 );
13153
13154 update_test_language_settings(cx, |settings| {
13155 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
13156 });
13157 let format = editor.update_in(cx, |editor, window, cx| {
13158 editor.perform_format(
13159 project.clone(),
13160 FormatTrigger::Manual,
13161 FormatTarget::Buffers,
13162 window,
13163 cx,
13164 )
13165 });
13166 format.await.unwrap();
13167 assert_eq!(
13168 editor.update(cx, |editor, cx| editor.text(cx)),
13169 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
13170 "Autoformatting (via test prettier) was not applied to the original buffer text",
13171 );
13172}
13173
13174#[gpui::test]
13175async fn test_addition_reverts(cx: &mut TestAppContext) {
13176 init_test(cx, |_| {});
13177 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13178 let base_text = indoc! {r#"
13179 struct Row;
13180 struct Row1;
13181 struct Row2;
13182
13183 struct Row4;
13184 struct Row5;
13185 struct Row6;
13186
13187 struct Row8;
13188 struct Row9;
13189 struct Row10;"#};
13190
13191 // When addition hunks are not adjacent to carets, no hunk revert is performed
13192 assert_hunk_revert(
13193 indoc! {r#"struct Row;
13194 struct Row1;
13195 struct Row1.1;
13196 struct Row1.2;
13197 struct Row2;ˇ
13198
13199 struct Row4;
13200 struct Row5;
13201 struct Row6;
13202
13203 struct Row8;
13204 ˇstruct Row9;
13205 struct Row9.1;
13206 struct Row9.2;
13207 struct Row9.3;
13208 struct Row10;"#},
13209 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
13210 indoc! {r#"struct Row;
13211 struct Row1;
13212 struct Row1.1;
13213 struct Row1.2;
13214 struct Row2;ˇ
13215
13216 struct Row4;
13217 struct Row5;
13218 struct Row6;
13219
13220 struct Row8;
13221 ˇstruct Row9;
13222 struct Row9.1;
13223 struct Row9.2;
13224 struct Row9.3;
13225 struct Row10;"#},
13226 base_text,
13227 &mut cx,
13228 );
13229 // Same for selections
13230 assert_hunk_revert(
13231 indoc! {r#"struct Row;
13232 struct Row1;
13233 struct Row2;
13234 struct Row2.1;
13235 struct Row2.2;
13236 «ˇ
13237 struct Row4;
13238 struct» Row5;
13239 «struct Row6;
13240 ˇ»
13241 struct Row9.1;
13242 struct Row9.2;
13243 struct Row9.3;
13244 struct Row8;
13245 struct Row9;
13246 struct Row10;"#},
13247 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
13248 indoc! {r#"struct Row;
13249 struct Row1;
13250 struct Row2;
13251 struct Row2.1;
13252 struct Row2.2;
13253 «ˇ
13254 struct Row4;
13255 struct» Row5;
13256 «struct Row6;
13257 ˇ»
13258 struct Row9.1;
13259 struct Row9.2;
13260 struct Row9.3;
13261 struct Row8;
13262 struct Row9;
13263 struct Row10;"#},
13264 base_text,
13265 &mut cx,
13266 );
13267
13268 // When carets and selections intersect the addition hunks, those are reverted.
13269 // Adjacent carets got merged.
13270 assert_hunk_revert(
13271 indoc! {r#"struct Row;
13272 ˇ// something on the top
13273 struct Row1;
13274 struct Row2;
13275 struct Roˇw3.1;
13276 struct Row2.2;
13277 struct Row2.3;ˇ
13278
13279 struct Row4;
13280 struct ˇRow5.1;
13281 struct Row5.2;
13282 struct «Rowˇ»5.3;
13283 struct Row5;
13284 struct Row6;
13285 ˇ
13286 struct Row9.1;
13287 struct «Rowˇ»9.2;
13288 struct «ˇRow»9.3;
13289 struct Row8;
13290 struct Row9;
13291 «ˇ// something on bottom»
13292 struct Row10;"#},
13293 vec![
13294 DiffHunkStatusKind::Added,
13295 DiffHunkStatusKind::Added,
13296 DiffHunkStatusKind::Added,
13297 DiffHunkStatusKind::Added,
13298 DiffHunkStatusKind::Added,
13299 ],
13300 indoc! {r#"struct Row;
13301 ˇstruct Row1;
13302 struct Row2;
13303 ˇ
13304 struct Row4;
13305 ˇstruct Row5;
13306 struct Row6;
13307 ˇ
13308 ˇstruct Row8;
13309 struct Row9;
13310 ˇstruct Row10;"#},
13311 base_text,
13312 &mut cx,
13313 );
13314}
13315
13316#[gpui::test]
13317async fn test_modification_reverts(cx: &mut TestAppContext) {
13318 init_test(cx, |_| {});
13319 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13320 let base_text = indoc! {r#"
13321 struct Row;
13322 struct Row1;
13323 struct Row2;
13324
13325 struct Row4;
13326 struct Row5;
13327 struct Row6;
13328
13329 struct Row8;
13330 struct Row9;
13331 struct Row10;"#};
13332
13333 // Modification hunks behave the same as the addition ones.
13334 assert_hunk_revert(
13335 indoc! {r#"struct Row;
13336 struct Row1;
13337 struct Row33;
13338 ˇ
13339 struct Row4;
13340 struct Row5;
13341 struct Row6;
13342 ˇ
13343 struct Row99;
13344 struct Row9;
13345 struct Row10;"#},
13346 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
13347 indoc! {r#"struct Row;
13348 struct Row1;
13349 struct Row33;
13350 ˇ
13351 struct Row4;
13352 struct Row5;
13353 struct Row6;
13354 ˇ
13355 struct Row99;
13356 struct Row9;
13357 struct Row10;"#},
13358 base_text,
13359 &mut cx,
13360 );
13361 assert_hunk_revert(
13362 indoc! {r#"struct Row;
13363 struct Row1;
13364 struct Row33;
13365 «ˇ
13366 struct Row4;
13367 struct» Row5;
13368 «struct Row6;
13369 ˇ»
13370 struct Row99;
13371 struct Row9;
13372 struct Row10;"#},
13373 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
13374 indoc! {r#"struct Row;
13375 struct Row1;
13376 struct Row33;
13377 «ˇ
13378 struct Row4;
13379 struct» Row5;
13380 «struct Row6;
13381 ˇ»
13382 struct Row99;
13383 struct Row9;
13384 struct Row10;"#},
13385 base_text,
13386 &mut cx,
13387 );
13388
13389 assert_hunk_revert(
13390 indoc! {r#"ˇstruct Row1.1;
13391 struct Row1;
13392 «ˇstr»uct Row22;
13393
13394 struct ˇRow44;
13395 struct Row5;
13396 struct «Rˇ»ow66;ˇ
13397
13398 «struˇ»ct Row88;
13399 struct Row9;
13400 struct Row1011;ˇ"#},
13401 vec![
13402 DiffHunkStatusKind::Modified,
13403 DiffHunkStatusKind::Modified,
13404 DiffHunkStatusKind::Modified,
13405 DiffHunkStatusKind::Modified,
13406 DiffHunkStatusKind::Modified,
13407 DiffHunkStatusKind::Modified,
13408 ],
13409 indoc! {r#"struct Row;
13410 ˇstruct Row1;
13411 struct Row2;
13412 ˇ
13413 struct Row4;
13414 ˇstruct Row5;
13415 struct Row6;
13416 ˇ
13417 struct Row8;
13418 ˇstruct Row9;
13419 struct Row10;ˇ"#},
13420 base_text,
13421 &mut cx,
13422 );
13423}
13424
13425#[gpui::test]
13426async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
13427 init_test(cx, |_| {});
13428 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13429 let base_text = indoc! {r#"
13430 one
13431
13432 two
13433 three
13434 "#};
13435
13436 cx.set_head_text(base_text);
13437 cx.set_state("\nˇ\n");
13438 cx.executor().run_until_parked();
13439 cx.update_editor(|editor, _window, cx| {
13440 editor.expand_selected_diff_hunks(cx);
13441 });
13442 cx.executor().run_until_parked();
13443 cx.update_editor(|editor, window, cx| {
13444 editor.backspace(&Default::default(), window, cx);
13445 });
13446 cx.run_until_parked();
13447 cx.assert_state_with_diff(
13448 indoc! {r#"
13449
13450 - two
13451 - threeˇ
13452 +
13453 "#}
13454 .to_string(),
13455 );
13456}
13457
13458#[gpui::test]
13459async fn test_deletion_reverts(cx: &mut TestAppContext) {
13460 init_test(cx, |_| {});
13461 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13462 let base_text = indoc! {r#"struct Row;
13463struct Row1;
13464struct Row2;
13465
13466struct Row4;
13467struct Row5;
13468struct Row6;
13469
13470struct Row8;
13471struct Row9;
13472struct Row10;"#};
13473
13474 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
13475 assert_hunk_revert(
13476 indoc! {r#"struct Row;
13477 struct Row2;
13478
13479 ˇstruct Row4;
13480 struct Row5;
13481 struct Row6;
13482 ˇ
13483 struct Row8;
13484 struct Row10;"#},
13485 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
13486 indoc! {r#"struct Row;
13487 struct Row2;
13488
13489 ˇstruct Row4;
13490 struct Row5;
13491 struct Row6;
13492 ˇ
13493 struct Row8;
13494 struct Row10;"#},
13495 base_text,
13496 &mut cx,
13497 );
13498 assert_hunk_revert(
13499 indoc! {r#"struct Row;
13500 struct Row2;
13501
13502 «ˇstruct Row4;
13503 struct» Row5;
13504 «struct Row6;
13505 ˇ»
13506 struct Row8;
13507 struct Row10;"#},
13508 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
13509 indoc! {r#"struct Row;
13510 struct Row2;
13511
13512 «ˇstruct Row4;
13513 struct» Row5;
13514 «struct Row6;
13515 ˇ»
13516 struct Row8;
13517 struct Row10;"#},
13518 base_text,
13519 &mut cx,
13520 );
13521
13522 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
13523 assert_hunk_revert(
13524 indoc! {r#"struct Row;
13525 ˇstruct Row2;
13526
13527 struct Row4;
13528 struct Row5;
13529 struct Row6;
13530
13531 struct Row8;ˇ
13532 struct Row10;"#},
13533 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
13534 indoc! {r#"struct Row;
13535 struct Row1;
13536 ˇstruct Row2;
13537
13538 struct Row4;
13539 struct Row5;
13540 struct Row6;
13541
13542 struct Row8;ˇ
13543 struct Row9;
13544 struct Row10;"#},
13545 base_text,
13546 &mut cx,
13547 );
13548 assert_hunk_revert(
13549 indoc! {r#"struct Row;
13550 struct Row2«ˇ;
13551 struct Row4;
13552 struct» Row5;
13553 «struct Row6;
13554
13555 struct Row8;ˇ»
13556 struct Row10;"#},
13557 vec![
13558 DiffHunkStatusKind::Deleted,
13559 DiffHunkStatusKind::Deleted,
13560 DiffHunkStatusKind::Deleted,
13561 ],
13562 indoc! {r#"struct Row;
13563 struct Row1;
13564 struct Row2«ˇ;
13565
13566 struct Row4;
13567 struct» Row5;
13568 «struct Row6;
13569
13570 struct Row8;ˇ»
13571 struct Row9;
13572 struct Row10;"#},
13573 base_text,
13574 &mut cx,
13575 );
13576}
13577
13578#[gpui::test]
13579async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
13580 init_test(cx, |_| {});
13581
13582 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
13583 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
13584 let base_text_3 =
13585 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
13586
13587 let text_1 = edit_first_char_of_every_line(base_text_1);
13588 let text_2 = edit_first_char_of_every_line(base_text_2);
13589 let text_3 = edit_first_char_of_every_line(base_text_3);
13590
13591 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
13592 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
13593 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
13594
13595 let multibuffer = cx.new(|cx| {
13596 let mut multibuffer = MultiBuffer::new(ReadWrite);
13597 multibuffer.push_excerpts(
13598 buffer_1.clone(),
13599 [
13600 ExcerptRange {
13601 context: Point::new(0, 0)..Point::new(3, 0),
13602 primary: None,
13603 },
13604 ExcerptRange {
13605 context: Point::new(5, 0)..Point::new(7, 0),
13606 primary: None,
13607 },
13608 ExcerptRange {
13609 context: Point::new(9, 0)..Point::new(10, 4),
13610 primary: None,
13611 },
13612 ],
13613 cx,
13614 );
13615 multibuffer.push_excerpts(
13616 buffer_2.clone(),
13617 [
13618 ExcerptRange {
13619 context: Point::new(0, 0)..Point::new(3, 0),
13620 primary: None,
13621 },
13622 ExcerptRange {
13623 context: Point::new(5, 0)..Point::new(7, 0),
13624 primary: None,
13625 },
13626 ExcerptRange {
13627 context: Point::new(9, 0)..Point::new(10, 4),
13628 primary: None,
13629 },
13630 ],
13631 cx,
13632 );
13633 multibuffer.push_excerpts(
13634 buffer_3.clone(),
13635 [
13636 ExcerptRange {
13637 context: Point::new(0, 0)..Point::new(3, 0),
13638 primary: None,
13639 },
13640 ExcerptRange {
13641 context: Point::new(5, 0)..Point::new(7, 0),
13642 primary: None,
13643 },
13644 ExcerptRange {
13645 context: Point::new(9, 0)..Point::new(10, 4),
13646 primary: None,
13647 },
13648 ],
13649 cx,
13650 );
13651 multibuffer
13652 });
13653
13654 let fs = FakeFs::new(cx.executor());
13655 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
13656 let (editor, cx) = cx
13657 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
13658 editor.update_in(cx, |editor, _window, cx| {
13659 for (buffer, diff_base) in [
13660 (buffer_1.clone(), base_text_1),
13661 (buffer_2.clone(), base_text_2),
13662 (buffer_3.clone(), base_text_3),
13663 ] {
13664 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
13665 editor
13666 .buffer
13667 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
13668 }
13669 });
13670 cx.executor().run_until_parked();
13671
13672 editor.update_in(cx, |editor, window, cx| {
13673 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}");
13674 editor.select_all(&SelectAll, window, cx);
13675 editor.git_restore(&Default::default(), window, cx);
13676 });
13677 cx.executor().run_until_parked();
13678
13679 // When all ranges are selected, all buffer hunks are reverted.
13680 editor.update(cx, |editor, cx| {
13681 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");
13682 });
13683 buffer_1.update(cx, |buffer, _| {
13684 assert_eq!(buffer.text(), base_text_1);
13685 });
13686 buffer_2.update(cx, |buffer, _| {
13687 assert_eq!(buffer.text(), base_text_2);
13688 });
13689 buffer_3.update(cx, |buffer, _| {
13690 assert_eq!(buffer.text(), base_text_3);
13691 });
13692
13693 editor.update_in(cx, |editor, window, cx| {
13694 editor.undo(&Default::default(), window, cx);
13695 });
13696
13697 editor.update_in(cx, |editor, window, cx| {
13698 editor.change_selections(None, window, cx, |s| {
13699 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
13700 });
13701 editor.git_restore(&Default::default(), window, cx);
13702 });
13703
13704 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
13705 // but not affect buffer_2 and its related excerpts.
13706 editor.update(cx, |editor, cx| {
13707 assert_eq!(
13708 editor.text(cx),
13709 "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}"
13710 );
13711 });
13712 buffer_1.update(cx, |buffer, _| {
13713 assert_eq!(buffer.text(), base_text_1);
13714 });
13715 buffer_2.update(cx, |buffer, _| {
13716 assert_eq!(
13717 buffer.text(),
13718 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
13719 );
13720 });
13721 buffer_3.update(cx, |buffer, _| {
13722 assert_eq!(
13723 buffer.text(),
13724 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
13725 );
13726 });
13727
13728 fn edit_first_char_of_every_line(text: &str) -> String {
13729 text.split('\n')
13730 .map(|line| format!("X{}", &line[1..]))
13731 .collect::<Vec<_>>()
13732 .join("\n")
13733 }
13734}
13735
13736#[gpui::test]
13737async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
13738 init_test(cx, |_| {});
13739
13740 let cols = 4;
13741 let rows = 10;
13742 let sample_text_1 = sample_text(rows, cols, 'a');
13743 assert_eq!(
13744 sample_text_1,
13745 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
13746 );
13747 let sample_text_2 = sample_text(rows, cols, 'l');
13748 assert_eq!(
13749 sample_text_2,
13750 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
13751 );
13752 let sample_text_3 = sample_text(rows, cols, 'v');
13753 assert_eq!(
13754 sample_text_3,
13755 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
13756 );
13757
13758 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
13759 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
13760 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
13761
13762 let multi_buffer = cx.new(|cx| {
13763 let mut multibuffer = MultiBuffer::new(ReadWrite);
13764 multibuffer.push_excerpts(
13765 buffer_1.clone(),
13766 [
13767 ExcerptRange {
13768 context: Point::new(0, 0)..Point::new(3, 0),
13769 primary: None,
13770 },
13771 ExcerptRange {
13772 context: Point::new(5, 0)..Point::new(7, 0),
13773 primary: None,
13774 },
13775 ExcerptRange {
13776 context: Point::new(9, 0)..Point::new(10, 4),
13777 primary: None,
13778 },
13779 ],
13780 cx,
13781 );
13782 multibuffer.push_excerpts(
13783 buffer_2.clone(),
13784 [
13785 ExcerptRange {
13786 context: Point::new(0, 0)..Point::new(3, 0),
13787 primary: None,
13788 },
13789 ExcerptRange {
13790 context: Point::new(5, 0)..Point::new(7, 0),
13791 primary: None,
13792 },
13793 ExcerptRange {
13794 context: Point::new(9, 0)..Point::new(10, 4),
13795 primary: None,
13796 },
13797 ],
13798 cx,
13799 );
13800 multibuffer.push_excerpts(
13801 buffer_3.clone(),
13802 [
13803 ExcerptRange {
13804 context: Point::new(0, 0)..Point::new(3, 0),
13805 primary: None,
13806 },
13807 ExcerptRange {
13808 context: Point::new(5, 0)..Point::new(7, 0),
13809 primary: None,
13810 },
13811 ExcerptRange {
13812 context: Point::new(9, 0)..Point::new(10, 4),
13813 primary: None,
13814 },
13815 ],
13816 cx,
13817 );
13818 multibuffer
13819 });
13820
13821 let fs = FakeFs::new(cx.executor());
13822 fs.insert_tree(
13823 "/a",
13824 json!({
13825 "main.rs": sample_text_1,
13826 "other.rs": sample_text_2,
13827 "lib.rs": sample_text_3,
13828 }),
13829 )
13830 .await;
13831 let project = Project::test(fs, ["/a".as_ref()], cx).await;
13832 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13833 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
13834 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
13835 Editor::new(
13836 EditorMode::Full,
13837 multi_buffer,
13838 Some(project.clone()),
13839 window,
13840 cx,
13841 )
13842 });
13843 let multibuffer_item_id = workspace
13844 .update(cx, |workspace, window, cx| {
13845 assert!(
13846 workspace.active_item(cx).is_none(),
13847 "active item should be None before the first item is added"
13848 );
13849 workspace.add_item_to_active_pane(
13850 Box::new(multi_buffer_editor.clone()),
13851 None,
13852 true,
13853 window,
13854 cx,
13855 );
13856 let active_item = workspace
13857 .active_item(cx)
13858 .expect("should have an active item after adding the multi buffer");
13859 assert!(
13860 !active_item.is_singleton(cx),
13861 "A multi buffer was expected to active after adding"
13862 );
13863 active_item.item_id()
13864 })
13865 .unwrap();
13866 cx.executor().run_until_parked();
13867
13868 multi_buffer_editor.update_in(cx, |editor, window, cx| {
13869 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
13870 s.select_ranges(Some(1..2))
13871 });
13872 editor.open_excerpts(&OpenExcerpts, window, cx);
13873 });
13874 cx.executor().run_until_parked();
13875 let first_item_id = workspace
13876 .update(cx, |workspace, window, cx| {
13877 let active_item = workspace
13878 .active_item(cx)
13879 .expect("should have an active item after navigating into the 1st buffer");
13880 let first_item_id = active_item.item_id();
13881 assert_ne!(
13882 first_item_id, multibuffer_item_id,
13883 "Should navigate into the 1st buffer and activate it"
13884 );
13885 assert!(
13886 active_item.is_singleton(cx),
13887 "New active item should be a singleton buffer"
13888 );
13889 assert_eq!(
13890 active_item
13891 .act_as::<Editor>(cx)
13892 .expect("should have navigated into an editor for the 1st buffer")
13893 .read(cx)
13894 .text(cx),
13895 sample_text_1
13896 );
13897
13898 workspace
13899 .go_back(workspace.active_pane().downgrade(), window, cx)
13900 .detach_and_log_err(cx);
13901
13902 first_item_id
13903 })
13904 .unwrap();
13905 cx.executor().run_until_parked();
13906 workspace
13907 .update(cx, |workspace, _, cx| {
13908 let active_item = workspace
13909 .active_item(cx)
13910 .expect("should have an active item after navigating back");
13911 assert_eq!(
13912 active_item.item_id(),
13913 multibuffer_item_id,
13914 "Should navigate back to the multi buffer"
13915 );
13916 assert!(!active_item.is_singleton(cx));
13917 })
13918 .unwrap();
13919
13920 multi_buffer_editor.update_in(cx, |editor, window, cx| {
13921 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
13922 s.select_ranges(Some(39..40))
13923 });
13924 editor.open_excerpts(&OpenExcerpts, window, cx);
13925 });
13926 cx.executor().run_until_parked();
13927 let second_item_id = workspace
13928 .update(cx, |workspace, window, cx| {
13929 let active_item = workspace
13930 .active_item(cx)
13931 .expect("should have an active item after navigating into the 2nd buffer");
13932 let second_item_id = active_item.item_id();
13933 assert_ne!(
13934 second_item_id, multibuffer_item_id,
13935 "Should navigate away from the multibuffer"
13936 );
13937 assert_ne!(
13938 second_item_id, first_item_id,
13939 "Should navigate into the 2nd buffer and activate it"
13940 );
13941 assert!(
13942 active_item.is_singleton(cx),
13943 "New active item should be a singleton buffer"
13944 );
13945 assert_eq!(
13946 active_item
13947 .act_as::<Editor>(cx)
13948 .expect("should have navigated into an editor")
13949 .read(cx)
13950 .text(cx),
13951 sample_text_2
13952 );
13953
13954 workspace
13955 .go_back(workspace.active_pane().downgrade(), window, cx)
13956 .detach_and_log_err(cx);
13957
13958 second_item_id
13959 })
13960 .unwrap();
13961 cx.executor().run_until_parked();
13962 workspace
13963 .update(cx, |workspace, _, cx| {
13964 let active_item = workspace
13965 .active_item(cx)
13966 .expect("should have an active item after navigating back from the 2nd buffer");
13967 assert_eq!(
13968 active_item.item_id(),
13969 multibuffer_item_id,
13970 "Should navigate back from the 2nd buffer to the multi buffer"
13971 );
13972 assert!(!active_item.is_singleton(cx));
13973 })
13974 .unwrap();
13975
13976 multi_buffer_editor.update_in(cx, |editor, window, cx| {
13977 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
13978 s.select_ranges(Some(70..70))
13979 });
13980 editor.open_excerpts(&OpenExcerpts, window, cx);
13981 });
13982 cx.executor().run_until_parked();
13983 workspace
13984 .update(cx, |workspace, window, cx| {
13985 let active_item = workspace
13986 .active_item(cx)
13987 .expect("should have an active item after navigating into the 3rd buffer");
13988 let third_item_id = active_item.item_id();
13989 assert_ne!(
13990 third_item_id, multibuffer_item_id,
13991 "Should navigate into the 3rd buffer and activate it"
13992 );
13993 assert_ne!(third_item_id, first_item_id);
13994 assert_ne!(third_item_id, second_item_id);
13995 assert!(
13996 active_item.is_singleton(cx),
13997 "New active item should be a singleton buffer"
13998 );
13999 assert_eq!(
14000 active_item
14001 .act_as::<Editor>(cx)
14002 .expect("should have navigated into an editor")
14003 .read(cx)
14004 .text(cx),
14005 sample_text_3
14006 );
14007
14008 workspace
14009 .go_back(workspace.active_pane().downgrade(), window, cx)
14010 .detach_and_log_err(cx);
14011 })
14012 .unwrap();
14013 cx.executor().run_until_parked();
14014 workspace
14015 .update(cx, |workspace, _, cx| {
14016 let active_item = workspace
14017 .active_item(cx)
14018 .expect("should have an active item after navigating back from the 3rd buffer");
14019 assert_eq!(
14020 active_item.item_id(),
14021 multibuffer_item_id,
14022 "Should navigate back from the 3rd buffer to the multi buffer"
14023 );
14024 assert!(!active_item.is_singleton(cx));
14025 })
14026 .unwrap();
14027}
14028
14029#[gpui::test]
14030async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14031 init_test(cx, |_| {});
14032
14033 let mut cx = EditorTestContext::new(cx).await;
14034
14035 let diff_base = r#"
14036 use some::mod;
14037
14038 const A: u32 = 42;
14039
14040 fn main() {
14041 println!("hello");
14042
14043 println!("world");
14044 }
14045 "#
14046 .unindent();
14047
14048 cx.set_state(
14049 &r#"
14050 use some::modified;
14051
14052 ˇ
14053 fn main() {
14054 println!("hello there");
14055
14056 println!("around the");
14057 println!("world");
14058 }
14059 "#
14060 .unindent(),
14061 );
14062
14063 cx.set_head_text(&diff_base);
14064 executor.run_until_parked();
14065
14066 cx.update_editor(|editor, window, cx| {
14067 editor.go_to_next_hunk(&GoToHunk, window, cx);
14068 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14069 });
14070 executor.run_until_parked();
14071 cx.assert_state_with_diff(
14072 r#"
14073 use some::modified;
14074
14075
14076 fn main() {
14077 - println!("hello");
14078 + ˇ println!("hello there");
14079
14080 println!("around the");
14081 println!("world");
14082 }
14083 "#
14084 .unindent(),
14085 );
14086
14087 cx.update_editor(|editor, window, cx| {
14088 for _ in 0..2 {
14089 editor.go_to_next_hunk(&GoToHunk, window, cx);
14090 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14091 }
14092 });
14093 executor.run_until_parked();
14094 cx.assert_state_with_diff(
14095 r#"
14096 - use some::mod;
14097 + ˇuse some::modified;
14098
14099
14100 fn main() {
14101 - println!("hello");
14102 + println!("hello there");
14103
14104 + println!("around the");
14105 println!("world");
14106 }
14107 "#
14108 .unindent(),
14109 );
14110
14111 cx.update_editor(|editor, window, cx| {
14112 editor.go_to_next_hunk(&GoToHunk, window, cx);
14113 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14114 });
14115 executor.run_until_parked();
14116 cx.assert_state_with_diff(
14117 r#"
14118 - use some::mod;
14119 + use some::modified;
14120
14121 - const A: u32 = 42;
14122 ˇ
14123 fn main() {
14124 - println!("hello");
14125 + println!("hello there");
14126
14127 + println!("around the");
14128 println!("world");
14129 }
14130 "#
14131 .unindent(),
14132 );
14133
14134 cx.update_editor(|editor, window, cx| {
14135 editor.cancel(&Cancel, window, cx);
14136 });
14137
14138 cx.assert_state_with_diff(
14139 r#"
14140 use some::modified;
14141
14142 ˇ
14143 fn main() {
14144 println!("hello there");
14145
14146 println!("around the");
14147 println!("world");
14148 }
14149 "#
14150 .unindent(),
14151 );
14152}
14153
14154#[gpui::test]
14155async fn test_diff_base_change_with_expanded_diff_hunks(
14156 executor: BackgroundExecutor,
14157 cx: &mut TestAppContext,
14158) {
14159 init_test(cx, |_| {});
14160
14161 let mut cx = EditorTestContext::new(cx).await;
14162
14163 let diff_base = r#"
14164 use some::mod1;
14165 use some::mod2;
14166
14167 const A: u32 = 42;
14168 const B: u32 = 42;
14169 const C: u32 = 42;
14170
14171 fn main() {
14172 println!("hello");
14173
14174 println!("world");
14175 }
14176 "#
14177 .unindent();
14178
14179 cx.set_state(
14180 &r#"
14181 use some::mod2;
14182
14183 const A: u32 = 42;
14184 const C: u32 = 42;
14185
14186 fn main(ˇ) {
14187 //println!("hello");
14188
14189 println!("world");
14190 //
14191 //
14192 }
14193 "#
14194 .unindent(),
14195 );
14196
14197 cx.set_head_text(&diff_base);
14198 executor.run_until_parked();
14199
14200 cx.update_editor(|editor, window, cx| {
14201 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14202 });
14203 executor.run_until_parked();
14204 cx.assert_state_with_diff(
14205 r#"
14206 - use some::mod1;
14207 use some::mod2;
14208
14209 const A: u32 = 42;
14210 - const B: u32 = 42;
14211 const C: u32 = 42;
14212
14213 fn main(ˇ) {
14214 - println!("hello");
14215 + //println!("hello");
14216
14217 println!("world");
14218 + //
14219 + //
14220 }
14221 "#
14222 .unindent(),
14223 );
14224
14225 cx.set_head_text("new diff base!");
14226 executor.run_until_parked();
14227 cx.assert_state_with_diff(
14228 r#"
14229 - new diff base!
14230 + use some::mod2;
14231 +
14232 + const A: u32 = 42;
14233 + const C: u32 = 42;
14234 +
14235 + fn main(ˇ) {
14236 + //println!("hello");
14237 +
14238 + println!("world");
14239 + //
14240 + //
14241 + }
14242 "#
14243 .unindent(),
14244 );
14245}
14246
14247#[gpui::test]
14248async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
14249 init_test(cx, |_| {});
14250
14251 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
14252 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
14253 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
14254 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
14255 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
14256 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
14257
14258 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
14259 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
14260 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
14261
14262 let multi_buffer = cx.new(|cx| {
14263 let mut multibuffer = MultiBuffer::new(ReadWrite);
14264 multibuffer.push_excerpts(
14265 buffer_1.clone(),
14266 [
14267 ExcerptRange {
14268 context: Point::new(0, 0)..Point::new(3, 0),
14269 primary: None,
14270 },
14271 ExcerptRange {
14272 context: Point::new(5, 0)..Point::new(7, 0),
14273 primary: None,
14274 },
14275 ExcerptRange {
14276 context: Point::new(9, 0)..Point::new(10, 3),
14277 primary: None,
14278 },
14279 ],
14280 cx,
14281 );
14282 multibuffer.push_excerpts(
14283 buffer_2.clone(),
14284 [
14285 ExcerptRange {
14286 context: Point::new(0, 0)..Point::new(3, 0),
14287 primary: None,
14288 },
14289 ExcerptRange {
14290 context: Point::new(5, 0)..Point::new(7, 0),
14291 primary: None,
14292 },
14293 ExcerptRange {
14294 context: Point::new(9, 0)..Point::new(10, 3),
14295 primary: None,
14296 },
14297 ],
14298 cx,
14299 );
14300 multibuffer.push_excerpts(
14301 buffer_3.clone(),
14302 [
14303 ExcerptRange {
14304 context: Point::new(0, 0)..Point::new(3, 0),
14305 primary: None,
14306 },
14307 ExcerptRange {
14308 context: Point::new(5, 0)..Point::new(7, 0),
14309 primary: None,
14310 },
14311 ExcerptRange {
14312 context: Point::new(9, 0)..Point::new(10, 3),
14313 primary: None,
14314 },
14315 ],
14316 cx,
14317 );
14318 multibuffer
14319 });
14320
14321 let editor =
14322 cx.add_window(|window, cx| Editor::new(EditorMode::Full, multi_buffer, None, window, cx));
14323 editor
14324 .update(cx, |editor, _window, cx| {
14325 for (buffer, diff_base) in [
14326 (buffer_1.clone(), file_1_old),
14327 (buffer_2.clone(), file_2_old),
14328 (buffer_3.clone(), file_3_old),
14329 ] {
14330 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
14331 editor
14332 .buffer
14333 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
14334 }
14335 })
14336 .unwrap();
14337
14338 let mut cx = EditorTestContext::for_editor(editor, cx).await;
14339 cx.run_until_parked();
14340
14341 cx.assert_editor_state(
14342 &"
14343 ˇaaa
14344 ccc
14345 ddd
14346
14347 ggg
14348 hhh
14349
14350
14351 lll
14352 mmm
14353 NNN
14354
14355 qqq
14356 rrr
14357
14358 uuu
14359 111
14360 222
14361 333
14362
14363 666
14364 777
14365
14366 000
14367 !!!"
14368 .unindent(),
14369 );
14370
14371 cx.update_editor(|editor, window, cx| {
14372 editor.select_all(&SelectAll, window, cx);
14373 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14374 });
14375 cx.executor().run_until_parked();
14376
14377 cx.assert_state_with_diff(
14378 "
14379 «aaa
14380 - bbb
14381 ccc
14382 ddd
14383
14384 ggg
14385 hhh
14386
14387
14388 lll
14389 mmm
14390 - nnn
14391 + NNN
14392
14393 qqq
14394 rrr
14395
14396 uuu
14397 111
14398 222
14399 333
14400
14401 + 666
14402 777
14403
14404 000
14405 !!!ˇ»"
14406 .unindent(),
14407 );
14408}
14409
14410#[gpui::test]
14411async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
14412 init_test(cx, |_| {});
14413
14414 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
14415 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
14416
14417 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
14418 let multi_buffer = cx.new(|cx| {
14419 let mut multibuffer = MultiBuffer::new(ReadWrite);
14420 multibuffer.push_excerpts(
14421 buffer.clone(),
14422 [
14423 ExcerptRange {
14424 context: Point::new(0, 0)..Point::new(2, 0),
14425 primary: None,
14426 },
14427 ExcerptRange {
14428 context: Point::new(4, 0)..Point::new(7, 0),
14429 primary: None,
14430 },
14431 ExcerptRange {
14432 context: Point::new(9, 0)..Point::new(10, 0),
14433 primary: None,
14434 },
14435 ],
14436 cx,
14437 );
14438 multibuffer
14439 });
14440
14441 let editor =
14442 cx.add_window(|window, cx| Editor::new(EditorMode::Full, multi_buffer, None, window, cx));
14443 editor
14444 .update(cx, |editor, _window, cx| {
14445 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
14446 editor
14447 .buffer
14448 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
14449 })
14450 .unwrap();
14451
14452 let mut cx = EditorTestContext::for_editor(editor, cx).await;
14453 cx.run_until_parked();
14454
14455 cx.update_editor(|editor, window, cx| {
14456 editor.expand_all_diff_hunks(&Default::default(), window, cx)
14457 });
14458 cx.executor().run_until_parked();
14459
14460 // When the start of a hunk coincides with the start of its excerpt,
14461 // the hunk is expanded. When the start of a a hunk is earlier than
14462 // the start of its excerpt, the hunk is not expanded.
14463 cx.assert_state_with_diff(
14464 "
14465 ˇaaa
14466 - bbb
14467 + BBB
14468
14469 - ddd
14470 - eee
14471 + DDD
14472 + EEE
14473 fff
14474
14475 iii
14476 "
14477 .unindent(),
14478 );
14479}
14480
14481#[gpui::test]
14482async fn test_edits_around_expanded_insertion_hunks(
14483 executor: BackgroundExecutor,
14484 cx: &mut TestAppContext,
14485) {
14486 init_test(cx, |_| {});
14487
14488 let mut cx = EditorTestContext::new(cx).await;
14489
14490 let diff_base = r#"
14491 use some::mod1;
14492 use some::mod2;
14493
14494 const A: u32 = 42;
14495
14496 fn main() {
14497 println!("hello");
14498
14499 println!("world");
14500 }
14501 "#
14502 .unindent();
14503 executor.run_until_parked();
14504 cx.set_state(
14505 &r#"
14506 use some::mod1;
14507 use some::mod2;
14508
14509 const A: u32 = 42;
14510 const B: u32 = 42;
14511 const C: u32 = 42;
14512 ˇ
14513
14514 fn main() {
14515 println!("hello");
14516
14517 println!("world");
14518 }
14519 "#
14520 .unindent(),
14521 );
14522
14523 cx.set_head_text(&diff_base);
14524 executor.run_until_parked();
14525
14526 cx.update_editor(|editor, window, cx| {
14527 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14528 });
14529 executor.run_until_parked();
14530
14531 cx.assert_state_with_diff(
14532 r#"
14533 use some::mod1;
14534 use some::mod2;
14535
14536 const A: u32 = 42;
14537 + const B: u32 = 42;
14538 + const C: u32 = 42;
14539 + ˇ
14540
14541 fn main() {
14542 println!("hello");
14543
14544 println!("world");
14545 }
14546 "#
14547 .unindent(),
14548 );
14549
14550 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
14551 executor.run_until_parked();
14552
14553 cx.assert_state_with_diff(
14554 r#"
14555 use some::mod1;
14556 use some::mod2;
14557
14558 const A: u32 = 42;
14559 + const B: u32 = 42;
14560 + const C: u32 = 42;
14561 + const D: u32 = 42;
14562 + ˇ
14563
14564 fn main() {
14565 println!("hello");
14566
14567 println!("world");
14568 }
14569 "#
14570 .unindent(),
14571 );
14572
14573 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
14574 executor.run_until_parked();
14575
14576 cx.assert_state_with_diff(
14577 r#"
14578 use some::mod1;
14579 use some::mod2;
14580
14581 const A: u32 = 42;
14582 + const B: u32 = 42;
14583 + const C: u32 = 42;
14584 + const D: u32 = 42;
14585 + const E: u32 = 42;
14586 + ˇ
14587
14588 fn main() {
14589 println!("hello");
14590
14591 println!("world");
14592 }
14593 "#
14594 .unindent(),
14595 );
14596
14597 cx.update_editor(|editor, window, cx| {
14598 editor.delete_line(&DeleteLine, window, cx);
14599 });
14600 executor.run_until_parked();
14601
14602 cx.assert_state_with_diff(
14603 r#"
14604 use some::mod1;
14605 use some::mod2;
14606
14607 const A: u32 = 42;
14608 + const B: u32 = 42;
14609 + const C: u32 = 42;
14610 + const D: u32 = 42;
14611 + const E: u32 = 42;
14612 ˇ
14613 fn main() {
14614 println!("hello");
14615
14616 println!("world");
14617 }
14618 "#
14619 .unindent(),
14620 );
14621
14622 cx.update_editor(|editor, window, cx| {
14623 editor.move_up(&MoveUp, window, cx);
14624 editor.delete_line(&DeleteLine, window, cx);
14625 editor.move_up(&MoveUp, window, cx);
14626 editor.delete_line(&DeleteLine, window, cx);
14627 editor.move_up(&MoveUp, window, cx);
14628 editor.delete_line(&DeleteLine, window, cx);
14629 });
14630 executor.run_until_parked();
14631 cx.assert_state_with_diff(
14632 r#"
14633 use some::mod1;
14634 use some::mod2;
14635
14636 const A: u32 = 42;
14637 + const B: u32 = 42;
14638 ˇ
14639 fn main() {
14640 println!("hello");
14641
14642 println!("world");
14643 }
14644 "#
14645 .unindent(),
14646 );
14647
14648 cx.update_editor(|editor, window, cx| {
14649 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
14650 editor.delete_line(&DeleteLine, window, cx);
14651 });
14652 executor.run_until_parked();
14653 cx.assert_state_with_diff(
14654 r#"
14655 ˇ
14656 fn main() {
14657 println!("hello");
14658
14659 println!("world");
14660 }
14661 "#
14662 .unindent(),
14663 );
14664}
14665
14666#[gpui::test]
14667async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
14668 init_test(cx, |_| {});
14669
14670 let mut cx = EditorTestContext::new(cx).await;
14671 cx.set_head_text(indoc! { "
14672 one
14673 two
14674 three
14675 four
14676 five
14677 "
14678 });
14679 cx.set_state(indoc! { "
14680 one
14681 ˇthree
14682 five
14683 "});
14684 cx.run_until_parked();
14685 cx.update_editor(|editor, window, cx| {
14686 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14687 });
14688 cx.assert_state_with_diff(
14689 indoc! { "
14690 one
14691 - two
14692 ˇthree
14693 - four
14694 five
14695 "}
14696 .to_string(),
14697 );
14698 cx.update_editor(|editor, window, cx| {
14699 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14700 });
14701
14702 cx.assert_state_with_diff(
14703 indoc! { "
14704 one
14705 ˇthree
14706 five
14707 "}
14708 .to_string(),
14709 );
14710
14711 cx.set_state(indoc! { "
14712 one
14713 ˇTWO
14714 three
14715 four
14716 five
14717 "});
14718 cx.run_until_parked();
14719 cx.update_editor(|editor, window, cx| {
14720 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14721 });
14722
14723 cx.assert_state_with_diff(
14724 indoc! { "
14725 one
14726 - two
14727 + ˇTWO
14728 three
14729 four
14730 five
14731 "}
14732 .to_string(),
14733 );
14734 cx.update_editor(|editor, window, cx| {
14735 editor.move_up(&Default::default(), window, cx);
14736 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14737 });
14738 cx.assert_state_with_diff(
14739 indoc! { "
14740 one
14741 ˇTWO
14742 three
14743 four
14744 five
14745 "}
14746 .to_string(),
14747 );
14748}
14749
14750#[gpui::test]
14751async fn test_edits_around_expanded_deletion_hunks(
14752 executor: BackgroundExecutor,
14753 cx: &mut TestAppContext,
14754) {
14755 init_test(cx, |_| {});
14756
14757 let mut cx = EditorTestContext::new(cx).await;
14758
14759 let diff_base = r#"
14760 use some::mod1;
14761 use some::mod2;
14762
14763 const A: u32 = 42;
14764 const B: u32 = 42;
14765 const C: u32 = 42;
14766
14767
14768 fn main() {
14769 println!("hello");
14770
14771 println!("world");
14772 }
14773 "#
14774 .unindent();
14775 executor.run_until_parked();
14776 cx.set_state(
14777 &r#"
14778 use some::mod1;
14779 use some::mod2;
14780
14781 ˇconst B: u32 = 42;
14782 const C: u32 = 42;
14783
14784
14785 fn main() {
14786 println!("hello");
14787
14788 println!("world");
14789 }
14790 "#
14791 .unindent(),
14792 );
14793
14794 cx.set_head_text(&diff_base);
14795 executor.run_until_parked();
14796
14797 cx.update_editor(|editor, window, cx| {
14798 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14799 });
14800 executor.run_until_parked();
14801
14802 cx.assert_state_with_diff(
14803 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
14811
14812 fn main() {
14813 println!("hello");
14814
14815 println!("world");
14816 }
14817 "#
14818 .unindent(),
14819 );
14820
14821 cx.update_editor(|editor, window, cx| {
14822 editor.delete_line(&DeleteLine, window, cx);
14823 });
14824 executor.run_until_parked();
14825 cx.assert_state_with_diff(
14826 r#"
14827 use some::mod1;
14828 use some::mod2;
14829
14830 - const A: u32 = 42;
14831 - const B: u32 = 42;
14832 ˇconst C: u32 = 42;
14833
14834
14835 fn main() {
14836 println!("hello");
14837
14838 println!("world");
14839 }
14840 "#
14841 .unindent(),
14842 );
14843
14844 cx.update_editor(|editor, window, cx| {
14845 editor.delete_line(&DeleteLine, window, cx);
14846 });
14847 executor.run_until_parked();
14848 cx.assert_state_with_diff(
14849 r#"
14850 use some::mod1;
14851 use some::mod2;
14852
14853 - const A: u32 = 42;
14854 - const B: u32 = 42;
14855 - const C: u32 = 42;
14856 ˇ
14857
14858 fn main() {
14859 println!("hello");
14860
14861 println!("world");
14862 }
14863 "#
14864 .unindent(),
14865 );
14866
14867 cx.update_editor(|editor, window, cx| {
14868 editor.handle_input("replacement", window, cx);
14869 });
14870 executor.run_until_parked();
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 -
14880 + replacementˇ
14881
14882 fn main() {
14883 println!("hello");
14884
14885 println!("world");
14886 }
14887 "#
14888 .unindent(),
14889 );
14890}
14891
14892#[gpui::test]
14893async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14894 init_test(cx, |_| {});
14895
14896 let mut cx = EditorTestContext::new(cx).await;
14897
14898 let base_text = r#"
14899 one
14900 two
14901 three
14902 four
14903 five
14904 "#
14905 .unindent();
14906 executor.run_until_parked();
14907 cx.set_state(
14908 &r#"
14909 one
14910 two
14911 fˇour
14912 five
14913 "#
14914 .unindent(),
14915 );
14916
14917 cx.set_head_text(&base_text);
14918 executor.run_until_parked();
14919
14920 cx.update_editor(|editor, window, cx| {
14921 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14922 });
14923 executor.run_until_parked();
14924
14925 cx.assert_state_with_diff(
14926 r#"
14927 one
14928 two
14929 - three
14930 fˇour
14931 five
14932 "#
14933 .unindent(),
14934 );
14935
14936 cx.update_editor(|editor, window, cx| {
14937 editor.backspace(&Backspace, window, cx);
14938 editor.backspace(&Backspace, window, cx);
14939 });
14940 executor.run_until_parked();
14941 cx.assert_state_with_diff(
14942 r#"
14943 one
14944 two
14945 - threeˇ
14946 - four
14947 + our
14948 five
14949 "#
14950 .unindent(),
14951 );
14952}
14953
14954#[gpui::test]
14955async fn test_edit_after_expanded_modification_hunk(
14956 executor: BackgroundExecutor,
14957 cx: &mut TestAppContext,
14958) {
14959 init_test(cx, |_| {});
14960
14961 let mut cx = EditorTestContext::new(cx).await;
14962
14963 let diff_base = r#"
14964 use some::mod1;
14965 use some::mod2;
14966
14967 const A: u32 = 42;
14968 const B: u32 = 42;
14969 const C: u32 = 42;
14970 const D: u32 = 42;
14971
14972
14973 fn main() {
14974 println!("hello");
14975
14976 println!("world");
14977 }"#
14978 .unindent();
14979
14980 cx.set_state(
14981 &r#"
14982 use some::mod1;
14983 use some::mod2;
14984
14985 const A: u32 = 42;
14986 const B: u32 = 42;
14987 const C: u32 = 43ˇ
14988 const D: u32 = 42;
14989
14990
14991 fn main() {
14992 println!("hello");
14993
14994 println!("world");
14995 }"#
14996 .unindent(),
14997 );
14998
14999 cx.set_head_text(&diff_base);
15000 executor.run_until_parked();
15001 cx.update_editor(|editor, window, cx| {
15002 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15003 });
15004 executor.run_until_parked();
15005
15006 cx.assert_state_with_diff(
15007 r#"
15008 use some::mod1;
15009 use some::mod2;
15010
15011 const A: u32 = 42;
15012 const B: u32 = 42;
15013 - const C: u32 = 42;
15014 + const C: u32 = 43ˇ
15015 const D: u32 = 42;
15016
15017
15018 fn main() {
15019 println!("hello");
15020
15021 println!("world");
15022 }"#
15023 .unindent(),
15024 );
15025
15026 cx.update_editor(|editor, window, cx| {
15027 editor.handle_input("\nnew_line\n", window, cx);
15028 });
15029 executor.run_until_parked();
15030
15031 cx.assert_state_with_diff(
15032 r#"
15033 use some::mod1;
15034 use some::mod2;
15035
15036 const A: u32 = 42;
15037 const B: u32 = 42;
15038 - const C: u32 = 42;
15039 + const C: u32 = 43
15040 + new_line
15041 + ˇ
15042 const D: u32 = 42;
15043
15044
15045 fn main() {
15046 println!("hello");
15047
15048 println!("world");
15049 }"#
15050 .unindent(),
15051 );
15052}
15053
15054#[gpui::test]
15055async fn test_stage_and_unstage_added_file_hunk(
15056 executor: BackgroundExecutor,
15057 cx: &mut TestAppContext,
15058) {
15059 init_test(cx, |_| {});
15060
15061 let mut cx = EditorTestContext::new(cx).await;
15062 cx.update_editor(|editor, _, cx| {
15063 editor.set_expand_all_diff_hunks(cx);
15064 });
15065
15066 let working_copy = r#"
15067 ˇfn main() {
15068 println!("hello, world!");
15069 }
15070 "#
15071 .unindent();
15072
15073 cx.set_state(&working_copy);
15074 executor.run_until_parked();
15075
15076 cx.assert_state_with_diff(
15077 r#"
15078 + ˇfn main() {
15079 + println!("hello, world!");
15080 + }
15081 "#
15082 .unindent(),
15083 );
15084 cx.assert_index_text(None);
15085
15086 cx.update_editor(|editor, window, cx| {
15087 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
15088 });
15089 executor.run_until_parked();
15090 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
15091 cx.assert_state_with_diff(
15092 r#"
15093 + ˇfn main() {
15094 + println!("hello, world!");
15095 + }
15096 "#
15097 .unindent(),
15098 );
15099
15100 cx.update_editor(|editor, window, cx| {
15101 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
15102 });
15103 executor.run_until_parked();
15104 cx.assert_index_text(None);
15105}
15106
15107async fn setup_indent_guides_editor(
15108 text: &str,
15109 cx: &mut TestAppContext,
15110) -> (BufferId, EditorTestContext) {
15111 init_test(cx, |_| {});
15112
15113 let mut cx = EditorTestContext::new(cx).await;
15114
15115 let buffer_id = cx.update_editor(|editor, window, cx| {
15116 editor.set_text(text, window, cx);
15117 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
15118
15119 buffer_ids[0]
15120 });
15121
15122 (buffer_id, cx)
15123}
15124
15125fn assert_indent_guides(
15126 range: Range<u32>,
15127 expected: Vec<IndentGuide>,
15128 active_indices: Option<Vec<usize>>,
15129 cx: &mut EditorTestContext,
15130) {
15131 let indent_guides = cx.update_editor(|editor, window, cx| {
15132 let snapshot = editor.snapshot(window, cx).display_snapshot;
15133 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
15134 editor,
15135 MultiBufferRow(range.start)..MultiBufferRow(range.end),
15136 true,
15137 &snapshot,
15138 cx,
15139 );
15140
15141 indent_guides.sort_by(|a, b| {
15142 a.depth.cmp(&b.depth).then(
15143 a.start_row
15144 .cmp(&b.start_row)
15145 .then(a.end_row.cmp(&b.end_row)),
15146 )
15147 });
15148 indent_guides
15149 });
15150
15151 if let Some(expected) = active_indices {
15152 let active_indices = cx.update_editor(|editor, window, cx| {
15153 let snapshot = editor.snapshot(window, cx).display_snapshot;
15154 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
15155 });
15156
15157 assert_eq!(
15158 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
15159 expected,
15160 "Active indent guide indices do not match"
15161 );
15162 }
15163
15164 assert_eq!(indent_guides, expected, "Indent guides do not match");
15165}
15166
15167fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
15168 IndentGuide {
15169 buffer_id,
15170 start_row: MultiBufferRow(start_row),
15171 end_row: MultiBufferRow(end_row),
15172 depth,
15173 tab_size: 4,
15174 settings: IndentGuideSettings {
15175 enabled: true,
15176 line_width: 1,
15177 active_line_width: 1,
15178 ..Default::default()
15179 },
15180 }
15181}
15182
15183#[gpui::test]
15184async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
15185 let (buffer_id, mut cx) = setup_indent_guides_editor(
15186 &"
15187 fn main() {
15188 let a = 1;
15189 }"
15190 .unindent(),
15191 cx,
15192 )
15193 .await;
15194
15195 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
15196}
15197
15198#[gpui::test]
15199async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
15200 let (buffer_id, mut cx) = setup_indent_guides_editor(
15201 &"
15202 fn main() {
15203 let a = 1;
15204 let b = 2;
15205 }"
15206 .unindent(),
15207 cx,
15208 )
15209 .await;
15210
15211 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
15212}
15213
15214#[gpui::test]
15215async fn test_indent_guide_nested(cx: &mut TestAppContext) {
15216 let (buffer_id, mut cx) = setup_indent_guides_editor(
15217 &"
15218 fn main() {
15219 let a = 1;
15220 if a == 3 {
15221 let b = 2;
15222 } else {
15223 let c = 3;
15224 }
15225 }"
15226 .unindent(),
15227 cx,
15228 )
15229 .await;
15230
15231 assert_indent_guides(
15232 0..8,
15233 vec![
15234 indent_guide(buffer_id, 1, 6, 0),
15235 indent_guide(buffer_id, 3, 3, 1),
15236 indent_guide(buffer_id, 5, 5, 1),
15237 ],
15238 None,
15239 &mut cx,
15240 );
15241}
15242
15243#[gpui::test]
15244async fn test_indent_guide_tab(cx: &mut TestAppContext) {
15245 let (buffer_id, mut cx) = setup_indent_guides_editor(
15246 &"
15247 fn main() {
15248 let a = 1;
15249 let b = 2;
15250 let c = 3;
15251 }"
15252 .unindent(),
15253 cx,
15254 )
15255 .await;
15256
15257 assert_indent_guides(
15258 0..5,
15259 vec![
15260 indent_guide(buffer_id, 1, 3, 0),
15261 indent_guide(buffer_id, 2, 2, 1),
15262 ],
15263 None,
15264 &mut cx,
15265 );
15266}
15267
15268#[gpui::test]
15269async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
15270 let (buffer_id, mut cx) = setup_indent_guides_editor(
15271 &"
15272 fn main() {
15273 let a = 1;
15274
15275 let c = 3;
15276 }"
15277 .unindent(),
15278 cx,
15279 )
15280 .await;
15281
15282 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
15283}
15284
15285#[gpui::test]
15286async fn test_indent_guide_complex(cx: &mut TestAppContext) {
15287 let (buffer_id, mut cx) = setup_indent_guides_editor(
15288 &"
15289 fn main() {
15290 let a = 1;
15291
15292 let c = 3;
15293
15294 if a == 3 {
15295 let b = 2;
15296 } else {
15297 let c = 3;
15298 }
15299 }"
15300 .unindent(),
15301 cx,
15302 )
15303 .await;
15304
15305 assert_indent_guides(
15306 0..11,
15307 vec![
15308 indent_guide(buffer_id, 1, 9, 0),
15309 indent_guide(buffer_id, 6, 6, 1),
15310 indent_guide(buffer_id, 8, 8, 1),
15311 ],
15312 None,
15313 &mut cx,
15314 );
15315}
15316
15317#[gpui::test]
15318async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
15319 let (buffer_id, mut cx) = setup_indent_guides_editor(
15320 &"
15321 fn main() {
15322 let a = 1;
15323
15324 let c = 3;
15325
15326 if a == 3 {
15327 let b = 2;
15328 } else {
15329 let c = 3;
15330 }
15331 }"
15332 .unindent(),
15333 cx,
15334 )
15335 .await;
15336
15337 assert_indent_guides(
15338 1..11,
15339 vec![
15340 indent_guide(buffer_id, 1, 9, 0),
15341 indent_guide(buffer_id, 6, 6, 1),
15342 indent_guide(buffer_id, 8, 8, 1),
15343 ],
15344 None,
15345 &mut cx,
15346 );
15347}
15348
15349#[gpui::test]
15350async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
15351 let (buffer_id, mut cx) = setup_indent_guides_editor(
15352 &"
15353 fn main() {
15354 let a = 1;
15355
15356 let c = 3;
15357
15358 if a == 3 {
15359 let b = 2;
15360 } else {
15361 let c = 3;
15362 }
15363 }"
15364 .unindent(),
15365 cx,
15366 )
15367 .await;
15368
15369 assert_indent_guides(
15370 1..10,
15371 vec![
15372 indent_guide(buffer_id, 1, 9, 0),
15373 indent_guide(buffer_id, 6, 6, 1),
15374 indent_guide(buffer_id, 8, 8, 1),
15375 ],
15376 None,
15377 &mut cx,
15378 );
15379}
15380
15381#[gpui::test]
15382async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
15383 let (buffer_id, mut cx) = setup_indent_guides_editor(
15384 &"
15385 block1
15386 block2
15387 block3
15388 block4
15389 block2
15390 block1
15391 block1"
15392 .unindent(),
15393 cx,
15394 )
15395 .await;
15396
15397 assert_indent_guides(
15398 1..10,
15399 vec![
15400 indent_guide(buffer_id, 1, 4, 0),
15401 indent_guide(buffer_id, 2, 3, 1),
15402 indent_guide(buffer_id, 3, 3, 2),
15403 ],
15404 None,
15405 &mut cx,
15406 );
15407}
15408
15409#[gpui::test]
15410async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
15411 let (buffer_id, mut cx) = setup_indent_guides_editor(
15412 &"
15413 block1
15414 block2
15415 block3
15416
15417 block1
15418 block1"
15419 .unindent(),
15420 cx,
15421 )
15422 .await;
15423
15424 assert_indent_guides(
15425 0..6,
15426 vec![
15427 indent_guide(buffer_id, 1, 2, 0),
15428 indent_guide(buffer_id, 2, 2, 1),
15429 ],
15430 None,
15431 &mut cx,
15432 );
15433}
15434
15435#[gpui::test]
15436async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
15437 let (buffer_id, mut cx) = setup_indent_guides_editor(
15438 &"
15439 block1
15440
15441
15442
15443 block2
15444 "
15445 .unindent(),
15446 cx,
15447 )
15448 .await;
15449
15450 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
15451}
15452
15453#[gpui::test]
15454async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
15455 let (buffer_id, mut cx) = setup_indent_guides_editor(
15456 &"
15457 def a:
15458 \tb = 3
15459 \tif True:
15460 \t\tc = 4
15461 \t\td = 5
15462 \tprint(b)
15463 "
15464 .unindent(),
15465 cx,
15466 )
15467 .await;
15468
15469 assert_indent_guides(
15470 0..6,
15471 vec![
15472 indent_guide(buffer_id, 1, 6, 0),
15473 indent_guide(buffer_id, 3, 4, 1),
15474 ],
15475 None,
15476 &mut cx,
15477 );
15478}
15479
15480#[gpui::test]
15481async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
15482 let (buffer_id, mut cx) = setup_indent_guides_editor(
15483 &"
15484 fn main() {
15485 let a = 1;
15486 }"
15487 .unindent(),
15488 cx,
15489 )
15490 .await;
15491
15492 cx.update_editor(|editor, window, cx| {
15493 editor.change_selections(None, window, cx, |s| {
15494 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
15495 });
15496 });
15497
15498 assert_indent_guides(
15499 0..3,
15500 vec![indent_guide(buffer_id, 1, 1, 0)],
15501 Some(vec![0]),
15502 &mut cx,
15503 );
15504}
15505
15506#[gpui::test]
15507async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
15508 let (buffer_id, mut cx) = setup_indent_guides_editor(
15509 &"
15510 fn main() {
15511 if 1 == 2 {
15512 let a = 1;
15513 }
15514 }"
15515 .unindent(),
15516 cx,
15517 )
15518 .await;
15519
15520 cx.update_editor(|editor, window, cx| {
15521 editor.change_selections(None, window, cx, |s| {
15522 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
15523 });
15524 });
15525
15526 assert_indent_guides(
15527 0..4,
15528 vec![
15529 indent_guide(buffer_id, 1, 3, 0),
15530 indent_guide(buffer_id, 2, 2, 1),
15531 ],
15532 Some(vec![1]),
15533 &mut cx,
15534 );
15535
15536 cx.update_editor(|editor, window, cx| {
15537 editor.change_selections(None, window, cx, |s| {
15538 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
15539 });
15540 });
15541
15542 assert_indent_guides(
15543 0..4,
15544 vec![
15545 indent_guide(buffer_id, 1, 3, 0),
15546 indent_guide(buffer_id, 2, 2, 1),
15547 ],
15548 Some(vec![1]),
15549 &mut cx,
15550 );
15551
15552 cx.update_editor(|editor, window, cx| {
15553 editor.change_selections(None, window, cx, |s| {
15554 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
15555 });
15556 });
15557
15558 assert_indent_guides(
15559 0..4,
15560 vec![
15561 indent_guide(buffer_id, 1, 3, 0),
15562 indent_guide(buffer_id, 2, 2, 1),
15563 ],
15564 Some(vec![0]),
15565 &mut cx,
15566 );
15567}
15568
15569#[gpui::test]
15570async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
15571 let (buffer_id, mut cx) = setup_indent_guides_editor(
15572 &"
15573 fn main() {
15574 let a = 1;
15575
15576 let b = 2;
15577 }"
15578 .unindent(),
15579 cx,
15580 )
15581 .await;
15582
15583 cx.update_editor(|editor, window, cx| {
15584 editor.change_selections(None, window, cx, |s| {
15585 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
15586 });
15587 });
15588
15589 assert_indent_guides(
15590 0..5,
15591 vec![indent_guide(buffer_id, 1, 3, 0)],
15592 Some(vec![0]),
15593 &mut cx,
15594 );
15595}
15596
15597#[gpui::test]
15598async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
15599 let (buffer_id, mut cx) = setup_indent_guides_editor(
15600 &"
15601 def m:
15602 a = 1
15603 pass"
15604 .unindent(),
15605 cx,
15606 )
15607 .await;
15608
15609 cx.update_editor(|editor, window, cx| {
15610 editor.change_selections(None, window, cx, |s| {
15611 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
15612 });
15613 });
15614
15615 assert_indent_guides(
15616 0..3,
15617 vec![indent_guide(buffer_id, 1, 2, 0)],
15618 Some(vec![0]),
15619 &mut cx,
15620 );
15621}
15622
15623#[gpui::test]
15624async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
15625 init_test(cx, |_| {});
15626 let mut cx = EditorTestContext::new(cx).await;
15627 let text = indoc! {
15628 "
15629 impl A {
15630 fn b() {
15631 0;
15632 3;
15633 5;
15634 6;
15635 7;
15636 }
15637 }
15638 "
15639 };
15640 let base_text = indoc! {
15641 "
15642 impl A {
15643 fn b() {
15644 0;
15645 1;
15646 2;
15647 3;
15648 4;
15649 }
15650 fn c() {
15651 5;
15652 6;
15653 7;
15654 }
15655 }
15656 "
15657 };
15658
15659 cx.update_editor(|editor, window, cx| {
15660 editor.set_text(text, window, cx);
15661
15662 editor.buffer().update(cx, |multibuffer, cx| {
15663 let buffer = multibuffer.as_singleton().unwrap();
15664 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
15665
15666 multibuffer.set_all_diff_hunks_expanded(cx);
15667 multibuffer.add_diff(diff, cx);
15668
15669 buffer.read(cx).remote_id()
15670 })
15671 });
15672 cx.run_until_parked();
15673
15674 cx.assert_state_with_diff(
15675 indoc! { "
15676 impl A {
15677 fn b() {
15678 0;
15679 - 1;
15680 - 2;
15681 3;
15682 - 4;
15683 - }
15684 - fn c() {
15685 5;
15686 6;
15687 7;
15688 }
15689 }
15690 ˇ"
15691 }
15692 .to_string(),
15693 );
15694
15695 let mut actual_guides = cx.update_editor(|editor, window, cx| {
15696 editor
15697 .snapshot(window, cx)
15698 .buffer_snapshot
15699 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
15700 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
15701 .collect::<Vec<_>>()
15702 });
15703 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
15704 assert_eq!(
15705 actual_guides,
15706 vec![
15707 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
15708 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
15709 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
15710 ]
15711 );
15712}
15713
15714#[gpui::test]
15715async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
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 ˇA
15729 b
15730 C
15731 "#
15732 .unindent(),
15733 );
15734 cx.set_head_text(&diff_base);
15735 cx.update_editor(|editor, window, cx| {
15736 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15737 });
15738 executor.run_until_parked();
15739
15740 let both_hunks_expanded = r#"
15741 - a
15742 + ˇA
15743 b
15744 - c
15745 + C
15746 "#
15747 .unindent();
15748
15749 cx.assert_state_with_diff(both_hunks_expanded.clone());
15750
15751 let hunk_ranges = cx.update_editor(|editor, window, cx| {
15752 let snapshot = editor.snapshot(window, cx);
15753 let hunks = editor
15754 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
15755 .collect::<Vec<_>>();
15756 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
15757 let buffer_id = hunks[0].buffer_id;
15758 hunks
15759 .into_iter()
15760 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
15761 .collect::<Vec<_>>()
15762 });
15763 assert_eq!(hunk_ranges.len(), 2);
15764
15765 cx.update_editor(|editor, _, cx| {
15766 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
15767 });
15768 executor.run_until_parked();
15769
15770 let second_hunk_expanded = r#"
15771 ˇA
15772 b
15773 - c
15774 + C
15775 "#
15776 .unindent();
15777
15778 cx.assert_state_with_diff(second_hunk_expanded);
15779
15780 cx.update_editor(|editor, _, cx| {
15781 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
15782 });
15783 executor.run_until_parked();
15784
15785 cx.assert_state_with_diff(both_hunks_expanded.clone());
15786
15787 cx.update_editor(|editor, _, cx| {
15788 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
15789 });
15790 executor.run_until_parked();
15791
15792 let first_hunk_expanded = r#"
15793 - a
15794 + ˇA
15795 b
15796 C
15797 "#
15798 .unindent();
15799
15800 cx.assert_state_with_diff(first_hunk_expanded);
15801
15802 cx.update_editor(|editor, _, cx| {
15803 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
15804 });
15805 executor.run_until_parked();
15806
15807 cx.assert_state_with_diff(both_hunks_expanded);
15808
15809 cx.set_state(
15810 &r#"
15811 ˇA
15812 b
15813 "#
15814 .unindent(),
15815 );
15816 cx.run_until_parked();
15817
15818 // TODO this cursor position seems bad
15819 cx.assert_state_with_diff(
15820 r#"
15821 - ˇa
15822 + A
15823 b
15824 "#
15825 .unindent(),
15826 );
15827
15828 cx.update_editor(|editor, window, cx| {
15829 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15830 });
15831
15832 cx.assert_state_with_diff(
15833 r#"
15834 - ˇa
15835 + A
15836 b
15837 - c
15838 "#
15839 .unindent(),
15840 );
15841
15842 let hunk_ranges = cx.update_editor(|editor, window, cx| {
15843 let snapshot = editor.snapshot(window, cx);
15844 let hunks = editor
15845 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
15846 .collect::<Vec<_>>();
15847 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
15848 let buffer_id = hunks[0].buffer_id;
15849 hunks
15850 .into_iter()
15851 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
15852 .collect::<Vec<_>>()
15853 });
15854 assert_eq!(hunk_ranges.len(), 2);
15855
15856 cx.update_editor(|editor, _, cx| {
15857 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
15858 });
15859 executor.run_until_parked();
15860
15861 cx.assert_state_with_diff(
15862 r#"
15863 - ˇa
15864 + A
15865 b
15866 "#
15867 .unindent(),
15868 );
15869}
15870
15871#[gpui::test]
15872async fn test_toggle_deletion_hunk_at_start_of_file(
15873 executor: BackgroundExecutor,
15874 cx: &mut TestAppContext,
15875) {
15876 init_test(cx, |_| {});
15877 let mut cx = EditorTestContext::new(cx).await;
15878
15879 let diff_base = r#"
15880 a
15881 b
15882 c
15883 "#
15884 .unindent();
15885
15886 cx.set_state(
15887 &r#"
15888 ˇb
15889 c
15890 "#
15891 .unindent(),
15892 );
15893 cx.set_head_text(&diff_base);
15894 cx.update_editor(|editor, window, cx| {
15895 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15896 });
15897 executor.run_until_parked();
15898
15899 let hunk_expanded = r#"
15900 - a
15901 ˇb
15902 c
15903 "#
15904 .unindent();
15905
15906 cx.assert_state_with_diff(hunk_expanded.clone());
15907
15908 let hunk_ranges = cx.update_editor(|editor, window, cx| {
15909 let snapshot = editor.snapshot(window, cx);
15910 let hunks = editor
15911 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
15912 .collect::<Vec<_>>();
15913 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
15914 let buffer_id = hunks[0].buffer_id;
15915 hunks
15916 .into_iter()
15917 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
15918 .collect::<Vec<_>>()
15919 });
15920 assert_eq!(hunk_ranges.len(), 1);
15921
15922 cx.update_editor(|editor, _, cx| {
15923 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
15924 });
15925 executor.run_until_parked();
15926
15927 let hunk_collapsed = r#"
15928 ˇb
15929 c
15930 "#
15931 .unindent();
15932
15933 cx.assert_state_with_diff(hunk_collapsed);
15934
15935 cx.update_editor(|editor, _, cx| {
15936 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
15937 });
15938 executor.run_until_parked();
15939
15940 cx.assert_state_with_diff(hunk_expanded.clone());
15941}
15942
15943#[gpui::test]
15944async fn test_display_diff_hunks(cx: &mut TestAppContext) {
15945 init_test(cx, |_| {});
15946
15947 let fs = FakeFs::new(cx.executor());
15948 fs.insert_tree(
15949 path!("/test"),
15950 json!({
15951 ".git": {},
15952 "file-1": "ONE\n",
15953 "file-2": "TWO\n",
15954 "file-3": "THREE\n",
15955 }),
15956 )
15957 .await;
15958
15959 fs.set_head_for_repo(
15960 path!("/test/.git").as_ref(),
15961 &[
15962 ("file-1".into(), "one\n".into()),
15963 ("file-2".into(), "two\n".into()),
15964 ("file-3".into(), "three\n".into()),
15965 ],
15966 );
15967
15968 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
15969 let mut buffers = vec![];
15970 for i in 1..=3 {
15971 let buffer = project
15972 .update(cx, |project, cx| {
15973 let path = format!(path!("/test/file-{}"), i);
15974 project.open_local_buffer(path, cx)
15975 })
15976 .await
15977 .unwrap();
15978 buffers.push(buffer);
15979 }
15980
15981 let multibuffer = cx.new(|cx| {
15982 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
15983 multibuffer.set_all_diff_hunks_expanded(cx);
15984 for buffer in &buffers {
15985 let snapshot = buffer.read(cx).snapshot();
15986 multibuffer.set_excerpts_for_path(
15987 PathKey::namespaced("", buffer.read(cx).file().unwrap().path().clone()),
15988 buffer.clone(),
15989 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
15990 DEFAULT_MULTIBUFFER_CONTEXT,
15991 cx,
15992 );
15993 }
15994 multibuffer
15995 });
15996
15997 let editor = cx.add_window(|window, cx| {
15998 Editor::new(EditorMode::Full, multibuffer, Some(project), window, cx)
15999 });
16000 cx.run_until_parked();
16001
16002 let snapshot = editor
16003 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16004 .unwrap();
16005 let hunks = snapshot
16006 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
16007 .map(|hunk| match hunk {
16008 DisplayDiffHunk::Unfolded {
16009 display_row_range, ..
16010 } => display_row_range,
16011 DisplayDiffHunk::Folded { .. } => unreachable!(),
16012 })
16013 .collect::<Vec<_>>();
16014 assert_eq!(
16015 hunks,
16016 [
16017 DisplayRow(2)..DisplayRow(4),
16018 DisplayRow(7)..DisplayRow(9),
16019 DisplayRow(12)..DisplayRow(14),
16020 ]
16021 );
16022}
16023
16024#[gpui::test]
16025async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
16026 init_test(cx, |_| {});
16027
16028 let mut cx = EditorTestContext::new(cx).await;
16029 cx.set_head_text(indoc! { "
16030 one
16031 two
16032 three
16033 four
16034 five
16035 "
16036 });
16037 cx.set_index_text(indoc! { "
16038 one
16039 two
16040 three
16041 four
16042 five
16043 "
16044 });
16045 cx.set_state(indoc! {"
16046 one
16047 TWO
16048 ˇTHREE
16049 FOUR
16050 five
16051 "});
16052 cx.run_until_parked();
16053 cx.update_editor(|editor, window, cx| {
16054 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16055 });
16056 cx.run_until_parked();
16057 cx.assert_index_text(Some(indoc! {"
16058 one
16059 TWO
16060 THREE
16061 FOUR
16062 five
16063 "}));
16064 cx.set_state(indoc! { "
16065 one
16066 TWO
16067 ˇTHREE-HUNDRED
16068 FOUR
16069 five
16070 "});
16071 cx.run_until_parked();
16072 cx.update_editor(|editor, window, cx| {
16073 let snapshot = editor.snapshot(window, cx);
16074 let hunks = editor
16075 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16076 .collect::<Vec<_>>();
16077 assert_eq!(hunks.len(), 1);
16078 assert_eq!(
16079 hunks[0].status(),
16080 DiffHunkStatus {
16081 kind: DiffHunkStatusKind::Modified,
16082 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
16083 }
16084 );
16085
16086 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16087 });
16088 cx.run_until_parked();
16089 cx.assert_index_text(Some(indoc! {"
16090 one
16091 TWO
16092 THREE-HUNDRED
16093 FOUR
16094 five
16095 "}));
16096}
16097
16098#[gpui::test]
16099fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
16100 init_test(cx, |_| {});
16101
16102 let editor = cx.add_window(|window, cx| {
16103 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
16104 build_editor(buffer, window, cx)
16105 });
16106
16107 let render_args = Arc::new(Mutex::new(None));
16108 let snapshot = editor
16109 .update(cx, |editor, window, cx| {
16110 let snapshot = editor.buffer().read(cx).snapshot(cx);
16111 let range =
16112 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
16113
16114 struct RenderArgs {
16115 row: MultiBufferRow,
16116 folded: bool,
16117 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
16118 }
16119
16120 let crease = Crease::inline(
16121 range,
16122 FoldPlaceholder::test(),
16123 {
16124 let toggle_callback = render_args.clone();
16125 move |row, folded, callback, _window, _cx| {
16126 *toggle_callback.lock() = Some(RenderArgs {
16127 row,
16128 folded,
16129 callback,
16130 });
16131 div()
16132 }
16133 },
16134 |_row, _folded, _window, _cx| div(),
16135 );
16136
16137 editor.insert_creases(Some(crease), cx);
16138 let snapshot = editor.snapshot(window, cx);
16139 let _div = snapshot.render_crease_toggle(
16140 MultiBufferRow(1),
16141 false,
16142 cx.entity().clone(),
16143 window,
16144 cx,
16145 );
16146 snapshot
16147 })
16148 .unwrap();
16149
16150 let render_args = render_args.lock().take().unwrap();
16151 assert_eq!(render_args.row, MultiBufferRow(1));
16152 assert!(!render_args.folded);
16153 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
16154
16155 cx.update_window(*editor, |_, window, cx| {
16156 (render_args.callback)(true, window, cx)
16157 })
16158 .unwrap();
16159 let snapshot = editor
16160 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16161 .unwrap();
16162 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
16163
16164 cx.update_window(*editor, |_, window, cx| {
16165 (render_args.callback)(false, window, cx)
16166 })
16167 .unwrap();
16168 let snapshot = editor
16169 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16170 .unwrap();
16171 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
16172}
16173
16174#[gpui::test]
16175async fn test_input_text(cx: &mut TestAppContext) {
16176 init_test(cx, |_| {});
16177 let mut cx = EditorTestContext::new(cx).await;
16178
16179 cx.set_state(
16180 &r#"ˇone
16181 two
16182
16183 three
16184 fourˇ
16185 five
16186
16187 siˇx"#
16188 .unindent(),
16189 );
16190
16191 cx.dispatch_action(HandleInput(String::new()));
16192 cx.assert_editor_state(
16193 &r#"ˇone
16194 two
16195
16196 three
16197 fourˇ
16198 five
16199
16200 siˇx"#
16201 .unindent(),
16202 );
16203
16204 cx.dispatch_action(HandleInput("AAAA".to_string()));
16205 cx.assert_editor_state(
16206 &r#"AAAAˇone
16207 two
16208
16209 three
16210 fourAAAAˇ
16211 five
16212
16213 siAAAAˇx"#
16214 .unindent(),
16215 );
16216}
16217
16218#[gpui::test]
16219async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
16220 init_test(cx, |_| {});
16221
16222 let mut cx = EditorTestContext::new(cx).await;
16223 cx.set_state(
16224 r#"let foo = 1;
16225let foo = 2;
16226let foo = 3;
16227let fooˇ = 4;
16228let foo = 5;
16229let foo = 6;
16230let foo = 7;
16231let foo = 8;
16232let foo = 9;
16233let foo = 10;
16234let foo = 11;
16235let foo = 12;
16236let foo = 13;
16237let foo = 14;
16238let foo = 15;"#,
16239 );
16240
16241 cx.update_editor(|e, window, cx| {
16242 assert_eq!(
16243 e.next_scroll_position,
16244 NextScrollCursorCenterTopBottom::Center,
16245 "Default next scroll direction is center",
16246 );
16247
16248 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16249 assert_eq!(
16250 e.next_scroll_position,
16251 NextScrollCursorCenterTopBottom::Top,
16252 "After center, next scroll direction should be top",
16253 );
16254
16255 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16256 assert_eq!(
16257 e.next_scroll_position,
16258 NextScrollCursorCenterTopBottom::Bottom,
16259 "After top, next scroll direction should be bottom",
16260 );
16261
16262 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16263 assert_eq!(
16264 e.next_scroll_position,
16265 NextScrollCursorCenterTopBottom::Center,
16266 "After bottom, scrolling should start over",
16267 );
16268
16269 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16270 assert_eq!(
16271 e.next_scroll_position,
16272 NextScrollCursorCenterTopBottom::Top,
16273 "Scrolling continues if retriggered fast enough"
16274 );
16275 });
16276
16277 cx.executor()
16278 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
16279 cx.executor().run_until_parked();
16280 cx.update_editor(|e, _, _| {
16281 assert_eq!(
16282 e.next_scroll_position,
16283 NextScrollCursorCenterTopBottom::Center,
16284 "If scrolling is not triggered fast enough, it should reset"
16285 );
16286 });
16287}
16288
16289#[gpui::test]
16290async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
16291 init_test(cx, |_| {});
16292 let mut cx = EditorLspTestContext::new_rust(
16293 lsp::ServerCapabilities {
16294 definition_provider: Some(lsp::OneOf::Left(true)),
16295 references_provider: Some(lsp::OneOf::Left(true)),
16296 ..lsp::ServerCapabilities::default()
16297 },
16298 cx,
16299 )
16300 .await;
16301
16302 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
16303 let go_to_definition = cx
16304 .lsp
16305 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
16306 move |params, _| async move {
16307 if empty_go_to_definition {
16308 Ok(None)
16309 } else {
16310 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
16311 uri: params.text_document_position_params.text_document.uri,
16312 range: lsp::Range::new(
16313 lsp::Position::new(4, 3),
16314 lsp::Position::new(4, 6),
16315 ),
16316 })))
16317 }
16318 },
16319 );
16320 let references = cx
16321 .lsp
16322 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
16323 Ok(Some(vec![lsp::Location {
16324 uri: params.text_document_position.text_document.uri,
16325 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
16326 }]))
16327 });
16328 (go_to_definition, references)
16329 };
16330
16331 cx.set_state(
16332 &r#"fn one() {
16333 let mut a = ˇtwo();
16334 }
16335
16336 fn two() {}"#
16337 .unindent(),
16338 );
16339 set_up_lsp_handlers(false, &mut cx);
16340 let navigated = cx
16341 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
16342 .await
16343 .expect("Failed to navigate to definition");
16344 assert_eq!(
16345 navigated,
16346 Navigated::Yes,
16347 "Should have navigated to definition from the GetDefinition response"
16348 );
16349 cx.assert_editor_state(
16350 &r#"fn one() {
16351 let mut a = two();
16352 }
16353
16354 fn «twoˇ»() {}"#
16355 .unindent(),
16356 );
16357
16358 let editors = cx.update_workspace(|workspace, _, cx| {
16359 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
16360 });
16361 cx.update_editor(|_, _, test_editor_cx| {
16362 assert_eq!(
16363 editors.len(),
16364 1,
16365 "Initially, only one, test, editor should be open in the workspace"
16366 );
16367 assert_eq!(
16368 test_editor_cx.entity(),
16369 editors.last().expect("Asserted len is 1").clone()
16370 );
16371 });
16372
16373 set_up_lsp_handlers(true, &mut cx);
16374 let navigated = cx
16375 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
16376 .await
16377 .expect("Failed to navigate to lookup references");
16378 assert_eq!(
16379 navigated,
16380 Navigated::Yes,
16381 "Should have navigated to references as a fallback after empty GoToDefinition response"
16382 );
16383 // We should not change the selections in the existing file,
16384 // if opening another milti buffer with the references
16385 cx.assert_editor_state(
16386 &r#"fn one() {
16387 let mut a = two();
16388 }
16389
16390 fn «twoˇ»() {}"#
16391 .unindent(),
16392 );
16393 let editors = cx.update_workspace(|workspace, _, cx| {
16394 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
16395 });
16396 cx.update_editor(|_, _, test_editor_cx| {
16397 assert_eq!(
16398 editors.len(),
16399 2,
16400 "After falling back to references search, we open a new editor with the results"
16401 );
16402 let references_fallback_text = editors
16403 .into_iter()
16404 .find(|new_editor| *new_editor != test_editor_cx.entity())
16405 .expect("Should have one non-test editor now")
16406 .read(test_editor_cx)
16407 .text(test_editor_cx);
16408 assert_eq!(
16409 references_fallback_text, "fn one() {\n let mut a = two();\n}",
16410 "Should use the range from the references response and not the GoToDefinition one"
16411 );
16412 });
16413}
16414
16415#[gpui::test]
16416async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
16417 init_test(cx, |_| {});
16418 cx.update(|cx| {
16419 let mut editor_settings = EditorSettings::get_global(cx).clone();
16420 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
16421 EditorSettings::override_global(editor_settings, cx);
16422 });
16423 let mut cx = EditorLspTestContext::new_rust(
16424 lsp::ServerCapabilities {
16425 definition_provider: Some(lsp::OneOf::Left(true)),
16426 references_provider: Some(lsp::OneOf::Left(true)),
16427 ..lsp::ServerCapabilities::default()
16428 },
16429 cx,
16430 )
16431 .await;
16432 let original_state = r#"fn one() {
16433 let mut a = ˇtwo();
16434 }
16435
16436 fn two() {}"#
16437 .unindent();
16438 cx.set_state(&original_state);
16439
16440 let mut go_to_definition = cx
16441 .lsp
16442 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
16443 move |_, _| async move { Ok(None) },
16444 );
16445 let _references = cx
16446 .lsp
16447 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
16448 panic!("Should not call for references with no go to definition fallback")
16449 });
16450
16451 let navigated = cx
16452 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
16453 .await
16454 .expect("Failed to navigate to lookup references");
16455 go_to_definition
16456 .next()
16457 .await
16458 .expect("Should have called the go_to_definition handler");
16459
16460 assert_eq!(
16461 navigated,
16462 Navigated::No,
16463 "Should have navigated to references as a fallback after empty GoToDefinition response"
16464 );
16465 cx.assert_editor_state(&original_state);
16466 let editors = cx.update_workspace(|workspace, _, cx| {
16467 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
16468 });
16469 cx.update_editor(|_, _, _| {
16470 assert_eq!(
16471 editors.len(),
16472 1,
16473 "After unsuccessful fallback, no other editor should have been opened"
16474 );
16475 });
16476}
16477
16478#[gpui::test]
16479async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
16480 init_test(cx, |_| {});
16481
16482 let language = Arc::new(Language::new(
16483 LanguageConfig::default(),
16484 Some(tree_sitter_rust::LANGUAGE.into()),
16485 ));
16486
16487 let text = r#"
16488 #[cfg(test)]
16489 mod tests() {
16490 #[test]
16491 fn runnable_1() {
16492 let a = 1;
16493 }
16494
16495 #[test]
16496 fn runnable_2() {
16497 let a = 1;
16498 let b = 2;
16499 }
16500 }
16501 "#
16502 .unindent();
16503
16504 let fs = FakeFs::new(cx.executor());
16505 fs.insert_file("/file.rs", Default::default()).await;
16506
16507 let project = Project::test(fs, ["/a".as_ref()], cx).await;
16508 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16509 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16510 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16511 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
16512
16513 let editor = cx.new_window_entity(|window, cx| {
16514 Editor::new(
16515 EditorMode::Full,
16516 multi_buffer,
16517 Some(project.clone()),
16518 window,
16519 cx,
16520 )
16521 });
16522
16523 editor.update_in(cx, |editor, window, cx| {
16524 let snapshot = editor.buffer().read(cx).snapshot(cx);
16525 editor.tasks.insert(
16526 (buffer.read(cx).remote_id(), 3),
16527 RunnableTasks {
16528 templates: vec![],
16529 offset: snapshot.anchor_before(43),
16530 column: 0,
16531 extra_variables: HashMap::default(),
16532 context_range: BufferOffset(43)..BufferOffset(85),
16533 },
16534 );
16535 editor.tasks.insert(
16536 (buffer.read(cx).remote_id(), 8),
16537 RunnableTasks {
16538 templates: vec![],
16539 offset: snapshot.anchor_before(86),
16540 column: 0,
16541 extra_variables: HashMap::default(),
16542 context_range: BufferOffset(86)..BufferOffset(191),
16543 },
16544 );
16545
16546 // Test finding task when cursor is inside function body
16547 editor.change_selections(None, window, cx, |s| {
16548 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
16549 });
16550 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
16551 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
16552
16553 // Test finding task when cursor is on function name
16554 editor.change_selections(None, window, cx, |s| {
16555 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
16556 });
16557 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
16558 assert_eq!(row, 8, "Should find task when cursor is on function name");
16559 });
16560}
16561
16562#[gpui::test]
16563async fn test_folding_buffers(cx: &mut TestAppContext) {
16564 init_test(cx, |_| {});
16565
16566 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
16567 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
16568 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
16569
16570 let fs = FakeFs::new(cx.executor());
16571 fs.insert_tree(
16572 path!("/a"),
16573 json!({
16574 "first.rs": sample_text_1,
16575 "second.rs": sample_text_2,
16576 "third.rs": sample_text_3,
16577 }),
16578 )
16579 .await;
16580 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16581 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16582 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16583 let worktree = project.update(cx, |project, cx| {
16584 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
16585 assert_eq!(worktrees.len(), 1);
16586 worktrees.pop().unwrap()
16587 });
16588 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
16589
16590 let buffer_1 = project
16591 .update(cx, |project, cx| {
16592 project.open_buffer((worktree_id, "first.rs"), cx)
16593 })
16594 .await
16595 .unwrap();
16596 let buffer_2 = project
16597 .update(cx, |project, cx| {
16598 project.open_buffer((worktree_id, "second.rs"), cx)
16599 })
16600 .await
16601 .unwrap();
16602 let buffer_3 = project
16603 .update(cx, |project, cx| {
16604 project.open_buffer((worktree_id, "third.rs"), cx)
16605 })
16606 .await
16607 .unwrap();
16608
16609 let multi_buffer = cx.new(|cx| {
16610 let mut multi_buffer = MultiBuffer::new(ReadWrite);
16611 multi_buffer.push_excerpts(
16612 buffer_1.clone(),
16613 [
16614 ExcerptRange {
16615 context: Point::new(0, 0)..Point::new(3, 0),
16616 primary: None,
16617 },
16618 ExcerptRange {
16619 context: Point::new(5, 0)..Point::new(7, 0),
16620 primary: None,
16621 },
16622 ExcerptRange {
16623 context: Point::new(9, 0)..Point::new(10, 4),
16624 primary: None,
16625 },
16626 ],
16627 cx,
16628 );
16629 multi_buffer.push_excerpts(
16630 buffer_2.clone(),
16631 [
16632 ExcerptRange {
16633 context: Point::new(0, 0)..Point::new(3, 0),
16634 primary: None,
16635 },
16636 ExcerptRange {
16637 context: Point::new(5, 0)..Point::new(7, 0),
16638 primary: None,
16639 },
16640 ExcerptRange {
16641 context: Point::new(9, 0)..Point::new(10, 4),
16642 primary: None,
16643 },
16644 ],
16645 cx,
16646 );
16647 multi_buffer.push_excerpts(
16648 buffer_3.clone(),
16649 [
16650 ExcerptRange {
16651 context: Point::new(0, 0)..Point::new(3, 0),
16652 primary: None,
16653 },
16654 ExcerptRange {
16655 context: Point::new(5, 0)..Point::new(7, 0),
16656 primary: None,
16657 },
16658 ExcerptRange {
16659 context: Point::new(9, 0)..Point::new(10, 4),
16660 primary: None,
16661 },
16662 ],
16663 cx,
16664 );
16665 multi_buffer
16666 });
16667 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16668 Editor::new(
16669 EditorMode::Full,
16670 multi_buffer.clone(),
16671 Some(project.clone()),
16672 window,
16673 cx,
16674 )
16675 });
16676
16677 assert_eq!(
16678 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16679 "\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",
16680 );
16681
16682 multi_buffer_editor.update(cx, |editor, cx| {
16683 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
16684 });
16685 assert_eq!(
16686 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16687 "\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",
16688 "After folding the first buffer, its text should not be displayed"
16689 );
16690
16691 multi_buffer_editor.update(cx, |editor, cx| {
16692 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
16693 });
16694 assert_eq!(
16695 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16696 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
16697 "After folding the second buffer, its text should not be displayed"
16698 );
16699
16700 multi_buffer_editor.update(cx, |editor, cx| {
16701 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
16702 });
16703 assert_eq!(
16704 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16705 "\n\n\n\n\n",
16706 "After folding the third buffer, its text should not be displayed"
16707 );
16708
16709 // Emulate selection inside the fold logic, that should work
16710 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16711 editor
16712 .snapshot(window, cx)
16713 .next_line_boundary(Point::new(0, 4));
16714 });
16715
16716 multi_buffer_editor.update(cx, |editor, cx| {
16717 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
16718 });
16719 assert_eq!(
16720 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16721 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
16722 "After unfolding the second buffer, its text should be displayed"
16723 );
16724
16725 // Typing inside of buffer 1 causes that buffer to be unfolded.
16726 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16727 assert_eq!(
16728 multi_buffer
16729 .read(cx)
16730 .snapshot(cx)
16731 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
16732 .collect::<String>(),
16733 "bbbb"
16734 );
16735 editor.change_selections(None, window, cx, |selections| {
16736 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
16737 });
16738 editor.handle_input("B", window, cx);
16739 });
16740
16741 assert_eq!(
16742 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16743 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
16744 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
16745 );
16746
16747 multi_buffer_editor.update(cx, |editor, cx| {
16748 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
16749 });
16750 assert_eq!(
16751 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16752 "\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",
16753 "After unfolding the all buffers, all original text should be displayed"
16754 );
16755}
16756
16757#[gpui::test]
16758async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
16759 init_test(cx, |_| {});
16760
16761 let sample_text_1 = "1111\n2222\n3333".to_string();
16762 let sample_text_2 = "4444\n5555\n6666".to_string();
16763 let sample_text_3 = "7777\n8888\n9999".to_string();
16764
16765 let fs = FakeFs::new(cx.executor());
16766 fs.insert_tree(
16767 path!("/a"),
16768 json!({
16769 "first.rs": sample_text_1,
16770 "second.rs": sample_text_2,
16771 "third.rs": sample_text_3,
16772 }),
16773 )
16774 .await;
16775 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16776 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16777 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16778 let worktree = project.update(cx, |project, cx| {
16779 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
16780 assert_eq!(worktrees.len(), 1);
16781 worktrees.pop().unwrap()
16782 });
16783 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
16784
16785 let buffer_1 = project
16786 .update(cx, |project, cx| {
16787 project.open_buffer((worktree_id, "first.rs"), cx)
16788 })
16789 .await
16790 .unwrap();
16791 let buffer_2 = project
16792 .update(cx, |project, cx| {
16793 project.open_buffer((worktree_id, "second.rs"), cx)
16794 })
16795 .await
16796 .unwrap();
16797 let buffer_3 = project
16798 .update(cx, |project, cx| {
16799 project.open_buffer((worktree_id, "third.rs"), cx)
16800 })
16801 .await
16802 .unwrap();
16803
16804 let multi_buffer = cx.new(|cx| {
16805 let mut multi_buffer = MultiBuffer::new(ReadWrite);
16806 multi_buffer.push_excerpts(
16807 buffer_1.clone(),
16808 [ExcerptRange {
16809 context: Point::new(0, 0)..Point::new(3, 0),
16810 primary: None,
16811 }],
16812 cx,
16813 );
16814 multi_buffer.push_excerpts(
16815 buffer_2.clone(),
16816 [ExcerptRange {
16817 context: Point::new(0, 0)..Point::new(3, 0),
16818 primary: None,
16819 }],
16820 cx,
16821 );
16822 multi_buffer.push_excerpts(
16823 buffer_3.clone(),
16824 [ExcerptRange {
16825 context: Point::new(0, 0)..Point::new(3, 0),
16826 primary: None,
16827 }],
16828 cx,
16829 );
16830 multi_buffer
16831 });
16832
16833 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16834 Editor::new(
16835 EditorMode::Full,
16836 multi_buffer,
16837 Some(project.clone()),
16838 window,
16839 cx,
16840 )
16841 });
16842
16843 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
16844 assert_eq!(
16845 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16846 full_text,
16847 );
16848
16849 multi_buffer_editor.update(cx, |editor, cx| {
16850 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
16851 });
16852 assert_eq!(
16853 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16854 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
16855 "After folding the first buffer, its text should not be displayed"
16856 );
16857
16858 multi_buffer_editor.update(cx, |editor, cx| {
16859 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
16860 });
16861
16862 assert_eq!(
16863 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16864 "\n\n\n\n\n\n7777\n8888\n9999",
16865 "After folding the second buffer, its text should not be displayed"
16866 );
16867
16868 multi_buffer_editor.update(cx, |editor, cx| {
16869 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
16870 });
16871 assert_eq!(
16872 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16873 "\n\n\n\n\n",
16874 "After folding the third buffer, its text should not be displayed"
16875 );
16876
16877 multi_buffer_editor.update(cx, |editor, cx| {
16878 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
16879 });
16880 assert_eq!(
16881 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16882 "\n\n\n\n4444\n5555\n6666\n\n",
16883 "After unfolding the second buffer, its text should be displayed"
16884 );
16885
16886 multi_buffer_editor.update(cx, |editor, cx| {
16887 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
16888 });
16889 assert_eq!(
16890 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16891 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
16892 "After unfolding the first buffer, its text should be displayed"
16893 );
16894
16895 multi_buffer_editor.update(cx, |editor, cx| {
16896 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
16897 });
16898 assert_eq!(
16899 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16900 full_text,
16901 "After unfolding all buffers, all original text should be displayed"
16902 );
16903}
16904
16905#[gpui::test]
16906async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
16907 init_test(cx, |_| {});
16908
16909 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
16910
16911 let fs = FakeFs::new(cx.executor());
16912 fs.insert_tree(
16913 path!("/a"),
16914 json!({
16915 "main.rs": sample_text,
16916 }),
16917 )
16918 .await;
16919 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16920 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16921 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16922 let worktree = project.update(cx, |project, cx| {
16923 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
16924 assert_eq!(worktrees.len(), 1);
16925 worktrees.pop().unwrap()
16926 });
16927 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
16928
16929 let buffer_1 = project
16930 .update(cx, |project, cx| {
16931 project.open_buffer((worktree_id, "main.rs"), cx)
16932 })
16933 .await
16934 .unwrap();
16935
16936 let multi_buffer = cx.new(|cx| {
16937 let mut multi_buffer = MultiBuffer::new(ReadWrite);
16938 multi_buffer.push_excerpts(
16939 buffer_1.clone(),
16940 [ExcerptRange {
16941 context: Point::new(0, 0)
16942 ..Point::new(
16943 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
16944 0,
16945 ),
16946 primary: None,
16947 }],
16948 cx,
16949 );
16950 multi_buffer
16951 });
16952 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16953 Editor::new(
16954 EditorMode::Full,
16955 multi_buffer,
16956 Some(project.clone()),
16957 window,
16958 cx,
16959 )
16960 });
16961
16962 let selection_range = Point::new(1, 0)..Point::new(2, 0);
16963 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16964 enum TestHighlight {}
16965 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
16966 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
16967 editor.highlight_text::<TestHighlight>(
16968 vec![highlight_range.clone()],
16969 HighlightStyle::color(Hsla::green()),
16970 cx,
16971 );
16972 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
16973 });
16974
16975 let full_text = format!("\n\n{sample_text}");
16976 assert_eq!(
16977 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16978 full_text,
16979 );
16980}
16981
16982#[gpui::test]
16983async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
16984 init_test(cx, |_| {});
16985 cx.update(|cx| {
16986 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
16987 "keymaps/default-linux.json",
16988 cx,
16989 )
16990 .unwrap();
16991 cx.bind_keys(default_key_bindings);
16992 });
16993
16994 let (editor, cx) = cx.add_window_view(|window, cx| {
16995 let multi_buffer = MultiBuffer::build_multi(
16996 [
16997 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
16998 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
16999 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
17000 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
17001 ],
17002 cx,
17003 );
17004 let mut editor = Editor::new(EditorMode::Full, multi_buffer.clone(), None, window, cx);
17005
17006 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
17007 // fold all but the second buffer, so that we test navigating between two
17008 // adjacent folded buffers, as well as folded buffers at the start and
17009 // end the multibuffer
17010 editor.fold_buffer(buffer_ids[0], cx);
17011 editor.fold_buffer(buffer_ids[2], cx);
17012 editor.fold_buffer(buffer_ids[3], cx);
17013
17014 editor
17015 });
17016 cx.simulate_resize(size(px(1000.), px(1000.)));
17017
17018 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
17019 cx.assert_excerpts_with_selections(indoc! {"
17020 [EXCERPT]
17021 ˇ[FOLDED]
17022 [EXCERPT]
17023 a1
17024 b1
17025 [EXCERPT]
17026 [FOLDED]
17027 [EXCERPT]
17028 [FOLDED]
17029 "
17030 });
17031 cx.simulate_keystroke("down");
17032 cx.assert_excerpts_with_selections(indoc! {"
17033 [EXCERPT]
17034 [FOLDED]
17035 [EXCERPT]
17036 ˇa1
17037 b1
17038 [EXCERPT]
17039 [FOLDED]
17040 [EXCERPT]
17041 [FOLDED]
17042 "
17043 });
17044 cx.simulate_keystroke("down");
17045 cx.assert_excerpts_with_selections(indoc! {"
17046 [EXCERPT]
17047 [FOLDED]
17048 [EXCERPT]
17049 a1
17050 ˇb1
17051 [EXCERPT]
17052 [FOLDED]
17053 [EXCERPT]
17054 [FOLDED]
17055 "
17056 });
17057 cx.simulate_keystroke("down");
17058 cx.assert_excerpts_with_selections(indoc! {"
17059 [EXCERPT]
17060 [FOLDED]
17061 [EXCERPT]
17062 a1
17063 b1
17064 ˇ[EXCERPT]
17065 [FOLDED]
17066 [EXCERPT]
17067 [FOLDED]
17068 "
17069 });
17070 cx.simulate_keystroke("down");
17071 cx.assert_excerpts_with_selections(indoc! {"
17072 [EXCERPT]
17073 [FOLDED]
17074 [EXCERPT]
17075 a1
17076 b1
17077 [EXCERPT]
17078 ˇ[FOLDED]
17079 [EXCERPT]
17080 [FOLDED]
17081 "
17082 });
17083 for _ in 0..5 {
17084 cx.simulate_keystroke("down");
17085 cx.assert_excerpts_with_selections(indoc! {"
17086 [EXCERPT]
17087 [FOLDED]
17088 [EXCERPT]
17089 a1
17090 b1
17091 [EXCERPT]
17092 [FOLDED]
17093 [EXCERPT]
17094 ˇ[FOLDED]
17095 "
17096 });
17097 }
17098
17099 cx.simulate_keystroke("up");
17100 cx.assert_excerpts_with_selections(indoc! {"
17101 [EXCERPT]
17102 [FOLDED]
17103 [EXCERPT]
17104 a1
17105 b1
17106 [EXCERPT]
17107 ˇ[FOLDED]
17108 [EXCERPT]
17109 [FOLDED]
17110 "
17111 });
17112 cx.simulate_keystroke("up");
17113 cx.assert_excerpts_with_selections(indoc! {"
17114 [EXCERPT]
17115 [FOLDED]
17116 [EXCERPT]
17117 a1
17118 b1
17119 ˇ[EXCERPT]
17120 [FOLDED]
17121 [EXCERPT]
17122 [FOLDED]
17123 "
17124 });
17125 cx.simulate_keystroke("up");
17126 cx.assert_excerpts_with_selections(indoc! {"
17127 [EXCERPT]
17128 [FOLDED]
17129 [EXCERPT]
17130 a1
17131 ˇb1
17132 [EXCERPT]
17133 [FOLDED]
17134 [EXCERPT]
17135 [FOLDED]
17136 "
17137 });
17138 cx.simulate_keystroke("up");
17139 cx.assert_excerpts_with_selections(indoc! {"
17140 [EXCERPT]
17141 [FOLDED]
17142 [EXCERPT]
17143 ˇa1
17144 b1
17145 [EXCERPT]
17146 [FOLDED]
17147 [EXCERPT]
17148 [FOLDED]
17149 "
17150 });
17151 for _ in 0..5 {
17152 cx.simulate_keystroke("up");
17153 cx.assert_excerpts_with_selections(indoc! {"
17154 [EXCERPT]
17155 ˇ[FOLDED]
17156 [EXCERPT]
17157 a1
17158 b1
17159 [EXCERPT]
17160 [FOLDED]
17161 [EXCERPT]
17162 [FOLDED]
17163 "
17164 });
17165 }
17166}
17167
17168#[gpui::test]
17169async fn test_inline_completion_text(cx: &mut TestAppContext) {
17170 init_test(cx, |_| {});
17171
17172 // Simple insertion
17173 assert_highlighted_edits(
17174 "Hello, world!",
17175 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
17176 true,
17177 cx,
17178 |highlighted_edits, cx| {
17179 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
17180 assert_eq!(highlighted_edits.highlights.len(), 1);
17181 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
17182 assert_eq!(
17183 highlighted_edits.highlights[0].1.background_color,
17184 Some(cx.theme().status().created_background)
17185 );
17186 },
17187 )
17188 .await;
17189
17190 // Replacement
17191 assert_highlighted_edits(
17192 "This is a test.",
17193 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
17194 false,
17195 cx,
17196 |highlighted_edits, cx| {
17197 assert_eq!(highlighted_edits.text, "That is a test.");
17198 assert_eq!(highlighted_edits.highlights.len(), 1);
17199 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
17200 assert_eq!(
17201 highlighted_edits.highlights[0].1.background_color,
17202 Some(cx.theme().status().created_background)
17203 );
17204 },
17205 )
17206 .await;
17207
17208 // Multiple edits
17209 assert_highlighted_edits(
17210 "Hello, world!",
17211 vec![
17212 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
17213 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
17214 ],
17215 false,
17216 cx,
17217 |highlighted_edits, cx| {
17218 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
17219 assert_eq!(highlighted_edits.highlights.len(), 2);
17220 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
17221 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
17222 assert_eq!(
17223 highlighted_edits.highlights[0].1.background_color,
17224 Some(cx.theme().status().created_background)
17225 );
17226 assert_eq!(
17227 highlighted_edits.highlights[1].1.background_color,
17228 Some(cx.theme().status().created_background)
17229 );
17230 },
17231 )
17232 .await;
17233
17234 // Multiple lines with edits
17235 assert_highlighted_edits(
17236 "First line\nSecond line\nThird line\nFourth line",
17237 vec![
17238 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
17239 (
17240 Point::new(2, 0)..Point::new(2, 10),
17241 "New third line".to_string(),
17242 ),
17243 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
17244 ],
17245 false,
17246 cx,
17247 |highlighted_edits, cx| {
17248 assert_eq!(
17249 highlighted_edits.text,
17250 "Second modified\nNew third line\nFourth updated line"
17251 );
17252 assert_eq!(highlighted_edits.highlights.len(), 3);
17253 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
17254 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
17255 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
17256 for highlight in &highlighted_edits.highlights {
17257 assert_eq!(
17258 highlight.1.background_color,
17259 Some(cx.theme().status().created_background)
17260 );
17261 }
17262 },
17263 )
17264 .await;
17265}
17266
17267#[gpui::test]
17268async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
17269 init_test(cx, |_| {});
17270
17271 // Deletion
17272 assert_highlighted_edits(
17273 "Hello, world!",
17274 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
17275 true,
17276 cx,
17277 |highlighted_edits, cx| {
17278 assert_eq!(highlighted_edits.text, "Hello, world!");
17279 assert_eq!(highlighted_edits.highlights.len(), 1);
17280 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
17281 assert_eq!(
17282 highlighted_edits.highlights[0].1.background_color,
17283 Some(cx.theme().status().deleted_background)
17284 );
17285 },
17286 )
17287 .await;
17288
17289 // Insertion
17290 assert_highlighted_edits(
17291 "Hello, world!",
17292 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
17293 true,
17294 cx,
17295 |highlighted_edits, cx| {
17296 assert_eq!(highlighted_edits.highlights.len(), 1);
17297 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
17298 assert_eq!(
17299 highlighted_edits.highlights[0].1.background_color,
17300 Some(cx.theme().status().created_background)
17301 );
17302 },
17303 )
17304 .await;
17305}
17306
17307async fn assert_highlighted_edits(
17308 text: &str,
17309 edits: Vec<(Range<Point>, String)>,
17310 include_deletions: bool,
17311 cx: &mut TestAppContext,
17312 assertion_fn: impl Fn(HighlightedText, &App),
17313) {
17314 let window = cx.add_window(|window, cx| {
17315 let buffer = MultiBuffer::build_simple(text, cx);
17316 Editor::new(EditorMode::Full, buffer, None, window, cx)
17317 });
17318 let cx = &mut VisualTestContext::from_window(*window, cx);
17319
17320 let (buffer, snapshot) = window
17321 .update(cx, |editor, _window, cx| {
17322 (
17323 editor.buffer().clone(),
17324 editor.buffer().read(cx).snapshot(cx),
17325 )
17326 })
17327 .unwrap();
17328
17329 let edits = edits
17330 .into_iter()
17331 .map(|(range, edit)| {
17332 (
17333 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
17334 edit,
17335 )
17336 })
17337 .collect::<Vec<_>>();
17338
17339 let text_anchor_edits = edits
17340 .clone()
17341 .into_iter()
17342 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
17343 .collect::<Vec<_>>();
17344
17345 let edit_preview = window
17346 .update(cx, |_, _window, cx| {
17347 buffer
17348 .read(cx)
17349 .as_singleton()
17350 .unwrap()
17351 .read(cx)
17352 .preview_edits(text_anchor_edits.into(), cx)
17353 })
17354 .unwrap()
17355 .await;
17356
17357 cx.update(|_window, cx| {
17358 let highlighted_edits = inline_completion_edit_text(
17359 &snapshot.as_singleton().unwrap().2,
17360 &edits,
17361 &edit_preview,
17362 include_deletions,
17363 cx,
17364 );
17365 assertion_fn(highlighted_edits, cx)
17366 });
17367}
17368
17369#[track_caller]
17370fn assert_breakpoint(
17371 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
17372 path: &Arc<Path>,
17373 expected: Vec<(u32, Breakpoint)>,
17374) {
17375 if expected.len() == 0usize {
17376 assert!(!breakpoints.contains_key(path), "{}", path.display());
17377 } else {
17378 let mut breakpoint = breakpoints
17379 .get(path)
17380 .unwrap()
17381 .into_iter()
17382 .map(|breakpoint| {
17383 (
17384 breakpoint.row,
17385 Breakpoint {
17386 message: breakpoint.message.clone(),
17387 state: breakpoint.state,
17388 },
17389 )
17390 })
17391 .collect::<Vec<_>>();
17392
17393 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
17394
17395 assert_eq!(expected, breakpoint);
17396 }
17397}
17398
17399fn add_log_breakpoint_at_cursor(
17400 editor: &mut Editor,
17401 log_message: &str,
17402 window: &mut Window,
17403 cx: &mut Context<Editor>,
17404) {
17405 let (anchor, bp) = editor
17406 .breakpoint_at_cursor_head(window, cx)
17407 .unwrap_or_else(|| {
17408 let cursor_position: Point = editor.selections.newest(cx).head();
17409
17410 let breakpoint_position = editor
17411 .snapshot(window, cx)
17412 .display_snapshot
17413 .buffer_snapshot
17414 .anchor_before(Point::new(cursor_position.row, 0));
17415
17416 (
17417 breakpoint_position,
17418 Breakpoint {
17419 message: Some(Arc::from(log_message)),
17420 state: BreakpointState::Enabled,
17421 },
17422 )
17423 });
17424
17425 editor.edit_breakpoint_at_anchor(
17426 anchor,
17427 bp,
17428 BreakpointEditAction::EditLogMessage(log_message.into()),
17429 cx,
17430 );
17431}
17432
17433#[gpui::test]
17434async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
17435 init_test(cx, |_| {});
17436
17437 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
17438 let fs = FakeFs::new(cx.executor());
17439 fs.insert_tree(
17440 path!("/a"),
17441 json!({
17442 "main.rs": sample_text,
17443 }),
17444 )
17445 .await;
17446 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17447 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17448 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17449
17450 let fs = FakeFs::new(cx.executor());
17451 fs.insert_tree(
17452 path!("/a"),
17453 json!({
17454 "main.rs": sample_text,
17455 }),
17456 )
17457 .await;
17458 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17459 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17460 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17461 let worktree_id = workspace
17462 .update(cx, |workspace, _window, cx| {
17463 workspace.project().update(cx, |project, cx| {
17464 project.worktrees(cx).next().unwrap().read(cx).id()
17465 })
17466 })
17467 .unwrap();
17468
17469 let buffer = project
17470 .update(cx, |project, cx| {
17471 project.open_buffer((worktree_id, "main.rs"), cx)
17472 })
17473 .await
17474 .unwrap();
17475
17476 let (editor, cx) = cx.add_window_view(|window, cx| {
17477 Editor::new(
17478 EditorMode::Full,
17479 MultiBuffer::build_from_buffer(buffer, cx),
17480 Some(project.clone()),
17481 window,
17482 cx,
17483 )
17484 });
17485
17486 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
17487 let abs_path = project.read_with(cx, |project, cx| {
17488 project
17489 .absolute_path(&project_path, cx)
17490 .map(|path_buf| Arc::from(path_buf.to_owned()))
17491 .unwrap()
17492 });
17493
17494 // assert we can add breakpoint on the first line
17495 editor.update_in(cx, |editor, window, cx| {
17496 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17497 editor.move_to_end(&MoveToEnd, window, cx);
17498 editor.toggle_breakpoint(&actions::ToggleBreakpoint, 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_eq!(1, breakpoints.len());
17512 assert_breakpoint(
17513 &breakpoints,
17514 &abs_path,
17515 vec![
17516 (0, Breakpoint::new_standard()),
17517 (3, Breakpoint::new_standard()),
17518 ],
17519 );
17520
17521 editor.update_in(cx, |editor, window, cx| {
17522 editor.move_to_beginning(&MoveToBeginning, window, cx);
17523 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17524 });
17525
17526 let breakpoints = editor.update(cx, |editor, cx| {
17527 editor
17528 .breakpoint_store()
17529 .as_ref()
17530 .unwrap()
17531 .read(cx)
17532 .all_breakpoints(cx)
17533 .clone()
17534 });
17535
17536 assert_eq!(1, breakpoints.len());
17537 assert_breakpoint(
17538 &breakpoints,
17539 &abs_path,
17540 vec![(3, Breakpoint::new_standard())],
17541 );
17542
17543 editor.update_in(cx, |editor, window, cx| {
17544 editor.move_to_end(&MoveToEnd, window, cx);
17545 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17546 });
17547
17548 let breakpoints = editor.update(cx, |editor, cx| {
17549 editor
17550 .breakpoint_store()
17551 .as_ref()
17552 .unwrap()
17553 .read(cx)
17554 .all_breakpoints(cx)
17555 .clone()
17556 });
17557
17558 assert_eq!(0, breakpoints.len());
17559 assert_breakpoint(&breakpoints, &abs_path, vec![]);
17560}
17561
17562#[gpui::test]
17563async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
17564 init_test(cx, |_| {});
17565
17566 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
17567
17568 let fs = FakeFs::new(cx.executor());
17569 fs.insert_tree(
17570 path!("/a"),
17571 json!({
17572 "main.rs": sample_text,
17573 }),
17574 )
17575 .await;
17576 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17577 let (workspace, cx) =
17578 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
17579
17580 let worktree_id = workspace.update(cx, |workspace, cx| {
17581 workspace.project().update(cx, |project, cx| {
17582 project.worktrees(cx).next().unwrap().read(cx).id()
17583 })
17584 });
17585
17586 let buffer = project
17587 .update(cx, |project, cx| {
17588 project.open_buffer((worktree_id, "main.rs"), cx)
17589 })
17590 .await
17591 .unwrap();
17592
17593 let (editor, cx) = cx.add_window_view(|window, cx| {
17594 Editor::new(
17595 EditorMode::Full,
17596 MultiBuffer::build_from_buffer(buffer, cx),
17597 Some(project.clone()),
17598 window,
17599 cx,
17600 )
17601 });
17602
17603 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
17604 let abs_path = project.read_with(cx, |project, cx| {
17605 project
17606 .absolute_path(&project_path, cx)
17607 .map(|path_buf| Arc::from(path_buf.to_owned()))
17608 .unwrap()
17609 });
17610
17611 editor.update_in(cx, |editor, window, cx| {
17612 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
17613 });
17614
17615 let breakpoints = editor.update(cx, |editor, cx| {
17616 editor
17617 .breakpoint_store()
17618 .as_ref()
17619 .unwrap()
17620 .read(cx)
17621 .all_breakpoints(cx)
17622 .clone()
17623 });
17624
17625 assert_breakpoint(
17626 &breakpoints,
17627 &abs_path,
17628 vec![(0, Breakpoint::new_log("hello world"))],
17629 );
17630
17631 // Removing a log message from a log breakpoint should remove it
17632 editor.update_in(cx, |editor, window, cx| {
17633 add_log_breakpoint_at_cursor(editor, "", window, cx);
17634 });
17635
17636 let breakpoints = editor.update(cx, |editor, cx| {
17637 editor
17638 .breakpoint_store()
17639 .as_ref()
17640 .unwrap()
17641 .read(cx)
17642 .all_breakpoints(cx)
17643 .clone()
17644 });
17645
17646 assert_breakpoint(&breakpoints, &abs_path, vec![]);
17647
17648 editor.update_in(cx, |editor, window, cx| {
17649 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17650 editor.move_to_end(&MoveToEnd, window, cx);
17651 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17652 // Not adding a log message to a standard breakpoint shouldn't remove it
17653 add_log_breakpoint_at_cursor(editor, "", window, cx);
17654 });
17655
17656 let breakpoints = editor.update(cx, |editor, cx| {
17657 editor
17658 .breakpoint_store()
17659 .as_ref()
17660 .unwrap()
17661 .read(cx)
17662 .all_breakpoints(cx)
17663 .clone()
17664 });
17665
17666 assert_breakpoint(
17667 &breakpoints,
17668 &abs_path,
17669 vec![
17670 (0, Breakpoint::new_standard()),
17671 (3, Breakpoint::new_standard()),
17672 ],
17673 );
17674
17675 editor.update_in(cx, |editor, window, cx| {
17676 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
17677 });
17678
17679 let breakpoints = editor.update(cx, |editor, cx| {
17680 editor
17681 .breakpoint_store()
17682 .as_ref()
17683 .unwrap()
17684 .read(cx)
17685 .all_breakpoints(cx)
17686 .clone()
17687 });
17688
17689 assert_breakpoint(
17690 &breakpoints,
17691 &abs_path,
17692 vec![
17693 (0, Breakpoint::new_standard()),
17694 (3, Breakpoint::new_log("hello world")),
17695 ],
17696 );
17697
17698 editor.update_in(cx, |editor, window, cx| {
17699 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
17700 });
17701
17702 let breakpoints = editor.update(cx, |editor, cx| {
17703 editor
17704 .breakpoint_store()
17705 .as_ref()
17706 .unwrap()
17707 .read(cx)
17708 .all_breakpoints(cx)
17709 .clone()
17710 });
17711
17712 assert_breakpoint(
17713 &breakpoints,
17714 &abs_path,
17715 vec![
17716 (0, Breakpoint::new_standard()),
17717 (3, Breakpoint::new_log("hello Earth!!")),
17718 ],
17719 );
17720}
17721
17722/// This also tests that Editor::breakpoint_at_cursor_head is working properly
17723/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
17724/// or when breakpoints were placed out of order. This tests for a regression too
17725#[gpui::test]
17726async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
17727 init_test(cx, |_| {});
17728
17729 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
17730 let fs = FakeFs::new(cx.executor());
17731 fs.insert_tree(
17732 path!("/a"),
17733 json!({
17734 "main.rs": sample_text,
17735 }),
17736 )
17737 .await;
17738 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17739 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17740 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17741
17742 let fs = FakeFs::new(cx.executor());
17743 fs.insert_tree(
17744 path!("/a"),
17745 json!({
17746 "main.rs": sample_text,
17747 }),
17748 )
17749 .await;
17750 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17751 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17752 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17753 let worktree_id = workspace
17754 .update(cx, |workspace, _window, cx| {
17755 workspace.project().update(cx, |project, cx| {
17756 project.worktrees(cx).next().unwrap().read(cx).id()
17757 })
17758 })
17759 .unwrap();
17760
17761 let buffer = project
17762 .update(cx, |project, cx| {
17763 project.open_buffer((worktree_id, "main.rs"), cx)
17764 })
17765 .await
17766 .unwrap();
17767
17768 let (editor, cx) = cx.add_window_view(|window, cx| {
17769 Editor::new(
17770 EditorMode::Full,
17771 MultiBuffer::build_from_buffer(buffer, cx),
17772 Some(project.clone()),
17773 window,
17774 cx,
17775 )
17776 });
17777
17778 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
17779 let abs_path = project.read_with(cx, |project, cx| {
17780 project
17781 .absolute_path(&project_path, cx)
17782 .map(|path_buf| Arc::from(path_buf.to_owned()))
17783 .unwrap()
17784 });
17785
17786 // assert we can add breakpoint on the first line
17787 editor.update_in(cx, |editor, window, cx| {
17788 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17789 editor.move_to_end(&MoveToEnd, window, cx);
17790 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17791 editor.move_up(&MoveUp, window, cx);
17792 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17793 });
17794
17795 let breakpoints = editor.update(cx, |editor, cx| {
17796 editor
17797 .breakpoint_store()
17798 .as_ref()
17799 .unwrap()
17800 .read(cx)
17801 .all_breakpoints(cx)
17802 .clone()
17803 });
17804
17805 assert_eq!(1, breakpoints.len());
17806 assert_breakpoint(
17807 &breakpoints,
17808 &abs_path,
17809 vec![
17810 (0, Breakpoint::new_standard()),
17811 (2, Breakpoint::new_standard()),
17812 (3, Breakpoint::new_standard()),
17813 ],
17814 );
17815
17816 editor.update_in(cx, |editor, window, cx| {
17817 editor.move_to_beginning(&MoveToBeginning, window, cx);
17818 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
17819 editor.move_to_end(&MoveToEnd, window, cx);
17820 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
17821 // Disabling a breakpoint that doesn't exist should do nothing
17822 editor.move_up(&MoveUp, window, cx);
17823 editor.move_up(&MoveUp, window, cx);
17824 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
17825 });
17826
17827 let breakpoints = editor.update(cx, |editor, cx| {
17828 editor
17829 .breakpoint_store()
17830 .as_ref()
17831 .unwrap()
17832 .read(cx)
17833 .all_breakpoints(cx)
17834 .clone()
17835 });
17836
17837 let disable_breakpoint = {
17838 let mut bp = Breakpoint::new_standard();
17839 bp.state = BreakpointState::Disabled;
17840 bp
17841 };
17842
17843 assert_eq!(1, breakpoints.len());
17844 assert_breakpoint(
17845 &breakpoints,
17846 &abs_path,
17847 vec![
17848 (0, disable_breakpoint.clone()),
17849 (2, Breakpoint::new_standard()),
17850 (3, disable_breakpoint.clone()),
17851 ],
17852 );
17853
17854 editor.update_in(cx, |editor, window, cx| {
17855 editor.move_to_beginning(&MoveToBeginning, window, cx);
17856 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
17857 editor.move_to_end(&MoveToEnd, window, cx);
17858 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
17859 editor.move_up(&MoveUp, window, cx);
17860 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
17861 });
17862
17863 let breakpoints = editor.update(cx, |editor, cx| {
17864 editor
17865 .breakpoint_store()
17866 .as_ref()
17867 .unwrap()
17868 .read(cx)
17869 .all_breakpoints(cx)
17870 .clone()
17871 });
17872
17873 assert_eq!(1, breakpoints.len());
17874 assert_breakpoint(
17875 &breakpoints,
17876 &abs_path,
17877 vec![
17878 (0, Breakpoint::new_standard()),
17879 (2, disable_breakpoint),
17880 (3, Breakpoint::new_standard()),
17881 ],
17882 );
17883}
17884
17885#[gpui::test]
17886async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
17887 init_test(cx, |_| {});
17888 let capabilities = lsp::ServerCapabilities {
17889 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
17890 prepare_provider: Some(true),
17891 work_done_progress_options: Default::default(),
17892 })),
17893 ..Default::default()
17894 };
17895 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
17896
17897 cx.set_state(indoc! {"
17898 struct Fˇoo {}
17899 "});
17900
17901 cx.update_editor(|editor, _, cx| {
17902 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
17903 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
17904 editor.highlight_background::<DocumentHighlightRead>(
17905 &[highlight_range],
17906 |c| c.editor_document_highlight_read_background,
17907 cx,
17908 );
17909 });
17910
17911 let mut prepare_rename_handler = cx
17912 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
17913 move |_, _, _| async move {
17914 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
17915 start: lsp::Position {
17916 line: 0,
17917 character: 7,
17918 },
17919 end: lsp::Position {
17920 line: 0,
17921 character: 10,
17922 },
17923 })))
17924 },
17925 );
17926 let prepare_rename_task = cx
17927 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
17928 .expect("Prepare rename was not started");
17929 prepare_rename_handler.next().await.unwrap();
17930 prepare_rename_task.await.expect("Prepare rename failed");
17931
17932 let mut rename_handler =
17933 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
17934 let edit = lsp::TextEdit {
17935 range: lsp::Range {
17936 start: lsp::Position {
17937 line: 0,
17938 character: 7,
17939 },
17940 end: lsp::Position {
17941 line: 0,
17942 character: 10,
17943 },
17944 },
17945 new_text: "FooRenamed".to_string(),
17946 };
17947 Ok(Some(lsp::WorkspaceEdit::new(
17948 // Specify the same edit twice
17949 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
17950 )))
17951 });
17952 let rename_task = cx
17953 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
17954 .expect("Confirm rename was not started");
17955 rename_handler.next().await.unwrap();
17956 rename_task.await.expect("Confirm rename failed");
17957 cx.run_until_parked();
17958
17959 // Despite two edits, only one is actually applied as those are identical
17960 cx.assert_editor_state(indoc! {"
17961 struct FooRenamedˇ {}
17962 "});
17963}
17964
17965#[gpui::test]
17966async fn test_rename_without_prepare(cx: &mut TestAppContext) {
17967 init_test(cx, |_| {});
17968 // These capabilities indicate that the server does not support prepare rename.
17969 let capabilities = lsp::ServerCapabilities {
17970 rename_provider: Some(lsp::OneOf::Left(true)),
17971 ..Default::default()
17972 };
17973 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
17974
17975 cx.set_state(indoc! {"
17976 struct Fˇoo {}
17977 "});
17978
17979 cx.update_editor(|editor, _window, cx| {
17980 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
17981 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
17982 editor.highlight_background::<DocumentHighlightRead>(
17983 &[highlight_range],
17984 |c| c.editor_document_highlight_read_background,
17985 cx,
17986 );
17987 });
17988
17989 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
17990 .expect("Prepare rename was not started")
17991 .await
17992 .expect("Prepare rename failed");
17993
17994 let mut rename_handler =
17995 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
17996 let edit = lsp::TextEdit {
17997 range: lsp::Range {
17998 start: lsp::Position {
17999 line: 0,
18000 character: 7,
18001 },
18002 end: lsp::Position {
18003 line: 0,
18004 character: 10,
18005 },
18006 },
18007 new_text: "FooRenamed".to_string(),
18008 };
18009 Ok(Some(lsp::WorkspaceEdit::new(
18010 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
18011 )))
18012 });
18013 let rename_task = cx
18014 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
18015 .expect("Confirm rename was not started");
18016 rename_handler.next().await.unwrap();
18017 rename_task.await.expect("Confirm rename failed");
18018 cx.run_until_parked();
18019
18020 // Correct range is renamed, as `surrounding_word` is used to find it.
18021 cx.assert_editor_state(indoc! {"
18022 struct FooRenamedˇ {}
18023 "});
18024}
18025
18026#[gpui::test]
18027async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
18028 init_test(cx, |_| {});
18029 let mut cx = EditorTestContext::new(cx).await;
18030
18031 let language = Arc::new(
18032 Language::new(
18033 LanguageConfig::default(),
18034 Some(tree_sitter_html::LANGUAGE.into()),
18035 )
18036 .with_brackets_query(
18037 r#"
18038 ("<" @open "/>" @close)
18039 ("</" @open ">" @close)
18040 ("<" @open ">" @close)
18041 ("\"" @open "\"" @close)
18042 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
18043 "#,
18044 )
18045 .unwrap(),
18046 );
18047 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
18048
18049 cx.set_state(indoc! {"
18050 <span>ˇ</span>
18051 "});
18052 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18053 cx.assert_editor_state(indoc! {"
18054 <span>
18055 ˇ
18056 </span>
18057 "});
18058
18059 cx.set_state(indoc! {"
18060 <span><span></span>ˇ</span>
18061 "});
18062 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18063 cx.assert_editor_state(indoc! {"
18064 <span><span></span>
18065 ˇ</span>
18066 "});
18067
18068 cx.set_state(indoc! {"
18069 <span>ˇ
18070 </span>
18071 "});
18072 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18073 cx.assert_editor_state(indoc! {"
18074 <span>
18075 ˇ
18076 </span>
18077 "});
18078}
18079
18080#[gpui::test(iterations = 10)]
18081async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
18082 init_test(cx, |_| {});
18083
18084 let fs = FakeFs::new(cx.executor());
18085 fs.insert_tree(
18086 path!("/dir"),
18087 json!({
18088 "a.ts": "a",
18089 }),
18090 )
18091 .await;
18092
18093 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
18094 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18095 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18096
18097 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18098 language_registry.add(Arc::new(Language::new(
18099 LanguageConfig {
18100 name: "TypeScript".into(),
18101 matcher: LanguageMatcher {
18102 path_suffixes: vec!["ts".to_string()],
18103 ..Default::default()
18104 },
18105 ..Default::default()
18106 },
18107 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18108 )));
18109 let mut fake_language_servers = language_registry.register_fake_lsp(
18110 "TypeScript",
18111 FakeLspAdapter {
18112 capabilities: lsp::ServerCapabilities {
18113 code_lens_provider: Some(lsp::CodeLensOptions {
18114 resolve_provider: Some(true),
18115 }),
18116 execute_command_provider: Some(lsp::ExecuteCommandOptions {
18117 commands: vec!["_the/command".to_string()],
18118 ..lsp::ExecuteCommandOptions::default()
18119 }),
18120 ..lsp::ServerCapabilities::default()
18121 },
18122 ..FakeLspAdapter::default()
18123 },
18124 );
18125
18126 let (buffer, _handle) = project
18127 .update(cx, |p, cx| {
18128 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
18129 })
18130 .await
18131 .unwrap();
18132 cx.executor().run_until_parked();
18133
18134 let fake_server = fake_language_servers.next().await.unwrap();
18135
18136 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
18137 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
18138 drop(buffer_snapshot);
18139 let actions = cx
18140 .update_window(*workspace, |_, window, cx| {
18141 project.code_actions(&buffer, anchor..anchor, window, cx)
18142 })
18143 .unwrap();
18144
18145 fake_server
18146 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
18147 Ok(Some(vec![
18148 lsp::CodeLens {
18149 range: lsp::Range::default(),
18150 command: Some(lsp::Command {
18151 title: "Code lens command".to_owned(),
18152 command: "_the/command".to_owned(),
18153 arguments: None,
18154 }),
18155 data: None,
18156 },
18157 lsp::CodeLens {
18158 range: lsp::Range::default(),
18159 command: Some(lsp::Command {
18160 title: "Command not in capabilities".to_owned(),
18161 command: "not in capabilities".to_owned(),
18162 arguments: None,
18163 }),
18164 data: None,
18165 },
18166 lsp::CodeLens {
18167 range: lsp::Range {
18168 start: lsp::Position {
18169 line: 1,
18170 character: 1,
18171 },
18172 end: lsp::Position {
18173 line: 1,
18174 character: 1,
18175 },
18176 },
18177 command: Some(lsp::Command {
18178 title: "Command not in range".to_owned(),
18179 command: "_the/command".to_owned(),
18180 arguments: None,
18181 }),
18182 data: None,
18183 },
18184 ]))
18185 })
18186 .next()
18187 .await;
18188
18189 let actions = actions.await.unwrap();
18190 assert_eq!(
18191 actions.len(),
18192 1,
18193 "Should have only one valid action for the 0..0 range"
18194 );
18195 let action = actions[0].clone();
18196 let apply = project.update(cx, |project, cx| {
18197 project.apply_code_action(buffer.clone(), action, true, cx)
18198 });
18199
18200 // Resolving the code action does not populate its edits. In absence of
18201 // edits, we must execute the given command.
18202 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
18203 |mut lens, _| async move {
18204 let lens_command = lens.command.as_mut().expect("should have a command");
18205 assert_eq!(lens_command.title, "Code lens command");
18206 lens_command.arguments = Some(vec![json!("the-argument")]);
18207 Ok(lens)
18208 },
18209 );
18210
18211 // While executing the command, the language server sends the editor
18212 // a `workspaceEdit` request.
18213 fake_server
18214 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
18215 let fake = fake_server.clone();
18216 move |params, _| {
18217 assert_eq!(params.command, "_the/command");
18218 let fake = fake.clone();
18219 async move {
18220 fake.server
18221 .request::<lsp::request::ApplyWorkspaceEdit>(
18222 lsp::ApplyWorkspaceEditParams {
18223 label: None,
18224 edit: lsp::WorkspaceEdit {
18225 changes: Some(
18226 [(
18227 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
18228 vec![lsp::TextEdit {
18229 range: lsp::Range::new(
18230 lsp::Position::new(0, 0),
18231 lsp::Position::new(0, 0),
18232 ),
18233 new_text: "X".into(),
18234 }],
18235 )]
18236 .into_iter()
18237 .collect(),
18238 ),
18239 ..Default::default()
18240 },
18241 },
18242 )
18243 .await
18244 .unwrap();
18245 Ok(Some(json!(null)))
18246 }
18247 }
18248 })
18249 .next()
18250 .await;
18251
18252 // Applying the code lens command returns a project transaction containing the edits
18253 // sent by the language server in its `workspaceEdit` request.
18254 let transaction = apply.await.unwrap();
18255 assert!(transaction.0.contains_key(&buffer));
18256 buffer.update(cx, |buffer, cx| {
18257 assert_eq!(buffer.text(), "Xa");
18258 buffer.undo(cx);
18259 assert_eq!(buffer.text(), "a");
18260 });
18261}
18262
18263#[gpui::test]
18264async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
18265 init_test(cx, |_| {});
18266
18267 let fs = FakeFs::new(cx.executor());
18268 let main_text = r#"fn main() {
18269println!("1");
18270println!("2");
18271println!("3");
18272println!("4");
18273println!("5");
18274}"#;
18275 let lib_text = "mod foo {}";
18276 fs.insert_tree(
18277 path!("/a"),
18278 json!({
18279 "lib.rs": lib_text,
18280 "main.rs": main_text,
18281 }),
18282 )
18283 .await;
18284
18285 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18286 let (workspace, cx) =
18287 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
18288 let worktree_id = workspace.update(cx, |workspace, cx| {
18289 workspace.project().update(cx, |project, cx| {
18290 project.worktrees(cx).next().unwrap().read(cx).id()
18291 })
18292 });
18293
18294 let expected_ranges = vec![
18295 Point::new(0, 0)..Point::new(0, 0),
18296 Point::new(1, 0)..Point::new(1, 1),
18297 Point::new(2, 0)..Point::new(2, 2),
18298 Point::new(3, 0)..Point::new(3, 3),
18299 ];
18300
18301 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
18302 let editor_1 = workspace
18303 .update_in(cx, |workspace, window, cx| {
18304 workspace.open_path(
18305 (worktree_id, "main.rs"),
18306 Some(pane_1.downgrade()),
18307 true,
18308 window,
18309 cx,
18310 )
18311 })
18312 .unwrap()
18313 .await
18314 .downcast::<Editor>()
18315 .unwrap();
18316 pane_1.update(cx, |pane, cx| {
18317 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18318 open_editor.update(cx, |editor, cx| {
18319 assert_eq!(
18320 editor.display_text(cx),
18321 main_text,
18322 "Original main.rs text on initial open",
18323 );
18324 assert_eq!(
18325 editor
18326 .selections
18327 .all::<Point>(cx)
18328 .into_iter()
18329 .map(|s| s.range())
18330 .collect::<Vec<_>>(),
18331 vec![Point::zero()..Point::zero()],
18332 "Default selections on initial open",
18333 );
18334 })
18335 });
18336 editor_1.update_in(cx, |editor, window, cx| {
18337 editor.change_selections(None, window, cx, |s| {
18338 s.select_ranges(expected_ranges.clone());
18339 });
18340 });
18341
18342 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
18343 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
18344 });
18345 let editor_2 = workspace
18346 .update_in(cx, |workspace, window, cx| {
18347 workspace.open_path(
18348 (worktree_id, "main.rs"),
18349 Some(pane_2.downgrade()),
18350 true,
18351 window,
18352 cx,
18353 )
18354 })
18355 .unwrap()
18356 .await
18357 .downcast::<Editor>()
18358 .unwrap();
18359 pane_2.update(cx, |pane, cx| {
18360 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18361 open_editor.update(cx, |editor, cx| {
18362 assert_eq!(
18363 editor.display_text(cx),
18364 main_text,
18365 "Original main.rs text on initial open in another panel",
18366 );
18367 assert_eq!(
18368 editor
18369 .selections
18370 .all::<Point>(cx)
18371 .into_iter()
18372 .map(|s| s.range())
18373 .collect::<Vec<_>>(),
18374 vec![Point::zero()..Point::zero()],
18375 "Default selections on initial open in another panel",
18376 );
18377 })
18378 });
18379
18380 editor_2.update_in(cx, |editor, window, cx| {
18381 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
18382 });
18383
18384 let _other_editor_1 = workspace
18385 .update_in(cx, |workspace, window, cx| {
18386 workspace.open_path(
18387 (worktree_id, "lib.rs"),
18388 Some(pane_1.downgrade()),
18389 true,
18390 window,
18391 cx,
18392 )
18393 })
18394 .unwrap()
18395 .await
18396 .downcast::<Editor>()
18397 .unwrap();
18398 pane_1
18399 .update_in(cx, |pane, window, cx| {
18400 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
18401 .unwrap()
18402 })
18403 .await
18404 .unwrap();
18405 drop(editor_1);
18406 pane_1.update(cx, |pane, cx| {
18407 pane.active_item()
18408 .unwrap()
18409 .downcast::<Editor>()
18410 .unwrap()
18411 .update(cx, |editor, cx| {
18412 assert_eq!(
18413 editor.display_text(cx),
18414 lib_text,
18415 "Other file should be open and active",
18416 );
18417 });
18418 assert_eq!(pane.items().count(), 1, "No other editors should be open");
18419 });
18420
18421 let _other_editor_2 = workspace
18422 .update_in(cx, |workspace, window, cx| {
18423 workspace.open_path(
18424 (worktree_id, "lib.rs"),
18425 Some(pane_2.downgrade()),
18426 true,
18427 window,
18428 cx,
18429 )
18430 })
18431 .unwrap()
18432 .await
18433 .downcast::<Editor>()
18434 .unwrap();
18435 pane_2
18436 .update_in(cx, |pane, window, cx| {
18437 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
18438 .unwrap()
18439 })
18440 .await
18441 .unwrap();
18442 drop(editor_2);
18443 pane_2.update(cx, |pane, cx| {
18444 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18445 open_editor.update(cx, |editor, cx| {
18446 assert_eq!(
18447 editor.display_text(cx),
18448 lib_text,
18449 "Other file should be open and active in another panel too",
18450 );
18451 });
18452 assert_eq!(
18453 pane.items().count(),
18454 1,
18455 "No other editors should be open in another pane",
18456 );
18457 });
18458
18459 let _editor_1_reopened = workspace
18460 .update_in(cx, |workspace, window, cx| {
18461 workspace.open_path(
18462 (worktree_id, "main.rs"),
18463 Some(pane_1.downgrade()),
18464 true,
18465 window,
18466 cx,
18467 )
18468 })
18469 .unwrap()
18470 .await
18471 .downcast::<Editor>()
18472 .unwrap();
18473 let _editor_2_reopened = workspace
18474 .update_in(cx, |workspace, window, cx| {
18475 workspace.open_path(
18476 (worktree_id, "main.rs"),
18477 Some(pane_2.downgrade()),
18478 true,
18479 window,
18480 cx,
18481 )
18482 })
18483 .unwrap()
18484 .await
18485 .downcast::<Editor>()
18486 .unwrap();
18487 pane_1.update(cx, |pane, cx| {
18488 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18489 open_editor.update(cx, |editor, cx| {
18490 assert_eq!(
18491 editor.display_text(cx),
18492 main_text,
18493 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
18494 );
18495 assert_eq!(
18496 editor
18497 .selections
18498 .all::<Point>(cx)
18499 .into_iter()
18500 .map(|s| s.range())
18501 .collect::<Vec<_>>(),
18502 expected_ranges,
18503 "Previous editor in the 1st panel had selections and should get them restored on reopen",
18504 );
18505 })
18506 });
18507 pane_2.update(cx, |pane, cx| {
18508 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18509 open_editor.update(cx, |editor, cx| {
18510 assert_eq!(
18511 editor.display_text(cx),
18512 r#"fn main() {
18513⋯rintln!("1");
18514⋯intln!("2");
18515⋯ntln!("3");
18516println!("4");
18517println!("5");
18518}"#,
18519 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
18520 );
18521 assert_eq!(
18522 editor
18523 .selections
18524 .all::<Point>(cx)
18525 .into_iter()
18526 .map(|s| s.range())
18527 .collect::<Vec<_>>(),
18528 vec![Point::zero()..Point::zero()],
18529 "Previous editor in the 2nd pane had no selections changed hence should restore none",
18530 );
18531 })
18532 });
18533}
18534
18535#[gpui::test]
18536async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
18537 init_test(cx, |_| {});
18538
18539 let fs = FakeFs::new(cx.executor());
18540 let main_text = r#"fn main() {
18541println!("1");
18542println!("2");
18543println!("3");
18544println!("4");
18545println!("5");
18546}"#;
18547 let lib_text = "mod foo {}";
18548 fs.insert_tree(
18549 path!("/a"),
18550 json!({
18551 "lib.rs": lib_text,
18552 "main.rs": main_text,
18553 }),
18554 )
18555 .await;
18556
18557 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18558 let (workspace, cx) =
18559 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
18560 let worktree_id = workspace.update(cx, |workspace, cx| {
18561 workspace.project().update(cx, |project, cx| {
18562 project.worktrees(cx).next().unwrap().read(cx).id()
18563 })
18564 });
18565
18566 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
18567 let editor = workspace
18568 .update_in(cx, |workspace, window, cx| {
18569 workspace.open_path(
18570 (worktree_id, "main.rs"),
18571 Some(pane.downgrade()),
18572 true,
18573 window,
18574 cx,
18575 )
18576 })
18577 .unwrap()
18578 .await
18579 .downcast::<Editor>()
18580 .unwrap();
18581 pane.update(cx, |pane, cx| {
18582 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18583 open_editor.update(cx, |editor, cx| {
18584 assert_eq!(
18585 editor.display_text(cx),
18586 main_text,
18587 "Original main.rs text on initial open",
18588 );
18589 })
18590 });
18591 editor.update_in(cx, |editor, window, cx| {
18592 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
18593 });
18594
18595 cx.update_global(|store: &mut SettingsStore, cx| {
18596 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
18597 s.restore_on_file_reopen = Some(false);
18598 });
18599 });
18600 editor.update_in(cx, |editor, window, cx| {
18601 editor.fold_ranges(
18602 vec![
18603 Point::new(1, 0)..Point::new(1, 1),
18604 Point::new(2, 0)..Point::new(2, 2),
18605 Point::new(3, 0)..Point::new(3, 3),
18606 ],
18607 false,
18608 window,
18609 cx,
18610 );
18611 });
18612 pane.update_in(cx, |pane, window, cx| {
18613 pane.close_all_items(&CloseAllItems::default(), window, cx)
18614 .unwrap()
18615 })
18616 .await
18617 .unwrap();
18618 pane.update(cx, |pane, _| {
18619 assert!(pane.active_item().is_none());
18620 });
18621 cx.update_global(|store: &mut SettingsStore, cx| {
18622 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
18623 s.restore_on_file_reopen = Some(true);
18624 });
18625 });
18626
18627 let _editor_reopened = workspace
18628 .update_in(cx, |workspace, window, cx| {
18629 workspace.open_path(
18630 (worktree_id, "main.rs"),
18631 Some(pane.downgrade()),
18632 true,
18633 window,
18634 cx,
18635 )
18636 })
18637 .unwrap()
18638 .await
18639 .downcast::<Editor>()
18640 .unwrap();
18641 pane.update(cx, |pane, cx| {
18642 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18643 open_editor.update(cx, |editor, cx| {
18644 assert_eq!(
18645 editor.display_text(cx),
18646 main_text,
18647 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
18648 );
18649 })
18650 });
18651}
18652
18653fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
18654 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
18655 point..point
18656}
18657
18658fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
18659 let (text, ranges) = marked_text_ranges(marked_text, true);
18660 assert_eq!(editor.text(cx), text);
18661 assert_eq!(
18662 editor.selections.ranges(cx),
18663 ranges,
18664 "Assert selections are {}",
18665 marked_text
18666 );
18667}
18668
18669pub fn handle_signature_help_request(
18670 cx: &mut EditorLspTestContext,
18671 mocked_response: lsp::SignatureHelp,
18672) -> impl Future<Output = ()> {
18673 let mut request =
18674 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
18675 let mocked_response = mocked_response.clone();
18676 async move { Ok(Some(mocked_response)) }
18677 });
18678
18679 async move {
18680 request.next().await;
18681 }
18682}
18683
18684/// Handle completion request passing a marked string specifying where the completion
18685/// should be triggered from using '|' character, what range should be replaced, and what completions
18686/// should be returned using '<' and '>' to delimit the range
18687pub fn handle_completion_request(
18688 cx: &mut EditorLspTestContext,
18689 marked_string: &str,
18690 completions: Vec<&'static str>,
18691 counter: Arc<AtomicUsize>,
18692) -> impl Future<Output = ()> {
18693 let complete_from_marker: TextRangeMarker = '|'.into();
18694 let replace_range_marker: TextRangeMarker = ('<', '>').into();
18695 let (_, mut marked_ranges) = marked_text_ranges_by(
18696 marked_string,
18697 vec![complete_from_marker.clone(), replace_range_marker.clone()],
18698 );
18699
18700 let complete_from_position =
18701 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
18702 let replace_range =
18703 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
18704
18705 let mut request =
18706 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
18707 let completions = completions.clone();
18708 counter.fetch_add(1, atomic::Ordering::Release);
18709 async move {
18710 assert_eq!(params.text_document_position.text_document.uri, url.clone());
18711 assert_eq!(
18712 params.text_document_position.position,
18713 complete_from_position
18714 );
18715 Ok(Some(lsp::CompletionResponse::Array(
18716 completions
18717 .iter()
18718 .map(|completion_text| lsp::CompletionItem {
18719 label: completion_text.to_string(),
18720 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18721 range: replace_range,
18722 new_text: completion_text.to_string(),
18723 })),
18724 ..Default::default()
18725 })
18726 .collect(),
18727 )))
18728 }
18729 });
18730
18731 async move {
18732 request.next().await;
18733 }
18734}
18735
18736fn handle_resolve_completion_request(
18737 cx: &mut EditorLspTestContext,
18738 edits: Option<Vec<(&'static str, &'static str)>>,
18739) -> impl Future<Output = ()> {
18740 let edits = edits.map(|edits| {
18741 edits
18742 .iter()
18743 .map(|(marked_string, new_text)| {
18744 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
18745 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
18746 lsp::TextEdit::new(replace_range, new_text.to_string())
18747 })
18748 .collect::<Vec<_>>()
18749 });
18750
18751 let mut request =
18752 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
18753 let edits = edits.clone();
18754 async move {
18755 Ok(lsp::CompletionItem {
18756 additional_text_edits: edits,
18757 ..Default::default()
18758 })
18759 }
18760 });
18761
18762 async move {
18763 request.next().await;
18764 }
18765}
18766
18767pub(crate) fn update_test_language_settings(
18768 cx: &mut TestAppContext,
18769 f: impl Fn(&mut AllLanguageSettingsContent),
18770) {
18771 cx.update(|cx| {
18772 SettingsStore::update_global(cx, |store, cx| {
18773 store.update_user_settings::<AllLanguageSettings>(cx, f);
18774 });
18775 });
18776}
18777
18778pub(crate) fn update_test_project_settings(
18779 cx: &mut TestAppContext,
18780 f: impl Fn(&mut ProjectSettings),
18781) {
18782 cx.update(|cx| {
18783 SettingsStore::update_global(cx, |store, cx| {
18784 store.update_user_settings::<ProjectSettings>(cx, f);
18785 });
18786 });
18787}
18788
18789pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
18790 cx.update(|cx| {
18791 assets::Assets.load_test_fonts(cx);
18792 let store = SettingsStore::test(cx);
18793 cx.set_global(store);
18794 theme::init(theme::LoadThemes::JustBase, cx);
18795 release_channel::init(SemanticVersion::default(), cx);
18796 client::init_settings(cx);
18797 language::init(cx);
18798 Project::init_settings(cx);
18799 workspace::init_settings(cx);
18800 crate::init(cx);
18801 });
18802
18803 update_test_language_settings(cx, f);
18804}
18805
18806#[track_caller]
18807fn assert_hunk_revert(
18808 not_reverted_text_with_selections: &str,
18809 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
18810 expected_reverted_text_with_selections: &str,
18811 base_text: &str,
18812 cx: &mut EditorLspTestContext,
18813) {
18814 cx.set_state(not_reverted_text_with_selections);
18815 cx.set_head_text(base_text);
18816 cx.executor().run_until_parked();
18817
18818 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
18819 let snapshot = editor.snapshot(window, cx);
18820 let reverted_hunk_statuses = snapshot
18821 .buffer_snapshot
18822 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
18823 .map(|hunk| hunk.status().kind)
18824 .collect::<Vec<_>>();
18825
18826 editor.git_restore(&Default::default(), window, cx);
18827 reverted_hunk_statuses
18828 });
18829 cx.executor().run_until_parked();
18830 cx.assert_editor_state(expected_reverted_text_with_selections);
18831 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
18832}