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::{BreakpointKind, BreakpointState, SerializedBreakpoint},
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 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 // Test backspace with line_mode set to true
3274 cx.update_editor(|e, _, _| e.selections.line_mode = true);
3275 cx.set_state(indoc! {"
3276 The ˇquick ˇbrown
3277 fox jumps over
3278 the lazy dog
3279 ˇThe qu«ick bˇ»rown"});
3280 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3281 cx.assert_editor_state(indoc! {"
3282 ˇfox jumps over
3283 the lazy dogˇ"});
3284}
3285
3286#[gpui::test]
3287async fn test_delete(cx: &mut TestAppContext) {
3288 init_test(cx, |_| {});
3289
3290 let mut cx = EditorTestContext::new(cx).await;
3291 cx.set_state(indoc! {"
3292 onˇe two three
3293 fou«rˇ» five six
3294 seven «ˇeight nine
3295 »ten
3296 "});
3297 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3298 cx.assert_editor_state(indoc! {"
3299 onˇ two three
3300 fouˇ five six
3301 seven ˇten
3302 "});
3303
3304 // Test backspace with line_mode set to true
3305 cx.update_editor(|e, _, _| e.selections.line_mode = true);
3306 cx.set_state(indoc! {"
3307 The ˇquick ˇbrown
3308 fox «ˇjum»ps over
3309 the lazy dog
3310 ˇThe qu«ick bˇ»rown"});
3311 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3312 cx.assert_editor_state("ˇthe lazy dogˇ");
3313}
3314
3315#[gpui::test]
3316fn test_delete_line(cx: &mut TestAppContext) {
3317 init_test(cx, |_| {});
3318
3319 let editor = cx.add_window(|window, cx| {
3320 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3321 build_editor(buffer, window, cx)
3322 });
3323 _ = editor.update(cx, |editor, window, cx| {
3324 editor.change_selections(None, window, cx, |s| {
3325 s.select_display_ranges([
3326 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3327 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3328 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3329 ])
3330 });
3331 editor.delete_line(&DeleteLine, window, cx);
3332 assert_eq!(editor.display_text(cx), "ghi");
3333 assert_eq!(
3334 editor.selections.display_ranges(cx),
3335 vec![
3336 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3337 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3338 ]
3339 );
3340 });
3341
3342 let editor = cx.add_window(|window, cx| {
3343 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3344 build_editor(buffer, window, cx)
3345 });
3346 _ = editor.update(cx, |editor, window, cx| {
3347 editor.change_selections(None, window, cx, |s| {
3348 s.select_display_ranges([
3349 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3350 ])
3351 });
3352 editor.delete_line(&DeleteLine, window, cx);
3353 assert_eq!(editor.display_text(cx), "ghi\n");
3354 assert_eq!(
3355 editor.selections.display_ranges(cx),
3356 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3357 );
3358 });
3359}
3360
3361#[gpui::test]
3362fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3363 init_test(cx, |_| {});
3364
3365 cx.add_window(|window, cx| {
3366 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3367 let mut editor = build_editor(buffer.clone(), window, cx);
3368 let buffer = buffer.read(cx).as_singleton().unwrap();
3369
3370 assert_eq!(
3371 editor.selections.ranges::<Point>(cx),
3372 &[Point::new(0, 0)..Point::new(0, 0)]
3373 );
3374
3375 // When on single line, replace newline at end by space
3376 editor.join_lines(&JoinLines, window, cx);
3377 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3378 assert_eq!(
3379 editor.selections.ranges::<Point>(cx),
3380 &[Point::new(0, 3)..Point::new(0, 3)]
3381 );
3382
3383 // When multiple lines are selected, remove newlines that are spanned by the selection
3384 editor.change_selections(None, window, cx, |s| {
3385 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3386 });
3387 editor.join_lines(&JoinLines, window, cx);
3388 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3389 assert_eq!(
3390 editor.selections.ranges::<Point>(cx),
3391 &[Point::new(0, 11)..Point::new(0, 11)]
3392 );
3393
3394 // Undo should be transactional
3395 editor.undo(&Undo, window, cx);
3396 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3397 assert_eq!(
3398 editor.selections.ranges::<Point>(cx),
3399 &[Point::new(0, 5)..Point::new(2, 2)]
3400 );
3401
3402 // When joining an empty line don't insert a space
3403 editor.change_selections(None, window, cx, |s| {
3404 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3405 });
3406 editor.join_lines(&JoinLines, window, cx);
3407 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3408 assert_eq!(
3409 editor.selections.ranges::<Point>(cx),
3410 [Point::new(2, 3)..Point::new(2, 3)]
3411 );
3412
3413 // We can remove trailing newlines
3414 editor.join_lines(&JoinLines, window, cx);
3415 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3416 assert_eq!(
3417 editor.selections.ranges::<Point>(cx),
3418 [Point::new(2, 3)..Point::new(2, 3)]
3419 );
3420
3421 // We don't blow up on the last line
3422 editor.join_lines(&JoinLines, window, cx);
3423 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3424 assert_eq!(
3425 editor.selections.ranges::<Point>(cx),
3426 [Point::new(2, 3)..Point::new(2, 3)]
3427 );
3428
3429 // reset to test indentation
3430 editor.buffer.update(cx, |buffer, cx| {
3431 buffer.edit(
3432 [
3433 (Point::new(1, 0)..Point::new(1, 2), " "),
3434 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3435 ],
3436 None,
3437 cx,
3438 )
3439 });
3440
3441 // We remove any leading spaces
3442 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3443 editor.change_selections(None, window, cx, |s| {
3444 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3445 });
3446 editor.join_lines(&JoinLines, window, cx);
3447 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3448
3449 // We don't insert a space for a line containing only spaces
3450 editor.join_lines(&JoinLines, window, cx);
3451 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3452
3453 // We ignore any leading tabs
3454 editor.join_lines(&JoinLines, window, cx);
3455 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3456
3457 editor
3458 });
3459}
3460
3461#[gpui::test]
3462fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3463 init_test(cx, |_| {});
3464
3465 cx.add_window(|window, cx| {
3466 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3467 let mut editor = build_editor(buffer.clone(), window, cx);
3468 let buffer = buffer.read(cx).as_singleton().unwrap();
3469
3470 editor.change_selections(None, window, cx, |s| {
3471 s.select_ranges([
3472 Point::new(0, 2)..Point::new(1, 1),
3473 Point::new(1, 2)..Point::new(1, 2),
3474 Point::new(3, 1)..Point::new(3, 2),
3475 ])
3476 });
3477
3478 editor.join_lines(&JoinLines, window, cx);
3479 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3480
3481 assert_eq!(
3482 editor.selections.ranges::<Point>(cx),
3483 [
3484 Point::new(0, 7)..Point::new(0, 7),
3485 Point::new(1, 3)..Point::new(1, 3)
3486 ]
3487 );
3488 editor
3489 });
3490}
3491
3492#[gpui::test]
3493async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3494 init_test(cx, |_| {});
3495
3496 let mut cx = EditorTestContext::new(cx).await;
3497
3498 let diff_base = r#"
3499 Line 0
3500 Line 1
3501 Line 2
3502 Line 3
3503 "#
3504 .unindent();
3505
3506 cx.set_state(
3507 &r#"
3508 ˇLine 0
3509 Line 1
3510 Line 2
3511 Line 3
3512 "#
3513 .unindent(),
3514 );
3515
3516 cx.set_head_text(&diff_base);
3517 executor.run_until_parked();
3518
3519 // Join lines
3520 cx.update_editor(|editor, window, cx| {
3521 editor.join_lines(&JoinLines, window, cx);
3522 });
3523 executor.run_until_parked();
3524
3525 cx.assert_editor_state(
3526 &r#"
3527 Line 0ˇ Line 1
3528 Line 2
3529 Line 3
3530 "#
3531 .unindent(),
3532 );
3533 // Join again
3534 cx.update_editor(|editor, window, cx| {
3535 editor.join_lines(&JoinLines, window, cx);
3536 });
3537 executor.run_until_parked();
3538
3539 cx.assert_editor_state(
3540 &r#"
3541 Line 0 Line 1ˇ Line 2
3542 Line 3
3543 "#
3544 .unindent(),
3545 );
3546}
3547
3548#[gpui::test]
3549async fn test_custom_newlines_cause_no_false_positive_diffs(
3550 executor: BackgroundExecutor,
3551 cx: &mut TestAppContext,
3552) {
3553 init_test(cx, |_| {});
3554 let mut cx = EditorTestContext::new(cx).await;
3555 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3556 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3557 executor.run_until_parked();
3558
3559 cx.update_editor(|editor, window, cx| {
3560 let snapshot = editor.snapshot(window, cx);
3561 assert_eq!(
3562 snapshot
3563 .buffer_snapshot
3564 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3565 .collect::<Vec<_>>(),
3566 Vec::new(),
3567 "Should not have any diffs for files with custom newlines"
3568 );
3569 });
3570}
3571
3572#[gpui::test]
3573async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3574 init_test(cx, |_| {});
3575
3576 let mut cx = EditorTestContext::new(cx).await;
3577
3578 // Test sort_lines_case_insensitive()
3579 cx.set_state(indoc! {"
3580 «z
3581 y
3582 x
3583 Z
3584 Y
3585 Xˇ»
3586 "});
3587 cx.update_editor(|e, window, cx| {
3588 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3589 });
3590 cx.assert_editor_state(indoc! {"
3591 «x
3592 X
3593 y
3594 Y
3595 z
3596 Zˇ»
3597 "});
3598
3599 // Test reverse_lines()
3600 cx.set_state(indoc! {"
3601 «5
3602 4
3603 3
3604 2
3605 1ˇ»
3606 "});
3607 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
3608 cx.assert_editor_state(indoc! {"
3609 «1
3610 2
3611 3
3612 4
3613 5ˇ»
3614 "});
3615
3616 // Skip testing shuffle_line()
3617
3618 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
3619 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
3620
3621 // Don't manipulate when cursor is on single line, but expand the selection
3622 cx.set_state(indoc! {"
3623 ddˇdd
3624 ccc
3625 bb
3626 a
3627 "});
3628 cx.update_editor(|e, window, cx| {
3629 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3630 });
3631 cx.assert_editor_state(indoc! {"
3632 «ddddˇ»
3633 ccc
3634 bb
3635 a
3636 "});
3637
3638 // Basic manipulate case
3639 // Start selection moves to column 0
3640 // End of selection shrinks to fit shorter line
3641 cx.set_state(indoc! {"
3642 dd«d
3643 ccc
3644 bb
3645 aaaaaˇ»
3646 "});
3647 cx.update_editor(|e, window, cx| {
3648 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3649 });
3650 cx.assert_editor_state(indoc! {"
3651 «aaaaa
3652 bb
3653 ccc
3654 dddˇ»
3655 "});
3656
3657 // Manipulate case with newlines
3658 cx.set_state(indoc! {"
3659 dd«d
3660 ccc
3661
3662 bb
3663 aaaaa
3664
3665 ˇ»
3666 "});
3667 cx.update_editor(|e, window, cx| {
3668 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3669 });
3670 cx.assert_editor_state(indoc! {"
3671 «
3672
3673 aaaaa
3674 bb
3675 ccc
3676 dddˇ»
3677
3678 "});
3679
3680 // Adding new line
3681 cx.set_state(indoc! {"
3682 aa«a
3683 bbˇ»b
3684 "});
3685 cx.update_editor(|e, window, cx| {
3686 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
3687 });
3688 cx.assert_editor_state(indoc! {"
3689 «aaa
3690 bbb
3691 added_lineˇ»
3692 "});
3693
3694 // Removing line
3695 cx.set_state(indoc! {"
3696 aa«a
3697 bbbˇ»
3698 "});
3699 cx.update_editor(|e, window, cx| {
3700 e.manipulate_lines(window, cx, |lines| {
3701 lines.pop();
3702 })
3703 });
3704 cx.assert_editor_state(indoc! {"
3705 «aaaˇ»
3706 "});
3707
3708 // Removing all lines
3709 cx.set_state(indoc! {"
3710 aa«a
3711 bbbˇ»
3712 "});
3713 cx.update_editor(|e, window, cx| {
3714 e.manipulate_lines(window, cx, |lines| {
3715 lines.drain(..);
3716 })
3717 });
3718 cx.assert_editor_state(indoc! {"
3719 ˇ
3720 "});
3721}
3722
3723#[gpui::test]
3724async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
3725 init_test(cx, |_| {});
3726
3727 let mut cx = EditorTestContext::new(cx).await;
3728
3729 // Consider continuous selection as single selection
3730 cx.set_state(indoc! {"
3731 Aaa«aa
3732 cˇ»c«c
3733 bb
3734 aaaˇ»aa
3735 "});
3736 cx.update_editor(|e, window, cx| {
3737 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3738 });
3739 cx.assert_editor_state(indoc! {"
3740 «Aaaaa
3741 ccc
3742 bb
3743 aaaaaˇ»
3744 "});
3745
3746 cx.set_state(indoc! {"
3747 Aaa«aa
3748 cˇ»c«c
3749 bb
3750 aaaˇ»aa
3751 "});
3752 cx.update_editor(|e, window, cx| {
3753 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3754 });
3755 cx.assert_editor_state(indoc! {"
3756 «Aaaaa
3757 ccc
3758 bbˇ»
3759 "});
3760
3761 // Consider non continuous selection as distinct dedup operations
3762 cx.set_state(indoc! {"
3763 «aaaaa
3764 bb
3765 aaaaa
3766 aaaaaˇ»
3767
3768 aaa«aaˇ»
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 «aaaaa
3775 bbˇ»
3776
3777 «aaaaaˇ»
3778 "});
3779}
3780
3781#[gpui::test]
3782async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
3783 init_test(cx, |_| {});
3784
3785 let mut cx = EditorTestContext::new(cx).await;
3786
3787 cx.set_state(indoc! {"
3788 «Aaa
3789 aAa
3790 Aaaˇ»
3791 "});
3792 cx.update_editor(|e, window, cx| {
3793 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3794 });
3795 cx.assert_editor_state(indoc! {"
3796 «Aaa
3797 aAaˇ»
3798 "});
3799
3800 cx.set_state(indoc! {"
3801 «Aaa
3802 aAa
3803 aaAˇ»
3804 "});
3805 cx.update_editor(|e, window, cx| {
3806 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3807 });
3808 cx.assert_editor_state(indoc! {"
3809 «Aaaˇ»
3810 "});
3811}
3812
3813#[gpui::test]
3814async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
3815 init_test(cx, |_| {});
3816
3817 let mut cx = EditorTestContext::new(cx).await;
3818
3819 // Manipulate with multiple selections on a single line
3820 cx.set_state(indoc! {"
3821 dd«dd
3822 cˇ»c«c
3823 bb
3824 aaaˇ»aa
3825 "});
3826 cx.update_editor(|e, window, cx| {
3827 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3828 });
3829 cx.assert_editor_state(indoc! {"
3830 «aaaaa
3831 bb
3832 ccc
3833 ddddˇ»
3834 "});
3835
3836 // Manipulate with multiple disjoin selections
3837 cx.set_state(indoc! {"
3838 5«
3839 4
3840 3
3841 2
3842 1ˇ»
3843
3844 dd«dd
3845 ccc
3846 bb
3847 aaaˇ»aa
3848 "});
3849 cx.update_editor(|e, window, cx| {
3850 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3851 });
3852 cx.assert_editor_state(indoc! {"
3853 «1
3854 2
3855 3
3856 4
3857 5ˇ»
3858
3859 «aaaaa
3860 bb
3861 ccc
3862 ddddˇ»
3863 "});
3864
3865 // Adding lines on each selection
3866 cx.set_state(indoc! {"
3867 2«
3868 1ˇ»
3869
3870 bb«bb
3871 aaaˇ»aa
3872 "});
3873 cx.update_editor(|e, window, cx| {
3874 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
3875 });
3876 cx.assert_editor_state(indoc! {"
3877 «2
3878 1
3879 added lineˇ»
3880
3881 «bbbb
3882 aaaaa
3883 added lineˇ»
3884 "});
3885
3886 // Removing lines on each selection
3887 cx.set_state(indoc! {"
3888 2«
3889 1ˇ»
3890
3891 bb«bb
3892 aaaˇ»aa
3893 "});
3894 cx.update_editor(|e, window, cx| {
3895 e.manipulate_lines(window, cx, |lines| {
3896 lines.pop();
3897 })
3898 });
3899 cx.assert_editor_state(indoc! {"
3900 «2ˇ»
3901
3902 «bbbbˇ»
3903 "});
3904}
3905
3906#[gpui::test]
3907async fn test_manipulate_text(cx: &mut TestAppContext) {
3908 init_test(cx, |_| {});
3909
3910 let mut cx = EditorTestContext::new(cx).await;
3911
3912 // Test convert_to_upper_case()
3913 cx.set_state(indoc! {"
3914 «hello worldˇ»
3915 "});
3916 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3917 cx.assert_editor_state(indoc! {"
3918 «HELLO WORLDˇ»
3919 "});
3920
3921 // Test convert_to_lower_case()
3922 cx.set_state(indoc! {"
3923 «HELLO WORLDˇ»
3924 "});
3925 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
3926 cx.assert_editor_state(indoc! {"
3927 «hello worldˇ»
3928 "});
3929
3930 // Test multiple line, single selection case
3931 cx.set_state(indoc! {"
3932 «The quick brown
3933 fox jumps over
3934 the lazy dogˇ»
3935 "});
3936 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
3937 cx.assert_editor_state(indoc! {"
3938 «The Quick Brown
3939 Fox Jumps Over
3940 The Lazy Dogˇ»
3941 "});
3942
3943 // Test multiple line, single selection case
3944 cx.set_state(indoc! {"
3945 «The quick brown
3946 fox jumps over
3947 the lazy dogˇ»
3948 "});
3949 cx.update_editor(|e, window, cx| {
3950 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
3951 });
3952 cx.assert_editor_state(indoc! {"
3953 «TheQuickBrown
3954 FoxJumpsOver
3955 TheLazyDogˇ»
3956 "});
3957
3958 // From here on out, test more complex cases of manipulate_text()
3959
3960 // Test no selection case - should affect words cursors are in
3961 // Cursor at beginning, middle, and end of word
3962 cx.set_state(indoc! {"
3963 ˇhello big beauˇtiful worldˇ
3964 "});
3965 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3966 cx.assert_editor_state(indoc! {"
3967 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
3968 "});
3969
3970 // Test multiple selections on a single line and across multiple lines
3971 cx.set_state(indoc! {"
3972 «Theˇ» quick «brown
3973 foxˇ» jumps «overˇ»
3974 the «lazyˇ» dog
3975 "});
3976 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3977 cx.assert_editor_state(indoc! {"
3978 «THEˇ» quick «BROWN
3979 FOXˇ» jumps «OVERˇ»
3980 the «LAZYˇ» dog
3981 "});
3982
3983 // Test case where text length grows
3984 cx.set_state(indoc! {"
3985 «tschüߡ»
3986 "});
3987 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3988 cx.assert_editor_state(indoc! {"
3989 «TSCHÜSSˇ»
3990 "});
3991
3992 // Test to make sure we don't crash when text shrinks
3993 cx.set_state(indoc! {"
3994 aaa_bbbˇ
3995 "});
3996 cx.update_editor(|e, window, cx| {
3997 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
3998 });
3999 cx.assert_editor_state(indoc! {"
4000 «aaaBbbˇ»
4001 "});
4002
4003 // Test to make sure we all aware of the fact that each word can grow and shrink
4004 // Final selections should be aware of this fact
4005 cx.set_state(indoc! {"
4006 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4007 "});
4008 cx.update_editor(|e, window, cx| {
4009 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4010 });
4011 cx.assert_editor_state(indoc! {"
4012 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4013 "});
4014
4015 cx.set_state(indoc! {"
4016 «hElLo, WoRld!ˇ»
4017 "});
4018 cx.update_editor(|e, window, cx| {
4019 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4020 });
4021 cx.assert_editor_state(indoc! {"
4022 «HeLlO, wOrLD!ˇ»
4023 "});
4024}
4025
4026#[gpui::test]
4027fn test_duplicate_line(cx: &mut TestAppContext) {
4028 init_test(cx, |_| {});
4029
4030 let editor = cx.add_window(|window, cx| {
4031 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4032 build_editor(buffer, window, cx)
4033 });
4034 _ = editor.update(cx, |editor, window, cx| {
4035 editor.change_selections(None, window, cx, |s| {
4036 s.select_display_ranges([
4037 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4038 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4039 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4040 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4041 ])
4042 });
4043 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4044 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4045 assert_eq!(
4046 editor.selections.display_ranges(cx),
4047 vec![
4048 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4049 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4050 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4051 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4052 ]
4053 );
4054 });
4055
4056 let editor = cx.add_window(|window, cx| {
4057 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4058 build_editor(buffer, window, cx)
4059 });
4060 _ = editor.update(cx, |editor, window, cx| {
4061 editor.change_selections(None, window, cx, |s| {
4062 s.select_display_ranges([
4063 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4064 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4065 ])
4066 });
4067 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4068 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4069 assert_eq!(
4070 editor.selections.display_ranges(cx),
4071 vec![
4072 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4073 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4074 ]
4075 );
4076 });
4077
4078 // With `move_upwards` the selections stay in place, except for
4079 // the lines inserted above them
4080 let editor = cx.add_window(|window, cx| {
4081 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4082 build_editor(buffer, window, cx)
4083 });
4084 _ = editor.update(cx, |editor, window, cx| {
4085 editor.change_selections(None, window, cx, |s| {
4086 s.select_display_ranges([
4087 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4088 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4089 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4090 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4091 ])
4092 });
4093 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4094 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4095 assert_eq!(
4096 editor.selections.display_ranges(cx),
4097 vec![
4098 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4099 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4100 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4101 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
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_line_up(&DuplicateLineUp, window, cx);
4118 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\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(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4124 ]
4125 );
4126 });
4127
4128 let editor = cx.add_window(|window, cx| {
4129 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4130 build_editor(buffer, window, cx)
4131 });
4132 _ = editor.update(cx, |editor, window, cx| {
4133 editor.change_selections(None, window, cx, |s| {
4134 s.select_display_ranges([
4135 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4136 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4137 ])
4138 });
4139 editor.duplicate_selection(&DuplicateSelection, window, cx);
4140 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4141 assert_eq!(
4142 editor.selections.display_ranges(cx),
4143 vec![
4144 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4145 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4146 ]
4147 );
4148 });
4149}
4150
4151#[gpui::test]
4152fn test_move_line_up_down(cx: &mut TestAppContext) {
4153 init_test(cx, |_| {});
4154
4155 let editor = cx.add_window(|window, cx| {
4156 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4157 build_editor(buffer, window, cx)
4158 });
4159 _ = editor.update(cx, |editor, window, cx| {
4160 editor.fold_creases(
4161 vec![
4162 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4163 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4164 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4165 ],
4166 true,
4167 window,
4168 cx,
4169 );
4170 editor.change_selections(None, window, cx, |s| {
4171 s.select_display_ranges([
4172 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4173 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4174 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4175 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4176 ])
4177 });
4178 assert_eq!(
4179 editor.display_text(cx),
4180 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4181 );
4182
4183 editor.move_line_up(&MoveLineUp, window, cx);
4184 assert_eq!(
4185 editor.display_text(cx),
4186 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4187 );
4188 assert_eq!(
4189 editor.selections.display_ranges(cx),
4190 vec![
4191 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4192 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4193 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4194 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4195 ]
4196 );
4197 });
4198
4199 _ = editor.update(cx, |editor, window, cx| {
4200 editor.move_line_down(&MoveLineDown, window, cx);
4201 assert_eq!(
4202 editor.display_text(cx),
4203 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4204 );
4205 assert_eq!(
4206 editor.selections.display_ranges(cx),
4207 vec![
4208 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4209 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4210 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4211 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4212 ]
4213 );
4214 });
4215
4216 _ = editor.update(cx, |editor, window, cx| {
4217 editor.move_line_down(&MoveLineDown, window, cx);
4218 assert_eq!(
4219 editor.display_text(cx),
4220 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4221 );
4222 assert_eq!(
4223 editor.selections.display_ranges(cx),
4224 vec![
4225 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4226 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4227 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4228 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4229 ]
4230 );
4231 });
4232
4233 _ = editor.update(cx, |editor, window, cx| {
4234 editor.move_line_up(&MoveLineUp, window, cx);
4235 assert_eq!(
4236 editor.display_text(cx),
4237 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4238 );
4239 assert_eq!(
4240 editor.selections.display_ranges(cx),
4241 vec![
4242 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4243 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4244 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4245 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4246 ]
4247 );
4248 });
4249}
4250
4251#[gpui::test]
4252fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4253 init_test(cx, |_| {});
4254
4255 let editor = cx.add_window(|window, cx| {
4256 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4257 build_editor(buffer, window, cx)
4258 });
4259 _ = editor.update(cx, |editor, window, cx| {
4260 let snapshot = editor.buffer.read(cx).snapshot(cx);
4261 editor.insert_blocks(
4262 [BlockProperties {
4263 style: BlockStyle::Fixed,
4264 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4265 height: 1,
4266 render: Arc::new(|_| div().into_any()),
4267 priority: 0,
4268 }],
4269 Some(Autoscroll::fit()),
4270 cx,
4271 );
4272 editor.change_selections(None, window, cx, |s| {
4273 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4274 });
4275 editor.move_line_down(&MoveLineDown, window, cx);
4276 });
4277}
4278
4279#[gpui::test]
4280async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4281 init_test(cx, |_| {});
4282
4283 let mut cx = EditorTestContext::new(cx).await;
4284 cx.set_state(
4285 &"
4286 ˇzero
4287 one
4288 two
4289 three
4290 four
4291 five
4292 "
4293 .unindent(),
4294 );
4295
4296 // Create a four-line block that replaces three lines of text.
4297 cx.update_editor(|editor, window, cx| {
4298 let snapshot = editor.snapshot(window, cx);
4299 let snapshot = &snapshot.buffer_snapshot;
4300 let placement = BlockPlacement::Replace(
4301 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4302 );
4303 editor.insert_blocks(
4304 [BlockProperties {
4305 placement,
4306 height: 4,
4307 style: BlockStyle::Sticky,
4308 render: Arc::new(|_| gpui::div().into_any_element()),
4309 priority: 0,
4310 }],
4311 None,
4312 cx,
4313 );
4314 });
4315
4316 // Move down so that the cursor touches the block.
4317 cx.update_editor(|editor, window, cx| {
4318 editor.move_down(&Default::default(), window, cx);
4319 });
4320 cx.assert_editor_state(
4321 &"
4322 zero
4323 «one
4324 two
4325 threeˇ»
4326 four
4327 five
4328 "
4329 .unindent(),
4330 );
4331
4332 // Move down past the block.
4333 cx.update_editor(|editor, window, cx| {
4334 editor.move_down(&Default::default(), window, cx);
4335 });
4336 cx.assert_editor_state(
4337 &"
4338 zero
4339 one
4340 two
4341 three
4342 ˇfour
4343 five
4344 "
4345 .unindent(),
4346 );
4347}
4348
4349#[gpui::test]
4350fn test_transpose(cx: &mut TestAppContext) {
4351 init_test(cx, |_| {});
4352
4353 _ = cx.add_window(|window, cx| {
4354 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4355 editor.set_style(EditorStyle::default(), window, cx);
4356 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4357 editor.transpose(&Default::default(), window, cx);
4358 assert_eq!(editor.text(cx), "bac");
4359 assert_eq!(editor.selections.ranges(cx), [2..2]);
4360
4361 editor.transpose(&Default::default(), window, cx);
4362 assert_eq!(editor.text(cx), "bca");
4363 assert_eq!(editor.selections.ranges(cx), [3..3]);
4364
4365 editor.transpose(&Default::default(), window, cx);
4366 assert_eq!(editor.text(cx), "bac");
4367 assert_eq!(editor.selections.ranges(cx), [3..3]);
4368
4369 editor
4370 });
4371
4372 _ = cx.add_window(|window, cx| {
4373 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4374 editor.set_style(EditorStyle::default(), window, cx);
4375 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4376 editor.transpose(&Default::default(), window, cx);
4377 assert_eq!(editor.text(cx), "acb\nde");
4378 assert_eq!(editor.selections.ranges(cx), [3..3]);
4379
4380 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4381 editor.transpose(&Default::default(), window, cx);
4382 assert_eq!(editor.text(cx), "acbd\ne");
4383 assert_eq!(editor.selections.ranges(cx), [5..5]);
4384
4385 editor.transpose(&Default::default(), window, cx);
4386 assert_eq!(editor.text(cx), "acbde\n");
4387 assert_eq!(editor.selections.ranges(cx), [6..6]);
4388
4389 editor.transpose(&Default::default(), window, cx);
4390 assert_eq!(editor.text(cx), "acbd\ne");
4391 assert_eq!(editor.selections.ranges(cx), [6..6]);
4392
4393 editor
4394 });
4395
4396 _ = cx.add_window(|window, cx| {
4397 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4398 editor.set_style(EditorStyle::default(), window, cx);
4399 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4400 editor.transpose(&Default::default(), window, cx);
4401 assert_eq!(editor.text(cx), "bacd\ne");
4402 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4403
4404 editor.transpose(&Default::default(), window, cx);
4405 assert_eq!(editor.text(cx), "bcade\n");
4406 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4407
4408 editor.transpose(&Default::default(), window, cx);
4409 assert_eq!(editor.text(cx), "bcda\ne");
4410 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4411
4412 editor.transpose(&Default::default(), window, cx);
4413 assert_eq!(editor.text(cx), "bcade\n");
4414 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4415
4416 editor.transpose(&Default::default(), window, cx);
4417 assert_eq!(editor.text(cx), "bcaed\n");
4418 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4419
4420 editor
4421 });
4422
4423 _ = cx.add_window(|window, cx| {
4424 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4425 editor.set_style(EditorStyle::default(), window, cx);
4426 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4427 editor.transpose(&Default::default(), window, cx);
4428 assert_eq!(editor.text(cx), "🏀🍐✋");
4429 assert_eq!(editor.selections.ranges(cx), [8..8]);
4430
4431 editor.transpose(&Default::default(), window, cx);
4432 assert_eq!(editor.text(cx), "🏀✋🍐");
4433 assert_eq!(editor.selections.ranges(cx), [11..11]);
4434
4435 editor.transpose(&Default::default(), window, cx);
4436 assert_eq!(editor.text(cx), "🏀🍐✋");
4437 assert_eq!(editor.selections.ranges(cx), [11..11]);
4438
4439 editor
4440 });
4441}
4442
4443#[gpui::test]
4444async fn test_rewrap(cx: &mut TestAppContext) {
4445 init_test(cx, |settings| {
4446 settings.languages.extend([
4447 (
4448 "Markdown".into(),
4449 LanguageSettingsContent {
4450 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4451 ..Default::default()
4452 },
4453 ),
4454 (
4455 "Plain Text".into(),
4456 LanguageSettingsContent {
4457 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4458 ..Default::default()
4459 },
4460 ),
4461 ])
4462 });
4463
4464 let mut cx = EditorTestContext::new(cx).await;
4465
4466 let language_with_c_comments = Arc::new(Language::new(
4467 LanguageConfig {
4468 line_comments: vec!["// ".into()],
4469 ..LanguageConfig::default()
4470 },
4471 None,
4472 ));
4473 let language_with_pound_comments = Arc::new(Language::new(
4474 LanguageConfig {
4475 line_comments: vec!["# ".into()],
4476 ..LanguageConfig::default()
4477 },
4478 None,
4479 ));
4480 let markdown_language = Arc::new(Language::new(
4481 LanguageConfig {
4482 name: "Markdown".into(),
4483 ..LanguageConfig::default()
4484 },
4485 None,
4486 ));
4487 let language_with_doc_comments = Arc::new(Language::new(
4488 LanguageConfig {
4489 line_comments: vec!["// ".into(), "/// ".into()],
4490 ..LanguageConfig::default()
4491 },
4492 Some(tree_sitter_rust::LANGUAGE.into()),
4493 ));
4494
4495 let plaintext_language = Arc::new(Language::new(
4496 LanguageConfig {
4497 name: "Plain Text".into(),
4498 ..LanguageConfig::default()
4499 },
4500 None,
4501 ));
4502
4503 assert_rewrap(
4504 indoc! {"
4505 // ˇ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.
4506 "},
4507 indoc! {"
4508 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4509 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4510 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4511 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4512 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4513 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4514 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4515 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4516 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4517 // porttitor id. Aliquam id accumsan eros.
4518 "},
4519 language_with_c_comments.clone(),
4520 &mut cx,
4521 );
4522
4523 // Test that rewrapping works inside of a selection
4524 assert_rewrap(
4525 indoc! {"
4526 «// 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.ˇ»
4527 "},
4528 indoc! {"
4529 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4530 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4531 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4532 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4533 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4534 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4535 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4536 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4537 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4538 // porttitor id. Aliquam id accumsan eros.ˇ»
4539 "},
4540 language_with_c_comments.clone(),
4541 &mut cx,
4542 );
4543
4544 // Test that cursors that expand to the same region are collapsed.
4545 assert_rewrap(
4546 indoc! {"
4547 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4548 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4549 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4550 // ˇ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.
4551 "},
4552 indoc! {"
4553 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4554 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4555 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
4556 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4557 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
4558 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4559 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4560 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4561 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4562 // porttitor id. Aliquam id accumsan eros.
4563 "},
4564 language_with_c_comments.clone(),
4565 &mut cx,
4566 );
4567
4568 // Test that non-contiguous selections are treated separately.
4569 assert_rewrap(
4570 indoc! {"
4571 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4572 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4573 //
4574 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4575 // ˇ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
4580 // auctor, eu lacinia sapien scelerisque.
4581 //
4582 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
4583 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4584 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
4585 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
4586 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
4587 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
4588 // vulputate turpis porttitor id. Aliquam id accumsan eros.
4589 "},
4590 language_with_c_comments.clone(),
4591 &mut cx,
4592 );
4593
4594 // Test that different comment prefixes are supported.
4595 assert_rewrap(
4596 indoc! {"
4597 # ˇ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.
4598 "},
4599 indoc! {"
4600 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4601 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4602 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4603 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4604 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
4605 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
4606 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
4607 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
4608 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
4609 # accumsan eros.
4610 "},
4611 language_with_pound_comments.clone(),
4612 &mut cx,
4613 );
4614
4615 // Test that rewrapping is ignored outside of comments in most languages.
4616 assert_rewrap(
4617 indoc! {"
4618 /// Adds two numbers.
4619 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4620 fn add(a: u32, b: u32) -> u32 {
4621 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ˇ
4622 }
4623 "},
4624 indoc! {"
4625 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
4626 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4627 fn add(a: u32, b: u32) -> u32 {
4628 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ˇ
4629 }
4630 "},
4631 language_with_doc_comments.clone(),
4632 &mut cx,
4633 );
4634
4635 // Test that rewrapping works in Markdown and Plain Text languages.
4636 assert_rewrap(
4637 indoc! {"
4638 # Hello
4639
4640 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.
4641 "},
4642 indoc! {"
4643 # Hello
4644
4645 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4646 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4647 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4648 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4649 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4650 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4651 Integer sit amet scelerisque nisi.
4652 "},
4653 markdown_language,
4654 &mut cx,
4655 );
4656
4657 assert_rewrap(
4658 indoc! {"
4659 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi.
4660 "},
4661 indoc! {"
4662 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4663 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4664 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4665 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4666 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4667 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4668 Integer sit amet scelerisque nisi.
4669 "},
4670 plaintext_language,
4671 &mut cx,
4672 );
4673
4674 // Test rewrapping unaligned comments in a selection.
4675 assert_rewrap(
4676 indoc! {"
4677 fn foo() {
4678 if true {
4679 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4680 // Praesent semper egestas tellus id dignissim.ˇ»
4681 do_something();
4682 } else {
4683 //
4684 }
4685 }
4686 "},
4687 indoc! {"
4688 fn foo() {
4689 if true {
4690 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4691 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4692 // egestas tellus id dignissim.ˇ»
4693 do_something();
4694 } else {
4695 //
4696 }
4697 }
4698 "},
4699 language_with_doc_comments.clone(),
4700 &mut cx,
4701 );
4702
4703 assert_rewrap(
4704 indoc! {"
4705 fn foo() {
4706 if true {
4707 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4708 // Praesent semper egestas tellus id dignissim.»
4709 do_something();
4710 } else {
4711 //
4712 }
4713
4714 }
4715 "},
4716 indoc! {"
4717 fn foo() {
4718 if true {
4719 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4720 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4721 // egestas tellus id dignissim.»
4722 do_something();
4723 } else {
4724 //
4725 }
4726
4727 }
4728 "},
4729 language_with_doc_comments.clone(),
4730 &mut cx,
4731 );
4732
4733 #[track_caller]
4734 fn assert_rewrap(
4735 unwrapped_text: &str,
4736 wrapped_text: &str,
4737 language: Arc<Language>,
4738 cx: &mut EditorTestContext,
4739 ) {
4740 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4741 cx.set_state(unwrapped_text);
4742 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
4743 cx.assert_editor_state(wrapped_text);
4744 }
4745}
4746
4747#[gpui::test]
4748async fn test_hard_wrap(cx: &mut TestAppContext) {
4749 init_test(cx, |_| {});
4750 let mut cx = EditorTestContext::new(cx).await;
4751
4752 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
4753 cx.update_editor(|editor, _, cx| {
4754 editor.set_hard_wrap(Some(14), cx);
4755 });
4756
4757 cx.set_state(indoc!(
4758 "
4759 one two three ˇ
4760 "
4761 ));
4762 cx.simulate_input("four");
4763 cx.run_until_parked();
4764
4765 cx.assert_editor_state(indoc!(
4766 "
4767 one two three
4768 fourˇ
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.assert_editor_state(indoc!(
4777 "
4778 one two three
4779 four
4780 ˇ
4781 "
4782 ));
4783
4784 cx.simulate_input("five");
4785 cx.run_until_parked();
4786 cx.assert_editor_state(indoc!(
4787 "
4788 one two three
4789 four
4790 fiveˇ
4791 "
4792 ));
4793
4794 cx.update_editor(|editor, window, cx| {
4795 editor.newline(&Default::default(), window, cx);
4796 });
4797 cx.run_until_parked();
4798 cx.simulate_input("# ");
4799 cx.run_until_parked();
4800 cx.assert_editor_state(indoc!(
4801 "
4802 one two three
4803 four
4804 five
4805 # ˇ
4806 "
4807 ));
4808
4809 cx.update_editor(|editor, window, cx| {
4810 editor.newline(&Default::default(), window, cx);
4811 });
4812 cx.run_until_parked();
4813 cx.assert_editor_state(indoc!(
4814 "
4815 one two three
4816 four
4817 five
4818 #\x20
4819 #ˇ
4820 "
4821 ));
4822
4823 cx.simulate_input(" 6");
4824 cx.run_until_parked();
4825 cx.assert_editor_state(indoc!(
4826 "
4827 one two three
4828 four
4829 five
4830 #
4831 # 6ˇ
4832 "
4833 ));
4834}
4835
4836#[gpui::test]
4837async fn test_clipboard(cx: &mut TestAppContext) {
4838 init_test(cx, |_| {});
4839
4840 let mut cx = EditorTestContext::new(cx).await;
4841
4842 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
4843 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4844 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
4845
4846 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
4847 cx.set_state("two ˇfour ˇsix ˇ");
4848 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4849 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
4850
4851 // Paste again but with only two cursors. Since the number of cursors doesn't
4852 // match the number of slices in the clipboard, the entire clipboard text
4853 // is pasted at each cursor.
4854 cx.set_state("ˇtwo one✅ four three six five ˇ");
4855 cx.update_editor(|e, window, cx| {
4856 e.handle_input("( ", window, cx);
4857 e.paste(&Paste, window, cx);
4858 e.handle_input(") ", window, cx);
4859 });
4860 cx.assert_editor_state(
4861 &([
4862 "( one✅ ",
4863 "three ",
4864 "five ) ˇtwo one✅ four three six five ( one✅ ",
4865 "three ",
4866 "five ) ˇ",
4867 ]
4868 .join("\n")),
4869 );
4870
4871 // Cut with three selections, one of which is full-line.
4872 cx.set_state(indoc! {"
4873 1«2ˇ»3
4874 4ˇ567
4875 «8ˇ»9"});
4876 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4877 cx.assert_editor_state(indoc! {"
4878 1ˇ3
4879 ˇ9"});
4880
4881 // Paste with three selections, noticing how the copied selection that was full-line
4882 // gets inserted before the second cursor.
4883 cx.set_state(indoc! {"
4884 1ˇ3
4885 9ˇ
4886 «oˇ»ne"});
4887 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4888 cx.assert_editor_state(indoc! {"
4889 12ˇ3
4890 4567
4891 9ˇ
4892 8ˇne"});
4893
4894 // Copy with a single cursor only, which writes the whole line into the clipboard.
4895 cx.set_state(indoc! {"
4896 The quick brown
4897 fox juˇmps over
4898 the lazy dog"});
4899 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4900 assert_eq!(
4901 cx.read_from_clipboard()
4902 .and_then(|item| item.text().as_deref().map(str::to_string)),
4903 Some("fox jumps over\n".to_string())
4904 );
4905
4906 // Paste with three selections, noticing how the copied full-line selection is inserted
4907 // before the empty selections but replaces the selection that is non-empty.
4908 cx.set_state(indoc! {"
4909 Tˇhe quick brown
4910 «foˇ»x jumps over
4911 tˇhe lazy dog"});
4912 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4913 cx.assert_editor_state(indoc! {"
4914 fox jumps over
4915 Tˇhe quick brown
4916 fox jumps over
4917 ˇx jumps over
4918 fox jumps over
4919 tˇhe lazy dog"});
4920}
4921
4922#[gpui::test]
4923async fn test_copy_trim(cx: &mut TestAppContext) {
4924 init_test(cx, |_| {});
4925
4926 let mut cx = EditorTestContext::new(cx).await;
4927 cx.set_state(
4928 r#" «for selection in selections.iter() {
4929 let mut start = selection.start;
4930 let mut end = selection.end;
4931 let is_entire_line = selection.is_empty() || self.selections.line_mode;
4932 if is_entire_line {
4933 start = Point::new(start.row, 0);ˇ»
4934 end = cmp::min(max_point, Point::new(end.row + 1, 0));
4935 }
4936 "#,
4937 );
4938 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4939 assert_eq!(
4940 cx.read_from_clipboard()
4941 .and_then(|item| item.text().as_deref().map(str::to_string)),
4942 Some(
4943 "for selection in selections.iter() {
4944 let mut start = selection.start;
4945 let mut end = selection.end;
4946 let is_entire_line = selection.is_empty() || self.selections.line_mode;
4947 if is_entire_line {
4948 start = Point::new(start.row, 0);"
4949 .to_string()
4950 ),
4951 "Regular copying preserves all indentation selected",
4952 );
4953 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
4954 assert_eq!(
4955 cx.read_from_clipboard()
4956 .and_then(|item| item.text().as_deref().map(str::to_string)),
4957 Some(
4958 "for selection in selections.iter() {
4959let mut start = selection.start;
4960let mut end = selection.end;
4961let is_entire_line = selection.is_empty() || self.selections.line_mode;
4962if is_entire_line {
4963 start = Point::new(start.row, 0);"
4964 .to_string()
4965 ),
4966 "Copying with stripping should strip all leading whitespaces"
4967 );
4968
4969 cx.set_state(
4970 r#" « for selection in selections.iter() {
4971 let mut start = selection.start;
4972 let mut end = selection.end;
4973 let is_entire_line = selection.is_empty() || self.selections.line_mode;
4974 if is_entire_line {
4975 start = Point::new(start.row, 0);ˇ»
4976 end = cmp::min(max_point, Point::new(end.row + 1, 0));
4977 }
4978 "#,
4979 );
4980 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4981 assert_eq!(
4982 cx.read_from_clipboard()
4983 .and_then(|item| item.text().as_deref().map(str::to_string)),
4984 Some(
4985 " for selection in selections.iter() {
4986 let mut start = selection.start;
4987 let mut end = selection.end;
4988 let is_entire_line = selection.is_empty() || self.selections.line_mode;
4989 if is_entire_line {
4990 start = Point::new(start.row, 0);"
4991 .to_string()
4992 ),
4993 "Regular copying preserves all indentation selected",
4994 );
4995 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
4996 assert_eq!(
4997 cx.read_from_clipboard()
4998 .and_then(|item| item.text().as_deref().map(str::to_string)),
4999 Some(
5000 "for selection in selections.iter() {
5001let mut start = selection.start;
5002let mut end = selection.end;
5003let is_entire_line = selection.is_empty() || self.selections.line_mode;
5004if is_entire_line {
5005 start = Point::new(start.row, 0);"
5006 .to_string()
5007 ),
5008 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5009 );
5010
5011 cx.set_state(
5012 r#" «ˇ for selection in selections.iter() {
5013 let mut start = selection.start;
5014 let mut end = selection.end;
5015 let is_entire_line = selection.is_empty() || self.selections.line_mode;
5016 if is_entire_line {
5017 start = Point::new(start.row, 0);»
5018 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5019 }
5020 "#,
5021 );
5022 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5023 assert_eq!(
5024 cx.read_from_clipboard()
5025 .and_then(|item| item.text().as_deref().map(str::to_string)),
5026 Some(
5027 " for selection in selections.iter() {
5028 let mut start = selection.start;
5029 let mut end = selection.end;
5030 let is_entire_line = selection.is_empty() || self.selections.line_mode;
5031 if is_entire_line {
5032 start = Point::new(start.row, 0);"
5033 .to_string()
5034 ),
5035 "Regular copying for reverse selection works the same",
5036 );
5037 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5038 assert_eq!(
5039 cx.read_from_clipboard()
5040 .and_then(|item| item.text().as_deref().map(str::to_string)),
5041 Some(
5042 "for selection in selections.iter() {
5043let mut start = selection.start;
5044let mut end = selection.end;
5045let is_entire_line = selection.is_empty() || self.selections.line_mode;
5046if is_entire_line {
5047 start = Point::new(start.row, 0);"
5048 .to_string()
5049 ),
5050 "Copying with stripping for reverse selection works the same"
5051 );
5052
5053 cx.set_state(
5054 r#" for selection «in selections.iter() {
5055 let mut start = selection.start;
5056 let mut end = selection.end;
5057 let is_entire_line = selection.is_empty() || self.selections.line_mode;
5058 if is_entire_line {
5059 start = Point::new(start.row, 0);ˇ»
5060 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5061 }
5062 "#,
5063 );
5064 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5065 assert_eq!(
5066 cx.read_from_clipboard()
5067 .and_then(|item| item.text().as_deref().map(str::to_string)),
5068 Some(
5069 "in selections.iter() {
5070 let mut start = selection.start;
5071 let mut end = selection.end;
5072 let is_entire_line = selection.is_empty() || self.selections.line_mode;
5073 if is_entire_line {
5074 start = Point::new(start.row, 0);"
5075 .to_string()
5076 ),
5077 "When selecting past the indent, the copying works as usual",
5078 );
5079 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5080 assert_eq!(
5081 cx.read_from_clipboard()
5082 .and_then(|item| item.text().as_deref().map(str::to_string)),
5083 Some(
5084 "in selections.iter() {
5085 let mut start = selection.start;
5086 let mut end = selection.end;
5087 let is_entire_line = selection.is_empty() || self.selections.line_mode;
5088 if is_entire_line {
5089 start = Point::new(start.row, 0);"
5090 .to_string()
5091 ),
5092 "When selecting past the indent, nothing is trimmed"
5093 );
5094}
5095
5096#[gpui::test]
5097async fn test_paste_multiline(cx: &mut TestAppContext) {
5098 init_test(cx, |_| {});
5099
5100 let mut cx = EditorTestContext::new(cx).await;
5101 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5102
5103 // Cut an indented block, without the leading whitespace.
5104 cx.set_state(indoc! {"
5105 const a: B = (
5106 c(),
5107 «d(
5108 e,
5109 f
5110 )ˇ»
5111 );
5112 "});
5113 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5114 cx.assert_editor_state(indoc! {"
5115 const a: B = (
5116 c(),
5117 ˇ
5118 );
5119 "});
5120
5121 // Paste it at the same position.
5122 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5123 cx.assert_editor_state(indoc! {"
5124 const a: B = (
5125 c(),
5126 d(
5127 e,
5128 f
5129 )ˇ
5130 );
5131 "});
5132
5133 // Paste it at a line with a lower indent level.
5134 cx.set_state(indoc! {"
5135 ˇ
5136 const a: B = (
5137 c(),
5138 );
5139 "});
5140 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5141 cx.assert_editor_state(indoc! {"
5142 d(
5143 e,
5144 f
5145 )ˇ
5146 const a: B = (
5147 c(),
5148 );
5149 "});
5150
5151 // Cut an indented block, with the leading whitespace.
5152 cx.set_state(indoc! {"
5153 const a: B = (
5154 c(),
5155 « d(
5156 e,
5157 f
5158 )
5159 ˇ»);
5160 "});
5161 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5162 cx.assert_editor_state(indoc! {"
5163 const a: B = (
5164 c(),
5165 ˇ);
5166 "});
5167
5168 // Paste it at the same position.
5169 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5170 cx.assert_editor_state(indoc! {"
5171 const a: B = (
5172 c(),
5173 d(
5174 e,
5175 f
5176 )
5177 ˇ);
5178 "});
5179
5180 // Paste it at a line with a higher indent level.
5181 cx.set_state(indoc! {"
5182 const a: B = (
5183 c(),
5184 d(
5185 e,
5186 fˇ
5187 )
5188 );
5189 "});
5190 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5191 cx.assert_editor_state(indoc! {"
5192 const a: B = (
5193 c(),
5194 d(
5195 e,
5196 f d(
5197 e,
5198 f
5199 )
5200 ˇ
5201 )
5202 );
5203 "});
5204
5205 // Copy an indented block, starting mid-line
5206 cx.set_state(indoc! {"
5207 const a: B = (
5208 c(),
5209 somethin«g(
5210 e,
5211 f
5212 )ˇ»
5213 );
5214 "});
5215 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5216
5217 // Paste it on a line with a lower indent level
5218 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
5219 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5220 cx.assert_editor_state(indoc! {"
5221 const a: B = (
5222 c(),
5223 something(
5224 e,
5225 f
5226 )
5227 );
5228 g(
5229 e,
5230 f
5231 )ˇ"});
5232}
5233
5234#[gpui::test]
5235async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
5236 init_test(cx, |_| {});
5237
5238 cx.write_to_clipboard(ClipboardItem::new_string(
5239 " d(\n e\n );\n".into(),
5240 ));
5241
5242 let mut cx = EditorTestContext::new(cx).await;
5243 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5244
5245 cx.set_state(indoc! {"
5246 fn a() {
5247 b();
5248 if c() {
5249 ˇ
5250 }
5251 }
5252 "});
5253
5254 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5255 cx.assert_editor_state(indoc! {"
5256 fn a() {
5257 b();
5258 if c() {
5259 d(
5260 e
5261 );
5262 ˇ
5263 }
5264 }
5265 "});
5266
5267 cx.set_state(indoc! {"
5268 fn a() {
5269 b();
5270 ˇ
5271 }
5272 "});
5273
5274 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5275 cx.assert_editor_state(indoc! {"
5276 fn a() {
5277 b();
5278 d(
5279 e
5280 );
5281 ˇ
5282 }
5283 "});
5284}
5285
5286#[gpui::test]
5287fn test_select_all(cx: &mut TestAppContext) {
5288 init_test(cx, |_| {});
5289
5290 let editor = cx.add_window(|window, cx| {
5291 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
5292 build_editor(buffer, window, cx)
5293 });
5294 _ = editor.update(cx, |editor, window, cx| {
5295 editor.select_all(&SelectAll, window, cx);
5296 assert_eq!(
5297 editor.selections.display_ranges(cx),
5298 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
5299 );
5300 });
5301}
5302
5303#[gpui::test]
5304fn test_select_line(cx: &mut TestAppContext) {
5305 init_test(cx, |_| {});
5306
5307 let editor = cx.add_window(|window, cx| {
5308 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
5309 build_editor(buffer, window, cx)
5310 });
5311 _ = editor.update(cx, |editor, window, cx| {
5312 editor.change_selections(None, window, cx, |s| {
5313 s.select_display_ranges([
5314 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5315 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5316 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5317 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
5318 ])
5319 });
5320 editor.select_line(&SelectLine, window, cx);
5321 assert_eq!(
5322 editor.selections.display_ranges(cx),
5323 vec![
5324 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
5325 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
5326 ]
5327 );
5328 });
5329
5330 _ = editor.update(cx, |editor, window, cx| {
5331 editor.select_line(&SelectLine, window, cx);
5332 assert_eq!(
5333 editor.selections.display_ranges(cx),
5334 vec![
5335 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
5336 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
5337 ]
5338 );
5339 });
5340
5341 _ = editor.update(cx, |editor, window, cx| {
5342 editor.select_line(&SelectLine, window, cx);
5343 assert_eq!(
5344 editor.selections.display_ranges(cx),
5345 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
5346 );
5347 });
5348}
5349
5350#[gpui::test]
5351async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
5352 init_test(cx, |_| {});
5353 let mut cx = EditorTestContext::new(cx).await;
5354
5355 #[track_caller]
5356 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
5357 cx.set_state(initial_state);
5358 cx.update_editor(|e, window, cx| {
5359 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
5360 });
5361 cx.assert_editor_state(expected_state);
5362 }
5363
5364 // Selection starts and ends at the middle of lines, left-to-right
5365 test(
5366 &mut cx,
5367 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
5368 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5369 );
5370 // Same thing, right-to-left
5371 test(
5372 &mut cx,
5373 "aa\nb«b\ncc\ndd\neˇ»e\nff",
5374 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5375 );
5376
5377 // Whole buffer, left-to-right, last line *doesn't* end with newline
5378 test(
5379 &mut cx,
5380 "«ˇaa\nbb\ncc\ndd\nee\nff»",
5381 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5382 );
5383 // Same thing, right-to-left
5384 test(
5385 &mut cx,
5386 "«aa\nbb\ncc\ndd\nee\nffˇ»",
5387 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5388 );
5389
5390 // Whole buffer, left-to-right, last line ends with newline
5391 test(
5392 &mut cx,
5393 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
5394 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5395 );
5396 // Same thing, right-to-left
5397 test(
5398 &mut cx,
5399 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
5400 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5401 );
5402
5403 // Starts at the end of a line, ends at the start of another
5404 test(
5405 &mut cx,
5406 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
5407 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
5408 );
5409}
5410
5411#[gpui::test]
5412async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
5413 init_test(cx, |_| {});
5414
5415 let editor = cx.add_window(|window, cx| {
5416 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
5417 build_editor(buffer, window, cx)
5418 });
5419
5420 // setup
5421 _ = editor.update(cx, |editor, window, cx| {
5422 editor.fold_creases(
5423 vec![
5424 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5425 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5426 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5427 ],
5428 true,
5429 window,
5430 cx,
5431 );
5432 assert_eq!(
5433 editor.display_text(cx),
5434 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5435 );
5436 });
5437
5438 _ = editor.update(cx, |editor, window, cx| {
5439 editor.change_selections(None, window, cx, |s| {
5440 s.select_display_ranges([
5441 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5442 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5443 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5444 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
5445 ])
5446 });
5447 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5448 assert_eq!(
5449 editor.display_text(cx),
5450 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5451 );
5452 });
5453 EditorTestContext::for_editor(editor, cx)
5454 .await
5455 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
5456
5457 _ = editor.update(cx, |editor, window, cx| {
5458 editor.change_selections(None, window, cx, |s| {
5459 s.select_display_ranges([
5460 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
5461 ])
5462 });
5463 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5464 assert_eq!(
5465 editor.display_text(cx),
5466 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
5467 );
5468 assert_eq!(
5469 editor.selections.display_ranges(cx),
5470 [
5471 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
5472 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
5473 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
5474 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
5475 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
5476 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
5477 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
5478 ]
5479 );
5480 });
5481 EditorTestContext::for_editor(editor, cx)
5482 .await
5483 .assert_editor_state(
5484 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
5485 );
5486}
5487
5488#[gpui::test]
5489async fn test_add_selection_above_below(cx: &mut TestAppContext) {
5490 init_test(cx, |_| {});
5491
5492 let mut cx = EditorTestContext::new(cx).await;
5493
5494 cx.set_state(indoc!(
5495 r#"abc
5496 defˇghi
5497
5498 jk
5499 nlmo
5500 "#
5501 ));
5502
5503 cx.update_editor(|editor, window, cx| {
5504 editor.add_selection_above(&Default::default(), window, cx);
5505 });
5506
5507 cx.assert_editor_state(indoc!(
5508 r#"abcˇ
5509 defˇghi
5510
5511 jk
5512 nlmo
5513 "#
5514 ));
5515
5516 cx.update_editor(|editor, window, cx| {
5517 editor.add_selection_above(&Default::default(), window, cx);
5518 });
5519
5520 cx.assert_editor_state(indoc!(
5521 r#"abcˇ
5522 defˇghi
5523
5524 jk
5525 nlmo
5526 "#
5527 ));
5528
5529 cx.update_editor(|editor, window, cx| {
5530 editor.add_selection_below(&Default::default(), window, cx);
5531 });
5532
5533 cx.assert_editor_state(indoc!(
5534 r#"abc
5535 defˇghi
5536
5537 jk
5538 nlmo
5539 "#
5540 ));
5541
5542 cx.update_editor(|editor, window, cx| {
5543 editor.undo_selection(&Default::default(), window, cx);
5544 });
5545
5546 cx.assert_editor_state(indoc!(
5547 r#"abcˇ
5548 defˇghi
5549
5550 jk
5551 nlmo
5552 "#
5553 ));
5554
5555 cx.update_editor(|editor, window, cx| {
5556 editor.redo_selection(&Default::default(), window, cx);
5557 });
5558
5559 cx.assert_editor_state(indoc!(
5560 r#"abc
5561 defˇghi
5562
5563 jk
5564 nlmo
5565 "#
5566 ));
5567
5568 cx.update_editor(|editor, window, cx| {
5569 editor.add_selection_below(&Default::default(), window, cx);
5570 });
5571
5572 cx.assert_editor_state(indoc!(
5573 r#"abc
5574 defˇghi
5575
5576 jk
5577 nlmˇo
5578 "#
5579 ));
5580
5581 cx.update_editor(|editor, window, cx| {
5582 editor.add_selection_below(&Default::default(), window, cx);
5583 });
5584
5585 cx.assert_editor_state(indoc!(
5586 r#"abc
5587 defˇghi
5588
5589 jk
5590 nlmˇo
5591 "#
5592 ));
5593
5594 // change selections
5595 cx.set_state(indoc!(
5596 r#"abc
5597 def«ˇg»hi
5598
5599 jk
5600 nlmo
5601 "#
5602 ));
5603
5604 cx.update_editor(|editor, window, cx| {
5605 editor.add_selection_below(&Default::default(), window, cx);
5606 });
5607
5608 cx.assert_editor_state(indoc!(
5609 r#"abc
5610 def«ˇg»hi
5611
5612 jk
5613 nlm«ˇo»
5614 "#
5615 ));
5616
5617 cx.update_editor(|editor, window, cx| {
5618 editor.add_selection_below(&Default::default(), window, cx);
5619 });
5620
5621 cx.assert_editor_state(indoc!(
5622 r#"abc
5623 def«ˇg»hi
5624
5625 jk
5626 nlm«ˇo»
5627 "#
5628 ));
5629
5630 cx.update_editor(|editor, window, cx| {
5631 editor.add_selection_above(&Default::default(), window, cx);
5632 });
5633
5634 cx.assert_editor_state(indoc!(
5635 r#"abc
5636 def«ˇg»hi
5637
5638 jk
5639 nlmo
5640 "#
5641 ));
5642
5643 cx.update_editor(|editor, window, cx| {
5644 editor.add_selection_above(&Default::default(), window, cx);
5645 });
5646
5647 cx.assert_editor_state(indoc!(
5648 r#"abc
5649 def«ˇg»hi
5650
5651 jk
5652 nlmo
5653 "#
5654 ));
5655
5656 // Change selections again
5657 cx.set_state(indoc!(
5658 r#"a«bc
5659 defgˇ»hi
5660
5661 jk
5662 nlmo
5663 "#
5664 ));
5665
5666 cx.update_editor(|editor, window, cx| {
5667 editor.add_selection_below(&Default::default(), window, cx);
5668 });
5669
5670 cx.assert_editor_state(indoc!(
5671 r#"a«bcˇ»
5672 d«efgˇ»hi
5673
5674 j«kˇ»
5675 nlmo
5676 "#
5677 ));
5678
5679 cx.update_editor(|editor, window, cx| {
5680 editor.add_selection_below(&Default::default(), window, cx);
5681 });
5682 cx.assert_editor_state(indoc!(
5683 r#"a«bcˇ»
5684 d«efgˇ»hi
5685
5686 j«kˇ»
5687 n«lmoˇ»
5688 "#
5689 ));
5690 cx.update_editor(|editor, window, cx| {
5691 editor.add_selection_above(&Default::default(), window, cx);
5692 });
5693
5694 cx.assert_editor_state(indoc!(
5695 r#"a«bcˇ»
5696 d«efgˇ»hi
5697
5698 j«kˇ»
5699 nlmo
5700 "#
5701 ));
5702
5703 // Change selections again
5704 cx.set_state(indoc!(
5705 r#"abc
5706 d«ˇefghi
5707
5708 jk
5709 nlm»o
5710 "#
5711 ));
5712
5713 cx.update_editor(|editor, window, cx| {
5714 editor.add_selection_above(&Default::default(), window, cx);
5715 });
5716
5717 cx.assert_editor_state(indoc!(
5718 r#"a«ˇbc»
5719 d«ˇef»ghi
5720
5721 j«ˇk»
5722 n«ˇlm»o
5723 "#
5724 ));
5725
5726 cx.update_editor(|editor, window, cx| {
5727 editor.add_selection_below(&Default::default(), window, cx);
5728 });
5729
5730 cx.assert_editor_state(indoc!(
5731 r#"abc
5732 d«ˇef»ghi
5733
5734 j«ˇk»
5735 n«ˇlm»o
5736 "#
5737 ));
5738}
5739
5740#[gpui::test]
5741async fn test_select_next(cx: &mut TestAppContext) {
5742 init_test(cx, |_| {});
5743
5744 let mut cx = EditorTestContext::new(cx).await;
5745 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5746
5747 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5748 .unwrap();
5749 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5750
5751 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5752 .unwrap();
5753 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5754
5755 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5756 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5757
5758 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5759 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5760
5761 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5762 .unwrap();
5763 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5764
5765 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5766 .unwrap();
5767 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5768}
5769
5770#[gpui::test]
5771async fn test_select_all_matches(cx: &mut TestAppContext) {
5772 init_test(cx, |_| {});
5773
5774 let mut cx = EditorTestContext::new(cx).await;
5775
5776 // Test caret-only selections
5777 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5778 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5779 .unwrap();
5780 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5781
5782 // Test left-to-right selections
5783 cx.set_state("abc\n«abcˇ»\nabc");
5784 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5785 .unwrap();
5786 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
5787
5788 // Test right-to-left selections
5789 cx.set_state("abc\n«ˇabc»\nabc");
5790 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5791 .unwrap();
5792 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
5793
5794 // Test selecting whitespace with caret selection
5795 cx.set_state("abc\nˇ abc\nabc");
5796 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5797 .unwrap();
5798 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
5799
5800 // Test selecting whitespace with left-to-right selection
5801 cx.set_state("abc\n«ˇ »abc\nabc");
5802 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5803 .unwrap();
5804 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
5805
5806 // Test no matches with right-to-left selection
5807 cx.set_state("abc\n« ˇ»abc\nabc");
5808 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5809 .unwrap();
5810 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
5811}
5812
5813#[gpui::test]
5814async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
5815 init_test(cx, |_| {});
5816
5817 let mut cx = EditorTestContext::new(cx).await;
5818 cx.set_state(
5819 r#"let foo = 2;
5820lˇet foo = 2;
5821let fooˇ = 2;
5822let foo = 2;
5823let foo = ˇ2;"#,
5824 );
5825
5826 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5827 .unwrap();
5828 cx.assert_editor_state(
5829 r#"let foo = 2;
5830«letˇ» foo = 2;
5831let «fooˇ» = 2;
5832let foo = 2;
5833let foo = «2ˇ»;"#,
5834 );
5835
5836 // noop for multiple selections with different contents
5837 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5838 .unwrap();
5839 cx.assert_editor_state(
5840 r#"let foo = 2;
5841«letˇ» foo = 2;
5842let «fooˇ» = 2;
5843let foo = 2;
5844let foo = «2ˇ»;"#,
5845 );
5846}
5847
5848#[gpui::test]
5849async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
5850 init_test(cx, |_| {});
5851
5852 let mut cx =
5853 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
5854
5855 cx.assert_editor_state(indoc! {"
5856 ˇbbb
5857 ccc
5858
5859 bbb
5860 ccc
5861 "});
5862 cx.dispatch_action(SelectPrevious::default());
5863 cx.assert_editor_state(indoc! {"
5864 «bbbˇ»
5865 ccc
5866
5867 bbb
5868 ccc
5869 "});
5870 cx.dispatch_action(SelectPrevious::default());
5871 cx.assert_editor_state(indoc! {"
5872 «bbbˇ»
5873 ccc
5874
5875 «bbbˇ»
5876 ccc
5877 "});
5878}
5879
5880#[gpui::test]
5881async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
5882 init_test(cx, |_| {});
5883
5884 let mut cx = EditorTestContext::new(cx).await;
5885 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
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\ndefabc\nabc");
5890
5891 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5892 .unwrap();
5893 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
5894
5895 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5896 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5897
5898 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5899 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
5900
5901 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5902 .unwrap();
5903 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
5904
5905 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5906 .unwrap();
5907 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndef«abcˇ»\n«abcˇ»");
5908
5909 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5910 .unwrap();
5911 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
5912}
5913
5914#[gpui::test]
5915async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
5916 init_test(cx, |_| {});
5917
5918 let mut cx = EditorTestContext::new(cx).await;
5919 cx.set_state("aˇ");
5920
5921 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5922 .unwrap();
5923 cx.assert_editor_state("«aˇ»");
5924 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5925 .unwrap();
5926 cx.assert_editor_state("«aˇ»");
5927}
5928
5929#[gpui::test]
5930async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
5931 init_test(cx, |_| {});
5932
5933 let mut cx = EditorTestContext::new(cx).await;
5934 cx.set_state(
5935 r#"let foo = 2;
5936lˇet foo = 2;
5937let fooˇ = 2;
5938let foo = 2;
5939let foo = ˇ2;"#,
5940 );
5941
5942 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5943 .unwrap();
5944 cx.assert_editor_state(
5945 r#"let foo = 2;
5946«letˇ» foo = 2;
5947let «fooˇ» = 2;
5948let foo = 2;
5949let foo = «2ˇ»;"#,
5950 );
5951
5952 // noop for multiple selections with different contents
5953 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5954 .unwrap();
5955 cx.assert_editor_state(
5956 r#"let foo = 2;
5957«letˇ» foo = 2;
5958let «fooˇ» = 2;
5959let foo = 2;
5960let foo = «2ˇ»;"#,
5961 );
5962}
5963
5964#[gpui::test]
5965async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
5966 init_test(cx, |_| {});
5967
5968 let mut cx = EditorTestContext::new(cx).await;
5969 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
5970
5971 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5972 .unwrap();
5973 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
5974
5975 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5976 .unwrap();
5977 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
5978
5979 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5980 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
5981
5982 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5983 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
5984
5985 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5986 .unwrap();
5987 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
5988
5989 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5990 .unwrap();
5991 cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
5992}
5993
5994#[gpui::test]
5995async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
5996 init_test(cx, |_| {});
5997
5998 let language = Arc::new(Language::new(
5999 LanguageConfig::default(),
6000 Some(tree_sitter_rust::LANGUAGE.into()),
6001 ));
6002
6003 let text = r#"
6004 use mod1::mod2::{mod3, mod4};
6005
6006 fn fn_1(param1: bool, param2: &str) {
6007 let var1 = "text";
6008 }
6009 "#
6010 .unindent();
6011
6012 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6013 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6014 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6015
6016 editor
6017 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6018 .await;
6019
6020 editor.update_in(cx, |editor, window, cx| {
6021 editor.change_selections(None, window, cx, |s| {
6022 s.select_display_ranges([
6023 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
6024 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
6025 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
6026 ]);
6027 });
6028 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6029 });
6030 editor.update(cx, |editor, cx| {
6031 assert_text_with_selections(
6032 editor,
6033 indoc! {r#"
6034 use mod1::mod2::{mod3, «mod4ˇ»};
6035
6036 fn fn_1«ˇ(param1: bool, param2: &str)» {
6037 let var1 = "«ˇtext»";
6038 }
6039 "#},
6040 cx,
6041 );
6042 });
6043
6044 editor.update_in(cx, |editor, window, cx| {
6045 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6046 });
6047 editor.update(cx, |editor, cx| {
6048 assert_text_with_selections(
6049 editor,
6050 indoc! {r#"
6051 use mod1::mod2::«{mod3, mod4}ˇ»;
6052
6053 «ˇfn fn_1(param1: bool, param2: &str) {
6054 let var1 = "text";
6055 }»
6056 "#},
6057 cx,
6058 );
6059 });
6060
6061 editor.update_in(cx, |editor, window, cx| {
6062 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6063 });
6064 assert_eq!(
6065 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6066 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6067 );
6068
6069 // Trying to expand the selected syntax node one more time has no effect.
6070 editor.update_in(cx, |editor, window, cx| {
6071 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6072 });
6073 assert_eq!(
6074 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6075 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6076 );
6077
6078 editor.update_in(cx, |editor, window, cx| {
6079 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6080 });
6081 editor.update(cx, |editor, cx| {
6082 assert_text_with_selections(
6083 editor,
6084 indoc! {r#"
6085 use mod1::mod2::«{mod3, mod4}ˇ»;
6086
6087 «ˇfn fn_1(param1: bool, param2: &str) {
6088 let var1 = "text";
6089 }»
6090 "#},
6091 cx,
6092 );
6093 });
6094
6095 editor.update_in(cx, |editor, window, cx| {
6096 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6097 });
6098 editor.update(cx, |editor, cx| {
6099 assert_text_with_selections(
6100 editor,
6101 indoc! {r#"
6102 use mod1::mod2::{mod3, «mod4ˇ»};
6103
6104 fn fn_1«ˇ(param1: bool, param2: &str)» {
6105 let var1 = "«ˇtext»";
6106 }
6107 "#},
6108 cx,
6109 );
6110 });
6111
6112 editor.update_in(cx, |editor, window, cx| {
6113 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6114 });
6115 editor.update(cx, |editor, cx| {
6116 assert_text_with_selections(
6117 editor,
6118 indoc! {r#"
6119 use mod1::mod2::{mod3, mo«ˇ»d4};
6120
6121 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6122 let var1 = "te«ˇ»xt";
6123 }
6124 "#},
6125 cx,
6126 );
6127 });
6128
6129 // Trying to shrink the selected syntax node one more time has no effect.
6130 editor.update_in(cx, |editor, window, cx| {
6131 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6132 });
6133 editor.update_in(cx, |editor, _, cx| {
6134 assert_text_with_selections(
6135 editor,
6136 indoc! {r#"
6137 use mod1::mod2::{mod3, mo«ˇ»d4};
6138
6139 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6140 let var1 = "te«ˇ»xt";
6141 }
6142 "#},
6143 cx,
6144 );
6145 });
6146
6147 // Ensure that we keep expanding the selection if the larger selection starts or ends within
6148 // a fold.
6149 editor.update_in(cx, |editor, window, cx| {
6150 editor.fold_creases(
6151 vec![
6152 Crease::simple(
6153 Point::new(0, 21)..Point::new(0, 24),
6154 FoldPlaceholder::test(),
6155 ),
6156 Crease::simple(
6157 Point::new(3, 20)..Point::new(3, 22),
6158 FoldPlaceholder::test(),
6159 ),
6160 ],
6161 true,
6162 window,
6163 cx,
6164 );
6165 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6166 });
6167 editor.update(cx, |editor, cx| {
6168 assert_text_with_selections(
6169 editor,
6170 indoc! {r#"
6171 use mod1::mod2::«{mod3, mod4}ˇ»;
6172
6173 fn fn_1«ˇ(param1: bool, param2: &str)» {
6174 «ˇlet var1 = "text";»
6175 }
6176 "#},
6177 cx,
6178 );
6179 });
6180}
6181
6182#[gpui::test]
6183async fn test_fold_function_bodies(cx: &mut TestAppContext) {
6184 init_test(cx, |_| {});
6185
6186 let base_text = r#"
6187 impl A {
6188 // this is an uncommitted comment
6189
6190 fn b() {
6191 c();
6192 }
6193
6194 // this is another uncommitted comment
6195
6196 fn d() {
6197 // e
6198 // f
6199 }
6200 }
6201
6202 fn g() {
6203 // h
6204 }
6205 "#
6206 .unindent();
6207
6208 let text = r#"
6209 ˇimpl A {
6210
6211 fn b() {
6212 c();
6213 }
6214
6215 fn d() {
6216 // e
6217 // f
6218 }
6219 }
6220
6221 fn g() {
6222 // h
6223 }
6224 "#
6225 .unindent();
6226
6227 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6228 cx.set_state(&text);
6229 cx.set_head_text(&base_text);
6230 cx.update_editor(|editor, window, cx| {
6231 editor.expand_all_diff_hunks(&Default::default(), window, cx);
6232 });
6233
6234 cx.assert_state_with_diff(
6235 "
6236 ˇimpl A {
6237 - // this is an uncommitted comment
6238
6239 fn b() {
6240 c();
6241 }
6242
6243 - // this is another uncommitted comment
6244 -
6245 fn d() {
6246 // e
6247 // f
6248 }
6249 }
6250
6251 fn g() {
6252 // h
6253 }
6254 "
6255 .unindent(),
6256 );
6257
6258 let expected_display_text = "
6259 impl A {
6260 // this is an uncommitted comment
6261
6262 fn b() {
6263 ⋯
6264 }
6265
6266 // this is another uncommitted comment
6267
6268 fn d() {
6269 ⋯
6270 }
6271 }
6272
6273 fn g() {
6274 ⋯
6275 }
6276 "
6277 .unindent();
6278
6279 cx.update_editor(|editor, window, cx| {
6280 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
6281 assert_eq!(editor.display_text(cx), expected_display_text);
6282 });
6283}
6284
6285#[gpui::test]
6286async fn test_autoindent(cx: &mut TestAppContext) {
6287 init_test(cx, |_| {});
6288
6289 let language = Arc::new(
6290 Language::new(
6291 LanguageConfig {
6292 brackets: BracketPairConfig {
6293 pairs: vec![
6294 BracketPair {
6295 start: "{".to_string(),
6296 end: "}".to_string(),
6297 close: false,
6298 surround: false,
6299 newline: true,
6300 },
6301 BracketPair {
6302 start: "(".to_string(),
6303 end: ")".to_string(),
6304 close: false,
6305 surround: false,
6306 newline: true,
6307 },
6308 ],
6309 ..Default::default()
6310 },
6311 ..Default::default()
6312 },
6313 Some(tree_sitter_rust::LANGUAGE.into()),
6314 )
6315 .with_indents_query(
6316 r#"
6317 (_ "(" ")" @end) @indent
6318 (_ "{" "}" @end) @indent
6319 "#,
6320 )
6321 .unwrap(),
6322 );
6323
6324 let text = "fn a() {}";
6325
6326 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6327 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6328 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6329 editor
6330 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6331 .await;
6332
6333 editor.update_in(cx, |editor, window, cx| {
6334 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
6335 editor.newline(&Newline, window, cx);
6336 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
6337 assert_eq!(
6338 editor.selections.ranges(cx),
6339 &[
6340 Point::new(1, 4)..Point::new(1, 4),
6341 Point::new(3, 4)..Point::new(3, 4),
6342 Point::new(5, 0)..Point::new(5, 0)
6343 ]
6344 );
6345 });
6346}
6347
6348#[gpui::test]
6349async fn test_autoindent_selections(cx: &mut TestAppContext) {
6350 init_test(cx, |_| {});
6351
6352 {
6353 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6354 cx.set_state(indoc! {"
6355 impl A {
6356
6357 fn b() {}
6358
6359 «fn c() {
6360
6361 }ˇ»
6362 }
6363 "});
6364
6365 cx.update_editor(|editor, window, cx| {
6366 editor.autoindent(&Default::default(), window, cx);
6367 });
6368
6369 cx.assert_editor_state(indoc! {"
6370 impl A {
6371
6372 fn b() {}
6373
6374 «fn c() {
6375
6376 }ˇ»
6377 }
6378 "});
6379 }
6380
6381 {
6382 let mut cx = EditorTestContext::new_multibuffer(
6383 cx,
6384 [indoc! { "
6385 impl A {
6386 «
6387 // a
6388 fn b(){}
6389 »
6390 «
6391 }
6392 fn c(){}
6393 »
6394 "}],
6395 );
6396
6397 let buffer = cx.update_editor(|editor, _, cx| {
6398 let buffer = editor.buffer().update(cx, |buffer, _| {
6399 buffer.all_buffers().iter().next().unwrap().clone()
6400 });
6401 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6402 buffer
6403 });
6404
6405 cx.run_until_parked();
6406 cx.update_editor(|editor, window, cx| {
6407 editor.select_all(&Default::default(), window, cx);
6408 editor.autoindent(&Default::default(), window, cx)
6409 });
6410 cx.run_until_parked();
6411
6412 cx.update(|_, cx| {
6413 pretty_assertions::assert_eq!(
6414 buffer.read(cx).text(),
6415 indoc! { "
6416 impl A {
6417
6418 // a
6419 fn b(){}
6420
6421
6422 }
6423 fn c(){}
6424
6425 " }
6426 )
6427 });
6428 }
6429}
6430
6431#[gpui::test]
6432async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
6433 init_test(cx, |_| {});
6434
6435 let mut cx = EditorTestContext::new(cx).await;
6436
6437 let language = Arc::new(Language::new(
6438 LanguageConfig {
6439 brackets: BracketPairConfig {
6440 pairs: vec![
6441 BracketPair {
6442 start: "{".to_string(),
6443 end: "}".to_string(),
6444 close: true,
6445 surround: true,
6446 newline: true,
6447 },
6448 BracketPair {
6449 start: "(".to_string(),
6450 end: ")".to_string(),
6451 close: true,
6452 surround: true,
6453 newline: true,
6454 },
6455 BracketPair {
6456 start: "/*".to_string(),
6457 end: " */".to_string(),
6458 close: true,
6459 surround: true,
6460 newline: true,
6461 },
6462 BracketPair {
6463 start: "[".to_string(),
6464 end: "]".to_string(),
6465 close: false,
6466 surround: false,
6467 newline: true,
6468 },
6469 BracketPair {
6470 start: "\"".to_string(),
6471 end: "\"".to_string(),
6472 close: true,
6473 surround: true,
6474 newline: false,
6475 },
6476 BracketPair {
6477 start: "<".to_string(),
6478 end: ">".to_string(),
6479 close: false,
6480 surround: true,
6481 newline: true,
6482 },
6483 ],
6484 ..Default::default()
6485 },
6486 autoclose_before: "})]".to_string(),
6487 ..Default::default()
6488 },
6489 Some(tree_sitter_rust::LANGUAGE.into()),
6490 ));
6491
6492 cx.language_registry().add(language.clone());
6493 cx.update_buffer(|buffer, cx| {
6494 buffer.set_language(Some(language), cx);
6495 });
6496
6497 cx.set_state(
6498 &r#"
6499 🏀ˇ
6500 εˇ
6501 ❤️ˇ
6502 "#
6503 .unindent(),
6504 );
6505
6506 // autoclose multiple nested brackets at multiple cursors
6507 cx.update_editor(|editor, window, cx| {
6508 editor.handle_input("{", window, cx);
6509 editor.handle_input("{", window, cx);
6510 editor.handle_input("{", window, cx);
6511 });
6512 cx.assert_editor_state(
6513 &"
6514 🏀{{{ˇ}}}
6515 ε{{{ˇ}}}
6516 ❤️{{{ˇ}}}
6517 "
6518 .unindent(),
6519 );
6520
6521 // insert a different closing bracket
6522 cx.update_editor(|editor, window, cx| {
6523 editor.handle_input(")", window, cx);
6524 });
6525 cx.assert_editor_state(
6526 &"
6527 🏀{{{)ˇ}}}
6528 ε{{{)ˇ}}}
6529 ❤️{{{)ˇ}}}
6530 "
6531 .unindent(),
6532 );
6533
6534 // skip over the auto-closed brackets when typing a closing bracket
6535 cx.update_editor(|editor, window, cx| {
6536 editor.move_right(&MoveRight, window, cx);
6537 editor.handle_input("}", window, cx);
6538 editor.handle_input("}", window, cx);
6539 editor.handle_input("}", window, cx);
6540 });
6541 cx.assert_editor_state(
6542 &"
6543 🏀{{{)}}}}ˇ
6544 ε{{{)}}}}ˇ
6545 ❤️{{{)}}}}ˇ
6546 "
6547 .unindent(),
6548 );
6549
6550 // autoclose multi-character pairs
6551 cx.set_state(
6552 &"
6553 ˇ
6554 ˇ
6555 "
6556 .unindent(),
6557 );
6558 cx.update_editor(|editor, window, cx| {
6559 editor.handle_input("/", window, cx);
6560 editor.handle_input("*", window, cx);
6561 });
6562 cx.assert_editor_state(
6563 &"
6564 /*ˇ */
6565 /*ˇ */
6566 "
6567 .unindent(),
6568 );
6569
6570 // one cursor autocloses a multi-character pair, one cursor
6571 // does not autoclose.
6572 cx.set_state(
6573 &"
6574 /ˇ
6575 ˇ
6576 "
6577 .unindent(),
6578 );
6579 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
6580 cx.assert_editor_state(
6581 &"
6582 /*ˇ */
6583 *ˇ
6584 "
6585 .unindent(),
6586 );
6587
6588 // Don't autoclose if the next character isn't whitespace and isn't
6589 // listed in the language's "autoclose_before" section.
6590 cx.set_state("ˇa b");
6591 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6592 cx.assert_editor_state("{ˇa b");
6593
6594 // Don't autoclose if `close` is false for the bracket pair
6595 cx.set_state("ˇ");
6596 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
6597 cx.assert_editor_state("[ˇ");
6598
6599 // Surround with brackets if text is selected
6600 cx.set_state("«aˇ» b");
6601 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6602 cx.assert_editor_state("{«aˇ»} b");
6603
6604 // Autoclose when not immediately after a word character
6605 cx.set_state("a ˇ");
6606 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6607 cx.assert_editor_state("a \"ˇ\"");
6608
6609 // Autoclose pair where the start and end characters are the same
6610 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6611 cx.assert_editor_state("a \"\"ˇ");
6612
6613 // Don't autoclose when immediately after a word character
6614 cx.set_state("aˇ");
6615 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6616 cx.assert_editor_state("a\"ˇ");
6617
6618 // Do autoclose when after a non-word character
6619 cx.set_state("{ˇ");
6620 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6621 cx.assert_editor_state("{\"ˇ\"");
6622
6623 // Non identical pairs autoclose regardless of preceding character
6624 cx.set_state("aˇ");
6625 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6626 cx.assert_editor_state("a{ˇ}");
6627
6628 // Don't autoclose pair if autoclose is disabled
6629 cx.set_state("ˇ");
6630 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6631 cx.assert_editor_state("<ˇ");
6632
6633 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
6634 cx.set_state("«aˇ» b");
6635 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6636 cx.assert_editor_state("<«aˇ»> b");
6637}
6638
6639#[gpui::test]
6640async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
6641 init_test(cx, |settings| {
6642 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
6643 });
6644
6645 let mut cx = EditorTestContext::new(cx).await;
6646
6647 let language = Arc::new(Language::new(
6648 LanguageConfig {
6649 brackets: BracketPairConfig {
6650 pairs: vec![
6651 BracketPair {
6652 start: "{".to_string(),
6653 end: "}".to_string(),
6654 close: true,
6655 surround: true,
6656 newline: true,
6657 },
6658 BracketPair {
6659 start: "(".to_string(),
6660 end: ")".to_string(),
6661 close: true,
6662 surround: true,
6663 newline: true,
6664 },
6665 BracketPair {
6666 start: "[".to_string(),
6667 end: "]".to_string(),
6668 close: false,
6669 surround: false,
6670 newline: true,
6671 },
6672 ],
6673 ..Default::default()
6674 },
6675 autoclose_before: "})]".to_string(),
6676 ..Default::default()
6677 },
6678 Some(tree_sitter_rust::LANGUAGE.into()),
6679 ));
6680
6681 cx.language_registry().add(language.clone());
6682 cx.update_buffer(|buffer, cx| {
6683 buffer.set_language(Some(language), cx);
6684 });
6685
6686 cx.set_state(
6687 &"
6688 ˇ
6689 ˇ
6690 ˇ
6691 "
6692 .unindent(),
6693 );
6694
6695 // ensure only matching closing brackets are skipped over
6696 cx.update_editor(|editor, window, cx| {
6697 editor.handle_input("}", window, cx);
6698 editor.move_left(&MoveLeft, window, cx);
6699 editor.handle_input(")", window, cx);
6700 editor.move_left(&MoveLeft, window, cx);
6701 });
6702 cx.assert_editor_state(
6703 &"
6704 ˇ)}
6705 ˇ)}
6706 ˇ)}
6707 "
6708 .unindent(),
6709 );
6710
6711 // skip-over closing brackets at multiple cursors
6712 cx.update_editor(|editor, window, cx| {
6713 editor.handle_input(")", window, cx);
6714 editor.handle_input("}", window, cx);
6715 });
6716 cx.assert_editor_state(
6717 &"
6718 )}ˇ
6719 )}ˇ
6720 )}ˇ
6721 "
6722 .unindent(),
6723 );
6724
6725 // ignore non-close brackets
6726 cx.update_editor(|editor, window, cx| {
6727 editor.handle_input("]", window, cx);
6728 editor.move_left(&MoveLeft, window, cx);
6729 editor.handle_input("]", window, cx);
6730 });
6731 cx.assert_editor_state(
6732 &"
6733 )}]ˇ]
6734 )}]ˇ]
6735 )}]ˇ]
6736 "
6737 .unindent(),
6738 );
6739}
6740
6741#[gpui::test]
6742async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
6743 init_test(cx, |_| {});
6744
6745 let mut cx = EditorTestContext::new(cx).await;
6746
6747 let html_language = Arc::new(
6748 Language::new(
6749 LanguageConfig {
6750 name: "HTML".into(),
6751 brackets: BracketPairConfig {
6752 pairs: vec![
6753 BracketPair {
6754 start: "<".into(),
6755 end: ">".into(),
6756 close: true,
6757 ..Default::default()
6758 },
6759 BracketPair {
6760 start: "{".into(),
6761 end: "}".into(),
6762 close: true,
6763 ..Default::default()
6764 },
6765 BracketPair {
6766 start: "(".into(),
6767 end: ")".into(),
6768 close: true,
6769 ..Default::default()
6770 },
6771 ],
6772 ..Default::default()
6773 },
6774 autoclose_before: "})]>".into(),
6775 ..Default::default()
6776 },
6777 Some(tree_sitter_html::LANGUAGE.into()),
6778 )
6779 .with_injection_query(
6780 r#"
6781 (script_element
6782 (raw_text) @injection.content
6783 (#set! injection.language "javascript"))
6784 "#,
6785 )
6786 .unwrap(),
6787 );
6788
6789 let javascript_language = Arc::new(Language::new(
6790 LanguageConfig {
6791 name: "JavaScript".into(),
6792 brackets: BracketPairConfig {
6793 pairs: vec![
6794 BracketPair {
6795 start: "/*".into(),
6796 end: " */".into(),
6797 close: true,
6798 ..Default::default()
6799 },
6800 BracketPair {
6801 start: "{".into(),
6802 end: "}".into(),
6803 close: true,
6804 ..Default::default()
6805 },
6806 BracketPair {
6807 start: "(".into(),
6808 end: ")".into(),
6809 close: true,
6810 ..Default::default()
6811 },
6812 ],
6813 ..Default::default()
6814 },
6815 autoclose_before: "})]>".into(),
6816 ..Default::default()
6817 },
6818 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
6819 ));
6820
6821 cx.language_registry().add(html_language.clone());
6822 cx.language_registry().add(javascript_language.clone());
6823
6824 cx.update_buffer(|buffer, cx| {
6825 buffer.set_language(Some(html_language), cx);
6826 });
6827
6828 cx.set_state(
6829 &r#"
6830 <body>ˇ
6831 <script>
6832 var x = 1;ˇ
6833 </script>
6834 </body>ˇ
6835 "#
6836 .unindent(),
6837 );
6838
6839 // Precondition: different languages are active at different locations.
6840 cx.update_editor(|editor, window, cx| {
6841 let snapshot = editor.snapshot(window, cx);
6842 let cursors = editor.selections.ranges::<usize>(cx);
6843 let languages = cursors
6844 .iter()
6845 .map(|c| snapshot.language_at(c.start).unwrap().name())
6846 .collect::<Vec<_>>();
6847 assert_eq!(
6848 languages,
6849 &["HTML".into(), "JavaScript".into(), "HTML".into()]
6850 );
6851 });
6852
6853 // Angle brackets autoclose in HTML, but not JavaScript.
6854 cx.update_editor(|editor, window, cx| {
6855 editor.handle_input("<", window, cx);
6856 editor.handle_input("a", window, cx);
6857 });
6858 cx.assert_editor_state(
6859 &r#"
6860 <body><aˇ>
6861 <script>
6862 var x = 1;<aˇ
6863 </script>
6864 </body><aˇ>
6865 "#
6866 .unindent(),
6867 );
6868
6869 // Curly braces and parens autoclose in both HTML and JavaScript.
6870 cx.update_editor(|editor, window, cx| {
6871 editor.handle_input(" b=", window, cx);
6872 editor.handle_input("{", window, cx);
6873 editor.handle_input("c", window, cx);
6874 editor.handle_input("(", window, cx);
6875 });
6876 cx.assert_editor_state(
6877 &r#"
6878 <body><a b={c(ˇ)}>
6879 <script>
6880 var x = 1;<a b={c(ˇ)}
6881 </script>
6882 </body><a b={c(ˇ)}>
6883 "#
6884 .unindent(),
6885 );
6886
6887 // Brackets that were already autoclosed are skipped.
6888 cx.update_editor(|editor, window, cx| {
6889 editor.handle_input(")", window, cx);
6890 editor.handle_input("d", window, cx);
6891 editor.handle_input("}", window, cx);
6892 });
6893 cx.assert_editor_state(
6894 &r#"
6895 <body><a b={c()d}ˇ>
6896 <script>
6897 var x = 1;<a b={c()d}ˇ
6898 </script>
6899 </body><a b={c()d}ˇ>
6900 "#
6901 .unindent(),
6902 );
6903 cx.update_editor(|editor, window, cx| {
6904 editor.handle_input(">", window, cx);
6905 });
6906 cx.assert_editor_state(
6907 &r#"
6908 <body><a b={c()d}>ˇ
6909 <script>
6910 var x = 1;<a b={c()d}>ˇ
6911 </script>
6912 </body><a b={c()d}>ˇ
6913 "#
6914 .unindent(),
6915 );
6916
6917 // Reset
6918 cx.set_state(
6919 &r#"
6920 <body>ˇ
6921 <script>
6922 var x = 1;ˇ
6923 </script>
6924 </body>ˇ
6925 "#
6926 .unindent(),
6927 );
6928
6929 cx.update_editor(|editor, window, cx| {
6930 editor.handle_input("<", window, cx);
6931 });
6932 cx.assert_editor_state(
6933 &r#"
6934 <body><ˇ>
6935 <script>
6936 var x = 1;<ˇ
6937 </script>
6938 </body><ˇ>
6939 "#
6940 .unindent(),
6941 );
6942
6943 // When backspacing, the closing angle brackets are removed.
6944 cx.update_editor(|editor, window, cx| {
6945 editor.backspace(&Backspace, window, cx);
6946 });
6947 cx.assert_editor_state(
6948 &r#"
6949 <body>ˇ
6950 <script>
6951 var x = 1;ˇ
6952 </script>
6953 </body>ˇ
6954 "#
6955 .unindent(),
6956 );
6957
6958 // Block comments autoclose in JavaScript, but not HTML.
6959 cx.update_editor(|editor, window, cx| {
6960 editor.handle_input("/", window, cx);
6961 editor.handle_input("*", window, cx);
6962 });
6963 cx.assert_editor_state(
6964 &r#"
6965 <body>/*ˇ
6966 <script>
6967 var x = 1;/*ˇ */
6968 </script>
6969 </body>/*ˇ
6970 "#
6971 .unindent(),
6972 );
6973}
6974
6975#[gpui::test]
6976async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
6977 init_test(cx, |_| {});
6978
6979 let mut cx = EditorTestContext::new(cx).await;
6980
6981 let rust_language = Arc::new(
6982 Language::new(
6983 LanguageConfig {
6984 name: "Rust".into(),
6985 brackets: serde_json::from_value(json!([
6986 { "start": "{", "end": "}", "close": true, "newline": true },
6987 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
6988 ]))
6989 .unwrap(),
6990 autoclose_before: "})]>".into(),
6991 ..Default::default()
6992 },
6993 Some(tree_sitter_rust::LANGUAGE.into()),
6994 )
6995 .with_override_query("(string_literal) @string")
6996 .unwrap(),
6997 );
6998
6999 cx.language_registry().add(rust_language.clone());
7000 cx.update_buffer(|buffer, cx| {
7001 buffer.set_language(Some(rust_language), cx);
7002 });
7003
7004 cx.set_state(
7005 &r#"
7006 let x = ˇ
7007 "#
7008 .unindent(),
7009 );
7010
7011 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
7012 cx.update_editor(|editor, window, cx| {
7013 editor.handle_input("\"", window, cx);
7014 });
7015 cx.assert_editor_state(
7016 &r#"
7017 let x = "ˇ"
7018 "#
7019 .unindent(),
7020 );
7021
7022 // Inserting another quotation mark. The cursor moves across the existing
7023 // automatically-inserted quotation mark.
7024 cx.update_editor(|editor, window, cx| {
7025 editor.handle_input("\"", window, cx);
7026 });
7027 cx.assert_editor_state(
7028 &r#"
7029 let x = ""ˇ
7030 "#
7031 .unindent(),
7032 );
7033
7034 // Reset
7035 cx.set_state(
7036 &r#"
7037 let x = ˇ
7038 "#
7039 .unindent(),
7040 );
7041
7042 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
7043 cx.update_editor(|editor, window, cx| {
7044 editor.handle_input("\"", window, cx);
7045 editor.handle_input(" ", window, cx);
7046 editor.move_left(&Default::default(), window, cx);
7047 editor.handle_input("\\", window, cx);
7048 editor.handle_input("\"", window, cx);
7049 });
7050 cx.assert_editor_state(
7051 &r#"
7052 let x = "\"ˇ "
7053 "#
7054 .unindent(),
7055 );
7056
7057 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
7058 // mark. Nothing is inserted.
7059 cx.update_editor(|editor, window, cx| {
7060 editor.move_right(&Default::default(), window, cx);
7061 editor.handle_input("\"", window, cx);
7062 });
7063 cx.assert_editor_state(
7064 &r#"
7065 let x = "\" "ˇ
7066 "#
7067 .unindent(),
7068 );
7069}
7070
7071#[gpui::test]
7072async fn test_surround_with_pair(cx: &mut TestAppContext) {
7073 init_test(cx, |_| {});
7074
7075 let language = Arc::new(Language::new(
7076 LanguageConfig {
7077 brackets: BracketPairConfig {
7078 pairs: vec![
7079 BracketPair {
7080 start: "{".to_string(),
7081 end: "}".to_string(),
7082 close: true,
7083 surround: true,
7084 newline: true,
7085 },
7086 BracketPair {
7087 start: "/* ".to_string(),
7088 end: "*/".to_string(),
7089 close: true,
7090 surround: true,
7091 ..Default::default()
7092 },
7093 ],
7094 ..Default::default()
7095 },
7096 ..Default::default()
7097 },
7098 Some(tree_sitter_rust::LANGUAGE.into()),
7099 ));
7100
7101 let text = r#"
7102 a
7103 b
7104 c
7105 "#
7106 .unindent();
7107
7108 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7109 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7110 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7111 editor
7112 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7113 .await;
7114
7115 editor.update_in(cx, |editor, window, cx| {
7116 editor.change_selections(None, window, cx, |s| {
7117 s.select_display_ranges([
7118 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7119 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7120 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
7121 ])
7122 });
7123
7124 editor.handle_input("{", window, cx);
7125 editor.handle_input("{", window, cx);
7126 editor.handle_input("{", window, cx);
7127 assert_eq!(
7128 editor.text(cx),
7129 "
7130 {{{a}}}
7131 {{{b}}}
7132 {{{c}}}
7133 "
7134 .unindent()
7135 );
7136 assert_eq!(
7137 editor.selections.display_ranges(cx),
7138 [
7139 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
7140 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
7141 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
7142 ]
7143 );
7144
7145 editor.undo(&Undo, window, cx);
7146 editor.undo(&Undo, window, cx);
7147 editor.undo(&Undo, window, cx);
7148 assert_eq!(
7149 editor.text(cx),
7150 "
7151 a
7152 b
7153 c
7154 "
7155 .unindent()
7156 );
7157 assert_eq!(
7158 editor.selections.display_ranges(cx),
7159 [
7160 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7161 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7162 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7163 ]
7164 );
7165
7166 // Ensure inserting the first character of a multi-byte bracket pair
7167 // doesn't surround the selections with the bracket.
7168 editor.handle_input("/", window, cx);
7169 assert_eq!(
7170 editor.text(cx),
7171 "
7172 /
7173 /
7174 /
7175 "
7176 .unindent()
7177 );
7178 assert_eq!(
7179 editor.selections.display_ranges(cx),
7180 [
7181 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7182 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7183 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7184 ]
7185 );
7186
7187 editor.undo(&Undo, window, cx);
7188 assert_eq!(
7189 editor.text(cx),
7190 "
7191 a
7192 b
7193 c
7194 "
7195 .unindent()
7196 );
7197 assert_eq!(
7198 editor.selections.display_ranges(cx),
7199 [
7200 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7201 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7202 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7203 ]
7204 );
7205
7206 // Ensure inserting the last character of a multi-byte bracket pair
7207 // doesn't surround the selections with the bracket.
7208 editor.handle_input("*", window, cx);
7209 assert_eq!(
7210 editor.text(cx),
7211 "
7212 *
7213 *
7214 *
7215 "
7216 .unindent()
7217 );
7218 assert_eq!(
7219 editor.selections.display_ranges(cx),
7220 [
7221 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7222 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7223 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7224 ]
7225 );
7226 });
7227}
7228
7229#[gpui::test]
7230async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
7231 init_test(cx, |_| {});
7232
7233 let language = Arc::new(Language::new(
7234 LanguageConfig {
7235 brackets: BracketPairConfig {
7236 pairs: vec![BracketPair {
7237 start: "{".to_string(),
7238 end: "}".to_string(),
7239 close: true,
7240 surround: true,
7241 newline: true,
7242 }],
7243 ..Default::default()
7244 },
7245 autoclose_before: "}".to_string(),
7246 ..Default::default()
7247 },
7248 Some(tree_sitter_rust::LANGUAGE.into()),
7249 ));
7250
7251 let text = r#"
7252 a
7253 b
7254 c
7255 "#
7256 .unindent();
7257
7258 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7259 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7260 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7261 editor
7262 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7263 .await;
7264
7265 editor.update_in(cx, |editor, window, cx| {
7266 editor.change_selections(None, window, cx, |s| {
7267 s.select_ranges([
7268 Point::new(0, 1)..Point::new(0, 1),
7269 Point::new(1, 1)..Point::new(1, 1),
7270 Point::new(2, 1)..Point::new(2, 1),
7271 ])
7272 });
7273
7274 editor.handle_input("{", window, cx);
7275 editor.handle_input("{", window, cx);
7276 editor.handle_input("_", window, cx);
7277 assert_eq!(
7278 editor.text(cx),
7279 "
7280 a{{_}}
7281 b{{_}}
7282 c{{_}}
7283 "
7284 .unindent()
7285 );
7286 assert_eq!(
7287 editor.selections.ranges::<Point>(cx),
7288 [
7289 Point::new(0, 4)..Point::new(0, 4),
7290 Point::new(1, 4)..Point::new(1, 4),
7291 Point::new(2, 4)..Point::new(2, 4)
7292 ]
7293 );
7294
7295 editor.backspace(&Default::default(), window, cx);
7296 editor.backspace(&Default::default(), window, cx);
7297 assert_eq!(
7298 editor.text(cx),
7299 "
7300 a{}
7301 b{}
7302 c{}
7303 "
7304 .unindent()
7305 );
7306 assert_eq!(
7307 editor.selections.ranges::<Point>(cx),
7308 [
7309 Point::new(0, 2)..Point::new(0, 2),
7310 Point::new(1, 2)..Point::new(1, 2),
7311 Point::new(2, 2)..Point::new(2, 2)
7312 ]
7313 );
7314
7315 editor.delete_to_previous_word_start(&Default::default(), window, cx);
7316 assert_eq!(
7317 editor.text(cx),
7318 "
7319 a
7320 b
7321 c
7322 "
7323 .unindent()
7324 );
7325 assert_eq!(
7326 editor.selections.ranges::<Point>(cx),
7327 [
7328 Point::new(0, 1)..Point::new(0, 1),
7329 Point::new(1, 1)..Point::new(1, 1),
7330 Point::new(2, 1)..Point::new(2, 1)
7331 ]
7332 );
7333 });
7334}
7335
7336#[gpui::test]
7337async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
7338 init_test(cx, |settings| {
7339 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7340 });
7341
7342 let mut cx = EditorTestContext::new(cx).await;
7343
7344 let language = Arc::new(Language::new(
7345 LanguageConfig {
7346 brackets: BracketPairConfig {
7347 pairs: vec![
7348 BracketPair {
7349 start: "{".to_string(),
7350 end: "}".to_string(),
7351 close: true,
7352 surround: true,
7353 newline: true,
7354 },
7355 BracketPair {
7356 start: "(".to_string(),
7357 end: ")".to_string(),
7358 close: true,
7359 surround: true,
7360 newline: true,
7361 },
7362 BracketPair {
7363 start: "[".to_string(),
7364 end: "]".to_string(),
7365 close: false,
7366 surround: true,
7367 newline: true,
7368 },
7369 ],
7370 ..Default::default()
7371 },
7372 autoclose_before: "})]".to_string(),
7373 ..Default::default()
7374 },
7375 Some(tree_sitter_rust::LANGUAGE.into()),
7376 ));
7377
7378 cx.language_registry().add(language.clone());
7379 cx.update_buffer(|buffer, cx| {
7380 buffer.set_language(Some(language), cx);
7381 });
7382
7383 cx.set_state(
7384 &"
7385 {(ˇ)}
7386 [[ˇ]]
7387 {(ˇ)}
7388 "
7389 .unindent(),
7390 );
7391
7392 cx.update_editor(|editor, window, cx| {
7393 editor.backspace(&Default::default(), window, cx);
7394 editor.backspace(&Default::default(), window, cx);
7395 });
7396
7397 cx.assert_editor_state(
7398 &"
7399 ˇ
7400 ˇ]]
7401 ˇ
7402 "
7403 .unindent(),
7404 );
7405
7406 cx.update_editor(|editor, window, cx| {
7407 editor.handle_input("{", window, cx);
7408 editor.handle_input("{", window, cx);
7409 editor.move_right(&MoveRight, window, cx);
7410 editor.move_right(&MoveRight, window, cx);
7411 editor.move_left(&MoveLeft, window, cx);
7412 editor.move_left(&MoveLeft, window, cx);
7413 editor.backspace(&Default::default(), window, cx);
7414 });
7415
7416 cx.assert_editor_state(
7417 &"
7418 {ˇ}
7419 {ˇ}]]
7420 {ˇ}
7421 "
7422 .unindent(),
7423 );
7424
7425 cx.update_editor(|editor, window, cx| {
7426 editor.backspace(&Default::default(), window, cx);
7427 });
7428
7429 cx.assert_editor_state(
7430 &"
7431 ˇ
7432 ˇ]]
7433 ˇ
7434 "
7435 .unindent(),
7436 );
7437}
7438
7439#[gpui::test]
7440async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
7441 init_test(cx, |_| {});
7442
7443 let language = Arc::new(Language::new(
7444 LanguageConfig::default(),
7445 Some(tree_sitter_rust::LANGUAGE.into()),
7446 ));
7447
7448 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
7449 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7450 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7451 editor
7452 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7453 .await;
7454
7455 editor.update_in(cx, |editor, window, cx| {
7456 editor.set_auto_replace_emoji_shortcode(true);
7457
7458 editor.handle_input("Hello ", window, cx);
7459 editor.handle_input(":wave", window, cx);
7460 assert_eq!(editor.text(cx), "Hello :wave".unindent());
7461
7462 editor.handle_input(":", window, cx);
7463 assert_eq!(editor.text(cx), "Hello 👋".unindent());
7464
7465 editor.handle_input(" :smile", window, cx);
7466 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
7467
7468 editor.handle_input(":", window, cx);
7469 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
7470
7471 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
7472 editor.handle_input(":wave", window, cx);
7473 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
7474
7475 editor.handle_input(":", window, cx);
7476 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
7477
7478 editor.handle_input(":1", window, cx);
7479 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
7480
7481 editor.handle_input(":", window, cx);
7482 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
7483
7484 // Ensure shortcode does not get replaced when it is part of a word
7485 editor.handle_input(" Test:wave", window, cx);
7486 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
7487
7488 editor.handle_input(":", window, cx);
7489 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
7490
7491 editor.set_auto_replace_emoji_shortcode(false);
7492
7493 // Ensure shortcode does not get replaced when auto replace is off
7494 editor.handle_input(" :wave", window, cx);
7495 assert_eq!(
7496 editor.text(cx),
7497 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
7498 );
7499
7500 editor.handle_input(":", window, cx);
7501 assert_eq!(
7502 editor.text(cx),
7503 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
7504 );
7505 });
7506}
7507
7508#[gpui::test]
7509async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
7510 init_test(cx, |_| {});
7511
7512 let (text, insertion_ranges) = marked_text_ranges(
7513 indoc! {"
7514 ˇ
7515 "},
7516 false,
7517 );
7518
7519 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7520 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7521
7522 _ = editor.update_in(cx, |editor, window, cx| {
7523 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
7524
7525 editor
7526 .insert_snippet(&insertion_ranges, snippet, window, cx)
7527 .unwrap();
7528
7529 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7530 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7531 assert_eq!(editor.text(cx), expected_text);
7532 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7533 }
7534
7535 assert(
7536 editor,
7537 cx,
7538 indoc! {"
7539 type «» =•
7540 "},
7541 );
7542
7543 assert!(editor.context_menu_visible(), "There should be a matches");
7544 });
7545}
7546
7547#[gpui::test]
7548async fn test_snippets(cx: &mut TestAppContext) {
7549 init_test(cx, |_| {});
7550
7551 let (text, insertion_ranges) = marked_text_ranges(
7552 indoc! {"
7553 a.ˇ b
7554 a.ˇ b
7555 a.ˇ b
7556 "},
7557 false,
7558 );
7559
7560 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7561 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7562
7563 editor.update_in(cx, |editor, window, cx| {
7564 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
7565
7566 editor
7567 .insert_snippet(&insertion_ranges, snippet, window, cx)
7568 .unwrap();
7569
7570 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7571 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7572 assert_eq!(editor.text(cx), expected_text);
7573 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7574 }
7575
7576 assert(
7577 editor,
7578 cx,
7579 indoc! {"
7580 a.f(«one», two, «three») b
7581 a.f(«one», two, «three») b
7582 a.f(«one», two, «three») b
7583 "},
7584 );
7585
7586 // Can't move earlier than the first tab stop
7587 assert!(!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
7609 editor.move_to_prev_snippet_tabstop(window, cx);
7610 assert(
7611 editor,
7612 cx,
7613 indoc! {"
7614 a.f(«one», two, «three») b
7615 a.f(«one», two, «three») b
7616 a.f(«one», two, «three») b
7617 "},
7618 );
7619
7620 assert!(editor.move_to_next_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 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7631 assert(
7632 editor,
7633 cx,
7634 indoc! {"
7635 a.f(one, two, three)ˇ b
7636 a.f(one, two, three)ˇ b
7637 a.f(one, two, three)ˇ b
7638 "},
7639 );
7640
7641 // As soon as the last tab stop is reached, snippet state is gone
7642 editor.move_to_prev_snippet_tabstop(window, cx);
7643 assert(
7644 editor,
7645 cx,
7646 indoc! {"
7647 a.f(one, two, three)ˇ b
7648 a.f(one, two, three)ˇ b
7649 a.f(one, two, three)ˇ b
7650 "},
7651 );
7652 });
7653}
7654
7655#[gpui::test]
7656async fn test_document_format_during_save(cx: &mut TestAppContext) {
7657 init_test(cx, |_| {});
7658
7659 let fs = FakeFs::new(cx.executor());
7660 fs.insert_file(path!("/file.rs"), Default::default()).await;
7661
7662 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
7663
7664 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7665 language_registry.add(rust_lang());
7666 let mut fake_servers = language_registry.register_fake_lsp(
7667 "Rust",
7668 FakeLspAdapter {
7669 capabilities: lsp::ServerCapabilities {
7670 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7671 ..Default::default()
7672 },
7673 ..Default::default()
7674 },
7675 );
7676
7677 let buffer = project
7678 .update(cx, |project, cx| {
7679 project.open_local_buffer(path!("/file.rs"), cx)
7680 })
7681 .await
7682 .unwrap();
7683
7684 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7685 let (editor, cx) = cx.add_window_view(|window, cx| {
7686 build_editor_with_project(project.clone(), buffer, window, cx)
7687 });
7688 editor.update_in(cx, |editor, window, cx| {
7689 editor.set_text("one\ntwo\nthree\n", window, cx)
7690 });
7691 assert!(cx.read(|cx| editor.is_dirty(cx)));
7692
7693 cx.executor().start_waiting();
7694 let fake_server = fake_servers.next().await.unwrap();
7695
7696 let save = editor
7697 .update_in(cx, |editor, window, cx| {
7698 editor.save(true, project.clone(), window, cx)
7699 })
7700 .unwrap();
7701 fake_server
7702 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
7703 assert_eq!(
7704 params.text_document.uri,
7705 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7706 );
7707 assert_eq!(params.options.tab_size, 4);
7708 Ok(Some(vec![lsp::TextEdit::new(
7709 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7710 ", ".to_string(),
7711 )]))
7712 })
7713 .next()
7714 .await;
7715 cx.executor().start_waiting();
7716 save.await;
7717
7718 assert_eq!(
7719 editor.update(cx, |editor, cx| editor.text(cx)),
7720 "one, two\nthree\n"
7721 );
7722 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7723
7724 editor.update_in(cx, |editor, window, cx| {
7725 editor.set_text("one\ntwo\nthree\n", window, cx)
7726 });
7727 assert!(cx.read(|cx| editor.is_dirty(cx)));
7728
7729 // Ensure we can still save even if formatting hangs.
7730 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
7731 move |params, _| async move {
7732 assert_eq!(
7733 params.text_document.uri,
7734 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7735 );
7736 futures::future::pending::<()>().await;
7737 unreachable!()
7738 },
7739 );
7740 let save = editor
7741 .update_in(cx, |editor, window, cx| {
7742 editor.save(true, project.clone(), window, cx)
7743 })
7744 .unwrap();
7745 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7746 cx.executor().start_waiting();
7747 save.await;
7748 assert_eq!(
7749 editor.update(cx, |editor, cx| editor.text(cx)),
7750 "one\ntwo\nthree\n"
7751 );
7752 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7753
7754 // For non-dirty buffer, no formatting request should be sent
7755 let save = editor
7756 .update_in(cx, |editor, window, cx| {
7757 editor.save(true, project.clone(), window, cx)
7758 })
7759 .unwrap();
7760 let _pending_format_request = fake_server
7761 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
7762 panic!("Should not be invoked on non-dirty buffer");
7763 })
7764 .next();
7765 cx.executor().start_waiting();
7766 save.await;
7767
7768 // Set rust language override and assert overridden tabsize is sent to language server
7769 update_test_language_settings(cx, |settings| {
7770 settings.languages.insert(
7771 "Rust".into(),
7772 LanguageSettingsContent {
7773 tab_size: NonZeroU32::new(8),
7774 ..Default::default()
7775 },
7776 );
7777 });
7778
7779 editor.update_in(cx, |editor, window, cx| {
7780 editor.set_text("somehting_new\n", window, cx)
7781 });
7782 assert!(cx.read(|cx| editor.is_dirty(cx)));
7783 let save = editor
7784 .update_in(cx, |editor, window, cx| {
7785 editor.save(true, project.clone(), window, cx)
7786 })
7787 .unwrap();
7788 fake_server
7789 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
7790 assert_eq!(
7791 params.text_document.uri,
7792 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7793 );
7794 assert_eq!(params.options.tab_size, 8);
7795 Ok(Some(vec![]))
7796 })
7797 .next()
7798 .await;
7799 cx.executor().start_waiting();
7800 save.await;
7801}
7802
7803#[gpui::test]
7804async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
7805 init_test(cx, |_| {});
7806
7807 let cols = 4;
7808 let rows = 10;
7809 let sample_text_1 = sample_text(rows, cols, 'a');
7810 assert_eq!(
7811 sample_text_1,
7812 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
7813 );
7814 let sample_text_2 = sample_text(rows, cols, 'l');
7815 assert_eq!(
7816 sample_text_2,
7817 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
7818 );
7819 let sample_text_3 = sample_text(rows, cols, 'v');
7820 assert_eq!(
7821 sample_text_3,
7822 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
7823 );
7824
7825 let fs = FakeFs::new(cx.executor());
7826 fs.insert_tree(
7827 path!("/a"),
7828 json!({
7829 "main.rs": sample_text_1,
7830 "other.rs": sample_text_2,
7831 "lib.rs": sample_text_3,
7832 }),
7833 )
7834 .await;
7835
7836 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
7837 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7838 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
7839
7840 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7841 language_registry.add(rust_lang());
7842 let mut fake_servers = language_registry.register_fake_lsp(
7843 "Rust",
7844 FakeLspAdapter {
7845 capabilities: lsp::ServerCapabilities {
7846 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7847 ..Default::default()
7848 },
7849 ..Default::default()
7850 },
7851 );
7852
7853 let worktree = project.update(cx, |project, cx| {
7854 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
7855 assert_eq!(worktrees.len(), 1);
7856 worktrees.pop().unwrap()
7857 });
7858 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
7859
7860 let buffer_1 = project
7861 .update(cx, |project, cx| {
7862 project.open_buffer((worktree_id, "main.rs"), cx)
7863 })
7864 .await
7865 .unwrap();
7866 let buffer_2 = project
7867 .update(cx, |project, cx| {
7868 project.open_buffer((worktree_id, "other.rs"), cx)
7869 })
7870 .await
7871 .unwrap();
7872 let buffer_3 = project
7873 .update(cx, |project, cx| {
7874 project.open_buffer((worktree_id, "lib.rs"), cx)
7875 })
7876 .await
7877 .unwrap();
7878
7879 let multi_buffer = cx.new(|cx| {
7880 let mut multi_buffer = MultiBuffer::new(ReadWrite);
7881 multi_buffer.push_excerpts(
7882 buffer_1.clone(),
7883 [
7884 ExcerptRange {
7885 context: Point::new(0, 0)..Point::new(3, 0),
7886 primary: None,
7887 },
7888 ExcerptRange {
7889 context: Point::new(5, 0)..Point::new(7, 0),
7890 primary: None,
7891 },
7892 ExcerptRange {
7893 context: Point::new(9, 0)..Point::new(10, 4),
7894 primary: None,
7895 },
7896 ],
7897 cx,
7898 );
7899 multi_buffer.push_excerpts(
7900 buffer_2.clone(),
7901 [
7902 ExcerptRange {
7903 context: Point::new(0, 0)..Point::new(3, 0),
7904 primary: None,
7905 },
7906 ExcerptRange {
7907 context: Point::new(5, 0)..Point::new(7, 0),
7908 primary: None,
7909 },
7910 ExcerptRange {
7911 context: Point::new(9, 0)..Point::new(10, 4),
7912 primary: None,
7913 },
7914 ],
7915 cx,
7916 );
7917 multi_buffer.push_excerpts(
7918 buffer_3.clone(),
7919 [
7920 ExcerptRange {
7921 context: Point::new(0, 0)..Point::new(3, 0),
7922 primary: None,
7923 },
7924 ExcerptRange {
7925 context: Point::new(5, 0)..Point::new(7, 0),
7926 primary: None,
7927 },
7928 ExcerptRange {
7929 context: Point::new(9, 0)..Point::new(10, 4),
7930 primary: None,
7931 },
7932 ],
7933 cx,
7934 );
7935 multi_buffer
7936 });
7937 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
7938 Editor::new(
7939 EditorMode::Full,
7940 multi_buffer,
7941 Some(project.clone()),
7942 window,
7943 cx,
7944 )
7945 });
7946
7947 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7948 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7949 s.select_ranges(Some(1..2))
7950 });
7951 editor.insert("|one|two|three|", window, cx);
7952 });
7953 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7954 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7955 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7956 s.select_ranges(Some(60..70))
7957 });
7958 editor.insert("|four|five|six|", window, cx);
7959 });
7960 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7961
7962 // First two buffers should be edited, but not the third one.
7963 assert_eq!(
7964 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
7965 "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}",
7966 );
7967 buffer_1.update(cx, |buffer, _| {
7968 assert!(buffer.is_dirty());
7969 assert_eq!(
7970 buffer.text(),
7971 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
7972 )
7973 });
7974 buffer_2.update(cx, |buffer, _| {
7975 assert!(buffer.is_dirty());
7976 assert_eq!(
7977 buffer.text(),
7978 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
7979 )
7980 });
7981 buffer_3.update(cx, |buffer, _| {
7982 assert!(!buffer.is_dirty());
7983 assert_eq!(buffer.text(), sample_text_3,)
7984 });
7985 cx.executor().run_until_parked();
7986
7987 cx.executor().start_waiting();
7988 let save = multi_buffer_editor
7989 .update_in(cx, |editor, window, cx| {
7990 editor.save(true, project.clone(), window, cx)
7991 })
7992 .unwrap();
7993
7994 let fake_server = fake_servers.next().await.unwrap();
7995 fake_server
7996 .server
7997 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7998 Ok(Some(vec![lsp::TextEdit::new(
7999 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8000 format!("[{} formatted]", params.text_document.uri),
8001 )]))
8002 })
8003 .detach();
8004 save.await;
8005
8006 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
8007 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
8008 assert_eq!(
8009 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8010 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}"),
8011 );
8012 buffer_1.update(cx, |buffer, _| {
8013 assert!(!buffer.is_dirty());
8014 assert_eq!(
8015 buffer.text(),
8016 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
8017 )
8018 });
8019 buffer_2.update(cx, |buffer, _| {
8020 assert!(!buffer.is_dirty());
8021 assert_eq!(
8022 buffer.text(),
8023 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
8024 )
8025 });
8026 buffer_3.update(cx, |buffer, _| {
8027 assert!(!buffer.is_dirty());
8028 assert_eq!(buffer.text(), sample_text_3,)
8029 });
8030}
8031
8032#[gpui::test]
8033async fn test_range_format_during_save(cx: &mut TestAppContext) {
8034 init_test(cx, |_| {});
8035
8036 let fs = FakeFs::new(cx.executor());
8037 fs.insert_file(path!("/file.rs"), Default::default()).await;
8038
8039 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8040
8041 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8042 language_registry.add(rust_lang());
8043 let mut fake_servers = language_registry.register_fake_lsp(
8044 "Rust",
8045 FakeLspAdapter {
8046 capabilities: lsp::ServerCapabilities {
8047 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
8048 ..Default::default()
8049 },
8050 ..Default::default()
8051 },
8052 );
8053
8054 let buffer = project
8055 .update(cx, |project, cx| {
8056 project.open_local_buffer(path!("/file.rs"), cx)
8057 })
8058 .await
8059 .unwrap();
8060
8061 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8062 let (editor, cx) = cx.add_window_view(|window, cx| {
8063 build_editor_with_project(project.clone(), buffer, window, cx)
8064 });
8065 editor.update_in(cx, |editor, window, cx| {
8066 editor.set_text("one\ntwo\nthree\n", window, cx)
8067 });
8068 assert!(cx.read(|cx| editor.is_dirty(cx)));
8069
8070 cx.executor().start_waiting();
8071 let fake_server = fake_servers.next().await.unwrap();
8072
8073 let save = editor
8074 .update_in(cx, |editor, window, cx| {
8075 editor.save(true, project.clone(), window, cx)
8076 })
8077 .unwrap();
8078 fake_server
8079 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8080 assert_eq!(
8081 params.text_document.uri,
8082 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8083 );
8084 assert_eq!(params.options.tab_size, 4);
8085 Ok(Some(vec![lsp::TextEdit::new(
8086 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8087 ", ".to_string(),
8088 )]))
8089 })
8090 .next()
8091 .await;
8092 cx.executor().start_waiting();
8093 save.await;
8094 assert_eq!(
8095 editor.update(cx, |editor, cx| editor.text(cx)),
8096 "one, two\nthree\n"
8097 );
8098 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8099
8100 editor.update_in(cx, |editor, window, cx| {
8101 editor.set_text("one\ntwo\nthree\n", window, cx)
8102 });
8103 assert!(cx.read(|cx| editor.is_dirty(cx)));
8104
8105 // Ensure we can still save even if formatting hangs.
8106 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
8107 move |params, _| async move {
8108 assert_eq!(
8109 params.text_document.uri,
8110 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8111 );
8112 futures::future::pending::<()>().await;
8113 unreachable!()
8114 },
8115 );
8116 let save = editor
8117 .update_in(cx, |editor, window, cx| {
8118 editor.save(true, project.clone(), window, cx)
8119 })
8120 .unwrap();
8121 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8122 cx.executor().start_waiting();
8123 save.await;
8124 assert_eq!(
8125 editor.update(cx, |editor, cx| editor.text(cx)),
8126 "one\ntwo\nthree\n"
8127 );
8128 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8129
8130 // For non-dirty buffer, no formatting request should be sent
8131 let save = editor
8132 .update_in(cx, |editor, window, cx| {
8133 editor.save(true, project.clone(), window, cx)
8134 })
8135 .unwrap();
8136 let _pending_format_request = fake_server
8137 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
8138 panic!("Should not be invoked on non-dirty buffer");
8139 })
8140 .next();
8141 cx.executor().start_waiting();
8142 save.await;
8143
8144 // Set Rust language override and assert overridden tabsize is sent to language server
8145 update_test_language_settings(cx, |settings| {
8146 settings.languages.insert(
8147 "Rust".into(),
8148 LanguageSettingsContent {
8149 tab_size: NonZeroU32::new(8),
8150 ..Default::default()
8151 },
8152 );
8153 });
8154
8155 editor.update_in(cx, |editor, window, cx| {
8156 editor.set_text("somehting_new\n", window, cx)
8157 });
8158 assert!(cx.read(|cx| editor.is_dirty(cx)));
8159 let save = editor
8160 .update_in(cx, |editor, window, cx| {
8161 editor.save(true, project.clone(), window, cx)
8162 })
8163 .unwrap();
8164 fake_server
8165 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8166 assert_eq!(
8167 params.text_document.uri,
8168 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8169 );
8170 assert_eq!(params.options.tab_size, 8);
8171 Ok(Some(vec![]))
8172 })
8173 .next()
8174 .await;
8175 cx.executor().start_waiting();
8176 save.await;
8177}
8178
8179#[gpui::test]
8180async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
8181 init_test(cx, |settings| {
8182 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8183 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8184 ))
8185 });
8186
8187 let fs = FakeFs::new(cx.executor());
8188 fs.insert_file(path!("/file.rs"), Default::default()).await;
8189
8190 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8191
8192 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8193 language_registry.add(Arc::new(Language::new(
8194 LanguageConfig {
8195 name: "Rust".into(),
8196 matcher: LanguageMatcher {
8197 path_suffixes: vec!["rs".to_string()],
8198 ..Default::default()
8199 },
8200 ..LanguageConfig::default()
8201 },
8202 Some(tree_sitter_rust::LANGUAGE.into()),
8203 )));
8204 update_test_language_settings(cx, |settings| {
8205 // Enable Prettier formatting for the same buffer, and ensure
8206 // LSP is called instead of Prettier.
8207 settings.defaults.prettier = Some(PrettierSettings {
8208 allowed: true,
8209 ..PrettierSettings::default()
8210 });
8211 });
8212 let mut fake_servers = language_registry.register_fake_lsp(
8213 "Rust",
8214 FakeLspAdapter {
8215 capabilities: lsp::ServerCapabilities {
8216 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8217 ..Default::default()
8218 },
8219 ..Default::default()
8220 },
8221 );
8222
8223 let buffer = project
8224 .update(cx, |project, cx| {
8225 project.open_local_buffer(path!("/file.rs"), cx)
8226 })
8227 .await
8228 .unwrap();
8229
8230 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8231 let (editor, cx) = cx.add_window_view(|window, cx| {
8232 build_editor_with_project(project.clone(), buffer, window, cx)
8233 });
8234 editor.update_in(cx, |editor, window, cx| {
8235 editor.set_text("one\ntwo\nthree\n", window, cx)
8236 });
8237
8238 cx.executor().start_waiting();
8239 let fake_server = fake_servers.next().await.unwrap();
8240
8241 let format = editor
8242 .update_in(cx, |editor, window, cx| {
8243 editor.perform_format(
8244 project.clone(),
8245 FormatTrigger::Manual,
8246 FormatTarget::Buffers,
8247 window,
8248 cx,
8249 )
8250 })
8251 .unwrap();
8252 fake_server
8253 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8254 assert_eq!(
8255 params.text_document.uri,
8256 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8257 );
8258 assert_eq!(params.options.tab_size, 4);
8259 Ok(Some(vec![lsp::TextEdit::new(
8260 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8261 ", ".to_string(),
8262 )]))
8263 })
8264 .next()
8265 .await;
8266 cx.executor().start_waiting();
8267 format.await;
8268 assert_eq!(
8269 editor.update(cx, |editor, cx| editor.text(cx)),
8270 "one, two\nthree\n"
8271 );
8272
8273 editor.update_in(cx, |editor, window, cx| {
8274 editor.set_text("one\ntwo\nthree\n", window, cx)
8275 });
8276 // Ensure we don't lock if formatting hangs.
8277 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8278 move |params, _| async move {
8279 assert_eq!(
8280 params.text_document.uri,
8281 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8282 );
8283 futures::future::pending::<()>().await;
8284 unreachable!()
8285 },
8286 );
8287 let format = editor
8288 .update_in(cx, |editor, window, cx| {
8289 editor.perform_format(
8290 project,
8291 FormatTrigger::Manual,
8292 FormatTarget::Buffers,
8293 window,
8294 cx,
8295 )
8296 })
8297 .unwrap();
8298 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8299 cx.executor().start_waiting();
8300 format.await;
8301 assert_eq!(
8302 editor.update(cx, |editor, cx| editor.text(cx)),
8303 "one\ntwo\nthree\n"
8304 );
8305}
8306
8307#[gpui::test]
8308async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
8309 init_test(cx, |settings| {
8310 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8311 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8312 ))
8313 });
8314
8315 let fs = FakeFs::new(cx.executor());
8316 fs.insert_file(path!("/file.ts"), Default::default()).await;
8317
8318 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8319
8320 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8321 language_registry.add(Arc::new(Language::new(
8322 LanguageConfig {
8323 name: "TypeScript".into(),
8324 matcher: LanguageMatcher {
8325 path_suffixes: vec!["ts".to_string()],
8326 ..Default::default()
8327 },
8328 ..LanguageConfig::default()
8329 },
8330 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
8331 )));
8332 update_test_language_settings(cx, |settings| {
8333 settings.defaults.prettier = Some(PrettierSettings {
8334 allowed: true,
8335 ..PrettierSettings::default()
8336 });
8337 });
8338 let mut fake_servers = language_registry.register_fake_lsp(
8339 "TypeScript",
8340 FakeLspAdapter {
8341 capabilities: lsp::ServerCapabilities {
8342 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
8343 ..Default::default()
8344 },
8345 ..Default::default()
8346 },
8347 );
8348
8349 let buffer = project
8350 .update(cx, |project, cx| {
8351 project.open_local_buffer(path!("/file.ts"), cx)
8352 })
8353 .await
8354 .unwrap();
8355
8356 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8357 let (editor, cx) = cx.add_window_view(|window, cx| {
8358 build_editor_with_project(project.clone(), buffer, window, cx)
8359 });
8360 editor.update_in(cx, |editor, window, cx| {
8361 editor.set_text(
8362 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
8363 window,
8364 cx,
8365 )
8366 });
8367
8368 cx.executor().start_waiting();
8369 let fake_server = fake_servers.next().await.unwrap();
8370
8371 let format = editor
8372 .update_in(cx, |editor, window, cx| {
8373 editor.perform_code_action_kind(
8374 project.clone(),
8375 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
8376 window,
8377 cx,
8378 )
8379 })
8380 .unwrap();
8381 fake_server
8382 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
8383 assert_eq!(
8384 params.text_document.uri,
8385 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
8386 );
8387 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
8388 lsp::CodeAction {
8389 title: "Organize Imports".to_string(),
8390 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
8391 edit: Some(lsp::WorkspaceEdit {
8392 changes: Some(
8393 [(
8394 params.text_document.uri.clone(),
8395 vec![lsp::TextEdit::new(
8396 lsp::Range::new(
8397 lsp::Position::new(1, 0),
8398 lsp::Position::new(2, 0),
8399 ),
8400 "".to_string(),
8401 )],
8402 )]
8403 .into_iter()
8404 .collect(),
8405 ),
8406 ..Default::default()
8407 }),
8408 ..Default::default()
8409 },
8410 )]))
8411 })
8412 .next()
8413 .await;
8414 cx.executor().start_waiting();
8415 format.await;
8416 assert_eq!(
8417 editor.update(cx, |editor, cx| editor.text(cx)),
8418 "import { a } from 'module';\n\nconst x = a;\n"
8419 );
8420
8421 editor.update_in(cx, |editor, window, cx| {
8422 editor.set_text(
8423 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
8424 window,
8425 cx,
8426 )
8427 });
8428 // Ensure we don't lock if code action hangs.
8429 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
8430 move |params, _| async move {
8431 assert_eq!(
8432 params.text_document.uri,
8433 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
8434 );
8435 futures::future::pending::<()>().await;
8436 unreachable!()
8437 },
8438 );
8439 let format = editor
8440 .update_in(cx, |editor, window, cx| {
8441 editor.perform_code_action_kind(
8442 project,
8443 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
8444 window,
8445 cx,
8446 )
8447 })
8448 .unwrap();
8449 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
8450 cx.executor().start_waiting();
8451 format.await;
8452 assert_eq!(
8453 editor.update(cx, |editor, cx| editor.text(cx)),
8454 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
8455 );
8456}
8457
8458#[gpui::test]
8459async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
8460 init_test(cx, |_| {});
8461
8462 let mut cx = EditorLspTestContext::new_rust(
8463 lsp::ServerCapabilities {
8464 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8465 ..Default::default()
8466 },
8467 cx,
8468 )
8469 .await;
8470
8471 cx.set_state(indoc! {"
8472 one.twoˇ
8473 "});
8474
8475 // The format request takes a long time. When it completes, it inserts
8476 // a newline and an indent before the `.`
8477 cx.lsp
8478 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
8479 let executor = cx.background_executor().clone();
8480 async move {
8481 executor.timer(Duration::from_millis(100)).await;
8482 Ok(Some(vec![lsp::TextEdit {
8483 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
8484 new_text: "\n ".into(),
8485 }]))
8486 }
8487 });
8488
8489 // Submit a format request.
8490 let format_1 = cx
8491 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8492 .unwrap();
8493 cx.executor().run_until_parked();
8494
8495 // Submit a second format request.
8496 let format_2 = cx
8497 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8498 .unwrap();
8499 cx.executor().run_until_parked();
8500
8501 // Wait for both format requests to complete
8502 cx.executor().advance_clock(Duration::from_millis(200));
8503 cx.executor().start_waiting();
8504 format_1.await.unwrap();
8505 cx.executor().start_waiting();
8506 format_2.await.unwrap();
8507
8508 // The formatting edits only happens once.
8509 cx.assert_editor_state(indoc! {"
8510 one
8511 .twoˇ
8512 "});
8513}
8514
8515#[gpui::test]
8516async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
8517 init_test(cx, |settings| {
8518 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
8519 });
8520
8521 let mut cx = EditorLspTestContext::new_rust(
8522 lsp::ServerCapabilities {
8523 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8524 ..Default::default()
8525 },
8526 cx,
8527 )
8528 .await;
8529
8530 // Set up a buffer white some trailing whitespace and no trailing newline.
8531 cx.set_state(
8532 &[
8533 "one ", //
8534 "twoˇ", //
8535 "three ", //
8536 "four", //
8537 ]
8538 .join("\n"),
8539 );
8540
8541 // Submit a format request.
8542 let format = cx
8543 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8544 .unwrap();
8545
8546 // Record which buffer changes have been sent to the language server
8547 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
8548 cx.lsp
8549 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
8550 let buffer_changes = buffer_changes.clone();
8551 move |params, _| {
8552 buffer_changes.lock().extend(
8553 params
8554 .content_changes
8555 .into_iter()
8556 .map(|e| (e.range.unwrap(), e.text)),
8557 );
8558 }
8559 });
8560
8561 // Handle formatting requests to the language server.
8562 cx.lsp
8563 .set_request_handler::<lsp::request::Formatting, _, _>({
8564 let buffer_changes = buffer_changes.clone();
8565 move |_, _| {
8566 // When formatting is requested, trailing whitespace has already been stripped,
8567 // and the trailing newline has already been added.
8568 assert_eq!(
8569 &buffer_changes.lock()[1..],
8570 &[
8571 (
8572 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
8573 "".into()
8574 ),
8575 (
8576 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
8577 "".into()
8578 ),
8579 (
8580 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
8581 "\n".into()
8582 ),
8583 ]
8584 );
8585
8586 // Insert blank lines between each line of the buffer.
8587 async move {
8588 Ok(Some(vec![
8589 lsp::TextEdit {
8590 range: lsp::Range::new(
8591 lsp::Position::new(1, 0),
8592 lsp::Position::new(1, 0),
8593 ),
8594 new_text: "\n".into(),
8595 },
8596 lsp::TextEdit {
8597 range: lsp::Range::new(
8598 lsp::Position::new(2, 0),
8599 lsp::Position::new(2, 0),
8600 ),
8601 new_text: "\n".into(),
8602 },
8603 ]))
8604 }
8605 }
8606 });
8607
8608 // After formatting the buffer, the trailing whitespace is stripped,
8609 // a newline is appended, and the edits provided by the language server
8610 // have been applied.
8611 format.await.unwrap();
8612 cx.assert_editor_state(
8613 &[
8614 "one", //
8615 "", //
8616 "twoˇ", //
8617 "", //
8618 "three", //
8619 "four", //
8620 "", //
8621 ]
8622 .join("\n"),
8623 );
8624
8625 // Undoing the formatting undoes the trailing whitespace removal, the
8626 // trailing newline, and the LSP edits.
8627 cx.update_buffer(|buffer, cx| buffer.undo(cx));
8628 cx.assert_editor_state(
8629 &[
8630 "one ", //
8631 "twoˇ", //
8632 "three ", //
8633 "four", //
8634 ]
8635 .join("\n"),
8636 );
8637}
8638
8639#[gpui::test]
8640async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
8641 cx: &mut TestAppContext,
8642) {
8643 init_test(cx, |_| {});
8644
8645 cx.update(|cx| {
8646 cx.update_global::<SettingsStore, _>(|settings, cx| {
8647 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8648 settings.auto_signature_help = Some(true);
8649 });
8650 });
8651 });
8652
8653 let mut cx = EditorLspTestContext::new_rust(
8654 lsp::ServerCapabilities {
8655 signature_help_provider: Some(lsp::SignatureHelpOptions {
8656 ..Default::default()
8657 }),
8658 ..Default::default()
8659 },
8660 cx,
8661 )
8662 .await;
8663
8664 let language = Language::new(
8665 LanguageConfig {
8666 name: "Rust".into(),
8667 brackets: BracketPairConfig {
8668 pairs: vec![
8669 BracketPair {
8670 start: "{".to_string(),
8671 end: "}".to_string(),
8672 close: true,
8673 surround: true,
8674 newline: true,
8675 },
8676 BracketPair {
8677 start: "(".to_string(),
8678 end: ")".to_string(),
8679 close: true,
8680 surround: true,
8681 newline: true,
8682 },
8683 BracketPair {
8684 start: "/*".to_string(),
8685 end: " */".to_string(),
8686 close: true,
8687 surround: true,
8688 newline: true,
8689 },
8690 BracketPair {
8691 start: "[".to_string(),
8692 end: "]".to_string(),
8693 close: false,
8694 surround: false,
8695 newline: true,
8696 },
8697 BracketPair {
8698 start: "\"".to_string(),
8699 end: "\"".to_string(),
8700 close: true,
8701 surround: true,
8702 newline: false,
8703 },
8704 BracketPair {
8705 start: "<".to_string(),
8706 end: ">".to_string(),
8707 close: false,
8708 surround: true,
8709 newline: true,
8710 },
8711 ],
8712 ..Default::default()
8713 },
8714 autoclose_before: "})]".to_string(),
8715 ..Default::default()
8716 },
8717 Some(tree_sitter_rust::LANGUAGE.into()),
8718 );
8719 let language = Arc::new(language);
8720
8721 cx.language_registry().add(language.clone());
8722 cx.update_buffer(|buffer, cx| {
8723 buffer.set_language(Some(language), cx);
8724 });
8725
8726 cx.set_state(
8727 &r#"
8728 fn main() {
8729 sampleˇ
8730 }
8731 "#
8732 .unindent(),
8733 );
8734
8735 cx.update_editor(|editor, window, cx| {
8736 editor.handle_input("(", window, cx);
8737 });
8738 cx.assert_editor_state(
8739 &"
8740 fn main() {
8741 sample(ˇ)
8742 }
8743 "
8744 .unindent(),
8745 );
8746
8747 let mocked_response = lsp::SignatureHelp {
8748 signatures: vec![lsp::SignatureInformation {
8749 label: "fn sample(param1: u8, param2: u8)".to_string(),
8750 documentation: None,
8751 parameters: Some(vec![
8752 lsp::ParameterInformation {
8753 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8754 documentation: None,
8755 },
8756 lsp::ParameterInformation {
8757 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8758 documentation: None,
8759 },
8760 ]),
8761 active_parameter: None,
8762 }],
8763 active_signature: Some(0),
8764 active_parameter: Some(0),
8765 };
8766 handle_signature_help_request(&mut cx, mocked_response).await;
8767
8768 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8769 .await;
8770
8771 cx.editor(|editor, _, _| {
8772 let signature_help_state = editor.signature_help_state.popover().cloned();
8773 assert_eq!(
8774 signature_help_state.unwrap().label,
8775 "param1: u8, param2: u8"
8776 );
8777 });
8778}
8779
8780#[gpui::test]
8781async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
8782 init_test(cx, |_| {});
8783
8784 cx.update(|cx| {
8785 cx.update_global::<SettingsStore, _>(|settings, cx| {
8786 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8787 settings.auto_signature_help = Some(false);
8788 settings.show_signature_help_after_edits = Some(false);
8789 });
8790 });
8791 });
8792
8793 let mut cx = EditorLspTestContext::new_rust(
8794 lsp::ServerCapabilities {
8795 signature_help_provider: Some(lsp::SignatureHelpOptions {
8796 ..Default::default()
8797 }),
8798 ..Default::default()
8799 },
8800 cx,
8801 )
8802 .await;
8803
8804 let language = Language::new(
8805 LanguageConfig {
8806 name: "Rust".into(),
8807 brackets: BracketPairConfig {
8808 pairs: vec![
8809 BracketPair {
8810 start: "{".to_string(),
8811 end: "}".to_string(),
8812 close: true,
8813 surround: true,
8814 newline: true,
8815 },
8816 BracketPair {
8817 start: "(".to_string(),
8818 end: ")".to_string(),
8819 close: true,
8820 surround: true,
8821 newline: true,
8822 },
8823 BracketPair {
8824 start: "/*".to_string(),
8825 end: " */".to_string(),
8826 close: true,
8827 surround: true,
8828 newline: true,
8829 },
8830 BracketPair {
8831 start: "[".to_string(),
8832 end: "]".to_string(),
8833 close: false,
8834 surround: false,
8835 newline: true,
8836 },
8837 BracketPair {
8838 start: "\"".to_string(),
8839 end: "\"".to_string(),
8840 close: true,
8841 surround: true,
8842 newline: false,
8843 },
8844 BracketPair {
8845 start: "<".to_string(),
8846 end: ">".to_string(),
8847 close: false,
8848 surround: true,
8849 newline: true,
8850 },
8851 ],
8852 ..Default::default()
8853 },
8854 autoclose_before: "})]".to_string(),
8855 ..Default::default()
8856 },
8857 Some(tree_sitter_rust::LANGUAGE.into()),
8858 );
8859 let language = Arc::new(language);
8860
8861 cx.language_registry().add(language.clone());
8862 cx.update_buffer(|buffer, cx| {
8863 buffer.set_language(Some(language), cx);
8864 });
8865
8866 // Ensure that signature_help is not called when no signature help is enabled.
8867 cx.set_state(
8868 &r#"
8869 fn main() {
8870 sampleˇ
8871 }
8872 "#
8873 .unindent(),
8874 );
8875 cx.update_editor(|editor, window, cx| {
8876 editor.handle_input("(", window, cx);
8877 });
8878 cx.assert_editor_state(
8879 &"
8880 fn main() {
8881 sample(ˇ)
8882 }
8883 "
8884 .unindent(),
8885 );
8886 cx.editor(|editor, _, _| {
8887 assert!(editor.signature_help_state.task().is_none());
8888 });
8889
8890 let mocked_response = lsp::SignatureHelp {
8891 signatures: vec![lsp::SignatureInformation {
8892 label: "fn sample(param1: u8, param2: u8)".to_string(),
8893 documentation: None,
8894 parameters: Some(vec![
8895 lsp::ParameterInformation {
8896 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8897 documentation: None,
8898 },
8899 lsp::ParameterInformation {
8900 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8901 documentation: None,
8902 },
8903 ]),
8904 active_parameter: None,
8905 }],
8906 active_signature: Some(0),
8907 active_parameter: Some(0),
8908 };
8909
8910 // Ensure that signature_help is called when enabled afte edits
8911 cx.update(|_, cx| {
8912 cx.update_global::<SettingsStore, _>(|settings, cx| {
8913 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8914 settings.auto_signature_help = Some(false);
8915 settings.show_signature_help_after_edits = Some(true);
8916 });
8917 });
8918 });
8919 cx.set_state(
8920 &r#"
8921 fn main() {
8922 sampleˇ
8923 }
8924 "#
8925 .unindent(),
8926 );
8927 cx.update_editor(|editor, window, cx| {
8928 editor.handle_input("(", window, cx);
8929 });
8930 cx.assert_editor_state(
8931 &"
8932 fn main() {
8933 sample(ˇ)
8934 }
8935 "
8936 .unindent(),
8937 );
8938 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8939 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8940 .await;
8941 cx.update_editor(|editor, _, _| {
8942 let signature_help_state = editor.signature_help_state.popover().cloned();
8943 assert!(signature_help_state.is_some());
8944 assert_eq!(
8945 signature_help_state.unwrap().label,
8946 "param1: u8, param2: u8"
8947 );
8948 editor.signature_help_state = SignatureHelpState::default();
8949 });
8950
8951 // Ensure that signature_help is called when auto signature help override is enabled
8952 cx.update(|_, cx| {
8953 cx.update_global::<SettingsStore, _>(|settings, cx| {
8954 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8955 settings.auto_signature_help = Some(true);
8956 settings.show_signature_help_after_edits = Some(false);
8957 });
8958 });
8959 });
8960 cx.set_state(
8961 &r#"
8962 fn main() {
8963 sampleˇ
8964 }
8965 "#
8966 .unindent(),
8967 );
8968 cx.update_editor(|editor, window, cx| {
8969 editor.handle_input("(", window, cx);
8970 });
8971 cx.assert_editor_state(
8972 &"
8973 fn main() {
8974 sample(ˇ)
8975 }
8976 "
8977 .unindent(),
8978 );
8979 handle_signature_help_request(&mut cx, mocked_response).await;
8980 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8981 .await;
8982 cx.editor(|editor, _, _| {
8983 let signature_help_state = editor.signature_help_state.popover().cloned();
8984 assert!(signature_help_state.is_some());
8985 assert_eq!(
8986 signature_help_state.unwrap().label,
8987 "param1: u8, param2: u8"
8988 );
8989 });
8990}
8991
8992#[gpui::test]
8993async fn test_signature_help(cx: &mut TestAppContext) {
8994 init_test(cx, |_| {});
8995 cx.update(|cx| {
8996 cx.update_global::<SettingsStore, _>(|settings, cx| {
8997 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8998 settings.auto_signature_help = Some(true);
8999 });
9000 });
9001 });
9002
9003 let mut cx = EditorLspTestContext::new_rust(
9004 lsp::ServerCapabilities {
9005 signature_help_provider: Some(lsp::SignatureHelpOptions {
9006 ..Default::default()
9007 }),
9008 ..Default::default()
9009 },
9010 cx,
9011 )
9012 .await;
9013
9014 // A test that directly calls `show_signature_help`
9015 cx.update_editor(|editor, window, cx| {
9016 editor.show_signature_help(&ShowSignatureHelp, window, cx);
9017 });
9018
9019 let mocked_response = lsp::SignatureHelp {
9020 signatures: vec![lsp::SignatureInformation {
9021 label: "fn sample(param1: u8, param2: u8)".to_string(),
9022 documentation: None,
9023 parameters: Some(vec![
9024 lsp::ParameterInformation {
9025 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9026 documentation: None,
9027 },
9028 lsp::ParameterInformation {
9029 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9030 documentation: None,
9031 },
9032 ]),
9033 active_parameter: None,
9034 }],
9035 active_signature: Some(0),
9036 active_parameter: Some(0),
9037 };
9038 handle_signature_help_request(&mut cx, mocked_response).await;
9039
9040 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9041 .await;
9042
9043 cx.editor(|editor, _, _| {
9044 let signature_help_state = editor.signature_help_state.popover().cloned();
9045 assert!(signature_help_state.is_some());
9046 assert_eq!(
9047 signature_help_state.unwrap().label,
9048 "param1: u8, param2: u8"
9049 );
9050 });
9051
9052 // When exiting outside from inside the brackets, `signature_help` is closed.
9053 cx.set_state(indoc! {"
9054 fn main() {
9055 sample(ˇ);
9056 }
9057
9058 fn sample(param1: u8, param2: u8) {}
9059 "});
9060
9061 cx.update_editor(|editor, window, cx| {
9062 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
9063 });
9064
9065 let mocked_response = lsp::SignatureHelp {
9066 signatures: Vec::new(),
9067 active_signature: None,
9068 active_parameter: None,
9069 };
9070 handle_signature_help_request(&mut cx, mocked_response).await;
9071
9072 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
9073 .await;
9074
9075 cx.editor(|editor, _, _| {
9076 assert!(!editor.signature_help_state.is_shown());
9077 });
9078
9079 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
9080 cx.set_state(indoc! {"
9081 fn main() {
9082 sample(ˇ);
9083 }
9084
9085 fn sample(param1: u8, param2: u8) {}
9086 "});
9087
9088 let mocked_response = lsp::SignatureHelp {
9089 signatures: vec![lsp::SignatureInformation {
9090 label: "fn sample(param1: u8, param2: u8)".to_string(),
9091 documentation: None,
9092 parameters: Some(vec![
9093 lsp::ParameterInformation {
9094 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9095 documentation: None,
9096 },
9097 lsp::ParameterInformation {
9098 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9099 documentation: None,
9100 },
9101 ]),
9102 active_parameter: None,
9103 }],
9104 active_signature: Some(0),
9105 active_parameter: Some(0),
9106 };
9107 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9108 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9109 .await;
9110 cx.editor(|editor, _, _| {
9111 assert!(editor.signature_help_state.is_shown());
9112 });
9113
9114 // Restore the popover with more parameter input
9115 cx.set_state(indoc! {"
9116 fn main() {
9117 sample(param1, param2ˇ);
9118 }
9119
9120 fn sample(param1: u8, param2: u8) {}
9121 "});
9122
9123 let mocked_response = lsp::SignatureHelp {
9124 signatures: vec![lsp::SignatureInformation {
9125 label: "fn sample(param1: u8, param2: u8)".to_string(),
9126 documentation: None,
9127 parameters: Some(vec![
9128 lsp::ParameterInformation {
9129 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9130 documentation: None,
9131 },
9132 lsp::ParameterInformation {
9133 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9134 documentation: None,
9135 },
9136 ]),
9137 active_parameter: None,
9138 }],
9139 active_signature: Some(0),
9140 active_parameter: Some(1),
9141 };
9142 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9143 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9144 .await;
9145
9146 // When selecting a range, the popover is gone.
9147 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
9148 cx.update_editor(|editor, window, cx| {
9149 editor.change_selections(None, window, cx, |s| {
9150 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
9151 })
9152 });
9153 cx.assert_editor_state(indoc! {"
9154 fn main() {
9155 sample(param1, «ˇparam2»);
9156 }
9157
9158 fn sample(param1: u8, param2: u8) {}
9159 "});
9160 cx.editor(|editor, _, _| {
9161 assert!(!editor.signature_help_state.is_shown());
9162 });
9163
9164 // When unselecting again, the popover is back if within the brackets.
9165 cx.update_editor(|editor, window, cx| {
9166 editor.change_selections(None, window, cx, |s| {
9167 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9168 })
9169 });
9170 cx.assert_editor_state(indoc! {"
9171 fn main() {
9172 sample(param1, ˇparam2);
9173 }
9174
9175 fn sample(param1: u8, param2: u8) {}
9176 "});
9177 handle_signature_help_request(&mut cx, mocked_response).await;
9178 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9179 .await;
9180 cx.editor(|editor, _, _| {
9181 assert!(editor.signature_help_state.is_shown());
9182 });
9183
9184 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
9185 cx.update_editor(|editor, window, cx| {
9186 editor.change_selections(None, window, cx, |s| {
9187 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
9188 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9189 })
9190 });
9191 cx.assert_editor_state(indoc! {"
9192 fn main() {
9193 sample(param1, ˇparam2);
9194 }
9195
9196 fn sample(param1: u8, param2: u8) {}
9197 "});
9198
9199 let mocked_response = lsp::SignatureHelp {
9200 signatures: vec![lsp::SignatureInformation {
9201 label: "fn sample(param1: u8, param2: u8)".to_string(),
9202 documentation: None,
9203 parameters: Some(vec![
9204 lsp::ParameterInformation {
9205 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9206 documentation: None,
9207 },
9208 lsp::ParameterInformation {
9209 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9210 documentation: None,
9211 },
9212 ]),
9213 active_parameter: None,
9214 }],
9215 active_signature: Some(0),
9216 active_parameter: Some(1),
9217 };
9218 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9219 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9220 .await;
9221 cx.update_editor(|editor, _, cx| {
9222 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
9223 });
9224 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
9225 .await;
9226 cx.update_editor(|editor, window, cx| {
9227 editor.change_selections(None, window, cx, |s| {
9228 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
9229 })
9230 });
9231 cx.assert_editor_state(indoc! {"
9232 fn main() {
9233 sample(param1, «ˇparam2»);
9234 }
9235
9236 fn sample(param1: u8, param2: u8) {}
9237 "});
9238 cx.update_editor(|editor, window, cx| {
9239 editor.change_selections(None, window, cx, |s| {
9240 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9241 })
9242 });
9243 cx.assert_editor_state(indoc! {"
9244 fn main() {
9245 sample(param1, ˇparam2);
9246 }
9247
9248 fn sample(param1: u8, param2: u8) {}
9249 "});
9250 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
9251 .await;
9252}
9253
9254#[gpui::test]
9255async fn test_completion(cx: &mut TestAppContext) {
9256 init_test(cx, |_| {});
9257
9258 let mut cx = EditorLspTestContext::new_rust(
9259 lsp::ServerCapabilities {
9260 completion_provider: Some(lsp::CompletionOptions {
9261 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9262 resolve_provider: Some(true),
9263 ..Default::default()
9264 }),
9265 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9266 ..Default::default()
9267 },
9268 cx,
9269 )
9270 .await;
9271 let counter = Arc::new(AtomicUsize::new(0));
9272
9273 cx.set_state(indoc! {"
9274 oneˇ
9275 two
9276 three
9277 "});
9278 cx.simulate_keystroke(".");
9279 handle_completion_request(
9280 &mut cx,
9281 indoc! {"
9282 one.|<>
9283 two
9284 three
9285 "},
9286 vec!["first_completion", "second_completion"],
9287 counter.clone(),
9288 )
9289 .await;
9290 cx.condition(|editor, _| editor.context_menu_visible())
9291 .await;
9292 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
9293
9294 let _handler = handle_signature_help_request(
9295 &mut cx,
9296 lsp::SignatureHelp {
9297 signatures: vec![lsp::SignatureInformation {
9298 label: "test signature".to_string(),
9299 documentation: None,
9300 parameters: Some(vec![lsp::ParameterInformation {
9301 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
9302 documentation: None,
9303 }]),
9304 active_parameter: None,
9305 }],
9306 active_signature: None,
9307 active_parameter: None,
9308 },
9309 );
9310 cx.update_editor(|editor, window, cx| {
9311 assert!(
9312 !editor.signature_help_state.is_shown(),
9313 "No signature help was called for"
9314 );
9315 editor.show_signature_help(&ShowSignatureHelp, window, cx);
9316 });
9317 cx.run_until_parked();
9318 cx.update_editor(|editor, _, _| {
9319 assert!(
9320 !editor.signature_help_state.is_shown(),
9321 "No signature help should be shown when completions menu is open"
9322 );
9323 });
9324
9325 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9326 editor.context_menu_next(&Default::default(), window, cx);
9327 editor
9328 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9329 .unwrap()
9330 });
9331 cx.assert_editor_state(indoc! {"
9332 one.second_completionˇ
9333 two
9334 three
9335 "});
9336
9337 handle_resolve_completion_request(
9338 &mut cx,
9339 Some(vec![
9340 (
9341 //This overlaps with the primary completion edit which is
9342 //misbehavior from the LSP spec, test that we filter it out
9343 indoc! {"
9344 one.second_ˇcompletion
9345 two
9346 threeˇ
9347 "},
9348 "overlapping additional edit",
9349 ),
9350 (
9351 indoc! {"
9352 one.second_completion
9353 two
9354 threeˇ
9355 "},
9356 "\nadditional edit",
9357 ),
9358 ]),
9359 )
9360 .await;
9361 apply_additional_edits.await.unwrap();
9362 cx.assert_editor_state(indoc! {"
9363 one.second_completionˇ
9364 two
9365 three
9366 additional edit
9367 "});
9368
9369 cx.set_state(indoc! {"
9370 one.second_completion
9371 twoˇ
9372 threeˇ
9373 additional edit
9374 "});
9375 cx.simulate_keystroke(" ");
9376 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9377 cx.simulate_keystroke("s");
9378 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9379
9380 cx.assert_editor_state(indoc! {"
9381 one.second_completion
9382 two sˇ
9383 three sˇ
9384 additional edit
9385 "});
9386 handle_completion_request(
9387 &mut cx,
9388 indoc! {"
9389 one.second_completion
9390 two s
9391 three <s|>
9392 additional edit
9393 "},
9394 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
9395 counter.clone(),
9396 )
9397 .await;
9398 cx.condition(|editor, _| editor.context_menu_visible())
9399 .await;
9400 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
9401
9402 cx.simulate_keystroke("i");
9403
9404 handle_completion_request(
9405 &mut cx,
9406 indoc! {"
9407 one.second_completion
9408 two si
9409 three <si|>
9410 additional edit
9411 "},
9412 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
9413 counter.clone(),
9414 )
9415 .await;
9416 cx.condition(|editor, _| editor.context_menu_visible())
9417 .await;
9418 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
9419
9420 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9421 editor
9422 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9423 .unwrap()
9424 });
9425 cx.assert_editor_state(indoc! {"
9426 one.second_completion
9427 two sixth_completionˇ
9428 three sixth_completionˇ
9429 additional edit
9430 "});
9431
9432 apply_additional_edits.await.unwrap();
9433
9434 update_test_language_settings(&mut cx, |settings| {
9435 settings.defaults.show_completions_on_input = Some(false);
9436 });
9437 cx.set_state("editorˇ");
9438 cx.simulate_keystroke(".");
9439 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9440 cx.simulate_keystrokes("c l o");
9441 cx.assert_editor_state("editor.cloˇ");
9442 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9443 cx.update_editor(|editor, window, cx| {
9444 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
9445 });
9446 handle_completion_request(
9447 &mut cx,
9448 "editor.<clo|>",
9449 vec!["close", "clobber"],
9450 counter.clone(),
9451 )
9452 .await;
9453 cx.condition(|editor, _| editor.context_menu_visible())
9454 .await;
9455 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
9456
9457 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9458 editor
9459 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9460 .unwrap()
9461 });
9462 cx.assert_editor_state("editor.closeˇ");
9463 handle_resolve_completion_request(&mut cx, None).await;
9464 apply_additional_edits.await.unwrap();
9465}
9466
9467#[gpui::test]
9468async fn test_word_completion(cx: &mut TestAppContext) {
9469 let lsp_fetch_timeout_ms = 10;
9470 init_test(cx, |language_settings| {
9471 language_settings.defaults.completions = Some(CompletionSettings {
9472 words: WordsCompletionMode::Fallback,
9473 lsp: true,
9474 lsp_fetch_timeout_ms: 10,
9475 });
9476 });
9477
9478 let mut cx = EditorLspTestContext::new_rust(
9479 lsp::ServerCapabilities {
9480 completion_provider: Some(lsp::CompletionOptions {
9481 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9482 ..lsp::CompletionOptions::default()
9483 }),
9484 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9485 ..lsp::ServerCapabilities::default()
9486 },
9487 cx,
9488 )
9489 .await;
9490
9491 let throttle_completions = Arc::new(AtomicBool::new(false));
9492
9493 let lsp_throttle_completions = throttle_completions.clone();
9494 let _completion_requests_handler =
9495 cx.lsp
9496 .server
9497 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
9498 let lsp_throttle_completions = lsp_throttle_completions.clone();
9499 let cx = cx.clone();
9500 async move {
9501 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
9502 cx.background_executor()
9503 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
9504 .await;
9505 }
9506 Ok(Some(lsp::CompletionResponse::Array(vec![
9507 lsp::CompletionItem {
9508 label: "first".into(),
9509 ..lsp::CompletionItem::default()
9510 },
9511 lsp::CompletionItem {
9512 label: "last".into(),
9513 ..lsp::CompletionItem::default()
9514 },
9515 ])))
9516 }
9517 });
9518
9519 cx.set_state(indoc! {"
9520 oneˇ
9521 two
9522 three
9523 "});
9524 cx.simulate_keystroke(".");
9525 cx.executor().run_until_parked();
9526 cx.condition(|editor, _| editor.context_menu_visible())
9527 .await;
9528 cx.update_editor(|editor, window, cx| {
9529 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9530 {
9531 assert_eq!(
9532 completion_menu_entries(&menu),
9533 &["first", "last"],
9534 "When LSP server is fast to reply, no fallback word completions are used"
9535 );
9536 } else {
9537 panic!("expected completion menu to be open");
9538 }
9539 editor.cancel(&Cancel, window, cx);
9540 });
9541 cx.executor().run_until_parked();
9542 cx.condition(|editor, _| !editor.context_menu_visible())
9543 .await;
9544
9545 throttle_completions.store(true, atomic::Ordering::Release);
9546 cx.simulate_keystroke(".");
9547 cx.executor()
9548 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
9549 cx.executor().run_until_parked();
9550 cx.condition(|editor, _| editor.context_menu_visible())
9551 .await;
9552 cx.update_editor(|editor, _, _| {
9553 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9554 {
9555 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
9556 "When LSP server is slow, document words can be shown instead, if configured accordingly");
9557 } else {
9558 panic!("expected completion menu to be open");
9559 }
9560 });
9561}
9562
9563#[gpui::test]
9564async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
9565 init_test(cx, |language_settings| {
9566 language_settings.defaults.completions = Some(CompletionSettings {
9567 words: WordsCompletionMode::Enabled,
9568 lsp: true,
9569 lsp_fetch_timeout_ms: 0,
9570 });
9571 });
9572
9573 let mut cx = EditorLspTestContext::new_rust(
9574 lsp::ServerCapabilities {
9575 completion_provider: Some(lsp::CompletionOptions {
9576 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9577 ..lsp::CompletionOptions::default()
9578 }),
9579 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9580 ..lsp::ServerCapabilities::default()
9581 },
9582 cx,
9583 )
9584 .await;
9585
9586 let _completion_requests_handler =
9587 cx.lsp
9588 .server
9589 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
9590 Ok(Some(lsp::CompletionResponse::Array(vec![
9591 lsp::CompletionItem {
9592 label: "first".into(),
9593 ..lsp::CompletionItem::default()
9594 },
9595 lsp::CompletionItem {
9596 label: "last".into(),
9597 ..lsp::CompletionItem::default()
9598 },
9599 ])))
9600 });
9601
9602 cx.set_state(indoc! {"ˇ
9603 first
9604 last
9605 second
9606 "});
9607 cx.simulate_keystroke(".");
9608 cx.executor().run_until_parked();
9609 cx.condition(|editor, _| editor.context_menu_visible())
9610 .await;
9611 cx.update_editor(|editor, _, _| {
9612 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9613 {
9614 assert_eq!(
9615 completion_menu_entries(&menu),
9616 &["first", "last", "second"],
9617 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
9618 );
9619 } else {
9620 panic!("expected completion menu to be open");
9621 }
9622 });
9623}
9624
9625#[gpui::test]
9626async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
9627 init_test(cx, |language_settings| {
9628 language_settings.defaults.completions = Some(CompletionSettings {
9629 words: WordsCompletionMode::Disabled,
9630 lsp: true,
9631 lsp_fetch_timeout_ms: 0,
9632 });
9633 });
9634
9635 let mut cx = EditorLspTestContext::new_rust(
9636 lsp::ServerCapabilities {
9637 completion_provider: Some(lsp::CompletionOptions {
9638 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9639 ..lsp::CompletionOptions::default()
9640 }),
9641 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9642 ..lsp::ServerCapabilities::default()
9643 },
9644 cx,
9645 )
9646 .await;
9647
9648 let _completion_requests_handler =
9649 cx.lsp
9650 .server
9651 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
9652 panic!("LSP completions should not be queried when dealing with word completions")
9653 });
9654
9655 cx.set_state(indoc! {"ˇ
9656 first
9657 last
9658 second
9659 "});
9660 cx.update_editor(|editor, window, cx| {
9661 editor.show_word_completions(&ShowWordCompletions, window, cx);
9662 });
9663 cx.executor().run_until_parked();
9664 cx.condition(|editor, _| editor.context_menu_visible())
9665 .await;
9666 cx.update_editor(|editor, _, _| {
9667 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9668 {
9669 assert_eq!(
9670 completion_menu_entries(&menu),
9671 &["first", "last", "second"],
9672 "`ShowWordCompletions` action should show word completions"
9673 );
9674 } else {
9675 panic!("expected completion menu to be open");
9676 }
9677 });
9678
9679 cx.simulate_keystroke("l");
9680 cx.executor().run_until_parked();
9681 cx.condition(|editor, _| editor.context_menu_visible())
9682 .await;
9683 cx.update_editor(|editor, _, _| {
9684 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9685 {
9686 assert_eq!(
9687 completion_menu_entries(&menu),
9688 &["last"],
9689 "After showing word completions, further editing should filter them and not query the LSP"
9690 );
9691 } else {
9692 panic!("expected completion menu to be open");
9693 }
9694 });
9695}
9696
9697#[gpui::test]
9698async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
9699 init_test(cx, |language_settings| {
9700 language_settings.defaults.completions = Some(CompletionSettings {
9701 words: WordsCompletionMode::Fallback,
9702 lsp: false,
9703 lsp_fetch_timeout_ms: 0,
9704 });
9705 });
9706
9707 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
9708
9709 cx.set_state(indoc! {"ˇ
9710 0_usize
9711 let
9712 33
9713 4.5f32
9714 "});
9715 cx.update_editor(|editor, window, cx| {
9716 editor.show_completions(&ShowCompletions::default(), window, cx);
9717 });
9718 cx.executor().run_until_parked();
9719 cx.condition(|editor, _| editor.context_menu_visible())
9720 .await;
9721 cx.update_editor(|editor, window, cx| {
9722 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9723 {
9724 assert_eq!(
9725 completion_menu_entries(&menu),
9726 &["let"],
9727 "With no digits in the completion query, no digits should be in the word completions"
9728 );
9729 } else {
9730 panic!("expected completion menu to be open");
9731 }
9732 editor.cancel(&Cancel, window, cx);
9733 });
9734
9735 cx.set_state(indoc! {"3ˇ
9736 0_usize
9737 let
9738 3
9739 33.35f32
9740 "});
9741 cx.update_editor(|editor, window, cx| {
9742 editor.show_completions(&ShowCompletions::default(), window, cx);
9743 });
9744 cx.executor().run_until_parked();
9745 cx.condition(|editor, _| editor.context_menu_visible())
9746 .await;
9747 cx.update_editor(|editor, _, _| {
9748 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9749 {
9750 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
9751 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
9752 } else {
9753 panic!("expected completion menu to be open");
9754 }
9755 });
9756}
9757
9758fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
9759 let position = || lsp::Position {
9760 line: params.text_document_position.position.line,
9761 character: params.text_document_position.position.character,
9762 };
9763 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
9764 range: lsp::Range {
9765 start: position(),
9766 end: position(),
9767 },
9768 new_text: text.to_string(),
9769 }))
9770}
9771
9772#[gpui::test]
9773async fn test_multiline_completion(cx: &mut TestAppContext) {
9774 init_test(cx, |_| {});
9775
9776 let fs = FakeFs::new(cx.executor());
9777 fs.insert_tree(
9778 path!("/a"),
9779 json!({
9780 "main.ts": "a",
9781 }),
9782 )
9783 .await;
9784
9785 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
9786 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9787 let typescript_language = Arc::new(Language::new(
9788 LanguageConfig {
9789 name: "TypeScript".into(),
9790 matcher: LanguageMatcher {
9791 path_suffixes: vec!["ts".to_string()],
9792 ..LanguageMatcher::default()
9793 },
9794 line_comments: vec!["// ".into()],
9795 ..LanguageConfig::default()
9796 },
9797 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
9798 ));
9799 language_registry.add(typescript_language.clone());
9800 let mut fake_servers = language_registry.register_fake_lsp(
9801 "TypeScript",
9802 FakeLspAdapter {
9803 capabilities: lsp::ServerCapabilities {
9804 completion_provider: Some(lsp::CompletionOptions {
9805 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9806 ..lsp::CompletionOptions::default()
9807 }),
9808 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9809 ..lsp::ServerCapabilities::default()
9810 },
9811 // Emulate vtsls label generation
9812 label_for_completion: Some(Box::new(|item, _| {
9813 let text = if let Some(description) = item
9814 .label_details
9815 .as_ref()
9816 .and_then(|label_details| label_details.description.as_ref())
9817 {
9818 format!("{} {}", item.label, description)
9819 } else if let Some(detail) = &item.detail {
9820 format!("{} {}", item.label, detail)
9821 } else {
9822 item.label.clone()
9823 };
9824 let len = text.len();
9825 Some(language::CodeLabel {
9826 text,
9827 runs: Vec::new(),
9828 filter_range: 0..len,
9829 })
9830 })),
9831 ..FakeLspAdapter::default()
9832 },
9833 );
9834 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9835 let cx = &mut VisualTestContext::from_window(*workspace, cx);
9836 let worktree_id = workspace
9837 .update(cx, |workspace, _window, cx| {
9838 workspace.project().update(cx, |project, cx| {
9839 project.worktrees(cx).next().unwrap().read(cx).id()
9840 })
9841 })
9842 .unwrap();
9843 let _buffer = project
9844 .update(cx, |project, cx| {
9845 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
9846 })
9847 .await
9848 .unwrap();
9849 let editor = workspace
9850 .update(cx, |workspace, window, cx| {
9851 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
9852 })
9853 .unwrap()
9854 .await
9855 .unwrap()
9856 .downcast::<Editor>()
9857 .unwrap();
9858 let fake_server = fake_servers.next().await.unwrap();
9859
9860 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
9861 let multiline_label_2 = "a\nb\nc\n";
9862 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
9863 let multiline_description = "d\ne\nf\n";
9864 let multiline_detail_2 = "g\nh\ni\n";
9865
9866 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
9867 move |params, _| async move {
9868 Ok(Some(lsp::CompletionResponse::Array(vec![
9869 lsp::CompletionItem {
9870 label: multiline_label.to_string(),
9871 text_edit: gen_text_edit(¶ms, "new_text_1"),
9872 ..lsp::CompletionItem::default()
9873 },
9874 lsp::CompletionItem {
9875 label: "single line label 1".to_string(),
9876 detail: Some(multiline_detail.to_string()),
9877 text_edit: gen_text_edit(¶ms, "new_text_2"),
9878 ..lsp::CompletionItem::default()
9879 },
9880 lsp::CompletionItem {
9881 label: "single line label 2".to_string(),
9882 label_details: Some(lsp::CompletionItemLabelDetails {
9883 description: Some(multiline_description.to_string()),
9884 detail: None,
9885 }),
9886 text_edit: gen_text_edit(¶ms, "new_text_2"),
9887 ..lsp::CompletionItem::default()
9888 },
9889 lsp::CompletionItem {
9890 label: multiline_label_2.to_string(),
9891 detail: Some(multiline_detail_2.to_string()),
9892 text_edit: gen_text_edit(¶ms, "new_text_3"),
9893 ..lsp::CompletionItem::default()
9894 },
9895 lsp::CompletionItem {
9896 label: "Label with many spaces and \t but without newlines".to_string(),
9897 detail: Some(
9898 "Details with many spaces and \t but without newlines".to_string(),
9899 ),
9900 text_edit: gen_text_edit(¶ms, "new_text_4"),
9901 ..lsp::CompletionItem::default()
9902 },
9903 ])))
9904 },
9905 );
9906
9907 editor.update_in(cx, |editor, window, cx| {
9908 cx.focus_self(window);
9909 editor.move_to_end(&MoveToEnd, window, cx);
9910 editor.handle_input(".", window, cx);
9911 });
9912 cx.run_until_parked();
9913 completion_handle.next().await.unwrap();
9914
9915 editor.update(cx, |editor, _| {
9916 assert!(editor.context_menu_visible());
9917 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9918 {
9919 let completion_labels = menu
9920 .completions
9921 .borrow()
9922 .iter()
9923 .map(|c| c.label.text.clone())
9924 .collect::<Vec<_>>();
9925 assert_eq!(
9926 completion_labels,
9927 &[
9928 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
9929 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
9930 "single line label 2 d e f ",
9931 "a b c g h i ",
9932 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
9933 ],
9934 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
9935 );
9936
9937 for completion in menu
9938 .completions
9939 .borrow()
9940 .iter() {
9941 assert_eq!(
9942 completion.label.filter_range,
9943 0..completion.label.text.len(),
9944 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
9945 );
9946 }
9947 } else {
9948 panic!("expected completion menu to be open");
9949 }
9950 });
9951}
9952
9953#[gpui::test]
9954async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
9955 init_test(cx, |_| {});
9956 let mut cx = EditorLspTestContext::new_rust(
9957 lsp::ServerCapabilities {
9958 completion_provider: Some(lsp::CompletionOptions {
9959 trigger_characters: Some(vec![".".to_string()]),
9960 ..Default::default()
9961 }),
9962 ..Default::default()
9963 },
9964 cx,
9965 )
9966 .await;
9967 cx.lsp
9968 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
9969 Ok(Some(lsp::CompletionResponse::Array(vec![
9970 lsp::CompletionItem {
9971 label: "first".into(),
9972 ..Default::default()
9973 },
9974 lsp::CompletionItem {
9975 label: "last".into(),
9976 ..Default::default()
9977 },
9978 ])))
9979 });
9980 cx.set_state("variableˇ");
9981 cx.simulate_keystroke(".");
9982 cx.executor().run_until_parked();
9983
9984 cx.update_editor(|editor, _, _| {
9985 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9986 {
9987 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
9988 } else {
9989 panic!("expected completion menu to be open");
9990 }
9991 });
9992
9993 cx.update_editor(|editor, window, cx| {
9994 editor.move_page_down(&MovePageDown::default(), window, cx);
9995 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9996 {
9997 assert!(
9998 menu.selected_item == 1,
9999 "expected PageDown to select the last item from the context menu"
10000 );
10001 } else {
10002 panic!("expected completion menu to stay open after PageDown");
10003 }
10004 });
10005
10006 cx.update_editor(|editor, window, cx| {
10007 editor.move_page_up(&MovePageUp::default(), window, cx);
10008 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10009 {
10010 assert!(
10011 menu.selected_item == 0,
10012 "expected PageUp to select the first item from the context menu"
10013 );
10014 } else {
10015 panic!("expected completion menu to stay open after PageUp");
10016 }
10017 });
10018}
10019
10020#[gpui::test]
10021async fn test_completion_sort(cx: &mut TestAppContext) {
10022 init_test(cx, |_| {});
10023 let mut cx = EditorLspTestContext::new_rust(
10024 lsp::ServerCapabilities {
10025 completion_provider: Some(lsp::CompletionOptions {
10026 trigger_characters: Some(vec![".".to_string()]),
10027 ..Default::default()
10028 }),
10029 ..Default::default()
10030 },
10031 cx,
10032 )
10033 .await;
10034 cx.lsp
10035 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
10036 Ok(Some(lsp::CompletionResponse::Array(vec![
10037 lsp::CompletionItem {
10038 label: "Range".into(),
10039 sort_text: Some("a".into()),
10040 ..Default::default()
10041 },
10042 lsp::CompletionItem {
10043 label: "r".into(),
10044 sort_text: Some("b".into()),
10045 ..Default::default()
10046 },
10047 lsp::CompletionItem {
10048 label: "ret".into(),
10049 sort_text: Some("c".into()),
10050 ..Default::default()
10051 },
10052 lsp::CompletionItem {
10053 label: "return".into(),
10054 sort_text: Some("d".into()),
10055 ..Default::default()
10056 },
10057 lsp::CompletionItem {
10058 label: "slice".into(),
10059 sort_text: Some("d".into()),
10060 ..Default::default()
10061 },
10062 ])))
10063 });
10064 cx.set_state("rˇ");
10065 cx.executor().run_until_parked();
10066 cx.update_editor(|editor, window, cx| {
10067 editor.show_completions(
10068 &ShowCompletions {
10069 trigger: Some("r".into()),
10070 },
10071 window,
10072 cx,
10073 );
10074 });
10075 cx.executor().run_until_parked();
10076
10077 cx.update_editor(|editor, _, _| {
10078 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10079 {
10080 assert_eq!(
10081 completion_menu_entries(&menu),
10082 &["r", "ret", "Range", "return"]
10083 );
10084 } else {
10085 panic!("expected completion menu to be open");
10086 }
10087 });
10088}
10089
10090#[gpui::test]
10091async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
10092 init_test(cx, |_| {});
10093
10094 let mut cx = EditorLspTestContext::new_rust(
10095 lsp::ServerCapabilities {
10096 completion_provider: Some(lsp::CompletionOptions {
10097 trigger_characters: Some(vec![".".to_string()]),
10098 resolve_provider: Some(true),
10099 ..Default::default()
10100 }),
10101 ..Default::default()
10102 },
10103 cx,
10104 )
10105 .await;
10106
10107 cx.set_state("fn main() { let a = 2ˇ; }");
10108 cx.simulate_keystroke(".");
10109 let completion_item = lsp::CompletionItem {
10110 label: "Some".into(),
10111 kind: Some(lsp::CompletionItemKind::SNIPPET),
10112 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
10113 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
10114 kind: lsp::MarkupKind::Markdown,
10115 value: "```rust\nSome(2)\n```".to_string(),
10116 })),
10117 deprecated: Some(false),
10118 sort_text: Some("Some".to_string()),
10119 filter_text: Some("Some".to_string()),
10120 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
10121 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
10122 range: lsp::Range {
10123 start: lsp::Position {
10124 line: 0,
10125 character: 22,
10126 },
10127 end: lsp::Position {
10128 line: 0,
10129 character: 22,
10130 },
10131 },
10132 new_text: "Some(2)".to_string(),
10133 })),
10134 additional_text_edits: Some(vec![lsp::TextEdit {
10135 range: lsp::Range {
10136 start: lsp::Position {
10137 line: 0,
10138 character: 20,
10139 },
10140 end: lsp::Position {
10141 line: 0,
10142 character: 22,
10143 },
10144 },
10145 new_text: "".to_string(),
10146 }]),
10147 ..Default::default()
10148 };
10149
10150 let closure_completion_item = completion_item.clone();
10151 let counter = Arc::new(AtomicUsize::new(0));
10152 let counter_clone = counter.clone();
10153 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
10154 let task_completion_item = closure_completion_item.clone();
10155 counter_clone.fetch_add(1, atomic::Ordering::Release);
10156 async move {
10157 Ok(Some(lsp::CompletionResponse::Array(vec![
10158 task_completion_item,
10159 ])))
10160 }
10161 });
10162
10163 cx.condition(|editor, _| editor.context_menu_visible())
10164 .await;
10165 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
10166 assert!(request.next().await.is_some());
10167 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10168
10169 cx.simulate_keystrokes("S o m");
10170 cx.condition(|editor, _| editor.context_menu_visible())
10171 .await;
10172 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
10173 assert!(request.next().await.is_some());
10174 assert!(request.next().await.is_some());
10175 assert!(request.next().await.is_some());
10176 request.close();
10177 assert!(request.next().await.is_none());
10178 assert_eq!(
10179 counter.load(atomic::Ordering::Acquire),
10180 4,
10181 "With the completions menu open, only one LSP request should happen per input"
10182 );
10183}
10184
10185#[gpui::test]
10186async fn test_toggle_comment(cx: &mut TestAppContext) {
10187 init_test(cx, |_| {});
10188 let mut cx = EditorTestContext::new(cx).await;
10189 let language = Arc::new(Language::new(
10190 LanguageConfig {
10191 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
10192 ..Default::default()
10193 },
10194 Some(tree_sitter_rust::LANGUAGE.into()),
10195 ));
10196 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
10197
10198 // If multiple selections intersect a line, the line is only toggled once.
10199 cx.set_state(indoc! {"
10200 fn a() {
10201 «//b();
10202 ˇ»// «c();
10203 //ˇ» d();
10204 }
10205 "});
10206
10207 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10208
10209 cx.assert_editor_state(indoc! {"
10210 fn a() {
10211 «b();
10212 c();
10213 ˇ» d();
10214 }
10215 "});
10216
10217 // The comment prefix is inserted at the same column for every line in a
10218 // selection.
10219 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10220
10221 cx.assert_editor_state(indoc! {"
10222 fn a() {
10223 // «b();
10224 // c();
10225 ˇ»// d();
10226 }
10227 "});
10228
10229 // If a selection ends at the beginning of a line, that line is not toggled.
10230 cx.set_selections_state(indoc! {"
10231 fn a() {
10232 // b();
10233 «// c();
10234 ˇ» // d();
10235 }
10236 "});
10237
10238 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10239
10240 cx.assert_editor_state(indoc! {"
10241 fn a() {
10242 // b();
10243 «c();
10244 ˇ» // d();
10245 }
10246 "});
10247
10248 // If a selection span a single line and is empty, the line is toggled.
10249 cx.set_state(indoc! {"
10250 fn a() {
10251 a();
10252 b();
10253 ˇ
10254 }
10255 "});
10256
10257 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10258
10259 cx.assert_editor_state(indoc! {"
10260 fn a() {
10261 a();
10262 b();
10263 //•ˇ
10264 }
10265 "});
10266
10267 // If a selection span multiple lines, empty lines are not toggled.
10268 cx.set_state(indoc! {"
10269 fn a() {
10270 «a();
10271
10272 c();ˇ»
10273 }
10274 "});
10275
10276 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10277
10278 cx.assert_editor_state(indoc! {"
10279 fn a() {
10280 // «a();
10281
10282 // c();ˇ»
10283 }
10284 "});
10285
10286 // If a selection includes multiple comment prefixes, all lines are uncommented.
10287 cx.set_state(indoc! {"
10288 fn a() {
10289 «// a();
10290 /// b();
10291 //! c();ˇ»
10292 }
10293 "});
10294
10295 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10296
10297 cx.assert_editor_state(indoc! {"
10298 fn a() {
10299 «a();
10300 b();
10301 c();ˇ»
10302 }
10303 "});
10304}
10305
10306#[gpui::test]
10307async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
10308 init_test(cx, |_| {});
10309 let mut cx = EditorTestContext::new(cx).await;
10310 let language = Arc::new(Language::new(
10311 LanguageConfig {
10312 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
10313 ..Default::default()
10314 },
10315 Some(tree_sitter_rust::LANGUAGE.into()),
10316 ));
10317 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
10318
10319 let toggle_comments = &ToggleComments {
10320 advance_downwards: false,
10321 ignore_indent: true,
10322 };
10323
10324 // If multiple selections intersect a line, the line is only toggled once.
10325 cx.set_state(indoc! {"
10326 fn a() {
10327 // «b();
10328 // c();
10329 // ˇ» d();
10330 }
10331 "});
10332
10333 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10334
10335 cx.assert_editor_state(indoc! {"
10336 fn a() {
10337 «b();
10338 c();
10339 ˇ» d();
10340 }
10341 "});
10342
10343 // The comment prefix is inserted at the beginning of each line
10344 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10345
10346 cx.assert_editor_state(indoc! {"
10347 fn a() {
10348 // «b();
10349 // c();
10350 // ˇ» d();
10351 }
10352 "});
10353
10354 // If a selection ends at the beginning of a line, that line is not toggled.
10355 cx.set_selections_state(indoc! {"
10356 fn a() {
10357 // b();
10358 // «c();
10359 ˇ»// d();
10360 }
10361 "});
10362
10363 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10364
10365 cx.assert_editor_state(indoc! {"
10366 fn a() {
10367 // b();
10368 «c();
10369 ˇ»// d();
10370 }
10371 "});
10372
10373 // If a selection span a single line and is empty, the line is toggled.
10374 cx.set_state(indoc! {"
10375 fn a() {
10376 a();
10377 b();
10378 ˇ
10379 }
10380 "});
10381
10382 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10383
10384 cx.assert_editor_state(indoc! {"
10385 fn a() {
10386 a();
10387 b();
10388 //ˇ
10389 }
10390 "});
10391
10392 // If a selection span multiple lines, empty lines are not toggled.
10393 cx.set_state(indoc! {"
10394 fn a() {
10395 «a();
10396
10397 c();ˇ»
10398 }
10399 "});
10400
10401 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10402
10403 cx.assert_editor_state(indoc! {"
10404 fn a() {
10405 // «a();
10406
10407 // c();ˇ»
10408 }
10409 "});
10410
10411 // If a selection includes multiple comment prefixes, all lines are uncommented.
10412 cx.set_state(indoc! {"
10413 fn a() {
10414 // «a();
10415 /// b();
10416 //! c();ˇ»
10417 }
10418 "});
10419
10420 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10421
10422 cx.assert_editor_state(indoc! {"
10423 fn a() {
10424 «a();
10425 b();
10426 c();ˇ»
10427 }
10428 "});
10429}
10430
10431#[gpui::test]
10432async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
10433 init_test(cx, |_| {});
10434
10435 let language = Arc::new(Language::new(
10436 LanguageConfig {
10437 line_comments: vec!["// ".into()],
10438 ..Default::default()
10439 },
10440 Some(tree_sitter_rust::LANGUAGE.into()),
10441 ));
10442
10443 let mut cx = EditorTestContext::new(cx).await;
10444
10445 cx.language_registry().add(language.clone());
10446 cx.update_buffer(|buffer, cx| {
10447 buffer.set_language(Some(language), cx);
10448 });
10449
10450 let toggle_comments = &ToggleComments {
10451 advance_downwards: true,
10452 ignore_indent: false,
10453 };
10454
10455 // Single cursor on one line -> advance
10456 // Cursor moves horizontally 3 characters as well on non-blank line
10457 cx.set_state(indoc!(
10458 "fn a() {
10459 ˇdog();
10460 cat();
10461 }"
10462 ));
10463 cx.update_editor(|editor, window, cx| {
10464 editor.toggle_comments(toggle_comments, window, cx);
10465 });
10466 cx.assert_editor_state(indoc!(
10467 "fn a() {
10468 // dog();
10469 catˇ();
10470 }"
10471 ));
10472
10473 // Single selection on one line -> don't advance
10474 cx.set_state(indoc!(
10475 "fn a() {
10476 «dog()ˇ»;
10477 cat();
10478 }"
10479 ));
10480 cx.update_editor(|editor, window, cx| {
10481 editor.toggle_comments(toggle_comments, window, cx);
10482 });
10483 cx.assert_editor_state(indoc!(
10484 "fn a() {
10485 // «dog()ˇ»;
10486 cat();
10487 }"
10488 ));
10489
10490 // Multiple cursors on one line -> advance
10491 cx.set_state(indoc!(
10492 "fn a() {
10493 ˇdˇog();
10494 cat();
10495 }"
10496 ));
10497 cx.update_editor(|editor, window, cx| {
10498 editor.toggle_comments(toggle_comments, window, cx);
10499 });
10500 cx.assert_editor_state(indoc!(
10501 "fn a() {
10502 // dog();
10503 catˇ(ˇ);
10504 }"
10505 ));
10506
10507 // Multiple cursors on one line, with selection -> don't advance
10508 cx.set_state(indoc!(
10509 "fn a() {
10510 ˇdˇog«()ˇ»;
10511 cat();
10512 }"
10513 ));
10514 cx.update_editor(|editor, window, cx| {
10515 editor.toggle_comments(toggle_comments, window, cx);
10516 });
10517 cx.assert_editor_state(indoc!(
10518 "fn a() {
10519 // ˇdˇog«()ˇ»;
10520 cat();
10521 }"
10522 ));
10523
10524 // Single cursor on one line -> advance
10525 // Cursor moves to column 0 on blank line
10526 cx.set_state(indoc!(
10527 "fn a() {
10528 ˇdog();
10529
10530 cat();
10531 }"
10532 ));
10533 cx.update_editor(|editor, window, cx| {
10534 editor.toggle_comments(toggle_comments, window, cx);
10535 });
10536 cx.assert_editor_state(indoc!(
10537 "fn a() {
10538 // dog();
10539 ˇ
10540 cat();
10541 }"
10542 ));
10543
10544 // Single cursor on one line -> advance
10545 // Cursor starts and ends at column 0
10546 cx.set_state(indoc!(
10547 "fn a() {
10548 ˇ dog();
10549 cat();
10550 }"
10551 ));
10552 cx.update_editor(|editor, window, cx| {
10553 editor.toggle_comments(toggle_comments, window, cx);
10554 });
10555 cx.assert_editor_state(indoc!(
10556 "fn a() {
10557 // dog();
10558 ˇ cat();
10559 }"
10560 ));
10561}
10562
10563#[gpui::test]
10564async fn test_toggle_block_comment(cx: &mut TestAppContext) {
10565 init_test(cx, |_| {});
10566
10567 let mut cx = EditorTestContext::new(cx).await;
10568
10569 let html_language = Arc::new(
10570 Language::new(
10571 LanguageConfig {
10572 name: "HTML".into(),
10573 block_comment: Some(("<!-- ".into(), " -->".into())),
10574 ..Default::default()
10575 },
10576 Some(tree_sitter_html::LANGUAGE.into()),
10577 )
10578 .with_injection_query(
10579 r#"
10580 (script_element
10581 (raw_text) @injection.content
10582 (#set! injection.language "javascript"))
10583 "#,
10584 )
10585 .unwrap(),
10586 );
10587
10588 let javascript_language = Arc::new(Language::new(
10589 LanguageConfig {
10590 name: "JavaScript".into(),
10591 line_comments: vec!["// ".into()],
10592 ..Default::default()
10593 },
10594 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10595 ));
10596
10597 cx.language_registry().add(html_language.clone());
10598 cx.language_registry().add(javascript_language.clone());
10599 cx.update_buffer(|buffer, cx| {
10600 buffer.set_language(Some(html_language), cx);
10601 });
10602
10603 // Toggle comments for empty selections
10604 cx.set_state(
10605 &r#"
10606 <p>A</p>ˇ
10607 <p>B</p>ˇ
10608 <p>C</p>ˇ
10609 "#
10610 .unindent(),
10611 );
10612 cx.update_editor(|editor, window, cx| {
10613 editor.toggle_comments(&ToggleComments::default(), window, cx)
10614 });
10615 cx.assert_editor_state(
10616 &r#"
10617 <!-- <p>A</p>ˇ -->
10618 <!-- <p>B</p>ˇ -->
10619 <!-- <p>C</p>ˇ -->
10620 "#
10621 .unindent(),
10622 );
10623 cx.update_editor(|editor, window, cx| {
10624 editor.toggle_comments(&ToggleComments::default(), window, cx)
10625 });
10626 cx.assert_editor_state(
10627 &r#"
10628 <p>A</p>ˇ
10629 <p>B</p>ˇ
10630 <p>C</p>ˇ
10631 "#
10632 .unindent(),
10633 );
10634
10635 // Toggle comments for mixture of empty and non-empty selections, where
10636 // multiple selections occupy a given line.
10637 cx.set_state(
10638 &r#"
10639 <p>A«</p>
10640 <p>ˇ»B</p>ˇ
10641 <p>C«</p>
10642 <p>ˇ»D</p>ˇ
10643 "#
10644 .unindent(),
10645 );
10646
10647 cx.update_editor(|editor, window, cx| {
10648 editor.toggle_comments(&ToggleComments::default(), window, cx)
10649 });
10650 cx.assert_editor_state(
10651 &r#"
10652 <!-- <p>A«</p>
10653 <p>ˇ»B</p>ˇ -->
10654 <!-- <p>C«</p>
10655 <p>ˇ»D</p>ˇ -->
10656 "#
10657 .unindent(),
10658 );
10659 cx.update_editor(|editor, window, cx| {
10660 editor.toggle_comments(&ToggleComments::default(), window, cx)
10661 });
10662 cx.assert_editor_state(
10663 &r#"
10664 <p>A«</p>
10665 <p>ˇ»B</p>ˇ
10666 <p>C«</p>
10667 <p>ˇ»D</p>ˇ
10668 "#
10669 .unindent(),
10670 );
10671
10672 // Toggle comments when different languages are active for different
10673 // selections.
10674 cx.set_state(
10675 &r#"
10676 ˇ<script>
10677 ˇvar x = new Y();
10678 ˇ</script>
10679 "#
10680 .unindent(),
10681 );
10682 cx.executor().run_until_parked();
10683 cx.update_editor(|editor, window, cx| {
10684 editor.toggle_comments(&ToggleComments::default(), window, cx)
10685 });
10686 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
10687 // Uncommenting and commenting from this position brings in even more wrong artifacts.
10688 cx.assert_editor_state(
10689 &r#"
10690 <!-- ˇ<script> -->
10691 // ˇvar x = new Y();
10692 <!-- ˇ</script> -->
10693 "#
10694 .unindent(),
10695 );
10696}
10697
10698#[gpui::test]
10699fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
10700 init_test(cx, |_| {});
10701
10702 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
10703 let multibuffer = cx.new(|cx| {
10704 let mut multibuffer = MultiBuffer::new(ReadWrite);
10705 multibuffer.push_excerpts(
10706 buffer.clone(),
10707 [
10708 ExcerptRange {
10709 context: Point::new(0, 0)..Point::new(0, 4),
10710 primary: None,
10711 },
10712 ExcerptRange {
10713 context: Point::new(1, 0)..Point::new(1, 4),
10714 primary: None,
10715 },
10716 ],
10717 cx,
10718 );
10719 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
10720 multibuffer
10721 });
10722
10723 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
10724 editor.update_in(cx, |editor, window, cx| {
10725 assert_eq!(editor.text(cx), "aaaa\nbbbb");
10726 editor.change_selections(None, window, cx, |s| {
10727 s.select_ranges([
10728 Point::new(0, 0)..Point::new(0, 0),
10729 Point::new(1, 0)..Point::new(1, 0),
10730 ])
10731 });
10732
10733 editor.handle_input("X", window, cx);
10734 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
10735 assert_eq!(
10736 editor.selections.ranges(cx),
10737 [
10738 Point::new(0, 1)..Point::new(0, 1),
10739 Point::new(1, 1)..Point::new(1, 1),
10740 ]
10741 );
10742
10743 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
10744 editor.change_selections(None, window, cx, |s| {
10745 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
10746 });
10747 editor.backspace(&Default::default(), window, cx);
10748 assert_eq!(editor.text(cx), "Xa\nbbb");
10749 assert_eq!(
10750 editor.selections.ranges(cx),
10751 [Point::new(1, 0)..Point::new(1, 0)]
10752 );
10753
10754 editor.change_selections(None, window, cx, |s| {
10755 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
10756 });
10757 editor.backspace(&Default::default(), window, cx);
10758 assert_eq!(editor.text(cx), "X\nbb");
10759 assert_eq!(
10760 editor.selections.ranges(cx),
10761 [Point::new(0, 1)..Point::new(0, 1)]
10762 );
10763 });
10764}
10765
10766#[gpui::test]
10767fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
10768 init_test(cx, |_| {});
10769
10770 let markers = vec![('[', ']').into(), ('(', ')').into()];
10771 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
10772 indoc! {"
10773 [aaaa
10774 (bbbb]
10775 cccc)",
10776 },
10777 markers.clone(),
10778 );
10779 let excerpt_ranges = markers.into_iter().map(|marker| {
10780 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
10781 ExcerptRange {
10782 context,
10783 primary: None,
10784 }
10785 });
10786 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
10787 let multibuffer = cx.new(|cx| {
10788 let mut multibuffer = MultiBuffer::new(ReadWrite);
10789 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
10790 multibuffer
10791 });
10792
10793 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
10794 editor.update_in(cx, |editor, window, cx| {
10795 let (expected_text, selection_ranges) = marked_text_ranges(
10796 indoc! {"
10797 aaaa
10798 bˇbbb
10799 bˇbbˇb
10800 cccc"
10801 },
10802 true,
10803 );
10804 assert_eq!(editor.text(cx), expected_text);
10805 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
10806
10807 editor.handle_input("X", window, cx);
10808
10809 let (expected_text, expected_selections) = marked_text_ranges(
10810 indoc! {"
10811 aaaa
10812 bXˇbbXb
10813 bXˇbbXˇb
10814 cccc"
10815 },
10816 false,
10817 );
10818 assert_eq!(editor.text(cx), expected_text);
10819 assert_eq!(editor.selections.ranges(cx), expected_selections);
10820
10821 editor.newline(&Newline, window, cx);
10822 let (expected_text, expected_selections) = marked_text_ranges(
10823 indoc! {"
10824 aaaa
10825 bX
10826 ˇbbX
10827 b
10828 bX
10829 ˇbbX
10830 ˇb
10831 cccc"
10832 },
10833 false,
10834 );
10835 assert_eq!(editor.text(cx), expected_text);
10836 assert_eq!(editor.selections.ranges(cx), expected_selections);
10837 });
10838}
10839
10840#[gpui::test]
10841fn test_refresh_selections(cx: &mut TestAppContext) {
10842 init_test(cx, |_| {});
10843
10844 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
10845 let mut excerpt1_id = None;
10846 let multibuffer = cx.new(|cx| {
10847 let mut multibuffer = MultiBuffer::new(ReadWrite);
10848 excerpt1_id = multibuffer
10849 .push_excerpts(
10850 buffer.clone(),
10851 [
10852 ExcerptRange {
10853 context: Point::new(0, 0)..Point::new(1, 4),
10854 primary: None,
10855 },
10856 ExcerptRange {
10857 context: Point::new(1, 0)..Point::new(2, 4),
10858 primary: None,
10859 },
10860 ],
10861 cx,
10862 )
10863 .into_iter()
10864 .next();
10865 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
10866 multibuffer
10867 });
10868
10869 let editor = cx.add_window(|window, cx| {
10870 let mut editor = build_editor(multibuffer.clone(), window, cx);
10871 let snapshot = editor.snapshot(window, cx);
10872 editor.change_selections(None, window, cx, |s| {
10873 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
10874 });
10875 editor.begin_selection(
10876 Point::new(2, 1).to_display_point(&snapshot),
10877 true,
10878 1,
10879 window,
10880 cx,
10881 );
10882 assert_eq!(
10883 editor.selections.ranges(cx),
10884 [
10885 Point::new(1, 3)..Point::new(1, 3),
10886 Point::new(2, 1)..Point::new(2, 1),
10887 ]
10888 );
10889 editor
10890 });
10891
10892 // Refreshing selections is a no-op when excerpts haven't changed.
10893 _ = editor.update(cx, |editor, window, cx| {
10894 editor.change_selections(None, window, cx, |s| s.refresh());
10895 assert_eq!(
10896 editor.selections.ranges(cx),
10897 [
10898 Point::new(1, 3)..Point::new(1, 3),
10899 Point::new(2, 1)..Point::new(2, 1),
10900 ]
10901 );
10902 });
10903
10904 multibuffer.update(cx, |multibuffer, cx| {
10905 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
10906 });
10907 _ = editor.update(cx, |editor, window, cx| {
10908 // Removing an excerpt causes the first selection to become degenerate.
10909 assert_eq!(
10910 editor.selections.ranges(cx),
10911 [
10912 Point::new(0, 0)..Point::new(0, 0),
10913 Point::new(0, 1)..Point::new(0, 1)
10914 ]
10915 );
10916
10917 // Refreshing selections will relocate the first selection to the original buffer
10918 // location.
10919 editor.change_selections(None, window, cx, |s| s.refresh());
10920 assert_eq!(
10921 editor.selections.ranges(cx),
10922 [
10923 Point::new(0, 1)..Point::new(0, 1),
10924 Point::new(0, 3)..Point::new(0, 3)
10925 ]
10926 );
10927 assert!(editor.selections.pending_anchor().is_some());
10928 });
10929}
10930
10931#[gpui::test]
10932fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
10933 init_test(cx, |_| {});
10934
10935 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
10936 let mut excerpt1_id = None;
10937 let multibuffer = cx.new(|cx| {
10938 let mut multibuffer = MultiBuffer::new(ReadWrite);
10939 excerpt1_id = multibuffer
10940 .push_excerpts(
10941 buffer.clone(),
10942 [
10943 ExcerptRange {
10944 context: Point::new(0, 0)..Point::new(1, 4),
10945 primary: None,
10946 },
10947 ExcerptRange {
10948 context: Point::new(1, 0)..Point::new(2, 4),
10949 primary: None,
10950 },
10951 ],
10952 cx,
10953 )
10954 .into_iter()
10955 .next();
10956 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
10957 multibuffer
10958 });
10959
10960 let editor = cx.add_window(|window, cx| {
10961 let mut editor = build_editor(multibuffer.clone(), window, cx);
10962 let snapshot = editor.snapshot(window, cx);
10963 editor.begin_selection(
10964 Point::new(1, 3).to_display_point(&snapshot),
10965 false,
10966 1,
10967 window,
10968 cx,
10969 );
10970 assert_eq!(
10971 editor.selections.ranges(cx),
10972 [Point::new(1, 3)..Point::new(1, 3)]
10973 );
10974 editor
10975 });
10976
10977 multibuffer.update(cx, |multibuffer, cx| {
10978 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
10979 });
10980 _ = editor.update(cx, |editor, window, cx| {
10981 assert_eq!(
10982 editor.selections.ranges(cx),
10983 [Point::new(0, 0)..Point::new(0, 0)]
10984 );
10985
10986 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
10987 editor.change_selections(None, window, cx, |s| s.refresh());
10988 assert_eq!(
10989 editor.selections.ranges(cx),
10990 [Point::new(0, 3)..Point::new(0, 3)]
10991 );
10992 assert!(editor.selections.pending_anchor().is_some());
10993 });
10994}
10995
10996#[gpui::test]
10997async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
10998 init_test(cx, |_| {});
10999
11000 let language = Arc::new(
11001 Language::new(
11002 LanguageConfig {
11003 brackets: BracketPairConfig {
11004 pairs: vec![
11005 BracketPair {
11006 start: "{".to_string(),
11007 end: "}".to_string(),
11008 close: true,
11009 surround: true,
11010 newline: true,
11011 },
11012 BracketPair {
11013 start: "/* ".to_string(),
11014 end: " */".to_string(),
11015 close: true,
11016 surround: true,
11017 newline: true,
11018 },
11019 ],
11020 ..Default::default()
11021 },
11022 ..Default::default()
11023 },
11024 Some(tree_sitter_rust::LANGUAGE.into()),
11025 )
11026 .with_indents_query("")
11027 .unwrap(),
11028 );
11029
11030 let text = concat!(
11031 "{ }\n", //
11032 " x\n", //
11033 " /* */\n", //
11034 "x\n", //
11035 "{{} }\n", //
11036 );
11037
11038 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
11039 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11040 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11041 editor
11042 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11043 .await;
11044
11045 editor.update_in(cx, |editor, window, cx| {
11046 editor.change_selections(None, window, cx, |s| {
11047 s.select_display_ranges([
11048 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
11049 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
11050 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
11051 ])
11052 });
11053 editor.newline(&Newline, window, cx);
11054
11055 assert_eq!(
11056 editor.buffer().read(cx).read(cx).text(),
11057 concat!(
11058 "{ \n", // Suppress rustfmt
11059 "\n", //
11060 "}\n", //
11061 " x\n", //
11062 " /* \n", //
11063 " \n", //
11064 " */\n", //
11065 "x\n", //
11066 "{{} \n", //
11067 "}\n", //
11068 )
11069 );
11070 });
11071}
11072
11073#[gpui::test]
11074fn test_highlighted_ranges(cx: &mut TestAppContext) {
11075 init_test(cx, |_| {});
11076
11077 let editor = cx.add_window(|window, cx| {
11078 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
11079 build_editor(buffer.clone(), window, cx)
11080 });
11081
11082 _ = editor.update(cx, |editor, window, cx| {
11083 struct Type1;
11084 struct Type2;
11085
11086 let buffer = editor.buffer.read(cx).snapshot(cx);
11087
11088 let anchor_range =
11089 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
11090
11091 editor.highlight_background::<Type1>(
11092 &[
11093 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
11094 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
11095 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
11096 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
11097 ],
11098 |_| Hsla::red(),
11099 cx,
11100 );
11101 editor.highlight_background::<Type2>(
11102 &[
11103 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
11104 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
11105 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
11106 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
11107 ],
11108 |_| Hsla::green(),
11109 cx,
11110 );
11111
11112 let snapshot = editor.snapshot(window, cx);
11113 let mut highlighted_ranges = editor.background_highlights_in_range(
11114 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
11115 &snapshot,
11116 cx.theme().colors(),
11117 );
11118 // Enforce a consistent ordering based on color without relying on the ordering of the
11119 // highlight's `TypeId` which is non-executor.
11120 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
11121 assert_eq!(
11122 highlighted_ranges,
11123 &[
11124 (
11125 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
11126 Hsla::red(),
11127 ),
11128 (
11129 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
11130 Hsla::red(),
11131 ),
11132 (
11133 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
11134 Hsla::green(),
11135 ),
11136 (
11137 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
11138 Hsla::green(),
11139 ),
11140 ]
11141 );
11142 assert_eq!(
11143 editor.background_highlights_in_range(
11144 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
11145 &snapshot,
11146 cx.theme().colors(),
11147 ),
11148 &[(
11149 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
11150 Hsla::red(),
11151 )]
11152 );
11153 });
11154}
11155
11156#[gpui::test]
11157async fn test_following(cx: &mut TestAppContext) {
11158 init_test(cx, |_| {});
11159
11160 let fs = FakeFs::new(cx.executor());
11161 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
11162
11163 let buffer = project.update(cx, |project, cx| {
11164 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
11165 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
11166 });
11167 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
11168 let follower = cx.update(|cx| {
11169 cx.open_window(
11170 WindowOptions {
11171 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
11172 gpui::Point::new(px(0.), px(0.)),
11173 gpui::Point::new(px(10.), px(80.)),
11174 ))),
11175 ..Default::default()
11176 },
11177 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
11178 )
11179 .unwrap()
11180 });
11181
11182 let is_still_following = Rc::new(RefCell::new(true));
11183 let follower_edit_event_count = Rc::new(RefCell::new(0));
11184 let pending_update = Rc::new(RefCell::new(None));
11185 let leader_entity = leader.root(cx).unwrap();
11186 let follower_entity = follower.root(cx).unwrap();
11187 _ = follower.update(cx, {
11188 let update = pending_update.clone();
11189 let is_still_following = is_still_following.clone();
11190 let follower_edit_event_count = follower_edit_event_count.clone();
11191 |_, window, cx| {
11192 cx.subscribe_in(
11193 &leader_entity,
11194 window,
11195 move |_, leader, event, window, cx| {
11196 leader.read(cx).add_event_to_update_proto(
11197 event,
11198 &mut update.borrow_mut(),
11199 window,
11200 cx,
11201 );
11202 },
11203 )
11204 .detach();
11205
11206 cx.subscribe_in(
11207 &follower_entity,
11208 window,
11209 move |_, _, event: &EditorEvent, _window, _cx| {
11210 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
11211 *is_still_following.borrow_mut() = false;
11212 }
11213
11214 if let EditorEvent::BufferEdited = event {
11215 *follower_edit_event_count.borrow_mut() += 1;
11216 }
11217 },
11218 )
11219 .detach();
11220 }
11221 });
11222
11223 // Update the selections only
11224 _ = leader.update(cx, |leader, window, cx| {
11225 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
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 _ = follower.update(cx, |follower, _, cx| {
11240 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
11241 });
11242 assert!(*is_still_following.borrow());
11243 assert_eq!(*follower_edit_event_count.borrow(), 0);
11244
11245 // Update the scroll position only
11246 _ = leader.update(cx, |leader, window, cx| {
11247 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
11248 });
11249 follower
11250 .update(cx, |follower, window, cx| {
11251 follower.apply_update_proto(
11252 &project,
11253 pending_update.borrow_mut().take().unwrap(),
11254 window,
11255 cx,
11256 )
11257 })
11258 .unwrap()
11259 .await
11260 .unwrap();
11261 assert_eq!(
11262 follower
11263 .update(cx, |follower, _, cx| follower.scroll_position(cx))
11264 .unwrap(),
11265 gpui::Point::new(1.5, 3.5)
11266 );
11267 assert!(*is_still_following.borrow());
11268 assert_eq!(*follower_edit_event_count.borrow(), 0);
11269
11270 // Update the selections and scroll position. The follower's scroll position is updated
11271 // via autoscroll, not via the leader's exact scroll position.
11272 _ = leader.update(cx, |leader, window, cx| {
11273 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
11274 leader.request_autoscroll(Autoscroll::newest(), cx);
11275 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
11276 });
11277 follower
11278 .update(cx, |follower, window, cx| {
11279 follower.apply_update_proto(
11280 &project,
11281 pending_update.borrow_mut().take().unwrap(),
11282 window,
11283 cx,
11284 )
11285 })
11286 .unwrap()
11287 .await
11288 .unwrap();
11289 _ = follower.update(cx, |follower, _, cx| {
11290 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
11291 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
11292 });
11293 assert!(*is_still_following.borrow());
11294
11295 // Creating a pending selection that precedes another selection
11296 _ = leader.update(cx, |leader, window, cx| {
11297 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
11298 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
11299 });
11300 follower
11301 .update(cx, |follower, window, cx| {
11302 follower.apply_update_proto(
11303 &project,
11304 pending_update.borrow_mut().take().unwrap(),
11305 window,
11306 cx,
11307 )
11308 })
11309 .unwrap()
11310 .await
11311 .unwrap();
11312 _ = follower.update(cx, |follower, _, cx| {
11313 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
11314 });
11315 assert!(*is_still_following.borrow());
11316
11317 // Extend the pending selection so that it surrounds another selection
11318 _ = leader.update(cx, |leader, window, cx| {
11319 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
11320 });
11321 follower
11322 .update(cx, |follower, window, cx| {
11323 follower.apply_update_proto(
11324 &project,
11325 pending_update.borrow_mut().take().unwrap(),
11326 window,
11327 cx,
11328 )
11329 })
11330 .unwrap()
11331 .await
11332 .unwrap();
11333 _ = follower.update(cx, |follower, _, cx| {
11334 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
11335 });
11336
11337 // Scrolling locally breaks the follow
11338 _ = follower.update(cx, |follower, window, cx| {
11339 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
11340 follower.set_scroll_anchor(
11341 ScrollAnchor {
11342 anchor: top_anchor,
11343 offset: gpui::Point::new(0.0, 0.5),
11344 },
11345 window,
11346 cx,
11347 );
11348 });
11349 assert!(!(*is_still_following.borrow()));
11350}
11351
11352#[gpui::test]
11353async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
11354 init_test(cx, |_| {});
11355
11356 let fs = FakeFs::new(cx.executor());
11357 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
11358 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11359 let pane = workspace
11360 .update(cx, |workspace, _, _| workspace.active_pane().clone())
11361 .unwrap();
11362
11363 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11364
11365 let leader = pane.update_in(cx, |_, window, cx| {
11366 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
11367 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
11368 });
11369
11370 // Start following the editor when it has no excerpts.
11371 let mut state_message =
11372 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
11373 let workspace_entity = workspace.root(cx).unwrap();
11374 let follower_1 = cx
11375 .update_window(*workspace.deref(), |_, window, cx| {
11376 Editor::from_state_proto(
11377 workspace_entity,
11378 ViewId {
11379 creator: Default::default(),
11380 id: 0,
11381 },
11382 &mut state_message,
11383 window,
11384 cx,
11385 )
11386 })
11387 .unwrap()
11388 .unwrap()
11389 .await
11390 .unwrap();
11391
11392 let update_message = Rc::new(RefCell::new(None));
11393 follower_1.update_in(cx, {
11394 let update = update_message.clone();
11395 |_, window, cx| {
11396 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
11397 leader.read(cx).add_event_to_update_proto(
11398 event,
11399 &mut update.borrow_mut(),
11400 window,
11401 cx,
11402 );
11403 })
11404 .detach();
11405 }
11406 });
11407
11408 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
11409 (
11410 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
11411 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
11412 )
11413 });
11414
11415 // Insert some excerpts.
11416 leader.update(cx, |leader, cx| {
11417 leader.buffer.update(cx, |multibuffer, cx| {
11418 let excerpt_ids = multibuffer.push_excerpts(
11419 buffer_1.clone(),
11420 [
11421 ExcerptRange {
11422 context: 1..6,
11423 primary: None,
11424 },
11425 ExcerptRange {
11426 context: 12..15,
11427 primary: None,
11428 },
11429 ExcerptRange {
11430 context: 0..3,
11431 primary: None,
11432 },
11433 ],
11434 cx,
11435 );
11436 multibuffer.insert_excerpts_after(
11437 excerpt_ids[0],
11438 buffer_2.clone(),
11439 [
11440 ExcerptRange {
11441 context: 8..12,
11442 primary: None,
11443 },
11444 ExcerptRange {
11445 context: 0..6,
11446 primary: None,
11447 },
11448 ],
11449 cx,
11450 );
11451 });
11452 });
11453
11454 // Apply the update of adding the excerpts.
11455 follower_1
11456 .update_in(cx, |follower, window, cx| {
11457 follower.apply_update_proto(
11458 &project,
11459 update_message.borrow().clone().unwrap(),
11460 window,
11461 cx,
11462 )
11463 })
11464 .await
11465 .unwrap();
11466 assert_eq!(
11467 follower_1.update(cx, |editor, cx| editor.text(cx)),
11468 leader.update(cx, |editor, cx| editor.text(cx))
11469 );
11470 update_message.borrow_mut().take();
11471
11472 // Start following separately after it already has excerpts.
11473 let mut state_message =
11474 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
11475 let workspace_entity = workspace.root(cx).unwrap();
11476 let follower_2 = cx
11477 .update_window(*workspace.deref(), |_, window, cx| {
11478 Editor::from_state_proto(
11479 workspace_entity,
11480 ViewId {
11481 creator: Default::default(),
11482 id: 0,
11483 },
11484 &mut state_message,
11485 window,
11486 cx,
11487 )
11488 })
11489 .unwrap()
11490 .unwrap()
11491 .await
11492 .unwrap();
11493 assert_eq!(
11494 follower_2.update(cx, |editor, cx| editor.text(cx)),
11495 leader.update(cx, |editor, cx| editor.text(cx))
11496 );
11497
11498 // Remove some excerpts.
11499 leader.update(cx, |leader, cx| {
11500 leader.buffer.update(cx, |multibuffer, cx| {
11501 let excerpt_ids = multibuffer.excerpt_ids();
11502 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
11503 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
11504 });
11505 });
11506
11507 // Apply the update of removing the excerpts.
11508 follower_1
11509 .update_in(cx, |follower, window, cx| {
11510 follower.apply_update_proto(
11511 &project,
11512 update_message.borrow().clone().unwrap(),
11513 window,
11514 cx,
11515 )
11516 })
11517 .await
11518 .unwrap();
11519 follower_2
11520 .update_in(cx, |follower, window, cx| {
11521 follower.apply_update_proto(
11522 &project,
11523 update_message.borrow().clone().unwrap(),
11524 window,
11525 cx,
11526 )
11527 })
11528 .await
11529 .unwrap();
11530 update_message.borrow_mut().take();
11531 assert_eq!(
11532 follower_1.update(cx, |editor, cx| editor.text(cx)),
11533 leader.update(cx, |editor, cx| editor.text(cx))
11534 );
11535}
11536
11537#[gpui::test]
11538async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
11539 init_test(cx, |_| {});
11540
11541 let mut cx = EditorTestContext::new(cx).await;
11542 let lsp_store =
11543 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
11544
11545 cx.set_state(indoc! {"
11546 ˇfn func(abc def: i32) -> u32 {
11547 }
11548 "});
11549
11550 cx.update(|_, cx| {
11551 lsp_store.update(cx, |lsp_store, cx| {
11552 lsp_store
11553 .update_diagnostics(
11554 LanguageServerId(0),
11555 lsp::PublishDiagnosticsParams {
11556 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11557 version: None,
11558 diagnostics: vec![
11559 lsp::Diagnostic {
11560 range: lsp::Range::new(
11561 lsp::Position::new(0, 11),
11562 lsp::Position::new(0, 12),
11563 ),
11564 severity: Some(lsp::DiagnosticSeverity::ERROR),
11565 ..Default::default()
11566 },
11567 lsp::Diagnostic {
11568 range: lsp::Range::new(
11569 lsp::Position::new(0, 12),
11570 lsp::Position::new(0, 15),
11571 ),
11572 severity: Some(lsp::DiagnosticSeverity::ERROR),
11573 ..Default::default()
11574 },
11575 lsp::Diagnostic {
11576 range: lsp::Range::new(
11577 lsp::Position::new(0, 25),
11578 lsp::Position::new(0, 28),
11579 ),
11580 severity: Some(lsp::DiagnosticSeverity::ERROR),
11581 ..Default::default()
11582 },
11583 ],
11584 },
11585 &[],
11586 cx,
11587 )
11588 .unwrap()
11589 });
11590 });
11591
11592 executor.run_until_parked();
11593
11594 cx.update_editor(|editor, window, cx| {
11595 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11596 });
11597
11598 cx.assert_editor_state(indoc! {"
11599 fn func(abc def: i32) -> ˇu32 {
11600 }
11601 "});
11602
11603 cx.update_editor(|editor, window, cx| {
11604 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11605 });
11606
11607 cx.assert_editor_state(indoc! {"
11608 fn func(abc ˇdef: i32) -> u32 {
11609 }
11610 "});
11611
11612 cx.update_editor(|editor, window, cx| {
11613 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11614 });
11615
11616 cx.assert_editor_state(indoc! {"
11617 fn func(abcˇ def: i32) -> u32 {
11618 }
11619 "});
11620
11621 cx.update_editor(|editor, window, cx| {
11622 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11623 });
11624
11625 cx.assert_editor_state(indoc! {"
11626 fn func(abc def: i32) -> ˇu32 {
11627 }
11628 "});
11629}
11630
11631#[gpui::test]
11632async fn cycle_through_same_place_diagnostics(
11633 executor: BackgroundExecutor,
11634 cx: &mut TestAppContext,
11635) {
11636 init_test(cx, |_| {});
11637
11638 let mut cx = EditorTestContext::new(cx).await;
11639 let lsp_store =
11640 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
11641
11642 cx.set_state(indoc! {"
11643 ˇfn func(abc def: i32) -> u32 {
11644 }
11645 "});
11646
11647 cx.update(|_, cx| {
11648 lsp_store.update(cx, |lsp_store, cx| {
11649 lsp_store
11650 .update_diagnostics(
11651 LanguageServerId(0),
11652 lsp::PublishDiagnosticsParams {
11653 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11654 version: None,
11655 diagnostics: vec![
11656 lsp::Diagnostic {
11657 range: lsp::Range::new(
11658 lsp::Position::new(0, 11),
11659 lsp::Position::new(0, 12),
11660 ),
11661 severity: Some(lsp::DiagnosticSeverity::ERROR),
11662 ..Default::default()
11663 },
11664 lsp::Diagnostic {
11665 range: lsp::Range::new(
11666 lsp::Position::new(0, 12),
11667 lsp::Position::new(0, 15),
11668 ),
11669 severity: Some(lsp::DiagnosticSeverity::ERROR),
11670 ..Default::default()
11671 },
11672 lsp::Diagnostic {
11673 range: lsp::Range::new(
11674 lsp::Position::new(0, 12),
11675 lsp::Position::new(0, 15),
11676 ),
11677 severity: Some(lsp::DiagnosticSeverity::ERROR),
11678 ..Default::default()
11679 },
11680 lsp::Diagnostic {
11681 range: lsp::Range::new(
11682 lsp::Position::new(0, 25),
11683 lsp::Position::new(0, 28),
11684 ),
11685 severity: Some(lsp::DiagnosticSeverity::ERROR),
11686 ..Default::default()
11687 },
11688 ],
11689 },
11690 &[],
11691 cx,
11692 )
11693 .unwrap()
11694 });
11695 });
11696 executor.run_until_parked();
11697
11698 //// Backward
11699
11700 // Fourth diagnostic
11701 cx.update_editor(|editor, window, cx| {
11702 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11703 });
11704 cx.assert_editor_state(indoc! {"
11705 fn func(abc def: i32) -> ˇu32 {
11706 }
11707 "});
11708
11709 // Third diagnostic
11710 cx.update_editor(|editor, window, cx| {
11711 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11712 });
11713 cx.assert_editor_state(indoc! {"
11714 fn func(abc ˇdef: i32) -> u32 {
11715 }
11716 "});
11717
11718 // Second diagnostic, same place
11719 cx.update_editor(|editor, window, cx| {
11720 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11721 });
11722 cx.assert_editor_state(indoc! {"
11723 fn func(abc ˇdef: i32) -> u32 {
11724 }
11725 "});
11726
11727 // First diagnostic
11728 cx.update_editor(|editor, window, cx| {
11729 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11730 });
11731 cx.assert_editor_state(indoc! {"
11732 fn func(abcˇ def: i32) -> u32 {
11733 }
11734 "});
11735
11736 // Wrapped over, fourth diagnostic
11737 cx.update_editor(|editor, window, cx| {
11738 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11739 });
11740 cx.assert_editor_state(indoc! {"
11741 fn func(abc def: i32) -> ˇu32 {
11742 }
11743 "});
11744
11745 cx.update_editor(|editor, window, cx| {
11746 editor.move_to_beginning(&MoveToBeginning, window, cx);
11747 });
11748 cx.assert_editor_state(indoc! {"
11749 ˇfn func(abc def: i32) -> u32 {
11750 }
11751 "});
11752
11753 //// Forward
11754
11755 // First diagnostic
11756 cx.update_editor(|editor, window, cx| {
11757 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11758 });
11759 cx.assert_editor_state(indoc! {"
11760 fn func(abcˇ def: i32) -> u32 {
11761 }
11762 "});
11763
11764 // Second diagnostic
11765 cx.update_editor(|editor, window, cx| {
11766 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11767 });
11768 cx.assert_editor_state(indoc! {"
11769 fn func(abc ˇdef: i32) -> u32 {
11770 }
11771 "});
11772
11773 // Third diagnostic, same place
11774 cx.update_editor(|editor, window, cx| {
11775 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11776 });
11777 cx.assert_editor_state(indoc! {"
11778 fn func(abc ˇdef: i32) -> u32 {
11779 }
11780 "});
11781
11782 // Fourth diagnostic
11783 cx.update_editor(|editor, window, cx| {
11784 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11785 });
11786 cx.assert_editor_state(indoc! {"
11787 fn func(abc def: i32) -> ˇu32 {
11788 }
11789 "});
11790
11791 // Wrapped around, first diagnostic
11792 cx.update_editor(|editor, window, cx| {
11793 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11794 });
11795 cx.assert_editor_state(indoc! {"
11796 fn func(abcˇ def: i32) -> u32 {
11797 }
11798 "});
11799}
11800
11801#[gpui::test]
11802async fn active_diagnostics_dismiss_after_invalidation(
11803 executor: BackgroundExecutor,
11804 cx: &mut TestAppContext,
11805) {
11806 init_test(cx, |_| {});
11807
11808 let mut cx = EditorTestContext::new(cx).await;
11809 let lsp_store =
11810 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
11811
11812 cx.set_state(indoc! {"
11813 ˇfn func(abc def: i32) -> u32 {
11814 }
11815 "});
11816
11817 let message = "Something's wrong!";
11818 cx.update(|_, cx| {
11819 lsp_store.update(cx, |lsp_store, cx| {
11820 lsp_store
11821 .update_diagnostics(
11822 LanguageServerId(0),
11823 lsp::PublishDiagnosticsParams {
11824 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11825 version: None,
11826 diagnostics: vec![lsp::Diagnostic {
11827 range: lsp::Range::new(
11828 lsp::Position::new(0, 11),
11829 lsp::Position::new(0, 12),
11830 ),
11831 severity: Some(lsp::DiagnosticSeverity::ERROR),
11832 message: message.to_string(),
11833 ..Default::default()
11834 }],
11835 },
11836 &[],
11837 cx,
11838 )
11839 .unwrap()
11840 });
11841 });
11842 executor.run_until_parked();
11843
11844 cx.update_editor(|editor, window, cx| {
11845 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11846 assert_eq!(
11847 editor
11848 .active_diagnostics
11849 .as_ref()
11850 .map(|diagnostics_group| diagnostics_group.primary_message.as_str()),
11851 Some(message),
11852 "Should have a diagnostics group activated"
11853 );
11854 });
11855 cx.assert_editor_state(indoc! {"
11856 fn func(abcˇ def: i32) -> u32 {
11857 }
11858 "});
11859
11860 cx.update(|_, cx| {
11861 lsp_store.update(cx, |lsp_store, cx| {
11862 lsp_store
11863 .update_diagnostics(
11864 LanguageServerId(0),
11865 lsp::PublishDiagnosticsParams {
11866 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11867 version: None,
11868 diagnostics: Vec::new(),
11869 },
11870 &[],
11871 cx,
11872 )
11873 .unwrap()
11874 });
11875 });
11876 executor.run_until_parked();
11877 cx.update_editor(|editor, _, _| {
11878 assert_eq!(
11879 editor.active_diagnostics, None,
11880 "After no diagnostics set to the editor, no diagnostics should be active"
11881 );
11882 });
11883 cx.assert_editor_state(indoc! {"
11884 fn func(abcˇ def: i32) -> u32 {
11885 }
11886 "});
11887
11888 cx.update_editor(|editor, window, cx| {
11889 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11890 assert_eq!(
11891 editor.active_diagnostics, None,
11892 "Should be no diagnostics to go to and activate"
11893 );
11894 });
11895 cx.assert_editor_state(indoc! {"
11896 fn func(abcˇ def: i32) -> u32 {
11897 }
11898 "});
11899}
11900
11901#[gpui::test]
11902async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
11903 init_test(cx, |_| {});
11904
11905 let mut cx = EditorTestContext::new(cx).await;
11906
11907 cx.set_state(indoc! {"
11908 fn func(abˇc def: i32) -> u32 {
11909 }
11910 "});
11911 let lsp_store =
11912 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
11913
11914 cx.update(|_, cx| {
11915 lsp_store.update(cx, |lsp_store, cx| {
11916 lsp_store.update_diagnostics(
11917 LanguageServerId(0),
11918 lsp::PublishDiagnosticsParams {
11919 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11920 version: None,
11921 diagnostics: vec![lsp::Diagnostic {
11922 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 12)),
11923 severity: Some(lsp::DiagnosticSeverity::ERROR),
11924 message: "we've had problems with <https://link.one>, and <https://link.two> is broken".to_string(),
11925 ..Default::default()
11926 }],
11927 },
11928 &[],
11929 cx,
11930 )
11931 })
11932 }).unwrap();
11933 cx.run_until_parked();
11934 cx.update_editor(|editor, window, cx| {
11935 hover_popover::hover(editor, &Default::default(), window, cx)
11936 });
11937 cx.run_until_parked();
11938 cx.update_editor(|editor, _, _| assert!(editor.hover_state.diagnostic_popover.is_some()))
11939}
11940
11941#[gpui::test]
11942async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
11943 init_test(cx, |_| {});
11944
11945 let mut cx = EditorTestContext::new(cx).await;
11946
11947 let diff_base = r#"
11948 use some::mod;
11949
11950 const A: u32 = 42;
11951
11952 fn main() {
11953 println!("hello");
11954
11955 println!("world");
11956 }
11957 "#
11958 .unindent();
11959
11960 // Edits are modified, removed, modified, added
11961 cx.set_state(
11962 &r#"
11963 use some::modified;
11964
11965 ˇ
11966 fn main() {
11967 println!("hello there");
11968
11969 println!("around the");
11970 println!("world");
11971 }
11972 "#
11973 .unindent(),
11974 );
11975
11976 cx.set_head_text(&diff_base);
11977 executor.run_until_parked();
11978
11979 cx.update_editor(|editor, window, cx| {
11980 //Wrap around the bottom of the buffer
11981 for _ in 0..3 {
11982 editor.go_to_next_hunk(&GoToHunk, 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 //Wrap around the top of the buffer
12003 for _ in 0..2 {
12004 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12005 }
12006 });
12007
12008 cx.assert_editor_state(
12009 &r#"
12010 use some::modified;
12011
12012
12013 fn main() {
12014 ˇ println!("hello there");
12015
12016 println!("around the");
12017 println!("world");
12018 }
12019 "#
12020 .unindent(),
12021 );
12022
12023 cx.update_editor(|editor, window, cx| {
12024 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12025 });
12026
12027 cx.assert_editor_state(
12028 &r#"
12029 use some::modified;
12030
12031 ˇ
12032 fn main() {
12033 println!("hello there");
12034
12035 println!("around the");
12036 println!("world");
12037 }
12038 "#
12039 .unindent(),
12040 );
12041
12042 cx.update_editor(|editor, window, cx| {
12043 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12044 });
12045
12046 cx.assert_editor_state(
12047 &r#"
12048 ˇuse some::modified;
12049
12050
12051 fn main() {
12052 println!("hello there");
12053
12054 println!("around the");
12055 println!("world");
12056 }
12057 "#
12058 .unindent(),
12059 );
12060
12061 cx.update_editor(|editor, window, cx| {
12062 for _ in 0..2 {
12063 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12064 }
12065 });
12066
12067 cx.assert_editor_state(
12068 &r#"
12069 use some::modified;
12070
12071
12072 fn main() {
12073 ˇ println!("hello there");
12074
12075 println!("around the");
12076 println!("world");
12077 }
12078 "#
12079 .unindent(),
12080 );
12081
12082 cx.update_editor(|editor, window, cx| {
12083 editor.fold(&Fold, window, cx);
12084 });
12085
12086 cx.update_editor(|editor, window, cx| {
12087 editor.go_to_next_hunk(&GoToHunk, window, cx);
12088 });
12089
12090 cx.assert_editor_state(
12091 &r#"
12092 ˇuse some::modified;
12093
12094
12095 fn main() {
12096 println!("hello there");
12097
12098 println!("around the");
12099 println!("world");
12100 }
12101 "#
12102 .unindent(),
12103 );
12104}
12105
12106#[test]
12107fn test_split_words() {
12108 fn split(text: &str) -> Vec<&str> {
12109 split_words(text).collect()
12110 }
12111
12112 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
12113 assert_eq!(split("hello_world"), &["hello_", "world"]);
12114 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
12115 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
12116 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
12117 assert_eq!(split("helloworld"), &["helloworld"]);
12118
12119 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
12120}
12121
12122#[gpui::test]
12123async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
12124 init_test(cx, |_| {});
12125
12126 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
12127 let mut assert = |before, after| {
12128 let _state_context = cx.set_state(before);
12129 cx.run_until_parked();
12130 cx.update_editor(|editor, window, cx| {
12131 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
12132 });
12133 cx.run_until_parked();
12134 cx.assert_editor_state(after);
12135 };
12136
12137 // Outside bracket jumps to outside of matching bracket
12138 assert("console.logˇ(var);", "console.log(var)ˇ;");
12139 assert("console.log(var)ˇ;", "console.logˇ(var);");
12140
12141 // Inside bracket jumps to inside of matching bracket
12142 assert("console.log(ˇvar);", "console.log(varˇ);");
12143 assert("console.log(varˇ);", "console.log(ˇvar);");
12144
12145 // When outside a bracket and inside, favor jumping to the inside bracket
12146 assert(
12147 "console.log('foo', [1, 2, 3]ˇ);",
12148 "console.log(ˇ'foo', [1, 2, 3]);",
12149 );
12150 assert(
12151 "console.log(ˇ'foo', [1, 2, 3]);",
12152 "console.log('foo', [1, 2, 3]ˇ);",
12153 );
12154
12155 // Bias forward if two options are equally likely
12156 assert(
12157 "let result = curried_fun()ˇ();",
12158 "let result = curried_fun()()ˇ;",
12159 );
12160
12161 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
12162 assert(
12163 indoc! {"
12164 function test() {
12165 console.log('test')ˇ
12166 }"},
12167 indoc! {"
12168 function test() {
12169 console.logˇ('test')
12170 }"},
12171 );
12172}
12173
12174#[gpui::test]
12175async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
12176 init_test(cx, |_| {});
12177
12178 let fs = FakeFs::new(cx.executor());
12179 fs.insert_tree(
12180 path!("/a"),
12181 json!({
12182 "main.rs": "fn main() { let a = 5; }",
12183 "other.rs": "// Test file",
12184 }),
12185 )
12186 .await;
12187 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12188
12189 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12190 language_registry.add(Arc::new(Language::new(
12191 LanguageConfig {
12192 name: "Rust".into(),
12193 matcher: LanguageMatcher {
12194 path_suffixes: vec!["rs".to_string()],
12195 ..Default::default()
12196 },
12197 brackets: BracketPairConfig {
12198 pairs: vec![BracketPair {
12199 start: "{".to_string(),
12200 end: "}".to_string(),
12201 close: true,
12202 surround: true,
12203 newline: true,
12204 }],
12205 disabled_scopes_by_bracket_ix: Vec::new(),
12206 },
12207 ..Default::default()
12208 },
12209 Some(tree_sitter_rust::LANGUAGE.into()),
12210 )));
12211 let mut fake_servers = language_registry.register_fake_lsp(
12212 "Rust",
12213 FakeLspAdapter {
12214 capabilities: lsp::ServerCapabilities {
12215 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
12216 first_trigger_character: "{".to_string(),
12217 more_trigger_character: None,
12218 }),
12219 ..Default::default()
12220 },
12221 ..Default::default()
12222 },
12223 );
12224
12225 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12226
12227 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12228
12229 let worktree_id = workspace
12230 .update(cx, |workspace, _, cx| {
12231 workspace.project().update(cx, |project, cx| {
12232 project.worktrees(cx).next().unwrap().read(cx).id()
12233 })
12234 })
12235 .unwrap();
12236
12237 let buffer = project
12238 .update(cx, |project, cx| {
12239 project.open_local_buffer(path!("/a/main.rs"), cx)
12240 })
12241 .await
12242 .unwrap();
12243 let editor_handle = workspace
12244 .update(cx, |workspace, window, cx| {
12245 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
12246 })
12247 .unwrap()
12248 .await
12249 .unwrap()
12250 .downcast::<Editor>()
12251 .unwrap();
12252
12253 cx.executor().start_waiting();
12254 let fake_server = fake_servers.next().await.unwrap();
12255
12256 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
12257 |params, _| async move {
12258 assert_eq!(
12259 params.text_document_position.text_document.uri,
12260 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
12261 );
12262 assert_eq!(
12263 params.text_document_position.position,
12264 lsp::Position::new(0, 21),
12265 );
12266
12267 Ok(Some(vec![lsp::TextEdit {
12268 new_text: "]".to_string(),
12269 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12270 }]))
12271 },
12272 );
12273
12274 editor_handle.update_in(cx, |editor, window, cx| {
12275 window.focus(&editor.focus_handle(cx));
12276 editor.change_selections(None, window, cx, |s| {
12277 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
12278 });
12279 editor.handle_input("{", window, cx);
12280 });
12281
12282 cx.executor().run_until_parked();
12283
12284 buffer.update(cx, |buffer, _| {
12285 assert_eq!(
12286 buffer.text(),
12287 "fn main() { let a = {5}; }",
12288 "No extra braces from on type formatting should appear in the buffer"
12289 )
12290 });
12291}
12292
12293#[gpui::test]
12294async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
12295 init_test(cx, |_| {});
12296
12297 let fs = FakeFs::new(cx.executor());
12298 fs.insert_tree(
12299 path!("/a"),
12300 json!({
12301 "main.rs": "fn main() { let a = 5; }",
12302 "other.rs": "// Test file",
12303 }),
12304 )
12305 .await;
12306
12307 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12308
12309 let server_restarts = Arc::new(AtomicUsize::new(0));
12310 let closure_restarts = Arc::clone(&server_restarts);
12311 let language_server_name = "test language server";
12312 let language_name: LanguageName = "Rust".into();
12313
12314 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12315 language_registry.add(Arc::new(Language::new(
12316 LanguageConfig {
12317 name: language_name.clone(),
12318 matcher: LanguageMatcher {
12319 path_suffixes: vec!["rs".to_string()],
12320 ..Default::default()
12321 },
12322 ..Default::default()
12323 },
12324 Some(tree_sitter_rust::LANGUAGE.into()),
12325 )));
12326 let mut fake_servers = language_registry.register_fake_lsp(
12327 "Rust",
12328 FakeLspAdapter {
12329 name: language_server_name,
12330 initialization_options: Some(json!({
12331 "testOptionValue": true
12332 })),
12333 initializer: Some(Box::new(move |fake_server| {
12334 let task_restarts = Arc::clone(&closure_restarts);
12335 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
12336 task_restarts.fetch_add(1, atomic::Ordering::Release);
12337 futures::future::ready(Ok(()))
12338 });
12339 })),
12340 ..Default::default()
12341 },
12342 );
12343
12344 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12345 let _buffer = project
12346 .update(cx, |project, cx| {
12347 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
12348 })
12349 .await
12350 .unwrap();
12351 let _fake_server = fake_servers.next().await.unwrap();
12352 update_test_language_settings(cx, |language_settings| {
12353 language_settings.languages.insert(
12354 language_name.clone(),
12355 LanguageSettingsContent {
12356 tab_size: NonZeroU32::new(8),
12357 ..Default::default()
12358 },
12359 );
12360 });
12361 cx.executor().run_until_parked();
12362 assert_eq!(
12363 server_restarts.load(atomic::Ordering::Acquire),
12364 0,
12365 "Should not restart LSP server on an unrelated change"
12366 );
12367
12368 update_test_project_settings(cx, |project_settings| {
12369 project_settings.lsp.insert(
12370 "Some other server name".into(),
12371 LspSettings {
12372 binary: None,
12373 settings: None,
12374 initialization_options: Some(json!({
12375 "some other init value": false
12376 })),
12377 },
12378 );
12379 });
12380 cx.executor().run_until_parked();
12381 assert_eq!(
12382 server_restarts.load(atomic::Ordering::Acquire),
12383 0,
12384 "Should not restart LSP server on an unrelated LSP settings change"
12385 );
12386
12387 update_test_project_settings(cx, |project_settings| {
12388 project_settings.lsp.insert(
12389 language_server_name.into(),
12390 LspSettings {
12391 binary: None,
12392 settings: None,
12393 initialization_options: Some(json!({
12394 "anotherInitValue": false
12395 })),
12396 },
12397 );
12398 });
12399 cx.executor().run_until_parked();
12400 assert_eq!(
12401 server_restarts.load(atomic::Ordering::Acquire),
12402 1,
12403 "Should restart LSP server on a related LSP settings change"
12404 );
12405
12406 update_test_project_settings(cx, |project_settings| {
12407 project_settings.lsp.insert(
12408 language_server_name.into(),
12409 LspSettings {
12410 binary: None,
12411 settings: None,
12412 initialization_options: Some(json!({
12413 "anotherInitValue": false
12414 })),
12415 },
12416 );
12417 });
12418 cx.executor().run_until_parked();
12419 assert_eq!(
12420 server_restarts.load(atomic::Ordering::Acquire),
12421 1,
12422 "Should not restart LSP server on a related LSP settings change that is the same"
12423 );
12424
12425 update_test_project_settings(cx, |project_settings| {
12426 project_settings.lsp.insert(
12427 language_server_name.into(),
12428 LspSettings {
12429 binary: None,
12430 settings: None,
12431 initialization_options: None,
12432 },
12433 );
12434 });
12435 cx.executor().run_until_parked();
12436 assert_eq!(
12437 server_restarts.load(atomic::Ordering::Acquire),
12438 2,
12439 "Should restart LSP server on another related LSP settings change"
12440 );
12441}
12442
12443#[gpui::test]
12444async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
12445 init_test(cx, |_| {});
12446
12447 let mut cx = EditorLspTestContext::new_rust(
12448 lsp::ServerCapabilities {
12449 completion_provider: Some(lsp::CompletionOptions {
12450 trigger_characters: Some(vec![".".to_string()]),
12451 resolve_provider: Some(true),
12452 ..Default::default()
12453 }),
12454 ..Default::default()
12455 },
12456 cx,
12457 )
12458 .await;
12459
12460 cx.set_state("fn main() { let a = 2ˇ; }");
12461 cx.simulate_keystroke(".");
12462 let completion_item = lsp::CompletionItem {
12463 label: "some".into(),
12464 kind: Some(lsp::CompletionItemKind::SNIPPET),
12465 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
12466 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
12467 kind: lsp::MarkupKind::Markdown,
12468 value: "```rust\nSome(2)\n```".to_string(),
12469 })),
12470 deprecated: Some(false),
12471 sort_text: Some("fffffff2".to_string()),
12472 filter_text: Some("some".to_string()),
12473 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
12474 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12475 range: lsp::Range {
12476 start: lsp::Position {
12477 line: 0,
12478 character: 22,
12479 },
12480 end: lsp::Position {
12481 line: 0,
12482 character: 22,
12483 },
12484 },
12485 new_text: "Some(2)".to_string(),
12486 })),
12487 additional_text_edits: Some(vec![lsp::TextEdit {
12488 range: lsp::Range {
12489 start: lsp::Position {
12490 line: 0,
12491 character: 20,
12492 },
12493 end: lsp::Position {
12494 line: 0,
12495 character: 22,
12496 },
12497 },
12498 new_text: "".to_string(),
12499 }]),
12500 ..Default::default()
12501 };
12502
12503 let closure_completion_item = completion_item.clone();
12504 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
12505 let task_completion_item = closure_completion_item.clone();
12506 async move {
12507 Ok(Some(lsp::CompletionResponse::Array(vec![
12508 task_completion_item,
12509 ])))
12510 }
12511 });
12512
12513 request.next().await;
12514
12515 cx.condition(|editor, _| editor.context_menu_visible())
12516 .await;
12517 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12518 editor
12519 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12520 .unwrap()
12521 });
12522 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
12523
12524 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
12525 let task_completion_item = completion_item.clone();
12526 async move { Ok(task_completion_item) }
12527 })
12528 .next()
12529 .await
12530 .unwrap();
12531 apply_additional_edits.await.unwrap();
12532 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
12533}
12534
12535#[gpui::test]
12536async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
12537 init_test(cx, |_| {});
12538
12539 let mut cx = EditorLspTestContext::new_rust(
12540 lsp::ServerCapabilities {
12541 completion_provider: Some(lsp::CompletionOptions {
12542 trigger_characters: Some(vec![".".to_string()]),
12543 resolve_provider: Some(true),
12544 ..Default::default()
12545 }),
12546 ..Default::default()
12547 },
12548 cx,
12549 )
12550 .await;
12551
12552 cx.set_state("fn main() { let a = 2ˇ; }");
12553 cx.simulate_keystroke(".");
12554
12555 let item1 = lsp::CompletionItem {
12556 label: "method id()".to_string(),
12557 filter_text: Some("id".to_string()),
12558 detail: None,
12559 documentation: None,
12560 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12561 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12562 new_text: ".id".to_string(),
12563 })),
12564 ..lsp::CompletionItem::default()
12565 };
12566
12567 let item2 = lsp::CompletionItem {
12568 label: "other".to_string(),
12569 filter_text: Some("other".to_string()),
12570 detail: None,
12571 documentation: None,
12572 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12573 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12574 new_text: ".other".to_string(),
12575 })),
12576 ..lsp::CompletionItem::default()
12577 };
12578
12579 let item1 = item1.clone();
12580 cx.set_request_handler::<lsp::request::Completion, _, _>({
12581 let item1 = item1.clone();
12582 move |_, _, _| {
12583 let item1 = item1.clone();
12584 let item2 = item2.clone();
12585 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
12586 }
12587 })
12588 .next()
12589 .await;
12590
12591 cx.condition(|editor, _| editor.context_menu_visible())
12592 .await;
12593 cx.update_editor(|editor, _, _| {
12594 let context_menu = editor.context_menu.borrow_mut();
12595 let context_menu = context_menu
12596 .as_ref()
12597 .expect("Should have the context menu deployed");
12598 match context_menu {
12599 CodeContextMenu::Completions(completions_menu) => {
12600 let completions = completions_menu.completions.borrow_mut();
12601 assert_eq!(
12602 completions
12603 .iter()
12604 .map(|completion| &completion.label.text)
12605 .collect::<Vec<_>>(),
12606 vec!["method id()", "other"]
12607 )
12608 }
12609 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
12610 }
12611 });
12612
12613 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
12614 let item1 = item1.clone();
12615 move |_, item_to_resolve, _| {
12616 let item1 = item1.clone();
12617 async move {
12618 if item1 == item_to_resolve {
12619 Ok(lsp::CompletionItem {
12620 label: "method id()".to_string(),
12621 filter_text: Some("id".to_string()),
12622 detail: Some("Now resolved!".to_string()),
12623 documentation: Some(lsp::Documentation::String("Docs".to_string())),
12624 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12625 range: lsp::Range::new(
12626 lsp::Position::new(0, 22),
12627 lsp::Position::new(0, 22),
12628 ),
12629 new_text: ".id".to_string(),
12630 })),
12631 ..lsp::CompletionItem::default()
12632 })
12633 } else {
12634 Ok(item_to_resolve)
12635 }
12636 }
12637 }
12638 })
12639 .next()
12640 .await
12641 .unwrap();
12642 cx.run_until_parked();
12643
12644 cx.update_editor(|editor, window, cx| {
12645 editor.context_menu_next(&Default::default(), window, cx);
12646 });
12647
12648 cx.update_editor(|editor, _, _| {
12649 let context_menu = editor.context_menu.borrow_mut();
12650 let context_menu = context_menu
12651 .as_ref()
12652 .expect("Should have the context menu deployed");
12653 match context_menu {
12654 CodeContextMenu::Completions(completions_menu) => {
12655 let completions = completions_menu.completions.borrow_mut();
12656 assert_eq!(
12657 completions
12658 .iter()
12659 .map(|completion| &completion.label.text)
12660 .collect::<Vec<_>>(),
12661 vec!["method id() Now resolved!", "other"],
12662 "Should update first completion label, but not second as the filter text did not match."
12663 );
12664 }
12665 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
12666 }
12667 });
12668}
12669
12670#[gpui::test]
12671async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
12672 init_test(cx, |_| {});
12673
12674 let mut cx = EditorLspTestContext::new_rust(
12675 lsp::ServerCapabilities {
12676 completion_provider: Some(lsp::CompletionOptions {
12677 trigger_characters: Some(vec![".".to_string()]),
12678 resolve_provider: Some(true),
12679 ..Default::default()
12680 }),
12681 ..Default::default()
12682 },
12683 cx,
12684 )
12685 .await;
12686
12687 cx.set_state("fn main() { let a = 2ˇ; }");
12688 cx.simulate_keystroke(".");
12689
12690 let unresolved_item_1 = lsp::CompletionItem {
12691 label: "id".to_string(),
12692 filter_text: Some("id".to_string()),
12693 detail: None,
12694 documentation: None,
12695 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12696 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12697 new_text: ".id".to_string(),
12698 })),
12699 ..lsp::CompletionItem::default()
12700 };
12701 let resolved_item_1 = lsp::CompletionItem {
12702 additional_text_edits: Some(vec![lsp::TextEdit {
12703 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
12704 new_text: "!!".to_string(),
12705 }]),
12706 ..unresolved_item_1.clone()
12707 };
12708 let unresolved_item_2 = lsp::CompletionItem {
12709 label: "other".to_string(),
12710 filter_text: Some("other".to_string()),
12711 detail: None,
12712 documentation: None,
12713 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12714 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12715 new_text: ".other".to_string(),
12716 })),
12717 ..lsp::CompletionItem::default()
12718 };
12719 let resolved_item_2 = lsp::CompletionItem {
12720 additional_text_edits: Some(vec![lsp::TextEdit {
12721 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
12722 new_text: "??".to_string(),
12723 }]),
12724 ..unresolved_item_2.clone()
12725 };
12726
12727 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
12728 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
12729 cx.lsp
12730 .server
12731 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
12732 let unresolved_item_1 = unresolved_item_1.clone();
12733 let resolved_item_1 = resolved_item_1.clone();
12734 let unresolved_item_2 = unresolved_item_2.clone();
12735 let resolved_item_2 = resolved_item_2.clone();
12736 let resolve_requests_1 = resolve_requests_1.clone();
12737 let resolve_requests_2 = resolve_requests_2.clone();
12738 move |unresolved_request, _| {
12739 let unresolved_item_1 = unresolved_item_1.clone();
12740 let resolved_item_1 = resolved_item_1.clone();
12741 let unresolved_item_2 = unresolved_item_2.clone();
12742 let resolved_item_2 = resolved_item_2.clone();
12743 let resolve_requests_1 = resolve_requests_1.clone();
12744 let resolve_requests_2 = resolve_requests_2.clone();
12745 async move {
12746 if unresolved_request == unresolved_item_1 {
12747 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
12748 Ok(resolved_item_1.clone())
12749 } else if unresolved_request == unresolved_item_2 {
12750 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
12751 Ok(resolved_item_2.clone())
12752 } else {
12753 panic!("Unexpected completion item {unresolved_request:?}")
12754 }
12755 }
12756 }
12757 })
12758 .detach();
12759
12760 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
12761 let unresolved_item_1 = unresolved_item_1.clone();
12762 let unresolved_item_2 = unresolved_item_2.clone();
12763 async move {
12764 Ok(Some(lsp::CompletionResponse::Array(vec![
12765 unresolved_item_1,
12766 unresolved_item_2,
12767 ])))
12768 }
12769 })
12770 .next()
12771 .await;
12772
12773 cx.condition(|editor, _| editor.context_menu_visible())
12774 .await;
12775 cx.update_editor(|editor, _, _| {
12776 let context_menu = editor.context_menu.borrow_mut();
12777 let context_menu = context_menu
12778 .as_ref()
12779 .expect("Should have the context menu deployed");
12780 match context_menu {
12781 CodeContextMenu::Completions(completions_menu) => {
12782 let completions = completions_menu.completions.borrow_mut();
12783 assert_eq!(
12784 completions
12785 .iter()
12786 .map(|completion| &completion.label.text)
12787 .collect::<Vec<_>>(),
12788 vec!["id", "other"]
12789 )
12790 }
12791 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
12792 }
12793 });
12794 cx.run_until_parked();
12795
12796 cx.update_editor(|editor, window, cx| {
12797 editor.context_menu_next(&ContextMenuNext, window, cx);
12798 });
12799 cx.run_until_parked();
12800 cx.update_editor(|editor, window, cx| {
12801 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
12802 });
12803 cx.run_until_parked();
12804 cx.update_editor(|editor, window, cx| {
12805 editor.context_menu_next(&ContextMenuNext, window, cx);
12806 });
12807 cx.run_until_parked();
12808 cx.update_editor(|editor, window, cx| {
12809 editor
12810 .compose_completion(&ComposeCompletion::default(), window, cx)
12811 .expect("No task returned")
12812 })
12813 .await
12814 .expect("Completion failed");
12815 cx.run_until_parked();
12816
12817 cx.update_editor(|editor, _, cx| {
12818 assert_eq!(
12819 resolve_requests_1.load(atomic::Ordering::Acquire),
12820 1,
12821 "Should always resolve once despite multiple selections"
12822 );
12823 assert_eq!(
12824 resolve_requests_2.load(atomic::Ordering::Acquire),
12825 1,
12826 "Should always resolve once after multiple selections and applying the completion"
12827 );
12828 assert_eq!(
12829 editor.text(cx),
12830 "fn main() { let a = ??.other; }",
12831 "Should use resolved data when applying the completion"
12832 );
12833 });
12834}
12835
12836#[gpui::test]
12837async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
12838 init_test(cx, |_| {});
12839
12840 let item_0 = lsp::CompletionItem {
12841 label: "abs".into(),
12842 insert_text: Some("abs".into()),
12843 data: Some(json!({ "very": "special"})),
12844 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
12845 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
12846 lsp::InsertReplaceEdit {
12847 new_text: "abs".to_string(),
12848 insert: lsp::Range::default(),
12849 replace: lsp::Range::default(),
12850 },
12851 )),
12852 ..lsp::CompletionItem::default()
12853 };
12854 let items = iter::once(item_0.clone())
12855 .chain((11..51).map(|i| lsp::CompletionItem {
12856 label: format!("item_{}", i),
12857 insert_text: Some(format!("item_{}", i)),
12858 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
12859 ..lsp::CompletionItem::default()
12860 }))
12861 .collect::<Vec<_>>();
12862
12863 let default_commit_characters = vec!["?".to_string()];
12864 let default_data = json!({ "default": "data"});
12865 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
12866 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
12867 let default_edit_range = lsp::Range {
12868 start: lsp::Position {
12869 line: 0,
12870 character: 5,
12871 },
12872 end: lsp::Position {
12873 line: 0,
12874 character: 5,
12875 },
12876 };
12877
12878 let mut cx = EditorLspTestContext::new_rust(
12879 lsp::ServerCapabilities {
12880 completion_provider: Some(lsp::CompletionOptions {
12881 trigger_characters: Some(vec![".".to_string()]),
12882 resolve_provider: Some(true),
12883 ..Default::default()
12884 }),
12885 ..Default::default()
12886 },
12887 cx,
12888 )
12889 .await;
12890
12891 cx.set_state("fn main() { let a = 2ˇ; }");
12892 cx.simulate_keystroke(".");
12893
12894 let completion_data = default_data.clone();
12895 let completion_characters = default_commit_characters.clone();
12896 let completion_items = items.clone();
12897 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
12898 let default_data = completion_data.clone();
12899 let default_commit_characters = completion_characters.clone();
12900 let items = completion_items.clone();
12901 async move {
12902 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
12903 items,
12904 item_defaults: Some(lsp::CompletionListItemDefaults {
12905 data: Some(default_data.clone()),
12906 commit_characters: Some(default_commit_characters.clone()),
12907 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
12908 default_edit_range,
12909 )),
12910 insert_text_format: Some(default_insert_text_format),
12911 insert_text_mode: Some(default_insert_text_mode),
12912 }),
12913 ..lsp::CompletionList::default()
12914 })))
12915 }
12916 })
12917 .next()
12918 .await;
12919
12920 let resolved_items = Arc::new(Mutex::new(Vec::new()));
12921 cx.lsp
12922 .server
12923 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
12924 let closure_resolved_items = resolved_items.clone();
12925 move |item_to_resolve, _| {
12926 let closure_resolved_items = closure_resolved_items.clone();
12927 async move {
12928 closure_resolved_items.lock().push(item_to_resolve.clone());
12929 Ok(item_to_resolve)
12930 }
12931 }
12932 })
12933 .detach();
12934
12935 cx.condition(|editor, _| editor.context_menu_visible())
12936 .await;
12937 cx.run_until_parked();
12938 cx.update_editor(|editor, _, _| {
12939 let menu = editor.context_menu.borrow_mut();
12940 match menu.as_ref().expect("should have the completions menu") {
12941 CodeContextMenu::Completions(completions_menu) => {
12942 assert_eq!(
12943 completions_menu
12944 .entries
12945 .borrow()
12946 .iter()
12947 .map(|mat| mat.string.clone())
12948 .collect::<Vec<String>>(),
12949 items
12950 .iter()
12951 .map(|completion| completion.label.clone())
12952 .collect::<Vec<String>>()
12953 );
12954 }
12955 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
12956 }
12957 });
12958 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
12959 // with 4 from the end.
12960 assert_eq!(
12961 *resolved_items.lock(),
12962 [&items[0..16], &items[items.len() - 4..items.len()]]
12963 .concat()
12964 .iter()
12965 .cloned()
12966 .map(|mut item| {
12967 if item.data.is_none() {
12968 item.data = Some(default_data.clone());
12969 }
12970 item
12971 })
12972 .collect::<Vec<lsp::CompletionItem>>(),
12973 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
12974 );
12975 resolved_items.lock().clear();
12976
12977 cx.update_editor(|editor, window, cx| {
12978 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
12979 });
12980 cx.run_until_parked();
12981 // Completions that have already been resolved are skipped.
12982 assert_eq!(
12983 *resolved_items.lock(),
12984 items[items.len() - 16..items.len() - 4]
12985 .iter()
12986 .cloned()
12987 .map(|mut item| {
12988 if item.data.is_none() {
12989 item.data = Some(default_data.clone());
12990 }
12991 item
12992 })
12993 .collect::<Vec<lsp::CompletionItem>>()
12994 );
12995 resolved_items.lock().clear();
12996}
12997
12998#[gpui::test]
12999async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
13000 init_test(cx, |_| {});
13001
13002 let mut cx = EditorLspTestContext::new(
13003 Language::new(
13004 LanguageConfig {
13005 matcher: LanguageMatcher {
13006 path_suffixes: vec!["jsx".into()],
13007 ..Default::default()
13008 },
13009 overrides: [(
13010 "element".into(),
13011 LanguageConfigOverride {
13012 completion_query_characters: Override::Set(['-'].into_iter().collect()),
13013 ..Default::default()
13014 },
13015 )]
13016 .into_iter()
13017 .collect(),
13018 ..Default::default()
13019 },
13020 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
13021 )
13022 .with_override_query("(jsx_self_closing_element) @element")
13023 .unwrap(),
13024 lsp::ServerCapabilities {
13025 completion_provider: Some(lsp::CompletionOptions {
13026 trigger_characters: Some(vec![":".to_string()]),
13027 ..Default::default()
13028 }),
13029 ..Default::default()
13030 },
13031 cx,
13032 )
13033 .await;
13034
13035 cx.lsp
13036 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13037 Ok(Some(lsp::CompletionResponse::Array(vec![
13038 lsp::CompletionItem {
13039 label: "bg-blue".into(),
13040 ..Default::default()
13041 },
13042 lsp::CompletionItem {
13043 label: "bg-red".into(),
13044 ..Default::default()
13045 },
13046 lsp::CompletionItem {
13047 label: "bg-yellow".into(),
13048 ..Default::default()
13049 },
13050 ])))
13051 });
13052
13053 cx.set_state(r#"<p class="bgˇ" />"#);
13054
13055 // Trigger completion when typing a dash, because the dash is an extra
13056 // word character in the 'element' scope, which contains the cursor.
13057 cx.simulate_keystroke("-");
13058 cx.executor().run_until_parked();
13059 cx.update_editor(|editor, _, _| {
13060 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13061 {
13062 assert_eq!(
13063 completion_menu_entries(&menu),
13064 &["bg-red", "bg-blue", "bg-yellow"]
13065 );
13066 } else {
13067 panic!("expected completion menu to be open");
13068 }
13069 });
13070
13071 cx.simulate_keystroke("l");
13072 cx.executor().run_until_parked();
13073 cx.update_editor(|editor, _, _| {
13074 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13075 {
13076 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
13077 } else {
13078 panic!("expected completion menu to be open");
13079 }
13080 });
13081
13082 // When filtering completions, consider the character after the '-' to
13083 // be the start of a subword.
13084 cx.set_state(r#"<p class="yelˇ" />"#);
13085 cx.simulate_keystroke("l");
13086 cx.executor().run_until_parked();
13087 cx.update_editor(|editor, _, _| {
13088 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13089 {
13090 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
13091 } else {
13092 panic!("expected completion menu to be open");
13093 }
13094 });
13095}
13096
13097fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
13098 let entries = menu.entries.borrow();
13099 entries.iter().map(|mat| mat.string.clone()).collect()
13100}
13101
13102#[gpui::test]
13103async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
13104 init_test(cx, |settings| {
13105 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
13106 FormatterList(vec![Formatter::Prettier].into()),
13107 ))
13108 });
13109
13110 let fs = FakeFs::new(cx.executor());
13111 fs.insert_file(path!("/file.ts"), Default::default()).await;
13112
13113 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
13114 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13115
13116 language_registry.add(Arc::new(Language::new(
13117 LanguageConfig {
13118 name: "TypeScript".into(),
13119 matcher: LanguageMatcher {
13120 path_suffixes: vec!["ts".to_string()],
13121 ..Default::default()
13122 },
13123 ..Default::default()
13124 },
13125 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
13126 )));
13127 update_test_language_settings(cx, |settings| {
13128 settings.defaults.prettier = Some(PrettierSettings {
13129 allowed: true,
13130 ..PrettierSettings::default()
13131 });
13132 });
13133
13134 let test_plugin = "test_plugin";
13135 let _ = language_registry.register_fake_lsp(
13136 "TypeScript",
13137 FakeLspAdapter {
13138 prettier_plugins: vec![test_plugin],
13139 ..Default::default()
13140 },
13141 );
13142
13143 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
13144 let buffer = project
13145 .update(cx, |project, cx| {
13146 project.open_local_buffer(path!("/file.ts"), cx)
13147 })
13148 .await
13149 .unwrap();
13150
13151 let buffer_text = "one\ntwo\nthree\n";
13152 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13153 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
13154 editor.update_in(cx, |editor, window, cx| {
13155 editor.set_text(buffer_text, window, cx)
13156 });
13157
13158 editor
13159 .update_in(cx, |editor, window, cx| {
13160 editor.perform_format(
13161 project.clone(),
13162 FormatTrigger::Manual,
13163 FormatTarget::Buffers,
13164 window,
13165 cx,
13166 )
13167 })
13168 .unwrap()
13169 .await;
13170 assert_eq!(
13171 editor.update(cx, |editor, cx| editor.text(cx)),
13172 buffer_text.to_string() + prettier_format_suffix,
13173 "Test prettier formatting was not applied to the original buffer text",
13174 );
13175
13176 update_test_language_settings(cx, |settings| {
13177 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
13178 });
13179 let format = editor.update_in(cx, |editor, window, cx| {
13180 editor.perform_format(
13181 project.clone(),
13182 FormatTrigger::Manual,
13183 FormatTarget::Buffers,
13184 window,
13185 cx,
13186 )
13187 });
13188 format.await.unwrap();
13189 assert_eq!(
13190 editor.update(cx, |editor, cx| editor.text(cx)),
13191 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
13192 "Autoformatting (via test prettier) was not applied to the original buffer text",
13193 );
13194}
13195
13196#[gpui::test]
13197async fn test_addition_reverts(cx: &mut TestAppContext) {
13198 init_test(cx, |_| {});
13199 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13200 let base_text = indoc! {r#"
13201 struct Row;
13202 struct Row1;
13203 struct Row2;
13204
13205 struct Row4;
13206 struct Row5;
13207 struct Row6;
13208
13209 struct Row8;
13210 struct Row9;
13211 struct Row10;"#};
13212
13213 // When addition hunks are not adjacent to carets, no hunk revert is performed
13214 assert_hunk_revert(
13215 indoc! {r#"struct Row;
13216 struct Row1;
13217 struct Row1.1;
13218 struct Row1.2;
13219 struct Row2;ˇ
13220
13221 struct Row4;
13222 struct Row5;
13223 struct Row6;
13224
13225 struct Row8;
13226 ˇstruct Row9;
13227 struct Row9.1;
13228 struct Row9.2;
13229 struct Row9.3;
13230 struct Row10;"#},
13231 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
13232 indoc! {r#"struct Row;
13233 struct Row1;
13234 struct Row1.1;
13235 struct Row1.2;
13236 struct Row2;ˇ
13237
13238 struct Row4;
13239 struct Row5;
13240 struct Row6;
13241
13242 struct Row8;
13243 ˇstruct Row9;
13244 struct Row9.1;
13245 struct Row9.2;
13246 struct Row9.3;
13247 struct Row10;"#},
13248 base_text,
13249 &mut cx,
13250 );
13251 // Same for selections
13252 assert_hunk_revert(
13253 indoc! {r#"struct Row;
13254 struct Row1;
13255 struct Row2;
13256 struct Row2.1;
13257 struct Row2.2;
13258 «ˇ
13259 struct Row4;
13260 struct» Row5;
13261 «struct Row6;
13262 ˇ»
13263 struct Row9.1;
13264 struct Row9.2;
13265 struct Row9.3;
13266 struct Row8;
13267 struct Row9;
13268 struct Row10;"#},
13269 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
13270 indoc! {r#"struct Row;
13271 struct Row1;
13272 struct Row2;
13273 struct Row2.1;
13274 struct Row2.2;
13275 «ˇ
13276 struct Row4;
13277 struct» Row5;
13278 «struct Row6;
13279 ˇ»
13280 struct Row9.1;
13281 struct Row9.2;
13282 struct Row9.3;
13283 struct Row8;
13284 struct Row9;
13285 struct Row10;"#},
13286 base_text,
13287 &mut cx,
13288 );
13289
13290 // When carets and selections intersect the addition hunks, those are reverted.
13291 // Adjacent carets got merged.
13292 assert_hunk_revert(
13293 indoc! {r#"struct Row;
13294 ˇ// something on the top
13295 struct Row1;
13296 struct Row2;
13297 struct Roˇw3.1;
13298 struct Row2.2;
13299 struct Row2.3;ˇ
13300
13301 struct Row4;
13302 struct ˇRow5.1;
13303 struct Row5.2;
13304 struct «Rowˇ»5.3;
13305 struct Row5;
13306 struct Row6;
13307 ˇ
13308 struct Row9.1;
13309 struct «Rowˇ»9.2;
13310 struct «ˇRow»9.3;
13311 struct Row8;
13312 struct Row9;
13313 «ˇ// something on bottom»
13314 struct Row10;"#},
13315 vec![
13316 DiffHunkStatusKind::Added,
13317 DiffHunkStatusKind::Added,
13318 DiffHunkStatusKind::Added,
13319 DiffHunkStatusKind::Added,
13320 DiffHunkStatusKind::Added,
13321 ],
13322 indoc! {r#"struct Row;
13323 ˇstruct Row1;
13324 struct Row2;
13325 ˇ
13326 struct Row4;
13327 ˇstruct Row5;
13328 struct Row6;
13329 ˇ
13330 ˇstruct Row8;
13331 struct Row9;
13332 ˇstruct Row10;"#},
13333 base_text,
13334 &mut cx,
13335 );
13336}
13337
13338#[gpui::test]
13339async fn test_modification_reverts(cx: &mut TestAppContext) {
13340 init_test(cx, |_| {});
13341 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13342 let base_text = indoc! {r#"
13343 struct Row;
13344 struct Row1;
13345 struct Row2;
13346
13347 struct Row4;
13348 struct Row5;
13349 struct Row6;
13350
13351 struct Row8;
13352 struct Row9;
13353 struct Row10;"#};
13354
13355 // Modification hunks behave the same as the addition ones.
13356 assert_hunk_revert(
13357 indoc! {r#"struct Row;
13358 struct Row1;
13359 struct Row33;
13360 ˇ
13361 struct Row4;
13362 struct Row5;
13363 struct Row6;
13364 ˇ
13365 struct Row99;
13366 struct Row9;
13367 struct Row10;"#},
13368 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
13369 indoc! {r#"struct Row;
13370 struct Row1;
13371 struct Row33;
13372 ˇ
13373 struct Row4;
13374 struct Row5;
13375 struct Row6;
13376 ˇ
13377 struct Row99;
13378 struct Row9;
13379 struct Row10;"#},
13380 base_text,
13381 &mut cx,
13382 );
13383 assert_hunk_revert(
13384 indoc! {r#"struct Row;
13385 struct Row1;
13386 struct Row33;
13387 «ˇ
13388 struct Row4;
13389 struct» Row5;
13390 «struct Row6;
13391 ˇ»
13392 struct Row99;
13393 struct Row9;
13394 struct Row10;"#},
13395 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
13396 indoc! {r#"struct Row;
13397 struct Row1;
13398 struct Row33;
13399 «ˇ
13400 struct Row4;
13401 struct» Row5;
13402 «struct Row6;
13403 ˇ»
13404 struct Row99;
13405 struct Row9;
13406 struct Row10;"#},
13407 base_text,
13408 &mut cx,
13409 );
13410
13411 assert_hunk_revert(
13412 indoc! {r#"ˇstruct Row1.1;
13413 struct Row1;
13414 «ˇstr»uct Row22;
13415
13416 struct ˇRow44;
13417 struct Row5;
13418 struct «Rˇ»ow66;ˇ
13419
13420 «struˇ»ct Row88;
13421 struct Row9;
13422 struct Row1011;ˇ"#},
13423 vec![
13424 DiffHunkStatusKind::Modified,
13425 DiffHunkStatusKind::Modified,
13426 DiffHunkStatusKind::Modified,
13427 DiffHunkStatusKind::Modified,
13428 DiffHunkStatusKind::Modified,
13429 DiffHunkStatusKind::Modified,
13430 ],
13431 indoc! {r#"struct Row;
13432 ˇstruct Row1;
13433 struct Row2;
13434 ˇ
13435 struct Row4;
13436 ˇstruct Row5;
13437 struct Row6;
13438 ˇ
13439 struct Row8;
13440 ˇstruct Row9;
13441 struct Row10;ˇ"#},
13442 base_text,
13443 &mut cx,
13444 );
13445}
13446
13447#[gpui::test]
13448async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
13449 init_test(cx, |_| {});
13450 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13451 let base_text = indoc! {r#"
13452 one
13453
13454 two
13455 three
13456 "#};
13457
13458 cx.set_head_text(base_text);
13459 cx.set_state("\nˇ\n");
13460 cx.executor().run_until_parked();
13461 cx.update_editor(|editor, _window, cx| {
13462 editor.expand_selected_diff_hunks(cx);
13463 });
13464 cx.executor().run_until_parked();
13465 cx.update_editor(|editor, window, cx| {
13466 editor.backspace(&Default::default(), window, cx);
13467 });
13468 cx.run_until_parked();
13469 cx.assert_state_with_diff(
13470 indoc! {r#"
13471
13472 - two
13473 - threeˇ
13474 +
13475 "#}
13476 .to_string(),
13477 );
13478}
13479
13480#[gpui::test]
13481async fn test_deletion_reverts(cx: &mut TestAppContext) {
13482 init_test(cx, |_| {});
13483 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13484 let base_text = indoc! {r#"struct Row;
13485struct Row1;
13486struct Row2;
13487
13488struct Row4;
13489struct Row5;
13490struct Row6;
13491
13492struct Row8;
13493struct Row9;
13494struct Row10;"#};
13495
13496 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
13497 assert_hunk_revert(
13498 indoc! {r#"struct Row;
13499 struct Row2;
13500
13501 ˇstruct Row4;
13502 struct Row5;
13503 struct Row6;
13504 ˇ
13505 struct Row8;
13506 struct Row10;"#},
13507 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
13508 indoc! {r#"struct Row;
13509 struct Row2;
13510
13511 ˇstruct Row4;
13512 struct Row5;
13513 struct Row6;
13514 ˇ
13515 struct Row8;
13516 struct Row10;"#},
13517 base_text,
13518 &mut cx,
13519 );
13520 assert_hunk_revert(
13521 indoc! {r#"struct Row;
13522 struct Row2;
13523
13524 «ˇstruct Row4;
13525 struct» Row5;
13526 «struct Row6;
13527 ˇ»
13528 struct Row8;
13529 struct Row10;"#},
13530 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
13531 indoc! {r#"struct Row;
13532 struct Row2;
13533
13534 «ˇstruct Row4;
13535 struct» Row5;
13536 «struct Row6;
13537 ˇ»
13538 struct Row8;
13539 struct Row10;"#},
13540 base_text,
13541 &mut cx,
13542 );
13543
13544 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
13545 assert_hunk_revert(
13546 indoc! {r#"struct Row;
13547 ˇstruct Row2;
13548
13549 struct Row4;
13550 struct Row5;
13551 struct Row6;
13552
13553 struct Row8;ˇ
13554 struct Row10;"#},
13555 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
13556 indoc! {r#"struct Row;
13557 struct Row1;
13558 ˇstruct Row2;
13559
13560 struct Row4;
13561 struct Row5;
13562 struct Row6;
13563
13564 struct Row8;ˇ
13565 struct Row9;
13566 struct Row10;"#},
13567 base_text,
13568 &mut cx,
13569 );
13570 assert_hunk_revert(
13571 indoc! {r#"struct Row;
13572 struct Row2«ˇ;
13573 struct Row4;
13574 struct» Row5;
13575 «struct Row6;
13576
13577 struct Row8;ˇ»
13578 struct Row10;"#},
13579 vec![
13580 DiffHunkStatusKind::Deleted,
13581 DiffHunkStatusKind::Deleted,
13582 DiffHunkStatusKind::Deleted,
13583 ],
13584 indoc! {r#"struct Row;
13585 struct Row1;
13586 struct Row2«ˇ;
13587
13588 struct Row4;
13589 struct» Row5;
13590 «struct Row6;
13591
13592 struct Row8;ˇ»
13593 struct Row9;
13594 struct Row10;"#},
13595 base_text,
13596 &mut cx,
13597 );
13598}
13599
13600#[gpui::test]
13601async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
13602 init_test(cx, |_| {});
13603
13604 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
13605 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
13606 let base_text_3 =
13607 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
13608
13609 let text_1 = edit_first_char_of_every_line(base_text_1);
13610 let text_2 = edit_first_char_of_every_line(base_text_2);
13611 let text_3 = edit_first_char_of_every_line(base_text_3);
13612
13613 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
13614 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
13615 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
13616
13617 let multibuffer = cx.new(|cx| {
13618 let mut multibuffer = MultiBuffer::new(ReadWrite);
13619 multibuffer.push_excerpts(
13620 buffer_1.clone(),
13621 [
13622 ExcerptRange {
13623 context: Point::new(0, 0)..Point::new(3, 0),
13624 primary: None,
13625 },
13626 ExcerptRange {
13627 context: Point::new(5, 0)..Point::new(7, 0),
13628 primary: None,
13629 },
13630 ExcerptRange {
13631 context: Point::new(9, 0)..Point::new(10, 4),
13632 primary: None,
13633 },
13634 ],
13635 cx,
13636 );
13637 multibuffer.push_excerpts(
13638 buffer_2.clone(),
13639 [
13640 ExcerptRange {
13641 context: Point::new(0, 0)..Point::new(3, 0),
13642 primary: None,
13643 },
13644 ExcerptRange {
13645 context: Point::new(5, 0)..Point::new(7, 0),
13646 primary: None,
13647 },
13648 ExcerptRange {
13649 context: Point::new(9, 0)..Point::new(10, 4),
13650 primary: None,
13651 },
13652 ],
13653 cx,
13654 );
13655 multibuffer.push_excerpts(
13656 buffer_3.clone(),
13657 [
13658 ExcerptRange {
13659 context: Point::new(0, 0)..Point::new(3, 0),
13660 primary: None,
13661 },
13662 ExcerptRange {
13663 context: Point::new(5, 0)..Point::new(7, 0),
13664 primary: None,
13665 },
13666 ExcerptRange {
13667 context: Point::new(9, 0)..Point::new(10, 4),
13668 primary: None,
13669 },
13670 ],
13671 cx,
13672 );
13673 multibuffer
13674 });
13675
13676 let fs = FakeFs::new(cx.executor());
13677 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
13678 let (editor, cx) = cx
13679 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
13680 editor.update_in(cx, |editor, _window, cx| {
13681 for (buffer, diff_base) in [
13682 (buffer_1.clone(), base_text_1),
13683 (buffer_2.clone(), base_text_2),
13684 (buffer_3.clone(), base_text_3),
13685 ] {
13686 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
13687 editor
13688 .buffer
13689 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
13690 }
13691 });
13692 cx.executor().run_until_parked();
13693
13694 editor.update_in(cx, |editor, window, cx| {
13695 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}");
13696 editor.select_all(&SelectAll, window, cx);
13697 editor.git_restore(&Default::default(), window, cx);
13698 });
13699 cx.executor().run_until_parked();
13700
13701 // When all ranges are selected, all buffer hunks are reverted.
13702 editor.update(cx, |editor, cx| {
13703 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");
13704 });
13705 buffer_1.update(cx, |buffer, _| {
13706 assert_eq!(buffer.text(), base_text_1);
13707 });
13708 buffer_2.update(cx, |buffer, _| {
13709 assert_eq!(buffer.text(), base_text_2);
13710 });
13711 buffer_3.update(cx, |buffer, _| {
13712 assert_eq!(buffer.text(), base_text_3);
13713 });
13714
13715 editor.update_in(cx, |editor, window, cx| {
13716 editor.undo(&Default::default(), window, cx);
13717 });
13718
13719 editor.update_in(cx, |editor, window, cx| {
13720 editor.change_selections(None, window, cx, |s| {
13721 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
13722 });
13723 editor.git_restore(&Default::default(), window, cx);
13724 });
13725
13726 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
13727 // but not affect buffer_2 and its related excerpts.
13728 editor.update(cx, |editor, cx| {
13729 assert_eq!(
13730 editor.text(cx),
13731 "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}"
13732 );
13733 });
13734 buffer_1.update(cx, |buffer, _| {
13735 assert_eq!(buffer.text(), base_text_1);
13736 });
13737 buffer_2.update(cx, |buffer, _| {
13738 assert_eq!(
13739 buffer.text(),
13740 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
13741 );
13742 });
13743 buffer_3.update(cx, |buffer, _| {
13744 assert_eq!(
13745 buffer.text(),
13746 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
13747 );
13748 });
13749
13750 fn edit_first_char_of_every_line(text: &str) -> String {
13751 text.split('\n')
13752 .map(|line| format!("X{}", &line[1..]))
13753 .collect::<Vec<_>>()
13754 .join("\n")
13755 }
13756}
13757
13758#[gpui::test]
13759async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
13760 init_test(cx, |_| {});
13761
13762 let cols = 4;
13763 let rows = 10;
13764 let sample_text_1 = sample_text(rows, cols, 'a');
13765 assert_eq!(
13766 sample_text_1,
13767 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
13768 );
13769 let sample_text_2 = sample_text(rows, cols, 'l');
13770 assert_eq!(
13771 sample_text_2,
13772 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
13773 );
13774 let sample_text_3 = sample_text(rows, cols, 'v');
13775 assert_eq!(
13776 sample_text_3,
13777 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
13778 );
13779
13780 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
13781 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
13782 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
13783
13784 let multi_buffer = cx.new(|cx| {
13785 let mut multibuffer = MultiBuffer::new(ReadWrite);
13786 multibuffer.push_excerpts(
13787 buffer_1.clone(),
13788 [
13789 ExcerptRange {
13790 context: Point::new(0, 0)..Point::new(3, 0),
13791 primary: None,
13792 },
13793 ExcerptRange {
13794 context: Point::new(5, 0)..Point::new(7, 0),
13795 primary: None,
13796 },
13797 ExcerptRange {
13798 context: Point::new(9, 0)..Point::new(10, 4),
13799 primary: None,
13800 },
13801 ],
13802 cx,
13803 );
13804 multibuffer.push_excerpts(
13805 buffer_2.clone(),
13806 [
13807 ExcerptRange {
13808 context: Point::new(0, 0)..Point::new(3, 0),
13809 primary: None,
13810 },
13811 ExcerptRange {
13812 context: Point::new(5, 0)..Point::new(7, 0),
13813 primary: None,
13814 },
13815 ExcerptRange {
13816 context: Point::new(9, 0)..Point::new(10, 4),
13817 primary: None,
13818 },
13819 ],
13820 cx,
13821 );
13822 multibuffer.push_excerpts(
13823 buffer_3.clone(),
13824 [
13825 ExcerptRange {
13826 context: Point::new(0, 0)..Point::new(3, 0),
13827 primary: None,
13828 },
13829 ExcerptRange {
13830 context: Point::new(5, 0)..Point::new(7, 0),
13831 primary: None,
13832 },
13833 ExcerptRange {
13834 context: Point::new(9, 0)..Point::new(10, 4),
13835 primary: None,
13836 },
13837 ],
13838 cx,
13839 );
13840 multibuffer
13841 });
13842
13843 let fs = FakeFs::new(cx.executor());
13844 fs.insert_tree(
13845 "/a",
13846 json!({
13847 "main.rs": sample_text_1,
13848 "other.rs": sample_text_2,
13849 "lib.rs": sample_text_3,
13850 }),
13851 )
13852 .await;
13853 let project = Project::test(fs, ["/a".as_ref()], cx).await;
13854 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13855 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
13856 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
13857 Editor::new(
13858 EditorMode::Full,
13859 multi_buffer,
13860 Some(project.clone()),
13861 window,
13862 cx,
13863 )
13864 });
13865 let multibuffer_item_id = workspace
13866 .update(cx, |workspace, window, cx| {
13867 assert!(
13868 workspace.active_item(cx).is_none(),
13869 "active item should be None before the first item is added"
13870 );
13871 workspace.add_item_to_active_pane(
13872 Box::new(multi_buffer_editor.clone()),
13873 None,
13874 true,
13875 window,
13876 cx,
13877 );
13878 let active_item = workspace
13879 .active_item(cx)
13880 .expect("should have an active item after adding the multi buffer");
13881 assert!(
13882 !active_item.is_singleton(cx),
13883 "A multi buffer was expected to active after adding"
13884 );
13885 active_item.item_id()
13886 })
13887 .unwrap();
13888 cx.executor().run_until_parked();
13889
13890 multi_buffer_editor.update_in(cx, |editor, window, cx| {
13891 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
13892 s.select_ranges(Some(1..2))
13893 });
13894 editor.open_excerpts(&OpenExcerpts, window, cx);
13895 });
13896 cx.executor().run_until_parked();
13897 let first_item_id = workspace
13898 .update(cx, |workspace, window, cx| {
13899 let active_item = workspace
13900 .active_item(cx)
13901 .expect("should have an active item after navigating into the 1st buffer");
13902 let first_item_id = active_item.item_id();
13903 assert_ne!(
13904 first_item_id, multibuffer_item_id,
13905 "Should navigate into the 1st buffer and activate it"
13906 );
13907 assert!(
13908 active_item.is_singleton(cx),
13909 "New active item should be a singleton buffer"
13910 );
13911 assert_eq!(
13912 active_item
13913 .act_as::<Editor>(cx)
13914 .expect("should have navigated into an editor for the 1st buffer")
13915 .read(cx)
13916 .text(cx),
13917 sample_text_1
13918 );
13919
13920 workspace
13921 .go_back(workspace.active_pane().downgrade(), window, cx)
13922 .detach_and_log_err(cx);
13923
13924 first_item_id
13925 })
13926 .unwrap();
13927 cx.executor().run_until_parked();
13928 workspace
13929 .update(cx, |workspace, _, cx| {
13930 let active_item = workspace
13931 .active_item(cx)
13932 .expect("should have an active item after navigating back");
13933 assert_eq!(
13934 active_item.item_id(),
13935 multibuffer_item_id,
13936 "Should navigate back to the multi buffer"
13937 );
13938 assert!(!active_item.is_singleton(cx));
13939 })
13940 .unwrap();
13941
13942 multi_buffer_editor.update_in(cx, |editor, window, cx| {
13943 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
13944 s.select_ranges(Some(39..40))
13945 });
13946 editor.open_excerpts(&OpenExcerpts, window, cx);
13947 });
13948 cx.executor().run_until_parked();
13949 let second_item_id = workspace
13950 .update(cx, |workspace, window, cx| {
13951 let active_item = workspace
13952 .active_item(cx)
13953 .expect("should have an active item after navigating into the 2nd buffer");
13954 let second_item_id = active_item.item_id();
13955 assert_ne!(
13956 second_item_id, multibuffer_item_id,
13957 "Should navigate away from the multibuffer"
13958 );
13959 assert_ne!(
13960 second_item_id, first_item_id,
13961 "Should navigate into the 2nd buffer and activate it"
13962 );
13963 assert!(
13964 active_item.is_singleton(cx),
13965 "New active item should be a singleton buffer"
13966 );
13967 assert_eq!(
13968 active_item
13969 .act_as::<Editor>(cx)
13970 .expect("should have navigated into an editor")
13971 .read(cx)
13972 .text(cx),
13973 sample_text_2
13974 );
13975
13976 workspace
13977 .go_back(workspace.active_pane().downgrade(), window, cx)
13978 .detach_and_log_err(cx);
13979
13980 second_item_id
13981 })
13982 .unwrap();
13983 cx.executor().run_until_parked();
13984 workspace
13985 .update(cx, |workspace, _, cx| {
13986 let active_item = workspace
13987 .active_item(cx)
13988 .expect("should have an active item after navigating back from the 2nd buffer");
13989 assert_eq!(
13990 active_item.item_id(),
13991 multibuffer_item_id,
13992 "Should navigate back from the 2nd buffer to the multi buffer"
13993 );
13994 assert!(!active_item.is_singleton(cx));
13995 })
13996 .unwrap();
13997
13998 multi_buffer_editor.update_in(cx, |editor, window, cx| {
13999 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14000 s.select_ranges(Some(70..70))
14001 });
14002 editor.open_excerpts(&OpenExcerpts, window, cx);
14003 });
14004 cx.executor().run_until_parked();
14005 workspace
14006 .update(cx, |workspace, window, cx| {
14007 let active_item = workspace
14008 .active_item(cx)
14009 .expect("should have an active item after navigating into the 3rd buffer");
14010 let third_item_id = active_item.item_id();
14011 assert_ne!(
14012 third_item_id, multibuffer_item_id,
14013 "Should navigate into the 3rd buffer and activate it"
14014 );
14015 assert_ne!(third_item_id, first_item_id);
14016 assert_ne!(third_item_id, second_item_id);
14017 assert!(
14018 active_item.is_singleton(cx),
14019 "New active item should be a singleton buffer"
14020 );
14021 assert_eq!(
14022 active_item
14023 .act_as::<Editor>(cx)
14024 .expect("should have navigated into an editor")
14025 .read(cx)
14026 .text(cx),
14027 sample_text_3
14028 );
14029
14030 workspace
14031 .go_back(workspace.active_pane().downgrade(), window, cx)
14032 .detach_and_log_err(cx);
14033 })
14034 .unwrap();
14035 cx.executor().run_until_parked();
14036 workspace
14037 .update(cx, |workspace, _, cx| {
14038 let active_item = workspace
14039 .active_item(cx)
14040 .expect("should have an active item after navigating back from the 3rd buffer");
14041 assert_eq!(
14042 active_item.item_id(),
14043 multibuffer_item_id,
14044 "Should navigate back from the 3rd buffer to the multi buffer"
14045 );
14046 assert!(!active_item.is_singleton(cx));
14047 })
14048 .unwrap();
14049}
14050
14051#[gpui::test]
14052async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14053 init_test(cx, |_| {});
14054
14055 let mut cx = EditorTestContext::new(cx).await;
14056
14057 let diff_base = r#"
14058 use some::mod;
14059
14060 const A: u32 = 42;
14061
14062 fn main() {
14063 println!("hello");
14064
14065 println!("world");
14066 }
14067 "#
14068 .unindent();
14069
14070 cx.set_state(
14071 &r#"
14072 use some::modified;
14073
14074 ˇ
14075 fn main() {
14076 println!("hello there");
14077
14078 println!("around the");
14079 println!("world");
14080 }
14081 "#
14082 .unindent(),
14083 );
14084
14085 cx.set_head_text(&diff_base);
14086 executor.run_until_parked();
14087
14088 cx.update_editor(|editor, window, cx| {
14089 editor.go_to_next_hunk(&GoToHunk, window, cx);
14090 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14091 });
14092 executor.run_until_parked();
14093 cx.assert_state_with_diff(
14094 r#"
14095 use some::modified;
14096
14097
14098 fn main() {
14099 - println!("hello");
14100 + ˇ println!("hello there");
14101
14102 println!("around the");
14103 println!("world");
14104 }
14105 "#
14106 .unindent(),
14107 );
14108
14109 cx.update_editor(|editor, window, cx| {
14110 for _ in 0..2 {
14111 editor.go_to_next_hunk(&GoToHunk, window, cx);
14112 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14113 }
14114 });
14115 executor.run_until_parked();
14116 cx.assert_state_with_diff(
14117 r#"
14118 - use some::mod;
14119 + ˇuse some::modified;
14120
14121
14122 fn main() {
14123 - println!("hello");
14124 + println!("hello there");
14125
14126 + println!("around the");
14127 println!("world");
14128 }
14129 "#
14130 .unindent(),
14131 );
14132
14133 cx.update_editor(|editor, window, cx| {
14134 editor.go_to_next_hunk(&GoToHunk, window, cx);
14135 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14136 });
14137 executor.run_until_parked();
14138 cx.assert_state_with_diff(
14139 r#"
14140 - use some::mod;
14141 + use some::modified;
14142
14143 - const A: u32 = 42;
14144 ˇ
14145 fn main() {
14146 - println!("hello");
14147 + println!("hello there");
14148
14149 + println!("around the");
14150 println!("world");
14151 }
14152 "#
14153 .unindent(),
14154 );
14155
14156 cx.update_editor(|editor, window, cx| {
14157 editor.cancel(&Cancel, window, cx);
14158 });
14159
14160 cx.assert_state_with_diff(
14161 r#"
14162 use some::modified;
14163
14164 ˇ
14165 fn main() {
14166 println!("hello there");
14167
14168 println!("around the");
14169 println!("world");
14170 }
14171 "#
14172 .unindent(),
14173 );
14174}
14175
14176#[gpui::test]
14177async fn test_diff_base_change_with_expanded_diff_hunks(
14178 executor: BackgroundExecutor,
14179 cx: &mut TestAppContext,
14180) {
14181 init_test(cx, |_| {});
14182
14183 let mut cx = EditorTestContext::new(cx).await;
14184
14185 let diff_base = r#"
14186 use some::mod1;
14187 use some::mod2;
14188
14189 const A: u32 = 42;
14190 const B: u32 = 42;
14191 const C: u32 = 42;
14192
14193 fn main() {
14194 println!("hello");
14195
14196 println!("world");
14197 }
14198 "#
14199 .unindent();
14200
14201 cx.set_state(
14202 &r#"
14203 use some::mod2;
14204
14205 const A: u32 = 42;
14206 const C: u32 = 42;
14207
14208 fn main(ˇ) {
14209 //println!("hello");
14210
14211 println!("world");
14212 //
14213 //
14214 }
14215 "#
14216 .unindent(),
14217 );
14218
14219 cx.set_head_text(&diff_base);
14220 executor.run_until_parked();
14221
14222 cx.update_editor(|editor, window, cx| {
14223 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14224 });
14225 executor.run_until_parked();
14226 cx.assert_state_with_diff(
14227 r#"
14228 - use some::mod1;
14229 use some::mod2;
14230
14231 const A: u32 = 42;
14232 - const B: u32 = 42;
14233 const C: u32 = 42;
14234
14235 fn main(ˇ) {
14236 - println!("hello");
14237 + //println!("hello");
14238
14239 println!("world");
14240 + //
14241 + //
14242 }
14243 "#
14244 .unindent(),
14245 );
14246
14247 cx.set_head_text("new diff base!");
14248 executor.run_until_parked();
14249 cx.assert_state_with_diff(
14250 r#"
14251 - new diff base!
14252 + use some::mod2;
14253 +
14254 + const A: u32 = 42;
14255 + const C: u32 = 42;
14256 +
14257 + fn main(ˇ) {
14258 + //println!("hello");
14259 +
14260 + println!("world");
14261 + //
14262 + //
14263 + }
14264 "#
14265 .unindent(),
14266 );
14267}
14268
14269#[gpui::test]
14270async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
14271 init_test(cx, |_| {});
14272
14273 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
14274 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
14275 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
14276 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
14277 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
14278 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
14279
14280 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
14281 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
14282 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
14283
14284 let multi_buffer = cx.new(|cx| {
14285 let mut multibuffer = MultiBuffer::new(ReadWrite);
14286 multibuffer.push_excerpts(
14287 buffer_1.clone(),
14288 [
14289 ExcerptRange {
14290 context: Point::new(0, 0)..Point::new(3, 0),
14291 primary: None,
14292 },
14293 ExcerptRange {
14294 context: Point::new(5, 0)..Point::new(7, 0),
14295 primary: None,
14296 },
14297 ExcerptRange {
14298 context: Point::new(9, 0)..Point::new(10, 3),
14299 primary: None,
14300 },
14301 ],
14302 cx,
14303 );
14304 multibuffer.push_excerpts(
14305 buffer_2.clone(),
14306 [
14307 ExcerptRange {
14308 context: Point::new(0, 0)..Point::new(3, 0),
14309 primary: None,
14310 },
14311 ExcerptRange {
14312 context: Point::new(5, 0)..Point::new(7, 0),
14313 primary: None,
14314 },
14315 ExcerptRange {
14316 context: Point::new(9, 0)..Point::new(10, 3),
14317 primary: None,
14318 },
14319 ],
14320 cx,
14321 );
14322 multibuffer.push_excerpts(
14323 buffer_3.clone(),
14324 [
14325 ExcerptRange {
14326 context: Point::new(0, 0)..Point::new(3, 0),
14327 primary: None,
14328 },
14329 ExcerptRange {
14330 context: Point::new(5, 0)..Point::new(7, 0),
14331 primary: None,
14332 },
14333 ExcerptRange {
14334 context: Point::new(9, 0)..Point::new(10, 3),
14335 primary: None,
14336 },
14337 ],
14338 cx,
14339 );
14340 multibuffer
14341 });
14342
14343 let editor =
14344 cx.add_window(|window, cx| Editor::new(EditorMode::Full, multi_buffer, None, window, cx));
14345 editor
14346 .update(cx, |editor, _window, cx| {
14347 for (buffer, diff_base) in [
14348 (buffer_1.clone(), file_1_old),
14349 (buffer_2.clone(), file_2_old),
14350 (buffer_3.clone(), file_3_old),
14351 ] {
14352 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
14353 editor
14354 .buffer
14355 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
14356 }
14357 })
14358 .unwrap();
14359
14360 let mut cx = EditorTestContext::for_editor(editor, cx).await;
14361 cx.run_until_parked();
14362
14363 cx.assert_editor_state(
14364 &"
14365 ˇaaa
14366 ccc
14367 ddd
14368
14369 ggg
14370 hhh
14371
14372
14373 lll
14374 mmm
14375 NNN
14376
14377 qqq
14378 rrr
14379
14380 uuu
14381 111
14382 222
14383 333
14384
14385 666
14386 777
14387
14388 000
14389 !!!"
14390 .unindent(),
14391 );
14392
14393 cx.update_editor(|editor, window, cx| {
14394 editor.select_all(&SelectAll, window, cx);
14395 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14396 });
14397 cx.executor().run_until_parked();
14398
14399 cx.assert_state_with_diff(
14400 "
14401 «aaa
14402 - bbb
14403 ccc
14404 ddd
14405
14406 ggg
14407 hhh
14408
14409
14410 lll
14411 mmm
14412 - nnn
14413 + NNN
14414
14415 qqq
14416 rrr
14417
14418 uuu
14419 111
14420 222
14421 333
14422
14423 + 666
14424 777
14425
14426 000
14427 !!!ˇ»"
14428 .unindent(),
14429 );
14430}
14431
14432#[gpui::test]
14433async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
14434 init_test(cx, |_| {});
14435
14436 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
14437 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
14438
14439 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
14440 let multi_buffer = cx.new(|cx| {
14441 let mut multibuffer = MultiBuffer::new(ReadWrite);
14442 multibuffer.push_excerpts(
14443 buffer.clone(),
14444 [
14445 ExcerptRange {
14446 context: Point::new(0, 0)..Point::new(2, 0),
14447 primary: None,
14448 },
14449 ExcerptRange {
14450 context: Point::new(4, 0)..Point::new(7, 0),
14451 primary: None,
14452 },
14453 ExcerptRange {
14454 context: Point::new(9, 0)..Point::new(10, 0),
14455 primary: None,
14456 },
14457 ],
14458 cx,
14459 );
14460 multibuffer
14461 });
14462
14463 let editor =
14464 cx.add_window(|window, cx| Editor::new(EditorMode::Full, multi_buffer, None, window, cx));
14465 editor
14466 .update(cx, |editor, _window, cx| {
14467 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
14468 editor
14469 .buffer
14470 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
14471 })
14472 .unwrap();
14473
14474 let mut cx = EditorTestContext::for_editor(editor, cx).await;
14475 cx.run_until_parked();
14476
14477 cx.update_editor(|editor, window, cx| {
14478 editor.expand_all_diff_hunks(&Default::default(), window, cx)
14479 });
14480 cx.executor().run_until_parked();
14481
14482 // When the start of a hunk coincides with the start of its excerpt,
14483 // the hunk is expanded. When the start of a a hunk is earlier than
14484 // the start of its excerpt, the hunk is not expanded.
14485 cx.assert_state_with_diff(
14486 "
14487 ˇaaa
14488 - bbb
14489 + BBB
14490
14491 - ddd
14492 - eee
14493 + DDD
14494 + EEE
14495 fff
14496
14497 iii
14498 "
14499 .unindent(),
14500 );
14501}
14502
14503#[gpui::test]
14504async fn test_edits_around_expanded_insertion_hunks(
14505 executor: BackgroundExecutor,
14506 cx: &mut TestAppContext,
14507) {
14508 init_test(cx, |_| {});
14509
14510 let mut cx = EditorTestContext::new(cx).await;
14511
14512 let diff_base = r#"
14513 use some::mod1;
14514 use some::mod2;
14515
14516 const A: u32 = 42;
14517
14518 fn main() {
14519 println!("hello");
14520
14521 println!("world");
14522 }
14523 "#
14524 .unindent();
14525 executor.run_until_parked();
14526 cx.set_state(
14527 &r#"
14528 use some::mod1;
14529 use some::mod2;
14530
14531 const A: u32 = 42;
14532 const B: u32 = 42;
14533 const C: u32 = 42;
14534 ˇ
14535
14536 fn main() {
14537 println!("hello");
14538
14539 println!("world");
14540 }
14541 "#
14542 .unindent(),
14543 );
14544
14545 cx.set_head_text(&diff_base);
14546 executor.run_until_parked();
14547
14548 cx.update_editor(|editor, window, cx| {
14549 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14550 });
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 + ˇ
14562
14563 fn main() {
14564 println!("hello");
14565
14566 println!("world");
14567 }
14568 "#
14569 .unindent(),
14570 );
14571
14572 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
14573 executor.run_until_parked();
14574
14575 cx.assert_state_with_diff(
14576 r#"
14577 use some::mod1;
14578 use some::mod2;
14579
14580 const A: u32 = 42;
14581 + const B: u32 = 42;
14582 + const C: u32 = 42;
14583 + const D: u32 = 42;
14584 + ˇ
14585
14586 fn main() {
14587 println!("hello");
14588
14589 println!("world");
14590 }
14591 "#
14592 .unindent(),
14593 );
14594
14595 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
14596 executor.run_until_parked();
14597
14598 cx.assert_state_with_diff(
14599 r#"
14600 use some::mod1;
14601 use some::mod2;
14602
14603 const A: u32 = 42;
14604 + const B: u32 = 42;
14605 + const C: u32 = 42;
14606 + const D: u32 = 42;
14607 + const E: u32 = 42;
14608 + ˇ
14609
14610 fn main() {
14611 println!("hello");
14612
14613 println!("world");
14614 }
14615 "#
14616 .unindent(),
14617 );
14618
14619 cx.update_editor(|editor, window, cx| {
14620 editor.delete_line(&DeleteLine, window, cx);
14621 });
14622 executor.run_until_parked();
14623
14624 cx.assert_state_with_diff(
14625 r#"
14626 use some::mod1;
14627 use some::mod2;
14628
14629 const A: u32 = 42;
14630 + const B: u32 = 42;
14631 + const C: u32 = 42;
14632 + const D: u32 = 42;
14633 + const E: u32 = 42;
14634 ˇ
14635 fn main() {
14636 println!("hello");
14637
14638 println!("world");
14639 }
14640 "#
14641 .unindent(),
14642 );
14643
14644 cx.update_editor(|editor, window, cx| {
14645 editor.move_up(&MoveUp, window, cx);
14646 editor.delete_line(&DeleteLine, window, cx);
14647 editor.move_up(&MoveUp, window, cx);
14648 editor.delete_line(&DeleteLine, window, cx);
14649 editor.move_up(&MoveUp, window, cx);
14650 editor.delete_line(&DeleteLine, window, cx);
14651 });
14652 executor.run_until_parked();
14653 cx.assert_state_with_diff(
14654 r#"
14655 use some::mod1;
14656 use some::mod2;
14657
14658 const A: u32 = 42;
14659 + const B: u32 = 42;
14660 ˇ
14661 fn main() {
14662 println!("hello");
14663
14664 println!("world");
14665 }
14666 "#
14667 .unindent(),
14668 );
14669
14670 cx.update_editor(|editor, window, cx| {
14671 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
14672 editor.delete_line(&DeleteLine, window, cx);
14673 });
14674 executor.run_until_parked();
14675 cx.assert_state_with_diff(
14676 r#"
14677 ˇ
14678 fn main() {
14679 println!("hello");
14680
14681 println!("world");
14682 }
14683 "#
14684 .unindent(),
14685 );
14686}
14687
14688#[gpui::test]
14689async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
14690 init_test(cx, |_| {});
14691
14692 let mut cx = EditorTestContext::new(cx).await;
14693 cx.set_head_text(indoc! { "
14694 one
14695 two
14696 three
14697 four
14698 five
14699 "
14700 });
14701 cx.set_state(indoc! { "
14702 one
14703 ˇthree
14704 five
14705 "});
14706 cx.run_until_parked();
14707 cx.update_editor(|editor, window, cx| {
14708 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14709 });
14710 cx.assert_state_with_diff(
14711 indoc! { "
14712 one
14713 - two
14714 ˇthree
14715 - four
14716 five
14717 "}
14718 .to_string(),
14719 );
14720 cx.update_editor(|editor, window, cx| {
14721 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14722 });
14723
14724 cx.assert_state_with_diff(
14725 indoc! { "
14726 one
14727 ˇthree
14728 five
14729 "}
14730 .to_string(),
14731 );
14732
14733 cx.set_state(indoc! { "
14734 one
14735 ˇTWO
14736 three
14737 four
14738 five
14739 "});
14740 cx.run_until_parked();
14741 cx.update_editor(|editor, window, cx| {
14742 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14743 });
14744
14745 cx.assert_state_with_diff(
14746 indoc! { "
14747 one
14748 - two
14749 + ˇTWO
14750 three
14751 four
14752 five
14753 "}
14754 .to_string(),
14755 );
14756 cx.update_editor(|editor, window, cx| {
14757 editor.move_up(&Default::default(), window, cx);
14758 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14759 });
14760 cx.assert_state_with_diff(
14761 indoc! { "
14762 one
14763 ˇTWO
14764 three
14765 four
14766 five
14767 "}
14768 .to_string(),
14769 );
14770}
14771
14772#[gpui::test]
14773async fn test_edits_around_expanded_deletion_hunks(
14774 executor: BackgroundExecutor,
14775 cx: &mut TestAppContext,
14776) {
14777 init_test(cx, |_| {});
14778
14779 let mut cx = EditorTestContext::new(cx).await;
14780
14781 let diff_base = r#"
14782 use some::mod1;
14783 use some::mod2;
14784
14785 const A: u32 = 42;
14786 const B: u32 = 42;
14787 const C: u32 = 42;
14788
14789
14790 fn main() {
14791 println!("hello");
14792
14793 println!("world");
14794 }
14795 "#
14796 .unindent();
14797 executor.run_until_parked();
14798 cx.set_state(
14799 &r#"
14800 use some::mod1;
14801 use some::mod2;
14802
14803 ˇconst B: u32 = 42;
14804 const C: u32 = 42;
14805
14806
14807 fn main() {
14808 println!("hello");
14809
14810 println!("world");
14811 }
14812 "#
14813 .unindent(),
14814 );
14815
14816 cx.set_head_text(&diff_base);
14817 executor.run_until_parked();
14818
14819 cx.update_editor(|editor, window, cx| {
14820 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14821 });
14822 executor.run_until_parked();
14823
14824 cx.assert_state_with_diff(
14825 r#"
14826 use some::mod1;
14827 use some::mod2;
14828
14829 - const A: u32 = 42;
14830 ˇconst B: u32 = 42;
14831 const C: u32 = 42;
14832
14833
14834 fn main() {
14835 println!("hello");
14836
14837 println!("world");
14838 }
14839 "#
14840 .unindent(),
14841 );
14842
14843 cx.update_editor(|editor, window, cx| {
14844 editor.delete_line(&DeleteLine, window, cx);
14845 });
14846 executor.run_until_parked();
14847 cx.assert_state_with_diff(
14848 r#"
14849 use some::mod1;
14850 use some::mod2;
14851
14852 - const A: u32 = 42;
14853 - const B: u32 = 42;
14854 ˇconst C: u32 = 42;
14855
14856
14857 fn main() {
14858 println!("hello");
14859
14860 println!("world");
14861 }
14862 "#
14863 .unindent(),
14864 );
14865
14866 cx.update_editor(|editor, window, cx| {
14867 editor.delete_line(&DeleteLine, window, cx);
14868 });
14869 executor.run_until_parked();
14870 cx.assert_state_with_diff(
14871 r#"
14872 use some::mod1;
14873 use some::mod2;
14874
14875 - const A: u32 = 42;
14876 - const B: u32 = 42;
14877 - const C: u32 = 42;
14878 ˇ
14879
14880 fn main() {
14881 println!("hello");
14882
14883 println!("world");
14884 }
14885 "#
14886 .unindent(),
14887 );
14888
14889 cx.update_editor(|editor, window, cx| {
14890 editor.handle_input("replacement", window, cx);
14891 });
14892 executor.run_until_parked();
14893 cx.assert_state_with_diff(
14894 r#"
14895 use some::mod1;
14896 use some::mod2;
14897
14898 - const A: u32 = 42;
14899 - const B: u32 = 42;
14900 - const C: u32 = 42;
14901 -
14902 + replacementˇ
14903
14904 fn main() {
14905 println!("hello");
14906
14907 println!("world");
14908 }
14909 "#
14910 .unindent(),
14911 );
14912}
14913
14914#[gpui::test]
14915async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14916 init_test(cx, |_| {});
14917
14918 let mut cx = EditorTestContext::new(cx).await;
14919
14920 let base_text = r#"
14921 one
14922 two
14923 three
14924 four
14925 five
14926 "#
14927 .unindent();
14928 executor.run_until_parked();
14929 cx.set_state(
14930 &r#"
14931 one
14932 two
14933 fˇour
14934 five
14935 "#
14936 .unindent(),
14937 );
14938
14939 cx.set_head_text(&base_text);
14940 executor.run_until_parked();
14941
14942 cx.update_editor(|editor, window, cx| {
14943 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14944 });
14945 executor.run_until_parked();
14946
14947 cx.assert_state_with_diff(
14948 r#"
14949 one
14950 two
14951 - three
14952 fˇour
14953 five
14954 "#
14955 .unindent(),
14956 );
14957
14958 cx.update_editor(|editor, window, cx| {
14959 editor.backspace(&Backspace, window, cx);
14960 editor.backspace(&Backspace, window, cx);
14961 });
14962 executor.run_until_parked();
14963 cx.assert_state_with_diff(
14964 r#"
14965 one
14966 two
14967 - threeˇ
14968 - four
14969 + our
14970 five
14971 "#
14972 .unindent(),
14973 );
14974}
14975
14976#[gpui::test]
14977async fn test_edit_after_expanded_modification_hunk(
14978 executor: BackgroundExecutor,
14979 cx: &mut TestAppContext,
14980) {
14981 init_test(cx, |_| {});
14982
14983 let mut cx = EditorTestContext::new(cx).await;
14984
14985 let diff_base = r#"
14986 use some::mod1;
14987 use some::mod2;
14988
14989 const A: u32 = 42;
14990 const B: u32 = 42;
14991 const C: u32 = 42;
14992 const D: u32 = 42;
14993
14994
14995 fn main() {
14996 println!("hello");
14997
14998 println!("world");
14999 }"#
15000 .unindent();
15001
15002 cx.set_state(
15003 &r#"
15004 use some::mod1;
15005 use some::mod2;
15006
15007 const A: u32 = 42;
15008 const B: u32 = 42;
15009 const C: u32 = 43ˇ
15010 const D: u32 = 42;
15011
15012
15013 fn main() {
15014 println!("hello");
15015
15016 println!("world");
15017 }"#
15018 .unindent(),
15019 );
15020
15021 cx.set_head_text(&diff_base);
15022 executor.run_until_parked();
15023 cx.update_editor(|editor, window, cx| {
15024 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15025 });
15026 executor.run_until_parked();
15027
15028 cx.assert_state_with_diff(
15029 r#"
15030 use some::mod1;
15031 use some::mod2;
15032
15033 const A: u32 = 42;
15034 const B: u32 = 42;
15035 - const C: u32 = 42;
15036 + const C: u32 = 43ˇ
15037 const D: u32 = 42;
15038
15039
15040 fn main() {
15041 println!("hello");
15042
15043 println!("world");
15044 }"#
15045 .unindent(),
15046 );
15047
15048 cx.update_editor(|editor, window, cx| {
15049 editor.handle_input("\nnew_line\n", window, cx);
15050 });
15051 executor.run_until_parked();
15052
15053 cx.assert_state_with_diff(
15054 r#"
15055 use some::mod1;
15056 use some::mod2;
15057
15058 const A: u32 = 42;
15059 const B: u32 = 42;
15060 - const C: u32 = 42;
15061 + const C: u32 = 43
15062 + new_line
15063 + ˇ
15064 const D: u32 = 42;
15065
15066
15067 fn main() {
15068 println!("hello");
15069
15070 println!("world");
15071 }"#
15072 .unindent(),
15073 );
15074}
15075
15076#[gpui::test]
15077async fn test_stage_and_unstage_added_file_hunk(
15078 executor: BackgroundExecutor,
15079 cx: &mut TestAppContext,
15080) {
15081 init_test(cx, |_| {});
15082
15083 let mut cx = EditorTestContext::new(cx).await;
15084 cx.update_editor(|editor, _, cx| {
15085 editor.set_expand_all_diff_hunks(cx);
15086 });
15087
15088 let working_copy = r#"
15089 ˇfn main() {
15090 println!("hello, world!");
15091 }
15092 "#
15093 .unindent();
15094
15095 cx.set_state(&working_copy);
15096 executor.run_until_parked();
15097
15098 cx.assert_state_with_diff(
15099 r#"
15100 + ˇfn main() {
15101 + println!("hello, world!");
15102 + }
15103 "#
15104 .unindent(),
15105 );
15106 cx.assert_index_text(None);
15107
15108 cx.update_editor(|editor, window, cx| {
15109 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
15110 });
15111 executor.run_until_parked();
15112 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
15113 cx.assert_state_with_diff(
15114 r#"
15115 + ˇfn main() {
15116 + println!("hello, world!");
15117 + }
15118 "#
15119 .unindent(),
15120 );
15121
15122 cx.update_editor(|editor, window, cx| {
15123 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
15124 });
15125 executor.run_until_parked();
15126 cx.assert_index_text(None);
15127}
15128
15129async fn setup_indent_guides_editor(
15130 text: &str,
15131 cx: &mut TestAppContext,
15132) -> (BufferId, EditorTestContext) {
15133 init_test(cx, |_| {});
15134
15135 let mut cx = EditorTestContext::new(cx).await;
15136
15137 let buffer_id = cx.update_editor(|editor, window, cx| {
15138 editor.set_text(text, window, cx);
15139 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
15140
15141 buffer_ids[0]
15142 });
15143
15144 (buffer_id, cx)
15145}
15146
15147fn assert_indent_guides(
15148 range: Range<u32>,
15149 expected: Vec<IndentGuide>,
15150 active_indices: Option<Vec<usize>>,
15151 cx: &mut EditorTestContext,
15152) {
15153 let indent_guides = cx.update_editor(|editor, window, cx| {
15154 let snapshot = editor.snapshot(window, cx).display_snapshot;
15155 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
15156 editor,
15157 MultiBufferRow(range.start)..MultiBufferRow(range.end),
15158 true,
15159 &snapshot,
15160 cx,
15161 );
15162
15163 indent_guides.sort_by(|a, b| {
15164 a.depth.cmp(&b.depth).then(
15165 a.start_row
15166 .cmp(&b.start_row)
15167 .then(a.end_row.cmp(&b.end_row)),
15168 )
15169 });
15170 indent_guides
15171 });
15172
15173 if let Some(expected) = active_indices {
15174 let active_indices = cx.update_editor(|editor, window, cx| {
15175 let snapshot = editor.snapshot(window, cx).display_snapshot;
15176 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
15177 });
15178
15179 assert_eq!(
15180 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
15181 expected,
15182 "Active indent guide indices do not match"
15183 );
15184 }
15185
15186 assert_eq!(indent_guides, expected, "Indent guides do not match");
15187}
15188
15189fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
15190 IndentGuide {
15191 buffer_id,
15192 start_row: MultiBufferRow(start_row),
15193 end_row: MultiBufferRow(end_row),
15194 depth,
15195 tab_size: 4,
15196 settings: IndentGuideSettings {
15197 enabled: true,
15198 line_width: 1,
15199 active_line_width: 1,
15200 ..Default::default()
15201 },
15202 }
15203}
15204
15205#[gpui::test]
15206async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
15207 let (buffer_id, mut cx) = setup_indent_guides_editor(
15208 &"
15209 fn main() {
15210 let a = 1;
15211 }"
15212 .unindent(),
15213 cx,
15214 )
15215 .await;
15216
15217 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
15218}
15219
15220#[gpui::test]
15221async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
15222 let (buffer_id, mut cx) = setup_indent_guides_editor(
15223 &"
15224 fn main() {
15225 let a = 1;
15226 let b = 2;
15227 }"
15228 .unindent(),
15229 cx,
15230 )
15231 .await;
15232
15233 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
15234}
15235
15236#[gpui::test]
15237async fn test_indent_guide_nested(cx: &mut TestAppContext) {
15238 let (buffer_id, mut cx) = setup_indent_guides_editor(
15239 &"
15240 fn main() {
15241 let a = 1;
15242 if a == 3 {
15243 let b = 2;
15244 } else {
15245 let c = 3;
15246 }
15247 }"
15248 .unindent(),
15249 cx,
15250 )
15251 .await;
15252
15253 assert_indent_guides(
15254 0..8,
15255 vec![
15256 indent_guide(buffer_id, 1, 6, 0),
15257 indent_guide(buffer_id, 3, 3, 1),
15258 indent_guide(buffer_id, 5, 5, 1),
15259 ],
15260 None,
15261 &mut cx,
15262 );
15263}
15264
15265#[gpui::test]
15266async fn test_indent_guide_tab(cx: &mut TestAppContext) {
15267 let (buffer_id, mut cx) = setup_indent_guides_editor(
15268 &"
15269 fn main() {
15270 let a = 1;
15271 let b = 2;
15272 let c = 3;
15273 }"
15274 .unindent(),
15275 cx,
15276 )
15277 .await;
15278
15279 assert_indent_guides(
15280 0..5,
15281 vec![
15282 indent_guide(buffer_id, 1, 3, 0),
15283 indent_guide(buffer_id, 2, 2, 1),
15284 ],
15285 None,
15286 &mut cx,
15287 );
15288}
15289
15290#[gpui::test]
15291async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
15292 let (buffer_id, mut cx) = setup_indent_guides_editor(
15293 &"
15294 fn main() {
15295 let a = 1;
15296
15297 let c = 3;
15298 }"
15299 .unindent(),
15300 cx,
15301 )
15302 .await;
15303
15304 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
15305}
15306
15307#[gpui::test]
15308async fn test_indent_guide_complex(cx: &mut TestAppContext) {
15309 let (buffer_id, mut cx) = setup_indent_guides_editor(
15310 &"
15311 fn main() {
15312 let a = 1;
15313
15314 let c = 3;
15315
15316 if a == 3 {
15317 let b = 2;
15318 } else {
15319 let c = 3;
15320 }
15321 }"
15322 .unindent(),
15323 cx,
15324 )
15325 .await;
15326
15327 assert_indent_guides(
15328 0..11,
15329 vec![
15330 indent_guide(buffer_id, 1, 9, 0),
15331 indent_guide(buffer_id, 6, 6, 1),
15332 indent_guide(buffer_id, 8, 8, 1),
15333 ],
15334 None,
15335 &mut cx,
15336 );
15337}
15338
15339#[gpui::test]
15340async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
15341 let (buffer_id, mut cx) = setup_indent_guides_editor(
15342 &"
15343 fn main() {
15344 let a = 1;
15345
15346 let c = 3;
15347
15348 if a == 3 {
15349 let b = 2;
15350 } else {
15351 let c = 3;
15352 }
15353 }"
15354 .unindent(),
15355 cx,
15356 )
15357 .await;
15358
15359 assert_indent_guides(
15360 1..11,
15361 vec![
15362 indent_guide(buffer_id, 1, 9, 0),
15363 indent_guide(buffer_id, 6, 6, 1),
15364 indent_guide(buffer_id, 8, 8, 1),
15365 ],
15366 None,
15367 &mut cx,
15368 );
15369}
15370
15371#[gpui::test]
15372async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
15373 let (buffer_id, mut cx) = setup_indent_guides_editor(
15374 &"
15375 fn main() {
15376 let a = 1;
15377
15378 let c = 3;
15379
15380 if a == 3 {
15381 let b = 2;
15382 } else {
15383 let c = 3;
15384 }
15385 }"
15386 .unindent(),
15387 cx,
15388 )
15389 .await;
15390
15391 assert_indent_guides(
15392 1..10,
15393 vec![
15394 indent_guide(buffer_id, 1, 9, 0),
15395 indent_guide(buffer_id, 6, 6, 1),
15396 indent_guide(buffer_id, 8, 8, 1),
15397 ],
15398 None,
15399 &mut cx,
15400 );
15401}
15402
15403#[gpui::test]
15404async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
15405 let (buffer_id, mut cx) = setup_indent_guides_editor(
15406 &"
15407 block1
15408 block2
15409 block3
15410 block4
15411 block2
15412 block1
15413 block1"
15414 .unindent(),
15415 cx,
15416 )
15417 .await;
15418
15419 assert_indent_guides(
15420 1..10,
15421 vec![
15422 indent_guide(buffer_id, 1, 4, 0),
15423 indent_guide(buffer_id, 2, 3, 1),
15424 indent_guide(buffer_id, 3, 3, 2),
15425 ],
15426 None,
15427 &mut cx,
15428 );
15429}
15430
15431#[gpui::test]
15432async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
15433 let (buffer_id, mut cx) = setup_indent_guides_editor(
15434 &"
15435 block1
15436 block2
15437 block3
15438
15439 block1
15440 block1"
15441 .unindent(),
15442 cx,
15443 )
15444 .await;
15445
15446 assert_indent_guides(
15447 0..6,
15448 vec![
15449 indent_guide(buffer_id, 1, 2, 0),
15450 indent_guide(buffer_id, 2, 2, 1),
15451 ],
15452 None,
15453 &mut cx,
15454 );
15455}
15456
15457#[gpui::test]
15458async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
15459 let (buffer_id, mut cx) = setup_indent_guides_editor(
15460 &"
15461 block1
15462
15463
15464
15465 block2
15466 "
15467 .unindent(),
15468 cx,
15469 )
15470 .await;
15471
15472 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
15473}
15474
15475#[gpui::test]
15476async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
15477 let (buffer_id, mut cx) = setup_indent_guides_editor(
15478 &"
15479 def a:
15480 \tb = 3
15481 \tif True:
15482 \t\tc = 4
15483 \t\td = 5
15484 \tprint(b)
15485 "
15486 .unindent(),
15487 cx,
15488 )
15489 .await;
15490
15491 assert_indent_guides(
15492 0..6,
15493 vec![
15494 indent_guide(buffer_id, 1, 6, 0),
15495 indent_guide(buffer_id, 3, 4, 1),
15496 ],
15497 None,
15498 &mut cx,
15499 );
15500}
15501
15502#[gpui::test]
15503async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
15504 let (buffer_id, mut cx) = setup_indent_guides_editor(
15505 &"
15506 fn main() {
15507 let a = 1;
15508 }"
15509 .unindent(),
15510 cx,
15511 )
15512 .await;
15513
15514 cx.update_editor(|editor, window, cx| {
15515 editor.change_selections(None, window, cx, |s| {
15516 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
15517 });
15518 });
15519
15520 assert_indent_guides(
15521 0..3,
15522 vec![indent_guide(buffer_id, 1, 1, 0)],
15523 Some(vec![0]),
15524 &mut cx,
15525 );
15526}
15527
15528#[gpui::test]
15529async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
15530 let (buffer_id, mut cx) = setup_indent_guides_editor(
15531 &"
15532 fn main() {
15533 if 1 == 2 {
15534 let a = 1;
15535 }
15536 }"
15537 .unindent(),
15538 cx,
15539 )
15540 .await;
15541
15542 cx.update_editor(|editor, window, cx| {
15543 editor.change_selections(None, window, cx, |s| {
15544 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
15545 });
15546 });
15547
15548 assert_indent_guides(
15549 0..4,
15550 vec![
15551 indent_guide(buffer_id, 1, 3, 0),
15552 indent_guide(buffer_id, 2, 2, 1),
15553 ],
15554 Some(vec![1]),
15555 &mut cx,
15556 );
15557
15558 cx.update_editor(|editor, window, cx| {
15559 editor.change_selections(None, window, cx, |s| {
15560 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
15561 });
15562 });
15563
15564 assert_indent_guides(
15565 0..4,
15566 vec![
15567 indent_guide(buffer_id, 1, 3, 0),
15568 indent_guide(buffer_id, 2, 2, 1),
15569 ],
15570 Some(vec![1]),
15571 &mut cx,
15572 );
15573
15574 cx.update_editor(|editor, window, cx| {
15575 editor.change_selections(None, window, cx, |s| {
15576 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
15577 });
15578 });
15579
15580 assert_indent_guides(
15581 0..4,
15582 vec![
15583 indent_guide(buffer_id, 1, 3, 0),
15584 indent_guide(buffer_id, 2, 2, 1),
15585 ],
15586 Some(vec![0]),
15587 &mut cx,
15588 );
15589}
15590
15591#[gpui::test]
15592async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
15593 let (buffer_id, mut cx) = setup_indent_guides_editor(
15594 &"
15595 fn main() {
15596 let a = 1;
15597
15598 let b = 2;
15599 }"
15600 .unindent(),
15601 cx,
15602 )
15603 .await;
15604
15605 cx.update_editor(|editor, window, cx| {
15606 editor.change_selections(None, window, cx, |s| {
15607 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
15608 });
15609 });
15610
15611 assert_indent_guides(
15612 0..5,
15613 vec![indent_guide(buffer_id, 1, 3, 0)],
15614 Some(vec![0]),
15615 &mut cx,
15616 );
15617}
15618
15619#[gpui::test]
15620async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
15621 let (buffer_id, mut cx) = setup_indent_guides_editor(
15622 &"
15623 def m:
15624 a = 1
15625 pass"
15626 .unindent(),
15627 cx,
15628 )
15629 .await;
15630
15631 cx.update_editor(|editor, window, cx| {
15632 editor.change_selections(None, window, cx, |s| {
15633 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
15634 });
15635 });
15636
15637 assert_indent_guides(
15638 0..3,
15639 vec![indent_guide(buffer_id, 1, 2, 0)],
15640 Some(vec![0]),
15641 &mut cx,
15642 );
15643}
15644
15645#[gpui::test]
15646async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
15647 init_test(cx, |_| {});
15648 let mut cx = EditorTestContext::new(cx).await;
15649 let text = indoc! {
15650 "
15651 impl A {
15652 fn b() {
15653 0;
15654 3;
15655 5;
15656 6;
15657 7;
15658 }
15659 }
15660 "
15661 };
15662 let base_text = indoc! {
15663 "
15664 impl A {
15665 fn b() {
15666 0;
15667 1;
15668 2;
15669 3;
15670 4;
15671 }
15672 fn c() {
15673 5;
15674 6;
15675 7;
15676 }
15677 }
15678 "
15679 };
15680
15681 cx.update_editor(|editor, window, cx| {
15682 editor.set_text(text, window, cx);
15683
15684 editor.buffer().update(cx, |multibuffer, cx| {
15685 let buffer = multibuffer.as_singleton().unwrap();
15686 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
15687
15688 multibuffer.set_all_diff_hunks_expanded(cx);
15689 multibuffer.add_diff(diff, cx);
15690
15691 buffer.read(cx).remote_id()
15692 })
15693 });
15694 cx.run_until_parked();
15695
15696 cx.assert_state_with_diff(
15697 indoc! { "
15698 impl A {
15699 fn b() {
15700 0;
15701 - 1;
15702 - 2;
15703 3;
15704 - 4;
15705 - }
15706 - fn c() {
15707 5;
15708 6;
15709 7;
15710 }
15711 }
15712 ˇ"
15713 }
15714 .to_string(),
15715 );
15716
15717 let mut actual_guides = cx.update_editor(|editor, window, cx| {
15718 editor
15719 .snapshot(window, cx)
15720 .buffer_snapshot
15721 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
15722 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
15723 .collect::<Vec<_>>()
15724 });
15725 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
15726 assert_eq!(
15727 actual_guides,
15728 vec![
15729 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
15730 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
15731 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
15732 ]
15733 );
15734}
15735
15736#[gpui::test]
15737async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15738 init_test(cx, |_| {});
15739 let mut cx = EditorTestContext::new(cx).await;
15740
15741 let diff_base = r#"
15742 a
15743 b
15744 c
15745 "#
15746 .unindent();
15747
15748 cx.set_state(
15749 &r#"
15750 ˇA
15751 b
15752 C
15753 "#
15754 .unindent(),
15755 );
15756 cx.set_head_text(&diff_base);
15757 cx.update_editor(|editor, window, cx| {
15758 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15759 });
15760 executor.run_until_parked();
15761
15762 let both_hunks_expanded = r#"
15763 - a
15764 + ˇA
15765 b
15766 - c
15767 + C
15768 "#
15769 .unindent();
15770
15771 cx.assert_state_with_diff(both_hunks_expanded.clone());
15772
15773 let hunk_ranges = cx.update_editor(|editor, window, cx| {
15774 let snapshot = editor.snapshot(window, cx);
15775 let hunks = editor
15776 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
15777 .collect::<Vec<_>>();
15778 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
15779 let buffer_id = hunks[0].buffer_id;
15780 hunks
15781 .into_iter()
15782 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
15783 .collect::<Vec<_>>()
15784 });
15785 assert_eq!(hunk_ranges.len(), 2);
15786
15787 cx.update_editor(|editor, _, cx| {
15788 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
15789 });
15790 executor.run_until_parked();
15791
15792 let second_hunk_expanded = r#"
15793 ˇA
15794 b
15795 - c
15796 + C
15797 "#
15798 .unindent();
15799
15800 cx.assert_state_with_diff(second_hunk_expanded);
15801
15802 cx.update_editor(|editor, _, cx| {
15803 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
15804 });
15805 executor.run_until_parked();
15806
15807 cx.assert_state_with_diff(both_hunks_expanded.clone());
15808
15809 cx.update_editor(|editor, _, cx| {
15810 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
15811 });
15812 executor.run_until_parked();
15813
15814 let first_hunk_expanded = r#"
15815 - a
15816 + ˇA
15817 b
15818 C
15819 "#
15820 .unindent();
15821
15822 cx.assert_state_with_diff(first_hunk_expanded);
15823
15824 cx.update_editor(|editor, _, cx| {
15825 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
15826 });
15827 executor.run_until_parked();
15828
15829 cx.assert_state_with_diff(both_hunks_expanded);
15830
15831 cx.set_state(
15832 &r#"
15833 ˇA
15834 b
15835 "#
15836 .unindent(),
15837 );
15838 cx.run_until_parked();
15839
15840 // TODO this cursor position seems bad
15841 cx.assert_state_with_diff(
15842 r#"
15843 - ˇa
15844 + A
15845 b
15846 "#
15847 .unindent(),
15848 );
15849
15850 cx.update_editor(|editor, window, cx| {
15851 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15852 });
15853
15854 cx.assert_state_with_diff(
15855 r#"
15856 - ˇa
15857 + A
15858 b
15859 - c
15860 "#
15861 .unindent(),
15862 );
15863
15864 let hunk_ranges = cx.update_editor(|editor, window, cx| {
15865 let snapshot = editor.snapshot(window, cx);
15866 let hunks = editor
15867 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
15868 .collect::<Vec<_>>();
15869 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
15870 let buffer_id = hunks[0].buffer_id;
15871 hunks
15872 .into_iter()
15873 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
15874 .collect::<Vec<_>>()
15875 });
15876 assert_eq!(hunk_ranges.len(), 2);
15877
15878 cx.update_editor(|editor, _, cx| {
15879 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
15880 });
15881 executor.run_until_parked();
15882
15883 cx.assert_state_with_diff(
15884 r#"
15885 - ˇa
15886 + A
15887 b
15888 "#
15889 .unindent(),
15890 );
15891}
15892
15893#[gpui::test]
15894async fn test_toggle_deletion_hunk_at_start_of_file(
15895 executor: BackgroundExecutor,
15896 cx: &mut TestAppContext,
15897) {
15898 init_test(cx, |_| {});
15899 let mut cx = EditorTestContext::new(cx).await;
15900
15901 let diff_base = r#"
15902 a
15903 b
15904 c
15905 "#
15906 .unindent();
15907
15908 cx.set_state(
15909 &r#"
15910 ˇb
15911 c
15912 "#
15913 .unindent(),
15914 );
15915 cx.set_head_text(&diff_base);
15916 cx.update_editor(|editor, window, cx| {
15917 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15918 });
15919 executor.run_until_parked();
15920
15921 let hunk_expanded = r#"
15922 - a
15923 ˇb
15924 c
15925 "#
15926 .unindent();
15927
15928 cx.assert_state_with_diff(hunk_expanded.clone());
15929
15930 let hunk_ranges = cx.update_editor(|editor, window, cx| {
15931 let snapshot = editor.snapshot(window, cx);
15932 let hunks = editor
15933 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
15934 .collect::<Vec<_>>();
15935 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
15936 let buffer_id = hunks[0].buffer_id;
15937 hunks
15938 .into_iter()
15939 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
15940 .collect::<Vec<_>>()
15941 });
15942 assert_eq!(hunk_ranges.len(), 1);
15943
15944 cx.update_editor(|editor, _, cx| {
15945 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
15946 });
15947 executor.run_until_parked();
15948
15949 let hunk_collapsed = r#"
15950 ˇb
15951 c
15952 "#
15953 .unindent();
15954
15955 cx.assert_state_with_diff(hunk_collapsed);
15956
15957 cx.update_editor(|editor, _, cx| {
15958 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
15959 });
15960 executor.run_until_parked();
15961
15962 cx.assert_state_with_diff(hunk_expanded.clone());
15963}
15964
15965#[gpui::test]
15966async fn test_display_diff_hunks(cx: &mut TestAppContext) {
15967 init_test(cx, |_| {});
15968
15969 let fs = FakeFs::new(cx.executor());
15970 fs.insert_tree(
15971 path!("/test"),
15972 json!({
15973 ".git": {},
15974 "file-1": "ONE\n",
15975 "file-2": "TWO\n",
15976 "file-3": "THREE\n",
15977 }),
15978 )
15979 .await;
15980
15981 fs.set_head_for_repo(
15982 path!("/test/.git").as_ref(),
15983 &[
15984 ("file-1".into(), "one\n".into()),
15985 ("file-2".into(), "two\n".into()),
15986 ("file-3".into(), "three\n".into()),
15987 ],
15988 );
15989
15990 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
15991 let mut buffers = vec![];
15992 for i in 1..=3 {
15993 let buffer = project
15994 .update(cx, |project, cx| {
15995 let path = format!(path!("/test/file-{}"), i);
15996 project.open_local_buffer(path, cx)
15997 })
15998 .await
15999 .unwrap();
16000 buffers.push(buffer);
16001 }
16002
16003 let multibuffer = cx.new(|cx| {
16004 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
16005 multibuffer.set_all_diff_hunks_expanded(cx);
16006 for buffer in &buffers {
16007 let snapshot = buffer.read(cx).snapshot();
16008 multibuffer.set_excerpts_for_path(
16009 PathKey::namespaced("", buffer.read(cx).file().unwrap().path().clone()),
16010 buffer.clone(),
16011 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
16012 DEFAULT_MULTIBUFFER_CONTEXT,
16013 cx,
16014 );
16015 }
16016 multibuffer
16017 });
16018
16019 let editor = cx.add_window(|window, cx| {
16020 Editor::new(EditorMode::Full, multibuffer, Some(project), window, cx)
16021 });
16022 cx.run_until_parked();
16023
16024 let snapshot = editor
16025 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16026 .unwrap();
16027 let hunks = snapshot
16028 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
16029 .map(|hunk| match hunk {
16030 DisplayDiffHunk::Unfolded {
16031 display_row_range, ..
16032 } => display_row_range,
16033 DisplayDiffHunk::Folded { .. } => unreachable!(),
16034 })
16035 .collect::<Vec<_>>();
16036 assert_eq!(
16037 hunks,
16038 [
16039 DisplayRow(2)..DisplayRow(4),
16040 DisplayRow(7)..DisplayRow(9),
16041 DisplayRow(12)..DisplayRow(14),
16042 ]
16043 );
16044}
16045
16046#[gpui::test]
16047async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
16048 init_test(cx, |_| {});
16049
16050 let mut cx = EditorTestContext::new(cx).await;
16051 cx.set_head_text(indoc! { "
16052 one
16053 two
16054 three
16055 four
16056 five
16057 "
16058 });
16059 cx.set_index_text(indoc! { "
16060 one
16061 two
16062 three
16063 four
16064 five
16065 "
16066 });
16067 cx.set_state(indoc! {"
16068 one
16069 TWO
16070 ˇTHREE
16071 FOUR
16072 five
16073 "});
16074 cx.run_until_parked();
16075 cx.update_editor(|editor, window, cx| {
16076 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16077 });
16078 cx.run_until_parked();
16079 cx.assert_index_text(Some(indoc! {"
16080 one
16081 TWO
16082 THREE
16083 FOUR
16084 five
16085 "}));
16086 cx.set_state(indoc! { "
16087 one
16088 TWO
16089 ˇTHREE-HUNDRED
16090 FOUR
16091 five
16092 "});
16093 cx.run_until_parked();
16094 cx.update_editor(|editor, window, cx| {
16095 let snapshot = editor.snapshot(window, cx);
16096 let hunks = editor
16097 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16098 .collect::<Vec<_>>();
16099 assert_eq!(hunks.len(), 1);
16100 assert_eq!(
16101 hunks[0].status(),
16102 DiffHunkStatus {
16103 kind: DiffHunkStatusKind::Modified,
16104 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
16105 }
16106 );
16107
16108 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16109 });
16110 cx.run_until_parked();
16111 cx.assert_index_text(Some(indoc! {"
16112 one
16113 TWO
16114 THREE-HUNDRED
16115 FOUR
16116 five
16117 "}));
16118}
16119
16120#[gpui::test]
16121fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
16122 init_test(cx, |_| {});
16123
16124 let editor = cx.add_window(|window, cx| {
16125 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
16126 build_editor(buffer, window, cx)
16127 });
16128
16129 let render_args = Arc::new(Mutex::new(None));
16130 let snapshot = editor
16131 .update(cx, |editor, window, cx| {
16132 let snapshot = editor.buffer().read(cx).snapshot(cx);
16133 let range =
16134 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
16135
16136 struct RenderArgs {
16137 row: MultiBufferRow,
16138 folded: bool,
16139 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
16140 }
16141
16142 let crease = Crease::inline(
16143 range,
16144 FoldPlaceholder::test(),
16145 {
16146 let toggle_callback = render_args.clone();
16147 move |row, folded, callback, _window, _cx| {
16148 *toggle_callback.lock() = Some(RenderArgs {
16149 row,
16150 folded,
16151 callback,
16152 });
16153 div()
16154 }
16155 },
16156 |_row, _folded, _window, _cx| div(),
16157 );
16158
16159 editor.insert_creases(Some(crease), cx);
16160 let snapshot = editor.snapshot(window, cx);
16161 let _div = snapshot.render_crease_toggle(
16162 MultiBufferRow(1),
16163 false,
16164 cx.entity().clone(),
16165 window,
16166 cx,
16167 );
16168 snapshot
16169 })
16170 .unwrap();
16171
16172 let render_args = render_args.lock().take().unwrap();
16173 assert_eq!(render_args.row, MultiBufferRow(1));
16174 assert!(!render_args.folded);
16175 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
16176
16177 cx.update_window(*editor, |_, window, cx| {
16178 (render_args.callback)(true, window, cx)
16179 })
16180 .unwrap();
16181 let snapshot = editor
16182 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16183 .unwrap();
16184 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
16185
16186 cx.update_window(*editor, |_, window, cx| {
16187 (render_args.callback)(false, window, cx)
16188 })
16189 .unwrap();
16190 let snapshot = editor
16191 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16192 .unwrap();
16193 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
16194}
16195
16196#[gpui::test]
16197async fn test_input_text(cx: &mut TestAppContext) {
16198 init_test(cx, |_| {});
16199 let mut cx = EditorTestContext::new(cx).await;
16200
16201 cx.set_state(
16202 &r#"ˇone
16203 two
16204
16205 three
16206 fourˇ
16207 five
16208
16209 siˇx"#
16210 .unindent(),
16211 );
16212
16213 cx.dispatch_action(HandleInput(String::new()));
16214 cx.assert_editor_state(
16215 &r#"ˇone
16216 two
16217
16218 three
16219 fourˇ
16220 five
16221
16222 siˇx"#
16223 .unindent(),
16224 );
16225
16226 cx.dispatch_action(HandleInput("AAAA".to_string()));
16227 cx.assert_editor_state(
16228 &r#"AAAAˇone
16229 two
16230
16231 three
16232 fourAAAAˇ
16233 five
16234
16235 siAAAAˇx"#
16236 .unindent(),
16237 );
16238}
16239
16240#[gpui::test]
16241async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
16242 init_test(cx, |_| {});
16243
16244 let mut cx = EditorTestContext::new(cx).await;
16245 cx.set_state(
16246 r#"let foo = 1;
16247let foo = 2;
16248let foo = 3;
16249let fooˇ = 4;
16250let foo = 5;
16251let foo = 6;
16252let foo = 7;
16253let foo = 8;
16254let foo = 9;
16255let foo = 10;
16256let foo = 11;
16257let foo = 12;
16258let foo = 13;
16259let foo = 14;
16260let foo = 15;"#,
16261 );
16262
16263 cx.update_editor(|e, window, cx| {
16264 assert_eq!(
16265 e.next_scroll_position,
16266 NextScrollCursorCenterTopBottom::Center,
16267 "Default next scroll direction is center",
16268 );
16269
16270 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16271 assert_eq!(
16272 e.next_scroll_position,
16273 NextScrollCursorCenterTopBottom::Top,
16274 "After center, next scroll direction should be top",
16275 );
16276
16277 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16278 assert_eq!(
16279 e.next_scroll_position,
16280 NextScrollCursorCenterTopBottom::Bottom,
16281 "After top, next scroll direction should be bottom",
16282 );
16283
16284 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16285 assert_eq!(
16286 e.next_scroll_position,
16287 NextScrollCursorCenterTopBottom::Center,
16288 "After bottom, scrolling should start over",
16289 );
16290
16291 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16292 assert_eq!(
16293 e.next_scroll_position,
16294 NextScrollCursorCenterTopBottom::Top,
16295 "Scrolling continues if retriggered fast enough"
16296 );
16297 });
16298
16299 cx.executor()
16300 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
16301 cx.executor().run_until_parked();
16302 cx.update_editor(|e, _, _| {
16303 assert_eq!(
16304 e.next_scroll_position,
16305 NextScrollCursorCenterTopBottom::Center,
16306 "If scrolling is not triggered fast enough, it should reset"
16307 );
16308 });
16309}
16310
16311#[gpui::test]
16312async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
16313 init_test(cx, |_| {});
16314 let mut cx = EditorLspTestContext::new_rust(
16315 lsp::ServerCapabilities {
16316 definition_provider: Some(lsp::OneOf::Left(true)),
16317 references_provider: Some(lsp::OneOf::Left(true)),
16318 ..lsp::ServerCapabilities::default()
16319 },
16320 cx,
16321 )
16322 .await;
16323
16324 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
16325 let go_to_definition = cx
16326 .lsp
16327 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
16328 move |params, _| async move {
16329 if empty_go_to_definition {
16330 Ok(None)
16331 } else {
16332 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
16333 uri: params.text_document_position_params.text_document.uri,
16334 range: lsp::Range::new(
16335 lsp::Position::new(4, 3),
16336 lsp::Position::new(4, 6),
16337 ),
16338 })))
16339 }
16340 },
16341 );
16342 let references = cx
16343 .lsp
16344 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
16345 Ok(Some(vec![lsp::Location {
16346 uri: params.text_document_position.text_document.uri,
16347 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
16348 }]))
16349 });
16350 (go_to_definition, references)
16351 };
16352
16353 cx.set_state(
16354 &r#"fn one() {
16355 let mut a = ˇtwo();
16356 }
16357
16358 fn two() {}"#
16359 .unindent(),
16360 );
16361 set_up_lsp_handlers(false, &mut cx);
16362 let navigated = cx
16363 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
16364 .await
16365 .expect("Failed to navigate to definition");
16366 assert_eq!(
16367 navigated,
16368 Navigated::Yes,
16369 "Should have navigated to definition from the GetDefinition response"
16370 );
16371 cx.assert_editor_state(
16372 &r#"fn one() {
16373 let mut a = two();
16374 }
16375
16376 fn «twoˇ»() {}"#
16377 .unindent(),
16378 );
16379
16380 let editors = cx.update_workspace(|workspace, _, cx| {
16381 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
16382 });
16383 cx.update_editor(|_, _, test_editor_cx| {
16384 assert_eq!(
16385 editors.len(),
16386 1,
16387 "Initially, only one, test, editor should be open in the workspace"
16388 );
16389 assert_eq!(
16390 test_editor_cx.entity(),
16391 editors.last().expect("Asserted len is 1").clone()
16392 );
16393 });
16394
16395 set_up_lsp_handlers(true, &mut cx);
16396 let navigated = cx
16397 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
16398 .await
16399 .expect("Failed to navigate to lookup references");
16400 assert_eq!(
16401 navigated,
16402 Navigated::Yes,
16403 "Should have navigated to references as a fallback after empty GoToDefinition response"
16404 );
16405 // We should not change the selections in the existing file,
16406 // if opening another milti buffer with the references
16407 cx.assert_editor_state(
16408 &r#"fn one() {
16409 let mut a = two();
16410 }
16411
16412 fn «twoˇ»() {}"#
16413 .unindent(),
16414 );
16415 let editors = cx.update_workspace(|workspace, _, cx| {
16416 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
16417 });
16418 cx.update_editor(|_, _, test_editor_cx| {
16419 assert_eq!(
16420 editors.len(),
16421 2,
16422 "After falling back to references search, we open a new editor with the results"
16423 );
16424 let references_fallback_text = editors
16425 .into_iter()
16426 .find(|new_editor| *new_editor != test_editor_cx.entity())
16427 .expect("Should have one non-test editor now")
16428 .read(test_editor_cx)
16429 .text(test_editor_cx);
16430 assert_eq!(
16431 references_fallback_text, "fn one() {\n let mut a = two();\n}",
16432 "Should use the range from the references response and not the GoToDefinition one"
16433 );
16434 });
16435}
16436
16437#[gpui::test]
16438async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
16439 init_test(cx, |_| {});
16440 cx.update(|cx| {
16441 let mut editor_settings = EditorSettings::get_global(cx).clone();
16442 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
16443 EditorSettings::override_global(editor_settings, cx);
16444 });
16445 let mut cx = EditorLspTestContext::new_rust(
16446 lsp::ServerCapabilities {
16447 definition_provider: Some(lsp::OneOf::Left(true)),
16448 references_provider: Some(lsp::OneOf::Left(true)),
16449 ..lsp::ServerCapabilities::default()
16450 },
16451 cx,
16452 )
16453 .await;
16454 let original_state = r#"fn one() {
16455 let mut a = ˇtwo();
16456 }
16457
16458 fn two() {}"#
16459 .unindent();
16460 cx.set_state(&original_state);
16461
16462 let mut go_to_definition = cx
16463 .lsp
16464 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
16465 move |_, _| async move { Ok(None) },
16466 );
16467 let _references = cx
16468 .lsp
16469 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
16470 panic!("Should not call for references with no go to definition fallback")
16471 });
16472
16473 let navigated = cx
16474 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
16475 .await
16476 .expect("Failed to navigate to lookup references");
16477 go_to_definition
16478 .next()
16479 .await
16480 .expect("Should have called the go_to_definition handler");
16481
16482 assert_eq!(
16483 navigated,
16484 Navigated::No,
16485 "Should have navigated to references as a fallback after empty GoToDefinition response"
16486 );
16487 cx.assert_editor_state(&original_state);
16488 let editors = cx.update_workspace(|workspace, _, cx| {
16489 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
16490 });
16491 cx.update_editor(|_, _, _| {
16492 assert_eq!(
16493 editors.len(),
16494 1,
16495 "After unsuccessful fallback, no other editor should have been opened"
16496 );
16497 });
16498}
16499
16500#[gpui::test]
16501async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
16502 init_test(cx, |_| {});
16503
16504 let language = Arc::new(Language::new(
16505 LanguageConfig::default(),
16506 Some(tree_sitter_rust::LANGUAGE.into()),
16507 ));
16508
16509 let text = r#"
16510 #[cfg(test)]
16511 mod tests() {
16512 #[test]
16513 fn runnable_1() {
16514 let a = 1;
16515 }
16516
16517 #[test]
16518 fn runnable_2() {
16519 let a = 1;
16520 let b = 2;
16521 }
16522 }
16523 "#
16524 .unindent();
16525
16526 let fs = FakeFs::new(cx.executor());
16527 fs.insert_file("/file.rs", Default::default()).await;
16528
16529 let project = Project::test(fs, ["/a".as_ref()], cx).await;
16530 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16531 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16532 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16533 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
16534
16535 let editor = cx.new_window_entity(|window, cx| {
16536 Editor::new(
16537 EditorMode::Full,
16538 multi_buffer,
16539 Some(project.clone()),
16540 window,
16541 cx,
16542 )
16543 });
16544
16545 editor.update_in(cx, |editor, window, cx| {
16546 let snapshot = editor.buffer().read(cx).snapshot(cx);
16547 editor.tasks.insert(
16548 (buffer.read(cx).remote_id(), 3),
16549 RunnableTasks {
16550 templates: vec![],
16551 offset: snapshot.anchor_before(43),
16552 column: 0,
16553 extra_variables: HashMap::default(),
16554 context_range: BufferOffset(43)..BufferOffset(85),
16555 },
16556 );
16557 editor.tasks.insert(
16558 (buffer.read(cx).remote_id(), 8),
16559 RunnableTasks {
16560 templates: vec![],
16561 offset: snapshot.anchor_before(86),
16562 column: 0,
16563 extra_variables: HashMap::default(),
16564 context_range: BufferOffset(86)..BufferOffset(191),
16565 },
16566 );
16567
16568 // Test finding task when cursor is inside function body
16569 editor.change_selections(None, window, cx, |s| {
16570 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
16571 });
16572 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
16573 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
16574
16575 // Test finding task when cursor is on function name
16576 editor.change_selections(None, window, cx, |s| {
16577 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
16578 });
16579 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
16580 assert_eq!(row, 8, "Should find task when cursor is on function name");
16581 });
16582}
16583
16584#[gpui::test]
16585async fn test_folding_buffers(cx: &mut TestAppContext) {
16586 init_test(cx, |_| {});
16587
16588 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
16589 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
16590 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
16591
16592 let fs = FakeFs::new(cx.executor());
16593 fs.insert_tree(
16594 path!("/a"),
16595 json!({
16596 "first.rs": sample_text_1,
16597 "second.rs": sample_text_2,
16598 "third.rs": sample_text_3,
16599 }),
16600 )
16601 .await;
16602 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16603 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16604 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16605 let worktree = project.update(cx, |project, cx| {
16606 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
16607 assert_eq!(worktrees.len(), 1);
16608 worktrees.pop().unwrap()
16609 });
16610 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
16611
16612 let buffer_1 = project
16613 .update(cx, |project, cx| {
16614 project.open_buffer((worktree_id, "first.rs"), cx)
16615 })
16616 .await
16617 .unwrap();
16618 let buffer_2 = project
16619 .update(cx, |project, cx| {
16620 project.open_buffer((worktree_id, "second.rs"), cx)
16621 })
16622 .await
16623 .unwrap();
16624 let buffer_3 = project
16625 .update(cx, |project, cx| {
16626 project.open_buffer((worktree_id, "third.rs"), cx)
16627 })
16628 .await
16629 .unwrap();
16630
16631 let multi_buffer = cx.new(|cx| {
16632 let mut multi_buffer = MultiBuffer::new(ReadWrite);
16633 multi_buffer.push_excerpts(
16634 buffer_1.clone(),
16635 [
16636 ExcerptRange {
16637 context: Point::new(0, 0)..Point::new(3, 0),
16638 primary: None,
16639 },
16640 ExcerptRange {
16641 context: Point::new(5, 0)..Point::new(7, 0),
16642 primary: None,
16643 },
16644 ExcerptRange {
16645 context: Point::new(9, 0)..Point::new(10, 4),
16646 primary: None,
16647 },
16648 ],
16649 cx,
16650 );
16651 multi_buffer.push_excerpts(
16652 buffer_2.clone(),
16653 [
16654 ExcerptRange {
16655 context: Point::new(0, 0)..Point::new(3, 0),
16656 primary: None,
16657 },
16658 ExcerptRange {
16659 context: Point::new(5, 0)..Point::new(7, 0),
16660 primary: None,
16661 },
16662 ExcerptRange {
16663 context: Point::new(9, 0)..Point::new(10, 4),
16664 primary: None,
16665 },
16666 ],
16667 cx,
16668 );
16669 multi_buffer.push_excerpts(
16670 buffer_3.clone(),
16671 [
16672 ExcerptRange {
16673 context: Point::new(0, 0)..Point::new(3, 0),
16674 primary: None,
16675 },
16676 ExcerptRange {
16677 context: Point::new(5, 0)..Point::new(7, 0),
16678 primary: None,
16679 },
16680 ExcerptRange {
16681 context: Point::new(9, 0)..Point::new(10, 4),
16682 primary: None,
16683 },
16684 ],
16685 cx,
16686 );
16687 multi_buffer
16688 });
16689 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16690 Editor::new(
16691 EditorMode::Full,
16692 multi_buffer.clone(),
16693 Some(project.clone()),
16694 window,
16695 cx,
16696 )
16697 });
16698
16699 assert_eq!(
16700 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16701 "\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",
16702 );
16703
16704 multi_buffer_editor.update(cx, |editor, cx| {
16705 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
16706 });
16707 assert_eq!(
16708 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16709 "\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",
16710 "After folding the first buffer, its text should not be displayed"
16711 );
16712
16713 multi_buffer_editor.update(cx, |editor, cx| {
16714 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
16715 });
16716 assert_eq!(
16717 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16718 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
16719 "After folding the second buffer, its text should not be displayed"
16720 );
16721
16722 multi_buffer_editor.update(cx, |editor, cx| {
16723 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
16724 });
16725 assert_eq!(
16726 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16727 "\n\n\n\n\n",
16728 "After folding the third buffer, its text should not be displayed"
16729 );
16730
16731 // Emulate selection inside the fold logic, that should work
16732 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16733 editor
16734 .snapshot(window, cx)
16735 .next_line_boundary(Point::new(0, 4));
16736 });
16737
16738 multi_buffer_editor.update(cx, |editor, cx| {
16739 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
16740 });
16741 assert_eq!(
16742 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16743 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
16744 "After unfolding the second buffer, its text should be displayed"
16745 );
16746
16747 // Typing inside of buffer 1 causes that buffer to be unfolded.
16748 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16749 assert_eq!(
16750 multi_buffer
16751 .read(cx)
16752 .snapshot(cx)
16753 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
16754 .collect::<String>(),
16755 "bbbb"
16756 );
16757 editor.change_selections(None, window, cx, |selections| {
16758 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
16759 });
16760 editor.handle_input("B", window, cx);
16761 });
16762
16763 assert_eq!(
16764 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16765 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
16766 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
16767 );
16768
16769 multi_buffer_editor.update(cx, |editor, cx| {
16770 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
16771 });
16772 assert_eq!(
16773 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16774 "\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",
16775 "After unfolding the all buffers, all original text should be displayed"
16776 );
16777}
16778
16779#[gpui::test]
16780async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
16781 init_test(cx, |_| {});
16782
16783 let sample_text_1 = "1111\n2222\n3333".to_string();
16784 let sample_text_2 = "4444\n5555\n6666".to_string();
16785 let sample_text_3 = "7777\n8888\n9999".to_string();
16786
16787 let fs = FakeFs::new(cx.executor());
16788 fs.insert_tree(
16789 path!("/a"),
16790 json!({
16791 "first.rs": sample_text_1,
16792 "second.rs": sample_text_2,
16793 "third.rs": sample_text_3,
16794 }),
16795 )
16796 .await;
16797 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16798 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16799 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16800 let worktree = project.update(cx, |project, cx| {
16801 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
16802 assert_eq!(worktrees.len(), 1);
16803 worktrees.pop().unwrap()
16804 });
16805 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
16806
16807 let buffer_1 = project
16808 .update(cx, |project, cx| {
16809 project.open_buffer((worktree_id, "first.rs"), cx)
16810 })
16811 .await
16812 .unwrap();
16813 let buffer_2 = project
16814 .update(cx, |project, cx| {
16815 project.open_buffer((worktree_id, "second.rs"), cx)
16816 })
16817 .await
16818 .unwrap();
16819 let buffer_3 = project
16820 .update(cx, |project, cx| {
16821 project.open_buffer((worktree_id, "third.rs"), cx)
16822 })
16823 .await
16824 .unwrap();
16825
16826 let multi_buffer = cx.new(|cx| {
16827 let mut multi_buffer = MultiBuffer::new(ReadWrite);
16828 multi_buffer.push_excerpts(
16829 buffer_1.clone(),
16830 [ExcerptRange {
16831 context: Point::new(0, 0)..Point::new(3, 0),
16832 primary: None,
16833 }],
16834 cx,
16835 );
16836 multi_buffer.push_excerpts(
16837 buffer_2.clone(),
16838 [ExcerptRange {
16839 context: Point::new(0, 0)..Point::new(3, 0),
16840 primary: None,
16841 }],
16842 cx,
16843 );
16844 multi_buffer.push_excerpts(
16845 buffer_3.clone(),
16846 [ExcerptRange {
16847 context: Point::new(0, 0)..Point::new(3, 0),
16848 primary: None,
16849 }],
16850 cx,
16851 );
16852 multi_buffer
16853 });
16854
16855 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16856 Editor::new(
16857 EditorMode::Full,
16858 multi_buffer,
16859 Some(project.clone()),
16860 window,
16861 cx,
16862 )
16863 });
16864
16865 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
16866 assert_eq!(
16867 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16868 full_text,
16869 );
16870
16871 multi_buffer_editor.update(cx, |editor, cx| {
16872 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
16873 });
16874 assert_eq!(
16875 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16876 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
16877 "After folding the first buffer, its text should not be displayed"
16878 );
16879
16880 multi_buffer_editor.update(cx, |editor, cx| {
16881 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
16882 });
16883
16884 assert_eq!(
16885 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16886 "\n\n\n\n\n\n7777\n8888\n9999",
16887 "After folding the second buffer, its text should not be displayed"
16888 );
16889
16890 multi_buffer_editor.update(cx, |editor, cx| {
16891 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
16892 });
16893 assert_eq!(
16894 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16895 "\n\n\n\n\n",
16896 "After folding the third buffer, its text should not be displayed"
16897 );
16898
16899 multi_buffer_editor.update(cx, |editor, cx| {
16900 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
16901 });
16902 assert_eq!(
16903 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16904 "\n\n\n\n4444\n5555\n6666\n\n",
16905 "After unfolding the second buffer, its text should be displayed"
16906 );
16907
16908 multi_buffer_editor.update(cx, |editor, cx| {
16909 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
16910 });
16911 assert_eq!(
16912 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16913 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
16914 "After unfolding the first buffer, its text should be displayed"
16915 );
16916
16917 multi_buffer_editor.update(cx, |editor, cx| {
16918 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
16919 });
16920 assert_eq!(
16921 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16922 full_text,
16923 "After unfolding all buffers, all original text should be displayed"
16924 );
16925}
16926
16927#[gpui::test]
16928async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
16929 init_test(cx, |_| {});
16930
16931 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
16932
16933 let fs = FakeFs::new(cx.executor());
16934 fs.insert_tree(
16935 path!("/a"),
16936 json!({
16937 "main.rs": sample_text,
16938 }),
16939 )
16940 .await;
16941 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16942 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16943 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16944 let worktree = project.update(cx, |project, cx| {
16945 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
16946 assert_eq!(worktrees.len(), 1);
16947 worktrees.pop().unwrap()
16948 });
16949 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
16950
16951 let buffer_1 = project
16952 .update(cx, |project, cx| {
16953 project.open_buffer((worktree_id, "main.rs"), cx)
16954 })
16955 .await
16956 .unwrap();
16957
16958 let multi_buffer = cx.new(|cx| {
16959 let mut multi_buffer = MultiBuffer::new(ReadWrite);
16960 multi_buffer.push_excerpts(
16961 buffer_1.clone(),
16962 [ExcerptRange {
16963 context: Point::new(0, 0)
16964 ..Point::new(
16965 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
16966 0,
16967 ),
16968 primary: None,
16969 }],
16970 cx,
16971 );
16972 multi_buffer
16973 });
16974 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16975 Editor::new(
16976 EditorMode::Full,
16977 multi_buffer,
16978 Some(project.clone()),
16979 window,
16980 cx,
16981 )
16982 });
16983
16984 let selection_range = Point::new(1, 0)..Point::new(2, 0);
16985 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16986 enum TestHighlight {}
16987 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
16988 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
16989 editor.highlight_text::<TestHighlight>(
16990 vec![highlight_range.clone()],
16991 HighlightStyle::color(Hsla::green()),
16992 cx,
16993 );
16994 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
16995 });
16996
16997 let full_text = format!("\n\n{sample_text}");
16998 assert_eq!(
16999 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17000 full_text,
17001 );
17002}
17003
17004#[gpui::test]
17005async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
17006 init_test(cx, |_| {});
17007 cx.update(|cx| {
17008 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
17009 "keymaps/default-linux.json",
17010 cx,
17011 )
17012 .unwrap();
17013 cx.bind_keys(default_key_bindings);
17014 });
17015
17016 let (editor, cx) = cx.add_window_view(|window, cx| {
17017 let multi_buffer = MultiBuffer::build_multi(
17018 [
17019 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
17020 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
17021 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
17022 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
17023 ],
17024 cx,
17025 );
17026 let mut editor = Editor::new(EditorMode::Full, multi_buffer.clone(), None, window, cx);
17027
17028 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
17029 // fold all but the second buffer, so that we test navigating between two
17030 // adjacent folded buffers, as well as folded buffers at the start and
17031 // end the multibuffer
17032 editor.fold_buffer(buffer_ids[0], cx);
17033 editor.fold_buffer(buffer_ids[2], cx);
17034 editor.fold_buffer(buffer_ids[3], cx);
17035
17036 editor
17037 });
17038 cx.simulate_resize(size(px(1000.), px(1000.)));
17039
17040 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
17041 cx.assert_excerpts_with_selections(indoc! {"
17042 [EXCERPT]
17043 ˇ[FOLDED]
17044 [EXCERPT]
17045 a1
17046 b1
17047 [EXCERPT]
17048 [FOLDED]
17049 [EXCERPT]
17050 [FOLDED]
17051 "
17052 });
17053 cx.simulate_keystroke("down");
17054 cx.assert_excerpts_with_selections(indoc! {"
17055 [EXCERPT]
17056 [FOLDED]
17057 [EXCERPT]
17058 ˇa1
17059 b1
17060 [EXCERPT]
17061 [FOLDED]
17062 [EXCERPT]
17063 [FOLDED]
17064 "
17065 });
17066 cx.simulate_keystroke("down");
17067 cx.assert_excerpts_with_selections(indoc! {"
17068 [EXCERPT]
17069 [FOLDED]
17070 [EXCERPT]
17071 a1
17072 ˇb1
17073 [EXCERPT]
17074 [FOLDED]
17075 [EXCERPT]
17076 [FOLDED]
17077 "
17078 });
17079 cx.simulate_keystroke("down");
17080 cx.assert_excerpts_with_selections(indoc! {"
17081 [EXCERPT]
17082 [FOLDED]
17083 [EXCERPT]
17084 a1
17085 b1
17086 ˇ[EXCERPT]
17087 [FOLDED]
17088 [EXCERPT]
17089 [FOLDED]
17090 "
17091 });
17092 cx.simulate_keystroke("down");
17093 cx.assert_excerpts_with_selections(indoc! {"
17094 [EXCERPT]
17095 [FOLDED]
17096 [EXCERPT]
17097 a1
17098 b1
17099 [EXCERPT]
17100 ˇ[FOLDED]
17101 [EXCERPT]
17102 [FOLDED]
17103 "
17104 });
17105 for _ in 0..5 {
17106 cx.simulate_keystroke("down");
17107 cx.assert_excerpts_with_selections(indoc! {"
17108 [EXCERPT]
17109 [FOLDED]
17110 [EXCERPT]
17111 a1
17112 b1
17113 [EXCERPT]
17114 [FOLDED]
17115 [EXCERPT]
17116 ˇ[FOLDED]
17117 "
17118 });
17119 }
17120
17121 cx.simulate_keystroke("up");
17122 cx.assert_excerpts_with_selections(indoc! {"
17123 [EXCERPT]
17124 [FOLDED]
17125 [EXCERPT]
17126 a1
17127 b1
17128 [EXCERPT]
17129 ˇ[FOLDED]
17130 [EXCERPT]
17131 [FOLDED]
17132 "
17133 });
17134 cx.simulate_keystroke("up");
17135 cx.assert_excerpts_with_selections(indoc! {"
17136 [EXCERPT]
17137 [FOLDED]
17138 [EXCERPT]
17139 a1
17140 b1
17141 ˇ[EXCERPT]
17142 [FOLDED]
17143 [EXCERPT]
17144 [FOLDED]
17145 "
17146 });
17147 cx.simulate_keystroke("up");
17148 cx.assert_excerpts_with_selections(indoc! {"
17149 [EXCERPT]
17150 [FOLDED]
17151 [EXCERPT]
17152 a1
17153 ˇb1
17154 [EXCERPT]
17155 [FOLDED]
17156 [EXCERPT]
17157 [FOLDED]
17158 "
17159 });
17160 cx.simulate_keystroke("up");
17161 cx.assert_excerpts_with_selections(indoc! {"
17162 [EXCERPT]
17163 [FOLDED]
17164 [EXCERPT]
17165 ˇa1
17166 b1
17167 [EXCERPT]
17168 [FOLDED]
17169 [EXCERPT]
17170 [FOLDED]
17171 "
17172 });
17173 for _ in 0..5 {
17174 cx.simulate_keystroke("up");
17175 cx.assert_excerpts_with_selections(indoc! {"
17176 [EXCERPT]
17177 ˇ[FOLDED]
17178 [EXCERPT]
17179 a1
17180 b1
17181 [EXCERPT]
17182 [FOLDED]
17183 [EXCERPT]
17184 [FOLDED]
17185 "
17186 });
17187 }
17188}
17189
17190#[gpui::test]
17191async fn test_inline_completion_text(cx: &mut TestAppContext) {
17192 init_test(cx, |_| {});
17193
17194 // Simple insertion
17195 assert_highlighted_edits(
17196 "Hello, world!",
17197 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
17198 true,
17199 cx,
17200 |highlighted_edits, cx| {
17201 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
17202 assert_eq!(highlighted_edits.highlights.len(), 1);
17203 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
17204 assert_eq!(
17205 highlighted_edits.highlights[0].1.background_color,
17206 Some(cx.theme().status().created_background)
17207 );
17208 },
17209 )
17210 .await;
17211
17212 // Replacement
17213 assert_highlighted_edits(
17214 "This is a test.",
17215 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
17216 false,
17217 cx,
17218 |highlighted_edits, cx| {
17219 assert_eq!(highlighted_edits.text, "That is a test.");
17220 assert_eq!(highlighted_edits.highlights.len(), 1);
17221 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
17222 assert_eq!(
17223 highlighted_edits.highlights[0].1.background_color,
17224 Some(cx.theme().status().created_background)
17225 );
17226 },
17227 )
17228 .await;
17229
17230 // Multiple edits
17231 assert_highlighted_edits(
17232 "Hello, world!",
17233 vec![
17234 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
17235 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
17236 ],
17237 false,
17238 cx,
17239 |highlighted_edits, cx| {
17240 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
17241 assert_eq!(highlighted_edits.highlights.len(), 2);
17242 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
17243 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
17244 assert_eq!(
17245 highlighted_edits.highlights[0].1.background_color,
17246 Some(cx.theme().status().created_background)
17247 );
17248 assert_eq!(
17249 highlighted_edits.highlights[1].1.background_color,
17250 Some(cx.theme().status().created_background)
17251 );
17252 },
17253 )
17254 .await;
17255
17256 // Multiple lines with edits
17257 assert_highlighted_edits(
17258 "First line\nSecond line\nThird line\nFourth line",
17259 vec![
17260 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
17261 (
17262 Point::new(2, 0)..Point::new(2, 10),
17263 "New third line".to_string(),
17264 ),
17265 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
17266 ],
17267 false,
17268 cx,
17269 |highlighted_edits, cx| {
17270 assert_eq!(
17271 highlighted_edits.text,
17272 "Second modified\nNew third line\nFourth updated line"
17273 );
17274 assert_eq!(highlighted_edits.highlights.len(), 3);
17275 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
17276 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
17277 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
17278 for highlight in &highlighted_edits.highlights {
17279 assert_eq!(
17280 highlight.1.background_color,
17281 Some(cx.theme().status().created_background)
17282 );
17283 }
17284 },
17285 )
17286 .await;
17287}
17288
17289#[gpui::test]
17290async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
17291 init_test(cx, |_| {});
17292
17293 // Deletion
17294 assert_highlighted_edits(
17295 "Hello, world!",
17296 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
17297 true,
17298 cx,
17299 |highlighted_edits, cx| {
17300 assert_eq!(highlighted_edits.text, "Hello, world!");
17301 assert_eq!(highlighted_edits.highlights.len(), 1);
17302 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
17303 assert_eq!(
17304 highlighted_edits.highlights[0].1.background_color,
17305 Some(cx.theme().status().deleted_background)
17306 );
17307 },
17308 )
17309 .await;
17310
17311 // Insertion
17312 assert_highlighted_edits(
17313 "Hello, world!",
17314 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
17315 true,
17316 cx,
17317 |highlighted_edits, cx| {
17318 assert_eq!(highlighted_edits.highlights.len(), 1);
17319 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
17320 assert_eq!(
17321 highlighted_edits.highlights[0].1.background_color,
17322 Some(cx.theme().status().created_background)
17323 );
17324 },
17325 )
17326 .await;
17327}
17328
17329async fn assert_highlighted_edits(
17330 text: &str,
17331 edits: Vec<(Range<Point>, String)>,
17332 include_deletions: bool,
17333 cx: &mut TestAppContext,
17334 assertion_fn: impl Fn(HighlightedText, &App),
17335) {
17336 let window = cx.add_window(|window, cx| {
17337 let buffer = MultiBuffer::build_simple(text, cx);
17338 Editor::new(EditorMode::Full, buffer, None, window, cx)
17339 });
17340 let cx = &mut VisualTestContext::from_window(*window, cx);
17341
17342 let (buffer, snapshot) = window
17343 .update(cx, |editor, _window, cx| {
17344 (
17345 editor.buffer().clone(),
17346 editor.buffer().read(cx).snapshot(cx),
17347 )
17348 })
17349 .unwrap();
17350
17351 let edits = edits
17352 .into_iter()
17353 .map(|(range, edit)| {
17354 (
17355 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
17356 edit,
17357 )
17358 })
17359 .collect::<Vec<_>>();
17360
17361 let text_anchor_edits = edits
17362 .clone()
17363 .into_iter()
17364 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
17365 .collect::<Vec<_>>();
17366
17367 let edit_preview = window
17368 .update(cx, |_, _window, cx| {
17369 buffer
17370 .read(cx)
17371 .as_singleton()
17372 .unwrap()
17373 .read(cx)
17374 .preview_edits(text_anchor_edits.into(), cx)
17375 })
17376 .unwrap()
17377 .await;
17378
17379 cx.update(|_window, cx| {
17380 let highlighted_edits = inline_completion_edit_text(
17381 &snapshot.as_singleton().unwrap().2,
17382 &edits,
17383 &edit_preview,
17384 include_deletions,
17385 cx,
17386 );
17387 assertion_fn(highlighted_edits, cx)
17388 });
17389}
17390
17391#[track_caller]
17392fn assert_breakpoint(
17393 breakpoints: &BTreeMap<Arc<Path>, Vec<SerializedBreakpoint>>,
17394 path: &Arc<Path>,
17395 expected: Vec<(u32, Breakpoint)>,
17396) {
17397 if expected.len() == 0usize {
17398 assert!(!breakpoints.contains_key(path));
17399 } else {
17400 let mut breakpoint = breakpoints
17401 .get(path)
17402 .unwrap()
17403 .into_iter()
17404 .map(|breakpoint| {
17405 (
17406 breakpoint.position,
17407 Breakpoint {
17408 kind: breakpoint.kind.clone(),
17409 state: breakpoint.state,
17410 },
17411 )
17412 })
17413 .collect::<Vec<_>>();
17414
17415 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
17416
17417 assert_eq!(expected, breakpoint);
17418 }
17419}
17420
17421fn add_log_breakpoint_at_cursor(
17422 editor: &mut Editor,
17423 log_message: &str,
17424 window: &mut Window,
17425 cx: &mut Context<Editor>,
17426) {
17427 let (anchor, bp) = editor
17428 .breakpoint_at_cursor_head(window, cx)
17429 .unwrap_or_else(|| {
17430 let cursor_position: Point = editor.selections.newest(cx).head();
17431
17432 let breakpoint_position = editor
17433 .snapshot(window, cx)
17434 .display_snapshot
17435 .buffer_snapshot
17436 .anchor_before(Point::new(cursor_position.row, 0));
17437
17438 let kind = BreakpointKind::Log(Arc::from(log_message));
17439
17440 (
17441 breakpoint_position,
17442 Breakpoint {
17443 kind,
17444 state: BreakpointState::Enabled,
17445 },
17446 )
17447 });
17448
17449 editor.edit_breakpoint_at_anchor(
17450 anchor,
17451 bp,
17452 BreakpointEditAction::EditLogMessage(log_message.into()),
17453 cx,
17454 );
17455}
17456
17457#[gpui::test]
17458async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
17459 init_test(cx, |_| {});
17460
17461 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
17462 let fs = FakeFs::new(cx.executor());
17463 fs.insert_tree(
17464 path!("/a"),
17465 json!({
17466 "main.rs": sample_text,
17467 }),
17468 )
17469 .await;
17470 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17471 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17472 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17473
17474 let fs = FakeFs::new(cx.executor());
17475 fs.insert_tree(
17476 path!("/a"),
17477 json!({
17478 "main.rs": sample_text,
17479 }),
17480 )
17481 .await;
17482 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17483 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17484 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17485 let worktree_id = workspace
17486 .update(cx, |workspace, _window, cx| {
17487 workspace.project().update(cx, |project, cx| {
17488 project.worktrees(cx).next().unwrap().read(cx).id()
17489 })
17490 })
17491 .unwrap();
17492
17493 let buffer = project
17494 .update(cx, |project, cx| {
17495 project.open_buffer((worktree_id, "main.rs"), cx)
17496 })
17497 .await
17498 .unwrap();
17499
17500 let (editor, cx) = cx.add_window_view(|window, cx| {
17501 Editor::new(
17502 EditorMode::Full,
17503 MultiBuffer::build_from_buffer(buffer, cx),
17504 Some(project.clone()),
17505 window,
17506 cx,
17507 )
17508 });
17509
17510 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
17511 let abs_path = project.read_with(cx, |project, cx| {
17512 project
17513 .absolute_path(&project_path, cx)
17514 .map(|path_buf| Arc::from(path_buf.to_owned()))
17515 .unwrap()
17516 });
17517
17518 // assert we can add breakpoint on the first line
17519 editor.update_in(cx, |editor, window, cx| {
17520 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17521 editor.move_to_end(&MoveToEnd, window, cx);
17522 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17523 });
17524
17525 let breakpoints = editor.update(cx, |editor, cx| {
17526 editor
17527 .breakpoint_store()
17528 .as_ref()
17529 .unwrap()
17530 .read(cx)
17531 .all_breakpoints(cx)
17532 .clone()
17533 });
17534
17535 assert_eq!(1, breakpoints.len());
17536 assert_breakpoint(
17537 &breakpoints,
17538 &abs_path,
17539 vec![
17540 (0, Breakpoint::new_standard()),
17541 (3, Breakpoint::new_standard()),
17542 ],
17543 );
17544
17545 editor.update_in(cx, |editor, window, cx| {
17546 editor.move_to_beginning(&MoveToBeginning, window, cx);
17547 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17548 });
17549
17550 let breakpoints = editor.update(cx, |editor, cx| {
17551 editor
17552 .breakpoint_store()
17553 .as_ref()
17554 .unwrap()
17555 .read(cx)
17556 .all_breakpoints(cx)
17557 .clone()
17558 });
17559
17560 assert_eq!(1, breakpoints.len());
17561 assert_breakpoint(
17562 &breakpoints,
17563 &abs_path,
17564 vec![(3, Breakpoint::new_standard())],
17565 );
17566
17567 editor.update_in(cx, |editor, window, cx| {
17568 editor.move_to_end(&MoveToEnd, window, cx);
17569 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17570 });
17571
17572 let breakpoints = editor.update(cx, |editor, cx| {
17573 editor
17574 .breakpoint_store()
17575 .as_ref()
17576 .unwrap()
17577 .read(cx)
17578 .all_breakpoints(cx)
17579 .clone()
17580 });
17581
17582 assert_eq!(0, breakpoints.len());
17583 assert_breakpoint(&breakpoints, &abs_path, vec![]);
17584}
17585
17586#[gpui::test]
17587async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
17588 init_test(cx, |_| {});
17589
17590 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
17591
17592 let fs = FakeFs::new(cx.executor());
17593 fs.insert_tree(
17594 path!("/a"),
17595 json!({
17596 "main.rs": sample_text,
17597 }),
17598 )
17599 .await;
17600 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17601 let (workspace, cx) =
17602 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
17603
17604 let worktree_id = workspace.update(cx, |workspace, cx| {
17605 workspace.project().update(cx, |project, cx| {
17606 project.worktrees(cx).next().unwrap().read(cx).id()
17607 })
17608 });
17609
17610 let buffer = project
17611 .update(cx, |project, cx| {
17612 project.open_buffer((worktree_id, "main.rs"), cx)
17613 })
17614 .await
17615 .unwrap();
17616
17617 let (editor, cx) = cx.add_window_view(|window, cx| {
17618 Editor::new(
17619 EditorMode::Full,
17620 MultiBuffer::build_from_buffer(buffer, cx),
17621 Some(project.clone()),
17622 window,
17623 cx,
17624 )
17625 });
17626
17627 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
17628 let abs_path = project.read_with(cx, |project, cx| {
17629 project
17630 .absolute_path(&project_path, cx)
17631 .map(|path_buf| Arc::from(path_buf.to_owned()))
17632 .unwrap()
17633 });
17634
17635 editor.update_in(cx, |editor, window, cx| {
17636 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
17637 });
17638
17639 let breakpoints = editor.update(cx, |editor, cx| {
17640 editor
17641 .breakpoint_store()
17642 .as_ref()
17643 .unwrap()
17644 .read(cx)
17645 .all_breakpoints(cx)
17646 .clone()
17647 });
17648
17649 assert_breakpoint(
17650 &breakpoints,
17651 &abs_path,
17652 vec![(0, Breakpoint::new_log("hello world"))],
17653 );
17654
17655 // Removing a log message from a log breakpoint should remove it
17656 editor.update_in(cx, |editor, window, cx| {
17657 add_log_breakpoint_at_cursor(editor, "", window, cx);
17658 });
17659
17660 let breakpoints = editor.update(cx, |editor, cx| {
17661 editor
17662 .breakpoint_store()
17663 .as_ref()
17664 .unwrap()
17665 .read(cx)
17666 .all_breakpoints(cx)
17667 .clone()
17668 });
17669
17670 assert_breakpoint(&breakpoints, &abs_path, vec![]);
17671
17672 editor.update_in(cx, |editor, window, cx| {
17673 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17674 editor.move_to_end(&MoveToEnd, window, cx);
17675 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17676 // Not adding a log message to a standard breakpoint shouldn't remove it
17677 add_log_breakpoint_at_cursor(editor, "", window, cx);
17678 });
17679
17680 let breakpoints = editor.update(cx, |editor, cx| {
17681 editor
17682 .breakpoint_store()
17683 .as_ref()
17684 .unwrap()
17685 .read(cx)
17686 .all_breakpoints(cx)
17687 .clone()
17688 });
17689
17690 assert_breakpoint(
17691 &breakpoints,
17692 &abs_path,
17693 vec![
17694 (0, Breakpoint::new_standard()),
17695 (3, Breakpoint::new_standard()),
17696 ],
17697 );
17698
17699 editor.update_in(cx, |editor, window, cx| {
17700 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
17701 });
17702
17703 let breakpoints = editor.update(cx, |editor, cx| {
17704 editor
17705 .breakpoint_store()
17706 .as_ref()
17707 .unwrap()
17708 .read(cx)
17709 .all_breakpoints(cx)
17710 .clone()
17711 });
17712
17713 assert_breakpoint(
17714 &breakpoints,
17715 &abs_path,
17716 vec![
17717 (0, Breakpoint::new_standard()),
17718 (3, Breakpoint::new_log("hello world")),
17719 ],
17720 );
17721
17722 editor.update_in(cx, |editor, window, cx| {
17723 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
17724 });
17725
17726 let breakpoints = editor.update(cx, |editor, cx| {
17727 editor
17728 .breakpoint_store()
17729 .as_ref()
17730 .unwrap()
17731 .read(cx)
17732 .all_breakpoints(cx)
17733 .clone()
17734 });
17735
17736 assert_breakpoint(
17737 &breakpoints,
17738 &abs_path,
17739 vec![
17740 (0, Breakpoint::new_standard()),
17741 (3, Breakpoint::new_log("hello Earth !!")),
17742 ],
17743 );
17744}
17745
17746/// This also tests that Editor::breakpoint_at_cursor_head is working properly
17747/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
17748/// or when breakpoints were placed out of order. This tests for a regression too
17749#[gpui::test]
17750async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
17751 init_test(cx, |_| {});
17752
17753 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
17754 let fs = FakeFs::new(cx.executor());
17755 fs.insert_tree(
17756 path!("/a"),
17757 json!({
17758 "main.rs": sample_text,
17759 }),
17760 )
17761 .await;
17762 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17763 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17764 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17765
17766 let fs = FakeFs::new(cx.executor());
17767 fs.insert_tree(
17768 path!("/a"),
17769 json!({
17770 "main.rs": sample_text,
17771 }),
17772 )
17773 .await;
17774 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17775 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17776 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17777 let worktree_id = workspace
17778 .update(cx, |workspace, _window, cx| {
17779 workspace.project().update(cx, |project, cx| {
17780 project.worktrees(cx).next().unwrap().read(cx).id()
17781 })
17782 })
17783 .unwrap();
17784
17785 let buffer = project
17786 .update(cx, |project, cx| {
17787 project.open_buffer((worktree_id, "main.rs"), cx)
17788 })
17789 .await
17790 .unwrap();
17791
17792 let (editor, cx) = cx.add_window_view(|window, cx| {
17793 Editor::new(
17794 EditorMode::Full,
17795 MultiBuffer::build_from_buffer(buffer, cx),
17796 Some(project.clone()),
17797 window,
17798 cx,
17799 )
17800 });
17801
17802 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
17803 let abs_path = project.read_with(cx, |project, cx| {
17804 project
17805 .absolute_path(&project_path, cx)
17806 .map(|path_buf| Arc::from(path_buf.to_owned()))
17807 .unwrap()
17808 });
17809
17810 // assert we can add breakpoint on the first line
17811 editor.update_in(cx, |editor, window, cx| {
17812 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17813 editor.move_to_end(&MoveToEnd, window, cx);
17814 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17815 editor.move_up(&MoveUp, window, cx);
17816 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17817 });
17818
17819 let breakpoints = editor.update(cx, |editor, cx| {
17820 editor
17821 .breakpoint_store()
17822 .as_ref()
17823 .unwrap()
17824 .read(cx)
17825 .all_breakpoints(cx)
17826 .clone()
17827 });
17828
17829 assert_eq!(1, breakpoints.len());
17830 assert_breakpoint(
17831 &breakpoints,
17832 &abs_path,
17833 vec![
17834 (0, Breakpoint::new_standard()),
17835 (2, Breakpoint::new_standard()),
17836 (3, Breakpoint::new_standard()),
17837 ],
17838 );
17839
17840 editor.update_in(cx, |editor, window, cx| {
17841 editor.move_to_beginning(&MoveToBeginning, window, cx);
17842 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
17843 editor.move_to_end(&MoveToEnd, window, cx);
17844 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
17845 });
17846
17847 let breakpoints = editor.update(cx, |editor, cx| {
17848 editor
17849 .breakpoint_store()
17850 .as_ref()
17851 .unwrap()
17852 .read(cx)
17853 .all_breakpoints(cx)
17854 .clone()
17855 });
17856
17857 let disable_breakpoint = {
17858 let mut bp = Breakpoint::new_standard();
17859 bp.state = BreakpointState::Disabled;
17860 bp
17861 };
17862
17863 assert_eq!(1, breakpoints.len());
17864 assert_breakpoint(
17865 &breakpoints,
17866 &abs_path,
17867 vec![
17868 (0, disable_breakpoint.clone()),
17869 (2, Breakpoint::new_standard()),
17870 (3, disable_breakpoint.clone()),
17871 ],
17872 );
17873
17874 editor.update_in(cx, |editor, window, cx| {
17875 editor.move_to_beginning(&MoveToBeginning, window, cx);
17876 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
17877 editor.move_to_end(&MoveToEnd, window, cx);
17878 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
17879 editor.move_up(&MoveUp, window, cx);
17880 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
17881 });
17882
17883 let breakpoints = editor.update(cx, |editor, cx| {
17884 editor
17885 .breakpoint_store()
17886 .as_ref()
17887 .unwrap()
17888 .read(cx)
17889 .all_breakpoints(cx)
17890 .clone()
17891 });
17892
17893 assert_eq!(1, breakpoints.len());
17894 assert_breakpoint(
17895 &breakpoints,
17896 &abs_path,
17897 vec![
17898 (0, Breakpoint::new_standard()),
17899 (2, disable_breakpoint),
17900 (3, Breakpoint::new_standard()),
17901 ],
17902 );
17903}
17904
17905#[gpui::test]
17906async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
17907 init_test(cx, |_| {});
17908 let capabilities = lsp::ServerCapabilities {
17909 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
17910 prepare_provider: Some(true),
17911 work_done_progress_options: Default::default(),
17912 })),
17913 ..Default::default()
17914 };
17915 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
17916
17917 cx.set_state(indoc! {"
17918 struct Fˇoo {}
17919 "});
17920
17921 cx.update_editor(|editor, _, cx| {
17922 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
17923 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
17924 editor.highlight_background::<DocumentHighlightRead>(
17925 &[highlight_range],
17926 |c| c.editor_document_highlight_read_background,
17927 cx,
17928 );
17929 });
17930
17931 let mut prepare_rename_handler = cx
17932 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
17933 move |_, _, _| async move {
17934 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
17935 start: lsp::Position {
17936 line: 0,
17937 character: 7,
17938 },
17939 end: lsp::Position {
17940 line: 0,
17941 character: 10,
17942 },
17943 })))
17944 },
17945 );
17946 let prepare_rename_task = cx
17947 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
17948 .expect("Prepare rename was not started");
17949 prepare_rename_handler.next().await.unwrap();
17950 prepare_rename_task.await.expect("Prepare rename failed");
17951
17952 let mut rename_handler =
17953 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
17954 let edit = lsp::TextEdit {
17955 range: lsp::Range {
17956 start: lsp::Position {
17957 line: 0,
17958 character: 7,
17959 },
17960 end: lsp::Position {
17961 line: 0,
17962 character: 10,
17963 },
17964 },
17965 new_text: "FooRenamed".to_string(),
17966 };
17967 Ok(Some(lsp::WorkspaceEdit::new(
17968 // Specify the same edit twice
17969 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
17970 )))
17971 });
17972 let rename_task = cx
17973 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
17974 .expect("Confirm rename was not started");
17975 rename_handler.next().await.unwrap();
17976 rename_task.await.expect("Confirm rename failed");
17977 cx.run_until_parked();
17978
17979 // Despite two edits, only one is actually applied as those are identical
17980 cx.assert_editor_state(indoc! {"
17981 struct FooRenamedˇ {}
17982 "});
17983}
17984
17985#[gpui::test]
17986async fn test_rename_without_prepare(cx: &mut TestAppContext) {
17987 init_test(cx, |_| {});
17988 // These capabilities indicate that the server does not support prepare rename.
17989 let capabilities = lsp::ServerCapabilities {
17990 rename_provider: Some(lsp::OneOf::Left(true)),
17991 ..Default::default()
17992 };
17993 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
17994
17995 cx.set_state(indoc! {"
17996 struct Fˇoo {}
17997 "});
17998
17999 cx.update_editor(|editor, _window, cx| {
18000 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
18001 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
18002 editor.highlight_background::<DocumentHighlightRead>(
18003 &[highlight_range],
18004 |c| c.editor_document_highlight_read_background,
18005 cx,
18006 );
18007 });
18008
18009 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
18010 .expect("Prepare rename was not started")
18011 .await
18012 .expect("Prepare rename failed");
18013
18014 let mut rename_handler =
18015 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
18016 let edit = lsp::TextEdit {
18017 range: lsp::Range {
18018 start: lsp::Position {
18019 line: 0,
18020 character: 7,
18021 },
18022 end: lsp::Position {
18023 line: 0,
18024 character: 10,
18025 },
18026 },
18027 new_text: "FooRenamed".to_string(),
18028 };
18029 Ok(Some(lsp::WorkspaceEdit::new(
18030 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
18031 )))
18032 });
18033 let rename_task = cx
18034 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
18035 .expect("Confirm rename was not started");
18036 rename_handler.next().await.unwrap();
18037 rename_task.await.expect("Confirm rename failed");
18038 cx.run_until_parked();
18039
18040 // Correct range is renamed, as `surrounding_word` is used to find it.
18041 cx.assert_editor_state(indoc! {"
18042 struct FooRenamedˇ {}
18043 "});
18044}
18045
18046#[gpui::test]
18047async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
18048 init_test(cx, |_| {});
18049 let mut cx = EditorTestContext::new(cx).await;
18050
18051 let language = Arc::new(
18052 Language::new(
18053 LanguageConfig::default(),
18054 Some(tree_sitter_html::LANGUAGE.into()),
18055 )
18056 .with_brackets_query(
18057 r#"
18058 ("<" @open "/>" @close)
18059 ("</" @open ">" @close)
18060 ("<" @open ">" @close)
18061 ("\"" @open "\"" @close)
18062 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
18063 "#,
18064 )
18065 .unwrap(),
18066 );
18067 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
18068
18069 cx.set_state(indoc! {"
18070 <span>ˇ</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 cx.set_state(indoc! {"
18080 <span><span></span>ˇ</span>
18081 "});
18082 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18083 cx.assert_editor_state(indoc! {"
18084 <span><span></span>
18085 ˇ</span>
18086 "});
18087
18088 cx.set_state(indoc! {"
18089 <span>ˇ
18090 </span>
18091 "});
18092 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18093 cx.assert_editor_state(indoc! {"
18094 <span>
18095 ˇ
18096 </span>
18097 "});
18098}
18099
18100#[gpui::test(iterations = 10)]
18101async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
18102 init_test(cx, |_| {});
18103
18104 let fs = FakeFs::new(cx.executor());
18105 fs.insert_tree(
18106 path!("/dir"),
18107 json!({
18108 "a.ts": "a",
18109 }),
18110 )
18111 .await;
18112
18113 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
18114 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18115 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18116
18117 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18118 language_registry.add(Arc::new(Language::new(
18119 LanguageConfig {
18120 name: "TypeScript".into(),
18121 matcher: LanguageMatcher {
18122 path_suffixes: vec!["ts".to_string()],
18123 ..Default::default()
18124 },
18125 ..Default::default()
18126 },
18127 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18128 )));
18129 let mut fake_language_servers = language_registry.register_fake_lsp(
18130 "TypeScript",
18131 FakeLspAdapter {
18132 capabilities: lsp::ServerCapabilities {
18133 code_lens_provider: Some(lsp::CodeLensOptions {
18134 resolve_provider: Some(true),
18135 }),
18136 execute_command_provider: Some(lsp::ExecuteCommandOptions {
18137 commands: vec!["_the/command".to_string()],
18138 ..lsp::ExecuteCommandOptions::default()
18139 }),
18140 ..lsp::ServerCapabilities::default()
18141 },
18142 ..FakeLspAdapter::default()
18143 },
18144 );
18145
18146 let (buffer, _handle) = project
18147 .update(cx, |p, cx| {
18148 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
18149 })
18150 .await
18151 .unwrap();
18152 cx.executor().run_until_parked();
18153
18154 let fake_server = fake_language_servers.next().await.unwrap();
18155
18156 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
18157 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
18158 drop(buffer_snapshot);
18159 let actions = cx
18160 .update_window(*workspace, |_, window, cx| {
18161 project.code_actions(&buffer, anchor..anchor, window, cx)
18162 })
18163 .unwrap();
18164
18165 fake_server
18166 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
18167 Ok(Some(vec![
18168 lsp::CodeLens {
18169 range: lsp::Range::default(),
18170 command: Some(lsp::Command {
18171 title: "Code lens command".to_owned(),
18172 command: "_the/command".to_owned(),
18173 arguments: None,
18174 }),
18175 data: None,
18176 },
18177 lsp::CodeLens {
18178 range: lsp::Range::default(),
18179 command: Some(lsp::Command {
18180 title: "Command not in capabilities".to_owned(),
18181 command: "not in capabilities".to_owned(),
18182 arguments: None,
18183 }),
18184 data: None,
18185 },
18186 lsp::CodeLens {
18187 range: lsp::Range {
18188 start: lsp::Position {
18189 line: 1,
18190 character: 1,
18191 },
18192 end: lsp::Position {
18193 line: 1,
18194 character: 1,
18195 },
18196 },
18197 command: Some(lsp::Command {
18198 title: "Command not in range".to_owned(),
18199 command: "_the/command".to_owned(),
18200 arguments: None,
18201 }),
18202 data: None,
18203 },
18204 ]))
18205 })
18206 .next()
18207 .await;
18208
18209 let actions = actions.await.unwrap();
18210 assert_eq!(
18211 actions.len(),
18212 1,
18213 "Should have only one valid action for the 0..0 range"
18214 );
18215 let action = actions[0].clone();
18216 let apply = project.update(cx, |project, cx| {
18217 project.apply_code_action(buffer.clone(), action, true, cx)
18218 });
18219
18220 // Resolving the code action does not populate its edits. In absence of
18221 // edits, we must execute the given command.
18222 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
18223 |mut lens, _| async move {
18224 let lens_command = lens.command.as_mut().expect("should have a command");
18225 assert_eq!(lens_command.title, "Code lens command");
18226 lens_command.arguments = Some(vec![json!("the-argument")]);
18227 Ok(lens)
18228 },
18229 );
18230
18231 // While executing the command, the language server sends the editor
18232 // a `workspaceEdit` request.
18233 fake_server
18234 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
18235 let fake = fake_server.clone();
18236 move |params, _| {
18237 assert_eq!(params.command, "_the/command");
18238 let fake = fake.clone();
18239 async move {
18240 fake.server
18241 .request::<lsp::request::ApplyWorkspaceEdit>(
18242 lsp::ApplyWorkspaceEditParams {
18243 label: None,
18244 edit: lsp::WorkspaceEdit {
18245 changes: Some(
18246 [(
18247 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
18248 vec![lsp::TextEdit {
18249 range: lsp::Range::new(
18250 lsp::Position::new(0, 0),
18251 lsp::Position::new(0, 0),
18252 ),
18253 new_text: "X".into(),
18254 }],
18255 )]
18256 .into_iter()
18257 .collect(),
18258 ),
18259 ..Default::default()
18260 },
18261 },
18262 )
18263 .await
18264 .unwrap();
18265 Ok(Some(json!(null)))
18266 }
18267 }
18268 })
18269 .next()
18270 .await;
18271
18272 // Applying the code lens command returns a project transaction containing the edits
18273 // sent by the language server in its `workspaceEdit` request.
18274 let transaction = apply.await.unwrap();
18275 assert!(transaction.0.contains_key(&buffer));
18276 buffer.update(cx, |buffer, cx| {
18277 assert_eq!(buffer.text(), "Xa");
18278 buffer.undo(cx);
18279 assert_eq!(buffer.text(), "a");
18280 });
18281}
18282
18283fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
18284 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
18285 point..point
18286}
18287
18288fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
18289 let (text, ranges) = marked_text_ranges(marked_text, true);
18290 assert_eq!(editor.text(cx), text);
18291 assert_eq!(
18292 editor.selections.ranges(cx),
18293 ranges,
18294 "Assert selections are {}",
18295 marked_text
18296 );
18297}
18298
18299pub fn handle_signature_help_request(
18300 cx: &mut EditorLspTestContext,
18301 mocked_response: lsp::SignatureHelp,
18302) -> impl Future<Output = ()> {
18303 let mut request =
18304 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
18305 let mocked_response = mocked_response.clone();
18306 async move { Ok(Some(mocked_response)) }
18307 });
18308
18309 async move {
18310 request.next().await;
18311 }
18312}
18313
18314/// Handle completion request passing a marked string specifying where the completion
18315/// should be triggered from using '|' character, what range should be replaced, and what completions
18316/// should be returned using '<' and '>' to delimit the range
18317pub fn handle_completion_request(
18318 cx: &mut EditorLspTestContext,
18319 marked_string: &str,
18320 completions: Vec<&'static str>,
18321 counter: Arc<AtomicUsize>,
18322) -> impl Future<Output = ()> {
18323 let complete_from_marker: TextRangeMarker = '|'.into();
18324 let replace_range_marker: TextRangeMarker = ('<', '>').into();
18325 let (_, mut marked_ranges) = marked_text_ranges_by(
18326 marked_string,
18327 vec![complete_from_marker.clone(), replace_range_marker.clone()],
18328 );
18329
18330 let complete_from_position =
18331 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
18332 let replace_range =
18333 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
18334
18335 let mut request =
18336 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
18337 let completions = completions.clone();
18338 counter.fetch_add(1, atomic::Ordering::Release);
18339 async move {
18340 assert_eq!(params.text_document_position.text_document.uri, url.clone());
18341 assert_eq!(
18342 params.text_document_position.position,
18343 complete_from_position
18344 );
18345 Ok(Some(lsp::CompletionResponse::Array(
18346 completions
18347 .iter()
18348 .map(|completion_text| lsp::CompletionItem {
18349 label: completion_text.to_string(),
18350 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18351 range: replace_range,
18352 new_text: completion_text.to_string(),
18353 })),
18354 ..Default::default()
18355 })
18356 .collect(),
18357 )))
18358 }
18359 });
18360
18361 async move {
18362 request.next().await;
18363 }
18364}
18365
18366fn handle_resolve_completion_request(
18367 cx: &mut EditorLspTestContext,
18368 edits: Option<Vec<(&'static str, &'static str)>>,
18369) -> impl Future<Output = ()> {
18370 let edits = edits.map(|edits| {
18371 edits
18372 .iter()
18373 .map(|(marked_string, new_text)| {
18374 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
18375 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
18376 lsp::TextEdit::new(replace_range, new_text.to_string())
18377 })
18378 .collect::<Vec<_>>()
18379 });
18380
18381 let mut request =
18382 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
18383 let edits = edits.clone();
18384 async move {
18385 Ok(lsp::CompletionItem {
18386 additional_text_edits: edits,
18387 ..Default::default()
18388 })
18389 }
18390 });
18391
18392 async move {
18393 request.next().await;
18394 }
18395}
18396
18397pub(crate) fn update_test_language_settings(
18398 cx: &mut TestAppContext,
18399 f: impl Fn(&mut AllLanguageSettingsContent),
18400) {
18401 cx.update(|cx| {
18402 SettingsStore::update_global(cx, |store, cx| {
18403 store.update_user_settings::<AllLanguageSettings>(cx, f);
18404 });
18405 });
18406}
18407
18408pub(crate) fn update_test_project_settings(
18409 cx: &mut TestAppContext,
18410 f: impl Fn(&mut ProjectSettings),
18411) {
18412 cx.update(|cx| {
18413 SettingsStore::update_global(cx, |store, cx| {
18414 store.update_user_settings::<ProjectSettings>(cx, f);
18415 });
18416 });
18417}
18418
18419pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
18420 cx.update(|cx| {
18421 assets::Assets.load_test_fonts(cx);
18422 let store = SettingsStore::test(cx);
18423 cx.set_global(store);
18424 theme::init(theme::LoadThemes::JustBase, cx);
18425 release_channel::init(SemanticVersion::default(), cx);
18426 client::init_settings(cx);
18427 language::init(cx);
18428 Project::init_settings(cx);
18429 workspace::init_settings(cx);
18430 crate::init(cx);
18431 });
18432
18433 update_test_language_settings(cx, f);
18434}
18435
18436#[track_caller]
18437fn assert_hunk_revert(
18438 not_reverted_text_with_selections: &str,
18439 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
18440 expected_reverted_text_with_selections: &str,
18441 base_text: &str,
18442 cx: &mut EditorLspTestContext,
18443) {
18444 cx.set_state(not_reverted_text_with_selections);
18445 cx.set_head_text(base_text);
18446 cx.executor().run_until_parked();
18447
18448 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
18449 let snapshot = editor.snapshot(window, cx);
18450 let reverted_hunk_statuses = snapshot
18451 .buffer_snapshot
18452 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
18453 .map(|hunk| hunk.status().kind)
18454 .collect::<Vec<_>>();
18455
18456 editor.git_restore(&Default::default(), window, cx);
18457 reverted_hunk_statuses
18458 });
18459 cx.executor().run_until_parked();
18460 cx.assert_editor_state(expected_reverted_text_with_selections);
18461 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
18462}