1use super::*;
2use crate::{
3 JoinLines,
4 scroll::scroll_amount::ScrollAmount,
5 test::{
6 assert_text_with_selections, build_editor,
7 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
8 editor_test_context::EditorTestContext,
9 select_ranges,
10 },
11};
12use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
13use futures::StreamExt;
14use gpui::{
15 BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext,
16 WindowBounds, WindowOptions, div,
17};
18use indoc::indoc;
19use language::{
20 BracketPairConfig,
21 Capability::ReadWrite,
22 FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName,
23 Override, Point,
24 language_settings::{
25 AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
26 LanguageSettingsContent, LspInsertMode, PrettierSettings,
27 },
28};
29use language_settings::{Formatter, FormatterList, IndentGuideSettings};
30use lsp::CompletionParams;
31use multi_buffer::{IndentGuide, PathKey};
32use parking_lot::Mutex;
33use pretty_assertions::{assert_eq, assert_ne};
34use project::{
35 FakeFs,
36 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
37 project_settings::{LspSettings, ProjectSettings},
38};
39use serde_json::{self, json};
40use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
41use std::{
42 iter,
43 sync::atomic::{self, AtomicUsize},
44};
45use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
46use text::ToPoint as _;
47use unindent::Unindent;
48use util::{
49 assert_set_eq, path,
50 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
51 uri,
52};
53use workspace::{
54 CloseAllItems, CloseInactiveItems, NavigationEntry, ViewId,
55 item::{FollowEvent, FollowableItem, Item, ItemHandle},
56};
57
58#[gpui::test]
59fn test_edit_events(cx: &mut TestAppContext) {
60 init_test(cx, |_| {});
61
62 let buffer = cx.new(|cx| {
63 let mut buffer = language::Buffer::local("123456", cx);
64 buffer.set_group_interval(Duration::from_secs(1));
65 buffer
66 });
67
68 let events = Rc::new(RefCell::new(Vec::new()));
69 let editor1 = cx.add_window({
70 let events = events.clone();
71 |window, cx| {
72 let entity = cx.entity().clone();
73 cx.subscribe_in(
74 &entity,
75 window,
76 move |_, _, event: &EditorEvent, _, _| match event {
77 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
78 EditorEvent::BufferEdited => {
79 events.borrow_mut().push(("editor1", "buffer edited"))
80 }
81 _ => {}
82 },
83 )
84 .detach();
85 Editor::for_buffer(buffer.clone(), None, window, cx)
86 }
87 });
88
89 let editor2 = cx.add_window({
90 let events = events.clone();
91 |window, cx| {
92 cx.subscribe_in(
93 &cx.entity().clone(),
94 window,
95 move |_, _, event: &EditorEvent, _, _| match event {
96 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
97 EditorEvent::BufferEdited => {
98 events.borrow_mut().push(("editor2", "buffer edited"))
99 }
100 _ => {}
101 },
102 )
103 .detach();
104 Editor::for_buffer(buffer.clone(), None, window, cx)
105 }
106 });
107
108 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
109
110 // Mutating editor 1 will emit an `Edited` event only for that editor.
111 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
112 assert_eq!(
113 mem::take(&mut *events.borrow_mut()),
114 [
115 ("editor1", "edited"),
116 ("editor1", "buffer edited"),
117 ("editor2", "buffer edited"),
118 ]
119 );
120
121 // Mutating editor 2 will emit an `Edited` event only for that editor.
122 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
123 assert_eq!(
124 mem::take(&mut *events.borrow_mut()),
125 [
126 ("editor2", "edited"),
127 ("editor1", "buffer edited"),
128 ("editor2", "buffer edited"),
129 ]
130 );
131
132 // Undoing on editor 1 will emit an `Edited` event only for that editor.
133 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
134 assert_eq!(
135 mem::take(&mut *events.borrow_mut()),
136 [
137 ("editor1", "edited"),
138 ("editor1", "buffer edited"),
139 ("editor2", "buffer edited"),
140 ]
141 );
142
143 // Redoing on editor 1 will emit an `Edited` event only for that editor.
144 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
145 assert_eq!(
146 mem::take(&mut *events.borrow_mut()),
147 [
148 ("editor1", "edited"),
149 ("editor1", "buffer edited"),
150 ("editor2", "buffer edited"),
151 ]
152 );
153
154 // Undoing on editor 2 will emit an `Edited` event only for that editor.
155 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
156 assert_eq!(
157 mem::take(&mut *events.borrow_mut()),
158 [
159 ("editor2", "edited"),
160 ("editor1", "buffer edited"),
161 ("editor2", "buffer edited"),
162 ]
163 );
164
165 // Redoing on editor 2 will emit an `Edited` event only for that editor.
166 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
167 assert_eq!(
168 mem::take(&mut *events.borrow_mut()),
169 [
170 ("editor2", "edited"),
171 ("editor1", "buffer edited"),
172 ("editor2", "buffer edited"),
173 ]
174 );
175
176 // No event is emitted when the mutation is a no-op.
177 _ = editor2.update(cx, |editor, window, cx| {
178 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
179
180 editor.backspace(&Backspace, window, cx);
181 });
182 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
183}
184
185#[gpui::test]
186fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
187 init_test(cx, |_| {});
188
189 let mut now = Instant::now();
190 let group_interval = Duration::from_millis(1);
191 let buffer = cx.new(|cx| {
192 let mut buf = language::Buffer::local("123456", cx);
193 buf.set_group_interval(group_interval);
194 buf
195 });
196 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
197 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
198
199 _ = editor.update(cx, |editor, window, cx| {
200 editor.start_transaction_at(now, window, cx);
201 editor.change_selections(None, window, cx, |s| s.select_ranges([2..4]));
202
203 editor.insert("cd", window, cx);
204 editor.end_transaction_at(now, cx);
205 assert_eq!(editor.text(cx), "12cd56");
206 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
207
208 editor.start_transaction_at(now, window, cx);
209 editor.change_selections(None, window, cx, |s| s.select_ranges([4..5]));
210 editor.insert("e", window, cx);
211 editor.end_transaction_at(now, cx);
212 assert_eq!(editor.text(cx), "12cde6");
213 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
214
215 now += group_interval + Duration::from_millis(1);
216 editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]));
217
218 // Simulate an edit in another editor
219 buffer.update(cx, |buffer, cx| {
220 buffer.start_transaction_at(now, cx);
221 buffer.edit([(0..1, "a")], None, cx);
222 buffer.edit([(1..1, "b")], None, cx);
223 buffer.end_transaction_at(now, cx);
224 });
225
226 assert_eq!(editor.text(cx), "ab2cde6");
227 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
228
229 // Last transaction happened past the group interval in a different editor.
230 // Undo it individually and don't restore selections.
231 editor.undo(&Undo, window, cx);
232 assert_eq!(editor.text(cx), "12cde6");
233 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
234
235 // First two transactions happened within the group interval in this editor.
236 // Undo them together and restore selections.
237 editor.undo(&Undo, window, cx);
238 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
239 assert_eq!(editor.text(cx), "123456");
240 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
241
242 // Redo the first two transactions together.
243 editor.redo(&Redo, window, cx);
244 assert_eq!(editor.text(cx), "12cde6");
245 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
246
247 // Redo the last transaction on its own.
248 editor.redo(&Redo, window, cx);
249 assert_eq!(editor.text(cx), "ab2cde6");
250 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
251
252 // Test empty transactions.
253 editor.start_transaction_at(now, window, cx);
254 editor.end_transaction_at(now, cx);
255 editor.undo(&Undo, window, cx);
256 assert_eq!(editor.text(cx), "12cde6");
257 });
258}
259
260#[gpui::test]
261fn test_ime_composition(cx: &mut TestAppContext) {
262 init_test(cx, |_| {});
263
264 let buffer = cx.new(|cx| {
265 let mut buffer = language::Buffer::local("abcde", cx);
266 // Ensure automatic grouping doesn't occur.
267 buffer.set_group_interval(Duration::ZERO);
268 buffer
269 });
270
271 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
272 cx.add_window(|window, cx| {
273 let mut editor = build_editor(buffer.clone(), window, cx);
274
275 // Start a new IME composition.
276 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
277 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
278 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
279 assert_eq!(editor.text(cx), "äbcde");
280 assert_eq!(
281 editor.marked_text_ranges(cx),
282 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
283 );
284
285 // Finalize IME composition.
286 editor.replace_text_in_range(None, "ā", window, cx);
287 assert_eq!(editor.text(cx), "ābcde");
288 assert_eq!(editor.marked_text_ranges(cx), None);
289
290 // IME composition edits are grouped and are undone/redone at once.
291 editor.undo(&Default::default(), window, cx);
292 assert_eq!(editor.text(cx), "abcde");
293 assert_eq!(editor.marked_text_ranges(cx), None);
294 editor.redo(&Default::default(), window, cx);
295 assert_eq!(editor.text(cx), "ābcde");
296 assert_eq!(editor.marked_text_ranges(cx), None);
297
298 // Start a new IME composition.
299 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
300 assert_eq!(
301 editor.marked_text_ranges(cx),
302 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
303 );
304
305 // Undoing during an IME composition cancels it.
306 editor.undo(&Default::default(), window, cx);
307 assert_eq!(editor.text(cx), "ābcde");
308 assert_eq!(editor.marked_text_ranges(cx), None);
309
310 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
311 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
312 assert_eq!(editor.text(cx), "ābcdè");
313 assert_eq!(
314 editor.marked_text_ranges(cx),
315 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
316 );
317
318 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
319 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
320 assert_eq!(editor.text(cx), "ābcdę");
321 assert_eq!(editor.marked_text_ranges(cx), None);
322
323 // Start a new IME composition with multiple cursors.
324 editor.change_selections(None, window, cx, |s| {
325 s.select_ranges([
326 OffsetUtf16(1)..OffsetUtf16(1),
327 OffsetUtf16(3)..OffsetUtf16(3),
328 OffsetUtf16(5)..OffsetUtf16(5),
329 ])
330 });
331 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
332 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
333 assert_eq!(
334 editor.marked_text_ranges(cx),
335 Some(vec![
336 OffsetUtf16(0)..OffsetUtf16(3),
337 OffsetUtf16(4)..OffsetUtf16(7),
338 OffsetUtf16(8)..OffsetUtf16(11)
339 ])
340 );
341
342 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
343 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
344 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
345 assert_eq!(
346 editor.marked_text_ranges(cx),
347 Some(vec![
348 OffsetUtf16(1)..OffsetUtf16(2),
349 OffsetUtf16(5)..OffsetUtf16(6),
350 OffsetUtf16(9)..OffsetUtf16(10)
351 ])
352 );
353
354 // Finalize IME composition with multiple cursors.
355 editor.replace_text_in_range(Some(9..10), "2", window, cx);
356 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
357 assert_eq!(editor.marked_text_ranges(cx), None);
358
359 editor
360 });
361}
362
363#[gpui::test]
364fn test_selection_with_mouse(cx: &mut TestAppContext) {
365 init_test(cx, |_| {});
366
367 let editor = cx.add_window(|window, cx| {
368 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
369 build_editor(buffer, window, cx)
370 });
371
372 _ = editor.update(cx, |editor, window, cx| {
373 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
374 });
375 assert_eq!(
376 editor
377 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
378 .unwrap(),
379 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
380 );
381
382 _ = editor.update(cx, |editor, window, cx| {
383 editor.update_selection(
384 DisplayPoint::new(DisplayRow(3), 3),
385 0,
386 gpui::Point::<f32>::default(),
387 window,
388 cx,
389 );
390 });
391
392 assert_eq!(
393 editor
394 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
395 .unwrap(),
396 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
397 );
398
399 _ = editor.update(cx, |editor, window, cx| {
400 editor.update_selection(
401 DisplayPoint::new(DisplayRow(1), 1),
402 0,
403 gpui::Point::<f32>::default(),
404 window,
405 cx,
406 );
407 });
408
409 assert_eq!(
410 editor
411 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
412 .unwrap(),
413 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
414 );
415
416 _ = editor.update(cx, |editor, window, cx| {
417 editor.end_selection(window, cx);
418 editor.update_selection(
419 DisplayPoint::new(DisplayRow(3), 3),
420 0,
421 gpui::Point::<f32>::default(),
422 window,
423 cx,
424 );
425 });
426
427 assert_eq!(
428 editor
429 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
430 .unwrap(),
431 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
432 );
433
434 _ = editor.update(cx, |editor, window, cx| {
435 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
436 editor.update_selection(
437 DisplayPoint::new(DisplayRow(0), 0),
438 0,
439 gpui::Point::<f32>::default(),
440 window,
441 cx,
442 );
443 });
444
445 assert_eq!(
446 editor
447 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
448 .unwrap(),
449 [
450 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
451 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
452 ]
453 );
454
455 _ = editor.update(cx, |editor, window, cx| {
456 editor.end_selection(window, cx);
457 });
458
459 assert_eq!(
460 editor
461 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
462 .unwrap(),
463 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
464 );
465}
466
467#[gpui::test]
468fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
469 init_test(cx, |_| {});
470
471 let editor = cx.add_window(|window, cx| {
472 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
473 build_editor(buffer, window, cx)
474 });
475
476 _ = editor.update(cx, |editor, window, cx| {
477 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
478 });
479
480 _ = editor.update(cx, |editor, window, cx| {
481 editor.end_selection(window, cx);
482 });
483
484 _ = editor.update(cx, |editor, window, cx| {
485 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
486 });
487
488 _ = editor.update(cx, |editor, window, cx| {
489 editor.end_selection(window, cx);
490 });
491
492 assert_eq!(
493 editor
494 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
495 .unwrap(),
496 [
497 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
498 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
499 ]
500 );
501
502 _ = editor.update(cx, |editor, window, cx| {
503 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
504 });
505
506 _ = editor.update(cx, |editor, window, cx| {
507 editor.end_selection(window, cx);
508 });
509
510 assert_eq!(
511 editor
512 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
513 .unwrap(),
514 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
515 );
516}
517
518#[gpui::test]
519fn test_canceling_pending_selection(cx: &mut TestAppContext) {
520 init_test(cx, |_| {});
521
522 let editor = cx.add_window(|window, cx| {
523 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
524 build_editor(buffer, window, cx)
525 });
526
527 _ = editor.update(cx, |editor, window, cx| {
528 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
529 assert_eq!(
530 editor.selections.display_ranges(cx),
531 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
532 );
533 });
534
535 _ = editor.update(cx, |editor, window, cx| {
536 editor.update_selection(
537 DisplayPoint::new(DisplayRow(3), 3),
538 0,
539 gpui::Point::<f32>::default(),
540 window,
541 cx,
542 );
543 assert_eq!(
544 editor.selections.display_ranges(cx),
545 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
546 );
547 });
548
549 _ = editor.update(cx, |editor, window, cx| {
550 editor.cancel(&Cancel, window, cx);
551 editor.update_selection(
552 DisplayPoint::new(DisplayRow(1), 1),
553 0,
554 gpui::Point::<f32>::default(),
555 window,
556 cx,
557 );
558 assert_eq!(
559 editor.selections.display_ranges(cx),
560 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
561 );
562 });
563}
564
565#[gpui::test]
566fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
567 init_test(cx, |_| {});
568
569 let editor = cx.add_window(|window, cx| {
570 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
571 build_editor(buffer, window, cx)
572 });
573
574 _ = editor.update(cx, |editor, window, cx| {
575 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
576 assert_eq!(
577 editor.selections.display_ranges(cx),
578 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
579 );
580
581 editor.move_down(&Default::default(), window, cx);
582 assert_eq!(
583 editor.selections.display_ranges(cx),
584 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
585 );
586
587 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
588 assert_eq!(
589 editor.selections.display_ranges(cx),
590 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
591 );
592
593 editor.move_up(&Default::default(), window, cx);
594 assert_eq!(
595 editor.selections.display_ranges(cx),
596 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
597 );
598 });
599}
600
601#[gpui::test]
602fn test_clone(cx: &mut TestAppContext) {
603 init_test(cx, |_| {});
604
605 let (text, selection_ranges) = marked_text_ranges(
606 indoc! {"
607 one
608 two
609 threeˇ
610 four
611 fiveˇ
612 "},
613 true,
614 );
615
616 let editor = cx.add_window(|window, cx| {
617 let buffer = MultiBuffer::build_simple(&text, cx);
618 build_editor(buffer, window, cx)
619 });
620
621 _ = editor.update(cx, |editor, window, cx| {
622 editor.change_selections(None, window, cx, |s| {
623 s.select_ranges(selection_ranges.clone())
624 });
625 editor.fold_creases(
626 vec![
627 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
628 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
629 ],
630 true,
631 window,
632 cx,
633 );
634 });
635
636 let cloned_editor = editor
637 .update(cx, |editor, _, cx| {
638 cx.open_window(Default::default(), |window, cx| {
639 cx.new(|cx| editor.clone(window, cx))
640 })
641 })
642 .unwrap()
643 .unwrap();
644
645 let snapshot = editor
646 .update(cx, |e, window, cx| e.snapshot(window, cx))
647 .unwrap();
648 let cloned_snapshot = cloned_editor
649 .update(cx, |e, window, cx| e.snapshot(window, cx))
650 .unwrap();
651
652 assert_eq!(
653 cloned_editor
654 .update(cx, |e, _, cx| e.display_text(cx))
655 .unwrap(),
656 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
657 );
658 assert_eq!(
659 cloned_snapshot
660 .folds_in_range(0..text.len())
661 .collect::<Vec<_>>(),
662 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
663 );
664 assert_set_eq!(
665 cloned_editor
666 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
667 .unwrap(),
668 editor
669 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
670 .unwrap()
671 );
672 assert_set_eq!(
673 cloned_editor
674 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
675 .unwrap(),
676 editor
677 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
678 .unwrap()
679 );
680}
681
682#[gpui::test]
683async fn test_navigation_history(cx: &mut TestAppContext) {
684 init_test(cx, |_| {});
685
686 use workspace::item::Item;
687
688 let fs = FakeFs::new(cx.executor());
689 let project = Project::test(fs, [], cx).await;
690 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
691 let pane = workspace
692 .update(cx, |workspace, _, _| workspace.active_pane().clone())
693 .unwrap();
694
695 _ = workspace.update(cx, |_v, window, cx| {
696 cx.new(|cx| {
697 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
698 let mut editor = build_editor(buffer.clone(), window, cx);
699 let handle = cx.entity();
700 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
701
702 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
703 editor.nav_history.as_mut().unwrap().pop_backward(cx)
704 }
705
706 // Move the cursor a small distance.
707 // Nothing is added to the navigation history.
708 editor.change_selections(None, window, cx, |s| {
709 s.select_display_ranges([
710 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
711 ])
712 });
713 editor.change_selections(None, window, cx, |s| {
714 s.select_display_ranges([
715 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
716 ])
717 });
718 assert!(pop_history(&mut editor, cx).is_none());
719
720 // Move the cursor a large distance.
721 // The history can jump back to the previous position.
722 editor.change_selections(None, window, cx, |s| {
723 s.select_display_ranges([
724 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
725 ])
726 });
727 let nav_entry = pop_history(&mut editor, cx).unwrap();
728 editor.navigate(nav_entry.data.unwrap(), window, cx);
729 assert_eq!(nav_entry.item.id(), cx.entity_id());
730 assert_eq!(
731 editor.selections.display_ranges(cx),
732 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
733 );
734 assert!(pop_history(&mut editor, cx).is_none());
735
736 // Move the cursor a small distance via the mouse.
737 // Nothing is added to the navigation history.
738 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
739 editor.end_selection(window, cx);
740 assert_eq!(
741 editor.selections.display_ranges(cx),
742 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
743 );
744 assert!(pop_history(&mut editor, cx).is_none());
745
746 // Move the cursor a large distance via the mouse.
747 // The history can jump back to the previous position.
748 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
749 editor.end_selection(window, cx);
750 assert_eq!(
751 editor.selections.display_ranges(cx),
752 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
753 );
754 let nav_entry = pop_history(&mut editor, cx).unwrap();
755 editor.navigate(nav_entry.data.unwrap(), window, cx);
756 assert_eq!(nav_entry.item.id(), cx.entity_id());
757 assert_eq!(
758 editor.selections.display_ranges(cx),
759 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
760 );
761 assert!(pop_history(&mut editor, cx).is_none());
762
763 // Set scroll position to check later
764 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
765 let original_scroll_position = editor.scroll_manager.anchor();
766
767 // Jump to the end of the document and adjust scroll
768 editor.move_to_end(&MoveToEnd, window, cx);
769 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
770 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
771
772 let nav_entry = pop_history(&mut editor, cx).unwrap();
773 editor.navigate(nav_entry.data.unwrap(), window, cx);
774 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
775
776 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
777 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
778 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
779 let invalid_point = Point::new(9999, 0);
780 editor.navigate(
781 Box::new(NavigationData {
782 cursor_anchor: invalid_anchor,
783 cursor_position: invalid_point,
784 scroll_anchor: ScrollAnchor {
785 anchor: invalid_anchor,
786 offset: Default::default(),
787 },
788 scroll_top_row: invalid_point.row,
789 }),
790 window,
791 cx,
792 );
793 assert_eq!(
794 editor.selections.display_ranges(cx),
795 &[editor.max_point(cx)..editor.max_point(cx)]
796 );
797 assert_eq!(
798 editor.scroll_position(cx),
799 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
800 );
801
802 editor
803 })
804 });
805}
806
807#[gpui::test]
808fn test_cancel(cx: &mut TestAppContext) {
809 init_test(cx, |_| {});
810
811 let editor = cx.add_window(|window, cx| {
812 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
813 build_editor(buffer, window, cx)
814 });
815
816 _ = editor.update(cx, |editor, window, cx| {
817 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
818 editor.update_selection(
819 DisplayPoint::new(DisplayRow(1), 1),
820 0,
821 gpui::Point::<f32>::default(),
822 window,
823 cx,
824 );
825 editor.end_selection(window, cx);
826
827 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
828 editor.update_selection(
829 DisplayPoint::new(DisplayRow(0), 3),
830 0,
831 gpui::Point::<f32>::default(),
832 window,
833 cx,
834 );
835 editor.end_selection(window, cx);
836 assert_eq!(
837 editor.selections.display_ranges(cx),
838 [
839 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
840 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
841 ]
842 );
843 });
844
845 _ = editor.update(cx, |editor, window, cx| {
846 editor.cancel(&Cancel, window, cx);
847 assert_eq!(
848 editor.selections.display_ranges(cx),
849 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
850 );
851 });
852
853 _ = editor.update(cx, |editor, window, cx| {
854 editor.cancel(&Cancel, window, cx);
855 assert_eq!(
856 editor.selections.display_ranges(cx),
857 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
858 );
859 });
860}
861
862#[gpui::test]
863fn test_fold_action(cx: &mut TestAppContext) {
864 init_test(cx, |_| {});
865
866 let editor = cx.add_window(|window, cx| {
867 let buffer = MultiBuffer::build_simple(
868 &"
869 impl Foo {
870 // Hello!
871
872 fn a() {
873 1
874 }
875
876 fn b() {
877 2
878 }
879
880 fn c() {
881 3
882 }
883 }
884 "
885 .unindent(),
886 cx,
887 );
888 build_editor(buffer.clone(), window, cx)
889 });
890
891 _ = editor.update(cx, |editor, window, cx| {
892 editor.change_selections(None, window, cx, |s| {
893 s.select_display_ranges([
894 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
895 ]);
896 });
897 editor.fold(&Fold, window, cx);
898 assert_eq!(
899 editor.display_text(cx),
900 "
901 impl Foo {
902 // Hello!
903
904 fn a() {
905 1
906 }
907
908 fn b() {⋯
909 }
910
911 fn c() {⋯
912 }
913 }
914 "
915 .unindent(),
916 );
917
918 editor.fold(&Fold, window, cx);
919 assert_eq!(
920 editor.display_text(cx),
921 "
922 impl Foo {⋯
923 }
924 "
925 .unindent(),
926 );
927
928 editor.unfold_lines(&UnfoldLines, window, cx);
929 assert_eq!(
930 editor.display_text(cx),
931 "
932 impl Foo {
933 // Hello!
934
935 fn a() {
936 1
937 }
938
939 fn b() {⋯
940 }
941
942 fn c() {⋯
943 }
944 }
945 "
946 .unindent(),
947 );
948
949 editor.unfold_lines(&UnfoldLines, window, cx);
950 assert_eq!(
951 editor.display_text(cx),
952 editor.buffer.read(cx).read(cx).text()
953 );
954 });
955}
956
957#[gpui::test]
958fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
959 init_test(cx, |_| {});
960
961 let editor = cx.add_window(|window, cx| {
962 let buffer = MultiBuffer::build_simple(
963 &"
964 class Foo:
965 # Hello!
966
967 def a():
968 print(1)
969
970 def b():
971 print(2)
972
973 def c():
974 print(3)
975 "
976 .unindent(),
977 cx,
978 );
979 build_editor(buffer.clone(), window, cx)
980 });
981
982 _ = editor.update(cx, |editor, window, cx| {
983 editor.change_selections(None, window, cx, |s| {
984 s.select_display_ranges([
985 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
986 ]);
987 });
988 editor.fold(&Fold, window, cx);
989 assert_eq!(
990 editor.display_text(cx),
991 "
992 class Foo:
993 # Hello!
994
995 def a():
996 print(1)
997
998 def b():⋯
999
1000 def c():⋯
1001 "
1002 .unindent(),
1003 );
1004
1005 editor.fold(&Fold, window, cx);
1006 assert_eq!(
1007 editor.display_text(cx),
1008 "
1009 class Foo:⋯
1010 "
1011 .unindent(),
1012 );
1013
1014 editor.unfold_lines(&UnfoldLines, window, cx);
1015 assert_eq!(
1016 editor.display_text(cx),
1017 "
1018 class Foo:
1019 # Hello!
1020
1021 def a():
1022 print(1)
1023
1024 def b():⋯
1025
1026 def c():⋯
1027 "
1028 .unindent(),
1029 );
1030
1031 editor.unfold_lines(&UnfoldLines, window, cx);
1032 assert_eq!(
1033 editor.display_text(cx),
1034 editor.buffer.read(cx).read(cx).text()
1035 );
1036 });
1037}
1038
1039#[gpui::test]
1040fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1041 init_test(cx, |_| {});
1042
1043 let editor = cx.add_window(|window, cx| {
1044 let buffer = MultiBuffer::build_simple(
1045 &"
1046 class Foo:
1047 # Hello!
1048
1049 def a():
1050 print(1)
1051
1052 def b():
1053 print(2)
1054
1055
1056 def c():
1057 print(3)
1058
1059
1060 "
1061 .unindent(),
1062 cx,
1063 );
1064 build_editor(buffer.clone(), window, cx)
1065 });
1066
1067 _ = editor.update(cx, |editor, window, cx| {
1068 editor.change_selections(None, window, cx, |s| {
1069 s.select_display_ranges([
1070 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1071 ]);
1072 });
1073 editor.fold(&Fold, window, cx);
1074 assert_eq!(
1075 editor.display_text(cx),
1076 "
1077 class Foo:
1078 # Hello!
1079
1080 def a():
1081 print(1)
1082
1083 def b():⋯
1084
1085
1086 def c():⋯
1087
1088
1089 "
1090 .unindent(),
1091 );
1092
1093 editor.fold(&Fold, window, cx);
1094 assert_eq!(
1095 editor.display_text(cx),
1096 "
1097 class Foo:⋯
1098
1099
1100 "
1101 .unindent(),
1102 );
1103
1104 editor.unfold_lines(&UnfoldLines, window, cx);
1105 assert_eq!(
1106 editor.display_text(cx),
1107 "
1108 class Foo:
1109 # Hello!
1110
1111 def a():
1112 print(1)
1113
1114 def b():⋯
1115
1116
1117 def c():⋯
1118
1119
1120 "
1121 .unindent(),
1122 );
1123
1124 editor.unfold_lines(&UnfoldLines, window, cx);
1125 assert_eq!(
1126 editor.display_text(cx),
1127 editor.buffer.read(cx).read(cx).text()
1128 );
1129 });
1130}
1131
1132#[gpui::test]
1133fn test_fold_at_level(cx: &mut TestAppContext) {
1134 init_test(cx, |_| {});
1135
1136 let editor = cx.add_window(|window, cx| {
1137 let buffer = MultiBuffer::build_simple(
1138 &"
1139 class Foo:
1140 # Hello!
1141
1142 def a():
1143 print(1)
1144
1145 def b():
1146 print(2)
1147
1148
1149 class Bar:
1150 # World!
1151
1152 def a():
1153 print(1)
1154
1155 def b():
1156 print(2)
1157
1158
1159 "
1160 .unindent(),
1161 cx,
1162 );
1163 build_editor(buffer.clone(), window, cx)
1164 });
1165
1166 _ = editor.update(cx, |editor, window, cx| {
1167 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1168 assert_eq!(
1169 editor.display_text(cx),
1170 "
1171 class Foo:
1172 # Hello!
1173
1174 def a():⋯
1175
1176 def b():⋯
1177
1178
1179 class Bar:
1180 # World!
1181
1182 def a():⋯
1183
1184 def b():⋯
1185
1186
1187 "
1188 .unindent(),
1189 );
1190
1191 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1192 assert_eq!(
1193 editor.display_text(cx),
1194 "
1195 class Foo:⋯
1196
1197
1198 class Bar:⋯
1199
1200
1201 "
1202 .unindent(),
1203 );
1204
1205 editor.unfold_all(&UnfoldAll, window, cx);
1206 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1207 assert_eq!(
1208 editor.display_text(cx),
1209 "
1210 class Foo:
1211 # Hello!
1212
1213 def a():
1214 print(1)
1215
1216 def b():
1217 print(2)
1218
1219
1220 class Bar:
1221 # World!
1222
1223 def a():
1224 print(1)
1225
1226 def b():
1227 print(2)
1228
1229
1230 "
1231 .unindent(),
1232 );
1233
1234 assert_eq!(
1235 editor.display_text(cx),
1236 editor.buffer.read(cx).read(cx).text()
1237 );
1238 });
1239}
1240
1241#[gpui::test]
1242fn test_move_cursor(cx: &mut TestAppContext) {
1243 init_test(cx, |_| {});
1244
1245 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1246 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1247
1248 buffer.update(cx, |buffer, cx| {
1249 buffer.edit(
1250 vec![
1251 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1252 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1253 ],
1254 None,
1255 cx,
1256 );
1257 });
1258 _ = editor.update(cx, |editor, window, cx| {
1259 assert_eq!(
1260 editor.selections.display_ranges(cx),
1261 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1262 );
1263
1264 editor.move_down(&MoveDown, window, cx);
1265 assert_eq!(
1266 editor.selections.display_ranges(cx),
1267 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1268 );
1269
1270 editor.move_right(&MoveRight, window, cx);
1271 assert_eq!(
1272 editor.selections.display_ranges(cx),
1273 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1274 );
1275
1276 editor.move_left(&MoveLeft, window, cx);
1277 assert_eq!(
1278 editor.selections.display_ranges(cx),
1279 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1280 );
1281
1282 editor.move_up(&MoveUp, window, cx);
1283 assert_eq!(
1284 editor.selections.display_ranges(cx),
1285 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1286 );
1287
1288 editor.move_to_end(&MoveToEnd, window, cx);
1289 assert_eq!(
1290 editor.selections.display_ranges(cx),
1291 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1292 );
1293
1294 editor.move_to_beginning(&MoveToBeginning, window, cx);
1295 assert_eq!(
1296 editor.selections.display_ranges(cx),
1297 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1298 );
1299
1300 editor.change_selections(None, window, cx, |s| {
1301 s.select_display_ranges([
1302 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1303 ]);
1304 });
1305 editor.select_to_beginning(&SelectToBeginning, window, cx);
1306 assert_eq!(
1307 editor.selections.display_ranges(cx),
1308 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1309 );
1310
1311 editor.select_to_end(&SelectToEnd, window, cx);
1312 assert_eq!(
1313 editor.selections.display_ranges(cx),
1314 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1315 );
1316 });
1317}
1318
1319// TODO: Re-enable this test
1320#[cfg(target_os = "macos")]
1321#[gpui::test]
1322fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1323 init_test(cx, |_| {});
1324
1325 let editor = cx.add_window(|window, cx| {
1326 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1327 build_editor(buffer.clone(), window, cx)
1328 });
1329
1330 assert_eq!('🟥'.len_utf8(), 4);
1331 assert_eq!('α'.len_utf8(), 2);
1332
1333 _ = editor.update(cx, |editor, window, cx| {
1334 editor.fold_creases(
1335 vec![
1336 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1337 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1338 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1339 ],
1340 true,
1341 window,
1342 cx,
1343 );
1344 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1345
1346 editor.move_right(&MoveRight, window, cx);
1347 assert_eq!(
1348 editor.selections.display_ranges(cx),
1349 &[empty_range(0, "🟥".len())]
1350 );
1351 editor.move_right(&MoveRight, window, cx);
1352 assert_eq!(
1353 editor.selections.display_ranges(cx),
1354 &[empty_range(0, "🟥🟧".len())]
1355 );
1356 editor.move_right(&MoveRight, window, cx);
1357 assert_eq!(
1358 editor.selections.display_ranges(cx),
1359 &[empty_range(0, "🟥🟧⋯".len())]
1360 );
1361
1362 editor.move_down(&MoveDown, window, cx);
1363 assert_eq!(
1364 editor.selections.display_ranges(cx),
1365 &[empty_range(1, "ab⋯e".len())]
1366 );
1367 editor.move_left(&MoveLeft, window, cx);
1368 assert_eq!(
1369 editor.selections.display_ranges(cx),
1370 &[empty_range(1, "ab⋯".len())]
1371 );
1372 editor.move_left(&MoveLeft, window, cx);
1373 assert_eq!(
1374 editor.selections.display_ranges(cx),
1375 &[empty_range(1, "ab".len())]
1376 );
1377 editor.move_left(&MoveLeft, window, cx);
1378 assert_eq!(
1379 editor.selections.display_ranges(cx),
1380 &[empty_range(1, "a".len())]
1381 );
1382
1383 editor.move_down(&MoveDown, window, cx);
1384 assert_eq!(
1385 editor.selections.display_ranges(cx),
1386 &[empty_range(2, "α".len())]
1387 );
1388 editor.move_right(&MoveRight, window, cx);
1389 assert_eq!(
1390 editor.selections.display_ranges(cx),
1391 &[empty_range(2, "αβ".len())]
1392 );
1393 editor.move_right(&MoveRight, window, cx);
1394 assert_eq!(
1395 editor.selections.display_ranges(cx),
1396 &[empty_range(2, "αβ⋯".len())]
1397 );
1398 editor.move_right(&MoveRight, window, cx);
1399 assert_eq!(
1400 editor.selections.display_ranges(cx),
1401 &[empty_range(2, "αβ⋯ε".len())]
1402 );
1403
1404 editor.move_up(&MoveUp, window, cx);
1405 assert_eq!(
1406 editor.selections.display_ranges(cx),
1407 &[empty_range(1, "ab⋯e".len())]
1408 );
1409 editor.move_down(&MoveDown, window, cx);
1410 assert_eq!(
1411 editor.selections.display_ranges(cx),
1412 &[empty_range(2, "αβ⋯ε".len())]
1413 );
1414 editor.move_up(&MoveUp, window, cx);
1415 assert_eq!(
1416 editor.selections.display_ranges(cx),
1417 &[empty_range(1, "ab⋯e".len())]
1418 );
1419
1420 editor.move_up(&MoveUp, window, cx);
1421 assert_eq!(
1422 editor.selections.display_ranges(cx),
1423 &[empty_range(0, "🟥🟧".len())]
1424 );
1425 editor.move_left(&MoveLeft, window, cx);
1426 assert_eq!(
1427 editor.selections.display_ranges(cx),
1428 &[empty_range(0, "🟥".len())]
1429 );
1430 editor.move_left(&MoveLeft, window, cx);
1431 assert_eq!(
1432 editor.selections.display_ranges(cx),
1433 &[empty_range(0, "".len())]
1434 );
1435 });
1436}
1437
1438#[gpui::test]
1439fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1440 init_test(cx, |_| {});
1441
1442 let editor = cx.add_window(|window, cx| {
1443 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1444 build_editor(buffer.clone(), window, cx)
1445 });
1446 _ = editor.update(cx, |editor, window, cx| {
1447 editor.change_selections(None, window, cx, |s| {
1448 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1449 });
1450
1451 // moving above start of document should move selection to start of document,
1452 // but the next move down should still be at the original goal_x
1453 editor.move_up(&MoveUp, window, cx);
1454 assert_eq!(
1455 editor.selections.display_ranges(cx),
1456 &[empty_range(0, "".len())]
1457 );
1458
1459 editor.move_down(&MoveDown, window, cx);
1460 assert_eq!(
1461 editor.selections.display_ranges(cx),
1462 &[empty_range(1, "abcd".len())]
1463 );
1464
1465 editor.move_down(&MoveDown, window, cx);
1466 assert_eq!(
1467 editor.selections.display_ranges(cx),
1468 &[empty_range(2, "αβγ".len())]
1469 );
1470
1471 editor.move_down(&MoveDown, window, cx);
1472 assert_eq!(
1473 editor.selections.display_ranges(cx),
1474 &[empty_range(3, "abcd".len())]
1475 );
1476
1477 editor.move_down(&MoveDown, window, cx);
1478 assert_eq!(
1479 editor.selections.display_ranges(cx),
1480 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1481 );
1482
1483 // moving past end of document should not change goal_x
1484 editor.move_down(&MoveDown, window, cx);
1485 assert_eq!(
1486 editor.selections.display_ranges(cx),
1487 &[empty_range(5, "".len())]
1488 );
1489
1490 editor.move_down(&MoveDown, window, cx);
1491 assert_eq!(
1492 editor.selections.display_ranges(cx),
1493 &[empty_range(5, "".len())]
1494 );
1495
1496 editor.move_up(&MoveUp, window, cx);
1497 assert_eq!(
1498 editor.selections.display_ranges(cx),
1499 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1500 );
1501
1502 editor.move_up(&MoveUp, window, cx);
1503 assert_eq!(
1504 editor.selections.display_ranges(cx),
1505 &[empty_range(3, "abcd".len())]
1506 );
1507
1508 editor.move_up(&MoveUp, window, cx);
1509 assert_eq!(
1510 editor.selections.display_ranges(cx),
1511 &[empty_range(2, "αβγ".len())]
1512 );
1513 });
1514}
1515
1516#[gpui::test]
1517fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1518 init_test(cx, |_| {});
1519 let move_to_beg = MoveToBeginningOfLine {
1520 stop_at_soft_wraps: true,
1521 stop_at_indent: true,
1522 };
1523
1524 let delete_to_beg = DeleteToBeginningOfLine {
1525 stop_at_indent: false,
1526 };
1527
1528 let move_to_end = MoveToEndOfLine {
1529 stop_at_soft_wraps: true,
1530 };
1531
1532 let editor = cx.add_window(|window, cx| {
1533 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1534 build_editor(buffer, window, cx)
1535 });
1536 _ = editor.update(cx, |editor, window, cx| {
1537 editor.change_selections(None, window, cx, |s| {
1538 s.select_display_ranges([
1539 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1540 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1541 ]);
1542 });
1543 });
1544
1545 _ = editor.update(cx, |editor, window, cx| {
1546 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1547 assert_eq!(
1548 editor.selections.display_ranges(cx),
1549 &[
1550 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1551 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1552 ]
1553 );
1554 });
1555
1556 _ = editor.update(cx, |editor, window, cx| {
1557 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1558 assert_eq!(
1559 editor.selections.display_ranges(cx),
1560 &[
1561 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1562 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1563 ]
1564 );
1565 });
1566
1567 _ = editor.update(cx, |editor, window, cx| {
1568 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1569 assert_eq!(
1570 editor.selections.display_ranges(cx),
1571 &[
1572 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1573 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1574 ]
1575 );
1576 });
1577
1578 _ = editor.update(cx, |editor, window, cx| {
1579 editor.move_to_end_of_line(&move_to_end, window, cx);
1580 assert_eq!(
1581 editor.selections.display_ranges(cx),
1582 &[
1583 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1584 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1585 ]
1586 );
1587 });
1588
1589 // Moving to the end of line again is a no-op.
1590 _ = editor.update(cx, |editor, window, cx| {
1591 editor.move_to_end_of_line(&move_to_end, window, cx);
1592 assert_eq!(
1593 editor.selections.display_ranges(cx),
1594 &[
1595 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1596 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1597 ]
1598 );
1599 });
1600
1601 _ = editor.update(cx, |editor, window, cx| {
1602 editor.move_left(&MoveLeft, window, cx);
1603 editor.select_to_beginning_of_line(
1604 &SelectToBeginningOfLine {
1605 stop_at_soft_wraps: true,
1606 stop_at_indent: true,
1607 },
1608 window,
1609 cx,
1610 );
1611 assert_eq!(
1612 editor.selections.display_ranges(cx),
1613 &[
1614 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1615 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1616 ]
1617 );
1618 });
1619
1620 _ = editor.update(cx, |editor, window, cx| {
1621 editor.select_to_beginning_of_line(
1622 &SelectToBeginningOfLine {
1623 stop_at_soft_wraps: true,
1624 stop_at_indent: true,
1625 },
1626 window,
1627 cx,
1628 );
1629 assert_eq!(
1630 editor.selections.display_ranges(cx),
1631 &[
1632 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1633 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1634 ]
1635 );
1636 });
1637
1638 _ = editor.update(cx, |editor, window, cx| {
1639 editor.select_to_beginning_of_line(
1640 &SelectToBeginningOfLine {
1641 stop_at_soft_wraps: true,
1642 stop_at_indent: true,
1643 },
1644 window,
1645 cx,
1646 );
1647 assert_eq!(
1648 editor.selections.display_ranges(cx),
1649 &[
1650 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1651 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1652 ]
1653 );
1654 });
1655
1656 _ = editor.update(cx, |editor, window, cx| {
1657 editor.select_to_end_of_line(
1658 &SelectToEndOfLine {
1659 stop_at_soft_wraps: true,
1660 },
1661 window,
1662 cx,
1663 );
1664 assert_eq!(
1665 editor.selections.display_ranges(cx),
1666 &[
1667 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1668 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1669 ]
1670 );
1671 });
1672
1673 _ = editor.update(cx, |editor, window, cx| {
1674 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1675 assert_eq!(editor.display_text(cx), "ab\n de");
1676 assert_eq!(
1677 editor.selections.display_ranges(cx),
1678 &[
1679 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1680 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1681 ]
1682 );
1683 });
1684
1685 _ = editor.update(cx, |editor, window, cx| {
1686 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1687 assert_eq!(editor.display_text(cx), "\n");
1688 assert_eq!(
1689 editor.selections.display_ranges(cx),
1690 &[
1691 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1692 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1693 ]
1694 );
1695 });
1696}
1697
1698#[gpui::test]
1699fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1700 init_test(cx, |_| {});
1701 let move_to_beg = MoveToBeginningOfLine {
1702 stop_at_soft_wraps: false,
1703 stop_at_indent: false,
1704 };
1705
1706 let move_to_end = MoveToEndOfLine {
1707 stop_at_soft_wraps: false,
1708 };
1709
1710 let editor = cx.add_window(|window, cx| {
1711 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1712 build_editor(buffer, window, cx)
1713 });
1714
1715 _ = editor.update(cx, |editor, window, cx| {
1716 editor.set_wrap_width(Some(140.0.into()), cx);
1717
1718 // We expect the following lines after wrapping
1719 // ```
1720 // thequickbrownfox
1721 // jumpedoverthelazydo
1722 // gs
1723 // ```
1724 // The final `gs` was soft-wrapped onto a new line.
1725 assert_eq!(
1726 "thequickbrownfox\njumpedoverthelaz\nydogs",
1727 editor.display_text(cx),
1728 );
1729
1730 // First, let's assert behavior on the first line, that was not soft-wrapped.
1731 // Start the cursor at the `k` on the first line
1732 editor.change_selections(None, window, cx, |s| {
1733 s.select_display_ranges([
1734 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1735 ]);
1736 });
1737
1738 // Moving to the beginning of the line should put us at the beginning of the line.
1739 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1740 assert_eq!(
1741 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1742 editor.selections.display_ranges(cx)
1743 );
1744
1745 // Moving to the end of the line should put us at the end of the line.
1746 editor.move_to_end_of_line(&move_to_end, window, cx);
1747 assert_eq!(
1748 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1749 editor.selections.display_ranges(cx)
1750 );
1751
1752 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1753 // Start the cursor at the last line (`y` that was wrapped to a new line)
1754 editor.change_selections(None, window, cx, |s| {
1755 s.select_display_ranges([
1756 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1757 ]);
1758 });
1759
1760 // Moving to the beginning of the line should put us at the start of the second line of
1761 // display text, i.e., the `j`.
1762 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1763 assert_eq!(
1764 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1765 editor.selections.display_ranges(cx)
1766 );
1767
1768 // Moving to the beginning of the line again should be a no-op.
1769 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1770 assert_eq!(
1771 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1772 editor.selections.display_ranges(cx)
1773 );
1774
1775 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1776 // next display line.
1777 editor.move_to_end_of_line(&move_to_end, window, cx);
1778 assert_eq!(
1779 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1780 editor.selections.display_ranges(cx)
1781 );
1782
1783 // Moving to the end of the line again should be a no-op.
1784 editor.move_to_end_of_line(&move_to_end, window, cx);
1785 assert_eq!(
1786 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1787 editor.selections.display_ranges(cx)
1788 );
1789 });
1790}
1791
1792#[gpui::test]
1793fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1794 init_test(cx, |_| {});
1795
1796 let move_to_beg = MoveToBeginningOfLine {
1797 stop_at_soft_wraps: true,
1798 stop_at_indent: true,
1799 };
1800
1801 let select_to_beg = SelectToBeginningOfLine {
1802 stop_at_soft_wraps: true,
1803 stop_at_indent: true,
1804 };
1805
1806 let delete_to_beg = DeleteToBeginningOfLine {
1807 stop_at_indent: true,
1808 };
1809
1810 let move_to_end = MoveToEndOfLine {
1811 stop_at_soft_wraps: false,
1812 };
1813
1814 let editor = cx.add_window(|window, cx| {
1815 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1816 build_editor(buffer, window, cx)
1817 });
1818
1819 _ = editor.update(cx, |editor, window, cx| {
1820 editor.change_selections(None, window, cx, |s| {
1821 s.select_display_ranges([
1822 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1823 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1824 ]);
1825 });
1826
1827 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1828 // and the second cursor at the first non-whitespace character in the line.
1829 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1830 assert_eq!(
1831 editor.selections.display_ranges(cx),
1832 &[
1833 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1834 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1835 ]
1836 );
1837
1838 // Moving to the beginning of the line again should be a no-op for the first cursor,
1839 // and should move the second cursor to the beginning of the line.
1840 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1841 assert_eq!(
1842 editor.selections.display_ranges(cx),
1843 &[
1844 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1845 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1846 ]
1847 );
1848
1849 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1850 // and should move the second cursor back to the first non-whitespace character in the line.
1851 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1852 assert_eq!(
1853 editor.selections.display_ranges(cx),
1854 &[
1855 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1856 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1857 ]
1858 );
1859
1860 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1861 // and to the first non-whitespace character in the line for the second cursor.
1862 editor.move_to_end_of_line(&move_to_end, window, cx);
1863 editor.move_left(&MoveLeft, window, cx);
1864 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1865 assert_eq!(
1866 editor.selections.display_ranges(cx),
1867 &[
1868 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1869 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1870 ]
1871 );
1872
1873 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1874 // and should select to the beginning of the line for the second cursor.
1875 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1876 assert_eq!(
1877 editor.selections.display_ranges(cx),
1878 &[
1879 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1880 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1881 ]
1882 );
1883
1884 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1885 // and should delete to the first non-whitespace character in the line for the second cursor.
1886 editor.move_to_end_of_line(&move_to_end, window, cx);
1887 editor.move_left(&MoveLeft, window, cx);
1888 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1889 assert_eq!(editor.text(cx), "c\n f");
1890 });
1891}
1892
1893#[gpui::test]
1894fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1895 init_test(cx, |_| {});
1896
1897 let editor = cx.add_window(|window, cx| {
1898 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1899 build_editor(buffer, window, cx)
1900 });
1901 _ = editor.update(cx, |editor, window, cx| {
1902 editor.change_selections(None, window, cx, |s| {
1903 s.select_display_ranges([
1904 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1905 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1906 ])
1907 });
1908
1909 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1910 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1911
1912 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1913 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", editor, cx);
1914
1915 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1916 assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1917
1918 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1919 assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1920
1921 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1922 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1923
1924 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1925 assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1926
1927 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1928 assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1929
1930 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1931 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1932
1933 editor.move_right(&MoveRight, window, cx);
1934 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1935 assert_selection_ranges(
1936 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1937 editor,
1938 cx,
1939 );
1940
1941 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1942 assert_selection_ranges(
1943 "use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}",
1944 editor,
1945 cx,
1946 );
1947
1948 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1949 assert_selection_ranges(
1950 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1951 editor,
1952 cx,
1953 );
1954 });
1955}
1956
1957#[gpui::test]
1958fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1959 init_test(cx, |_| {});
1960
1961 let editor = cx.add_window(|window, cx| {
1962 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1963 build_editor(buffer, window, cx)
1964 });
1965
1966 _ = editor.update(cx, |editor, window, cx| {
1967 editor.set_wrap_width(Some(140.0.into()), cx);
1968 assert_eq!(
1969 editor.display_text(cx),
1970 "use one::{\n two::three::\n four::five\n};"
1971 );
1972
1973 editor.change_selections(None, window, cx, |s| {
1974 s.select_display_ranges([
1975 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1976 ]);
1977 });
1978
1979 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1980 assert_eq!(
1981 editor.selections.display_ranges(cx),
1982 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1983 );
1984
1985 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1986 assert_eq!(
1987 editor.selections.display_ranges(cx),
1988 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1989 );
1990
1991 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1992 assert_eq!(
1993 editor.selections.display_ranges(cx),
1994 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1995 );
1996
1997 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1998 assert_eq!(
1999 editor.selections.display_ranges(cx),
2000 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2001 );
2002
2003 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2004 assert_eq!(
2005 editor.selections.display_ranges(cx),
2006 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2007 );
2008
2009 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2010 assert_eq!(
2011 editor.selections.display_ranges(cx),
2012 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2013 );
2014 });
2015}
2016
2017#[gpui::test]
2018async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2019 init_test(cx, |_| {});
2020 let mut cx = EditorTestContext::new(cx).await;
2021
2022 let line_height = cx.editor(|editor, window, _| {
2023 editor
2024 .style()
2025 .unwrap()
2026 .text
2027 .line_height_in_pixels(window.rem_size())
2028 });
2029 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2030
2031 cx.set_state(
2032 &r#"ˇone
2033 two
2034
2035 three
2036 fourˇ
2037 five
2038
2039 six"#
2040 .unindent(),
2041 );
2042
2043 cx.update_editor(|editor, window, cx| {
2044 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2045 });
2046 cx.assert_editor_state(
2047 &r#"one
2048 two
2049 ˇ
2050 three
2051 four
2052 five
2053 ˇ
2054 six"#
2055 .unindent(),
2056 );
2057
2058 cx.update_editor(|editor, window, cx| {
2059 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2060 });
2061 cx.assert_editor_state(
2062 &r#"one
2063 two
2064
2065 three
2066 four
2067 five
2068 ˇ
2069 sixˇ"#
2070 .unindent(),
2071 );
2072
2073 cx.update_editor(|editor, window, cx| {
2074 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2075 });
2076 cx.assert_editor_state(
2077 &r#"one
2078 two
2079
2080 three
2081 four
2082 five
2083
2084 sixˇ"#
2085 .unindent(),
2086 );
2087
2088 cx.update_editor(|editor, window, cx| {
2089 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2090 });
2091 cx.assert_editor_state(
2092 &r#"one
2093 two
2094
2095 three
2096 four
2097 five
2098 ˇ
2099 six"#
2100 .unindent(),
2101 );
2102
2103 cx.update_editor(|editor, window, cx| {
2104 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2105 });
2106 cx.assert_editor_state(
2107 &r#"one
2108 two
2109 ˇ
2110 three
2111 four
2112 five
2113
2114 six"#
2115 .unindent(),
2116 );
2117
2118 cx.update_editor(|editor, window, cx| {
2119 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2120 });
2121 cx.assert_editor_state(
2122 &r#"ˇone
2123 two
2124
2125 three
2126 four
2127 five
2128
2129 six"#
2130 .unindent(),
2131 );
2132}
2133
2134#[gpui::test]
2135async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2136 init_test(cx, |_| {});
2137 let mut cx = EditorTestContext::new(cx).await;
2138 let line_height = cx.editor(|editor, window, _| {
2139 editor
2140 .style()
2141 .unwrap()
2142 .text
2143 .line_height_in_pixels(window.rem_size())
2144 });
2145 let window = cx.window;
2146 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2147
2148 cx.set_state(
2149 r#"ˇone
2150 two
2151 three
2152 four
2153 five
2154 six
2155 seven
2156 eight
2157 nine
2158 ten
2159 "#,
2160 );
2161
2162 cx.update_editor(|editor, window, cx| {
2163 assert_eq!(
2164 editor.snapshot(window, cx).scroll_position(),
2165 gpui::Point::new(0., 0.)
2166 );
2167 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2168 assert_eq!(
2169 editor.snapshot(window, cx).scroll_position(),
2170 gpui::Point::new(0., 3.)
2171 );
2172 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2173 assert_eq!(
2174 editor.snapshot(window, cx).scroll_position(),
2175 gpui::Point::new(0., 6.)
2176 );
2177 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2178 assert_eq!(
2179 editor.snapshot(window, cx).scroll_position(),
2180 gpui::Point::new(0., 3.)
2181 );
2182
2183 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2184 assert_eq!(
2185 editor.snapshot(window, cx).scroll_position(),
2186 gpui::Point::new(0., 1.)
2187 );
2188 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2189 assert_eq!(
2190 editor.snapshot(window, cx).scroll_position(),
2191 gpui::Point::new(0., 3.)
2192 );
2193 });
2194}
2195
2196#[gpui::test]
2197async fn test_autoscroll(cx: &mut TestAppContext) {
2198 init_test(cx, |_| {});
2199 let mut cx = EditorTestContext::new(cx).await;
2200
2201 let line_height = cx.update_editor(|editor, window, cx| {
2202 editor.set_vertical_scroll_margin(2, cx);
2203 editor
2204 .style()
2205 .unwrap()
2206 .text
2207 .line_height_in_pixels(window.rem_size())
2208 });
2209 let window = cx.window;
2210 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2211
2212 cx.set_state(
2213 r#"ˇone
2214 two
2215 three
2216 four
2217 five
2218 six
2219 seven
2220 eight
2221 nine
2222 ten
2223 "#,
2224 );
2225 cx.update_editor(|editor, window, cx| {
2226 assert_eq!(
2227 editor.snapshot(window, cx).scroll_position(),
2228 gpui::Point::new(0., 0.0)
2229 );
2230 });
2231
2232 // Add a cursor below the visible area. Since both cursors cannot fit
2233 // on screen, the editor autoscrolls to reveal the newest cursor, and
2234 // allows the vertical scroll margin below that cursor.
2235 cx.update_editor(|editor, window, cx| {
2236 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2237 selections.select_ranges([
2238 Point::new(0, 0)..Point::new(0, 0),
2239 Point::new(6, 0)..Point::new(6, 0),
2240 ]);
2241 })
2242 });
2243 cx.update_editor(|editor, window, cx| {
2244 assert_eq!(
2245 editor.snapshot(window, cx).scroll_position(),
2246 gpui::Point::new(0., 3.0)
2247 );
2248 });
2249
2250 // Move down. The editor cursor scrolls down to track the newest cursor.
2251 cx.update_editor(|editor, window, cx| {
2252 editor.move_down(&Default::default(), window, cx);
2253 });
2254 cx.update_editor(|editor, window, cx| {
2255 assert_eq!(
2256 editor.snapshot(window, cx).scroll_position(),
2257 gpui::Point::new(0., 4.0)
2258 );
2259 });
2260
2261 // Add a cursor above the visible area. Since both cursors fit on screen,
2262 // the editor scrolls to show both.
2263 cx.update_editor(|editor, window, cx| {
2264 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2265 selections.select_ranges([
2266 Point::new(1, 0)..Point::new(1, 0),
2267 Point::new(6, 0)..Point::new(6, 0),
2268 ]);
2269 })
2270 });
2271 cx.update_editor(|editor, window, cx| {
2272 assert_eq!(
2273 editor.snapshot(window, cx).scroll_position(),
2274 gpui::Point::new(0., 1.0)
2275 );
2276 });
2277}
2278
2279#[gpui::test]
2280async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2281 init_test(cx, |_| {});
2282 let mut cx = EditorTestContext::new(cx).await;
2283
2284 let line_height = cx.editor(|editor, window, _cx| {
2285 editor
2286 .style()
2287 .unwrap()
2288 .text
2289 .line_height_in_pixels(window.rem_size())
2290 });
2291 let window = cx.window;
2292 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2293 cx.set_state(
2294 &r#"
2295 ˇone
2296 two
2297 threeˇ
2298 four
2299 five
2300 six
2301 seven
2302 eight
2303 nine
2304 ten
2305 "#
2306 .unindent(),
2307 );
2308
2309 cx.update_editor(|editor, window, cx| {
2310 editor.move_page_down(&MovePageDown::default(), window, cx)
2311 });
2312 cx.assert_editor_state(
2313 &r#"
2314 one
2315 two
2316 three
2317 ˇfour
2318 five
2319 sixˇ
2320 seven
2321 eight
2322 nine
2323 ten
2324 "#
2325 .unindent(),
2326 );
2327
2328 cx.update_editor(|editor, window, cx| {
2329 editor.move_page_down(&MovePageDown::default(), window, cx)
2330 });
2331 cx.assert_editor_state(
2332 &r#"
2333 one
2334 two
2335 three
2336 four
2337 five
2338 six
2339 ˇseven
2340 eight
2341 nineˇ
2342 ten
2343 "#
2344 .unindent(),
2345 );
2346
2347 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2348 cx.assert_editor_state(
2349 &r#"
2350 one
2351 two
2352 three
2353 ˇfour
2354 five
2355 sixˇ
2356 seven
2357 eight
2358 nine
2359 ten
2360 "#
2361 .unindent(),
2362 );
2363
2364 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2365 cx.assert_editor_state(
2366 &r#"
2367 ˇone
2368 two
2369 threeˇ
2370 four
2371 five
2372 six
2373 seven
2374 eight
2375 nine
2376 ten
2377 "#
2378 .unindent(),
2379 );
2380
2381 // Test select collapsing
2382 cx.update_editor(|editor, window, cx| {
2383 editor.move_page_down(&MovePageDown::default(), window, cx);
2384 editor.move_page_down(&MovePageDown::default(), window, cx);
2385 editor.move_page_down(&MovePageDown::default(), window, cx);
2386 });
2387 cx.assert_editor_state(
2388 &r#"
2389 one
2390 two
2391 three
2392 four
2393 five
2394 six
2395 seven
2396 eight
2397 nine
2398 ˇten
2399 ˇ"#
2400 .unindent(),
2401 );
2402}
2403
2404#[gpui::test]
2405async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2406 init_test(cx, |_| {});
2407 let mut cx = EditorTestContext::new(cx).await;
2408 cx.set_state("one «two threeˇ» four");
2409 cx.update_editor(|editor, window, cx| {
2410 editor.delete_to_beginning_of_line(
2411 &DeleteToBeginningOfLine {
2412 stop_at_indent: false,
2413 },
2414 window,
2415 cx,
2416 );
2417 assert_eq!(editor.text(cx), " four");
2418 });
2419}
2420
2421#[gpui::test]
2422fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2423 init_test(cx, |_| {});
2424
2425 let editor = cx.add_window(|window, cx| {
2426 let buffer = MultiBuffer::build_simple("one two three four", cx);
2427 build_editor(buffer.clone(), window, cx)
2428 });
2429
2430 _ = editor.update(cx, |editor, window, cx| {
2431 editor.change_selections(None, window, cx, |s| {
2432 s.select_display_ranges([
2433 // an empty selection - the preceding word fragment is deleted
2434 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2435 // characters selected - they are deleted
2436 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2437 ])
2438 });
2439 editor.delete_to_previous_word_start(
2440 &DeleteToPreviousWordStart {
2441 ignore_newlines: false,
2442 },
2443 window,
2444 cx,
2445 );
2446 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2447 });
2448
2449 _ = editor.update(cx, |editor, window, cx| {
2450 editor.change_selections(None, window, cx, |s| {
2451 s.select_display_ranges([
2452 // an empty selection - the following word fragment is deleted
2453 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2454 // characters selected - they are deleted
2455 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2456 ])
2457 });
2458 editor.delete_to_next_word_end(
2459 &DeleteToNextWordEnd {
2460 ignore_newlines: false,
2461 },
2462 window,
2463 cx,
2464 );
2465 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2466 });
2467}
2468
2469#[gpui::test]
2470fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2471 init_test(cx, |_| {});
2472
2473 let editor = cx.add_window(|window, cx| {
2474 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2475 build_editor(buffer.clone(), window, cx)
2476 });
2477 let del_to_prev_word_start = DeleteToPreviousWordStart {
2478 ignore_newlines: false,
2479 };
2480 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2481 ignore_newlines: true,
2482 };
2483
2484 _ = editor.update(cx, |editor, window, cx| {
2485 editor.change_selections(None, window, cx, |s| {
2486 s.select_display_ranges([
2487 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2488 ])
2489 });
2490 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2491 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2492 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2493 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2494 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2495 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2496 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2497 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2498 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2499 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2500 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2501 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2502 });
2503}
2504
2505#[gpui::test]
2506fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2507 init_test(cx, |_| {});
2508
2509 let editor = cx.add_window(|window, cx| {
2510 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2511 build_editor(buffer.clone(), window, cx)
2512 });
2513 let del_to_next_word_end = DeleteToNextWordEnd {
2514 ignore_newlines: false,
2515 };
2516 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2517 ignore_newlines: true,
2518 };
2519
2520 _ = editor.update(cx, |editor, window, cx| {
2521 editor.change_selections(None, window, cx, |s| {
2522 s.select_display_ranges([
2523 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2524 ])
2525 });
2526 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2527 assert_eq!(
2528 editor.buffer.read(cx).read(cx).text(),
2529 "one\n two\nthree\n four"
2530 );
2531 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2532 assert_eq!(
2533 editor.buffer.read(cx).read(cx).text(),
2534 "\n two\nthree\n four"
2535 );
2536 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2537 assert_eq!(
2538 editor.buffer.read(cx).read(cx).text(),
2539 "two\nthree\n four"
2540 );
2541 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2542 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2543 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2544 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2545 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2546 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2547 });
2548}
2549
2550#[gpui::test]
2551fn test_newline(cx: &mut TestAppContext) {
2552 init_test(cx, |_| {});
2553
2554 let editor = cx.add_window(|window, cx| {
2555 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2556 build_editor(buffer.clone(), window, cx)
2557 });
2558
2559 _ = editor.update(cx, |editor, window, cx| {
2560 editor.change_selections(None, window, cx, |s| {
2561 s.select_display_ranges([
2562 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2563 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2564 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2565 ])
2566 });
2567
2568 editor.newline(&Newline, window, cx);
2569 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2570 });
2571}
2572
2573#[gpui::test]
2574fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2575 init_test(cx, |_| {});
2576
2577 let editor = cx.add_window(|window, cx| {
2578 let buffer = MultiBuffer::build_simple(
2579 "
2580 a
2581 b(
2582 X
2583 )
2584 c(
2585 X
2586 )
2587 "
2588 .unindent()
2589 .as_str(),
2590 cx,
2591 );
2592 let mut editor = build_editor(buffer.clone(), window, cx);
2593 editor.change_selections(None, window, cx, |s| {
2594 s.select_ranges([
2595 Point::new(2, 4)..Point::new(2, 5),
2596 Point::new(5, 4)..Point::new(5, 5),
2597 ])
2598 });
2599 editor
2600 });
2601
2602 _ = editor.update(cx, |editor, window, cx| {
2603 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2604 editor.buffer.update(cx, |buffer, cx| {
2605 buffer.edit(
2606 [
2607 (Point::new(1, 2)..Point::new(3, 0), ""),
2608 (Point::new(4, 2)..Point::new(6, 0), ""),
2609 ],
2610 None,
2611 cx,
2612 );
2613 assert_eq!(
2614 buffer.read(cx).text(),
2615 "
2616 a
2617 b()
2618 c()
2619 "
2620 .unindent()
2621 );
2622 });
2623 assert_eq!(
2624 editor.selections.ranges(cx),
2625 &[
2626 Point::new(1, 2)..Point::new(1, 2),
2627 Point::new(2, 2)..Point::new(2, 2),
2628 ],
2629 );
2630
2631 editor.newline(&Newline, window, cx);
2632 assert_eq!(
2633 editor.text(cx),
2634 "
2635 a
2636 b(
2637 )
2638 c(
2639 )
2640 "
2641 .unindent()
2642 );
2643
2644 // The selections are moved after the inserted newlines
2645 assert_eq!(
2646 editor.selections.ranges(cx),
2647 &[
2648 Point::new(2, 0)..Point::new(2, 0),
2649 Point::new(4, 0)..Point::new(4, 0),
2650 ],
2651 );
2652 });
2653}
2654
2655#[gpui::test]
2656async fn test_newline_above(cx: &mut TestAppContext) {
2657 init_test(cx, |settings| {
2658 settings.defaults.tab_size = NonZeroU32::new(4)
2659 });
2660
2661 let language = Arc::new(
2662 Language::new(
2663 LanguageConfig::default(),
2664 Some(tree_sitter_rust::LANGUAGE.into()),
2665 )
2666 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2667 .unwrap(),
2668 );
2669
2670 let mut cx = EditorTestContext::new(cx).await;
2671 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2672 cx.set_state(indoc! {"
2673 const a: ˇA = (
2674 (ˇ
2675 «const_functionˇ»(ˇ),
2676 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2677 )ˇ
2678 ˇ);ˇ
2679 "});
2680
2681 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2682 cx.assert_editor_state(indoc! {"
2683 ˇ
2684 const a: A = (
2685 ˇ
2686 (
2687 ˇ
2688 ˇ
2689 const_function(),
2690 ˇ
2691 ˇ
2692 ˇ
2693 ˇ
2694 something_else,
2695 ˇ
2696 )
2697 ˇ
2698 ˇ
2699 );
2700 "});
2701}
2702
2703#[gpui::test]
2704async fn test_newline_below(cx: &mut TestAppContext) {
2705 init_test(cx, |settings| {
2706 settings.defaults.tab_size = NonZeroU32::new(4)
2707 });
2708
2709 let language = Arc::new(
2710 Language::new(
2711 LanguageConfig::default(),
2712 Some(tree_sitter_rust::LANGUAGE.into()),
2713 )
2714 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2715 .unwrap(),
2716 );
2717
2718 let mut cx = EditorTestContext::new(cx).await;
2719 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2720 cx.set_state(indoc! {"
2721 const a: ˇA = (
2722 (ˇ
2723 «const_functionˇ»(ˇ),
2724 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2725 )ˇ
2726 ˇ);ˇ
2727 "});
2728
2729 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2730 cx.assert_editor_state(indoc! {"
2731 const a: A = (
2732 ˇ
2733 (
2734 ˇ
2735 const_function(),
2736 ˇ
2737 ˇ
2738 something_else,
2739 ˇ
2740 ˇ
2741 ˇ
2742 ˇ
2743 )
2744 ˇ
2745 );
2746 ˇ
2747 ˇ
2748 "});
2749}
2750
2751#[gpui::test]
2752async fn test_newline_comments(cx: &mut TestAppContext) {
2753 init_test(cx, |settings| {
2754 settings.defaults.tab_size = NonZeroU32::new(4)
2755 });
2756
2757 let language = Arc::new(Language::new(
2758 LanguageConfig {
2759 line_comments: vec!["//".into()],
2760 ..LanguageConfig::default()
2761 },
2762 None,
2763 ));
2764 {
2765 let mut cx = EditorTestContext::new(cx).await;
2766 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2767 cx.set_state(indoc! {"
2768 // Fooˇ
2769 "});
2770
2771 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2772 cx.assert_editor_state(indoc! {"
2773 // Foo
2774 //ˇ
2775 "});
2776 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2777 cx.set_state(indoc! {"
2778 ˇ// Foo
2779 "});
2780 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2781 cx.assert_editor_state(indoc! {"
2782
2783 ˇ// Foo
2784 "});
2785 }
2786 // Ensure that comment continuations can be disabled.
2787 update_test_language_settings(cx, |settings| {
2788 settings.defaults.extend_comment_on_newline = Some(false);
2789 });
2790 let mut cx = EditorTestContext::new(cx).await;
2791 cx.set_state(indoc! {"
2792 // Fooˇ
2793 "});
2794 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2795 cx.assert_editor_state(indoc! {"
2796 // Foo
2797 ˇ
2798 "});
2799}
2800
2801#[gpui::test]
2802fn test_insert_with_old_selections(cx: &mut TestAppContext) {
2803 init_test(cx, |_| {});
2804
2805 let editor = cx.add_window(|window, cx| {
2806 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
2807 let mut editor = build_editor(buffer.clone(), window, cx);
2808 editor.change_selections(None, window, cx, |s| {
2809 s.select_ranges([3..4, 11..12, 19..20])
2810 });
2811 editor
2812 });
2813
2814 _ = editor.update(cx, |editor, window, cx| {
2815 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2816 editor.buffer.update(cx, |buffer, cx| {
2817 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
2818 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
2819 });
2820 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
2821
2822 editor.insert("Z", window, cx);
2823 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
2824
2825 // The selections are moved after the inserted characters
2826 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
2827 });
2828}
2829
2830#[gpui::test]
2831async fn test_tab(cx: &mut TestAppContext) {
2832 init_test(cx, |settings| {
2833 settings.defaults.tab_size = NonZeroU32::new(3)
2834 });
2835
2836 let mut cx = EditorTestContext::new(cx).await;
2837 cx.set_state(indoc! {"
2838 ˇabˇc
2839 ˇ🏀ˇ🏀ˇefg
2840 dˇ
2841 "});
2842 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2843 cx.assert_editor_state(indoc! {"
2844 ˇab ˇc
2845 ˇ🏀 ˇ🏀 ˇefg
2846 d ˇ
2847 "});
2848
2849 cx.set_state(indoc! {"
2850 a
2851 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2852 "});
2853 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2854 cx.assert_editor_state(indoc! {"
2855 a
2856 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2857 "});
2858}
2859
2860#[gpui::test]
2861async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
2862 init_test(cx, |_| {});
2863
2864 let mut cx = EditorTestContext::new(cx).await;
2865 let language = Arc::new(
2866 Language::new(
2867 LanguageConfig::default(),
2868 Some(tree_sitter_rust::LANGUAGE.into()),
2869 )
2870 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2871 .unwrap(),
2872 );
2873 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2874
2875 // cursors that are already at the suggested indent level insert
2876 // a soft tab. cursors that are to the left of the suggested indent
2877 // auto-indent their line.
2878 cx.set_state(indoc! {"
2879 ˇ
2880 const a: B = (
2881 c(
2882 d(
2883 ˇ
2884 )
2885 ˇ
2886 ˇ )
2887 );
2888 "});
2889 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2890 cx.assert_editor_state(indoc! {"
2891 ˇ
2892 const a: B = (
2893 c(
2894 d(
2895 ˇ
2896 )
2897 ˇ
2898 ˇ)
2899 );
2900 "});
2901
2902 // handle auto-indent when there are multiple cursors on the same line
2903 cx.set_state(indoc! {"
2904 const a: B = (
2905 c(
2906 ˇ ˇ
2907 ˇ )
2908 );
2909 "});
2910 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2911 cx.assert_editor_state(indoc! {"
2912 const a: B = (
2913 c(
2914 ˇ
2915 ˇ)
2916 );
2917 "});
2918}
2919
2920#[gpui::test]
2921async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
2922 init_test(cx, |settings| {
2923 settings.defaults.tab_size = NonZeroU32::new(3)
2924 });
2925
2926 let mut cx = EditorTestContext::new(cx).await;
2927 cx.set_state(indoc! {"
2928 ˇ
2929 \t ˇ
2930 \t ˇ
2931 \t ˇ
2932 \t \t\t \t \t\t \t\t \t \t ˇ
2933 "});
2934
2935 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2936 cx.assert_editor_state(indoc! {"
2937 ˇ
2938 \t ˇ
2939 \t ˇ
2940 \t ˇ
2941 \t \t\t \t \t\t \t\t \t \t ˇ
2942 "});
2943}
2944
2945#[gpui::test]
2946async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
2947 init_test(cx, |settings| {
2948 settings.defaults.tab_size = NonZeroU32::new(4)
2949 });
2950
2951 let language = Arc::new(
2952 Language::new(
2953 LanguageConfig::default(),
2954 Some(tree_sitter_rust::LANGUAGE.into()),
2955 )
2956 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
2957 .unwrap(),
2958 );
2959
2960 let mut cx = EditorTestContext::new(cx).await;
2961 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2962 cx.set_state(indoc! {"
2963 fn a() {
2964 if b {
2965 \t ˇc
2966 }
2967 }
2968 "});
2969
2970 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2971 cx.assert_editor_state(indoc! {"
2972 fn a() {
2973 if b {
2974 ˇc
2975 }
2976 }
2977 "});
2978}
2979
2980#[gpui::test]
2981async fn test_indent_outdent(cx: &mut TestAppContext) {
2982 init_test(cx, |settings| {
2983 settings.defaults.tab_size = NonZeroU32::new(4);
2984 });
2985
2986 let mut cx = EditorTestContext::new(cx).await;
2987
2988 cx.set_state(indoc! {"
2989 «oneˇ» «twoˇ»
2990 three
2991 four
2992 "});
2993 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2994 cx.assert_editor_state(indoc! {"
2995 «oneˇ» «twoˇ»
2996 three
2997 four
2998 "});
2999
3000 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3001 cx.assert_editor_state(indoc! {"
3002 «oneˇ» «twoˇ»
3003 three
3004 four
3005 "});
3006
3007 // select across line ending
3008 cx.set_state(indoc! {"
3009 one two
3010 t«hree
3011 ˇ» four
3012 "});
3013 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3014 cx.assert_editor_state(indoc! {"
3015 one two
3016 t«hree
3017 ˇ» four
3018 "});
3019
3020 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3021 cx.assert_editor_state(indoc! {"
3022 one two
3023 t«hree
3024 ˇ» four
3025 "});
3026
3027 // Ensure that indenting/outdenting works when the cursor is at column 0.
3028 cx.set_state(indoc! {"
3029 one two
3030 ˇthree
3031 four
3032 "});
3033 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3034 cx.assert_editor_state(indoc! {"
3035 one two
3036 ˇthree
3037 four
3038 "});
3039
3040 cx.set_state(indoc! {"
3041 one two
3042 ˇ three
3043 four
3044 "});
3045 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3046 cx.assert_editor_state(indoc! {"
3047 one two
3048 ˇthree
3049 four
3050 "});
3051}
3052
3053#[gpui::test]
3054async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3055 init_test(cx, |settings| {
3056 settings.defaults.hard_tabs = Some(true);
3057 });
3058
3059 let mut cx = EditorTestContext::new(cx).await;
3060
3061 // select two ranges on one line
3062 cx.set_state(indoc! {"
3063 «oneˇ» «twoˇ»
3064 three
3065 four
3066 "});
3067 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3068 cx.assert_editor_state(indoc! {"
3069 \t«oneˇ» «twoˇ»
3070 three
3071 four
3072 "});
3073 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3074 cx.assert_editor_state(indoc! {"
3075 \t\t«oneˇ» «twoˇ»
3076 three
3077 four
3078 "});
3079 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3080 cx.assert_editor_state(indoc! {"
3081 \t«oneˇ» «twoˇ»
3082 three
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 three
3089 four
3090 "});
3091
3092 // select across a line ending
3093 cx.set_state(indoc! {"
3094 one two
3095 t«hree
3096 ˇ»four
3097 "});
3098 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3099 cx.assert_editor_state(indoc! {"
3100 one two
3101 \tt«hree
3102 ˇ»four
3103 "});
3104 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3105 cx.assert_editor_state(indoc! {"
3106 one two
3107 \t\tt«hree
3108 ˇ»four
3109 "});
3110 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3111 cx.assert_editor_state(indoc! {"
3112 one two
3113 \tt«hree
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 t«hree
3120 ˇ»four
3121 "});
3122
3123 // Ensure that indenting/outdenting works when the cursor is at column 0.
3124 cx.set_state(indoc! {"
3125 one two
3126 ˇthree
3127 four
3128 "});
3129 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3130 cx.assert_editor_state(indoc! {"
3131 one two
3132 ˇthree
3133 four
3134 "});
3135 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3136 cx.assert_editor_state(indoc! {"
3137 one two
3138 \tˇthree
3139 four
3140 "});
3141 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3142 cx.assert_editor_state(indoc! {"
3143 one two
3144 ˇthree
3145 four
3146 "});
3147}
3148
3149#[gpui::test]
3150fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3151 init_test(cx, |settings| {
3152 settings.languages.extend([
3153 (
3154 "TOML".into(),
3155 LanguageSettingsContent {
3156 tab_size: NonZeroU32::new(2),
3157 ..Default::default()
3158 },
3159 ),
3160 (
3161 "Rust".into(),
3162 LanguageSettingsContent {
3163 tab_size: NonZeroU32::new(4),
3164 ..Default::default()
3165 },
3166 ),
3167 ]);
3168 });
3169
3170 let toml_language = Arc::new(Language::new(
3171 LanguageConfig {
3172 name: "TOML".into(),
3173 ..Default::default()
3174 },
3175 None,
3176 ));
3177 let rust_language = Arc::new(Language::new(
3178 LanguageConfig {
3179 name: "Rust".into(),
3180 ..Default::default()
3181 },
3182 None,
3183 ));
3184
3185 let toml_buffer =
3186 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3187 let rust_buffer =
3188 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3189 let multibuffer = cx.new(|cx| {
3190 let mut multibuffer = MultiBuffer::new(ReadWrite);
3191 multibuffer.push_excerpts(
3192 toml_buffer.clone(),
3193 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3194 cx,
3195 );
3196 multibuffer.push_excerpts(
3197 rust_buffer.clone(),
3198 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3199 cx,
3200 );
3201 multibuffer
3202 });
3203
3204 cx.add_window(|window, cx| {
3205 let mut editor = build_editor(multibuffer, window, cx);
3206
3207 assert_eq!(
3208 editor.text(cx),
3209 indoc! {"
3210 a = 1
3211 b = 2
3212
3213 const c: usize = 3;
3214 "}
3215 );
3216
3217 select_ranges(
3218 &mut editor,
3219 indoc! {"
3220 «aˇ» = 1
3221 b = 2
3222
3223 «const c:ˇ» usize = 3;
3224 "},
3225 window,
3226 cx,
3227 );
3228
3229 editor.tab(&Tab, window, cx);
3230 assert_text_with_selections(
3231 &mut editor,
3232 indoc! {"
3233 «aˇ» = 1
3234 b = 2
3235
3236 «const c:ˇ» usize = 3;
3237 "},
3238 cx,
3239 );
3240 editor.backtab(&Backtab, window, cx);
3241 assert_text_with_selections(
3242 &mut editor,
3243 indoc! {"
3244 «aˇ» = 1
3245 b = 2
3246
3247 «const c:ˇ» usize = 3;
3248 "},
3249 cx,
3250 );
3251
3252 editor
3253 });
3254}
3255
3256#[gpui::test]
3257async fn test_backspace(cx: &mut TestAppContext) {
3258 init_test(cx, |_| {});
3259
3260 let mut cx = EditorTestContext::new(cx).await;
3261
3262 // Basic backspace
3263 cx.set_state(indoc! {"
3264 onˇe two three
3265 fou«rˇ» five six
3266 seven «ˇeight nine
3267 »ten
3268 "});
3269 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3270 cx.assert_editor_state(indoc! {"
3271 oˇe two three
3272 fouˇ five six
3273 seven ˇten
3274 "});
3275
3276 // Test backspace inside and around indents
3277 cx.set_state(indoc! {"
3278 zero
3279 ˇone
3280 ˇtwo
3281 ˇ ˇ ˇ three
3282 ˇ ˇ four
3283 "});
3284 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3285 cx.assert_editor_state(indoc! {"
3286 zero
3287 ˇone
3288 ˇtwo
3289 ˇ threeˇ four
3290 "});
3291}
3292
3293#[gpui::test]
3294async fn test_delete(cx: &mut TestAppContext) {
3295 init_test(cx, |_| {});
3296
3297 let mut cx = EditorTestContext::new(cx).await;
3298 cx.set_state(indoc! {"
3299 onˇe two three
3300 fou«rˇ» five six
3301 seven «ˇeight nine
3302 »ten
3303 "});
3304 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3305 cx.assert_editor_state(indoc! {"
3306 onˇ two three
3307 fouˇ five six
3308 seven ˇten
3309 "});
3310}
3311
3312#[gpui::test]
3313fn test_delete_line(cx: &mut TestAppContext) {
3314 init_test(cx, |_| {});
3315
3316 let editor = cx.add_window(|window, cx| {
3317 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3318 build_editor(buffer, window, cx)
3319 });
3320 _ = editor.update(cx, |editor, window, cx| {
3321 editor.change_selections(None, window, cx, |s| {
3322 s.select_display_ranges([
3323 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3324 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3325 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3326 ])
3327 });
3328 editor.delete_line(&DeleteLine, window, cx);
3329 assert_eq!(editor.display_text(cx), "ghi");
3330 assert_eq!(
3331 editor.selections.display_ranges(cx),
3332 vec![
3333 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3334 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3335 ]
3336 );
3337 });
3338
3339 let editor = cx.add_window(|window, cx| {
3340 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3341 build_editor(buffer, window, cx)
3342 });
3343 _ = editor.update(cx, |editor, window, cx| {
3344 editor.change_selections(None, window, cx, |s| {
3345 s.select_display_ranges([
3346 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3347 ])
3348 });
3349 editor.delete_line(&DeleteLine, window, cx);
3350 assert_eq!(editor.display_text(cx), "ghi\n");
3351 assert_eq!(
3352 editor.selections.display_ranges(cx),
3353 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3354 );
3355 });
3356}
3357
3358#[gpui::test]
3359fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3360 init_test(cx, |_| {});
3361
3362 cx.add_window(|window, cx| {
3363 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3364 let mut editor = build_editor(buffer.clone(), window, cx);
3365 let buffer = buffer.read(cx).as_singleton().unwrap();
3366
3367 assert_eq!(
3368 editor.selections.ranges::<Point>(cx),
3369 &[Point::new(0, 0)..Point::new(0, 0)]
3370 );
3371
3372 // When on single line, replace newline at end by space
3373 editor.join_lines(&JoinLines, window, cx);
3374 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3375 assert_eq!(
3376 editor.selections.ranges::<Point>(cx),
3377 &[Point::new(0, 3)..Point::new(0, 3)]
3378 );
3379
3380 // When multiple lines are selected, remove newlines that are spanned by the selection
3381 editor.change_selections(None, window, cx, |s| {
3382 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3383 });
3384 editor.join_lines(&JoinLines, window, cx);
3385 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3386 assert_eq!(
3387 editor.selections.ranges::<Point>(cx),
3388 &[Point::new(0, 11)..Point::new(0, 11)]
3389 );
3390
3391 // Undo should be transactional
3392 editor.undo(&Undo, window, cx);
3393 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3394 assert_eq!(
3395 editor.selections.ranges::<Point>(cx),
3396 &[Point::new(0, 5)..Point::new(2, 2)]
3397 );
3398
3399 // When joining an empty line don't insert a space
3400 editor.change_selections(None, window, cx, |s| {
3401 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3402 });
3403 editor.join_lines(&JoinLines, window, cx);
3404 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3405 assert_eq!(
3406 editor.selections.ranges::<Point>(cx),
3407 [Point::new(2, 3)..Point::new(2, 3)]
3408 );
3409
3410 // We can remove trailing newlines
3411 editor.join_lines(&JoinLines, window, cx);
3412 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3413 assert_eq!(
3414 editor.selections.ranges::<Point>(cx),
3415 [Point::new(2, 3)..Point::new(2, 3)]
3416 );
3417
3418 // We don't blow up on the last line
3419 editor.join_lines(&JoinLines, window, cx);
3420 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3421 assert_eq!(
3422 editor.selections.ranges::<Point>(cx),
3423 [Point::new(2, 3)..Point::new(2, 3)]
3424 );
3425
3426 // reset to test indentation
3427 editor.buffer.update(cx, |buffer, cx| {
3428 buffer.edit(
3429 [
3430 (Point::new(1, 0)..Point::new(1, 2), " "),
3431 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3432 ],
3433 None,
3434 cx,
3435 )
3436 });
3437
3438 // We remove any leading spaces
3439 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3440 editor.change_selections(None, window, cx, |s| {
3441 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3442 });
3443 editor.join_lines(&JoinLines, window, cx);
3444 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3445
3446 // We don't insert a space for a line containing only spaces
3447 editor.join_lines(&JoinLines, window, cx);
3448 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3449
3450 // We ignore any leading tabs
3451 editor.join_lines(&JoinLines, window, cx);
3452 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3453
3454 editor
3455 });
3456}
3457
3458#[gpui::test]
3459fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3460 init_test(cx, |_| {});
3461
3462 cx.add_window(|window, cx| {
3463 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3464 let mut editor = build_editor(buffer.clone(), window, cx);
3465 let buffer = buffer.read(cx).as_singleton().unwrap();
3466
3467 editor.change_selections(None, window, cx, |s| {
3468 s.select_ranges([
3469 Point::new(0, 2)..Point::new(1, 1),
3470 Point::new(1, 2)..Point::new(1, 2),
3471 Point::new(3, 1)..Point::new(3, 2),
3472 ])
3473 });
3474
3475 editor.join_lines(&JoinLines, window, cx);
3476 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3477
3478 assert_eq!(
3479 editor.selections.ranges::<Point>(cx),
3480 [
3481 Point::new(0, 7)..Point::new(0, 7),
3482 Point::new(1, 3)..Point::new(1, 3)
3483 ]
3484 );
3485 editor
3486 });
3487}
3488
3489#[gpui::test]
3490async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3491 init_test(cx, |_| {});
3492
3493 let mut cx = EditorTestContext::new(cx).await;
3494
3495 let diff_base = r#"
3496 Line 0
3497 Line 1
3498 Line 2
3499 Line 3
3500 "#
3501 .unindent();
3502
3503 cx.set_state(
3504 &r#"
3505 ˇLine 0
3506 Line 1
3507 Line 2
3508 Line 3
3509 "#
3510 .unindent(),
3511 );
3512
3513 cx.set_head_text(&diff_base);
3514 executor.run_until_parked();
3515
3516 // Join lines
3517 cx.update_editor(|editor, window, cx| {
3518 editor.join_lines(&JoinLines, window, cx);
3519 });
3520 executor.run_until_parked();
3521
3522 cx.assert_editor_state(
3523 &r#"
3524 Line 0ˇ Line 1
3525 Line 2
3526 Line 3
3527 "#
3528 .unindent(),
3529 );
3530 // Join again
3531 cx.update_editor(|editor, window, cx| {
3532 editor.join_lines(&JoinLines, window, cx);
3533 });
3534 executor.run_until_parked();
3535
3536 cx.assert_editor_state(
3537 &r#"
3538 Line 0 Line 1ˇ Line 2
3539 Line 3
3540 "#
3541 .unindent(),
3542 );
3543}
3544
3545#[gpui::test]
3546async fn test_custom_newlines_cause_no_false_positive_diffs(
3547 executor: BackgroundExecutor,
3548 cx: &mut TestAppContext,
3549) {
3550 init_test(cx, |_| {});
3551 let mut cx = EditorTestContext::new(cx).await;
3552 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3553 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3554 executor.run_until_parked();
3555
3556 cx.update_editor(|editor, window, cx| {
3557 let snapshot = editor.snapshot(window, cx);
3558 assert_eq!(
3559 snapshot
3560 .buffer_snapshot
3561 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3562 .collect::<Vec<_>>(),
3563 Vec::new(),
3564 "Should not have any diffs for files with custom newlines"
3565 );
3566 });
3567}
3568
3569#[gpui::test]
3570async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3571 init_test(cx, |_| {});
3572
3573 let mut cx = EditorTestContext::new(cx).await;
3574
3575 // Test sort_lines_case_insensitive()
3576 cx.set_state(indoc! {"
3577 «z
3578 y
3579 x
3580 Z
3581 Y
3582 Xˇ»
3583 "});
3584 cx.update_editor(|e, window, cx| {
3585 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3586 });
3587 cx.assert_editor_state(indoc! {"
3588 «x
3589 X
3590 y
3591 Y
3592 z
3593 Zˇ»
3594 "});
3595
3596 // Test reverse_lines()
3597 cx.set_state(indoc! {"
3598 «5
3599 4
3600 3
3601 2
3602 1ˇ»
3603 "});
3604 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
3605 cx.assert_editor_state(indoc! {"
3606 «1
3607 2
3608 3
3609 4
3610 5ˇ»
3611 "});
3612
3613 // Skip testing shuffle_line()
3614
3615 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
3616 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
3617
3618 // Don't manipulate when cursor is on single line, but expand the selection
3619 cx.set_state(indoc! {"
3620 ddˇdd
3621 ccc
3622 bb
3623 a
3624 "});
3625 cx.update_editor(|e, window, cx| {
3626 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3627 });
3628 cx.assert_editor_state(indoc! {"
3629 «ddddˇ»
3630 ccc
3631 bb
3632 a
3633 "});
3634
3635 // Basic manipulate case
3636 // Start selection moves to column 0
3637 // End of selection shrinks to fit shorter line
3638 cx.set_state(indoc! {"
3639 dd«d
3640 ccc
3641 bb
3642 aaaaaˇ»
3643 "});
3644 cx.update_editor(|e, window, cx| {
3645 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3646 });
3647 cx.assert_editor_state(indoc! {"
3648 «aaaaa
3649 bb
3650 ccc
3651 dddˇ»
3652 "});
3653
3654 // Manipulate case with newlines
3655 cx.set_state(indoc! {"
3656 dd«d
3657 ccc
3658
3659 bb
3660 aaaaa
3661
3662 ˇ»
3663 "});
3664 cx.update_editor(|e, window, cx| {
3665 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3666 });
3667 cx.assert_editor_state(indoc! {"
3668 «
3669
3670 aaaaa
3671 bb
3672 ccc
3673 dddˇ»
3674
3675 "});
3676
3677 // Adding new line
3678 cx.set_state(indoc! {"
3679 aa«a
3680 bbˇ»b
3681 "});
3682 cx.update_editor(|e, window, cx| {
3683 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
3684 });
3685 cx.assert_editor_state(indoc! {"
3686 «aaa
3687 bbb
3688 added_lineˇ»
3689 "});
3690
3691 // Removing line
3692 cx.set_state(indoc! {"
3693 aa«a
3694 bbbˇ»
3695 "});
3696 cx.update_editor(|e, window, cx| {
3697 e.manipulate_lines(window, cx, |lines| {
3698 lines.pop();
3699 })
3700 });
3701 cx.assert_editor_state(indoc! {"
3702 «aaaˇ»
3703 "});
3704
3705 // Removing all lines
3706 cx.set_state(indoc! {"
3707 aa«a
3708 bbbˇ»
3709 "});
3710 cx.update_editor(|e, window, cx| {
3711 e.manipulate_lines(window, cx, |lines| {
3712 lines.drain(..);
3713 })
3714 });
3715 cx.assert_editor_state(indoc! {"
3716 ˇ
3717 "});
3718}
3719
3720#[gpui::test]
3721async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
3722 init_test(cx, |_| {});
3723
3724 let mut cx = EditorTestContext::new(cx).await;
3725
3726 // Consider continuous selection as single selection
3727 cx.set_state(indoc! {"
3728 Aaa«aa
3729 cˇ»c«c
3730 bb
3731 aaaˇ»aa
3732 "});
3733 cx.update_editor(|e, window, cx| {
3734 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3735 });
3736 cx.assert_editor_state(indoc! {"
3737 «Aaaaa
3738 ccc
3739 bb
3740 aaaaaˇ»
3741 "});
3742
3743 cx.set_state(indoc! {"
3744 Aaa«aa
3745 cˇ»c«c
3746 bb
3747 aaaˇ»aa
3748 "});
3749 cx.update_editor(|e, window, cx| {
3750 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3751 });
3752 cx.assert_editor_state(indoc! {"
3753 «Aaaaa
3754 ccc
3755 bbˇ»
3756 "});
3757
3758 // Consider non continuous selection as distinct dedup operations
3759 cx.set_state(indoc! {"
3760 «aaaaa
3761 bb
3762 aaaaa
3763 aaaaaˇ»
3764
3765 aaa«aaˇ»
3766 "});
3767 cx.update_editor(|e, window, cx| {
3768 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3769 });
3770 cx.assert_editor_state(indoc! {"
3771 «aaaaa
3772 bbˇ»
3773
3774 «aaaaaˇ»
3775 "});
3776}
3777
3778#[gpui::test]
3779async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
3780 init_test(cx, |_| {});
3781
3782 let mut cx = EditorTestContext::new(cx).await;
3783
3784 cx.set_state(indoc! {"
3785 «Aaa
3786 aAa
3787 Aaaˇ»
3788 "});
3789 cx.update_editor(|e, window, cx| {
3790 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3791 });
3792 cx.assert_editor_state(indoc! {"
3793 «Aaa
3794 aAaˇ»
3795 "});
3796
3797 cx.set_state(indoc! {"
3798 «Aaa
3799 aAa
3800 aaAˇ»
3801 "});
3802 cx.update_editor(|e, window, cx| {
3803 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3804 });
3805 cx.assert_editor_state(indoc! {"
3806 «Aaaˇ»
3807 "});
3808}
3809
3810#[gpui::test]
3811async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
3812 init_test(cx, |_| {});
3813
3814 let mut cx = EditorTestContext::new(cx).await;
3815
3816 // Manipulate with multiple selections on a single line
3817 cx.set_state(indoc! {"
3818 dd«dd
3819 cˇ»c«c
3820 bb
3821 aaaˇ»aa
3822 "});
3823 cx.update_editor(|e, window, cx| {
3824 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3825 });
3826 cx.assert_editor_state(indoc! {"
3827 «aaaaa
3828 bb
3829 ccc
3830 ddddˇ»
3831 "});
3832
3833 // Manipulate with multiple disjoin selections
3834 cx.set_state(indoc! {"
3835 5«
3836 4
3837 3
3838 2
3839 1ˇ»
3840
3841 dd«dd
3842 ccc
3843 bb
3844 aaaˇ»aa
3845 "});
3846 cx.update_editor(|e, window, cx| {
3847 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3848 });
3849 cx.assert_editor_state(indoc! {"
3850 «1
3851 2
3852 3
3853 4
3854 5ˇ»
3855
3856 «aaaaa
3857 bb
3858 ccc
3859 ddddˇ»
3860 "});
3861
3862 // Adding lines on each selection
3863 cx.set_state(indoc! {"
3864 2«
3865 1ˇ»
3866
3867 bb«bb
3868 aaaˇ»aa
3869 "});
3870 cx.update_editor(|e, window, cx| {
3871 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
3872 });
3873 cx.assert_editor_state(indoc! {"
3874 «2
3875 1
3876 added lineˇ»
3877
3878 «bbbb
3879 aaaaa
3880 added lineˇ»
3881 "});
3882
3883 // Removing lines on each selection
3884 cx.set_state(indoc! {"
3885 2«
3886 1ˇ»
3887
3888 bb«bb
3889 aaaˇ»aa
3890 "});
3891 cx.update_editor(|e, window, cx| {
3892 e.manipulate_lines(window, cx, |lines| {
3893 lines.pop();
3894 })
3895 });
3896 cx.assert_editor_state(indoc! {"
3897 «2ˇ»
3898
3899 «bbbbˇ»
3900 "});
3901}
3902
3903#[gpui::test]
3904async fn test_toggle_case(cx: &mut TestAppContext) {
3905 init_test(cx, |_| {});
3906
3907 let mut cx = EditorTestContext::new(cx).await;
3908
3909 // If all lower case -> upper case
3910 cx.set_state(indoc! {"
3911 «hello worldˇ»
3912 "});
3913 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
3914 cx.assert_editor_state(indoc! {"
3915 «HELLO WORLDˇ»
3916 "});
3917
3918 // If all upper case -> lower case
3919 cx.set_state(indoc! {"
3920 «HELLO WORLDˇ»
3921 "});
3922 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
3923 cx.assert_editor_state(indoc! {"
3924 «hello worldˇ»
3925 "});
3926
3927 // If any upper case characters are identified -> lower case
3928 // This matches JetBrains IDEs
3929 cx.set_state(indoc! {"
3930 «hEllo worldˇ»
3931 "});
3932 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
3933 cx.assert_editor_state(indoc! {"
3934 «hello worldˇ»
3935 "});
3936}
3937
3938#[gpui::test]
3939async fn test_manipulate_text(cx: &mut TestAppContext) {
3940 init_test(cx, |_| {});
3941
3942 let mut cx = EditorTestContext::new(cx).await;
3943
3944 // Test convert_to_upper_case()
3945 cx.set_state(indoc! {"
3946 «hello worldˇ»
3947 "});
3948 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3949 cx.assert_editor_state(indoc! {"
3950 «HELLO WORLDˇ»
3951 "});
3952
3953 // Test convert_to_lower_case()
3954 cx.set_state(indoc! {"
3955 «HELLO WORLDˇ»
3956 "});
3957 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
3958 cx.assert_editor_state(indoc! {"
3959 «hello worldˇ»
3960 "});
3961
3962 // Test multiple line, single selection case
3963 cx.set_state(indoc! {"
3964 «The quick brown
3965 fox jumps over
3966 the lazy dogˇ»
3967 "});
3968 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
3969 cx.assert_editor_state(indoc! {"
3970 «The Quick Brown
3971 Fox Jumps Over
3972 The Lazy Dogˇ»
3973 "});
3974
3975 // Test multiple line, single selection case
3976 cx.set_state(indoc! {"
3977 «The quick brown
3978 fox jumps over
3979 the lazy dogˇ»
3980 "});
3981 cx.update_editor(|e, window, cx| {
3982 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
3983 });
3984 cx.assert_editor_state(indoc! {"
3985 «TheQuickBrown
3986 FoxJumpsOver
3987 TheLazyDogˇ»
3988 "});
3989
3990 // From here on out, test more complex cases of manipulate_text()
3991
3992 // Test no selection case - should affect words cursors are in
3993 // Cursor at beginning, middle, and end of word
3994 cx.set_state(indoc! {"
3995 ˇhello big beauˇtiful worldˇ
3996 "});
3997 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3998 cx.assert_editor_state(indoc! {"
3999 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4000 "});
4001
4002 // Test multiple selections on a single line and across multiple lines
4003 cx.set_state(indoc! {"
4004 «Theˇ» quick «brown
4005 foxˇ» jumps «overˇ»
4006 the «lazyˇ» dog
4007 "});
4008 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4009 cx.assert_editor_state(indoc! {"
4010 «THEˇ» quick «BROWN
4011 FOXˇ» jumps «OVERˇ»
4012 the «LAZYˇ» dog
4013 "});
4014
4015 // Test case where text length grows
4016 cx.set_state(indoc! {"
4017 «tschüߡ»
4018 "});
4019 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4020 cx.assert_editor_state(indoc! {"
4021 «TSCHÜSSˇ»
4022 "});
4023
4024 // Test to make sure we don't crash when text shrinks
4025 cx.set_state(indoc! {"
4026 aaa_bbbˇ
4027 "});
4028 cx.update_editor(|e, window, cx| {
4029 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4030 });
4031 cx.assert_editor_state(indoc! {"
4032 «aaaBbbˇ»
4033 "});
4034
4035 // Test to make sure we all aware of the fact that each word can grow and shrink
4036 // Final selections should be aware of this fact
4037 cx.set_state(indoc! {"
4038 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4039 "});
4040 cx.update_editor(|e, window, cx| {
4041 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4042 });
4043 cx.assert_editor_state(indoc! {"
4044 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4045 "});
4046
4047 cx.set_state(indoc! {"
4048 «hElLo, WoRld!ˇ»
4049 "});
4050 cx.update_editor(|e, window, cx| {
4051 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4052 });
4053 cx.assert_editor_state(indoc! {"
4054 «HeLlO, wOrLD!ˇ»
4055 "});
4056}
4057
4058#[gpui::test]
4059fn test_duplicate_line(cx: &mut TestAppContext) {
4060 init_test(cx, |_| {});
4061
4062 let editor = cx.add_window(|window, cx| {
4063 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4064 build_editor(buffer, window, cx)
4065 });
4066 _ = editor.update(cx, |editor, window, cx| {
4067 editor.change_selections(None, window, cx, |s| {
4068 s.select_display_ranges([
4069 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4070 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4071 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4072 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4073 ])
4074 });
4075 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4076 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4077 assert_eq!(
4078 editor.selections.display_ranges(cx),
4079 vec![
4080 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4081 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4082 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4083 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4084 ]
4085 );
4086 });
4087
4088 let editor = cx.add_window(|window, cx| {
4089 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4090 build_editor(buffer, window, cx)
4091 });
4092 _ = editor.update(cx, |editor, window, cx| {
4093 editor.change_selections(None, window, cx, |s| {
4094 s.select_display_ranges([
4095 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4096 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4097 ])
4098 });
4099 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4100 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4101 assert_eq!(
4102 editor.selections.display_ranges(cx),
4103 vec![
4104 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4105 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4106 ]
4107 );
4108 });
4109
4110 // With `move_upwards` the selections stay in place, except for
4111 // the lines inserted above them
4112 let editor = cx.add_window(|window, cx| {
4113 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4114 build_editor(buffer, window, cx)
4115 });
4116 _ = editor.update(cx, |editor, window, cx| {
4117 editor.change_selections(None, window, cx, |s| {
4118 s.select_display_ranges([
4119 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4120 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4121 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4122 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4123 ])
4124 });
4125 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4126 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4127 assert_eq!(
4128 editor.selections.display_ranges(cx),
4129 vec![
4130 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4131 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4132 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4133 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4134 ]
4135 );
4136 });
4137
4138 let editor = cx.add_window(|window, cx| {
4139 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4140 build_editor(buffer, window, cx)
4141 });
4142 _ = editor.update(cx, |editor, window, cx| {
4143 editor.change_selections(None, window, cx, |s| {
4144 s.select_display_ranges([
4145 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4146 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4147 ])
4148 });
4149 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4150 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4151 assert_eq!(
4152 editor.selections.display_ranges(cx),
4153 vec![
4154 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4155 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4156 ]
4157 );
4158 });
4159
4160 let editor = cx.add_window(|window, cx| {
4161 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4162 build_editor(buffer, window, cx)
4163 });
4164 _ = editor.update(cx, |editor, window, cx| {
4165 editor.change_selections(None, window, cx, |s| {
4166 s.select_display_ranges([
4167 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4168 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4169 ])
4170 });
4171 editor.duplicate_selection(&DuplicateSelection, window, cx);
4172 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4173 assert_eq!(
4174 editor.selections.display_ranges(cx),
4175 vec![
4176 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4177 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4178 ]
4179 );
4180 });
4181}
4182
4183#[gpui::test]
4184fn test_move_line_up_down(cx: &mut TestAppContext) {
4185 init_test(cx, |_| {});
4186
4187 let editor = cx.add_window(|window, cx| {
4188 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4189 build_editor(buffer, window, cx)
4190 });
4191 _ = editor.update(cx, |editor, window, cx| {
4192 editor.fold_creases(
4193 vec![
4194 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4195 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4196 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4197 ],
4198 true,
4199 window,
4200 cx,
4201 );
4202 editor.change_selections(None, window, cx, |s| {
4203 s.select_display_ranges([
4204 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4205 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4206 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4207 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4208 ])
4209 });
4210 assert_eq!(
4211 editor.display_text(cx),
4212 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4213 );
4214
4215 editor.move_line_up(&MoveLineUp, window, cx);
4216 assert_eq!(
4217 editor.display_text(cx),
4218 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4219 );
4220 assert_eq!(
4221 editor.selections.display_ranges(cx),
4222 vec![
4223 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4224 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4225 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4226 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4227 ]
4228 );
4229 });
4230
4231 _ = editor.update(cx, |editor, window, cx| {
4232 editor.move_line_down(&MoveLineDown, window, cx);
4233 assert_eq!(
4234 editor.display_text(cx),
4235 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4236 );
4237 assert_eq!(
4238 editor.selections.display_ranges(cx),
4239 vec![
4240 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4241 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4242 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4243 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4244 ]
4245 );
4246 });
4247
4248 _ = editor.update(cx, |editor, window, cx| {
4249 editor.move_line_down(&MoveLineDown, window, cx);
4250 assert_eq!(
4251 editor.display_text(cx),
4252 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4253 );
4254 assert_eq!(
4255 editor.selections.display_ranges(cx),
4256 vec![
4257 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4258 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4259 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4260 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4261 ]
4262 );
4263 });
4264
4265 _ = editor.update(cx, |editor, window, cx| {
4266 editor.move_line_up(&MoveLineUp, window, cx);
4267 assert_eq!(
4268 editor.display_text(cx),
4269 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4270 );
4271 assert_eq!(
4272 editor.selections.display_ranges(cx),
4273 vec![
4274 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4275 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4276 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4277 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4278 ]
4279 );
4280 });
4281}
4282
4283#[gpui::test]
4284fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4285 init_test(cx, |_| {});
4286
4287 let editor = cx.add_window(|window, cx| {
4288 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4289 build_editor(buffer, window, cx)
4290 });
4291 _ = editor.update(cx, |editor, window, cx| {
4292 let snapshot = editor.buffer.read(cx).snapshot(cx);
4293 editor.insert_blocks(
4294 [BlockProperties {
4295 style: BlockStyle::Fixed,
4296 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4297 height: Some(1),
4298 render: Arc::new(|_| div().into_any()),
4299 priority: 0,
4300 }],
4301 Some(Autoscroll::fit()),
4302 cx,
4303 );
4304 editor.change_selections(None, window, cx, |s| {
4305 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4306 });
4307 editor.move_line_down(&MoveLineDown, window, cx);
4308 });
4309}
4310
4311#[gpui::test]
4312async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4313 init_test(cx, |_| {});
4314
4315 let mut cx = EditorTestContext::new(cx).await;
4316 cx.set_state(
4317 &"
4318 ˇzero
4319 one
4320 two
4321 three
4322 four
4323 five
4324 "
4325 .unindent(),
4326 );
4327
4328 // Create a four-line block that replaces three lines of text.
4329 cx.update_editor(|editor, window, cx| {
4330 let snapshot = editor.snapshot(window, cx);
4331 let snapshot = &snapshot.buffer_snapshot;
4332 let placement = BlockPlacement::Replace(
4333 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4334 );
4335 editor.insert_blocks(
4336 [BlockProperties {
4337 placement,
4338 height: Some(4),
4339 style: BlockStyle::Sticky,
4340 render: Arc::new(|_| gpui::div().into_any_element()),
4341 priority: 0,
4342 }],
4343 None,
4344 cx,
4345 );
4346 });
4347
4348 // Move down so that the cursor touches the block.
4349 cx.update_editor(|editor, window, cx| {
4350 editor.move_down(&Default::default(), window, cx);
4351 });
4352 cx.assert_editor_state(
4353 &"
4354 zero
4355 «one
4356 two
4357 threeˇ»
4358 four
4359 five
4360 "
4361 .unindent(),
4362 );
4363
4364 // Move down past the block.
4365 cx.update_editor(|editor, window, cx| {
4366 editor.move_down(&Default::default(), window, cx);
4367 });
4368 cx.assert_editor_state(
4369 &"
4370 zero
4371 one
4372 two
4373 three
4374 ˇfour
4375 five
4376 "
4377 .unindent(),
4378 );
4379}
4380
4381#[gpui::test]
4382fn test_transpose(cx: &mut TestAppContext) {
4383 init_test(cx, |_| {});
4384
4385 _ = cx.add_window(|window, cx| {
4386 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4387 editor.set_style(EditorStyle::default(), window, cx);
4388 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4389 editor.transpose(&Default::default(), window, cx);
4390 assert_eq!(editor.text(cx), "bac");
4391 assert_eq!(editor.selections.ranges(cx), [2..2]);
4392
4393 editor.transpose(&Default::default(), window, cx);
4394 assert_eq!(editor.text(cx), "bca");
4395 assert_eq!(editor.selections.ranges(cx), [3..3]);
4396
4397 editor.transpose(&Default::default(), window, cx);
4398 assert_eq!(editor.text(cx), "bac");
4399 assert_eq!(editor.selections.ranges(cx), [3..3]);
4400
4401 editor
4402 });
4403
4404 _ = cx.add_window(|window, cx| {
4405 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4406 editor.set_style(EditorStyle::default(), window, cx);
4407 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4408 editor.transpose(&Default::default(), window, cx);
4409 assert_eq!(editor.text(cx), "acb\nde");
4410 assert_eq!(editor.selections.ranges(cx), [3..3]);
4411
4412 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4413 editor.transpose(&Default::default(), window, cx);
4414 assert_eq!(editor.text(cx), "acbd\ne");
4415 assert_eq!(editor.selections.ranges(cx), [5..5]);
4416
4417 editor.transpose(&Default::default(), window, cx);
4418 assert_eq!(editor.text(cx), "acbde\n");
4419 assert_eq!(editor.selections.ranges(cx), [6..6]);
4420
4421 editor.transpose(&Default::default(), window, cx);
4422 assert_eq!(editor.text(cx), "acbd\ne");
4423 assert_eq!(editor.selections.ranges(cx), [6..6]);
4424
4425 editor
4426 });
4427
4428 _ = cx.add_window(|window, cx| {
4429 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4430 editor.set_style(EditorStyle::default(), window, cx);
4431 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4432 editor.transpose(&Default::default(), window, cx);
4433 assert_eq!(editor.text(cx), "bacd\ne");
4434 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4435
4436 editor.transpose(&Default::default(), window, cx);
4437 assert_eq!(editor.text(cx), "bcade\n");
4438 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4439
4440 editor.transpose(&Default::default(), window, cx);
4441 assert_eq!(editor.text(cx), "bcda\ne");
4442 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4443
4444 editor.transpose(&Default::default(), window, cx);
4445 assert_eq!(editor.text(cx), "bcade\n");
4446 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4447
4448 editor.transpose(&Default::default(), window, cx);
4449 assert_eq!(editor.text(cx), "bcaed\n");
4450 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4451
4452 editor
4453 });
4454
4455 _ = cx.add_window(|window, cx| {
4456 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4457 editor.set_style(EditorStyle::default(), window, cx);
4458 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4459 editor.transpose(&Default::default(), window, cx);
4460 assert_eq!(editor.text(cx), "🏀🍐✋");
4461 assert_eq!(editor.selections.ranges(cx), [8..8]);
4462
4463 editor.transpose(&Default::default(), window, cx);
4464 assert_eq!(editor.text(cx), "🏀✋🍐");
4465 assert_eq!(editor.selections.ranges(cx), [11..11]);
4466
4467 editor.transpose(&Default::default(), window, cx);
4468 assert_eq!(editor.text(cx), "🏀🍐✋");
4469 assert_eq!(editor.selections.ranges(cx), [11..11]);
4470
4471 editor
4472 });
4473}
4474
4475#[gpui::test]
4476async fn test_rewrap(cx: &mut TestAppContext) {
4477 init_test(cx, |settings| {
4478 settings.languages.extend([
4479 (
4480 "Markdown".into(),
4481 LanguageSettingsContent {
4482 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4483 ..Default::default()
4484 },
4485 ),
4486 (
4487 "Plain Text".into(),
4488 LanguageSettingsContent {
4489 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4490 ..Default::default()
4491 },
4492 ),
4493 ])
4494 });
4495
4496 let mut cx = EditorTestContext::new(cx).await;
4497
4498 let language_with_c_comments = Arc::new(Language::new(
4499 LanguageConfig {
4500 line_comments: vec!["// ".into()],
4501 ..LanguageConfig::default()
4502 },
4503 None,
4504 ));
4505 let language_with_pound_comments = Arc::new(Language::new(
4506 LanguageConfig {
4507 line_comments: vec!["# ".into()],
4508 ..LanguageConfig::default()
4509 },
4510 None,
4511 ));
4512 let markdown_language = Arc::new(Language::new(
4513 LanguageConfig {
4514 name: "Markdown".into(),
4515 ..LanguageConfig::default()
4516 },
4517 None,
4518 ));
4519 let language_with_doc_comments = Arc::new(Language::new(
4520 LanguageConfig {
4521 line_comments: vec!["// ".into(), "/// ".into()],
4522 ..LanguageConfig::default()
4523 },
4524 Some(tree_sitter_rust::LANGUAGE.into()),
4525 ));
4526
4527 let plaintext_language = Arc::new(Language::new(
4528 LanguageConfig {
4529 name: "Plain Text".into(),
4530 ..LanguageConfig::default()
4531 },
4532 None,
4533 ));
4534
4535 assert_rewrap(
4536 indoc! {"
4537 // ˇ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.
4538 "},
4539 indoc! {"
4540 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4541 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4542 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4543 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4544 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4545 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4546 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4547 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4548 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4549 // porttitor id. Aliquam id accumsan eros.
4550 "},
4551 language_with_c_comments.clone(),
4552 &mut cx,
4553 );
4554
4555 // Test that rewrapping works inside of a selection
4556 assert_rewrap(
4557 indoc! {"
4558 «// 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.ˇ»
4559 "},
4560 indoc! {"
4561 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4562 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4563 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4564 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4565 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4566 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4567 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4568 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4569 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4570 // porttitor id. Aliquam id accumsan eros.ˇ»
4571 "},
4572 language_with_c_comments.clone(),
4573 &mut cx,
4574 );
4575
4576 // Test that cursors that expand to the same region are collapsed.
4577 assert_rewrap(
4578 indoc! {"
4579 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4580 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4581 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4582 // ˇ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.
4583 "},
4584 indoc! {"
4585 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4586 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4587 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
4588 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4589 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
4590 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4591 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4592 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4593 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4594 // porttitor id. Aliquam id accumsan eros.
4595 "},
4596 language_with_c_comments.clone(),
4597 &mut cx,
4598 );
4599
4600 // Test that non-contiguous selections are treated separately.
4601 assert_rewrap(
4602 indoc! {"
4603 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4604 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4605 //
4606 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4607 // ˇ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.
4608 "},
4609 indoc! {"
4610 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4611 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4612 // auctor, eu lacinia sapien scelerisque.
4613 //
4614 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
4615 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4616 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
4617 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
4618 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
4619 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
4620 // vulputate turpis porttitor id. Aliquam id accumsan eros.
4621 "},
4622 language_with_c_comments.clone(),
4623 &mut cx,
4624 );
4625
4626 // Test that different comment prefixes are supported.
4627 assert_rewrap(
4628 indoc! {"
4629 # ˇ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.
4630 "},
4631 indoc! {"
4632 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4633 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4634 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4635 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4636 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
4637 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
4638 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
4639 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
4640 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
4641 # accumsan eros.
4642 "},
4643 language_with_pound_comments.clone(),
4644 &mut cx,
4645 );
4646
4647 // Test that rewrapping is ignored outside of comments in most languages.
4648 assert_rewrap(
4649 indoc! {"
4650 /// Adds two numbers.
4651 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4652 fn add(a: u32, b: u32) -> u32 {
4653 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ˇ
4654 }
4655 "},
4656 indoc! {"
4657 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
4658 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4659 fn add(a: u32, b: u32) -> u32 {
4660 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ˇ
4661 }
4662 "},
4663 language_with_doc_comments.clone(),
4664 &mut cx,
4665 );
4666
4667 // Test that rewrapping works in Markdown and Plain Text languages.
4668 assert_rewrap(
4669 indoc! {"
4670 # Hello
4671
4672 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.
4673 "},
4674 indoc! {"
4675 # Hello
4676
4677 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4678 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4679 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4680 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4681 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4682 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4683 Integer sit amet scelerisque nisi.
4684 "},
4685 markdown_language,
4686 &mut cx,
4687 );
4688
4689 assert_rewrap(
4690 indoc! {"
4691 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.
4692 "},
4693 indoc! {"
4694 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4695 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4696 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4697 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4698 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4699 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4700 Integer sit amet scelerisque nisi.
4701 "},
4702 plaintext_language,
4703 &mut cx,
4704 );
4705
4706 // Test rewrapping unaligned comments in a selection.
4707 assert_rewrap(
4708 indoc! {"
4709 fn foo() {
4710 if true {
4711 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4712 // Praesent semper egestas tellus id dignissim.ˇ»
4713 do_something();
4714 } else {
4715 //
4716 }
4717 }
4718 "},
4719 indoc! {"
4720 fn foo() {
4721 if true {
4722 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4723 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4724 // egestas tellus id dignissim.ˇ»
4725 do_something();
4726 } else {
4727 //
4728 }
4729 }
4730 "},
4731 language_with_doc_comments.clone(),
4732 &mut cx,
4733 );
4734
4735 assert_rewrap(
4736 indoc! {"
4737 fn foo() {
4738 if true {
4739 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4740 // Praesent semper egestas tellus id dignissim.»
4741 do_something();
4742 } else {
4743 //
4744 }
4745
4746 }
4747 "},
4748 indoc! {"
4749 fn foo() {
4750 if true {
4751 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4752 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4753 // egestas tellus id dignissim.»
4754 do_something();
4755 } else {
4756 //
4757 }
4758
4759 }
4760 "},
4761 language_with_doc_comments.clone(),
4762 &mut cx,
4763 );
4764
4765 #[track_caller]
4766 fn assert_rewrap(
4767 unwrapped_text: &str,
4768 wrapped_text: &str,
4769 language: Arc<Language>,
4770 cx: &mut EditorTestContext,
4771 ) {
4772 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4773 cx.set_state(unwrapped_text);
4774 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
4775 cx.assert_editor_state(wrapped_text);
4776 }
4777}
4778
4779#[gpui::test]
4780async fn test_hard_wrap(cx: &mut TestAppContext) {
4781 init_test(cx, |_| {});
4782 let mut cx = EditorTestContext::new(cx).await;
4783
4784 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
4785 cx.update_editor(|editor, _, cx| {
4786 editor.set_hard_wrap(Some(14), cx);
4787 });
4788
4789 cx.set_state(indoc!(
4790 "
4791 one two three ˇ
4792 "
4793 ));
4794 cx.simulate_input("four");
4795 cx.run_until_parked();
4796
4797 cx.assert_editor_state(indoc!(
4798 "
4799 one two three
4800 fourˇ
4801 "
4802 ));
4803
4804 cx.update_editor(|editor, window, cx| {
4805 editor.newline(&Default::default(), window, cx);
4806 });
4807 cx.run_until_parked();
4808 cx.assert_editor_state(indoc!(
4809 "
4810 one two three
4811 four
4812 ˇ
4813 "
4814 ));
4815
4816 cx.simulate_input("five");
4817 cx.run_until_parked();
4818 cx.assert_editor_state(indoc!(
4819 "
4820 one two three
4821 four
4822 fiveˇ
4823 "
4824 ));
4825
4826 cx.update_editor(|editor, window, cx| {
4827 editor.newline(&Default::default(), window, cx);
4828 });
4829 cx.run_until_parked();
4830 cx.simulate_input("# ");
4831 cx.run_until_parked();
4832 cx.assert_editor_state(indoc!(
4833 "
4834 one two three
4835 four
4836 five
4837 # ˇ
4838 "
4839 ));
4840
4841 cx.update_editor(|editor, window, cx| {
4842 editor.newline(&Default::default(), window, cx);
4843 });
4844 cx.run_until_parked();
4845 cx.assert_editor_state(indoc!(
4846 "
4847 one two three
4848 four
4849 five
4850 #\x20
4851 #ˇ
4852 "
4853 ));
4854
4855 cx.simulate_input(" 6");
4856 cx.run_until_parked();
4857 cx.assert_editor_state(indoc!(
4858 "
4859 one two three
4860 four
4861 five
4862 #
4863 # 6ˇ
4864 "
4865 ));
4866}
4867
4868#[gpui::test]
4869async fn test_clipboard(cx: &mut TestAppContext) {
4870 init_test(cx, |_| {});
4871
4872 let mut cx = EditorTestContext::new(cx).await;
4873
4874 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
4875 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4876 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
4877
4878 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
4879 cx.set_state("two ˇfour ˇsix ˇ");
4880 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4881 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
4882
4883 // Paste again but with only two cursors. Since the number of cursors doesn't
4884 // match the number of slices in the clipboard, the entire clipboard text
4885 // is pasted at each cursor.
4886 cx.set_state("ˇtwo one✅ four three six five ˇ");
4887 cx.update_editor(|e, window, cx| {
4888 e.handle_input("( ", window, cx);
4889 e.paste(&Paste, window, cx);
4890 e.handle_input(") ", window, cx);
4891 });
4892 cx.assert_editor_state(
4893 &([
4894 "( one✅ ",
4895 "three ",
4896 "five ) ˇtwo one✅ four three six five ( one✅ ",
4897 "three ",
4898 "five ) ˇ",
4899 ]
4900 .join("\n")),
4901 );
4902
4903 // Cut with three selections, one of which is full-line.
4904 cx.set_state(indoc! {"
4905 1«2ˇ»3
4906 4ˇ567
4907 «8ˇ»9"});
4908 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4909 cx.assert_editor_state(indoc! {"
4910 1ˇ3
4911 ˇ9"});
4912
4913 // Paste with three selections, noticing how the copied selection that was full-line
4914 // gets inserted before the second cursor.
4915 cx.set_state(indoc! {"
4916 1ˇ3
4917 9ˇ
4918 «oˇ»ne"});
4919 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4920 cx.assert_editor_state(indoc! {"
4921 12ˇ3
4922 4567
4923 9ˇ
4924 8ˇne"});
4925
4926 // Copy with a single cursor only, which writes the whole line into the clipboard.
4927 cx.set_state(indoc! {"
4928 The quick brown
4929 fox juˇmps over
4930 the lazy dog"});
4931 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4932 assert_eq!(
4933 cx.read_from_clipboard()
4934 .and_then(|item| item.text().as_deref().map(str::to_string)),
4935 Some("fox jumps over\n".to_string())
4936 );
4937
4938 // Paste with three selections, noticing how the copied full-line selection is inserted
4939 // before the empty selections but replaces the selection that is non-empty.
4940 cx.set_state(indoc! {"
4941 Tˇhe quick brown
4942 «foˇ»x jumps over
4943 tˇhe lazy dog"});
4944 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4945 cx.assert_editor_state(indoc! {"
4946 fox jumps over
4947 Tˇhe quick brown
4948 fox jumps over
4949 ˇx jumps over
4950 fox jumps over
4951 tˇhe lazy dog"});
4952}
4953
4954#[gpui::test]
4955async fn test_copy_trim(cx: &mut TestAppContext) {
4956 init_test(cx, |_| {});
4957
4958 let mut cx = EditorTestContext::new(cx).await;
4959 cx.set_state(
4960 r#" «for selection in selections.iter() {
4961 let mut start = selection.start;
4962 let mut end = selection.end;
4963 let is_entire_line = selection.is_empty();
4964 if is_entire_line {
4965 start = Point::new(start.row, 0);ˇ»
4966 end = cmp::min(max_point, Point::new(end.row + 1, 0));
4967 }
4968 "#,
4969 );
4970 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4971 assert_eq!(
4972 cx.read_from_clipboard()
4973 .and_then(|item| item.text().as_deref().map(str::to_string)),
4974 Some(
4975 "for selection in selections.iter() {
4976 let mut start = selection.start;
4977 let mut end = selection.end;
4978 let is_entire_line = selection.is_empty();
4979 if is_entire_line {
4980 start = Point::new(start.row, 0);"
4981 .to_string()
4982 ),
4983 "Regular copying preserves all indentation selected",
4984 );
4985 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
4986 assert_eq!(
4987 cx.read_from_clipboard()
4988 .and_then(|item| item.text().as_deref().map(str::to_string)),
4989 Some(
4990 "for selection in selections.iter() {
4991let mut start = selection.start;
4992let mut end = selection.end;
4993let is_entire_line = selection.is_empty();
4994if is_entire_line {
4995 start = Point::new(start.row, 0);"
4996 .to_string()
4997 ),
4998 "Copying with stripping should strip all leading whitespaces"
4999 );
5000
5001 cx.set_state(
5002 r#" « for selection in selections.iter() {
5003 let mut start = selection.start;
5004 let mut end = selection.end;
5005 let is_entire_line = selection.is_empty();
5006 if is_entire_line {
5007 start = Point::new(start.row, 0);ˇ»
5008 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5009 }
5010 "#,
5011 );
5012 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5013 assert_eq!(
5014 cx.read_from_clipboard()
5015 .and_then(|item| item.text().as_deref().map(str::to_string)),
5016 Some(
5017 " for selection in selections.iter() {
5018 let mut start = selection.start;
5019 let mut end = selection.end;
5020 let is_entire_line = selection.is_empty();
5021 if is_entire_line {
5022 start = Point::new(start.row, 0);"
5023 .to_string()
5024 ),
5025 "Regular copying preserves all indentation selected",
5026 );
5027 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5028 assert_eq!(
5029 cx.read_from_clipboard()
5030 .and_then(|item| item.text().as_deref().map(str::to_string)),
5031 Some(
5032 "for selection in selections.iter() {
5033let mut start = selection.start;
5034let mut end = selection.end;
5035let is_entire_line = selection.is_empty();
5036if is_entire_line {
5037 start = Point::new(start.row, 0);"
5038 .to_string()
5039 ),
5040 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5041 );
5042
5043 cx.set_state(
5044 r#" «ˇ for selection in selections.iter() {
5045 let mut start = selection.start;
5046 let mut end = selection.end;
5047 let is_entire_line = selection.is_empty();
5048 if is_entire_line {
5049 start = Point::new(start.row, 0);»
5050 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5051 }
5052 "#,
5053 );
5054 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5055 assert_eq!(
5056 cx.read_from_clipboard()
5057 .and_then(|item| item.text().as_deref().map(str::to_string)),
5058 Some(
5059 " for selection in selections.iter() {
5060 let mut start = selection.start;
5061 let mut end = selection.end;
5062 let is_entire_line = selection.is_empty();
5063 if is_entire_line {
5064 start = Point::new(start.row, 0);"
5065 .to_string()
5066 ),
5067 "Regular copying for reverse selection works the same",
5068 );
5069 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5070 assert_eq!(
5071 cx.read_from_clipboard()
5072 .and_then(|item| item.text().as_deref().map(str::to_string)),
5073 Some(
5074 "for selection in selections.iter() {
5075let mut start = selection.start;
5076let mut end = selection.end;
5077let is_entire_line = selection.is_empty();
5078if is_entire_line {
5079 start = Point::new(start.row, 0);"
5080 .to_string()
5081 ),
5082 "Copying with stripping for reverse selection works the same"
5083 );
5084
5085 cx.set_state(
5086 r#" for selection «in selections.iter() {
5087 let mut start = selection.start;
5088 let mut end = selection.end;
5089 let is_entire_line = selection.is_empty();
5090 if is_entire_line {
5091 start = Point::new(start.row, 0);ˇ»
5092 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5093 }
5094 "#,
5095 );
5096 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5097 assert_eq!(
5098 cx.read_from_clipboard()
5099 .and_then(|item| item.text().as_deref().map(str::to_string)),
5100 Some(
5101 "in selections.iter() {
5102 let mut start = selection.start;
5103 let mut end = selection.end;
5104 let is_entire_line = selection.is_empty();
5105 if is_entire_line {
5106 start = Point::new(start.row, 0);"
5107 .to_string()
5108 ),
5109 "When selecting past the indent, the copying works as usual",
5110 );
5111 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5112 assert_eq!(
5113 cx.read_from_clipboard()
5114 .and_then(|item| item.text().as_deref().map(str::to_string)),
5115 Some(
5116 "in selections.iter() {
5117 let mut start = selection.start;
5118 let mut end = selection.end;
5119 let is_entire_line = selection.is_empty();
5120 if is_entire_line {
5121 start = Point::new(start.row, 0);"
5122 .to_string()
5123 ),
5124 "When selecting past the indent, nothing is trimmed"
5125 );
5126}
5127
5128#[gpui::test]
5129async fn test_paste_multiline(cx: &mut TestAppContext) {
5130 init_test(cx, |_| {});
5131
5132 let mut cx = EditorTestContext::new(cx).await;
5133 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5134
5135 // Cut an indented block, without the leading whitespace.
5136 cx.set_state(indoc! {"
5137 const a: B = (
5138 c(),
5139 «d(
5140 e,
5141 f
5142 )ˇ»
5143 );
5144 "});
5145 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5146 cx.assert_editor_state(indoc! {"
5147 const a: B = (
5148 c(),
5149 ˇ
5150 );
5151 "});
5152
5153 // Paste it at the same position.
5154 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5155 cx.assert_editor_state(indoc! {"
5156 const a: B = (
5157 c(),
5158 d(
5159 e,
5160 f
5161 )ˇ
5162 );
5163 "});
5164
5165 // Paste it at a line with a lower indent level.
5166 cx.set_state(indoc! {"
5167 ˇ
5168 const a: B = (
5169 c(),
5170 );
5171 "});
5172 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5173 cx.assert_editor_state(indoc! {"
5174 d(
5175 e,
5176 f
5177 )ˇ
5178 const a: B = (
5179 c(),
5180 );
5181 "});
5182
5183 // Cut an indented block, with the leading whitespace.
5184 cx.set_state(indoc! {"
5185 const a: B = (
5186 c(),
5187 « d(
5188 e,
5189 f
5190 )
5191 ˇ»);
5192 "});
5193 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5194 cx.assert_editor_state(indoc! {"
5195 const a: B = (
5196 c(),
5197 ˇ);
5198 "});
5199
5200 // Paste it at the same position.
5201 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5202 cx.assert_editor_state(indoc! {"
5203 const a: B = (
5204 c(),
5205 d(
5206 e,
5207 f
5208 )
5209 ˇ);
5210 "});
5211
5212 // Paste it at a line with a higher indent level.
5213 cx.set_state(indoc! {"
5214 const a: B = (
5215 c(),
5216 d(
5217 e,
5218 fˇ
5219 )
5220 );
5221 "});
5222 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5223 cx.assert_editor_state(indoc! {"
5224 const a: B = (
5225 c(),
5226 d(
5227 e,
5228 f d(
5229 e,
5230 f
5231 )
5232 ˇ
5233 )
5234 );
5235 "});
5236
5237 // Copy an indented block, starting mid-line
5238 cx.set_state(indoc! {"
5239 const a: B = (
5240 c(),
5241 somethin«g(
5242 e,
5243 f
5244 )ˇ»
5245 );
5246 "});
5247 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5248
5249 // Paste it on a line with a lower indent level
5250 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
5251 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5252 cx.assert_editor_state(indoc! {"
5253 const a: B = (
5254 c(),
5255 something(
5256 e,
5257 f
5258 )
5259 );
5260 g(
5261 e,
5262 f
5263 )ˇ"});
5264}
5265
5266#[gpui::test]
5267async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
5268 init_test(cx, |_| {});
5269
5270 cx.write_to_clipboard(ClipboardItem::new_string(
5271 " d(\n e\n );\n".into(),
5272 ));
5273
5274 let mut cx = EditorTestContext::new(cx).await;
5275 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5276
5277 cx.set_state(indoc! {"
5278 fn a() {
5279 b();
5280 if c() {
5281 ˇ
5282 }
5283 }
5284 "});
5285
5286 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5287 cx.assert_editor_state(indoc! {"
5288 fn a() {
5289 b();
5290 if c() {
5291 d(
5292 e
5293 );
5294 ˇ
5295 }
5296 }
5297 "});
5298
5299 cx.set_state(indoc! {"
5300 fn a() {
5301 b();
5302 ˇ
5303 }
5304 "});
5305
5306 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5307 cx.assert_editor_state(indoc! {"
5308 fn a() {
5309 b();
5310 d(
5311 e
5312 );
5313 ˇ
5314 }
5315 "});
5316}
5317
5318#[gpui::test]
5319fn test_select_all(cx: &mut TestAppContext) {
5320 init_test(cx, |_| {});
5321
5322 let editor = cx.add_window(|window, cx| {
5323 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
5324 build_editor(buffer, window, cx)
5325 });
5326 _ = editor.update(cx, |editor, window, cx| {
5327 editor.select_all(&SelectAll, window, cx);
5328 assert_eq!(
5329 editor.selections.display_ranges(cx),
5330 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
5331 );
5332 });
5333}
5334
5335#[gpui::test]
5336fn test_select_line(cx: &mut TestAppContext) {
5337 init_test(cx, |_| {});
5338
5339 let editor = cx.add_window(|window, cx| {
5340 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
5341 build_editor(buffer, window, cx)
5342 });
5343 _ = editor.update(cx, |editor, window, cx| {
5344 editor.change_selections(None, window, cx, |s| {
5345 s.select_display_ranges([
5346 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5347 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5348 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5349 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
5350 ])
5351 });
5352 editor.select_line(&SelectLine, window, cx);
5353 assert_eq!(
5354 editor.selections.display_ranges(cx),
5355 vec![
5356 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
5357 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
5358 ]
5359 );
5360 });
5361
5362 _ = editor.update(cx, |editor, window, cx| {
5363 editor.select_line(&SelectLine, window, cx);
5364 assert_eq!(
5365 editor.selections.display_ranges(cx),
5366 vec![
5367 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
5368 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
5369 ]
5370 );
5371 });
5372
5373 _ = editor.update(cx, |editor, window, cx| {
5374 editor.select_line(&SelectLine, window, cx);
5375 assert_eq!(
5376 editor.selections.display_ranges(cx),
5377 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
5378 );
5379 });
5380}
5381
5382#[gpui::test]
5383async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
5384 init_test(cx, |_| {});
5385 let mut cx = EditorTestContext::new(cx).await;
5386
5387 #[track_caller]
5388 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
5389 cx.set_state(initial_state);
5390 cx.update_editor(|e, window, cx| {
5391 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
5392 });
5393 cx.assert_editor_state(expected_state);
5394 }
5395
5396 // Selection starts and ends at the middle of lines, left-to-right
5397 test(
5398 &mut cx,
5399 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
5400 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5401 );
5402 // Same thing, right-to-left
5403 test(
5404 &mut cx,
5405 "aa\nb«b\ncc\ndd\neˇ»e\nff",
5406 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5407 );
5408
5409 // Whole buffer, left-to-right, last line *doesn't* end with newline
5410 test(
5411 &mut cx,
5412 "«ˇaa\nbb\ncc\ndd\nee\nff»",
5413 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5414 );
5415 // Same thing, right-to-left
5416 test(
5417 &mut cx,
5418 "«aa\nbb\ncc\ndd\nee\nffˇ»",
5419 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5420 );
5421
5422 // Whole buffer, left-to-right, last line ends with newline
5423 test(
5424 &mut cx,
5425 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
5426 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5427 );
5428 // Same thing, right-to-left
5429 test(
5430 &mut cx,
5431 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
5432 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5433 );
5434
5435 // Starts at the end of a line, ends at the start of another
5436 test(
5437 &mut cx,
5438 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
5439 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
5440 );
5441}
5442
5443#[gpui::test]
5444async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
5445 init_test(cx, |_| {});
5446
5447 let editor = cx.add_window(|window, cx| {
5448 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
5449 build_editor(buffer, window, cx)
5450 });
5451
5452 // setup
5453 _ = editor.update(cx, |editor, window, cx| {
5454 editor.fold_creases(
5455 vec![
5456 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5457 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5458 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5459 ],
5460 true,
5461 window,
5462 cx,
5463 );
5464 assert_eq!(
5465 editor.display_text(cx),
5466 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5467 );
5468 });
5469
5470 _ = editor.update(cx, |editor, window, cx| {
5471 editor.change_selections(None, window, cx, |s| {
5472 s.select_display_ranges([
5473 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5474 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5475 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5476 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
5477 ])
5478 });
5479 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5480 assert_eq!(
5481 editor.display_text(cx),
5482 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5483 );
5484 });
5485 EditorTestContext::for_editor(editor, cx)
5486 .await
5487 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
5488
5489 _ = editor.update(cx, |editor, window, cx| {
5490 editor.change_selections(None, window, cx, |s| {
5491 s.select_display_ranges([
5492 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
5493 ])
5494 });
5495 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5496 assert_eq!(
5497 editor.display_text(cx),
5498 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
5499 );
5500 assert_eq!(
5501 editor.selections.display_ranges(cx),
5502 [
5503 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
5504 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
5505 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
5506 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
5507 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
5508 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
5509 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
5510 ]
5511 );
5512 });
5513 EditorTestContext::for_editor(editor, cx)
5514 .await
5515 .assert_editor_state(
5516 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
5517 );
5518}
5519
5520#[gpui::test]
5521async fn test_add_selection_above_below(cx: &mut TestAppContext) {
5522 init_test(cx, |_| {});
5523
5524 let mut cx = EditorTestContext::new(cx).await;
5525
5526 cx.set_state(indoc!(
5527 r#"abc
5528 defˇghi
5529
5530 jk
5531 nlmo
5532 "#
5533 ));
5534
5535 cx.update_editor(|editor, window, cx| {
5536 editor.add_selection_above(&Default::default(), window, cx);
5537 });
5538
5539 cx.assert_editor_state(indoc!(
5540 r#"abcˇ
5541 defˇghi
5542
5543 jk
5544 nlmo
5545 "#
5546 ));
5547
5548 cx.update_editor(|editor, window, cx| {
5549 editor.add_selection_above(&Default::default(), window, cx);
5550 });
5551
5552 cx.assert_editor_state(indoc!(
5553 r#"abcˇ
5554 defˇghi
5555
5556 jk
5557 nlmo
5558 "#
5559 ));
5560
5561 cx.update_editor(|editor, window, cx| {
5562 editor.add_selection_below(&Default::default(), window, cx);
5563 });
5564
5565 cx.assert_editor_state(indoc!(
5566 r#"abc
5567 defˇghi
5568
5569 jk
5570 nlmo
5571 "#
5572 ));
5573
5574 cx.update_editor(|editor, window, cx| {
5575 editor.undo_selection(&Default::default(), window, cx);
5576 });
5577
5578 cx.assert_editor_state(indoc!(
5579 r#"abcˇ
5580 defˇghi
5581
5582 jk
5583 nlmo
5584 "#
5585 ));
5586
5587 cx.update_editor(|editor, window, cx| {
5588 editor.redo_selection(&Default::default(), window, cx);
5589 });
5590
5591 cx.assert_editor_state(indoc!(
5592 r#"abc
5593 defˇghi
5594
5595 jk
5596 nlmo
5597 "#
5598 ));
5599
5600 cx.update_editor(|editor, window, cx| {
5601 editor.add_selection_below(&Default::default(), window, cx);
5602 });
5603
5604 cx.assert_editor_state(indoc!(
5605 r#"abc
5606 defˇghi
5607
5608 jk
5609 nlmˇo
5610 "#
5611 ));
5612
5613 cx.update_editor(|editor, window, cx| {
5614 editor.add_selection_below(&Default::default(), window, cx);
5615 });
5616
5617 cx.assert_editor_state(indoc!(
5618 r#"abc
5619 defˇghi
5620
5621 jk
5622 nlmˇo
5623 "#
5624 ));
5625
5626 // change selections
5627 cx.set_state(indoc!(
5628 r#"abc
5629 def«ˇg»hi
5630
5631 jk
5632 nlmo
5633 "#
5634 ));
5635
5636 cx.update_editor(|editor, window, cx| {
5637 editor.add_selection_below(&Default::default(), window, cx);
5638 });
5639
5640 cx.assert_editor_state(indoc!(
5641 r#"abc
5642 def«ˇg»hi
5643
5644 jk
5645 nlm«ˇo»
5646 "#
5647 ));
5648
5649 cx.update_editor(|editor, window, cx| {
5650 editor.add_selection_below(&Default::default(), window, cx);
5651 });
5652
5653 cx.assert_editor_state(indoc!(
5654 r#"abc
5655 def«ˇg»hi
5656
5657 jk
5658 nlm«ˇo»
5659 "#
5660 ));
5661
5662 cx.update_editor(|editor, window, cx| {
5663 editor.add_selection_above(&Default::default(), window, cx);
5664 });
5665
5666 cx.assert_editor_state(indoc!(
5667 r#"abc
5668 def«ˇg»hi
5669
5670 jk
5671 nlmo
5672 "#
5673 ));
5674
5675 cx.update_editor(|editor, window, cx| {
5676 editor.add_selection_above(&Default::default(), window, cx);
5677 });
5678
5679 cx.assert_editor_state(indoc!(
5680 r#"abc
5681 def«ˇg»hi
5682
5683 jk
5684 nlmo
5685 "#
5686 ));
5687
5688 // Change selections again
5689 cx.set_state(indoc!(
5690 r#"a«bc
5691 defgˇ»hi
5692
5693 jk
5694 nlmo
5695 "#
5696 ));
5697
5698 cx.update_editor(|editor, window, cx| {
5699 editor.add_selection_below(&Default::default(), window, cx);
5700 });
5701
5702 cx.assert_editor_state(indoc!(
5703 r#"a«bcˇ»
5704 d«efgˇ»hi
5705
5706 j«kˇ»
5707 nlmo
5708 "#
5709 ));
5710
5711 cx.update_editor(|editor, window, cx| {
5712 editor.add_selection_below(&Default::default(), window, cx);
5713 });
5714 cx.assert_editor_state(indoc!(
5715 r#"a«bcˇ»
5716 d«efgˇ»hi
5717
5718 j«kˇ»
5719 n«lmoˇ»
5720 "#
5721 ));
5722 cx.update_editor(|editor, window, cx| {
5723 editor.add_selection_above(&Default::default(), window, cx);
5724 });
5725
5726 cx.assert_editor_state(indoc!(
5727 r#"a«bcˇ»
5728 d«efgˇ»hi
5729
5730 j«kˇ»
5731 nlmo
5732 "#
5733 ));
5734
5735 // Change selections again
5736 cx.set_state(indoc!(
5737 r#"abc
5738 d«ˇefghi
5739
5740 jk
5741 nlm»o
5742 "#
5743 ));
5744
5745 cx.update_editor(|editor, window, cx| {
5746 editor.add_selection_above(&Default::default(), window, cx);
5747 });
5748
5749 cx.assert_editor_state(indoc!(
5750 r#"a«ˇbc»
5751 d«ˇef»ghi
5752
5753 j«ˇk»
5754 n«ˇlm»o
5755 "#
5756 ));
5757
5758 cx.update_editor(|editor, window, cx| {
5759 editor.add_selection_below(&Default::default(), window, cx);
5760 });
5761
5762 cx.assert_editor_state(indoc!(
5763 r#"abc
5764 d«ˇef»ghi
5765
5766 j«ˇk»
5767 n«ˇlm»o
5768 "#
5769 ));
5770}
5771
5772#[gpui::test]
5773async fn test_select_next(cx: &mut TestAppContext) {
5774 init_test(cx, |_| {});
5775
5776 let mut cx = EditorTestContext::new(cx).await;
5777 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5778
5779 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5780 .unwrap();
5781 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5782
5783 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5784 .unwrap();
5785 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5786
5787 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5788 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5789
5790 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5791 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5792
5793 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5794 .unwrap();
5795 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5796
5797 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5798 .unwrap();
5799 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5800}
5801
5802#[gpui::test]
5803async fn test_select_all_matches(cx: &mut TestAppContext) {
5804 init_test(cx, |_| {});
5805
5806 let mut cx = EditorTestContext::new(cx).await;
5807
5808 // Test caret-only selections
5809 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5810 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5811 .unwrap();
5812 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5813
5814 // Test left-to-right selections
5815 cx.set_state("abc\n«abcˇ»\nabc");
5816 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5817 .unwrap();
5818 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
5819
5820 // Test right-to-left selections
5821 cx.set_state("abc\n«ˇabc»\nabc");
5822 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5823 .unwrap();
5824 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
5825
5826 // Test selecting whitespace with caret selection
5827 cx.set_state("abc\nˇ abc\nabc");
5828 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5829 .unwrap();
5830 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
5831
5832 // Test selecting whitespace with left-to-right selection
5833 cx.set_state("abc\n«ˇ »abc\nabc");
5834 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5835 .unwrap();
5836 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
5837
5838 // Test no matches with right-to-left selection
5839 cx.set_state("abc\n« ˇ»abc\nabc");
5840 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5841 .unwrap();
5842 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
5843}
5844
5845#[gpui::test]
5846async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
5847 init_test(cx, |_| {});
5848
5849 let mut cx = EditorTestContext::new(cx).await;
5850
5851 let large_body_1 = "\nd".repeat(200);
5852 let large_body_2 = "\ne".repeat(200);
5853
5854 cx.set_state(&format!(
5855 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
5856 ));
5857 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
5858 let scroll_position = editor.scroll_position(cx);
5859 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
5860 scroll_position
5861 });
5862
5863 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5864 .unwrap();
5865 cx.assert_editor_state(&format!(
5866 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
5867 ));
5868 let scroll_position_after_selection =
5869 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
5870 assert_eq!(
5871 initial_scroll_position, scroll_position_after_selection,
5872 "Scroll position should not change after selecting all matches"
5873 );
5874}
5875
5876#[gpui::test]
5877async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
5878 init_test(cx, |_| {});
5879
5880 let mut cx = EditorTestContext::new(cx).await;
5881 cx.set_state(
5882 r#"let foo = 2;
5883lˇet foo = 2;
5884let fooˇ = 2;
5885let foo = 2;
5886let foo = ˇ2;"#,
5887 );
5888
5889 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5890 .unwrap();
5891 cx.assert_editor_state(
5892 r#"let foo = 2;
5893«letˇ» foo = 2;
5894let «fooˇ» = 2;
5895let foo = 2;
5896let foo = «2ˇ»;"#,
5897 );
5898
5899 // noop for multiple selections with different contents
5900 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5901 .unwrap();
5902 cx.assert_editor_state(
5903 r#"let foo = 2;
5904«letˇ» foo = 2;
5905let «fooˇ» = 2;
5906let foo = 2;
5907let foo = «2ˇ»;"#,
5908 );
5909}
5910
5911#[gpui::test]
5912async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
5913 init_test(cx, |_| {});
5914
5915 let mut cx =
5916 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
5917
5918 cx.assert_editor_state(indoc! {"
5919 ˇbbb
5920 ccc
5921
5922 bbb
5923 ccc
5924 "});
5925 cx.dispatch_action(SelectPrevious::default());
5926 cx.assert_editor_state(indoc! {"
5927 «bbbˇ»
5928 ccc
5929
5930 bbb
5931 ccc
5932 "});
5933 cx.dispatch_action(SelectPrevious::default());
5934 cx.assert_editor_state(indoc! {"
5935 «bbbˇ»
5936 ccc
5937
5938 «bbbˇ»
5939 ccc
5940 "});
5941}
5942
5943#[gpui::test]
5944async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
5945 init_test(cx, |_| {});
5946
5947 let mut cx = EditorTestContext::new(cx).await;
5948 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5949
5950 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5951 .unwrap();
5952 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5953
5954 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5955 .unwrap();
5956 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
5957
5958 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5959 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5960
5961 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5962 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
5963
5964 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5965 .unwrap();
5966 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
5967
5968 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5969 .unwrap();
5970 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndef«abcˇ»\n«abcˇ»");
5971
5972 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5973 .unwrap();
5974 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
5975}
5976
5977#[gpui::test]
5978async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
5979 init_test(cx, |_| {});
5980
5981 let mut cx = EditorTestContext::new(cx).await;
5982 cx.set_state("aˇ");
5983
5984 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5985 .unwrap();
5986 cx.assert_editor_state("«aˇ»");
5987 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5988 .unwrap();
5989 cx.assert_editor_state("«aˇ»");
5990}
5991
5992#[gpui::test]
5993async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
5994 init_test(cx, |_| {});
5995
5996 let mut cx = EditorTestContext::new(cx).await;
5997 cx.set_state(
5998 r#"let foo = 2;
5999lˇet foo = 2;
6000let fooˇ = 2;
6001let foo = 2;
6002let foo = ˇ2;"#,
6003 );
6004
6005 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6006 .unwrap();
6007 cx.assert_editor_state(
6008 r#"let foo = 2;
6009«letˇ» foo = 2;
6010let «fooˇ» = 2;
6011let foo = 2;
6012let foo = «2ˇ»;"#,
6013 );
6014
6015 // noop for multiple selections with different contents
6016 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6017 .unwrap();
6018 cx.assert_editor_state(
6019 r#"let foo = 2;
6020«letˇ» foo = 2;
6021let «fooˇ» = 2;
6022let foo = 2;
6023let foo = «2ˇ»;"#,
6024 );
6025}
6026
6027#[gpui::test]
6028async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
6029 init_test(cx, |_| {});
6030
6031 let mut cx = EditorTestContext::new(cx).await;
6032 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6033
6034 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6035 .unwrap();
6036 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
6037
6038 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6039 .unwrap();
6040 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
6041
6042 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6043 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
6044
6045 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6046 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
6047
6048 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6049 .unwrap();
6050 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
6051
6052 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6053 .unwrap();
6054 cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
6055}
6056
6057#[gpui::test]
6058async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
6059 init_test(cx, |_| {});
6060
6061 let language = Arc::new(Language::new(
6062 LanguageConfig::default(),
6063 Some(tree_sitter_rust::LANGUAGE.into()),
6064 ));
6065
6066 let text = r#"
6067 use mod1::mod2::{mod3, mod4};
6068
6069 fn fn_1(param1: bool, param2: &str) {
6070 let var1 = "text";
6071 }
6072 "#
6073 .unindent();
6074
6075 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6076 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6077 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6078
6079 editor
6080 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6081 .await;
6082
6083 editor.update_in(cx, |editor, window, cx| {
6084 editor.change_selections(None, window, cx, |s| {
6085 s.select_display_ranges([
6086 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
6087 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
6088 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
6089 ]);
6090 });
6091 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6092 });
6093 editor.update(cx, |editor, cx| {
6094 assert_text_with_selections(
6095 editor,
6096 indoc! {r#"
6097 use mod1::mod2::{mod3, «mod4ˇ»};
6098
6099 fn fn_1«ˇ(param1: bool, param2: &str)» {
6100 let var1 = "«ˇtext»";
6101 }
6102 "#},
6103 cx,
6104 );
6105 });
6106
6107 editor.update_in(cx, |editor, window, cx| {
6108 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6109 });
6110 editor.update(cx, |editor, cx| {
6111 assert_text_with_selections(
6112 editor,
6113 indoc! {r#"
6114 use mod1::mod2::«{mod3, mod4}ˇ»;
6115
6116 «ˇfn fn_1(param1: bool, param2: &str) {
6117 let var1 = "text";
6118 }»
6119 "#},
6120 cx,
6121 );
6122 });
6123
6124 editor.update_in(cx, |editor, window, cx| {
6125 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6126 });
6127 assert_eq!(
6128 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6129 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6130 );
6131
6132 // Trying to expand the selected syntax node one more time has no effect.
6133 editor.update_in(cx, |editor, window, cx| {
6134 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6135 });
6136 assert_eq!(
6137 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6138 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6139 );
6140
6141 editor.update_in(cx, |editor, window, cx| {
6142 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6143 });
6144 editor.update(cx, |editor, cx| {
6145 assert_text_with_selections(
6146 editor,
6147 indoc! {r#"
6148 use mod1::mod2::«{mod3, mod4}ˇ»;
6149
6150 «ˇfn fn_1(param1: bool, param2: &str) {
6151 let var1 = "text";
6152 }»
6153 "#},
6154 cx,
6155 );
6156 });
6157
6158 editor.update_in(cx, |editor, window, cx| {
6159 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6160 });
6161 editor.update(cx, |editor, cx| {
6162 assert_text_with_selections(
6163 editor,
6164 indoc! {r#"
6165 use mod1::mod2::{mod3, «mod4ˇ»};
6166
6167 fn fn_1«ˇ(param1: bool, param2: &str)» {
6168 let var1 = "«ˇtext»";
6169 }
6170 "#},
6171 cx,
6172 );
6173 });
6174
6175 editor.update_in(cx, |editor, window, cx| {
6176 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6177 });
6178 editor.update(cx, |editor, cx| {
6179 assert_text_with_selections(
6180 editor,
6181 indoc! {r#"
6182 use mod1::mod2::{mod3, mo«ˇ»d4};
6183
6184 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6185 let var1 = "te«ˇ»xt";
6186 }
6187 "#},
6188 cx,
6189 );
6190 });
6191
6192 // Trying to shrink the selected syntax node one more time has no effect.
6193 editor.update_in(cx, |editor, window, cx| {
6194 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6195 });
6196 editor.update_in(cx, |editor, _, cx| {
6197 assert_text_with_selections(
6198 editor,
6199 indoc! {r#"
6200 use mod1::mod2::{mod3, mo«ˇ»d4};
6201
6202 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6203 let var1 = "te«ˇ»xt";
6204 }
6205 "#},
6206 cx,
6207 );
6208 });
6209
6210 // Ensure that we keep expanding the selection if the larger selection starts or ends within
6211 // a fold.
6212 editor.update_in(cx, |editor, window, cx| {
6213 editor.fold_creases(
6214 vec![
6215 Crease::simple(
6216 Point::new(0, 21)..Point::new(0, 24),
6217 FoldPlaceholder::test(),
6218 ),
6219 Crease::simple(
6220 Point::new(3, 20)..Point::new(3, 22),
6221 FoldPlaceholder::test(),
6222 ),
6223 ],
6224 true,
6225 window,
6226 cx,
6227 );
6228 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6229 });
6230 editor.update(cx, |editor, cx| {
6231 assert_text_with_selections(
6232 editor,
6233 indoc! {r#"
6234 use mod1::mod2::«{mod3, mod4}ˇ»;
6235
6236 fn fn_1«ˇ(param1: bool, param2: &str)» {
6237 «ˇlet var1 = "text";»
6238 }
6239 "#},
6240 cx,
6241 );
6242 });
6243}
6244
6245#[gpui::test]
6246async fn test_fold_function_bodies(cx: &mut TestAppContext) {
6247 init_test(cx, |_| {});
6248
6249 let base_text = r#"
6250 impl A {
6251 // this is an uncommitted comment
6252
6253 fn b() {
6254 c();
6255 }
6256
6257 // this is another uncommitted comment
6258
6259 fn d() {
6260 // e
6261 // f
6262 }
6263 }
6264
6265 fn g() {
6266 // h
6267 }
6268 "#
6269 .unindent();
6270
6271 let text = r#"
6272 ˇimpl A {
6273
6274 fn b() {
6275 c();
6276 }
6277
6278 fn d() {
6279 // e
6280 // f
6281 }
6282 }
6283
6284 fn g() {
6285 // h
6286 }
6287 "#
6288 .unindent();
6289
6290 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6291 cx.set_state(&text);
6292 cx.set_head_text(&base_text);
6293 cx.update_editor(|editor, window, cx| {
6294 editor.expand_all_diff_hunks(&Default::default(), window, cx);
6295 });
6296
6297 cx.assert_state_with_diff(
6298 "
6299 ˇimpl A {
6300 - // this is an uncommitted comment
6301
6302 fn b() {
6303 c();
6304 }
6305
6306 - // this is another uncommitted comment
6307 -
6308 fn d() {
6309 // e
6310 // f
6311 }
6312 }
6313
6314 fn g() {
6315 // h
6316 }
6317 "
6318 .unindent(),
6319 );
6320
6321 let expected_display_text = "
6322 impl A {
6323 // this is an uncommitted comment
6324
6325 fn b() {
6326 ⋯
6327 }
6328
6329 // this is another uncommitted comment
6330
6331 fn d() {
6332 ⋯
6333 }
6334 }
6335
6336 fn g() {
6337 ⋯
6338 }
6339 "
6340 .unindent();
6341
6342 cx.update_editor(|editor, window, cx| {
6343 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
6344 assert_eq!(editor.display_text(cx), expected_display_text);
6345 });
6346}
6347
6348#[gpui::test]
6349async fn test_autoindent(cx: &mut TestAppContext) {
6350 init_test(cx, |_| {});
6351
6352 let language = Arc::new(
6353 Language::new(
6354 LanguageConfig {
6355 brackets: BracketPairConfig {
6356 pairs: vec![
6357 BracketPair {
6358 start: "{".to_string(),
6359 end: "}".to_string(),
6360 close: false,
6361 surround: false,
6362 newline: true,
6363 },
6364 BracketPair {
6365 start: "(".to_string(),
6366 end: ")".to_string(),
6367 close: false,
6368 surround: false,
6369 newline: true,
6370 },
6371 ],
6372 ..Default::default()
6373 },
6374 ..Default::default()
6375 },
6376 Some(tree_sitter_rust::LANGUAGE.into()),
6377 )
6378 .with_indents_query(
6379 r#"
6380 (_ "(" ")" @end) @indent
6381 (_ "{" "}" @end) @indent
6382 "#,
6383 )
6384 .unwrap(),
6385 );
6386
6387 let text = "fn a() {}";
6388
6389 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6390 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6391 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6392 editor
6393 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6394 .await;
6395
6396 editor.update_in(cx, |editor, window, cx| {
6397 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
6398 editor.newline(&Newline, window, cx);
6399 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
6400 assert_eq!(
6401 editor.selections.ranges(cx),
6402 &[
6403 Point::new(1, 4)..Point::new(1, 4),
6404 Point::new(3, 4)..Point::new(3, 4),
6405 Point::new(5, 0)..Point::new(5, 0)
6406 ]
6407 );
6408 });
6409}
6410
6411#[gpui::test]
6412async fn test_autoindent_selections(cx: &mut TestAppContext) {
6413 init_test(cx, |_| {});
6414
6415 {
6416 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6417 cx.set_state(indoc! {"
6418 impl A {
6419
6420 fn b() {}
6421
6422 «fn c() {
6423
6424 }ˇ»
6425 }
6426 "});
6427
6428 cx.update_editor(|editor, window, cx| {
6429 editor.autoindent(&Default::default(), window, cx);
6430 });
6431
6432 cx.assert_editor_state(indoc! {"
6433 impl A {
6434
6435 fn b() {}
6436
6437 «fn c() {
6438
6439 }ˇ»
6440 }
6441 "});
6442 }
6443
6444 {
6445 let mut cx = EditorTestContext::new_multibuffer(
6446 cx,
6447 [indoc! { "
6448 impl A {
6449 «
6450 // a
6451 fn b(){}
6452 »
6453 «
6454 }
6455 fn c(){}
6456 »
6457 "}],
6458 );
6459
6460 let buffer = cx.update_editor(|editor, _, cx| {
6461 let buffer = editor.buffer().update(cx, |buffer, _| {
6462 buffer.all_buffers().iter().next().unwrap().clone()
6463 });
6464 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6465 buffer
6466 });
6467
6468 cx.run_until_parked();
6469 cx.update_editor(|editor, window, cx| {
6470 editor.select_all(&Default::default(), window, cx);
6471 editor.autoindent(&Default::default(), window, cx)
6472 });
6473 cx.run_until_parked();
6474
6475 cx.update(|_, cx| {
6476 assert_eq!(
6477 buffer.read(cx).text(),
6478 indoc! { "
6479 impl A {
6480
6481 // a
6482 fn b(){}
6483
6484
6485 }
6486 fn c(){}
6487
6488 " }
6489 )
6490 });
6491 }
6492}
6493
6494#[gpui::test]
6495async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
6496 init_test(cx, |_| {});
6497
6498 let mut cx = EditorTestContext::new(cx).await;
6499
6500 let language = Arc::new(Language::new(
6501 LanguageConfig {
6502 brackets: BracketPairConfig {
6503 pairs: vec![
6504 BracketPair {
6505 start: "{".to_string(),
6506 end: "}".to_string(),
6507 close: true,
6508 surround: true,
6509 newline: true,
6510 },
6511 BracketPair {
6512 start: "(".to_string(),
6513 end: ")".to_string(),
6514 close: true,
6515 surround: true,
6516 newline: true,
6517 },
6518 BracketPair {
6519 start: "/*".to_string(),
6520 end: " */".to_string(),
6521 close: true,
6522 surround: true,
6523 newline: true,
6524 },
6525 BracketPair {
6526 start: "[".to_string(),
6527 end: "]".to_string(),
6528 close: false,
6529 surround: false,
6530 newline: true,
6531 },
6532 BracketPair {
6533 start: "\"".to_string(),
6534 end: "\"".to_string(),
6535 close: true,
6536 surround: true,
6537 newline: false,
6538 },
6539 BracketPair {
6540 start: "<".to_string(),
6541 end: ">".to_string(),
6542 close: false,
6543 surround: true,
6544 newline: true,
6545 },
6546 ],
6547 ..Default::default()
6548 },
6549 autoclose_before: "})]".to_string(),
6550 ..Default::default()
6551 },
6552 Some(tree_sitter_rust::LANGUAGE.into()),
6553 ));
6554
6555 cx.language_registry().add(language.clone());
6556 cx.update_buffer(|buffer, cx| {
6557 buffer.set_language(Some(language), cx);
6558 });
6559
6560 cx.set_state(
6561 &r#"
6562 🏀ˇ
6563 εˇ
6564 ❤️ˇ
6565 "#
6566 .unindent(),
6567 );
6568
6569 // autoclose multiple nested brackets at multiple cursors
6570 cx.update_editor(|editor, window, cx| {
6571 editor.handle_input("{", window, cx);
6572 editor.handle_input("{", window, cx);
6573 editor.handle_input("{", window, cx);
6574 });
6575 cx.assert_editor_state(
6576 &"
6577 🏀{{{ˇ}}}
6578 ε{{{ˇ}}}
6579 ❤️{{{ˇ}}}
6580 "
6581 .unindent(),
6582 );
6583
6584 // insert a different closing bracket
6585 cx.update_editor(|editor, window, cx| {
6586 editor.handle_input(")", window, cx);
6587 });
6588 cx.assert_editor_state(
6589 &"
6590 🏀{{{)ˇ}}}
6591 ε{{{)ˇ}}}
6592 ❤️{{{)ˇ}}}
6593 "
6594 .unindent(),
6595 );
6596
6597 // skip over the auto-closed brackets when typing a closing bracket
6598 cx.update_editor(|editor, window, cx| {
6599 editor.move_right(&MoveRight, window, cx);
6600 editor.handle_input("}", window, cx);
6601 editor.handle_input("}", window, cx);
6602 editor.handle_input("}", window, cx);
6603 });
6604 cx.assert_editor_state(
6605 &"
6606 🏀{{{)}}}}ˇ
6607 ε{{{)}}}}ˇ
6608 ❤️{{{)}}}}ˇ
6609 "
6610 .unindent(),
6611 );
6612
6613 // autoclose multi-character pairs
6614 cx.set_state(
6615 &"
6616 ˇ
6617 ˇ
6618 "
6619 .unindent(),
6620 );
6621 cx.update_editor(|editor, window, cx| {
6622 editor.handle_input("/", window, cx);
6623 editor.handle_input("*", window, cx);
6624 });
6625 cx.assert_editor_state(
6626 &"
6627 /*ˇ */
6628 /*ˇ */
6629 "
6630 .unindent(),
6631 );
6632
6633 // one cursor autocloses a multi-character pair, one cursor
6634 // does not autoclose.
6635 cx.set_state(
6636 &"
6637 /ˇ
6638 ˇ
6639 "
6640 .unindent(),
6641 );
6642 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
6643 cx.assert_editor_state(
6644 &"
6645 /*ˇ */
6646 *ˇ
6647 "
6648 .unindent(),
6649 );
6650
6651 // Don't autoclose if the next character isn't whitespace and isn't
6652 // listed in the language's "autoclose_before" section.
6653 cx.set_state("ˇa b");
6654 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6655 cx.assert_editor_state("{ˇa b");
6656
6657 // Don't autoclose if `close` is false for the bracket pair
6658 cx.set_state("ˇ");
6659 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
6660 cx.assert_editor_state("[ˇ");
6661
6662 // Surround with brackets if text is selected
6663 cx.set_state("«aˇ» b");
6664 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6665 cx.assert_editor_state("{«aˇ»} b");
6666
6667 // Autoclose when not immediately after a word character
6668 cx.set_state("a ˇ");
6669 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6670 cx.assert_editor_state("a \"ˇ\"");
6671
6672 // Autoclose pair where the start and end characters are the same
6673 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6674 cx.assert_editor_state("a \"\"ˇ");
6675
6676 // Don't autoclose when immediately after a word character
6677 cx.set_state("aˇ");
6678 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6679 cx.assert_editor_state("a\"ˇ");
6680
6681 // Do autoclose when after a non-word character
6682 cx.set_state("{ˇ");
6683 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6684 cx.assert_editor_state("{\"ˇ\"");
6685
6686 // Non identical pairs autoclose regardless of preceding character
6687 cx.set_state("aˇ");
6688 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6689 cx.assert_editor_state("a{ˇ}");
6690
6691 // Don't autoclose pair if autoclose is disabled
6692 cx.set_state("ˇ");
6693 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6694 cx.assert_editor_state("<ˇ");
6695
6696 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
6697 cx.set_state("«aˇ» b");
6698 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6699 cx.assert_editor_state("<«aˇ»> b");
6700}
6701
6702#[gpui::test]
6703async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
6704 init_test(cx, |settings| {
6705 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
6706 });
6707
6708 let mut cx = EditorTestContext::new(cx).await;
6709
6710 let language = Arc::new(Language::new(
6711 LanguageConfig {
6712 brackets: BracketPairConfig {
6713 pairs: vec![
6714 BracketPair {
6715 start: "{".to_string(),
6716 end: "}".to_string(),
6717 close: true,
6718 surround: true,
6719 newline: true,
6720 },
6721 BracketPair {
6722 start: "(".to_string(),
6723 end: ")".to_string(),
6724 close: true,
6725 surround: true,
6726 newline: true,
6727 },
6728 BracketPair {
6729 start: "[".to_string(),
6730 end: "]".to_string(),
6731 close: false,
6732 surround: false,
6733 newline: true,
6734 },
6735 ],
6736 ..Default::default()
6737 },
6738 autoclose_before: "})]".to_string(),
6739 ..Default::default()
6740 },
6741 Some(tree_sitter_rust::LANGUAGE.into()),
6742 ));
6743
6744 cx.language_registry().add(language.clone());
6745 cx.update_buffer(|buffer, cx| {
6746 buffer.set_language(Some(language), cx);
6747 });
6748
6749 cx.set_state(
6750 &"
6751 ˇ
6752 ˇ
6753 ˇ
6754 "
6755 .unindent(),
6756 );
6757
6758 // ensure only matching closing brackets are skipped over
6759 cx.update_editor(|editor, window, cx| {
6760 editor.handle_input("}", window, cx);
6761 editor.move_left(&MoveLeft, window, cx);
6762 editor.handle_input(")", window, cx);
6763 editor.move_left(&MoveLeft, window, cx);
6764 });
6765 cx.assert_editor_state(
6766 &"
6767 ˇ)}
6768 ˇ)}
6769 ˇ)}
6770 "
6771 .unindent(),
6772 );
6773
6774 // skip-over closing brackets at multiple cursors
6775 cx.update_editor(|editor, window, cx| {
6776 editor.handle_input(")", window, cx);
6777 editor.handle_input("}", window, cx);
6778 });
6779 cx.assert_editor_state(
6780 &"
6781 )}ˇ
6782 )}ˇ
6783 )}ˇ
6784 "
6785 .unindent(),
6786 );
6787
6788 // ignore non-close brackets
6789 cx.update_editor(|editor, window, cx| {
6790 editor.handle_input("]", window, cx);
6791 editor.move_left(&MoveLeft, window, cx);
6792 editor.handle_input("]", window, cx);
6793 });
6794 cx.assert_editor_state(
6795 &"
6796 )}]ˇ]
6797 )}]ˇ]
6798 )}]ˇ]
6799 "
6800 .unindent(),
6801 );
6802}
6803
6804#[gpui::test]
6805async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
6806 init_test(cx, |_| {});
6807
6808 let mut cx = EditorTestContext::new(cx).await;
6809
6810 let html_language = Arc::new(
6811 Language::new(
6812 LanguageConfig {
6813 name: "HTML".into(),
6814 brackets: BracketPairConfig {
6815 pairs: vec![
6816 BracketPair {
6817 start: "<".into(),
6818 end: ">".into(),
6819 close: true,
6820 ..Default::default()
6821 },
6822 BracketPair {
6823 start: "{".into(),
6824 end: "}".into(),
6825 close: true,
6826 ..Default::default()
6827 },
6828 BracketPair {
6829 start: "(".into(),
6830 end: ")".into(),
6831 close: true,
6832 ..Default::default()
6833 },
6834 ],
6835 ..Default::default()
6836 },
6837 autoclose_before: "})]>".into(),
6838 ..Default::default()
6839 },
6840 Some(tree_sitter_html::LANGUAGE.into()),
6841 )
6842 .with_injection_query(
6843 r#"
6844 (script_element
6845 (raw_text) @injection.content
6846 (#set! injection.language "javascript"))
6847 "#,
6848 )
6849 .unwrap(),
6850 );
6851
6852 let javascript_language = Arc::new(Language::new(
6853 LanguageConfig {
6854 name: "JavaScript".into(),
6855 brackets: BracketPairConfig {
6856 pairs: vec![
6857 BracketPair {
6858 start: "/*".into(),
6859 end: " */".into(),
6860 close: true,
6861 ..Default::default()
6862 },
6863 BracketPair {
6864 start: "{".into(),
6865 end: "}".into(),
6866 close: true,
6867 ..Default::default()
6868 },
6869 BracketPair {
6870 start: "(".into(),
6871 end: ")".into(),
6872 close: true,
6873 ..Default::default()
6874 },
6875 ],
6876 ..Default::default()
6877 },
6878 autoclose_before: "})]>".into(),
6879 ..Default::default()
6880 },
6881 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
6882 ));
6883
6884 cx.language_registry().add(html_language.clone());
6885 cx.language_registry().add(javascript_language.clone());
6886
6887 cx.update_buffer(|buffer, cx| {
6888 buffer.set_language(Some(html_language), cx);
6889 });
6890
6891 cx.set_state(
6892 &r#"
6893 <body>ˇ
6894 <script>
6895 var x = 1;ˇ
6896 </script>
6897 </body>ˇ
6898 "#
6899 .unindent(),
6900 );
6901
6902 // Precondition: different languages are active at different locations.
6903 cx.update_editor(|editor, window, cx| {
6904 let snapshot = editor.snapshot(window, cx);
6905 let cursors = editor.selections.ranges::<usize>(cx);
6906 let languages = cursors
6907 .iter()
6908 .map(|c| snapshot.language_at(c.start).unwrap().name())
6909 .collect::<Vec<_>>();
6910 assert_eq!(
6911 languages,
6912 &["HTML".into(), "JavaScript".into(), "HTML".into()]
6913 );
6914 });
6915
6916 // Angle brackets autoclose in HTML, but not JavaScript.
6917 cx.update_editor(|editor, window, cx| {
6918 editor.handle_input("<", window, cx);
6919 editor.handle_input("a", window, cx);
6920 });
6921 cx.assert_editor_state(
6922 &r#"
6923 <body><aˇ>
6924 <script>
6925 var x = 1;<aˇ
6926 </script>
6927 </body><aˇ>
6928 "#
6929 .unindent(),
6930 );
6931
6932 // Curly braces and parens autoclose in both HTML and JavaScript.
6933 cx.update_editor(|editor, window, cx| {
6934 editor.handle_input(" b=", window, cx);
6935 editor.handle_input("{", window, cx);
6936 editor.handle_input("c", window, cx);
6937 editor.handle_input("(", window, cx);
6938 });
6939 cx.assert_editor_state(
6940 &r#"
6941 <body><a b={c(ˇ)}>
6942 <script>
6943 var x = 1;<a b={c(ˇ)}
6944 </script>
6945 </body><a b={c(ˇ)}>
6946 "#
6947 .unindent(),
6948 );
6949
6950 // Brackets that were already autoclosed are skipped.
6951 cx.update_editor(|editor, window, cx| {
6952 editor.handle_input(")", window, cx);
6953 editor.handle_input("d", window, cx);
6954 editor.handle_input("}", window, cx);
6955 });
6956 cx.assert_editor_state(
6957 &r#"
6958 <body><a b={c()d}ˇ>
6959 <script>
6960 var x = 1;<a b={c()d}ˇ
6961 </script>
6962 </body><a b={c()d}ˇ>
6963 "#
6964 .unindent(),
6965 );
6966 cx.update_editor(|editor, window, cx| {
6967 editor.handle_input(">", window, cx);
6968 });
6969 cx.assert_editor_state(
6970 &r#"
6971 <body><a b={c()d}>ˇ
6972 <script>
6973 var x = 1;<a b={c()d}>ˇ
6974 </script>
6975 </body><a b={c()d}>ˇ
6976 "#
6977 .unindent(),
6978 );
6979
6980 // Reset
6981 cx.set_state(
6982 &r#"
6983 <body>ˇ
6984 <script>
6985 var x = 1;ˇ
6986 </script>
6987 </body>ˇ
6988 "#
6989 .unindent(),
6990 );
6991
6992 cx.update_editor(|editor, window, cx| {
6993 editor.handle_input("<", window, cx);
6994 });
6995 cx.assert_editor_state(
6996 &r#"
6997 <body><ˇ>
6998 <script>
6999 var x = 1;<ˇ
7000 </script>
7001 </body><ˇ>
7002 "#
7003 .unindent(),
7004 );
7005
7006 // When backspacing, the closing angle brackets are removed.
7007 cx.update_editor(|editor, window, cx| {
7008 editor.backspace(&Backspace, window, cx);
7009 });
7010 cx.assert_editor_state(
7011 &r#"
7012 <body>ˇ
7013 <script>
7014 var x = 1;ˇ
7015 </script>
7016 </body>ˇ
7017 "#
7018 .unindent(),
7019 );
7020
7021 // Block comments autoclose in JavaScript, but not HTML.
7022 cx.update_editor(|editor, window, cx| {
7023 editor.handle_input("/", window, cx);
7024 editor.handle_input("*", window, cx);
7025 });
7026 cx.assert_editor_state(
7027 &r#"
7028 <body>/*ˇ
7029 <script>
7030 var x = 1;/*ˇ */
7031 </script>
7032 </body>/*ˇ
7033 "#
7034 .unindent(),
7035 );
7036}
7037
7038#[gpui::test]
7039async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
7040 init_test(cx, |_| {});
7041
7042 let mut cx = EditorTestContext::new(cx).await;
7043
7044 let rust_language = Arc::new(
7045 Language::new(
7046 LanguageConfig {
7047 name: "Rust".into(),
7048 brackets: serde_json::from_value(json!([
7049 { "start": "{", "end": "}", "close": true, "newline": true },
7050 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
7051 ]))
7052 .unwrap(),
7053 autoclose_before: "})]>".into(),
7054 ..Default::default()
7055 },
7056 Some(tree_sitter_rust::LANGUAGE.into()),
7057 )
7058 .with_override_query("(string_literal) @string")
7059 .unwrap(),
7060 );
7061
7062 cx.language_registry().add(rust_language.clone());
7063 cx.update_buffer(|buffer, cx| {
7064 buffer.set_language(Some(rust_language), cx);
7065 });
7066
7067 cx.set_state(
7068 &r#"
7069 let x = ˇ
7070 "#
7071 .unindent(),
7072 );
7073
7074 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
7075 cx.update_editor(|editor, window, cx| {
7076 editor.handle_input("\"", window, cx);
7077 });
7078 cx.assert_editor_state(
7079 &r#"
7080 let x = "ˇ"
7081 "#
7082 .unindent(),
7083 );
7084
7085 // Inserting another quotation mark. The cursor moves across the existing
7086 // automatically-inserted quotation mark.
7087 cx.update_editor(|editor, window, cx| {
7088 editor.handle_input("\"", window, cx);
7089 });
7090 cx.assert_editor_state(
7091 &r#"
7092 let x = ""ˇ
7093 "#
7094 .unindent(),
7095 );
7096
7097 // Reset
7098 cx.set_state(
7099 &r#"
7100 let x = ˇ
7101 "#
7102 .unindent(),
7103 );
7104
7105 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
7106 cx.update_editor(|editor, window, cx| {
7107 editor.handle_input("\"", window, cx);
7108 editor.handle_input(" ", window, cx);
7109 editor.move_left(&Default::default(), window, cx);
7110 editor.handle_input("\\", window, cx);
7111 editor.handle_input("\"", window, cx);
7112 });
7113 cx.assert_editor_state(
7114 &r#"
7115 let x = "\"ˇ "
7116 "#
7117 .unindent(),
7118 );
7119
7120 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
7121 // mark. Nothing is inserted.
7122 cx.update_editor(|editor, window, cx| {
7123 editor.move_right(&Default::default(), window, cx);
7124 editor.handle_input("\"", window, cx);
7125 });
7126 cx.assert_editor_state(
7127 &r#"
7128 let x = "\" "ˇ
7129 "#
7130 .unindent(),
7131 );
7132}
7133
7134#[gpui::test]
7135async fn test_surround_with_pair(cx: &mut TestAppContext) {
7136 init_test(cx, |_| {});
7137
7138 let language = Arc::new(Language::new(
7139 LanguageConfig {
7140 brackets: BracketPairConfig {
7141 pairs: vec![
7142 BracketPair {
7143 start: "{".to_string(),
7144 end: "}".to_string(),
7145 close: true,
7146 surround: true,
7147 newline: true,
7148 },
7149 BracketPair {
7150 start: "/* ".to_string(),
7151 end: "*/".to_string(),
7152 close: true,
7153 surround: true,
7154 ..Default::default()
7155 },
7156 ],
7157 ..Default::default()
7158 },
7159 ..Default::default()
7160 },
7161 Some(tree_sitter_rust::LANGUAGE.into()),
7162 ));
7163
7164 let text = r#"
7165 a
7166 b
7167 c
7168 "#
7169 .unindent();
7170
7171 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7172 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7173 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7174 editor
7175 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7176 .await;
7177
7178 editor.update_in(cx, |editor, window, cx| {
7179 editor.change_selections(None, window, cx, |s| {
7180 s.select_display_ranges([
7181 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7182 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7183 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
7184 ])
7185 });
7186
7187 editor.handle_input("{", window, cx);
7188 editor.handle_input("{", window, cx);
7189 editor.handle_input("{", window, cx);
7190 assert_eq!(
7191 editor.text(cx),
7192 "
7193 {{{a}}}
7194 {{{b}}}
7195 {{{c}}}
7196 "
7197 .unindent()
7198 );
7199 assert_eq!(
7200 editor.selections.display_ranges(cx),
7201 [
7202 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
7203 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
7204 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
7205 ]
7206 );
7207
7208 editor.undo(&Undo, window, cx);
7209 editor.undo(&Undo, window, cx);
7210 editor.undo(&Undo, window, cx);
7211 assert_eq!(
7212 editor.text(cx),
7213 "
7214 a
7215 b
7216 c
7217 "
7218 .unindent()
7219 );
7220 assert_eq!(
7221 editor.selections.display_ranges(cx),
7222 [
7223 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7224 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7225 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7226 ]
7227 );
7228
7229 // Ensure inserting the first character of a multi-byte bracket pair
7230 // doesn't surround the selections with the bracket.
7231 editor.handle_input("/", window, cx);
7232 assert_eq!(
7233 editor.text(cx),
7234 "
7235 /
7236 /
7237 /
7238 "
7239 .unindent()
7240 );
7241 assert_eq!(
7242 editor.selections.display_ranges(cx),
7243 [
7244 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7245 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7246 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7247 ]
7248 );
7249
7250 editor.undo(&Undo, window, cx);
7251 assert_eq!(
7252 editor.text(cx),
7253 "
7254 a
7255 b
7256 c
7257 "
7258 .unindent()
7259 );
7260 assert_eq!(
7261 editor.selections.display_ranges(cx),
7262 [
7263 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7264 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7265 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7266 ]
7267 );
7268
7269 // Ensure inserting the last character of a multi-byte bracket pair
7270 // doesn't surround the selections with the bracket.
7271 editor.handle_input("*", window, cx);
7272 assert_eq!(
7273 editor.text(cx),
7274 "
7275 *
7276 *
7277 *
7278 "
7279 .unindent()
7280 );
7281 assert_eq!(
7282 editor.selections.display_ranges(cx),
7283 [
7284 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7285 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7286 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7287 ]
7288 );
7289 });
7290}
7291
7292#[gpui::test]
7293async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
7294 init_test(cx, |_| {});
7295
7296 let language = Arc::new(Language::new(
7297 LanguageConfig {
7298 brackets: BracketPairConfig {
7299 pairs: vec![BracketPair {
7300 start: "{".to_string(),
7301 end: "}".to_string(),
7302 close: true,
7303 surround: true,
7304 newline: true,
7305 }],
7306 ..Default::default()
7307 },
7308 autoclose_before: "}".to_string(),
7309 ..Default::default()
7310 },
7311 Some(tree_sitter_rust::LANGUAGE.into()),
7312 ));
7313
7314 let text = r#"
7315 a
7316 b
7317 c
7318 "#
7319 .unindent();
7320
7321 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7322 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7323 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7324 editor
7325 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7326 .await;
7327
7328 editor.update_in(cx, |editor, window, cx| {
7329 editor.change_selections(None, window, cx, |s| {
7330 s.select_ranges([
7331 Point::new(0, 1)..Point::new(0, 1),
7332 Point::new(1, 1)..Point::new(1, 1),
7333 Point::new(2, 1)..Point::new(2, 1),
7334 ])
7335 });
7336
7337 editor.handle_input("{", window, cx);
7338 editor.handle_input("{", window, cx);
7339 editor.handle_input("_", window, cx);
7340 assert_eq!(
7341 editor.text(cx),
7342 "
7343 a{{_}}
7344 b{{_}}
7345 c{{_}}
7346 "
7347 .unindent()
7348 );
7349 assert_eq!(
7350 editor.selections.ranges::<Point>(cx),
7351 [
7352 Point::new(0, 4)..Point::new(0, 4),
7353 Point::new(1, 4)..Point::new(1, 4),
7354 Point::new(2, 4)..Point::new(2, 4)
7355 ]
7356 );
7357
7358 editor.backspace(&Default::default(), window, cx);
7359 editor.backspace(&Default::default(), window, cx);
7360 assert_eq!(
7361 editor.text(cx),
7362 "
7363 a{}
7364 b{}
7365 c{}
7366 "
7367 .unindent()
7368 );
7369 assert_eq!(
7370 editor.selections.ranges::<Point>(cx),
7371 [
7372 Point::new(0, 2)..Point::new(0, 2),
7373 Point::new(1, 2)..Point::new(1, 2),
7374 Point::new(2, 2)..Point::new(2, 2)
7375 ]
7376 );
7377
7378 editor.delete_to_previous_word_start(&Default::default(), window, cx);
7379 assert_eq!(
7380 editor.text(cx),
7381 "
7382 a
7383 b
7384 c
7385 "
7386 .unindent()
7387 );
7388 assert_eq!(
7389 editor.selections.ranges::<Point>(cx),
7390 [
7391 Point::new(0, 1)..Point::new(0, 1),
7392 Point::new(1, 1)..Point::new(1, 1),
7393 Point::new(2, 1)..Point::new(2, 1)
7394 ]
7395 );
7396 });
7397}
7398
7399#[gpui::test]
7400async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
7401 init_test(cx, |settings| {
7402 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7403 });
7404
7405 let mut cx = EditorTestContext::new(cx).await;
7406
7407 let language = Arc::new(Language::new(
7408 LanguageConfig {
7409 brackets: BracketPairConfig {
7410 pairs: vec![
7411 BracketPair {
7412 start: "{".to_string(),
7413 end: "}".to_string(),
7414 close: true,
7415 surround: true,
7416 newline: true,
7417 },
7418 BracketPair {
7419 start: "(".to_string(),
7420 end: ")".to_string(),
7421 close: true,
7422 surround: true,
7423 newline: true,
7424 },
7425 BracketPair {
7426 start: "[".to_string(),
7427 end: "]".to_string(),
7428 close: false,
7429 surround: true,
7430 newline: true,
7431 },
7432 ],
7433 ..Default::default()
7434 },
7435 autoclose_before: "})]".to_string(),
7436 ..Default::default()
7437 },
7438 Some(tree_sitter_rust::LANGUAGE.into()),
7439 ));
7440
7441 cx.language_registry().add(language.clone());
7442 cx.update_buffer(|buffer, cx| {
7443 buffer.set_language(Some(language), cx);
7444 });
7445
7446 cx.set_state(
7447 &"
7448 {(ˇ)}
7449 [[ˇ]]
7450 {(ˇ)}
7451 "
7452 .unindent(),
7453 );
7454
7455 cx.update_editor(|editor, window, cx| {
7456 editor.backspace(&Default::default(), window, cx);
7457 editor.backspace(&Default::default(), window, cx);
7458 });
7459
7460 cx.assert_editor_state(
7461 &"
7462 ˇ
7463 ˇ]]
7464 ˇ
7465 "
7466 .unindent(),
7467 );
7468
7469 cx.update_editor(|editor, window, cx| {
7470 editor.handle_input("{", window, cx);
7471 editor.handle_input("{", window, cx);
7472 editor.move_right(&MoveRight, window, cx);
7473 editor.move_right(&MoveRight, window, cx);
7474 editor.move_left(&MoveLeft, window, cx);
7475 editor.move_left(&MoveLeft, window, cx);
7476 editor.backspace(&Default::default(), window, cx);
7477 });
7478
7479 cx.assert_editor_state(
7480 &"
7481 {ˇ}
7482 {ˇ}]]
7483 {ˇ}
7484 "
7485 .unindent(),
7486 );
7487
7488 cx.update_editor(|editor, window, cx| {
7489 editor.backspace(&Default::default(), window, cx);
7490 });
7491
7492 cx.assert_editor_state(
7493 &"
7494 ˇ
7495 ˇ]]
7496 ˇ
7497 "
7498 .unindent(),
7499 );
7500}
7501
7502#[gpui::test]
7503async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
7504 init_test(cx, |_| {});
7505
7506 let language = Arc::new(Language::new(
7507 LanguageConfig::default(),
7508 Some(tree_sitter_rust::LANGUAGE.into()),
7509 ));
7510
7511 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
7512 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7513 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7514 editor
7515 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7516 .await;
7517
7518 editor.update_in(cx, |editor, window, cx| {
7519 editor.set_auto_replace_emoji_shortcode(true);
7520
7521 editor.handle_input("Hello ", window, cx);
7522 editor.handle_input(":wave", window, cx);
7523 assert_eq!(editor.text(cx), "Hello :wave".unindent());
7524
7525 editor.handle_input(":", window, cx);
7526 assert_eq!(editor.text(cx), "Hello 👋".unindent());
7527
7528 editor.handle_input(" :smile", window, cx);
7529 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
7530
7531 editor.handle_input(":", window, cx);
7532 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
7533
7534 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
7535 editor.handle_input(":wave", window, cx);
7536 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
7537
7538 editor.handle_input(":", window, cx);
7539 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
7540
7541 editor.handle_input(":1", window, cx);
7542 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
7543
7544 editor.handle_input(":", window, cx);
7545 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
7546
7547 // Ensure shortcode does not get replaced when it is part of a word
7548 editor.handle_input(" Test:wave", window, cx);
7549 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
7550
7551 editor.handle_input(":", window, cx);
7552 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
7553
7554 editor.set_auto_replace_emoji_shortcode(false);
7555
7556 // Ensure shortcode does not get replaced when auto replace is off
7557 editor.handle_input(" :wave", window, cx);
7558 assert_eq!(
7559 editor.text(cx),
7560 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
7561 );
7562
7563 editor.handle_input(":", window, cx);
7564 assert_eq!(
7565 editor.text(cx),
7566 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
7567 );
7568 });
7569}
7570
7571#[gpui::test]
7572async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
7573 init_test(cx, |_| {});
7574
7575 let (text, insertion_ranges) = marked_text_ranges(
7576 indoc! {"
7577 ˇ
7578 "},
7579 false,
7580 );
7581
7582 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7583 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7584
7585 _ = editor.update_in(cx, |editor, window, cx| {
7586 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
7587
7588 editor
7589 .insert_snippet(&insertion_ranges, snippet, window, cx)
7590 .unwrap();
7591
7592 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7593 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7594 assert_eq!(editor.text(cx), expected_text);
7595 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7596 }
7597
7598 assert(
7599 editor,
7600 cx,
7601 indoc! {"
7602 type «» =•
7603 "},
7604 );
7605
7606 assert!(editor.context_menu_visible(), "There should be a matches");
7607 });
7608}
7609
7610#[gpui::test]
7611async fn test_snippets(cx: &mut TestAppContext) {
7612 init_test(cx, |_| {});
7613
7614 let (text, insertion_ranges) = marked_text_ranges(
7615 indoc! {"
7616 a.ˇ b
7617 a.ˇ b
7618 a.ˇ b
7619 "},
7620 false,
7621 );
7622
7623 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7624 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7625
7626 editor.update_in(cx, |editor, window, cx| {
7627 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
7628
7629 editor
7630 .insert_snippet(&insertion_ranges, snippet, window, cx)
7631 .unwrap();
7632
7633 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7634 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7635 assert_eq!(editor.text(cx), expected_text);
7636 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7637 }
7638
7639 assert(
7640 editor,
7641 cx,
7642 indoc! {"
7643 a.f(«one», two, «three») b
7644 a.f(«one», two, «three») b
7645 a.f(«one», two, «three») b
7646 "},
7647 );
7648
7649 // Can't move earlier than the first tab stop
7650 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
7651 assert(
7652 editor,
7653 cx,
7654 indoc! {"
7655 a.f(«one», two, «three») b
7656 a.f(«one», two, «three») b
7657 a.f(«one», two, «three») b
7658 "},
7659 );
7660
7661 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7662 assert(
7663 editor,
7664 cx,
7665 indoc! {"
7666 a.f(one, «two», three) b
7667 a.f(one, «two», three) b
7668 a.f(one, «two», three) b
7669 "},
7670 );
7671
7672 editor.move_to_prev_snippet_tabstop(window, cx);
7673 assert(
7674 editor,
7675 cx,
7676 indoc! {"
7677 a.f(«one», two, «three») b
7678 a.f(«one», two, «three») b
7679 a.f(«one», two, «three») b
7680 "},
7681 );
7682
7683 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7684 assert(
7685 editor,
7686 cx,
7687 indoc! {"
7688 a.f(one, «two», three) b
7689 a.f(one, «two», three) b
7690 a.f(one, «two», three) b
7691 "},
7692 );
7693 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7694 assert(
7695 editor,
7696 cx,
7697 indoc! {"
7698 a.f(one, two, three)ˇ b
7699 a.f(one, two, three)ˇ b
7700 a.f(one, two, three)ˇ b
7701 "},
7702 );
7703
7704 // As soon as the last tab stop is reached, snippet state is gone
7705 editor.move_to_prev_snippet_tabstop(window, cx);
7706 assert(
7707 editor,
7708 cx,
7709 indoc! {"
7710 a.f(one, two, three)ˇ b
7711 a.f(one, two, three)ˇ b
7712 a.f(one, two, three)ˇ b
7713 "},
7714 );
7715 });
7716}
7717
7718#[gpui::test]
7719async fn test_document_format_during_save(cx: &mut TestAppContext) {
7720 init_test(cx, |_| {});
7721
7722 let fs = FakeFs::new(cx.executor());
7723 fs.insert_file(path!("/file.rs"), Default::default()).await;
7724
7725 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
7726
7727 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7728 language_registry.add(rust_lang());
7729 let mut fake_servers = language_registry.register_fake_lsp(
7730 "Rust",
7731 FakeLspAdapter {
7732 capabilities: lsp::ServerCapabilities {
7733 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7734 ..Default::default()
7735 },
7736 ..Default::default()
7737 },
7738 );
7739
7740 let buffer = project
7741 .update(cx, |project, cx| {
7742 project.open_local_buffer(path!("/file.rs"), cx)
7743 })
7744 .await
7745 .unwrap();
7746
7747 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7748 let (editor, cx) = cx.add_window_view(|window, cx| {
7749 build_editor_with_project(project.clone(), buffer, window, cx)
7750 });
7751 editor.update_in(cx, |editor, window, cx| {
7752 editor.set_text("one\ntwo\nthree\n", window, cx)
7753 });
7754 assert!(cx.read(|cx| editor.is_dirty(cx)));
7755
7756 cx.executor().start_waiting();
7757 let fake_server = fake_servers.next().await.unwrap();
7758
7759 {
7760 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
7761 move |params, _| async move {
7762 assert_eq!(
7763 params.text_document.uri,
7764 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7765 );
7766 assert_eq!(params.options.tab_size, 4);
7767 Ok(Some(vec![lsp::TextEdit::new(
7768 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7769 ", ".to_string(),
7770 )]))
7771 },
7772 );
7773 let save = editor
7774 .update_in(cx, |editor, window, cx| {
7775 editor.save(true, project.clone(), window, cx)
7776 })
7777 .unwrap();
7778 cx.executor().start_waiting();
7779 save.await;
7780
7781 assert_eq!(
7782 editor.update(cx, |editor, cx| editor.text(cx)),
7783 "one, two\nthree\n"
7784 );
7785 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7786 }
7787
7788 {
7789 editor.update_in(cx, |editor, window, cx| {
7790 editor.set_text("one\ntwo\nthree\n", window, cx)
7791 });
7792 assert!(cx.read(|cx| editor.is_dirty(cx)));
7793
7794 // Ensure we can still save even if formatting hangs.
7795 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
7796 move |params, _| async move {
7797 assert_eq!(
7798 params.text_document.uri,
7799 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7800 );
7801 futures::future::pending::<()>().await;
7802 unreachable!()
7803 },
7804 );
7805 let save = editor
7806 .update_in(cx, |editor, window, cx| {
7807 editor.save(true, project.clone(), window, cx)
7808 })
7809 .unwrap();
7810 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7811 cx.executor().start_waiting();
7812 save.await;
7813 assert_eq!(
7814 editor.update(cx, |editor, cx| editor.text(cx)),
7815 "one\ntwo\nthree\n"
7816 );
7817 }
7818
7819 // For non-dirty buffer, no formatting request should be sent
7820 {
7821 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7822
7823 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
7824 panic!("Should not be invoked on non-dirty buffer");
7825 });
7826 let save = editor
7827 .update_in(cx, |editor, window, cx| {
7828 editor.save(true, project.clone(), window, cx)
7829 })
7830 .unwrap();
7831 cx.executor().start_waiting();
7832 save.await;
7833 }
7834
7835 // Set rust language override and assert overridden tabsize is sent to language server
7836 update_test_language_settings(cx, |settings| {
7837 settings.languages.insert(
7838 "Rust".into(),
7839 LanguageSettingsContent {
7840 tab_size: NonZeroU32::new(8),
7841 ..Default::default()
7842 },
7843 );
7844 });
7845
7846 {
7847 editor.update_in(cx, |editor, window, cx| {
7848 editor.set_text("somehting_new\n", window, cx)
7849 });
7850 assert!(cx.read(|cx| editor.is_dirty(cx)));
7851 let _formatting_request_signal = fake_server
7852 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
7853 assert_eq!(
7854 params.text_document.uri,
7855 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7856 );
7857 assert_eq!(params.options.tab_size, 8);
7858 Ok(Some(vec![]))
7859 });
7860 let save = editor
7861 .update_in(cx, |editor, window, cx| {
7862 editor.save(true, project.clone(), window, cx)
7863 })
7864 .unwrap();
7865 cx.executor().start_waiting();
7866 save.await;
7867 }
7868}
7869
7870#[gpui::test]
7871async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
7872 init_test(cx, |_| {});
7873
7874 let cols = 4;
7875 let rows = 10;
7876 let sample_text_1 = sample_text(rows, cols, 'a');
7877 assert_eq!(
7878 sample_text_1,
7879 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
7880 );
7881 let sample_text_2 = sample_text(rows, cols, 'l');
7882 assert_eq!(
7883 sample_text_2,
7884 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
7885 );
7886 let sample_text_3 = sample_text(rows, cols, 'v');
7887 assert_eq!(
7888 sample_text_3,
7889 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
7890 );
7891
7892 let fs = FakeFs::new(cx.executor());
7893 fs.insert_tree(
7894 path!("/a"),
7895 json!({
7896 "main.rs": sample_text_1,
7897 "other.rs": sample_text_2,
7898 "lib.rs": sample_text_3,
7899 }),
7900 )
7901 .await;
7902
7903 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
7904 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7905 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
7906
7907 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7908 language_registry.add(rust_lang());
7909 let mut fake_servers = language_registry.register_fake_lsp(
7910 "Rust",
7911 FakeLspAdapter {
7912 capabilities: lsp::ServerCapabilities {
7913 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7914 ..Default::default()
7915 },
7916 ..Default::default()
7917 },
7918 );
7919
7920 let worktree = project.update(cx, |project, cx| {
7921 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
7922 assert_eq!(worktrees.len(), 1);
7923 worktrees.pop().unwrap()
7924 });
7925 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
7926
7927 let buffer_1 = project
7928 .update(cx, |project, cx| {
7929 project.open_buffer((worktree_id, "main.rs"), cx)
7930 })
7931 .await
7932 .unwrap();
7933 let buffer_2 = project
7934 .update(cx, |project, cx| {
7935 project.open_buffer((worktree_id, "other.rs"), cx)
7936 })
7937 .await
7938 .unwrap();
7939 let buffer_3 = project
7940 .update(cx, |project, cx| {
7941 project.open_buffer((worktree_id, "lib.rs"), cx)
7942 })
7943 .await
7944 .unwrap();
7945
7946 let multi_buffer = cx.new(|cx| {
7947 let mut multi_buffer = MultiBuffer::new(ReadWrite);
7948 multi_buffer.push_excerpts(
7949 buffer_1.clone(),
7950 [
7951 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
7952 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
7953 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
7954 ],
7955 cx,
7956 );
7957 multi_buffer.push_excerpts(
7958 buffer_2.clone(),
7959 [
7960 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
7961 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
7962 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
7963 ],
7964 cx,
7965 );
7966 multi_buffer.push_excerpts(
7967 buffer_3.clone(),
7968 [
7969 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
7970 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
7971 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
7972 ],
7973 cx,
7974 );
7975 multi_buffer
7976 });
7977 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
7978 Editor::new(
7979 EditorMode::Full,
7980 multi_buffer,
7981 Some(project.clone()),
7982 window,
7983 cx,
7984 )
7985 });
7986
7987 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7988 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7989 s.select_ranges(Some(1..2))
7990 });
7991 editor.insert("|one|two|three|", window, cx);
7992 });
7993 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7994 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7995 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7996 s.select_ranges(Some(60..70))
7997 });
7998 editor.insert("|four|five|six|", window, cx);
7999 });
8000 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8001
8002 // First two buffers should be edited, but not the third one.
8003 assert_eq!(
8004 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8005 "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}",
8006 );
8007 buffer_1.update(cx, |buffer, _| {
8008 assert!(buffer.is_dirty());
8009 assert_eq!(
8010 buffer.text(),
8011 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
8012 )
8013 });
8014 buffer_2.update(cx, |buffer, _| {
8015 assert!(buffer.is_dirty());
8016 assert_eq!(
8017 buffer.text(),
8018 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
8019 )
8020 });
8021 buffer_3.update(cx, |buffer, _| {
8022 assert!(!buffer.is_dirty());
8023 assert_eq!(buffer.text(), sample_text_3,)
8024 });
8025 cx.executor().run_until_parked();
8026
8027 cx.executor().start_waiting();
8028 let save = multi_buffer_editor
8029 .update_in(cx, |editor, window, cx| {
8030 editor.save(true, project.clone(), window, cx)
8031 })
8032 .unwrap();
8033
8034 let fake_server = fake_servers.next().await.unwrap();
8035 fake_server
8036 .server
8037 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
8038 Ok(Some(vec![lsp::TextEdit::new(
8039 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8040 format!("[{} formatted]", params.text_document.uri),
8041 )]))
8042 })
8043 .detach();
8044 save.await;
8045
8046 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
8047 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
8048 assert_eq!(
8049 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8050 uri!(
8051 "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}"
8052 ),
8053 );
8054 buffer_1.update(cx, |buffer, _| {
8055 assert!(!buffer.is_dirty());
8056 assert_eq!(
8057 buffer.text(),
8058 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
8059 )
8060 });
8061 buffer_2.update(cx, |buffer, _| {
8062 assert!(!buffer.is_dirty());
8063 assert_eq!(
8064 buffer.text(),
8065 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
8066 )
8067 });
8068 buffer_3.update(cx, |buffer, _| {
8069 assert!(!buffer.is_dirty());
8070 assert_eq!(buffer.text(), sample_text_3,)
8071 });
8072}
8073
8074#[gpui::test]
8075async fn test_range_format_during_save(cx: &mut TestAppContext) {
8076 init_test(cx, |_| {});
8077
8078 let fs = FakeFs::new(cx.executor());
8079 fs.insert_file(path!("/file.rs"), Default::default()).await;
8080
8081 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8082
8083 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8084 language_registry.add(rust_lang());
8085 let mut fake_servers = language_registry.register_fake_lsp(
8086 "Rust",
8087 FakeLspAdapter {
8088 capabilities: lsp::ServerCapabilities {
8089 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
8090 ..Default::default()
8091 },
8092 ..Default::default()
8093 },
8094 );
8095
8096 let buffer = project
8097 .update(cx, |project, cx| {
8098 project.open_local_buffer(path!("/file.rs"), cx)
8099 })
8100 .await
8101 .unwrap();
8102
8103 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8104 let (editor, cx) = cx.add_window_view(|window, cx| {
8105 build_editor_with_project(project.clone(), buffer, window, cx)
8106 });
8107 editor.update_in(cx, |editor, window, cx| {
8108 editor.set_text("one\ntwo\nthree\n", window, cx)
8109 });
8110 assert!(cx.read(|cx| editor.is_dirty(cx)));
8111
8112 cx.executor().start_waiting();
8113 let fake_server = fake_servers.next().await.unwrap();
8114
8115 let save = editor
8116 .update_in(cx, |editor, window, cx| {
8117 editor.save(true, project.clone(), window, cx)
8118 })
8119 .unwrap();
8120 fake_server
8121 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8122 assert_eq!(
8123 params.text_document.uri,
8124 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8125 );
8126 assert_eq!(params.options.tab_size, 4);
8127 Ok(Some(vec![lsp::TextEdit::new(
8128 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8129 ", ".to_string(),
8130 )]))
8131 })
8132 .next()
8133 .await;
8134 cx.executor().start_waiting();
8135 save.await;
8136 assert_eq!(
8137 editor.update(cx, |editor, cx| editor.text(cx)),
8138 "one, two\nthree\n"
8139 );
8140 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8141
8142 editor.update_in(cx, |editor, window, cx| {
8143 editor.set_text("one\ntwo\nthree\n", window, cx)
8144 });
8145 assert!(cx.read(|cx| editor.is_dirty(cx)));
8146
8147 // Ensure we can still save even if formatting hangs.
8148 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
8149 move |params, _| async move {
8150 assert_eq!(
8151 params.text_document.uri,
8152 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8153 );
8154 futures::future::pending::<()>().await;
8155 unreachable!()
8156 },
8157 );
8158 let save = editor
8159 .update_in(cx, |editor, window, cx| {
8160 editor.save(true, project.clone(), window, cx)
8161 })
8162 .unwrap();
8163 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8164 cx.executor().start_waiting();
8165 save.await;
8166 assert_eq!(
8167 editor.update(cx, |editor, cx| editor.text(cx)),
8168 "one\ntwo\nthree\n"
8169 );
8170 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8171
8172 // For non-dirty buffer, no formatting request should be sent
8173 let save = editor
8174 .update_in(cx, |editor, window, cx| {
8175 editor.save(true, project.clone(), window, cx)
8176 })
8177 .unwrap();
8178 let _pending_format_request = fake_server
8179 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
8180 panic!("Should not be invoked on non-dirty buffer");
8181 })
8182 .next();
8183 cx.executor().start_waiting();
8184 save.await;
8185
8186 // Set Rust language override and assert overridden tabsize is sent to language server
8187 update_test_language_settings(cx, |settings| {
8188 settings.languages.insert(
8189 "Rust".into(),
8190 LanguageSettingsContent {
8191 tab_size: NonZeroU32::new(8),
8192 ..Default::default()
8193 },
8194 );
8195 });
8196
8197 editor.update_in(cx, |editor, window, cx| {
8198 editor.set_text("somehting_new\n", window, cx)
8199 });
8200 assert!(cx.read(|cx| editor.is_dirty(cx)));
8201 let save = editor
8202 .update_in(cx, |editor, window, cx| {
8203 editor.save(true, project.clone(), window, cx)
8204 })
8205 .unwrap();
8206 fake_server
8207 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8208 assert_eq!(
8209 params.text_document.uri,
8210 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8211 );
8212 assert_eq!(params.options.tab_size, 8);
8213 Ok(Some(vec![]))
8214 })
8215 .next()
8216 .await;
8217 cx.executor().start_waiting();
8218 save.await;
8219}
8220
8221#[gpui::test]
8222async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
8223 init_test(cx, |settings| {
8224 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8225 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8226 ))
8227 });
8228
8229 let fs = FakeFs::new(cx.executor());
8230 fs.insert_file(path!("/file.rs"), Default::default()).await;
8231
8232 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8233
8234 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8235 language_registry.add(Arc::new(Language::new(
8236 LanguageConfig {
8237 name: "Rust".into(),
8238 matcher: LanguageMatcher {
8239 path_suffixes: vec!["rs".to_string()],
8240 ..Default::default()
8241 },
8242 ..LanguageConfig::default()
8243 },
8244 Some(tree_sitter_rust::LANGUAGE.into()),
8245 )));
8246 update_test_language_settings(cx, |settings| {
8247 // Enable Prettier formatting for the same buffer, and ensure
8248 // LSP is called instead of Prettier.
8249 settings.defaults.prettier = Some(PrettierSettings {
8250 allowed: true,
8251 ..PrettierSettings::default()
8252 });
8253 });
8254 let mut fake_servers = language_registry.register_fake_lsp(
8255 "Rust",
8256 FakeLspAdapter {
8257 capabilities: lsp::ServerCapabilities {
8258 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8259 ..Default::default()
8260 },
8261 ..Default::default()
8262 },
8263 );
8264
8265 let buffer = project
8266 .update(cx, |project, cx| {
8267 project.open_local_buffer(path!("/file.rs"), cx)
8268 })
8269 .await
8270 .unwrap();
8271
8272 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8273 let (editor, cx) = cx.add_window_view(|window, cx| {
8274 build_editor_with_project(project.clone(), buffer, window, cx)
8275 });
8276 editor.update_in(cx, |editor, window, cx| {
8277 editor.set_text("one\ntwo\nthree\n", window, cx)
8278 });
8279
8280 cx.executor().start_waiting();
8281 let fake_server = fake_servers.next().await.unwrap();
8282
8283 let format = editor
8284 .update_in(cx, |editor, window, cx| {
8285 editor.perform_format(
8286 project.clone(),
8287 FormatTrigger::Manual,
8288 FormatTarget::Buffers,
8289 window,
8290 cx,
8291 )
8292 })
8293 .unwrap();
8294 fake_server
8295 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8296 assert_eq!(
8297 params.text_document.uri,
8298 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8299 );
8300 assert_eq!(params.options.tab_size, 4);
8301 Ok(Some(vec![lsp::TextEdit::new(
8302 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8303 ", ".to_string(),
8304 )]))
8305 })
8306 .next()
8307 .await;
8308 cx.executor().start_waiting();
8309 format.await;
8310 assert_eq!(
8311 editor.update(cx, |editor, cx| editor.text(cx)),
8312 "one, two\nthree\n"
8313 );
8314
8315 editor.update_in(cx, |editor, window, cx| {
8316 editor.set_text("one\ntwo\nthree\n", window, cx)
8317 });
8318 // Ensure we don't lock if formatting hangs.
8319 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8320 move |params, _| async move {
8321 assert_eq!(
8322 params.text_document.uri,
8323 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8324 );
8325 futures::future::pending::<()>().await;
8326 unreachable!()
8327 },
8328 );
8329 let format = editor
8330 .update_in(cx, |editor, window, cx| {
8331 editor.perform_format(
8332 project,
8333 FormatTrigger::Manual,
8334 FormatTarget::Buffers,
8335 window,
8336 cx,
8337 )
8338 })
8339 .unwrap();
8340 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8341 cx.executor().start_waiting();
8342 format.await;
8343 assert_eq!(
8344 editor.update(cx, |editor, cx| editor.text(cx)),
8345 "one\ntwo\nthree\n"
8346 );
8347}
8348
8349#[gpui::test]
8350async fn test_multiple_formatters(cx: &mut TestAppContext) {
8351 init_test(cx, |settings| {
8352 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
8353 settings.defaults.formatter =
8354 Some(language_settings::SelectedFormatter::List(FormatterList(
8355 vec![
8356 Formatter::LanguageServer { name: None },
8357 Formatter::CodeActions(
8358 [
8359 ("code-action-1".into(), true),
8360 ("code-action-2".into(), true),
8361 ]
8362 .into_iter()
8363 .collect(),
8364 ),
8365 ]
8366 .into(),
8367 )))
8368 });
8369
8370 let fs = FakeFs::new(cx.executor());
8371 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
8372 .await;
8373
8374 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8375 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8376 language_registry.add(rust_lang());
8377
8378 let mut fake_servers = language_registry.register_fake_lsp(
8379 "Rust",
8380 FakeLspAdapter {
8381 capabilities: lsp::ServerCapabilities {
8382 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8383 execute_command_provider: Some(lsp::ExecuteCommandOptions {
8384 commands: vec!["the-command-for-code-action-1".into()],
8385 ..Default::default()
8386 }),
8387 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
8388 ..Default::default()
8389 },
8390 ..Default::default()
8391 },
8392 );
8393
8394 let buffer = project
8395 .update(cx, |project, cx| {
8396 project.open_local_buffer(path!("/file.rs"), cx)
8397 })
8398 .await
8399 .unwrap();
8400
8401 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8402 let (editor, cx) = cx.add_window_view(|window, cx| {
8403 build_editor_with_project(project.clone(), buffer, window, cx)
8404 });
8405
8406 cx.executor().start_waiting();
8407
8408 let fake_server = fake_servers.next().await.unwrap();
8409 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8410 move |_params, _| async move {
8411 Ok(Some(vec![lsp::TextEdit::new(
8412 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8413 "applied-formatting\n".to_string(),
8414 )]))
8415 },
8416 );
8417 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
8418 move |params, _| async move {
8419 assert_eq!(
8420 params.context.only,
8421 Some(vec!["code-action-1".into(), "code-action-2".into()])
8422 );
8423 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
8424 Ok(Some(vec![
8425 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
8426 kind: Some("code-action-1".into()),
8427 edit: Some(lsp::WorkspaceEdit::new(
8428 [(
8429 uri.clone(),
8430 vec![lsp::TextEdit::new(
8431 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8432 "applied-code-action-1-edit\n".to_string(),
8433 )],
8434 )]
8435 .into_iter()
8436 .collect(),
8437 )),
8438 command: Some(lsp::Command {
8439 command: "the-command-for-code-action-1".into(),
8440 ..Default::default()
8441 }),
8442 ..Default::default()
8443 }),
8444 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
8445 kind: Some("code-action-2".into()),
8446 edit: Some(lsp::WorkspaceEdit::new(
8447 [(
8448 uri.clone(),
8449 vec![lsp::TextEdit::new(
8450 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8451 "applied-code-action-2-edit\n".to_string(),
8452 )],
8453 )]
8454 .into_iter()
8455 .collect(),
8456 )),
8457 ..Default::default()
8458 }),
8459 ]))
8460 },
8461 );
8462
8463 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
8464 move |params, _| async move { Ok(params) }
8465 });
8466
8467 let command_lock = Arc::new(futures::lock::Mutex::new(()));
8468 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
8469 let fake = fake_server.clone();
8470 let lock = command_lock.clone();
8471 move |params, _| {
8472 assert_eq!(params.command, "the-command-for-code-action-1");
8473 let fake = fake.clone();
8474 let lock = lock.clone();
8475 async move {
8476 lock.lock().await;
8477 fake.server
8478 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
8479 label: None,
8480 edit: lsp::WorkspaceEdit {
8481 changes: Some(
8482 [(
8483 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
8484 vec![lsp::TextEdit {
8485 range: lsp::Range::new(
8486 lsp::Position::new(0, 0),
8487 lsp::Position::new(0, 0),
8488 ),
8489 new_text: "applied-code-action-1-command\n".into(),
8490 }],
8491 )]
8492 .into_iter()
8493 .collect(),
8494 ),
8495 ..Default::default()
8496 },
8497 })
8498 .await
8499 .unwrap();
8500 Ok(Some(json!(null)))
8501 }
8502 }
8503 });
8504
8505 cx.executor().start_waiting();
8506 editor
8507 .update_in(cx, |editor, window, cx| {
8508 editor.perform_format(
8509 project.clone(),
8510 FormatTrigger::Manual,
8511 FormatTarget::Buffers,
8512 window,
8513 cx,
8514 )
8515 })
8516 .unwrap()
8517 .await;
8518 editor.update(cx, |editor, cx| {
8519 assert_eq!(
8520 editor.text(cx),
8521 r#"
8522 applied-code-action-2-edit
8523 applied-code-action-1-command
8524 applied-code-action-1-edit
8525 applied-formatting
8526 one
8527 two
8528 three
8529 "#
8530 .unindent()
8531 );
8532 });
8533
8534 editor.update_in(cx, |editor, window, cx| {
8535 editor.undo(&Default::default(), window, cx);
8536 assert_eq!(editor.text(cx), "one \ntwo \nthree");
8537 });
8538
8539 // Perform a manual edit while waiting for an LSP command
8540 // that's being run as part of a formatting code action.
8541 let lock_guard = command_lock.lock().await;
8542 let format = editor
8543 .update_in(cx, |editor, window, cx| {
8544 editor.perform_format(
8545 project.clone(),
8546 FormatTrigger::Manual,
8547 FormatTarget::Buffers,
8548 window,
8549 cx,
8550 )
8551 })
8552 .unwrap();
8553 cx.run_until_parked();
8554 editor.update(cx, |editor, cx| {
8555 assert_eq!(
8556 editor.text(cx),
8557 r#"
8558 applied-code-action-1-edit
8559 applied-formatting
8560 one
8561 two
8562 three
8563 "#
8564 .unindent()
8565 );
8566
8567 editor.buffer.update(cx, |buffer, cx| {
8568 let ix = buffer.len(cx);
8569 buffer.edit([(ix..ix, "edited\n")], None, cx);
8570 });
8571 });
8572
8573 // Allow the LSP command to proceed. Because the buffer was edited,
8574 // the second code action will not be run.
8575 drop(lock_guard);
8576 format.await;
8577 editor.update_in(cx, |editor, window, cx| {
8578 assert_eq!(
8579 editor.text(cx),
8580 r#"
8581 applied-code-action-1-command
8582 applied-code-action-1-edit
8583 applied-formatting
8584 one
8585 two
8586 three
8587 edited
8588 "#
8589 .unindent()
8590 );
8591
8592 // The manual edit is undone first, because it is the last thing the user did
8593 // (even though the command completed afterwards).
8594 editor.undo(&Default::default(), window, cx);
8595 assert_eq!(
8596 editor.text(cx),
8597 r#"
8598 applied-code-action-1-command
8599 applied-code-action-1-edit
8600 applied-formatting
8601 one
8602 two
8603 three
8604 "#
8605 .unindent()
8606 );
8607
8608 // All the formatting (including the command, which completed after the manual edit)
8609 // is undone together.
8610 editor.undo(&Default::default(), window, cx);
8611 assert_eq!(editor.text(cx), "one \ntwo \nthree");
8612 });
8613}
8614
8615#[gpui::test]
8616async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
8617 init_test(cx, |settings| {
8618 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8619 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8620 ))
8621 });
8622
8623 let fs = FakeFs::new(cx.executor());
8624 fs.insert_file(path!("/file.ts"), Default::default()).await;
8625
8626 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8627
8628 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8629 language_registry.add(Arc::new(Language::new(
8630 LanguageConfig {
8631 name: "TypeScript".into(),
8632 matcher: LanguageMatcher {
8633 path_suffixes: vec!["ts".to_string()],
8634 ..Default::default()
8635 },
8636 ..LanguageConfig::default()
8637 },
8638 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
8639 )));
8640 update_test_language_settings(cx, |settings| {
8641 settings.defaults.prettier = Some(PrettierSettings {
8642 allowed: true,
8643 ..PrettierSettings::default()
8644 });
8645 });
8646 let mut fake_servers = language_registry.register_fake_lsp(
8647 "TypeScript",
8648 FakeLspAdapter {
8649 capabilities: lsp::ServerCapabilities {
8650 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
8651 ..Default::default()
8652 },
8653 ..Default::default()
8654 },
8655 );
8656
8657 let buffer = project
8658 .update(cx, |project, cx| {
8659 project.open_local_buffer(path!("/file.ts"), cx)
8660 })
8661 .await
8662 .unwrap();
8663
8664 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8665 let (editor, cx) = cx.add_window_view(|window, cx| {
8666 build_editor_with_project(project.clone(), buffer, window, cx)
8667 });
8668 editor.update_in(cx, |editor, window, cx| {
8669 editor.set_text(
8670 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
8671 window,
8672 cx,
8673 )
8674 });
8675
8676 cx.executor().start_waiting();
8677 let fake_server = fake_servers.next().await.unwrap();
8678
8679 let format = editor
8680 .update_in(cx, |editor, window, cx| {
8681 editor.perform_code_action_kind(
8682 project.clone(),
8683 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
8684 window,
8685 cx,
8686 )
8687 })
8688 .unwrap();
8689 fake_server
8690 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
8691 assert_eq!(
8692 params.text_document.uri,
8693 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
8694 );
8695 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
8696 lsp::CodeAction {
8697 title: "Organize Imports".to_string(),
8698 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
8699 edit: Some(lsp::WorkspaceEdit {
8700 changes: Some(
8701 [(
8702 params.text_document.uri.clone(),
8703 vec![lsp::TextEdit::new(
8704 lsp::Range::new(
8705 lsp::Position::new(1, 0),
8706 lsp::Position::new(2, 0),
8707 ),
8708 "".to_string(),
8709 )],
8710 )]
8711 .into_iter()
8712 .collect(),
8713 ),
8714 ..Default::default()
8715 }),
8716 ..Default::default()
8717 },
8718 )]))
8719 })
8720 .next()
8721 .await;
8722 cx.executor().start_waiting();
8723 format.await;
8724 assert_eq!(
8725 editor.update(cx, |editor, cx| editor.text(cx)),
8726 "import { a } from 'module';\n\nconst x = a;\n"
8727 );
8728
8729 editor.update_in(cx, |editor, window, cx| {
8730 editor.set_text(
8731 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
8732 window,
8733 cx,
8734 )
8735 });
8736 // Ensure we don't lock if code action hangs.
8737 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
8738 move |params, _| async move {
8739 assert_eq!(
8740 params.text_document.uri,
8741 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
8742 );
8743 futures::future::pending::<()>().await;
8744 unreachable!()
8745 },
8746 );
8747 let format = editor
8748 .update_in(cx, |editor, window, cx| {
8749 editor.perform_code_action_kind(
8750 project,
8751 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
8752 window,
8753 cx,
8754 )
8755 })
8756 .unwrap();
8757 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
8758 cx.executor().start_waiting();
8759 format.await;
8760 assert_eq!(
8761 editor.update(cx, |editor, cx| editor.text(cx)),
8762 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
8763 );
8764}
8765
8766#[gpui::test]
8767async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
8768 init_test(cx, |_| {});
8769
8770 let mut cx = EditorLspTestContext::new_rust(
8771 lsp::ServerCapabilities {
8772 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8773 ..Default::default()
8774 },
8775 cx,
8776 )
8777 .await;
8778
8779 cx.set_state(indoc! {"
8780 one.twoˇ
8781 "});
8782
8783 // The format request takes a long time. When it completes, it inserts
8784 // a newline and an indent before the `.`
8785 cx.lsp
8786 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
8787 let executor = cx.background_executor().clone();
8788 async move {
8789 executor.timer(Duration::from_millis(100)).await;
8790 Ok(Some(vec![lsp::TextEdit {
8791 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
8792 new_text: "\n ".into(),
8793 }]))
8794 }
8795 });
8796
8797 // Submit a format request.
8798 let format_1 = cx
8799 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8800 .unwrap();
8801 cx.executor().run_until_parked();
8802
8803 // Submit a second format request.
8804 let format_2 = cx
8805 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8806 .unwrap();
8807 cx.executor().run_until_parked();
8808
8809 // Wait for both format requests to complete
8810 cx.executor().advance_clock(Duration::from_millis(200));
8811 cx.executor().start_waiting();
8812 format_1.await.unwrap();
8813 cx.executor().start_waiting();
8814 format_2.await.unwrap();
8815
8816 // The formatting edits only happens once.
8817 cx.assert_editor_state(indoc! {"
8818 one
8819 .twoˇ
8820 "});
8821}
8822
8823#[gpui::test]
8824async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
8825 init_test(cx, |settings| {
8826 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
8827 });
8828
8829 let mut cx = EditorLspTestContext::new_rust(
8830 lsp::ServerCapabilities {
8831 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8832 ..Default::default()
8833 },
8834 cx,
8835 )
8836 .await;
8837
8838 // Set up a buffer white some trailing whitespace and no trailing newline.
8839 cx.set_state(
8840 &[
8841 "one ", //
8842 "twoˇ", //
8843 "three ", //
8844 "four", //
8845 ]
8846 .join("\n"),
8847 );
8848
8849 // Submit a format request.
8850 let format = cx
8851 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8852 .unwrap();
8853
8854 // Record which buffer changes have been sent to the language server
8855 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
8856 cx.lsp
8857 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
8858 let buffer_changes = buffer_changes.clone();
8859 move |params, _| {
8860 buffer_changes.lock().extend(
8861 params
8862 .content_changes
8863 .into_iter()
8864 .map(|e| (e.range.unwrap(), e.text)),
8865 );
8866 }
8867 });
8868
8869 // Handle formatting requests to the language server.
8870 cx.lsp
8871 .set_request_handler::<lsp::request::Formatting, _, _>({
8872 let buffer_changes = buffer_changes.clone();
8873 move |_, _| {
8874 // When formatting is requested, trailing whitespace has already been stripped,
8875 // and the trailing newline has already been added.
8876 assert_eq!(
8877 &buffer_changes.lock()[1..],
8878 &[
8879 (
8880 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
8881 "".into()
8882 ),
8883 (
8884 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
8885 "".into()
8886 ),
8887 (
8888 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
8889 "\n".into()
8890 ),
8891 ]
8892 );
8893
8894 // Insert blank lines between each line of the buffer.
8895 async move {
8896 Ok(Some(vec![
8897 lsp::TextEdit {
8898 range: lsp::Range::new(
8899 lsp::Position::new(1, 0),
8900 lsp::Position::new(1, 0),
8901 ),
8902 new_text: "\n".into(),
8903 },
8904 lsp::TextEdit {
8905 range: lsp::Range::new(
8906 lsp::Position::new(2, 0),
8907 lsp::Position::new(2, 0),
8908 ),
8909 new_text: "\n".into(),
8910 },
8911 ]))
8912 }
8913 }
8914 });
8915
8916 // After formatting the buffer, the trailing whitespace is stripped,
8917 // a newline is appended, and the edits provided by the language server
8918 // have been applied.
8919 format.await.unwrap();
8920 cx.assert_editor_state(
8921 &[
8922 "one", //
8923 "", //
8924 "twoˇ", //
8925 "", //
8926 "three", //
8927 "four", //
8928 "", //
8929 ]
8930 .join("\n"),
8931 );
8932
8933 // Undoing the formatting undoes the trailing whitespace removal, the
8934 // trailing newline, and the LSP edits.
8935 cx.update_buffer(|buffer, cx| buffer.undo(cx));
8936 cx.assert_editor_state(
8937 &[
8938 "one ", //
8939 "twoˇ", //
8940 "three ", //
8941 "four", //
8942 ]
8943 .join("\n"),
8944 );
8945}
8946
8947#[gpui::test]
8948async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
8949 cx: &mut TestAppContext,
8950) {
8951 init_test(cx, |_| {});
8952
8953 cx.update(|cx| {
8954 cx.update_global::<SettingsStore, _>(|settings, cx| {
8955 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8956 settings.auto_signature_help = Some(true);
8957 });
8958 });
8959 });
8960
8961 let mut cx = EditorLspTestContext::new_rust(
8962 lsp::ServerCapabilities {
8963 signature_help_provider: Some(lsp::SignatureHelpOptions {
8964 ..Default::default()
8965 }),
8966 ..Default::default()
8967 },
8968 cx,
8969 )
8970 .await;
8971
8972 let language = Language::new(
8973 LanguageConfig {
8974 name: "Rust".into(),
8975 brackets: BracketPairConfig {
8976 pairs: vec![
8977 BracketPair {
8978 start: "{".to_string(),
8979 end: "}".to_string(),
8980 close: true,
8981 surround: true,
8982 newline: true,
8983 },
8984 BracketPair {
8985 start: "(".to_string(),
8986 end: ")".to_string(),
8987 close: true,
8988 surround: true,
8989 newline: true,
8990 },
8991 BracketPair {
8992 start: "/*".to_string(),
8993 end: " */".to_string(),
8994 close: true,
8995 surround: true,
8996 newline: true,
8997 },
8998 BracketPair {
8999 start: "[".to_string(),
9000 end: "]".to_string(),
9001 close: false,
9002 surround: false,
9003 newline: true,
9004 },
9005 BracketPair {
9006 start: "\"".to_string(),
9007 end: "\"".to_string(),
9008 close: true,
9009 surround: true,
9010 newline: false,
9011 },
9012 BracketPair {
9013 start: "<".to_string(),
9014 end: ">".to_string(),
9015 close: false,
9016 surround: true,
9017 newline: true,
9018 },
9019 ],
9020 ..Default::default()
9021 },
9022 autoclose_before: "})]".to_string(),
9023 ..Default::default()
9024 },
9025 Some(tree_sitter_rust::LANGUAGE.into()),
9026 );
9027 let language = Arc::new(language);
9028
9029 cx.language_registry().add(language.clone());
9030 cx.update_buffer(|buffer, cx| {
9031 buffer.set_language(Some(language), cx);
9032 });
9033
9034 cx.set_state(
9035 &r#"
9036 fn main() {
9037 sampleˇ
9038 }
9039 "#
9040 .unindent(),
9041 );
9042
9043 cx.update_editor(|editor, window, cx| {
9044 editor.handle_input("(", window, cx);
9045 });
9046 cx.assert_editor_state(
9047 &"
9048 fn main() {
9049 sample(ˇ)
9050 }
9051 "
9052 .unindent(),
9053 );
9054
9055 let mocked_response = lsp::SignatureHelp {
9056 signatures: vec![lsp::SignatureInformation {
9057 label: "fn sample(param1: u8, param2: u8)".to_string(),
9058 documentation: None,
9059 parameters: Some(vec![
9060 lsp::ParameterInformation {
9061 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9062 documentation: None,
9063 },
9064 lsp::ParameterInformation {
9065 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9066 documentation: None,
9067 },
9068 ]),
9069 active_parameter: None,
9070 }],
9071 active_signature: Some(0),
9072 active_parameter: Some(0),
9073 };
9074 handle_signature_help_request(&mut cx, mocked_response).await;
9075
9076 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9077 .await;
9078
9079 cx.editor(|editor, _, _| {
9080 let signature_help_state = editor.signature_help_state.popover().cloned();
9081 assert_eq!(
9082 signature_help_state.unwrap().label,
9083 "param1: u8, param2: u8"
9084 );
9085 });
9086}
9087
9088#[gpui::test]
9089async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
9090 init_test(cx, |_| {});
9091
9092 cx.update(|cx| {
9093 cx.update_global::<SettingsStore, _>(|settings, cx| {
9094 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9095 settings.auto_signature_help = Some(false);
9096 settings.show_signature_help_after_edits = Some(false);
9097 });
9098 });
9099 });
9100
9101 let mut cx = EditorLspTestContext::new_rust(
9102 lsp::ServerCapabilities {
9103 signature_help_provider: Some(lsp::SignatureHelpOptions {
9104 ..Default::default()
9105 }),
9106 ..Default::default()
9107 },
9108 cx,
9109 )
9110 .await;
9111
9112 let language = Language::new(
9113 LanguageConfig {
9114 name: "Rust".into(),
9115 brackets: BracketPairConfig {
9116 pairs: vec![
9117 BracketPair {
9118 start: "{".to_string(),
9119 end: "}".to_string(),
9120 close: true,
9121 surround: true,
9122 newline: true,
9123 },
9124 BracketPair {
9125 start: "(".to_string(),
9126 end: ")".to_string(),
9127 close: true,
9128 surround: true,
9129 newline: true,
9130 },
9131 BracketPair {
9132 start: "/*".to_string(),
9133 end: " */".to_string(),
9134 close: true,
9135 surround: true,
9136 newline: true,
9137 },
9138 BracketPair {
9139 start: "[".to_string(),
9140 end: "]".to_string(),
9141 close: false,
9142 surround: false,
9143 newline: true,
9144 },
9145 BracketPair {
9146 start: "\"".to_string(),
9147 end: "\"".to_string(),
9148 close: true,
9149 surround: true,
9150 newline: false,
9151 },
9152 BracketPair {
9153 start: "<".to_string(),
9154 end: ">".to_string(),
9155 close: false,
9156 surround: true,
9157 newline: true,
9158 },
9159 ],
9160 ..Default::default()
9161 },
9162 autoclose_before: "})]".to_string(),
9163 ..Default::default()
9164 },
9165 Some(tree_sitter_rust::LANGUAGE.into()),
9166 );
9167 let language = Arc::new(language);
9168
9169 cx.language_registry().add(language.clone());
9170 cx.update_buffer(|buffer, cx| {
9171 buffer.set_language(Some(language), cx);
9172 });
9173
9174 // Ensure that signature_help is not called when no signature help is enabled.
9175 cx.set_state(
9176 &r#"
9177 fn main() {
9178 sampleˇ
9179 }
9180 "#
9181 .unindent(),
9182 );
9183 cx.update_editor(|editor, window, cx| {
9184 editor.handle_input("(", window, cx);
9185 });
9186 cx.assert_editor_state(
9187 &"
9188 fn main() {
9189 sample(ˇ)
9190 }
9191 "
9192 .unindent(),
9193 );
9194 cx.editor(|editor, _, _| {
9195 assert!(editor.signature_help_state.task().is_none());
9196 });
9197
9198 let mocked_response = lsp::SignatureHelp {
9199 signatures: vec![lsp::SignatureInformation {
9200 label: "fn sample(param1: u8, param2: u8)".to_string(),
9201 documentation: None,
9202 parameters: Some(vec![
9203 lsp::ParameterInformation {
9204 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9205 documentation: None,
9206 },
9207 lsp::ParameterInformation {
9208 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9209 documentation: None,
9210 },
9211 ]),
9212 active_parameter: None,
9213 }],
9214 active_signature: Some(0),
9215 active_parameter: Some(0),
9216 };
9217
9218 // Ensure that signature_help is called when enabled afte edits
9219 cx.update(|_, cx| {
9220 cx.update_global::<SettingsStore, _>(|settings, cx| {
9221 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9222 settings.auto_signature_help = Some(false);
9223 settings.show_signature_help_after_edits = Some(true);
9224 });
9225 });
9226 });
9227 cx.set_state(
9228 &r#"
9229 fn main() {
9230 sampleˇ
9231 }
9232 "#
9233 .unindent(),
9234 );
9235 cx.update_editor(|editor, window, cx| {
9236 editor.handle_input("(", window, cx);
9237 });
9238 cx.assert_editor_state(
9239 &"
9240 fn main() {
9241 sample(ˇ)
9242 }
9243 "
9244 .unindent(),
9245 );
9246 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9247 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9248 .await;
9249 cx.update_editor(|editor, _, _| {
9250 let signature_help_state = editor.signature_help_state.popover().cloned();
9251 assert!(signature_help_state.is_some());
9252 assert_eq!(
9253 signature_help_state.unwrap().label,
9254 "param1: u8, param2: u8"
9255 );
9256 editor.signature_help_state = SignatureHelpState::default();
9257 });
9258
9259 // Ensure that signature_help is called when auto signature help override is enabled
9260 cx.update(|_, cx| {
9261 cx.update_global::<SettingsStore, _>(|settings, cx| {
9262 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9263 settings.auto_signature_help = Some(true);
9264 settings.show_signature_help_after_edits = Some(false);
9265 });
9266 });
9267 });
9268 cx.set_state(
9269 &r#"
9270 fn main() {
9271 sampleˇ
9272 }
9273 "#
9274 .unindent(),
9275 );
9276 cx.update_editor(|editor, window, cx| {
9277 editor.handle_input("(", window, cx);
9278 });
9279 cx.assert_editor_state(
9280 &"
9281 fn main() {
9282 sample(ˇ)
9283 }
9284 "
9285 .unindent(),
9286 );
9287 handle_signature_help_request(&mut cx, mocked_response).await;
9288 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9289 .await;
9290 cx.editor(|editor, _, _| {
9291 let signature_help_state = editor.signature_help_state.popover().cloned();
9292 assert!(signature_help_state.is_some());
9293 assert_eq!(
9294 signature_help_state.unwrap().label,
9295 "param1: u8, param2: u8"
9296 );
9297 });
9298}
9299
9300#[gpui::test]
9301async fn test_signature_help(cx: &mut TestAppContext) {
9302 init_test(cx, |_| {});
9303 cx.update(|cx| {
9304 cx.update_global::<SettingsStore, _>(|settings, cx| {
9305 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9306 settings.auto_signature_help = Some(true);
9307 });
9308 });
9309 });
9310
9311 let mut cx = EditorLspTestContext::new_rust(
9312 lsp::ServerCapabilities {
9313 signature_help_provider: Some(lsp::SignatureHelpOptions {
9314 ..Default::default()
9315 }),
9316 ..Default::default()
9317 },
9318 cx,
9319 )
9320 .await;
9321
9322 // A test that directly calls `show_signature_help`
9323 cx.update_editor(|editor, window, cx| {
9324 editor.show_signature_help(&ShowSignatureHelp, window, cx);
9325 });
9326
9327 let mocked_response = lsp::SignatureHelp {
9328 signatures: vec![lsp::SignatureInformation {
9329 label: "fn sample(param1: u8, param2: u8)".to_string(),
9330 documentation: None,
9331 parameters: Some(vec![
9332 lsp::ParameterInformation {
9333 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9334 documentation: None,
9335 },
9336 lsp::ParameterInformation {
9337 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9338 documentation: None,
9339 },
9340 ]),
9341 active_parameter: None,
9342 }],
9343 active_signature: Some(0),
9344 active_parameter: Some(0),
9345 };
9346 handle_signature_help_request(&mut cx, mocked_response).await;
9347
9348 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9349 .await;
9350
9351 cx.editor(|editor, _, _| {
9352 let signature_help_state = editor.signature_help_state.popover().cloned();
9353 assert!(signature_help_state.is_some());
9354 assert_eq!(
9355 signature_help_state.unwrap().label,
9356 "param1: u8, param2: u8"
9357 );
9358 });
9359
9360 // When exiting outside from inside the brackets, `signature_help` is closed.
9361 cx.set_state(indoc! {"
9362 fn main() {
9363 sample(ˇ);
9364 }
9365
9366 fn sample(param1: u8, param2: u8) {}
9367 "});
9368
9369 cx.update_editor(|editor, window, cx| {
9370 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
9371 });
9372
9373 let mocked_response = lsp::SignatureHelp {
9374 signatures: Vec::new(),
9375 active_signature: None,
9376 active_parameter: None,
9377 };
9378 handle_signature_help_request(&mut cx, mocked_response).await;
9379
9380 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
9381 .await;
9382
9383 cx.editor(|editor, _, _| {
9384 assert!(!editor.signature_help_state.is_shown());
9385 });
9386
9387 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
9388 cx.set_state(indoc! {"
9389 fn main() {
9390 sample(ˇ);
9391 }
9392
9393 fn sample(param1: u8, param2: u8) {}
9394 "});
9395
9396 let mocked_response = lsp::SignatureHelp {
9397 signatures: vec![lsp::SignatureInformation {
9398 label: "fn sample(param1: u8, param2: u8)".to_string(),
9399 documentation: None,
9400 parameters: Some(vec![
9401 lsp::ParameterInformation {
9402 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9403 documentation: None,
9404 },
9405 lsp::ParameterInformation {
9406 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9407 documentation: None,
9408 },
9409 ]),
9410 active_parameter: None,
9411 }],
9412 active_signature: Some(0),
9413 active_parameter: Some(0),
9414 };
9415 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9416 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9417 .await;
9418 cx.editor(|editor, _, _| {
9419 assert!(editor.signature_help_state.is_shown());
9420 });
9421
9422 // Restore the popover with more parameter input
9423 cx.set_state(indoc! {"
9424 fn main() {
9425 sample(param1, param2ˇ);
9426 }
9427
9428 fn sample(param1: u8, param2: u8) {}
9429 "});
9430
9431 let mocked_response = lsp::SignatureHelp {
9432 signatures: vec![lsp::SignatureInformation {
9433 label: "fn sample(param1: u8, param2: u8)".to_string(),
9434 documentation: None,
9435 parameters: Some(vec![
9436 lsp::ParameterInformation {
9437 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9438 documentation: None,
9439 },
9440 lsp::ParameterInformation {
9441 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9442 documentation: None,
9443 },
9444 ]),
9445 active_parameter: None,
9446 }],
9447 active_signature: Some(0),
9448 active_parameter: Some(1),
9449 };
9450 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9451 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9452 .await;
9453
9454 // When selecting a range, the popover is gone.
9455 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
9456 cx.update_editor(|editor, window, cx| {
9457 editor.change_selections(None, window, cx, |s| {
9458 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
9459 })
9460 });
9461 cx.assert_editor_state(indoc! {"
9462 fn main() {
9463 sample(param1, «ˇparam2»);
9464 }
9465
9466 fn sample(param1: u8, param2: u8) {}
9467 "});
9468 cx.editor(|editor, _, _| {
9469 assert!(!editor.signature_help_state.is_shown());
9470 });
9471
9472 // When unselecting again, the popover is back if within the brackets.
9473 cx.update_editor(|editor, window, cx| {
9474 editor.change_selections(None, window, cx, |s| {
9475 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9476 })
9477 });
9478 cx.assert_editor_state(indoc! {"
9479 fn main() {
9480 sample(param1, ˇparam2);
9481 }
9482
9483 fn sample(param1: u8, param2: u8) {}
9484 "});
9485 handle_signature_help_request(&mut cx, mocked_response).await;
9486 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9487 .await;
9488 cx.editor(|editor, _, _| {
9489 assert!(editor.signature_help_state.is_shown());
9490 });
9491
9492 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
9493 cx.update_editor(|editor, window, cx| {
9494 editor.change_selections(None, window, cx, |s| {
9495 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
9496 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9497 })
9498 });
9499 cx.assert_editor_state(indoc! {"
9500 fn main() {
9501 sample(param1, ˇparam2);
9502 }
9503
9504 fn sample(param1: u8, param2: u8) {}
9505 "});
9506
9507 let mocked_response = lsp::SignatureHelp {
9508 signatures: vec![lsp::SignatureInformation {
9509 label: "fn sample(param1: u8, param2: u8)".to_string(),
9510 documentation: None,
9511 parameters: Some(vec![
9512 lsp::ParameterInformation {
9513 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9514 documentation: None,
9515 },
9516 lsp::ParameterInformation {
9517 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9518 documentation: None,
9519 },
9520 ]),
9521 active_parameter: None,
9522 }],
9523 active_signature: Some(0),
9524 active_parameter: Some(1),
9525 };
9526 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9527 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9528 .await;
9529 cx.update_editor(|editor, _, cx| {
9530 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
9531 });
9532 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
9533 .await;
9534 cx.update_editor(|editor, window, cx| {
9535 editor.change_selections(None, window, cx, |s| {
9536 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
9537 })
9538 });
9539 cx.assert_editor_state(indoc! {"
9540 fn main() {
9541 sample(param1, «ˇparam2»);
9542 }
9543
9544 fn sample(param1: u8, param2: u8) {}
9545 "});
9546 cx.update_editor(|editor, window, cx| {
9547 editor.change_selections(None, window, cx, |s| {
9548 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9549 })
9550 });
9551 cx.assert_editor_state(indoc! {"
9552 fn main() {
9553 sample(param1, ˇparam2);
9554 }
9555
9556 fn sample(param1: u8, param2: u8) {}
9557 "});
9558 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
9559 .await;
9560}
9561
9562#[gpui::test]
9563async fn test_completion_mode(cx: &mut TestAppContext) {
9564 init_test(cx, |_| {});
9565 let mut cx = EditorLspTestContext::new_rust(
9566 lsp::ServerCapabilities {
9567 completion_provider: Some(lsp::CompletionOptions {
9568 resolve_provider: Some(true),
9569 ..Default::default()
9570 }),
9571 ..Default::default()
9572 },
9573 cx,
9574 )
9575 .await;
9576
9577 struct Run {
9578 run_description: &'static str,
9579 initial_state: String,
9580 buffer_marked_text: String,
9581 completion_text: &'static str,
9582 expected_with_insert_mode: String,
9583 expected_with_replace_mode: String,
9584 expected_with_replace_subsequence_mode: String,
9585 expected_with_replace_suffix_mode: String,
9586 }
9587
9588 let runs = [
9589 Run {
9590 run_description: "Start of word matches completion text",
9591 initial_state: "before ediˇ after".into(),
9592 buffer_marked_text: "before <edi|> after".into(),
9593 completion_text: "editor",
9594 expected_with_insert_mode: "before editorˇ after".into(),
9595 expected_with_replace_mode: "before editorˇ after".into(),
9596 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
9597 expected_with_replace_suffix_mode: "before editorˇ after".into(),
9598 },
9599 Run {
9600 run_description: "Accept same text at the middle of the word",
9601 initial_state: "before ediˇtor after".into(),
9602 buffer_marked_text: "before <edi|tor> after".into(),
9603 completion_text: "editor",
9604 expected_with_insert_mode: "before editorˇtor after".into(),
9605 expected_with_replace_mode: "before ediˇtor after".into(),
9606 expected_with_replace_subsequence_mode: "before ediˇtor after".into(),
9607 expected_with_replace_suffix_mode: "before ediˇtor after".into(),
9608 },
9609 Run {
9610 run_description: "End of word matches completion text -- cursor at end",
9611 initial_state: "before torˇ after".into(),
9612 buffer_marked_text: "before <tor|> after".into(),
9613 completion_text: "editor",
9614 expected_with_insert_mode: "before editorˇ after".into(),
9615 expected_with_replace_mode: "before editorˇ after".into(),
9616 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
9617 expected_with_replace_suffix_mode: "before editorˇ after".into(),
9618 },
9619 Run {
9620 run_description: "End of word matches completion text -- cursor at start",
9621 initial_state: "before ˇtor after".into(),
9622 buffer_marked_text: "before <|tor> after".into(),
9623 completion_text: "editor",
9624 expected_with_insert_mode: "before editorˇtor after".into(),
9625 expected_with_replace_mode: "before editorˇ after".into(),
9626 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
9627 expected_with_replace_suffix_mode: "before editorˇ after".into(),
9628 },
9629 Run {
9630 run_description: "Prepend text containing whitespace",
9631 initial_state: "pˇfield: bool".into(),
9632 buffer_marked_text: "<p|field>: bool".into(),
9633 completion_text: "pub ",
9634 expected_with_insert_mode: "pub ˇfield: bool".into(),
9635 expected_with_replace_mode: "pub ˇ: bool".into(),
9636 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
9637 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
9638 },
9639 Run {
9640 run_description: "Add element to start of list",
9641 initial_state: "[element_ˇelement_2]".into(),
9642 buffer_marked_text: "[<element_|element_2>]".into(),
9643 completion_text: "element_1",
9644 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
9645 expected_with_replace_mode: "[element_1ˇ]".into(),
9646 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
9647 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
9648 },
9649 Run {
9650 run_description: "Add element to start of list -- first and second elements are equal",
9651 initial_state: "[elˇelement]".into(),
9652 buffer_marked_text: "[<el|element>]".into(),
9653 completion_text: "element",
9654 expected_with_insert_mode: "[elementˇelement]".into(),
9655 expected_with_replace_mode: "[elˇement]".into(),
9656 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
9657 expected_with_replace_suffix_mode: "[elˇement]".into(),
9658 },
9659 Run {
9660 run_description: "Ends with matching suffix",
9661 initial_state: "SubˇError".into(),
9662 buffer_marked_text: "<Sub|Error>".into(),
9663 completion_text: "SubscriptionError",
9664 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
9665 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
9666 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
9667 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
9668 },
9669 Run {
9670 run_description: "Suffix is a subsequence -- contiguous",
9671 initial_state: "SubˇErr".into(),
9672 buffer_marked_text: "<Sub|Err>".into(),
9673 completion_text: "SubscriptionError",
9674 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
9675 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
9676 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
9677 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
9678 },
9679 Run {
9680 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
9681 initial_state: "Suˇscrirr".into(),
9682 buffer_marked_text: "<Su|scrirr>".into(),
9683 completion_text: "SubscriptionError",
9684 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
9685 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
9686 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
9687 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
9688 },
9689 Run {
9690 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
9691 initial_state: "foo(indˇix)".into(),
9692 buffer_marked_text: "foo(<ind|ix>)".into(),
9693 completion_text: "node_index",
9694 expected_with_insert_mode: "foo(node_indexˇix)".into(),
9695 expected_with_replace_mode: "foo(node_indexˇ)".into(),
9696 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
9697 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
9698 },
9699 ];
9700
9701 for run in runs {
9702 let run_variations = [
9703 (LspInsertMode::Insert, run.expected_with_insert_mode),
9704 (LspInsertMode::Replace, run.expected_with_replace_mode),
9705 (
9706 LspInsertMode::ReplaceSubsequence,
9707 run.expected_with_replace_subsequence_mode,
9708 ),
9709 (
9710 LspInsertMode::ReplaceSuffix,
9711 run.expected_with_replace_suffix_mode,
9712 ),
9713 ];
9714
9715 for (lsp_insert_mode, expected_text) in run_variations {
9716 eprintln!(
9717 "run = {:?}, mode = {lsp_insert_mode:.?}",
9718 run.run_description,
9719 );
9720
9721 update_test_language_settings(&mut cx, |settings| {
9722 settings.defaults.completions = Some(CompletionSettings {
9723 lsp_insert_mode,
9724 words: WordsCompletionMode::Disabled,
9725 lsp: true,
9726 lsp_fetch_timeout_ms: 0,
9727 });
9728 });
9729
9730 cx.set_state(&run.initial_state);
9731 cx.update_editor(|editor, window, cx| {
9732 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
9733 });
9734
9735 let counter = Arc::new(AtomicUsize::new(0));
9736 handle_completion_request_with_insert_and_replace(
9737 &mut cx,
9738 &run.buffer_marked_text,
9739 vec![run.completion_text],
9740 counter.clone(),
9741 )
9742 .await;
9743 cx.condition(|editor, _| editor.context_menu_visible())
9744 .await;
9745 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
9746
9747 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9748 editor
9749 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9750 .unwrap()
9751 });
9752 cx.assert_editor_state(&expected_text);
9753 handle_resolve_completion_request(&mut cx, None).await;
9754 apply_additional_edits.await.unwrap();
9755 }
9756 }
9757}
9758
9759#[gpui::test]
9760async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
9761 init_test(cx, |_| {});
9762 let mut cx = EditorLspTestContext::new_rust(
9763 lsp::ServerCapabilities {
9764 completion_provider: Some(lsp::CompletionOptions {
9765 resolve_provider: Some(true),
9766 ..Default::default()
9767 }),
9768 ..Default::default()
9769 },
9770 cx,
9771 )
9772 .await;
9773
9774 let initial_state = "SubˇError";
9775 let buffer_marked_text = "<Sub|Error>";
9776 let completion_text = "SubscriptionError";
9777 let expected_with_insert_mode = "SubscriptionErrorˇError";
9778 let expected_with_replace_mode = "SubscriptionErrorˇ";
9779
9780 update_test_language_settings(&mut cx, |settings| {
9781 settings.defaults.completions = Some(CompletionSettings {
9782 words: WordsCompletionMode::Disabled,
9783 // set the opposite here to ensure that the action is overriding the default behavior
9784 lsp_insert_mode: LspInsertMode::Insert,
9785 lsp: true,
9786 lsp_fetch_timeout_ms: 0,
9787 });
9788 });
9789
9790 cx.set_state(initial_state);
9791 cx.update_editor(|editor, window, cx| {
9792 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
9793 });
9794
9795 let counter = Arc::new(AtomicUsize::new(0));
9796 handle_completion_request_with_insert_and_replace(
9797 &mut cx,
9798 &buffer_marked_text,
9799 vec![completion_text],
9800 counter.clone(),
9801 )
9802 .await;
9803 cx.condition(|editor, _| editor.context_menu_visible())
9804 .await;
9805 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
9806
9807 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9808 editor
9809 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
9810 .unwrap()
9811 });
9812 cx.assert_editor_state(&expected_with_replace_mode);
9813 handle_resolve_completion_request(&mut cx, None).await;
9814 apply_additional_edits.await.unwrap();
9815
9816 update_test_language_settings(&mut cx, |settings| {
9817 settings.defaults.completions = Some(CompletionSettings {
9818 words: WordsCompletionMode::Disabled,
9819 // set the opposite here to ensure that the action is overriding the default behavior
9820 lsp_insert_mode: LspInsertMode::Replace,
9821 lsp: true,
9822 lsp_fetch_timeout_ms: 0,
9823 });
9824 });
9825
9826 cx.set_state(initial_state);
9827 cx.update_editor(|editor, window, cx| {
9828 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
9829 });
9830 handle_completion_request_with_insert_and_replace(
9831 &mut cx,
9832 &buffer_marked_text,
9833 vec![completion_text],
9834 counter.clone(),
9835 )
9836 .await;
9837 cx.condition(|editor, _| editor.context_menu_visible())
9838 .await;
9839 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
9840
9841 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9842 editor
9843 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
9844 .unwrap()
9845 });
9846 cx.assert_editor_state(&expected_with_insert_mode);
9847 handle_resolve_completion_request(&mut cx, None).await;
9848 apply_additional_edits.await.unwrap();
9849}
9850
9851#[gpui::test]
9852async fn test_completion(cx: &mut TestAppContext) {
9853 init_test(cx, |_| {});
9854
9855 let mut cx = EditorLspTestContext::new_rust(
9856 lsp::ServerCapabilities {
9857 completion_provider: Some(lsp::CompletionOptions {
9858 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9859 resolve_provider: Some(true),
9860 ..Default::default()
9861 }),
9862 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9863 ..Default::default()
9864 },
9865 cx,
9866 )
9867 .await;
9868 let counter = Arc::new(AtomicUsize::new(0));
9869
9870 cx.set_state(indoc! {"
9871 oneˇ
9872 two
9873 three
9874 "});
9875 cx.simulate_keystroke(".");
9876 handle_completion_request(
9877 &mut cx,
9878 indoc! {"
9879 one.|<>
9880 two
9881 three
9882 "},
9883 vec!["first_completion", "second_completion"],
9884 counter.clone(),
9885 )
9886 .await;
9887 cx.condition(|editor, _| editor.context_menu_visible())
9888 .await;
9889 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
9890
9891 let _handler = handle_signature_help_request(
9892 &mut cx,
9893 lsp::SignatureHelp {
9894 signatures: vec![lsp::SignatureInformation {
9895 label: "test signature".to_string(),
9896 documentation: None,
9897 parameters: Some(vec![lsp::ParameterInformation {
9898 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
9899 documentation: None,
9900 }]),
9901 active_parameter: None,
9902 }],
9903 active_signature: None,
9904 active_parameter: None,
9905 },
9906 );
9907 cx.update_editor(|editor, window, cx| {
9908 assert!(
9909 !editor.signature_help_state.is_shown(),
9910 "No signature help was called for"
9911 );
9912 editor.show_signature_help(&ShowSignatureHelp, window, cx);
9913 });
9914 cx.run_until_parked();
9915 cx.update_editor(|editor, _, _| {
9916 assert!(
9917 !editor.signature_help_state.is_shown(),
9918 "No signature help should be shown when completions menu is open"
9919 );
9920 });
9921
9922 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9923 editor.context_menu_next(&Default::default(), window, cx);
9924 editor
9925 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9926 .unwrap()
9927 });
9928 cx.assert_editor_state(indoc! {"
9929 one.second_completionˇ
9930 two
9931 three
9932 "});
9933
9934 handle_resolve_completion_request(
9935 &mut cx,
9936 Some(vec![
9937 (
9938 //This overlaps with the primary completion edit which is
9939 //misbehavior from the LSP spec, test that we filter it out
9940 indoc! {"
9941 one.second_ˇcompletion
9942 two
9943 threeˇ
9944 "},
9945 "overlapping additional edit",
9946 ),
9947 (
9948 indoc! {"
9949 one.second_completion
9950 two
9951 threeˇ
9952 "},
9953 "\nadditional edit",
9954 ),
9955 ]),
9956 )
9957 .await;
9958 apply_additional_edits.await.unwrap();
9959 cx.assert_editor_state(indoc! {"
9960 one.second_completionˇ
9961 two
9962 three
9963 additional edit
9964 "});
9965
9966 cx.set_state(indoc! {"
9967 one.second_completion
9968 twoˇ
9969 threeˇ
9970 additional edit
9971 "});
9972 cx.simulate_keystroke(" ");
9973 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9974 cx.simulate_keystroke("s");
9975 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9976
9977 cx.assert_editor_state(indoc! {"
9978 one.second_completion
9979 two sˇ
9980 three sˇ
9981 additional edit
9982 "});
9983 handle_completion_request(
9984 &mut cx,
9985 indoc! {"
9986 one.second_completion
9987 two s
9988 three <s|>
9989 additional edit
9990 "},
9991 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
9992 counter.clone(),
9993 )
9994 .await;
9995 cx.condition(|editor, _| editor.context_menu_visible())
9996 .await;
9997 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
9998
9999 cx.simulate_keystroke("i");
10000
10001 handle_completion_request(
10002 &mut cx,
10003 indoc! {"
10004 one.second_completion
10005 two si
10006 three <si|>
10007 additional edit
10008 "},
10009 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
10010 counter.clone(),
10011 )
10012 .await;
10013 cx.condition(|editor, _| editor.context_menu_visible())
10014 .await;
10015 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
10016
10017 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10018 editor
10019 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10020 .unwrap()
10021 });
10022 cx.assert_editor_state(indoc! {"
10023 one.second_completion
10024 two sixth_completionˇ
10025 three sixth_completionˇ
10026 additional edit
10027 "});
10028
10029 apply_additional_edits.await.unwrap();
10030
10031 update_test_language_settings(&mut cx, |settings| {
10032 settings.defaults.show_completions_on_input = Some(false);
10033 });
10034 cx.set_state("editorˇ");
10035 cx.simulate_keystroke(".");
10036 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
10037 cx.simulate_keystrokes("c l o");
10038 cx.assert_editor_state("editor.cloˇ");
10039 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
10040 cx.update_editor(|editor, window, cx| {
10041 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10042 });
10043 handle_completion_request(
10044 &mut cx,
10045 "editor.<clo|>",
10046 vec!["close", "clobber"],
10047 counter.clone(),
10048 )
10049 .await;
10050 cx.condition(|editor, _| editor.context_menu_visible())
10051 .await;
10052 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
10053
10054 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10055 editor
10056 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10057 .unwrap()
10058 });
10059 cx.assert_editor_state("editor.closeˇ");
10060 handle_resolve_completion_request(&mut cx, None).await;
10061 apply_additional_edits.await.unwrap();
10062}
10063
10064#[gpui::test]
10065async fn test_word_completion(cx: &mut TestAppContext) {
10066 let lsp_fetch_timeout_ms = 10;
10067 init_test(cx, |language_settings| {
10068 language_settings.defaults.completions = Some(CompletionSettings {
10069 words: WordsCompletionMode::Fallback,
10070 lsp: true,
10071 lsp_fetch_timeout_ms: 10,
10072 lsp_insert_mode: LspInsertMode::Insert,
10073 });
10074 });
10075
10076 let mut cx = EditorLspTestContext::new_rust(
10077 lsp::ServerCapabilities {
10078 completion_provider: Some(lsp::CompletionOptions {
10079 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10080 ..lsp::CompletionOptions::default()
10081 }),
10082 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10083 ..lsp::ServerCapabilities::default()
10084 },
10085 cx,
10086 )
10087 .await;
10088
10089 let throttle_completions = Arc::new(AtomicBool::new(false));
10090
10091 let lsp_throttle_completions = throttle_completions.clone();
10092 let _completion_requests_handler =
10093 cx.lsp
10094 .server
10095 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
10096 let lsp_throttle_completions = lsp_throttle_completions.clone();
10097 let cx = cx.clone();
10098 async move {
10099 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
10100 cx.background_executor()
10101 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
10102 .await;
10103 }
10104 Ok(Some(lsp::CompletionResponse::Array(vec![
10105 lsp::CompletionItem {
10106 label: "first".into(),
10107 ..lsp::CompletionItem::default()
10108 },
10109 lsp::CompletionItem {
10110 label: "last".into(),
10111 ..lsp::CompletionItem::default()
10112 },
10113 ])))
10114 }
10115 });
10116
10117 cx.set_state(indoc! {"
10118 oneˇ
10119 two
10120 three
10121 "});
10122 cx.simulate_keystroke(".");
10123 cx.executor().run_until_parked();
10124 cx.condition(|editor, _| editor.context_menu_visible())
10125 .await;
10126 cx.update_editor(|editor, window, cx| {
10127 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10128 {
10129 assert_eq!(
10130 completion_menu_entries(&menu),
10131 &["first", "last"],
10132 "When LSP server is fast to reply, no fallback word completions are used"
10133 );
10134 } else {
10135 panic!("expected completion menu to be open");
10136 }
10137 editor.cancel(&Cancel, window, cx);
10138 });
10139 cx.executor().run_until_parked();
10140 cx.condition(|editor, _| !editor.context_menu_visible())
10141 .await;
10142
10143 throttle_completions.store(true, atomic::Ordering::Release);
10144 cx.simulate_keystroke(".");
10145 cx.executor()
10146 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
10147 cx.executor().run_until_parked();
10148 cx.condition(|editor, _| editor.context_menu_visible())
10149 .await;
10150 cx.update_editor(|editor, _, _| {
10151 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10152 {
10153 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
10154 "When LSP server is slow, document words can be shown instead, if configured accordingly");
10155 } else {
10156 panic!("expected completion menu to be open");
10157 }
10158 });
10159}
10160
10161#[gpui::test]
10162async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
10163 init_test(cx, |language_settings| {
10164 language_settings.defaults.completions = Some(CompletionSettings {
10165 words: WordsCompletionMode::Enabled,
10166 lsp: true,
10167 lsp_fetch_timeout_ms: 0,
10168 lsp_insert_mode: LspInsertMode::Insert,
10169 });
10170 });
10171
10172 let mut cx = EditorLspTestContext::new_rust(
10173 lsp::ServerCapabilities {
10174 completion_provider: Some(lsp::CompletionOptions {
10175 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10176 ..lsp::CompletionOptions::default()
10177 }),
10178 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10179 ..lsp::ServerCapabilities::default()
10180 },
10181 cx,
10182 )
10183 .await;
10184
10185 let _completion_requests_handler =
10186 cx.lsp
10187 .server
10188 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
10189 Ok(Some(lsp::CompletionResponse::Array(vec![
10190 lsp::CompletionItem {
10191 label: "first".into(),
10192 ..lsp::CompletionItem::default()
10193 },
10194 lsp::CompletionItem {
10195 label: "last".into(),
10196 ..lsp::CompletionItem::default()
10197 },
10198 ])))
10199 });
10200
10201 cx.set_state(indoc! {"ˇ
10202 first
10203 last
10204 second
10205 "});
10206 cx.simulate_keystroke(".");
10207 cx.executor().run_until_parked();
10208 cx.condition(|editor, _| editor.context_menu_visible())
10209 .await;
10210 cx.update_editor(|editor, _, _| {
10211 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10212 {
10213 assert_eq!(
10214 completion_menu_entries(&menu),
10215 &["first", "last", "second"],
10216 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
10217 );
10218 } else {
10219 panic!("expected completion menu to be open");
10220 }
10221 });
10222}
10223
10224#[gpui::test]
10225async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
10226 init_test(cx, |language_settings| {
10227 language_settings.defaults.completions = Some(CompletionSettings {
10228 words: WordsCompletionMode::Disabled,
10229 lsp: true,
10230 lsp_fetch_timeout_ms: 0,
10231 lsp_insert_mode: LspInsertMode::Insert,
10232 });
10233 });
10234
10235 let mut cx = EditorLspTestContext::new_rust(
10236 lsp::ServerCapabilities {
10237 completion_provider: Some(lsp::CompletionOptions {
10238 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10239 ..lsp::CompletionOptions::default()
10240 }),
10241 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10242 ..lsp::ServerCapabilities::default()
10243 },
10244 cx,
10245 )
10246 .await;
10247
10248 let _completion_requests_handler =
10249 cx.lsp
10250 .server
10251 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
10252 panic!("LSP completions should not be queried when dealing with word completions")
10253 });
10254
10255 cx.set_state(indoc! {"ˇ
10256 first
10257 last
10258 second
10259 "});
10260 cx.update_editor(|editor, window, cx| {
10261 editor.show_word_completions(&ShowWordCompletions, window, cx);
10262 });
10263 cx.executor().run_until_parked();
10264 cx.condition(|editor, _| editor.context_menu_visible())
10265 .await;
10266 cx.update_editor(|editor, _, _| {
10267 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10268 {
10269 assert_eq!(
10270 completion_menu_entries(&menu),
10271 &["first", "last", "second"],
10272 "`ShowWordCompletions` action should show word completions"
10273 );
10274 } else {
10275 panic!("expected completion menu to be open");
10276 }
10277 });
10278
10279 cx.simulate_keystroke("l");
10280 cx.executor().run_until_parked();
10281 cx.condition(|editor, _| editor.context_menu_visible())
10282 .await;
10283 cx.update_editor(|editor, _, _| {
10284 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10285 {
10286 assert_eq!(
10287 completion_menu_entries(&menu),
10288 &["last"],
10289 "After showing word completions, further editing should filter them and not query the LSP"
10290 );
10291 } else {
10292 panic!("expected completion menu to be open");
10293 }
10294 });
10295}
10296
10297#[gpui::test]
10298async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
10299 init_test(cx, |language_settings| {
10300 language_settings.defaults.completions = Some(CompletionSettings {
10301 words: WordsCompletionMode::Fallback,
10302 lsp: false,
10303 lsp_fetch_timeout_ms: 0,
10304 lsp_insert_mode: LspInsertMode::Insert,
10305 });
10306 });
10307
10308 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
10309
10310 cx.set_state(indoc! {"ˇ
10311 0_usize
10312 let
10313 33
10314 4.5f32
10315 "});
10316 cx.update_editor(|editor, window, cx| {
10317 editor.show_completions(&ShowCompletions::default(), window, cx);
10318 });
10319 cx.executor().run_until_parked();
10320 cx.condition(|editor, _| editor.context_menu_visible())
10321 .await;
10322 cx.update_editor(|editor, window, cx| {
10323 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10324 {
10325 assert_eq!(
10326 completion_menu_entries(&menu),
10327 &["let"],
10328 "With no digits in the completion query, no digits should be in the word completions"
10329 );
10330 } else {
10331 panic!("expected completion menu to be open");
10332 }
10333 editor.cancel(&Cancel, window, cx);
10334 });
10335
10336 cx.set_state(indoc! {"3ˇ
10337 0_usize
10338 let
10339 3
10340 33.35f32
10341 "});
10342 cx.update_editor(|editor, window, cx| {
10343 editor.show_completions(&ShowCompletions::default(), window, cx);
10344 });
10345 cx.executor().run_until_parked();
10346 cx.condition(|editor, _| editor.context_menu_visible())
10347 .await;
10348 cx.update_editor(|editor, _, _| {
10349 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10350 {
10351 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
10352 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
10353 } else {
10354 panic!("expected completion menu to be open");
10355 }
10356 });
10357}
10358
10359fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
10360 let position = || lsp::Position {
10361 line: params.text_document_position.position.line,
10362 character: params.text_document_position.position.character,
10363 };
10364 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
10365 range: lsp::Range {
10366 start: position(),
10367 end: position(),
10368 },
10369 new_text: text.to_string(),
10370 }))
10371}
10372
10373#[gpui::test]
10374async fn test_multiline_completion(cx: &mut TestAppContext) {
10375 init_test(cx, |_| {});
10376
10377 let fs = FakeFs::new(cx.executor());
10378 fs.insert_tree(
10379 path!("/a"),
10380 json!({
10381 "main.ts": "a",
10382 }),
10383 )
10384 .await;
10385
10386 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
10387 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10388 let typescript_language = Arc::new(Language::new(
10389 LanguageConfig {
10390 name: "TypeScript".into(),
10391 matcher: LanguageMatcher {
10392 path_suffixes: vec!["ts".to_string()],
10393 ..LanguageMatcher::default()
10394 },
10395 line_comments: vec!["// ".into()],
10396 ..LanguageConfig::default()
10397 },
10398 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
10399 ));
10400 language_registry.add(typescript_language.clone());
10401 let mut fake_servers = language_registry.register_fake_lsp(
10402 "TypeScript",
10403 FakeLspAdapter {
10404 capabilities: lsp::ServerCapabilities {
10405 completion_provider: Some(lsp::CompletionOptions {
10406 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10407 ..lsp::CompletionOptions::default()
10408 }),
10409 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10410 ..lsp::ServerCapabilities::default()
10411 },
10412 // Emulate vtsls label generation
10413 label_for_completion: Some(Box::new(|item, _| {
10414 let text = if let Some(description) = item
10415 .label_details
10416 .as_ref()
10417 .and_then(|label_details| label_details.description.as_ref())
10418 {
10419 format!("{} {}", item.label, description)
10420 } else if let Some(detail) = &item.detail {
10421 format!("{} {}", item.label, detail)
10422 } else {
10423 item.label.clone()
10424 };
10425 let len = text.len();
10426 Some(language::CodeLabel {
10427 text,
10428 runs: Vec::new(),
10429 filter_range: 0..len,
10430 })
10431 })),
10432 ..FakeLspAdapter::default()
10433 },
10434 );
10435 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10436 let cx = &mut VisualTestContext::from_window(*workspace, cx);
10437 let worktree_id = workspace
10438 .update(cx, |workspace, _window, cx| {
10439 workspace.project().update(cx, |project, cx| {
10440 project.worktrees(cx).next().unwrap().read(cx).id()
10441 })
10442 })
10443 .unwrap();
10444 let _buffer = project
10445 .update(cx, |project, cx| {
10446 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
10447 })
10448 .await
10449 .unwrap();
10450 let editor = workspace
10451 .update(cx, |workspace, window, cx| {
10452 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
10453 })
10454 .unwrap()
10455 .await
10456 .unwrap()
10457 .downcast::<Editor>()
10458 .unwrap();
10459 let fake_server = fake_servers.next().await.unwrap();
10460
10461 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
10462 let multiline_label_2 = "a\nb\nc\n";
10463 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
10464 let multiline_description = "d\ne\nf\n";
10465 let multiline_detail_2 = "g\nh\ni\n";
10466
10467 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
10468 move |params, _| async move {
10469 Ok(Some(lsp::CompletionResponse::Array(vec![
10470 lsp::CompletionItem {
10471 label: multiline_label.to_string(),
10472 text_edit: gen_text_edit(¶ms, "new_text_1"),
10473 ..lsp::CompletionItem::default()
10474 },
10475 lsp::CompletionItem {
10476 label: "single line label 1".to_string(),
10477 detail: Some(multiline_detail.to_string()),
10478 text_edit: gen_text_edit(¶ms, "new_text_2"),
10479 ..lsp::CompletionItem::default()
10480 },
10481 lsp::CompletionItem {
10482 label: "single line label 2".to_string(),
10483 label_details: Some(lsp::CompletionItemLabelDetails {
10484 description: Some(multiline_description.to_string()),
10485 detail: None,
10486 }),
10487 text_edit: gen_text_edit(¶ms, "new_text_2"),
10488 ..lsp::CompletionItem::default()
10489 },
10490 lsp::CompletionItem {
10491 label: multiline_label_2.to_string(),
10492 detail: Some(multiline_detail_2.to_string()),
10493 text_edit: gen_text_edit(¶ms, "new_text_3"),
10494 ..lsp::CompletionItem::default()
10495 },
10496 lsp::CompletionItem {
10497 label: "Label with many spaces and \t but without newlines".to_string(),
10498 detail: Some(
10499 "Details with many spaces and \t but without newlines".to_string(),
10500 ),
10501 text_edit: gen_text_edit(¶ms, "new_text_4"),
10502 ..lsp::CompletionItem::default()
10503 },
10504 ])))
10505 },
10506 );
10507
10508 editor.update_in(cx, |editor, window, cx| {
10509 cx.focus_self(window);
10510 editor.move_to_end(&MoveToEnd, window, cx);
10511 editor.handle_input(".", window, cx);
10512 });
10513 cx.run_until_parked();
10514 completion_handle.next().await.unwrap();
10515
10516 editor.update(cx, |editor, _| {
10517 assert!(editor.context_menu_visible());
10518 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10519 {
10520 let completion_labels = menu
10521 .completions
10522 .borrow()
10523 .iter()
10524 .map(|c| c.label.text.clone())
10525 .collect::<Vec<_>>();
10526 assert_eq!(
10527 completion_labels,
10528 &[
10529 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
10530 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
10531 "single line label 2 d e f ",
10532 "a b c g h i ",
10533 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
10534 ],
10535 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
10536 );
10537
10538 for completion in menu
10539 .completions
10540 .borrow()
10541 .iter() {
10542 assert_eq!(
10543 completion.label.filter_range,
10544 0..completion.label.text.len(),
10545 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
10546 );
10547 }
10548 } else {
10549 panic!("expected completion menu to be open");
10550 }
10551 });
10552}
10553
10554#[gpui::test]
10555async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
10556 init_test(cx, |_| {});
10557 let mut cx = EditorLspTestContext::new_rust(
10558 lsp::ServerCapabilities {
10559 completion_provider: Some(lsp::CompletionOptions {
10560 trigger_characters: Some(vec![".".to_string()]),
10561 ..Default::default()
10562 }),
10563 ..Default::default()
10564 },
10565 cx,
10566 )
10567 .await;
10568 cx.lsp
10569 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
10570 Ok(Some(lsp::CompletionResponse::Array(vec![
10571 lsp::CompletionItem {
10572 label: "first".into(),
10573 ..Default::default()
10574 },
10575 lsp::CompletionItem {
10576 label: "last".into(),
10577 ..Default::default()
10578 },
10579 ])))
10580 });
10581 cx.set_state("variableˇ");
10582 cx.simulate_keystroke(".");
10583 cx.executor().run_until_parked();
10584
10585 cx.update_editor(|editor, _, _| {
10586 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10587 {
10588 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
10589 } else {
10590 panic!("expected completion menu to be open");
10591 }
10592 });
10593
10594 cx.update_editor(|editor, window, cx| {
10595 editor.move_page_down(&MovePageDown::default(), window, cx);
10596 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10597 {
10598 assert!(
10599 menu.selected_item == 1,
10600 "expected PageDown to select the last item from the context menu"
10601 );
10602 } else {
10603 panic!("expected completion menu to stay open after PageDown");
10604 }
10605 });
10606
10607 cx.update_editor(|editor, window, cx| {
10608 editor.move_page_up(&MovePageUp::default(), window, cx);
10609 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10610 {
10611 assert!(
10612 menu.selected_item == 0,
10613 "expected PageUp to select the first item from the context menu"
10614 );
10615 } else {
10616 panic!("expected completion menu to stay open after PageUp");
10617 }
10618 });
10619}
10620
10621#[gpui::test]
10622async fn test_completion_sort(cx: &mut TestAppContext) {
10623 init_test(cx, |_| {});
10624 let mut cx = EditorLspTestContext::new_rust(
10625 lsp::ServerCapabilities {
10626 completion_provider: Some(lsp::CompletionOptions {
10627 trigger_characters: Some(vec![".".to_string()]),
10628 ..Default::default()
10629 }),
10630 ..Default::default()
10631 },
10632 cx,
10633 )
10634 .await;
10635 cx.lsp
10636 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
10637 Ok(Some(lsp::CompletionResponse::Array(vec![
10638 lsp::CompletionItem {
10639 label: "Range".into(),
10640 sort_text: Some("a".into()),
10641 ..Default::default()
10642 },
10643 lsp::CompletionItem {
10644 label: "r".into(),
10645 sort_text: Some("b".into()),
10646 ..Default::default()
10647 },
10648 lsp::CompletionItem {
10649 label: "ret".into(),
10650 sort_text: Some("c".into()),
10651 ..Default::default()
10652 },
10653 lsp::CompletionItem {
10654 label: "return".into(),
10655 sort_text: Some("d".into()),
10656 ..Default::default()
10657 },
10658 lsp::CompletionItem {
10659 label: "slice".into(),
10660 sort_text: Some("d".into()),
10661 ..Default::default()
10662 },
10663 ])))
10664 });
10665 cx.set_state("rˇ");
10666 cx.executor().run_until_parked();
10667 cx.update_editor(|editor, window, cx| {
10668 editor.show_completions(
10669 &ShowCompletions {
10670 trigger: Some("r".into()),
10671 },
10672 window,
10673 cx,
10674 );
10675 });
10676 cx.executor().run_until_parked();
10677
10678 cx.update_editor(|editor, _, _| {
10679 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10680 {
10681 assert_eq!(
10682 completion_menu_entries(&menu),
10683 &["r", "ret", "Range", "return"]
10684 );
10685 } else {
10686 panic!("expected completion menu to be open");
10687 }
10688 });
10689}
10690
10691#[gpui::test]
10692async fn test_as_is_completions(cx: &mut TestAppContext) {
10693 init_test(cx, |_| {});
10694 let mut cx = EditorLspTestContext::new_rust(
10695 lsp::ServerCapabilities {
10696 completion_provider: Some(lsp::CompletionOptions {
10697 ..Default::default()
10698 }),
10699 ..Default::default()
10700 },
10701 cx,
10702 )
10703 .await;
10704 cx.lsp
10705 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
10706 Ok(Some(lsp::CompletionResponse::Array(vec![
10707 lsp::CompletionItem {
10708 label: "unsafe".into(),
10709 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
10710 range: lsp::Range {
10711 start: lsp::Position {
10712 line: 1,
10713 character: 2,
10714 },
10715 end: lsp::Position {
10716 line: 1,
10717 character: 3,
10718 },
10719 },
10720 new_text: "unsafe".to_string(),
10721 })),
10722 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
10723 ..Default::default()
10724 },
10725 ])))
10726 });
10727 cx.set_state("fn a() {}\n nˇ");
10728 cx.executor().run_until_parked();
10729 cx.update_editor(|editor, window, cx| {
10730 editor.show_completions(
10731 &ShowCompletions {
10732 trigger: Some("\n".into()),
10733 },
10734 window,
10735 cx,
10736 );
10737 });
10738 cx.executor().run_until_parked();
10739
10740 cx.update_editor(|editor, window, cx| {
10741 editor.confirm_completion(&Default::default(), window, cx)
10742 });
10743 cx.executor().run_until_parked();
10744 cx.assert_editor_state("fn a() {}\n unsafeˇ");
10745}
10746
10747#[gpui::test]
10748async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
10749 init_test(cx, |_| {});
10750
10751 let mut cx = EditorLspTestContext::new_rust(
10752 lsp::ServerCapabilities {
10753 completion_provider: Some(lsp::CompletionOptions {
10754 trigger_characters: Some(vec![".".to_string()]),
10755 resolve_provider: Some(true),
10756 ..Default::default()
10757 }),
10758 ..Default::default()
10759 },
10760 cx,
10761 )
10762 .await;
10763
10764 cx.set_state("fn main() { let a = 2ˇ; }");
10765 cx.simulate_keystroke(".");
10766 let completion_item = lsp::CompletionItem {
10767 label: "Some".into(),
10768 kind: Some(lsp::CompletionItemKind::SNIPPET),
10769 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
10770 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
10771 kind: lsp::MarkupKind::Markdown,
10772 value: "```rust\nSome(2)\n```".to_string(),
10773 })),
10774 deprecated: Some(false),
10775 sort_text: Some("Some".to_string()),
10776 filter_text: Some("Some".to_string()),
10777 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
10778 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
10779 range: lsp::Range {
10780 start: lsp::Position {
10781 line: 0,
10782 character: 22,
10783 },
10784 end: lsp::Position {
10785 line: 0,
10786 character: 22,
10787 },
10788 },
10789 new_text: "Some(2)".to_string(),
10790 })),
10791 additional_text_edits: Some(vec![lsp::TextEdit {
10792 range: lsp::Range {
10793 start: lsp::Position {
10794 line: 0,
10795 character: 20,
10796 },
10797 end: lsp::Position {
10798 line: 0,
10799 character: 22,
10800 },
10801 },
10802 new_text: "".to_string(),
10803 }]),
10804 ..Default::default()
10805 };
10806
10807 let closure_completion_item = completion_item.clone();
10808 let counter = Arc::new(AtomicUsize::new(0));
10809 let counter_clone = counter.clone();
10810 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
10811 let task_completion_item = closure_completion_item.clone();
10812 counter_clone.fetch_add(1, atomic::Ordering::Release);
10813 async move {
10814 Ok(Some(lsp::CompletionResponse::Array(vec![
10815 task_completion_item,
10816 ])))
10817 }
10818 });
10819
10820 cx.condition(|editor, _| editor.context_menu_visible())
10821 .await;
10822 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
10823 assert!(request.next().await.is_some());
10824 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10825
10826 cx.simulate_keystrokes("S o m");
10827 cx.condition(|editor, _| editor.context_menu_visible())
10828 .await;
10829 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
10830 assert!(request.next().await.is_some());
10831 assert!(request.next().await.is_some());
10832 assert!(request.next().await.is_some());
10833 request.close();
10834 assert!(request.next().await.is_none());
10835 assert_eq!(
10836 counter.load(atomic::Ordering::Acquire),
10837 4,
10838 "With the completions menu open, only one LSP request should happen per input"
10839 );
10840}
10841
10842#[gpui::test]
10843async fn test_toggle_comment(cx: &mut TestAppContext) {
10844 init_test(cx, |_| {});
10845 let mut cx = EditorTestContext::new(cx).await;
10846 let language = Arc::new(Language::new(
10847 LanguageConfig {
10848 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
10849 ..Default::default()
10850 },
10851 Some(tree_sitter_rust::LANGUAGE.into()),
10852 ));
10853 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
10854
10855 // If multiple selections intersect a line, the line is only toggled once.
10856 cx.set_state(indoc! {"
10857 fn a() {
10858 «//b();
10859 ˇ»// «c();
10860 //ˇ» d();
10861 }
10862 "});
10863
10864 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10865
10866 cx.assert_editor_state(indoc! {"
10867 fn a() {
10868 «b();
10869 c();
10870 ˇ» d();
10871 }
10872 "});
10873
10874 // The comment prefix is inserted at the same column for every line in a
10875 // selection.
10876 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10877
10878 cx.assert_editor_state(indoc! {"
10879 fn a() {
10880 // «b();
10881 // c();
10882 ˇ»// d();
10883 }
10884 "});
10885
10886 // If a selection ends at the beginning of a line, that line is not toggled.
10887 cx.set_selections_state(indoc! {"
10888 fn a() {
10889 // b();
10890 «// c();
10891 ˇ» // d();
10892 }
10893 "});
10894
10895 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10896
10897 cx.assert_editor_state(indoc! {"
10898 fn a() {
10899 // b();
10900 «c();
10901 ˇ» // d();
10902 }
10903 "});
10904
10905 // If a selection span a single line and is empty, the line is toggled.
10906 cx.set_state(indoc! {"
10907 fn a() {
10908 a();
10909 b();
10910 ˇ
10911 }
10912 "});
10913
10914 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10915
10916 cx.assert_editor_state(indoc! {"
10917 fn a() {
10918 a();
10919 b();
10920 //•ˇ
10921 }
10922 "});
10923
10924 // If a selection span multiple lines, empty lines are not toggled.
10925 cx.set_state(indoc! {"
10926 fn a() {
10927 «a();
10928
10929 c();ˇ»
10930 }
10931 "});
10932
10933 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10934
10935 cx.assert_editor_state(indoc! {"
10936 fn a() {
10937 // «a();
10938
10939 // c();ˇ»
10940 }
10941 "});
10942
10943 // If a selection includes multiple comment prefixes, all lines are uncommented.
10944 cx.set_state(indoc! {"
10945 fn a() {
10946 «// a();
10947 /// b();
10948 //! c();ˇ»
10949 }
10950 "});
10951
10952 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10953
10954 cx.assert_editor_state(indoc! {"
10955 fn a() {
10956 «a();
10957 b();
10958 c();ˇ»
10959 }
10960 "});
10961}
10962
10963#[gpui::test]
10964async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
10965 init_test(cx, |_| {});
10966 let mut cx = EditorTestContext::new(cx).await;
10967 let language = Arc::new(Language::new(
10968 LanguageConfig {
10969 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
10970 ..Default::default()
10971 },
10972 Some(tree_sitter_rust::LANGUAGE.into()),
10973 ));
10974 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
10975
10976 let toggle_comments = &ToggleComments {
10977 advance_downwards: false,
10978 ignore_indent: true,
10979 };
10980
10981 // If multiple selections intersect a line, the line is only toggled once.
10982 cx.set_state(indoc! {"
10983 fn a() {
10984 // «b();
10985 // c();
10986 // ˇ» d();
10987 }
10988 "});
10989
10990 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10991
10992 cx.assert_editor_state(indoc! {"
10993 fn a() {
10994 «b();
10995 c();
10996 ˇ» d();
10997 }
10998 "});
10999
11000 // The comment prefix is inserted at the beginning of each line
11001 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11002
11003 cx.assert_editor_state(indoc! {"
11004 fn a() {
11005 // «b();
11006 // c();
11007 // ˇ» d();
11008 }
11009 "});
11010
11011 // If a selection ends at the beginning of a line, that line is not toggled.
11012 cx.set_selections_state(indoc! {"
11013 fn a() {
11014 // b();
11015 // «c();
11016 ˇ»// d();
11017 }
11018 "});
11019
11020 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11021
11022 cx.assert_editor_state(indoc! {"
11023 fn a() {
11024 // b();
11025 «c();
11026 ˇ»// d();
11027 }
11028 "});
11029
11030 // If a selection span a single line and is empty, the line is toggled.
11031 cx.set_state(indoc! {"
11032 fn a() {
11033 a();
11034 b();
11035 ˇ
11036 }
11037 "});
11038
11039 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11040
11041 cx.assert_editor_state(indoc! {"
11042 fn a() {
11043 a();
11044 b();
11045 //ˇ
11046 }
11047 "});
11048
11049 // If a selection span multiple lines, empty lines are not toggled.
11050 cx.set_state(indoc! {"
11051 fn a() {
11052 «a();
11053
11054 c();ˇ»
11055 }
11056 "});
11057
11058 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11059
11060 cx.assert_editor_state(indoc! {"
11061 fn a() {
11062 // «a();
11063
11064 // c();ˇ»
11065 }
11066 "});
11067
11068 // If a selection includes multiple comment prefixes, all lines are uncommented.
11069 cx.set_state(indoc! {"
11070 fn a() {
11071 // «a();
11072 /// b();
11073 //! c();ˇ»
11074 }
11075 "});
11076
11077 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11078
11079 cx.assert_editor_state(indoc! {"
11080 fn a() {
11081 «a();
11082 b();
11083 c();ˇ»
11084 }
11085 "});
11086}
11087
11088#[gpui::test]
11089async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
11090 init_test(cx, |_| {});
11091
11092 let language = Arc::new(Language::new(
11093 LanguageConfig {
11094 line_comments: vec!["// ".into()],
11095 ..Default::default()
11096 },
11097 Some(tree_sitter_rust::LANGUAGE.into()),
11098 ));
11099
11100 let mut cx = EditorTestContext::new(cx).await;
11101
11102 cx.language_registry().add(language.clone());
11103 cx.update_buffer(|buffer, cx| {
11104 buffer.set_language(Some(language), cx);
11105 });
11106
11107 let toggle_comments = &ToggleComments {
11108 advance_downwards: true,
11109 ignore_indent: false,
11110 };
11111
11112 // Single cursor on one line -> advance
11113 // Cursor moves horizontally 3 characters as well on non-blank line
11114 cx.set_state(indoc!(
11115 "fn a() {
11116 ˇdog();
11117 cat();
11118 }"
11119 ));
11120 cx.update_editor(|editor, window, cx| {
11121 editor.toggle_comments(toggle_comments, window, cx);
11122 });
11123 cx.assert_editor_state(indoc!(
11124 "fn a() {
11125 // dog();
11126 catˇ();
11127 }"
11128 ));
11129
11130 // Single selection on one line -> don't advance
11131 cx.set_state(indoc!(
11132 "fn a() {
11133 «dog()ˇ»;
11134 cat();
11135 }"
11136 ));
11137 cx.update_editor(|editor, window, cx| {
11138 editor.toggle_comments(toggle_comments, window, cx);
11139 });
11140 cx.assert_editor_state(indoc!(
11141 "fn a() {
11142 // «dog()ˇ»;
11143 cat();
11144 }"
11145 ));
11146
11147 // Multiple cursors on one line -> advance
11148 cx.set_state(indoc!(
11149 "fn a() {
11150 ˇdˇog();
11151 cat();
11152 }"
11153 ));
11154 cx.update_editor(|editor, window, cx| {
11155 editor.toggle_comments(toggle_comments, window, cx);
11156 });
11157 cx.assert_editor_state(indoc!(
11158 "fn a() {
11159 // dog();
11160 catˇ(ˇ);
11161 }"
11162 ));
11163
11164 // Multiple cursors on one line, with selection -> don't advance
11165 cx.set_state(indoc!(
11166 "fn a() {
11167 ˇdˇog«()ˇ»;
11168 cat();
11169 }"
11170 ));
11171 cx.update_editor(|editor, window, cx| {
11172 editor.toggle_comments(toggle_comments, window, cx);
11173 });
11174 cx.assert_editor_state(indoc!(
11175 "fn a() {
11176 // ˇdˇog«()ˇ»;
11177 cat();
11178 }"
11179 ));
11180
11181 // Single cursor on one line -> advance
11182 // Cursor moves to column 0 on blank line
11183 cx.set_state(indoc!(
11184 "fn a() {
11185 ˇdog();
11186
11187 cat();
11188 }"
11189 ));
11190 cx.update_editor(|editor, window, cx| {
11191 editor.toggle_comments(toggle_comments, window, cx);
11192 });
11193 cx.assert_editor_state(indoc!(
11194 "fn a() {
11195 // dog();
11196 ˇ
11197 cat();
11198 }"
11199 ));
11200
11201 // Single cursor on one line -> advance
11202 // Cursor starts and ends at column 0
11203 cx.set_state(indoc!(
11204 "fn a() {
11205 ˇ dog();
11206 cat();
11207 }"
11208 ));
11209 cx.update_editor(|editor, window, cx| {
11210 editor.toggle_comments(toggle_comments, window, cx);
11211 });
11212 cx.assert_editor_state(indoc!(
11213 "fn a() {
11214 // dog();
11215 ˇ cat();
11216 }"
11217 ));
11218}
11219
11220#[gpui::test]
11221async fn test_toggle_block_comment(cx: &mut TestAppContext) {
11222 init_test(cx, |_| {});
11223
11224 let mut cx = EditorTestContext::new(cx).await;
11225
11226 let html_language = Arc::new(
11227 Language::new(
11228 LanguageConfig {
11229 name: "HTML".into(),
11230 block_comment: Some(("<!-- ".into(), " -->".into())),
11231 ..Default::default()
11232 },
11233 Some(tree_sitter_html::LANGUAGE.into()),
11234 )
11235 .with_injection_query(
11236 r#"
11237 (script_element
11238 (raw_text) @injection.content
11239 (#set! injection.language "javascript"))
11240 "#,
11241 )
11242 .unwrap(),
11243 );
11244
11245 let javascript_language = Arc::new(Language::new(
11246 LanguageConfig {
11247 name: "JavaScript".into(),
11248 line_comments: vec!["// ".into()],
11249 ..Default::default()
11250 },
11251 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
11252 ));
11253
11254 cx.language_registry().add(html_language.clone());
11255 cx.language_registry().add(javascript_language.clone());
11256 cx.update_buffer(|buffer, cx| {
11257 buffer.set_language(Some(html_language), cx);
11258 });
11259
11260 // Toggle comments for empty selections
11261 cx.set_state(
11262 &r#"
11263 <p>A</p>ˇ
11264 <p>B</p>ˇ
11265 <p>C</p>ˇ
11266 "#
11267 .unindent(),
11268 );
11269 cx.update_editor(|editor, window, cx| {
11270 editor.toggle_comments(&ToggleComments::default(), window, cx)
11271 });
11272 cx.assert_editor_state(
11273 &r#"
11274 <!-- <p>A</p>ˇ -->
11275 <!-- <p>B</p>ˇ -->
11276 <!-- <p>C</p>ˇ -->
11277 "#
11278 .unindent(),
11279 );
11280 cx.update_editor(|editor, window, cx| {
11281 editor.toggle_comments(&ToggleComments::default(), window, cx)
11282 });
11283 cx.assert_editor_state(
11284 &r#"
11285 <p>A</p>ˇ
11286 <p>B</p>ˇ
11287 <p>C</p>ˇ
11288 "#
11289 .unindent(),
11290 );
11291
11292 // Toggle comments for mixture of empty and non-empty selections, where
11293 // multiple selections occupy a given line.
11294 cx.set_state(
11295 &r#"
11296 <p>A«</p>
11297 <p>ˇ»B</p>ˇ
11298 <p>C«</p>
11299 <p>ˇ»D</p>ˇ
11300 "#
11301 .unindent(),
11302 );
11303
11304 cx.update_editor(|editor, window, cx| {
11305 editor.toggle_comments(&ToggleComments::default(), window, cx)
11306 });
11307 cx.assert_editor_state(
11308 &r#"
11309 <!-- <p>A«</p>
11310 <p>ˇ»B</p>ˇ -->
11311 <!-- <p>C«</p>
11312 <p>ˇ»D</p>ˇ -->
11313 "#
11314 .unindent(),
11315 );
11316 cx.update_editor(|editor, window, cx| {
11317 editor.toggle_comments(&ToggleComments::default(), window, cx)
11318 });
11319 cx.assert_editor_state(
11320 &r#"
11321 <p>A«</p>
11322 <p>ˇ»B</p>ˇ
11323 <p>C«</p>
11324 <p>ˇ»D</p>ˇ
11325 "#
11326 .unindent(),
11327 );
11328
11329 // Toggle comments when different languages are active for different
11330 // selections.
11331 cx.set_state(
11332 &r#"
11333 ˇ<script>
11334 ˇvar x = new Y();
11335 ˇ</script>
11336 "#
11337 .unindent(),
11338 );
11339 cx.executor().run_until_parked();
11340 cx.update_editor(|editor, window, cx| {
11341 editor.toggle_comments(&ToggleComments::default(), window, cx)
11342 });
11343 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
11344 // Uncommenting and commenting from this position brings in even more wrong artifacts.
11345 cx.assert_editor_state(
11346 &r#"
11347 <!-- ˇ<script> -->
11348 // ˇvar x = new Y();
11349 <!-- ˇ</script> -->
11350 "#
11351 .unindent(),
11352 );
11353}
11354
11355#[gpui::test]
11356fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
11357 init_test(cx, |_| {});
11358
11359 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
11360 let multibuffer = cx.new(|cx| {
11361 let mut multibuffer = MultiBuffer::new(ReadWrite);
11362 multibuffer.push_excerpts(
11363 buffer.clone(),
11364 [
11365 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
11366 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
11367 ],
11368 cx,
11369 );
11370 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
11371 multibuffer
11372 });
11373
11374 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
11375 editor.update_in(cx, |editor, window, cx| {
11376 assert_eq!(editor.text(cx), "aaaa\nbbbb");
11377 editor.change_selections(None, window, cx, |s| {
11378 s.select_ranges([
11379 Point::new(0, 0)..Point::new(0, 0),
11380 Point::new(1, 0)..Point::new(1, 0),
11381 ])
11382 });
11383
11384 editor.handle_input("X", window, cx);
11385 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
11386 assert_eq!(
11387 editor.selections.ranges(cx),
11388 [
11389 Point::new(0, 1)..Point::new(0, 1),
11390 Point::new(1, 1)..Point::new(1, 1),
11391 ]
11392 );
11393
11394 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
11395 editor.change_selections(None, window, cx, |s| {
11396 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
11397 });
11398 editor.backspace(&Default::default(), window, cx);
11399 assert_eq!(editor.text(cx), "Xa\nbbb");
11400 assert_eq!(
11401 editor.selections.ranges(cx),
11402 [Point::new(1, 0)..Point::new(1, 0)]
11403 );
11404
11405 editor.change_selections(None, window, cx, |s| {
11406 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
11407 });
11408 editor.backspace(&Default::default(), window, cx);
11409 assert_eq!(editor.text(cx), "X\nbb");
11410 assert_eq!(
11411 editor.selections.ranges(cx),
11412 [Point::new(0, 1)..Point::new(0, 1)]
11413 );
11414 });
11415}
11416
11417#[gpui::test]
11418fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
11419 init_test(cx, |_| {});
11420
11421 let markers = vec![('[', ']').into(), ('(', ')').into()];
11422 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
11423 indoc! {"
11424 [aaaa
11425 (bbbb]
11426 cccc)",
11427 },
11428 markers.clone(),
11429 );
11430 let excerpt_ranges = markers.into_iter().map(|marker| {
11431 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
11432 ExcerptRange::new(context.clone())
11433 });
11434 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
11435 let multibuffer = cx.new(|cx| {
11436 let mut multibuffer = MultiBuffer::new(ReadWrite);
11437 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
11438 multibuffer
11439 });
11440
11441 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
11442 editor.update_in(cx, |editor, window, cx| {
11443 let (expected_text, selection_ranges) = marked_text_ranges(
11444 indoc! {"
11445 aaaa
11446 bˇbbb
11447 bˇbbˇb
11448 cccc"
11449 },
11450 true,
11451 );
11452 assert_eq!(editor.text(cx), expected_text);
11453 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
11454
11455 editor.handle_input("X", window, cx);
11456
11457 let (expected_text, expected_selections) = marked_text_ranges(
11458 indoc! {"
11459 aaaa
11460 bXˇbbXb
11461 bXˇbbXˇb
11462 cccc"
11463 },
11464 false,
11465 );
11466 assert_eq!(editor.text(cx), expected_text);
11467 assert_eq!(editor.selections.ranges(cx), expected_selections);
11468
11469 editor.newline(&Newline, window, cx);
11470 let (expected_text, expected_selections) = marked_text_ranges(
11471 indoc! {"
11472 aaaa
11473 bX
11474 ˇbbX
11475 b
11476 bX
11477 ˇbbX
11478 ˇb
11479 cccc"
11480 },
11481 false,
11482 );
11483 assert_eq!(editor.text(cx), expected_text);
11484 assert_eq!(editor.selections.ranges(cx), expected_selections);
11485 });
11486}
11487
11488#[gpui::test]
11489fn test_refresh_selections(cx: &mut TestAppContext) {
11490 init_test(cx, |_| {});
11491
11492 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
11493 let mut excerpt1_id = None;
11494 let multibuffer = cx.new(|cx| {
11495 let mut multibuffer = MultiBuffer::new(ReadWrite);
11496 excerpt1_id = multibuffer
11497 .push_excerpts(
11498 buffer.clone(),
11499 [
11500 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
11501 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
11502 ],
11503 cx,
11504 )
11505 .into_iter()
11506 .next();
11507 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
11508 multibuffer
11509 });
11510
11511 let editor = cx.add_window(|window, cx| {
11512 let mut editor = build_editor(multibuffer.clone(), window, cx);
11513 let snapshot = editor.snapshot(window, cx);
11514 editor.change_selections(None, window, cx, |s| {
11515 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
11516 });
11517 editor.begin_selection(
11518 Point::new(2, 1).to_display_point(&snapshot),
11519 true,
11520 1,
11521 window,
11522 cx,
11523 );
11524 assert_eq!(
11525 editor.selections.ranges(cx),
11526 [
11527 Point::new(1, 3)..Point::new(1, 3),
11528 Point::new(2, 1)..Point::new(2, 1),
11529 ]
11530 );
11531 editor
11532 });
11533
11534 // Refreshing selections is a no-op when excerpts haven't changed.
11535 _ = editor.update(cx, |editor, window, cx| {
11536 editor.change_selections(None, window, cx, |s| s.refresh());
11537 assert_eq!(
11538 editor.selections.ranges(cx),
11539 [
11540 Point::new(1, 3)..Point::new(1, 3),
11541 Point::new(2, 1)..Point::new(2, 1),
11542 ]
11543 );
11544 });
11545
11546 multibuffer.update(cx, |multibuffer, cx| {
11547 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
11548 });
11549 _ = editor.update(cx, |editor, window, cx| {
11550 // Removing an excerpt causes the first selection to become degenerate.
11551 assert_eq!(
11552 editor.selections.ranges(cx),
11553 [
11554 Point::new(0, 0)..Point::new(0, 0),
11555 Point::new(0, 1)..Point::new(0, 1)
11556 ]
11557 );
11558
11559 // Refreshing selections will relocate the first selection to the original buffer
11560 // location.
11561 editor.change_selections(None, window, cx, |s| s.refresh());
11562 assert_eq!(
11563 editor.selections.ranges(cx),
11564 [
11565 Point::new(0, 1)..Point::new(0, 1),
11566 Point::new(0, 3)..Point::new(0, 3)
11567 ]
11568 );
11569 assert!(editor.selections.pending_anchor().is_some());
11570 });
11571}
11572
11573#[gpui::test]
11574fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
11575 init_test(cx, |_| {});
11576
11577 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
11578 let mut excerpt1_id = None;
11579 let multibuffer = cx.new(|cx| {
11580 let mut multibuffer = MultiBuffer::new(ReadWrite);
11581 excerpt1_id = multibuffer
11582 .push_excerpts(
11583 buffer.clone(),
11584 [
11585 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
11586 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
11587 ],
11588 cx,
11589 )
11590 .into_iter()
11591 .next();
11592 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
11593 multibuffer
11594 });
11595
11596 let editor = cx.add_window(|window, cx| {
11597 let mut editor = build_editor(multibuffer.clone(), window, cx);
11598 let snapshot = editor.snapshot(window, cx);
11599 editor.begin_selection(
11600 Point::new(1, 3).to_display_point(&snapshot),
11601 false,
11602 1,
11603 window,
11604 cx,
11605 );
11606 assert_eq!(
11607 editor.selections.ranges(cx),
11608 [Point::new(1, 3)..Point::new(1, 3)]
11609 );
11610 editor
11611 });
11612
11613 multibuffer.update(cx, |multibuffer, cx| {
11614 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
11615 });
11616 _ = editor.update(cx, |editor, window, cx| {
11617 assert_eq!(
11618 editor.selections.ranges(cx),
11619 [Point::new(0, 0)..Point::new(0, 0)]
11620 );
11621
11622 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
11623 editor.change_selections(None, window, cx, |s| s.refresh());
11624 assert_eq!(
11625 editor.selections.ranges(cx),
11626 [Point::new(0, 3)..Point::new(0, 3)]
11627 );
11628 assert!(editor.selections.pending_anchor().is_some());
11629 });
11630}
11631
11632#[gpui::test]
11633async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
11634 init_test(cx, |_| {});
11635
11636 let language = Arc::new(
11637 Language::new(
11638 LanguageConfig {
11639 brackets: BracketPairConfig {
11640 pairs: vec![
11641 BracketPair {
11642 start: "{".to_string(),
11643 end: "}".to_string(),
11644 close: true,
11645 surround: true,
11646 newline: true,
11647 },
11648 BracketPair {
11649 start: "/* ".to_string(),
11650 end: " */".to_string(),
11651 close: true,
11652 surround: true,
11653 newline: true,
11654 },
11655 ],
11656 ..Default::default()
11657 },
11658 ..Default::default()
11659 },
11660 Some(tree_sitter_rust::LANGUAGE.into()),
11661 )
11662 .with_indents_query("")
11663 .unwrap(),
11664 );
11665
11666 let text = concat!(
11667 "{ }\n", //
11668 " x\n", //
11669 " /* */\n", //
11670 "x\n", //
11671 "{{} }\n", //
11672 );
11673
11674 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
11675 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11676 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11677 editor
11678 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11679 .await;
11680
11681 editor.update_in(cx, |editor, window, cx| {
11682 editor.change_selections(None, window, cx, |s| {
11683 s.select_display_ranges([
11684 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
11685 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
11686 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
11687 ])
11688 });
11689 editor.newline(&Newline, window, cx);
11690
11691 assert_eq!(
11692 editor.buffer().read(cx).read(cx).text(),
11693 concat!(
11694 "{ \n", // Suppress rustfmt
11695 "\n", //
11696 "}\n", //
11697 " x\n", //
11698 " /* \n", //
11699 " \n", //
11700 " */\n", //
11701 "x\n", //
11702 "{{} \n", //
11703 "}\n", //
11704 )
11705 );
11706 });
11707}
11708
11709#[gpui::test]
11710fn test_highlighted_ranges(cx: &mut TestAppContext) {
11711 init_test(cx, |_| {});
11712
11713 let editor = cx.add_window(|window, cx| {
11714 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
11715 build_editor(buffer.clone(), window, cx)
11716 });
11717
11718 _ = editor.update(cx, |editor, window, cx| {
11719 struct Type1;
11720 struct Type2;
11721
11722 let buffer = editor.buffer.read(cx).snapshot(cx);
11723
11724 let anchor_range =
11725 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
11726
11727 editor.highlight_background::<Type1>(
11728 &[
11729 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
11730 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
11731 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
11732 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
11733 ],
11734 |_| Hsla::red(),
11735 cx,
11736 );
11737 editor.highlight_background::<Type2>(
11738 &[
11739 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
11740 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
11741 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
11742 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
11743 ],
11744 |_| Hsla::green(),
11745 cx,
11746 );
11747
11748 let snapshot = editor.snapshot(window, cx);
11749 let mut highlighted_ranges = editor.background_highlights_in_range(
11750 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
11751 &snapshot,
11752 cx.theme().colors(),
11753 );
11754 // Enforce a consistent ordering based on color without relying on the ordering of the
11755 // highlight's `TypeId` which is non-executor.
11756 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
11757 assert_eq!(
11758 highlighted_ranges,
11759 &[
11760 (
11761 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
11762 Hsla::red(),
11763 ),
11764 (
11765 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
11766 Hsla::red(),
11767 ),
11768 (
11769 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
11770 Hsla::green(),
11771 ),
11772 (
11773 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
11774 Hsla::green(),
11775 ),
11776 ]
11777 );
11778 assert_eq!(
11779 editor.background_highlights_in_range(
11780 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
11781 &snapshot,
11782 cx.theme().colors(),
11783 ),
11784 &[(
11785 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
11786 Hsla::red(),
11787 )]
11788 );
11789 });
11790}
11791
11792#[gpui::test]
11793async fn test_following(cx: &mut TestAppContext) {
11794 init_test(cx, |_| {});
11795
11796 let fs = FakeFs::new(cx.executor());
11797 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
11798
11799 let buffer = project.update(cx, |project, cx| {
11800 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
11801 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
11802 });
11803 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
11804 let follower = cx.update(|cx| {
11805 cx.open_window(
11806 WindowOptions {
11807 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
11808 gpui::Point::new(px(0.), px(0.)),
11809 gpui::Point::new(px(10.), px(80.)),
11810 ))),
11811 ..Default::default()
11812 },
11813 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
11814 )
11815 .unwrap()
11816 });
11817
11818 let is_still_following = Rc::new(RefCell::new(true));
11819 let follower_edit_event_count = Rc::new(RefCell::new(0));
11820 let pending_update = Rc::new(RefCell::new(None));
11821 let leader_entity = leader.root(cx).unwrap();
11822 let follower_entity = follower.root(cx).unwrap();
11823 _ = follower.update(cx, {
11824 let update = pending_update.clone();
11825 let is_still_following = is_still_following.clone();
11826 let follower_edit_event_count = follower_edit_event_count.clone();
11827 |_, window, cx| {
11828 cx.subscribe_in(
11829 &leader_entity,
11830 window,
11831 move |_, leader, event, window, cx| {
11832 leader.read(cx).add_event_to_update_proto(
11833 event,
11834 &mut update.borrow_mut(),
11835 window,
11836 cx,
11837 );
11838 },
11839 )
11840 .detach();
11841
11842 cx.subscribe_in(
11843 &follower_entity,
11844 window,
11845 move |_, _, event: &EditorEvent, _window, _cx| {
11846 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
11847 *is_still_following.borrow_mut() = false;
11848 }
11849
11850 if let EditorEvent::BufferEdited = event {
11851 *follower_edit_event_count.borrow_mut() += 1;
11852 }
11853 },
11854 )
11855 .detach();
11856 }
11857 });
11858
11859 // Update the selections only
11860 _ = leader.update(cx, |leader, window, cx| {
11861 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
11862 });
11863 follower
11864 .update(cx, |follower, window, cx| {
11865 follower.apply_update_proto(
11866 &project,
11867 pending_update.borrow_mut().take().unwrap(),
11868 window,
11869 cx,
11870 )
11871 })
11872 .unwrap()
11873 .await
11874 .unwrap();
11875 _ = follower.update(cx, |follower, _, cx| {
11876 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
11877 });
11878 assert!(*is_still_following.borrow());
11879 assert_eq!(*follower_edit_event_count.borrow(), 0);
11880
11881 // Update the scroll position only
11882 _ = leader.update(cx, |leader, window, cx| {
11883 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
11884 });
11885 follower
11886 .update(cx, |follower, window, cx| {
11887 follower.apply_update_proto(
11888 &project,
11889 pending_update.borrow_mut().take().unwrap(),
11890 window,
11891 cx,
11892 )
11893 })
11894 .unwrap()
11895 .await
11896 .unwrap();
11897 assert_eq!(
11898 follower
11899 .update(cx, |follower, _, cx| follower.scroll_position(cx))
11900 .unwrap(),
11901 gpui::Point::new(1.5, 3.5)
11902 );
11903 assert!(*is_still_following.borrow());
11904 assert_eq!(*follower_edit_event_count.borrow(), 0);
11905
11906 // Update the selections and scroll position. The follower's scroll position is updated
11907 // via autoscroll, not via the leader's exact scroll position.
11908 _ = leader.update(cx, |leader, window, cx| {
11909 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
11910 leader.request_autoscroll(Autoscroll::newest(), cx);
11911 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
11912 });
11913 follower
11914 .update(cx, |follower, window, cx| {
11915 follower.apply_update_proto(
11916 &project,
11917 pending_update.borrow_mut().take().unwrap(),
11918 window,
11919 cx,
11920 )
11921 })
11922 .unwrap()
11923 .await
11924 .unwrap();
11925 _ = follower.update(cx, |follower, _, cx| {
11926 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
11927 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
11928 });
11929 assert!(*is_still_following.borrow());
11930
11931 // Creating a pending selection that precedes another selection
11932 _ = leader.update(cx, |leader, window, cx| {
11933 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
11934 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
11935 });
11936 follower
11937 .update(cx, |follower, window, cx| {
11938 follower.apply_update_proto(
11939 &project,
11940 pending_update.borrow_mut().take().unwrap(),
11941 window,
11942 cx,
11943 )
11944 })
11945 .unwrap()
11946 .await
11947 .unwrap();
11948 _ = follower.update(cx, |follower, _, cx| {
11949 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
11950 });
11951 assert!(*is_still_following.borrow());
11952
11953 // Extend the pending selection so that it surrounds another selection
11954 _ = leader.update(cx, |leader, window, cx| {
11955 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
11956 });
11957 follower
11958 .update(cx, |follower, window, cx| {
11959 follower.apply_update_proto(
11960 &project,
11961 pending_update.borrow_mut().take().unwrap(),
11962 window,
11963 cx,
11964 )
11965 })
11966 .unwrap()
11967 .await
11968 .unwrap();
11969 _ = follower.update(cx, |follower, _, cx| {
11970 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
11971 });
11972
11973 // Scrolling locally breaks the follow
11974 _ = follower.update(cx, |follower, window, cx| {
11975 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
11976 follower.set_scroll_anchor(
11977 ScrollAnchor {
11978 anchor: top_anchor,
11979 offset: gpui::Point::new(0.0, 0.5),
11980 },
11981 window,
11982 cx,
11983 );
11984 });
11985 assert!(!(*is_still_following.borrow()));
11986}
11987
11988#[gpui::test]
11989async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
11990 init_test(cx, |_| {});
11991
11992 let fs = FakeFs::new(cx.executor());
11993 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
11994 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11995 let pane = workspace
11996 .update(cx, |workspace, _, _| workspace.active_pane().clone())
11997 .unwrap();
11998
11999 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12000
12001 let leader = pane.update_in(cx, |_, window, cx| {
12002 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
12003 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
12004 });
12005
12006 // Start following the editor when it has no excerpts.
12007 let mut state_message =
12008 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
12009 let workspace_entity = workspace.root(cx).unwrap();
12010 let follower_1 = cx
12011 .update_window(*workspace.deref(), |_, window, cx| {
12012 Editor::from_state_proto(
12013 workspace_entity,
12014 ViewId {
12015 creator: Default::default(),
12016 id: 0,
12017 },
12018 &mut state_message,
12019 window,
12020 cx,
12021 )
12022 })
12023 .unwrap()
12024 .unwrap()
12025 .await
12026 .unwrap();
12027
12028 let update_message = Rc::new(RefCell::new(None));
12029 follower_1.update_in(cx, {
12030 let update = update_message.clone();
12031 |_, window, cx| {
12032 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
12033 leader.read(cx).add_event_to_update_proto(
12034 event,
12035 &mut update.borrow_mut(),
12036 window,
12037 cx,
12038 );
12039 })
12040 .detach();
12041 }
12042 });
12043
12044 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
12045 (
12046 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
12047 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
12048 )
12049 });
12050
12051 // Insert some excerpts.
12052 leader.update(cx, |leader, cx| {
12053 leader.buffer.update(cx, |multibuffer, cx| {
12054 let excerpt_ids = multibuffer.push_excerpts(
12055 buffer_1.clone(),
12056 [
12057 ExcerptRange::new(1..6),
12058 ExcerptRange::new(12..15),
12059 ExcerptRange::new(0..3),
12060 ],
12061 cx,
12062 );
12063 multibuffer.insert_excerpts_after(
12064 excerpt_ids[0],
12065 buffer_2.clone(),
12066 [ExcerptRange::new(8..12), ExcerptRange::new(0..6)],
12067 cx,
12068 );
12069 });
12070 });
12071
12072 // Apply the update of adding the excerpts.
12073 follower_1
12074 .update_in(cx, |follower, window, cx| {
12075 follower.apply_update_proto(
12076 &project,
12077 update_message.borrow().clone().unwrap(),
12078 window,
12079 cx,
12080 )
12081 })
12082 .await
12083 .unwrap();
12084 assert_eq!(
12085 follower_1.update(cx, |editor, cx| editor.text(cx)),
12086 leader.update(cx, |editor, cx| editor.text(cx))
12087 );
12088 update_message.borrow_mut().take();
12089
12090 // Start following separately after it already has excerpts.
12091 let mut state_message =
12092 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
12093 let workspace_entity = workspace.root(cx).unwrap();
12094 let follower_2 = cx
12095 .update_window(*workspace.deref(), |_, window, cx| {
12096 Editor::from_state_proto(
12097 workspace_entity,
12098 ViewId {
12099 creator: Default::default(),
12100 id: 0,
12101 },
12102 &mut state_message,
12103 window,
12104 cx,
12105 )
12106 })
12107 .unwrap()
12108 .unwrap()
12109 .await
12110 .unwrap();
12111 assert_eq!(
12112 follower_2.update(cx, |editor, cx| editor.text(cx)),
12113 leader.update(cx, |editor, cx| editor.text(cx))
12114 );
12115
12116 // Remove some excerpts.
12117 leader.update(cx, |leader, cx| {
12118 leader.buffer.update(cx, |multibuffer, cx| {
12119 let excerpt_ids = multibuffer.excerpt_ids();
12120 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
12121 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
12122 });
12123 });
12124
12125 // Apply the update of removing the excerpts.
12126 follower_1
12127 .update_in(cx, |follower, window, cx| {
12128 follower.apply_update_proto(
12129 &project,
12130 update_message.borrow().clone().unwrap(),
12131 window,
12132 cx,
12133 )
12134 })
12135 .await
12136 .unwrap();
12137 follower_2
12138 .update_in(cx, |follower, window, cx| {
12139 follower.apply_update_proto(
12140 &project,
12141 update_message.borrow().clone().unwrap(),
12142 window,
12143 cx,
12144 )
12145 })
12146 .await
12147 .unwrap();
12148 update_message.borrow_mut().take();
12149 assert_eq!(
12150 follower_1.update(cx, |editor, cx| editor.text(cx)),
12151 leader.update(cx, |editor, cx| editor.text(cx))
12152 );
12153}
12154
12155#[gpui::test]
12156async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
12157 init_test(cx, |_| {});
12158
12159 let mut cx = EditorTestContext::new(cx).await;
12160 let lsp_store =
12161 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
12162
12163 cx.set_state(indoc! {"
12164 ˇfn func(abc def: i32) -> u32 {
12165 }
12166 "});
12167
12168 cx.update(|_, cx| {
12169 lsp_store.update(cx, |lsp_store, cx| {
12170 lsp_store
12171 .update_diagnostics(
12172 LanguageServerId(0),
12173 lsp::PublishDiagnosticsParams {
12174 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
12175 version: None,
12176 diagnostics: vec![
12177 lsp::Diagnostic {
12178 range: lsp::Range::new(
12179 lsp::Position::new(0, 11),
12180 lsp::Position::new(0, 12),
12181 ),
12182 severity: Some(lsp::DiagnosticSeverity::ERROR),
12183 ..Default::default()
12184 },
12185 lsp::Diagnostic {
12186 range: lsp::Range::new(
12187 lsp::Position::new(0, 12),
12188 lsp::Position::new(0, 15),
12189 ),
12190 severity: Some(lsp::DiagnosticSeverity::ERROR),
12191 ..Default::default()
12192 },
12193 lsp::Diagnostic {
12194 range: lsp::Range::new(
12195 lsp::Position::new(0, 25),
12196 lsp::Position::new(0, 28),
12197 ),
12198 severity: Some(lsp::DiagnosticSeverity::ERROR),
12199 ..Default::default()
12200 },
12201 ],
12202 },
12203 &[],
12204 cx,
12205 )
12206 .unwrap()
12207 });
12208 });
12209
12210 executor.run_until_parked();
12211
12212 cx.update_editor(|editor, window, cx| {
12213 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12214 });
12215
12216 cx.assert_editor_state(indoc! {"
12217 fn func(abc def: i32) -> ˇu32 {
12218 }
12219 "});
12220
12221 cx.update_editor(|editor, window, cx| {
12222 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12223 });
12224
12225 cx.assert_editor_state(indoc! {"
12226 fn func(abc ˇdef: i32) -> u32 {
12227 }
12228 "});
12229
12230 cx.update_editor(|editor, window, cx| {
12231 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12232 });
12233
12234 cx.assert_editor_state(indoc! {"
12235 fn func(abcˇ def: i32) -> u32 {
12236 }
12237 "});
12238
12239 cx.update_editor(|editor, window, cx| {
12240 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12241 });
12242
12243 cx.assert_editor_state(indoc! {"
12244 fn func(abc def: i32) -> ˇu32 {
12245 }
12246 "});
12247}
12248
12249#[gpui::test]
12250async fn cycle_through_same_place_diagnostics(
12251 executor: BackgroundExecutor,
12252 cx: &mut TestAppContext,
12253) {
12254 init_test(cx, |_| {});
12255
12256 let mut cx = EditorTestContext::new(cx).await;
12257 let lsp_store =
12258 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
12259
12260 cx.set_state(indoc! {"
12261 ˇfn func(abc def: i32) -> u32 {
12262 }
12263 "});
12264
12265 cx.update(|_, cx| {
12266 lsp_store.update(cx, |lsp_store, cx| {
12267 lsp_store
12268 .update_diagnostics(
12269 LanguageServerId(0),
12270 lsp::PublishDiagnosticsParams {
12271 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
12272 version: None,
12273 diagnostics: vec![
12274 lsp::Diagnostic {
12275 range: lsp::Range::new(
12276 lsp::Position::new(0, 11),
12277 lsp::Position::new(0, 12),
12278 ),
12279 severity: Some(lsp::DiagnosticSeverity::ERROR),
12280 ..Default::default()
12281 },
12282 lsp::Diagnostic {
12283 range: lsp::Range::new(
12284 lsp::Position::new(0, 12),
12285 lsp::Position::new(0, 15),
12286 ),
12287 severity: Some(lsp::DiagnosticSeverity::ERROR),
12288 ..Default::default()
12289 },
12290 lsp::Diagnostic {
12291 range: lsp::Range::new(
12292 lsp::Position::new(0, 12),
12293 lsp::Position::new(0, 15),
12294 ),
12295 severity: Some(lsp::DiagnosticSeverity::ERROR),
12296 ..Default::default()
12297 },
12298 lsp::Diagnostic {
12299 range: lsp::Range::new(
12300 lsp::Position::new(0, 25),
12301 lsp::Position::new(0, 28),
12302 ),
12303 severity: Some(lsp::DiagnosticSeverity::ERROR),
12304 ..Default::default()
12305 },
12306 ],
12307 },
12308 &[],
12309 cx,
12310 )
12311 .unwrap()
12312 });
12313 });
12314 executor.run_until_parked();
12315
12316 //// Backward
12317
12318 // Fourth diagnostic
12319 cx.update_editor(|editor, window, cx| {
12320 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12321 });
12322 cx.assert_editor_state(indoc! {"
12323 fn func(abc def: i32) -> ˇu32 {
12324 }
12325 "});
12326
12327 // Third diagnostic
12328 cx.update_editor(|editor, window, cx| {
12329 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12330 });
12331 cx.assert_editor_state(indoc! {"
12332 fn func(abc ˇdef: i32) -> u32 {
12333 }
12334 "});
12335
12336 // Second diagnostic, same place
12337 cx.update_editor(|editor, window, cx| {
12338 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12339 });
12340 cx.assert_editor_state(indoc! {"
12341 fn func(abc ˇdef: i32) -> u32 {
12342 }
12343 "});
12344
12345 // First diagnostic
12346 cx.update_editor(|editor, window, cx| {
12347 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12348 });
12349 cx.assert_editor_state(indoc! {"
12350 fn func(abcˇ def: i32) -> u32 {
12351 }
12352 "});
12353
12354 // Wrapped over, fourth diagnostic
12355 cx.update_editor(|editor, window, cx| {
12356 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12357 });
12358 cx.assert_editor_state(indoc! {"
12359 fn func(abc def: i32) -> ˇu32 {
12360 }
12361 "});
12362
12363 cx.update_editor(|editor, window, cx| {
12364 editor.move_to_beginning(&MoveToBeginning, window, cx);
12365 });
12366 cx.assert_editor_state(indoc! {"
12367 ˇfn func(abc def: i32) -> u32 {
12368 }
12369 "});
12370
12371 //// Forward
12372
12373 // First diagnostic
12374 cx.update_editor(|editor, window, cx| {
12375 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12376 });
12377 cx.assert_editor_state(indoc! {"
12378 fn func(abcˇ def: i32) -> u32 {
12379 }
12380 "});
12381
12382 // Second diagnostic
12383 cx.update_editor(|editor, window, cx| {
12384 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12385 });
12386 cx.assert_editor_state(indoc! {"
12387 fn func(abc ˇdef: i32) -> u32 {
12388 }
12389 "});
12390
12391 // Third diagnostic, same place
12392 cx.update_editor(|editor, window, cx| {
12393 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12394 });
12395 cx.assert_editor_state(indoc! {"
12396 fn func(abc ˇdef: i32) -> u32 {
12397 }
12398 "});
12399
12400 // Fourth diagnostic
12401 cx.update_editor(|editor, window, cx| {
12402 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12403 });
12404 cx.assert_editor_state(indoc! {"
12405 fn func(abc def: i32) -> ˇu32 {
12406 }
12407 "});
12408
12409 // Wrapped around, first diagnostic
12410 cx.update_editor(|editor, window, cx| {
12411 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12412 });
12413 cx.assert_editor_state(indoc! {"
12414 fn func(abcˇ def: i32) -> u32 {
12415 }
12416 "});
12417}
12418
12419#[gpui::test]
12420async fn active_diagnostics_dismiss_after_invalidation(
12421 executor: BackgroundExecutor,
12422 cx: &mut TestAppContext,
12423) {
12424 init_test(cx, |_| {});
12425
12426 let mut cx = EditorTestContext::new(cx).await;
12427 let lsp_store =
12428 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
12429
12430 cx.set_state(indoc! {"
12431 ˇfn func(abc def: i32) -> u32 {
12432 }
12433 "});
12434
12435 let message = "Something's wrong!";
12436 cx.update(|_, cx| {
12437 lsp_store.update(cx, |lsp_store, cx| {
12438 lsp_store
12439 .update_diagnostics(
12440 LanguageServerId(0),
12441 lsp::PublishDiagnosticsParams {
12442 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
12443 version: None,
12444 diagnostics: vec![lsp::Diagnostic {
12445 range: lsp::Range::new(
12446 lsp::Position::new(0, 11),
12447 lsp::Position::new(0, 12),
12448 ),
12449 severity: Some(lsp::DiagnosticSeverity::ERROR),
12450 message: message.to_string(),
12451 ..Default::default()
12452 }],
12453 },
12454 &[],
12455 cx,
12456 )
12457 .unwrap()
12458 });
12459 });
12460 executor.run_until_parked();
12461
12462 cx.update_editor(|editor, window, cx| {
12463 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12464 assert_eq!(
12465 editor
12466 .active_diagnostics
12467 .as_ref()
12468 .map(|diagnostics_group| diagnostics_group.primary_message.as_str()),
12469 Some(message),
12470 "Should have a diagnostics group activated"
12471 );
12472 });
12473 cx.assert_editor_state(indoc! {"
12474 fn func(abcˇ def: i32) -> u32 {
12475 }
12476 "});
12477
12478 cx.update(|_, cx| {
12479 lsp_store.update(cx, |lsp_store, cx| {
12480 lsp_store
12481 .update_diagnostics(
12482 LanguageServerId(0),
12483 lsp::PublishDiagnosticsParams {
12484 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
12485 version: None,
12486 diagnostics: Vec::new(),
12487 },
12488 &[],
12489 cx,
12490 )
12491 .unwrap()
12492 });
12493 });
12494 executor.run_until_parked();
12495 cx.update_editor(|editor, _, _| {
12496 assert_eq!(
12497 editor.active_diagnostics, None,
12498 "After no diagnostics set to the editor, no diagnostics should be active"
12499 );
12500 });
12501 cx.assert_editor_state(indoc! {"
12502 fn func(abcˇ def: i32) -> u32 {
12503 }
12504 "});
12505
12506 cx.update_editor(|editor, window, cx| {
12507 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12508 assert_eq!(
12509 editor.active_diagnostics, None,
12510 "Should be no diagnostics to go to and activate"
12511 );
12512 });
12513 cx.assert_editor_state(indoc! {"
12514 fn func(abcˇ def: i32) -> u32 {
12515 }
12516 "});
12517}
12518
12519#[gpui::test]
12520async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
12521 init_test(cx, |_| {});
12522
12523 let mut cx = EditorTestContext::new(cx).await;
12524
12525 cx.set_state(indoc! {"
12526 fn func(abˇc def: i32) -> u32 {
12527 }
12528 "});
12529 let lsp_store =
12530 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
12531
12532 cx.update(|_, cx| {
12533 lsp_store.update(cx, |lsp_store, cx| {
12534 lsp_store.update_diagnostics(
12535 LanguageServerId(0),
12536 lsp::PublishDiagnosticsParams {
12537 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
12538 version: None,
12539 diagnostics: vec![lsp::Diagnostic {
12540 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 12)),
12541 severity: Some(lsp::DiagnosticSeverity::ERROR),
12542 message: "we've had problems with <https://link.one>, and <https://link.two> is broken".to_string(),
12543 ..Default::default()
12544 }],
12545 },
12546 &[],
12547 cx,
12548 )
12549 })
12550 }).unwrap();
12551 cx.run_until_parked();
12552 cx.update_editor(|editor, window, cx| {
12553 hover_popover::hover(editor, &Default::default(), window, cx)
12554 });
12555 cx.run_until_parked();
12556 cx.update_editor(|editor, _, _| assert!(editor.hover_state.diagnostic_popover.is_some()))
12557}
12558
12559#[gpui::test]
12560async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
12561 init_test(cx, |_| {});
12562
12563 let mut cx = EditorTestContext::new(cx).await;
12564
12565 let diff_base = r#"
12566 use some::mod;
12567
12568 const A: u32 = 42;
12569
12570 fn main() {
12571 println!("hello");
12572
12573 println!("world");
12574 }
12575 "#
12576 .unindent();
12577
12578 // Edits are modified, removed, modified, added
12579 cx.set_state(
12580 &r#"
12581 use some::modified;
12582
12583 ˇ
12584 fn main() {
12585 println!("hello there");
12586
12587 println!("around the");
12588 println!("world");
12589 }
12590 "#
12591 .unindent(),
12592 );
12593
12594 cx.set_head_text(&diff_base);
12595 executor.run_until_parked();
12596
12597 cx.update_editor(|editor, window, cx| {
12598 //Wrap around the bottom of the buffer
12599 for _ in 0..3 {
12600 editor.go_to_next_hunk(&GoToHunk, window, cx);
12601 }
12602 });
12603
12604 cx.assert_editor_state(
12605 &r#"
12606 ˇuse some::modified;
12607
12608
12609 fn main() {
12610 println!("hello there");
12611
12612 println!("around the");
12613 println!("world");
12614 }
12615 "#
12616 .unindent(),
12617 );
12618
12619 cx.update_editor(|editor, window, cx| {
12620 //Wrap around the top of the buffer
12621 for _ in 0..2 {
12622 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12623 }
12624 });
12625
12626 cx.assert_editor_state(
12627 &r#"
12628 use some::modified;
12629
12630
12631 fn main() {
12632 ˇ println!("hello there");
12633
12634 println!("around the");
12635 println!("world");
12636 }
12637 "#
12638 .unindent(),
12639 );
12640
12641 cx.update_editor(|editor, window, cx| {
12642 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12643 });
12644
12645 cx.assert_editor_state(
12646 &r#"
12647 use some::modified;
12648
12649 ˇ
12650 fn main() {
12651 println!("hello there");
12652
12653 println!("around the");
12654 println!("world");
12655 }
12656 "#
12657 .unindent(),
12658 );
12659
12660 cx.update_editor(|editor, window, cx| {
12661 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12662 });
12663
12664 cx.assert_editor_state(
12665 &r#"
12666 ˇuse some::modified;
12667
12668
12669 fn main() {
12670 println!("hello there");
12671
12672 println!("around the");
12673 println!("world");
12674 }
12675 "#
12676 .unindent(),
12677 );
12678
12679 cx.update_editor(|editor, window, cx| {
12680 for _ in 0..2 {
12681 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12682 }
12683 });
12684
12685 cx.assert_editor_state(
12686 &r#"
12687 use some::modified;
12688
12689
12690 fn main() {
12691 ˇ println!("hello there");
12692
12693 println!("around the");
12694 println!("world");
12695 }
12696 "#
12697 .unindent(),
12698 );
12699
12700 cx.update_editor(|editor, window, cx| {
12701 editor.fold(&Fold, window, cx);
12702 });
12703
12704 cx.update_editor(|editor, window, cx| {
12705 editor.go_to_next_hunk(&GoToHunk, window, cx);
12706 });
12707
12708 cx.assert_editor_state(
12709 &r#"
12710 ˇuse some::modified;
12711
12712
12713 fn main() {
12714 println!("hello there");
12715
12716 println!("around the");
12717 println!("world");
12718 }
12719 "#
12720 .unindent(),
12721 );
12722}
12723
12724#[test]
12725fn test_split_words() {
12726 fn split(text: &str) -> Vec<&str> {
12727 split_words(text).collect()
12728 }
12729
12730 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
12731 assert_eq!(split("hello_world"), &["hello_", "world"]);
12732 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
12733 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
12734 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
12735 assert_eq!(split("helloworld"), &["helloworld"]);
12736
12737 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
12738}
12739
12740#[gpui::test]
12741async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
12742 init_test(cx, |_| {});
12743
12744 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
12745 let mut assert = |before, after| {
12746 let _state_context = cx.set_state(before);
12747 cx.run_until_parked();
12748 cx.update_editor(|editor, window, cx| {
12749 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
12750 });
12751 cx.run_until_parked();
12752 cx.assert_editor_state(after);
12753 };
12754
12755 // Outside bracket jumps to outside of matching bracket
12756 assert("console.logˇ(var);", "console.log(var)ˇ;");
12757 assert("console.log(var)ˇ;", "console.logˇ(var);");
12758
12759 // Inside bracket jumps to inside of matching bracket
12760 assert("console.log(ˇvar);", "console.log(varˇ);");
12761 assert("console.log(varˇ);", "console.log(ˇvar);");
12762
12763 // When outside a bracket and inside, favor jumping to the inside bracket
12764 assert(
12765 "console.log('foo', [1, 2, 3]ˇ);",
12766 "console.log(ˇ'foo', [1, 2, 3]);",
12767 );
12768 assert(
12769 "console.log(ˇ'foo', [1, 2, 3]);",
12770 "console.log('foo', [1, 2, 3]ˇ);",
12771 );
12772
12773 // Bias forward if two options are equally likely
12774 assert(
12775 "let result = curried_fun()ˇ();",
12776 "let result = curried_fun()()ˇ;",
12777 );
12778
12779 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
12780 assert(
12781 indoc! {"
12782 function test() {
12783 console.log('test')ˇ
12784 }"},
12785 indoc! {"
12786 function test() {
12787 console.logˇ('test')
12788 }"},
12789 );
12790}
12791
12792#[gpui::test]
12793async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
12794 init_test(cx, |_| {});
12795
12796 let fs = FakeFs::new(cx.executor());
12797 fs.insert_tree(
12798 path!("/a"),
12799 json!({
12800 "main.rs": "fn main() { let a = 5; }",
12801 "other.rs": "// Test file",
12802 }),
12803 )
12804 .await;
12805 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12806
12807 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12808 language_registry.add(Arc::new(Language::new(
12809 LanguageConfig {
12810 name: "Rust".into(),
12811 matcher: LanguageMatcher {
12812 path_suffixes: vec!["rs".to_string()],
12813 ..Default::default()
12814 },
12815 brackets: BracketPairConfig {
12816 pairs: vec![BracketPair {
12817 start: "{".to_string(),
12818 end: "}".to_string(),
12819 close: true,
12820 surround: true,
12821 newline: true,
12822 }],
12823 disabled_scopes_by_bracket_ix: Vec::new(),
12824 },
12825 ..Default::default()
12826 },
12827 Some(tree_sitter_rust::LANGUAGE.into()),
12828 )));
12829 let mut fake_servers = language_registry.register_fake_lsp(
12830 "Rust",
12831 FakeLspAdapter {
12832 capabilities: lsp::ServerCapabilities {
12833 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
12834 first_trigger_character: "{".to_string(),
12835 more_trigger_character: None,
12836 }),
12837 ..Default::default()
12838 },
12839 ..Default::default()
12840 },
12841 );
12842
12843 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12844
12845 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12846
12847 let worktree_id = workspace
12848 .update(cx, |workspace, _, cx| {
12849 workspace.project().update(cx, |project, cx| {
12850 project.worktrees(cx).next().unwrap().read(cx).id()
12851 })
12852 })
12853 .unwrap();
12854
12855 let buffer = project
12856 .update(cx, |project, cx| {
12857 project.open_local_buffer(path!("/a/main.rs"), cx)
12858 })
12859 .await
12860 .unwrap();
12861 let editor_handle = workspace
12862 .update(cx, |workspace, window, cx| {
12863 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
12864 })
12865 .unwrap()
12866 .await
12867 .unwrap()
12868 .downcast::<Editor>()
12869 .unwrap();
12870
12871 cx.executor().start_waiting();
12872 let fake_server = fake_servers.next().await.unwrap();
12873
12874 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
12875 |params, _| async move {
12876 assert_eq!(
12877 params.text_document_position.text_document.uri,
12878 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
12879 );
12880 assert_eq!(
12881 params.text_document_position.position,
12882 lsp::Position::new(0, 21),
12883 );
12884
12885 Ok(Some(vec![lsp::TextEdit {
12886 new_text: "]".to_string(),
12887 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12888 }]))
12889 },
12890 );
12891
12892 editor_handle.update_in(cx, |editor, window, cx| {
12893 window.focus(&editor.focus_handle(cx));
12894 editor.change_selections(None, window, cx, |s| {
12895 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
12896 });
12897 editor.handle_input("{", window, cx);
12898 });
12899
12900 cx.executor().run_until_parked();
12901
12902 buffer.update(cx, |buffer, _| {
12903 assert_eq!(
12904 buffer.text(),
12905 "fn main() { let a = {5}; }",
12906 "No extra braces from on type formatting should appear in the buffer"
12907 )
12908 });
12909}
12910
12911#[gpui::test]
12912async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
12913 init_test(cx, |_| {});
12914
12915 let fs = FakeFs::new(cx.executor());
12916 fs.insert_tree(
12917 path!("/a"),
12918 json!({
12919 "main.rs": "fn main() { let a = 5; }",
12920 "other.rs": "// Test file",
12921 }),
12922 )
12923 .await;
12924
12925 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12926
12927 let server_restarts = Arc::new(AtomicUsize::new(0));
12928 let closure_restarts = Arc::clone(&server_restarts);
12929 let language_server_name = "test language server";
12930 let language_name: LanguageName = "Rust".into();
12931
12932 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12933 language_registry.add(Arc::new(Language::new(
12934 LanguageConfig {
12935 name: language_name.clone(),
12936 matcher: LanguageMatcher {
12937 path_suffixes: vec!["rs".to_string()],
12938 ..Default::default()
12939 },
12940 ..Default::default()
12941 },
12942 Some(tree_sitter_rust::LANGUAGE.into()),
12943 )));
12944 let mut fake_servers = language_registry.register_fake_lsp(
12945 "Rust",
12946 FakeLspAdapter {
12947 name: language_server_name,
12948 initialization_options: Some(json!({
12949 "testOptionValue": true
12950 })),
12951 initializer: Some(Box::new(move |fake_server| {
12952 let task_restarts = Arc::clone(&closure_restarts);
12953 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
12954 task_restarts.fetch_add(1, atomic::Ordering::Release);
12955 futures::future::ready(Ok(()))
12956 });
12957 })),
12958 ..Default::default()
12959 },
12960 );
12961
12962 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12963 let _buffer = project
12964 .update(cx, |project, cx| {
12965 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
12966 })
12967 .await
12968 .unwrap();
12969 let _fake_server = fake_servers.next().await.unwrap();
12970 update_test_language_settings(cx, |language_settings| {
12971 language_settings.languages.insert(
12972 language_name.clone(),
12973 LanguageSettingsContent {
12974 tab_size: NonZeroU32::new(8),
12975 ..Default::default()
12976 },
12977 );
12978 });
12979 cx.executor().run_until_parked();
12980 assert_eq!(
12981 server_restarts.load(atomic::Ordering::Acquire),
12982 0,
12983 "Should not restart LSP server on an unrelated change"
12984 );
12985
12986 update_test_project_settings(cx, |project_settings| {
12987 project_settings.lsp.insert(
12988 "Some other server name".into(),
12989 LspSettings {
12990 binary: None,
12991 settings: None,
12992 initialization_options: Some(json!({
12993 "some other init value": false
12994 })),
12995 enable_lsp_tasks: false,
12996 },
12997 );
12998 });
12999 cx.executor().run_until_parked();
13000 assert_eq!(
13001 server_restarts.load(atomic::Ordering::Acquire),
13002 0,
13003 "Should not restart LSP server on an unrelated LSP settings change"
13004 );
13005
13006 update_test_project_settings(cx, |project_settings| {
13007 project_settings.lsp.insert(
13008 language_server_name.into(),
13009 LspSettings {
13010 binary: None,
13011 settings: None,
13012 initialization_options: Some(json!({
13013 "anotherInitValue": false
13014 })),
13015 enable_lsp_tasks: false,
13016 },
13017 );
13018 });
13019 cx.executor().run_until_parked();
13020 assert_eq!(
13021 server_restarts.load(atomic::Ordering::Acquire),
13022 1,
13023 "Should restart LSP server on a related LSP settings change"
13024 );
13025
13026 update_test_project_settings(cx, |project_settings| {
13027 project_settings.lsp.insert(
13028 language_server_name.into(),
13029 LspSettings {
13030 binary: None,
13031 settings: None,
13032 initialization_options: Some(json!({
13033 "anotherInitValue": false
13034 })),
13035 enable_lsp_tasks: false,
13036 },
13037 );
13038 });
13039 cx.executor().run_until_parked();
13040 assert_eq!(
13041 server_restarts.load(atomic::Ordering::Acquire),
13042 1,
13043 "Should not restart LSP server on a related LSP settings change that is the same"
13044 );
13045
13046 update_test_project_settings(cx, |project_settings| {
13047 project_settings.lsp.insert(
13048 language_server_name.into(),
13049 LspSettings {
13050 binary: None,
13051 settings: None,
13052 initialization_options: None,
13053 enable_lsp_tasks: false,
13054 },
13055 );
13056 });
13057 cx.executor().run_until_parked();
13058 assert_eq!(
13059 server_restarts.load(atomic::Ordering::Acquire),
13060 2,
13061 "Should restart LSP server on another related LSP settings change"
13062 );
13063}
13064
13065#[gpui::test]
13066async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
13067 init_test(cx, |_| {});
13068
13069 let mut cx = EditorLspTestContext::new_rust(
13070 lsp::ServerCapabilities {
13071 completion_provider: Some(lsp::CompletionOptions {
13072 trigger_characters: Some(vec![".".to_string()]),
13073 resolve_provider: Some(true),
13074 ..Default::default()
13075 }),
13076 ..Default::default()
13077 },
13078 cx,
13079 )
13080 .await;
13081
13082 cx.set_state("fn main() { let a = 2ˇ; }");
13083 cx.simulate_keystroke(".");
13084 let completion_item = lsp::CompletionItem {
13085 label: "some".into(),
13086 kind: Some(lsp::CompletionItemKind::SNIPPET),
13087 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13088 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13089 kind: lsp::MarkupKind::Markdown,
13090 value: "```rust\nSome(2)\n```".to_string(),
13091 })),
13092 deprecated: Some(false),
13093 sort_text: Some("fffffff2".to_string()),
13094 filter_text: Some("some".to_string()),
13095 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13096 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13097 range: lsp::Range {
13098 start: lsp::Position {
13099 line: 0,
13100 character: 22,
13101 },
13102 end: lsp::Position {
13103 line: 0,
13104 character: 22,
13105 },
13106 },
13107 new_text: "Some(2)".to_string(),
13108 })),
13109 additional_text_edits: Some(vec![lsp::TextEdit {
13110 range: lsp::Range {
13111 start: lsp::Position {
13112 line: 0,
13113 character: 20,
13114 },
13115 end: lsp::Position {
13116 line: 0,
13117 character: 22,
13118 },
13119 },
13120 new_text: "".to_string(),
13121 }]),
13122 ..Default::default()
13123 };
13124
13125 let closure_completion_item = completion_item.clone();
13126 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13127 let task_completion_item = closure_completion_item.clone();
13128 async move {
13129 Ok(Some(lsp::CompletionResponse::Array(vec![
13130 task_completion_item,
13131 ])))
13132 }
13133 });
13134
13135 request.next().await;
13136
13137 cx.condition(|editor, _| editor.context_menu_visible())
13138 .await;
13139 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13140 editor
13141 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13142 .unwrap()
13143 });
13144 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
13145
13146 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
13147 let task_completion_item = completion_item.clone();
13148 async move { Ok(task_completion_item) }
13149 })
13150 .next()
13151 .await
13152 .unwrap();
13153 apply_additional_edits.await.unwrap();
13154 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
13155}
13156
13157#[gpui::test]
13158async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
13159 init_test(cx, |_| {});
13160
13161 let mut cx = EditorLspTestContext::new_rust(
13162 lsp::ServerCapabilities {
13163 completion_provider: Some(lsp::CompletionOptions {
13164 trigger_characters: Some(vec![".".to_string()]),
13165 resolve_provider: Some(true),
13166 ..Default::default()
13167 }),
13168 ..Default::default()
13169 },
13170 cx,
13171 )
13172 .await;
13173
13174 cx.set_state("fn main() { let a = 2ˇ; }");
13175 cx.simulate_keystroke(".");
13176
13177 let item1 = lsp::CompletionItem {
13178 label: "method id()".to_string(),
13179 filter_text: Some("id".to_string()),
13180 detail: None,
13181 documentation: None,
13182 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13183 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13184 new_text: ".id".to_string(),
13185 })),
13186 ..lsp::CompletionItem::default()
13187 };
13188
13189 let item2 = lsp::CompletionItem {
13190 label: "other".to_string(),
13191 filter_text: Some("other".to_string()),
13192 detail: None,
13193 documentation: None,
13194 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13195 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13196 new_text: ".other".to_string(),
13197 })),
13198 ..lsp::CompletionItem::default()
13199 };
13200
13201 let item1 = item1.clone();
13202 cx.set_request_handler::<lsp::request::Completion, _, _>({
13203 let item1 = item1.clone();
13204 move |_, _, _| {
13205 let item1 = item1.clone();
13206 let item2 = item2.clone();
13207 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
13208 }
13209 })
13210 .next()
13211 .await;
13212
13213 cx.condition(|editor, _| editor.context_menu_visible())
13214 .await;
13215 cx.update_editor(|editor, _, _| {
13216 let context_menu = editor.context_menu.borrow_mut();
13217 let context_menu = context_menu
13218 .as_ref()
13219 .expect("Should have the context menu deployed");
13220 match context_menu {
13221 CodeContextMenu::Completions(completions_menu) => {
13222 let completions = completions_menu.completions.borrow_mut();
13223 assert_eq!(
13224 completions
13225 .iter()
13226 .map(|completion| &completion.label.text)
13227 .collect::<Vec<_>>(),
13228 vec!["method id()", "other"]
13229 )
13230 }
13231 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13232 }
13233 });
13234
13235 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
13236 let item1 = item1.clone();
13237 move |_, item_to_resolve, _| {
13238 let item1 = item1.clone();
13239 async move {
13240 if item1 == item_to_resolve {
13241 Ok(lsp::CompletionItem {
13242 label: "method id()".to_string(),
13243 filter_text: Some("id".to_string()),
13244 detail: Some("Now resolved!".to_string()),
13245 documentation: Some(lsp::Documentation::String("Docs".to_string())),
13246 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13247 range: lsp::Range::new(
13248 lsp::Position::new(0, 22),
13249 lsp::Position::new(0, 22),
13250 ),
13251 new_text: ".id".to_string(),
13252 })),
13253 ..lsp::CompletionItem::default()
13254 })
13255 } else {
13256 Ok(item_to_resolve)
13257 }
13258 }
13259 }
13260 })
13261 .next()
13262 .await
13263 .unwrap();
13264 cx.run_until_parked();
13265
13266 cx.update_editor(|editor, window, cx| {
13267 editor.context_menu_next(&Default::default(), window, cx);
13268 });
13269
13270 cx.update_editor(|editor, _, _| {
13271 let context_menu = editor.context_menu.borrow_mut();
13272 let context_menu = context_menu
13273 .as_ref()
13274 .expect("Should have the context menu deployed");
13275 match context_menu {
13276 CodeContextMenu::Completions(completions_menu) => {
13277 let completions = completions_menu.completions.borrow_mut();
13278 assert_eq!(
13279 completions
13280 .iter()
13281 .map(|completion| &completion.label.text)
13282 .collect::<Vec<_>>(),
13283 vec!["method id() Now resolved!", "other"],
13284 "Should update first completion label, but not second as the filter text did not match."
13285 );
13286 }
13287 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13288 }
13289 });
13290}
13291
13292#[gpui::test]
13293async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
13294 init_test(cx, |_| {});
13295
13296 let mut cx = EditorLspTestContext::new_rust(
13297 lsp::ServerCapabilities {
13298 completion_provider: Some(lsp::CompletionOptions {
13299 trigger_characters: Some(vec![".".to_string()]),
13300 resolve_provider: Some(true),
13301 ..Default::default()
13302 }),
13303 ..Default::default()
13304 },
13305 cx,
13306 )
13307 .await;
13308
13309 cx.set_state("fn main() { let a = 2ˇ; }");
13310 cx.simulate_keystroke(".");
13311
13312 let unresolved_item_1 = lsp::CompletionItem {
13313 label: "id".to_string(),
13314 filter_text: Some("id".to_string()),
13315 detail: None,
13316 documentation: None,
13317 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13318 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13319 new_text: ".id".to_string(),
13320 })),
13321 ..lsp::CompletionItem::default()
13322 };
13323 let resolved_item_1 = lsp::CompletionItem {
13324 additional_text_edits: Some(vec![lsp::TextEdit {
13325 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
13326 new_text: "!!".to_string(),
13327 }]),
13328 ..unresolved_item_1.clone()
13329 };
13330 let unresolved_item_2 = lsp::CompletionItem {
13331 label: "other".to_string(),
13332 filter_text: Some("other".to_string()),
13333 detail: None,
13334 documentation: None,
13335 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13336 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13337 new_text: ".other".to_string(),
13338 })),
13339 ..lsp::CompletionItem::default()
13340 };
13341 let resolved_item_2 = lsp::CompletionItem {
13342 additional_text_edits: Some(vec![lsp::TextEdit {
13343 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
13344 new_text: "??".to_string(),
13345 }]),
13346 ..unresolved_item_2.clone()
13347 };
13348
13349 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
13350 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
13351 cx.lsp
13352 .server
13353 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
13354 let unresolved_item_1 = unresolved_item_1.clone();
13355 let resolved_item_1 = resolved_item_1.clone();
13356 let unresolved_item_2 = unresolved_item_2.clone();
13357 let resolved_item_2 = resolved_item_2.clone();
13358 let resolve_requests_1 = resolve_requests_1.clone();
13359 let resolve_requests_2 = resolve_requests_2.clone();
13360 move |unresolved_request, _| {
13361 let unresolved_item_1 = unresolved_item_1.clone();
13362 let resolved_item_1 = resolved_item_1.clone();
13363 let unresolved_item_2 = unresolved_item_2.clone();
13364 let resolved_item_2 = resolved_item_2.clone();
13365 let resolve_requests_1 = resolve_requests_1.clone();
13366 let resolve_requests_2 = resolve_requests_2.clone();
13367 async move {
13368 if unresolved_request == unresolved_item_1 {
13369 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
13370 Ok(resolved_item_1.clone())
13371 } else if unresolved_request == unresolved_item_2 {
13372 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
13373 Ok(resolved_item_2.clone())
13374 } else {
13375 panic!("Unexpected completion item {unresolved_request:?}")
13376 }
13377 }
13378 }
13379 })
13380 .detach();
13381
13382 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13383 let unresolved_item_1 = unresolved_item_1.clone();
13384 let unresolved_item_2 = unresolved_item_2.clone();
13385 async move {
13386 Ok(Some(lsp::CompletionResponse::Array(vec![
13387 unresolved_item_1,
13388 unresolved_item_2,
13389 ])))
13390 }
13391 })
13392 .next()
13393 .await;
13394
13395 cx.condition(|editor, _| editor.context_menu_visible())
13396 .await;
13397 cx.update_editor(|editor, _, _| {
13398 let context_menu = editor.context_menu.borrow_mut();
13399 let context_menu = context_menu
13400 .as_ref()
13401 .expect("Should have the context menu deployed");
13402 match context_menu {
13403 CodeContextMenu::Completions(completions_menu) => {
13404 let completions = completions_menu.completions.borrow_mut();
13405 assert_eq!(
13406 completions
13407 .iter()
13408 .map(|completion| &completion.label.text)
13409 .collect::<Vec<_>>(),
13410 vec!["id", "other"]
13411 )
13412 }
13413 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13414 }
13415 });
13416 cx.run_until_parked();
13417
13418 cx.update_editor(|editor, window, cx| {
13419 editor.context_menu_next(&ContextMenuNext, window, cx);
13420 });
13421 cx.run_until_parked();
13422 cx.update_editor(|editor, window, cx| {
13423 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
13424 });
13425 cx.run_until_parked();
13426 cx.update_editor(|editor, window, cx| {
13427 editor.context_menu_next(&ContextMenuNext, window, cx);
13428 });
13429 cx.run_until_parked();
13430 cx.update_editor(|editor, window, cx| {
13431 editor
13432 .compose_completion(&ComposeCompletion::default(), window, cx)
13433 .expect("No task returned")
13434 })
13435 .await
13436 .expect("Completion failed");
13437 cx.run_until_parked();
13438
13439 cx.update_editor(|editor, _, cx| {
13440 assert_eq!(
13441 resolve_requests_1.load(atomic::Ordering::Acquire),
13442 1,
13443 "Should always resolve once despite multiple selections"
13444 );
13445 assert_eq!(
13446 resolve_requests_2.load(atomic::Ordering::Acquire),
13447 1,
13448 "Should always resolve once after multiple selections and applying the completion"
13449 );
13450 assert_eq!(
13451 editor.text(cx),
13452 "fn main() { let a = ??.other; }",
13453 "Should use resolved data when applying the completion"
13454 );
13455 });
13456}
13457
13458#[gpui::test]
13459async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
13460 init_test(cx, |_| {});
13461
13462 let item_0 = lsp::CompletionItem {
13463 label: "abs".into(),
13464 insert_text: Some("abs".into()),
13465 data: Some(json!({ "very": "special"})),
13466 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
13467 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
13468 lsp::InsertReplaceEdit {
13469 new_text: "abs".to_string(),
13470 insert: lsp::Range::default(),
13471 replace: lsp::Range::default(),
13472 },
13473 )),
13474 ..lsp::CompletionItem::default()
13475 };
13476 let items = iter::once(item_0.clone())
13477 .chain((11..51).map(|i| lsp::CompletionItem {
13478 label: format!("item_{}", i),
13479 insert_text: Some(format!("item_{}", i)),
13480 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
13481 ..lsp::CompletionItem::default()
13482 }))
13483 .collect::<Vec<_>>();
13484
13485 let default_commit_characters = vec!["?".to_string()];
13486 let default_data = json!({ "default": "data"});
13487 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
13488 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
13489 let default_edit_range = lsp::Range {
13490 start: lsp::Position {
13491 line: 0,
13492 character: 5,
13493 },
13494 end: lsp::Position {
13495 line: 0,
13496 character: 5,
13497 },
13498 };
13499
13500 let mut cx = EditorLspTestContext::new_rust(
13501 lsp::ServerCapabilities {
13502 completion_provider: Some(lsp::CompletionOptions {
13503 trigger_characters: Some(vec![".".to_string()]),
13504 resolve_provider: Some(true),
13505 ..Default::default()
13506 }),
13507 ..Default::default()
13508 },
13509 cx,
13510 )
13511 .await;
13512
13513 cx.set_state("fn main() { let a = 2ˇ; }");
13514 cx.simulate_keystroke(".");
13515
13516 let completion_data = default_data.clone();
13517 let completion_characters = default_commit_characters.clone();
13518 let completion_items = items.clone();
13519 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13520 let default_data = completion_data.clone();
13521 let default_commit_characters = completion_characters.clone();
13522 let items = completion_items.clone();
13523 async move {
13524 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13525 items,
13526 item_defaults: Some(lsp::CompletionListItemDefaults {
13527 data: Some(default_data.clone()),
13528 commit_characters: Some(default_commit_characters.clone()),
13529 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
13530 default_edit_range,
13531 )),
13532 insert_text_format: Some(default_insert_text_format),
13533 insert_text_mode: Some(default_insert_text_mode),
13534 }),
13535 ..lsp::CompletionList::default()
13536 })))
13537 }
13538 })
13539 .next()
13540 .await;
13541
13542 let resolved_items = Arc::new(Mutex::new(Vec::new()));
13543 cx.lsp
13544 .server
13545 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
13546 let closure_resolved_items = resolved_items.clone();
13547 move |item_to_resolve, _| {
13548 let closure_resolved_items = closure_resolved_items.clone();
13549 async move {
13550 closure_resolved_items.lock().push(item_to_resolve.clone());
13551 Ok(item_to_resolve)
13552 }
13553 }
13554 })
13555 .detach();
13556
13557 cx.condition(|editor, _| editor.context_menu_visible())
13558 .await;
13559 cx.run_until_parked();
13560 cx.update_editor(|editor, _, _| {
13561 let menu = editor.context_menu.borrow_mut();
13562 match menu.as_ref().expect("should have the completions menu") {
13563 CodeContextMenu::Completions(completions_menu) => {
13564 assert_eq!(
13565 completions_menu
13566 .entries
13567 .borrow()
13568 .iter()
13569 .map(|mat| mat.string.clone())
13570 .collect::<Vec<String>>(),
13571 items
13572 .iter()
13573 .map(|completion| completion.label.clone())
13574 .collect::<Vec<String>>()
13575 );
13576 }
13577 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
13578 }
13579 });
13580 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
13581 // with 4 from the end.
13582 assert_eq!(
13583 *resolved_items.lock(),
13584 [&items[0..16], &items[items.len() - 4..items.len()]]
13585 .concat()
13586 .iter()
13587 .cloned()
13588 .map(|mut item| {
13589 if item.data.is_none() {
13590 item.data = Some(default_data.clone());
13591 }
13592 item
13593 })
13594 .collect::<Vec<lsp::CompletionItem>>(),
13595 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
13596 );
13597 resolved_items.lock().clear();
13598
13599 cx.update_editor(|editor, window, cx| {
13600 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
13601 });
13602 cx.run_until_parked();
13603 // Completions that have already been resolved are skipped.
13604 assert_eq!(
13605 *resolved_items.lock(),
13606 items[items.len() - 16..items.len() - 4]
13607 .iter()
13608 .cloned()
13609 .map(|mut item| {
13610 if item.data.is_none() {
13611 item.data = Some(default_data.clone());
13612 }
13613 item
13614 })
13615 .collect::<Vec<lsp::CompletionItem>>()
13616 );
13617 resolved_items.lock().clear();
13618}
13619
13620#[gpui::test]
13621async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
13622 init_test(cx, |_| {});
13623
13624 let mut cx = EditorLspTestContext::new(
13625 Language::new(
13626 LanguageConfig {
13627 matcher: LanguageMatcher {
13628 path_suffixes: vec!["jsx".into()],
13629 ..Default::default()
13630 },
13631 overrides: [(
13632 "element".into(),
13633 LanguageConfigOverride {
13634 completion_query_characters: Override::Set(['-'].into_iter().collect()),
13635 ..Default::default()
13636 },
13637 )]
13638 .into_iter()
13639 .collect(),
13640 ..Default::default()
13641 },
13642 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
13643 )
13644 .with_override_query("(jsx_self_closing_element) @element")
13645 .unwrap(),
13646 lsp::ServerCapabilities {
13647 completion_provider: Some(lsp::CompletionOptions {
13648 trigger_characters: Some(vec![":".to_string()]),
13649 ..Default::default()
13650 }),
13651 ..Default::default()
13652 },
13653 cx,
13654 )
13655 .await;
13656
13657 cx.lsp
13658 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13659 Ok(Some(lsp::CompletionResponse::Array(vec![
13660 lsp::CompletionItem {
13661 label: "bg-blue".into(),
13662 ..Default::default()
13663 },
13664 lsp::CompletionItem {
13665 label: "bg-red".into(),
13666 ..Default::default()
13667 },
13668 lsp::CompletionItem {
13669 label: "bg-yellow".into(),
13670 ..Default::default()
13671 },
13672 ])))
13673 });
13674
13675 cx.set_state(r#"<p class="bgˇ" />"#);
13676
13677 // Trigger completion when typing a dash, because the dash is an extra
13678 // word character in the 'element' scope, which contains the cursor.
13679 cx.simulate_keystroke("-");
13680 cx.executor().run_until_parked();
13681 cx.update_editor(|editor, _, _| {
13682 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13683 {
13684 assert_eq!(
13685 completion_menu_entries(&menu),
13686 &["bg-red", "bg-blue", "bg-yellow"]
13687 );
13688 } else {
13689 panic!("expected completion menu to be open");
13690 }
13691 });
13692
13693 cx.simulate_keystroke("l");
13694 cx.executor().run_until_parked();
13695 cx.update_editor(|editor, _, _| {
13696 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13697 {
13698 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
13699 } else {
13700 panic!("expected completion menu to be open");
13701 }
13702 });
13703
13704 // When filtering completions, consider the character after the '-' to
13705 // be the start of a subword.
13706 cx.set_state(r#"<p class="yelˇ" />"#);
13707 cx.simulate_keystroke("l");
13708 cx.executor().run_until_parked();
13709 cx.update_editor(|editor, _, _| {
13710 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13711 {
13712 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
13713 } else {
13714 panic!("expected completion menu to be open");
13715 }
13716 });
13717}
13718
13719fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
13720 let entries = menu.entries.borrow();
13721 entries.iter().map(|mat| mat.string.clone()).collect()
13722}
13723
13724#[gpui::test]
13725async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
13726 init_test(cx, |settings| {
13727 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
13728 FormatterList(vec![Formatter::Prettier].into()),
13729 ))
13730 });
13731
13732 let fs = FakeFs::new(cx.executor());
13733 fs.insert_file(path!("/file.ts"), Default::default()).await;
13734
13735 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
13736 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13737
13738 language_registry.add(Arc::new(Language::new(
13739 LanguageConfig {
13740 name: "TypeScript".into(),
13741 matcher: LanguageMatcher {
13742 path_suffixes: vec!["ts".to_string()],
13743 ..Default::default()
13744 },
13745 ..Default::default()
13746 },
13747 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
13748 )));
13749 update_test_language_settings(cx, |settings| {
13750 settings.defaults.prettier = Some(PrettierSettings {
13751 allowed: true,
13752 ..PrettierSettings::default()
13753 });
13754 });
13755
13756 let test_plugin = "test_plugin";
13757 let _ = language_registry.register_fake_lsp(
13758 "TypeScript",
13759 FakeLspAdapter {
13760 prettier_plugins: vec![test_plugin],
13761 ..Default::default()
13762 },
13763 );
13764
13765 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
13766 let buffer = project
13767 .update(cx, |project, cx| {
13768 project.open_local_buffer(path!("/file.ts"), cx)
13769 })
13770 .await
13771 .unwrap();
13772
13773 let buffer_text = "one\ntwo\nthree\n";
13774 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13775 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
13776 editor.update_in(cx, |editor, window, cx| {
13777 editor.set_text(buffer_text, window, cx)
13778 });
13779
13780 editor
13781 .update_in(cx, |editor, window, cx| {
13782 editor.perform_format(
13783 project.clone(),
13784 FormatTrigger::Manual,
13785 FormatTarget::Buffers,
13786 window,
13787 cx,
13788 )
13789 })
13790 .unwrap()
13791 .await;
13792 assert_eq!(
13793 editor.update(cx, |editor, cx| editor.text(cx)),
13794 buffer_text.to_string() + prettier_format_suffix,
13795 "Test prettier formatting was not applied to the original buffer text",
13796 );
13797
13798 update_test_language_settings(cx, |settings| {
13799 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
13800 });
13801 let format = editor.update_in(cx, |editor, window, cx| {
13802 editor.perform_format(
13803 project.clone(),
13804 FormatTrigger::Manual,
13805 FormatTarget::Buffers,
13806 window,
13807 cx,
13808 )
13809 });
13810 format.await.unwrap();
13811 assert_eq!(
13812 editor.update(cx, |editor, cx| editor.text(cx)),
13813 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
13814 "Autoformatting (via test prettier) was not applied to the original buffer text",
13815 );
13816}
13817
13818#[gpui::test]
13819async fn test_addition_reverts(cx: &mut TestAppContext) {
13820 init_test(cx, |_| {});
13821 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13822 let base_text = indoc! {r#"
13823 struct Row;
13824 struct Row1;
13825 struct Row2;
13826
13827 struct Row4;
13828 struct Row5;
13829 struct Row6;
13830
13831 struct Row8;
13832 struct Row9;
13833 struct Row10;"#};
13834
13835 // When addition hunks are not adjacent to carets, no hunk revert is performed
13836 assert_hunk_revert(
13837 indoc! {r#"struct Row;
13838 struct Row1;
13839 struct Row1.1;
13840 struct Row1.2;
13841 struct Row2;ˇ
13842
13843 struct Row4;
13844 struct Row5;
13845 struct Row6;
13846
13847 struct Row8;
13848 ˇstruct Row9;
13849 struct Row9.1;
13850 struct Row9.2;
13851 struct Row9.3;
13852 struct Row10;"#},
13853 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
13854 indoc! {r#"struct Row;
13855 struct Row1;
13856 struct Row1.1;
13857 struct Row1.2;
13858 struct Row2;ˇ
13859
13860 struct Row4;
13861 struct Row5;
13862 struct Row6;
13863
13864 struct Row8;
13865 ˇstruct Row9;
13866 struct Row9.1;
13867 struct Row9.2;
13868 struct Row9.3;
13869 struct Row10;"#},
13870 base_text,
13871 &mut cx,
13872 );
13873 // Same for selections
13874 assert_hunk_revert(
13875 indoc! {r#"struct Row;
13876 struct Row1;
13877 struct Row2;
13878 struct Row2.1;
13879 struct Row2.2;
13880 «ˇ
13881 struct Row4;
13882 struct» Row5;
13883 «struct Row6;
13884 ˇ»
13885 struct Row9.1;
13886 struct Row9.2;
13887 struct Row9.3;
13888 struct Row8;
13889 struct Row9;
13890 struct Row10;"#},
13891 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
13892 indoc! {r#"struct Row;
13893 struct Row1;
13894 struct Row2;
13895 struct Row2.1;
13896 struct Row2.2;
13897 «ˇ
13898 struct Row4;
13899 struct» Row5;
13900 «struct Row6;
13901 ˇ»
13902 struct Row9.1;
13903 struct Row9.2;
13904 struct Row9.3;
13905 struct Row8;
13906 struct Row9;
13907 struct Row10;"#},
13908 base_text,
13909 &mut cx,
13910 );
13911
13912 // When carets and selections intersect the addition hunks, those are reverted.
13913 // Adjacent carets got merged.
13914 assert_hunk_revert(
13915 indoc! {r#"struct Row;
13916 ˇ// something on the top
13917 struct Row1;
13918 struct Row2;
13919 struct Roˇw3.1;
13920 struct Row2.2;
13921 struct Row2.3;ˇ
13922
13923 struct Row4;
13924 struct ˇRow5.1;
13925 struct Row5.2;
13926 struct «Rowˇ»5.3;
13927 struct Row5;
13928 struct Row6;
13929 ˇ
13930 struct Row9.1;
13931 struct «Rowˇ»9.2;
13932 struct «ˇRow»9.3;
13933 struct Row8;
13934 struct Row9;
13935 «ˇ// something on bottom»
13936 struct Row10;"#},
13937 vec![
13938 DiffHunkStatusKind::Added,
13939 DiffHunkStatusKind::Added,
13940 DiffHunkStatusKind::Added,
13941 DiffHunkStatusKind::Added,
13942 DiffHunkStatusKind::Added,
13943 ],
13944 indoc! {r#"struct Row;
13945 ˇstruct Row1;
13946 struct Row2;
13947 ˇ
13948 struct Row4;
13949 ˇstruct Row5;
13950 struct Row6;
13951 ˇ
13952 ˇstruct Row8;
13953 struct Row9;
13954 ˇstruct Row10;"#},
13955 base_text,
13956 &mut cx,
13957 );
13958}
13959
13960#[gpui::test]
13961async fn test_modification_reverts(cx: &mut TestAppContext) {
13962 init_test(cx, |_| {});
13963 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13964 let base_text = indoc! {r#"
13965 struct Row;
13966 struct Row1;
13967 struct Row2;
13968
13969 struct Row4;
13970 struct Row5;
13971 struct Row6;
13972
13973 struct Row8;
13974 struct Row9;
13975 struct Row10;"#};
13976
13977 // Modification hunks behave the same as the addition ones.
13978 assert_hunk_revert(
13979 indoc! {r#"struct Row;
13980 struct Row1;
13981 struct Row33;
13982 ˇ
13983 struct Row4;
13984 struct Row5;
13985 struct Row6;
13986 ˇ
13987 struct Row99;
13988 struct Row9;
13989 struct Row10;"#},
13990 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
13991 indoc! {r#"struct Row;
13992 struct Row1;
13993 struct Row33;
13994 ˇ
13995 struct Row4;
13996 struct Row5;
13997 struct Row6;
13998 ˇ
13999 struct Row99;
14000 struct Row9;
14001 struct Row10;"#},
14002 base_text,
14003 &mut cx,
14004 );
14005 assert_hunk_revert(
14006 indoc! {r#"struct Row;
14007 struct Row1;
14008 struct Row33;
14009 «ˇ
14010 struct Row4;
14011 struct» Row5;
14012 «struct Row6;
14013 ˇ»
14014 struct Row99;
14015 struct Row9;
14016 struct Row10;"#},
14017 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
14018 indoc! {r#"struct Row;
14019 struct Row1;
14020 struct Row33;
14021 «ˇ
14022 struct Row4;
14023 struct» Row5;
14024 «struct Row6;
14025 ˇ»
14026 struct Row99;
14027 struct Row9;
14028 struct Row10;"#},
14029 base_text,
14030 &mut cx,
14031 );
14032
14033 assert_hunk_revert(
14034 indoc! {r#"ˇstruct Row1.1;
14035 struct Row1;
14036 «ˇstr»uct Row22;
14037
14038 struct ˇRow44;
14039 struct Row5;
14040 struct «Rˇ»ow66;ˇ
14041
14042 «struˇ»ct Row88;
14043 struct Row9;
14044 struct Row1011;ˇ"#},
14045 vec![
14046 DiffHunkStatusKind::Modified,
14047 DiffHunkStatusKind::Modified,
14048 DiffHunkStatusKind::Modified,
14049 DiffHunkStatusKind::Modified,
14050 DiffHunkStatusKind::Modified,
14051 DiffHunkStatusKind::Modified,
14052 ],
14053 indoc! {r#"struct Row;
14054 ˇstruct Row1;
14055 struct Row2;
14056 ˇ
14057 struct Row4;
14058 ˇstruct Row5;
14059 struct Row6;
14060 ˇ
14061 struct Row8;
14062 ˇstruct Row9;
14063 struct Row10;ˇ"#},
14064 base_text,
14065 &mut cx,
14066 );
14067}
14068
14069#[gpui::test]
14070async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
14071 init_test(cx, |_| {});
14072 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14073 let base_text = indoc! {r#"
14074 one
14075
14076 two
14077 three
14078 "#};
14079
14080 cx.set_head_text(base_text);
14081 cx.set_state("\nˇ\n");
14082 cx.executor().run_until_parked();
14083 cx.update_editor(|editor, _window, cx| {
14084 editor.expand_selected_diff_hunks(cx);
14085 });
14086 cx.executor().run_until_parked();
14087 cx.update_editor(|editor, window, cx| {
14088 editor.backspace(&Default::default(), window, cx);
14089 });
14090 cx.run_until_parked();
14091 cx.assert_state_with_diff(
14092 indoc! {r#"
14093
14094 - two
14095 - threeˇ
14096 +
14097 "#}
14098 .to_string(),
14099 );
14100}
14101
14102#[gpui::test]
14103async fn test_deletion_reverts(cx: &mut TestAppContext) {
14104 init_test(cx, |_| {});
14105 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14106 let base_text = indoc! {r#"struct Row;
14107struct Row1;
14108struct Row2;
14109
14110struct Row4;
14111struct Row5;
14112struct Row6;
14113
14114struct Row8;
14115struct Row9;
14116struct Row10;"#};
14117
14118 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
14119 assert_hunk_revert(
14120 indoc! {r#"struct Row;
14121 struct Row2;
14122
14123 ˇstruct Row4;
14124 struct Row5;
14125 struct Row6;
14126 ˇ
14127 struct Row8;
14128 struct Row10;"#},
14129 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14130 indoc! {r#"struct Row;
14131 struct Row2;
14132
14133 ˇstruct Row4;
14134 struct Row5;
14135 struct Row6;
14136 ˇ
14137 struct Row8;
14138 struct Row10;"#},
14139 base_text,
14140 &mut cx,
14141 );
14142 assert_hunk_revert(
14143 indoc! {r#"struct Row;
14144 struct Row2;
14145
14146 «ˇstruct Row4;
14147 struct» Row5;
14148 «struct Row6;
14149 ˇ»
14150 struct Row8;
14151 struct Row10;"#},
14152 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14153 indoc! {r#"struct Row;
14154 struct Row2;
14155
14156 «ˇstruct Row4;
14157 struct» Row5;
14158 «struct Row6;
14159 ˇ»
14160 struct Row8;
14161 struct Row10;"#},
14162 base_text,
14163 &mut cx,
14164 );
14165
14166 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
14167 assert_hunk_revert(
14168 indoc! {r#"struct Row;
14169 ˇstruct Row2;
14170
14171 struct Row4;
14172 struct Row5;
14173 struct Row6;
14174
14175 struct Row8;ˇ
14176 struct Row10;"#},
14177 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14178 indoc! {r#"struct Row;
14179 struct Row1;
14180 ˇstruct Row2;
14181
14182 struct Row4;
14183 struct Row5;
14184 struct Row6;
14185
14186 struct Row8;ˇ
14187 struct Row9;
14188 struct Row10;"#},
14189 base_text,
14190 &mut cx,
14191 );
14192 assert_hunk_revert(
14193 indoc! {r#"struct Row;
14194 struct Row2«ˇ;
14195 struct Row4;
14196 struct» Row5;
14197 «struct Row6;
14198
14199 struct Row8;ˇ»
14200 struct Row10;"#},
14201 vec![
14202 DiffHunkStatusKind::Deleted,
14203 DiffHunkStatusKind::Deleted,
14204 DiffHunkStatusKind::Deleted,
14205 ],
14206 indoc! {r#"struct Row;
14207 struct Row1;
14208 struct Row2«ˇ;
14209
14210 struct Row4;
14211 struct» Row5;
14212 «struct Row6;
14213
14214 struct Row8;ˇ»
14215 struct Row9;
14216 struct Row10;"#},
14217 base_text,
14218 &mut cx,
14219 );
14220}
14221
14222#[gpui::test]
14223async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
14224 init_test(cx, |_| {});
14225
14226 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
14227 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
14228 let base_text_3 =
14229 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
14230
14231 let text_1 = edit_first_char_of_every_line(base_text_1);
14232 let text_2 = edit_first_char_of_every_line(base_text_2);
14233 let text_3 = edit_first_char_of_every_line(base_text_3);
14234
14235 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
14236 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
14237 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
14238
14239 let multibuffer = cx.new(|cx| {
14240 let mut multibuffer = MultiBuffer::new(ReadWrite);
14241 multibuffer.push_excerpts(
14242 buffer_1.clone(),
14243 [
14244 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14245 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14246 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14247 ],
14248 cx,
14249 );
14250 multibuffer.push_excerpts(
14251 buffer_2.clone(),
14252 [
14253 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14254 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14255 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14256 ],
14257 cx,
14258 );
14259 multibuffer.push_excerpts(
14260 buffer_3.clone(),
14261 [
14262 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14263 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14264 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14265 ],
14266 cx,
14267 );
14268 multibuffer
14269 });
14270
14271 let fs = FakeFs::new(cx.executor());
14272 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
14273 let (editor, cx) = cx
14274 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
14275 editor.update_in(cx, |editor, _window, cx| {
14276 for (buffer, diff_base) in [
14277 (buffer_1.clone(), base_text_1),
14278 (buffer_2.clone(), base_text_2),
14279 (buffer_3.clone(), base_text_3),
14280 ] {
14281 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
14282 editor
14283 .buffer
14284 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
14285 }
14286 });
14287 cx.executor().run_until_parked();
14288
14289 editor.update_in(cx, |editor, window, cx| {
14290 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}");
14291 editor.select_all(&SelectAll, window, cx);
14292 editor.git_restore(&Default::default(), window, cx);
14293 });
14294 cx.executor().run_until_parked();
14295
14296 // When all ranges are selected, all buffer hunks are reverted.
14297 editor.update(cx, |editor, cx| {
14298 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");
14299 });
14300 buffer_1.update(cx, |buffer, _| {
14301 assert_eq!(buffer.text(), base_text_1);
14302 });
14303 buffer_2.update(cx, |buffer, _| {
14304 assert_eq!(buffer.text(), base_text_2);
14305 });
14306 buffer_3.update(cx, |buffer, _| {
14307 assert_eq!(buffer.text(), base_text_3);
14308 });
14309
14310 editor.update_in(cx, |editor, window, cx| {
14311 editor.undo(&Default::default(), window, cx);
14312 });
14313
14314 editor.update_in(cx, |editor, window, cx| {
14315 editor.change_selections(None, window, cx, |s| {
14316 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
14317 });
14318 editor.git_restore(&Default::default(), window, cx);
14319 });
14320
14321 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
14322 // but not affect buffer_2 and its related excerpts.
14323 editor.update(cx, |editor, cx| {
14324 assert_eq!(
14325 editor.text(cx),
14326 "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}"
14327 );
14328 });
14329 buffer_1.update(cx, |buffer, _| {
14330 assert_eq!(buffer.text(), base_text_1);
14331 });
14332 buffer_2.update(cx, |buffer, _| {
14333 assert_eq!(
14334 buffer.text(),
14335 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
14336 );
14337 });
14338 buffer_3.update(cx, |buffer, _| {
14339 assert_eq!(
14340 buffer.text(),
14341 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
14342 );
14343 });
14344
14345 fn edit_first_char_of_every_line(text: &str) -> String {
14346 text.split('\n')
14347 .map(|line| format!("X{}", &line[1..]))
14348 .collect::<Vec<_>>()
14349 .join("\n")
14350 }
14351}
14352
14353#[gpui::test]
14354async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
14355 init_test(cx, |_| {});
14356
14357 let cols = 4;
14358 let rows = 10;
14359 let sample_text_1 = sample_text(rows, cols, 'a');
14360 assert_eq!(
14361 sample_text_1,
14362 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
14363 );
14364 let sample_text_2 = sample_text(rows, cols, 'l');
14365 assert_eq!(
14366 sample_text_2,
14367 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
14368 );
14369 let sample_text_3 = sample_text(rows, cols, 'v');
14370 assert_eq!(
14371 sample_text_3,
14372 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
14373 );
14374
14375 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
14376 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
14377 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
14378
14379 let multi_buffer = cx.new(|cx| {
14380 let mut multibuffer = MultiBuffer::new(ReadWrite);
14381 multibuffer.push_excerpts(
14382 buffer_1.clone(),
14383 [
14384 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14385 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14386 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14387 ],
14388 cx,
14389 );
14390 multibuffer.push_excerpts(
14391 buffer_2.clone(),
14392 [
14393 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14394 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14395 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14396 ],
14397 cx,
14398 );
14399 multibuffer.push_excerpts(
14400 buffer_3.clone(),
14401 [
14402 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14403 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14404 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14405 ],
14406 cx,
14407 );
14408 multibuffer
14409 });
14410
14411 let fs = FakeFs::new(cx.executor());
14412 fs.insert_tree(
14413 "/a",
14414 json!({
14415 "main.rs": sample_text_1,
14416 "other.rs": sample_text_2,
14417 "lib.rs": sample_text_3,
14418 }),
14419 )
14420 .await;
14421 let project = Project::test(fs, ["/a".as_ref()], cx).await;
14422 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14423 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14424 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
14425 Editor::new(
14426 EditorMode::Full,
14427 multi_buffer,
14428 Some(project.clone()),
14429 window,
14430 cx,
14431 )
14432 });
14433 let multibuffer_item_id = workspace
14434 .update(cx, |workspace, window, cx| {
14435 assert!(
14436 workspace.active_item(cx).is_none(),
14437 "active item should be None before the first item is added"
14438 );
14439 workspace.add_item_to_active_pane(
14440 Box::new(multi_buffer_editor.clone()),
14441 None,
14442 true,
14443 window,
14444 cx,
14445 );
14446 let active_item = workspace
14447 .active_item(cx)
14448 .expect("should have an active item after adding the multi buffer");
14449 assert!(
14450 !active_item.is_singleton(cx),
14451 "A multi buffer was expected to active after adding"
14452 );
14453 active_item.item_id()
14454 })
14455 .unwrap();
14456 cx.executor().run_until_parked();
14457
14458 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14459 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14460 s.select_ranges(Some(1..2))
14461 });
14462 editor.open_excerpts(&OpenExcerpts, window, cx);
14463 });
14464 cx.executor().run_until_parked();
14465 let first_item_id = workspace
14466 .update(cx, |workspace, window, cx| {
14467 let active_item = workspace
14468 .active_item(cx)
14469 .expect("should have an active item after navigating into the 1st buffer");
14470 let first_item_id = active_item.item_id();
14471 assert_ne!(
14472 first_item_id, multibuffer_item_id,
14473 "Should navigate into the 1st buffer and activate it"
14474 );
14475 assert!(
14476 active_item.is_singleton(cx),
14477 "New active item should be a singleton buffer"
14478 );
14479 assert_eq!(
14480 active_item
14481 .act_as::<Editor>(cx)
14482 .expect("should have navigated into an editor for the 1st buffer")
14483 .read(cx)
14484 .text(cx),
14485 sample_text_1
14486 );
14487
14488 workspace
14489 .go_back(workspace.active_pane().downgrade(), window, cx)
14490 .detach_and_log_err(cx);
14491
14492 first_item_id
14493 })
14494 .unwrap();
14495 cx.executor().run_until_parked();
14496 workspace
14497 .update(cx, |workspace, _, cx| {
14498 let active_item = workspace
14499 .active_item(cx)
14500 .expect("should have an active item after navigating back");
14501 assert_eq!(
14502 active_item.item_id(),
14503 multibuffer_item_id,
14504 "Should navigate back to the multi buffer"
14505 );
14506 assert!(!active_item.is_singleton(cx));
14507 })
14508 .unwrap();
14509
14510 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14511 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14512 s.select_ranges(Some(39..40))
14513 });
14514 editor.open_excerpts(&OpenExcerpts, window, cx);
14515 });
14516 cx.executor().run_until_parked();
14517 let second_item_id = workspace
14518 .update(cx, |workspace, window, cx| {
14519 let active_item = workspace
14520 .active_item(cx)
14521 .expect("should have an active item after navigating into the 2nd buffer");
14522 let second_item_id = active_item.item_id();
14523 assert_ne!(
14524 second_item_id, multibuffer_item_id,
14525 "Should navigate away from the multibuffer"
14526 );
14527 assert_ne!(
14528 second_item_id, first_item_id,
14529 "Should navigate into the 2nd buffer and activate it"
14530 );
14531 assert!(
14532 active_item.is_singleton(cx),
14533 "New active item should be a singleton buffer"
14534 );
14535 assert_eq!(
14536 active_item
14537 .act_as::<Editor>(cx)
14538 .expect("should have navigated into an editor")
14539 .read(cx)
14540 .text(cx),
14541 sample_text_2
14542 );
14543
14544 workspace
14545 .go_back(workspace.active_pane().downgrade(), window, cx)
14546 .detach_and_log_err(cx);
14547
14548 second_item_id
14549 })
14550 .unwrap();
14551 cx.executor().run_until_parked();
14552 workspace
14553 .update(cx, |workspace, _, cx| {
14554 let active_item = workspace
14555 .active_item(cx)
14556 .expect("should have an active item after navigating back from the 2nd buffer");
14557 assert_eq!(
14558 active_item.item_id(),
14559 multibuffer_item_id,
14560 "Should navigate back from the 2nd buffer to the multi buffer"
14561 );
14562 assert!(!active_item.is_singleton(cx));
14563 })
14564 .unwrap();
14565
14566 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14567 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14568 s.select_ranges(Some(70..70))
14569 });
14570 editor.open_excerpts(&OpenExcerpts, window, cx);
14571 });
14572 cx.executor().run_until_parked();
14573 workspace
14574 .update(cx, |workspace, window, cx| {
14575 let active_item = workspace
14576 .active_item(cx)
14577 .expect("should have an active item after navigating into the 3rd buffer");
14578 let third_item_id = active_item.item_id();
14579 assert_ne!(
14580 third_item_id, multibuffer_item_id,
14581 "Should navigate into the 3rd buffer and activate it"
14582 );
14583 assert_ne!(third_item_id, first_item_id);
14584 assert_ne!(third_item_id, second_item_id);
14585 assert!(
14586 active_item.is_singleton(cx),
14587 "New active item should be a singleton buffer"
14588 );
14589 assert_eq!(
14590 active_item
14591 .act_as::<Editor>(cx)
14592 .expect("should have navigated into an editor")
14593 .read(cx)
14594 .text(cx),
14595 sample_text_3
14596 );
14597
14598 workspace
14599 .go_back(workspace.active_pane().downgrade(), window, cx)
14600 .detach_and_log_err(cx);
14601 })
14602 .unwrap();
14603 cx.executor().run_until_parked();
14604 workspace
14605 .update(cx, |workspace, _, cx| {
14606 let active_item = workspace
14607 .active_item(cx)
14608 .expect("should have an active item after navigating back from the 3rd buffer");
14609 assert_eq!(
14610 active_item.item_id(),
14611 multibuffer_item_id,
14612 "Should navigate back from the 3rd buffer to the multi buffer"
14613 );
14614 assert!(!active_item.is_singleton(cx));
14615 })
14616 .unwrap();
14617}
14618
14619#[gpui::test]
14620async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14621 init_test(cx, |_| {});
14622
14623 let mut cx = EditorTestContext::new(cx).await;
14624
14625 let diff_base = r#"
14626 use some::mod;
14627
14628 const A: u32 = 42;
14629
14630 fn main() {
14631 println!("hello");
14632
14633 println!("world");
14634 }
14635 "#
14636 .unindent();
14637
14638 cx.set_state(
14639 &r#"
14640 use some::modified;
14641
14642 ˇ
14643 fn main() {
14644 println!("hello there");
14645
14646 println!("around the");
14647 println!("world");
14648 }
14649 "#
14650 .unindent(),
14651 );
14652
14653 cx.set_head_text(&diff_base);
14654 executor.run_until_parked();
14655
14656 cx.update_editor(|editor, window, cx| {
14657 editor.go_to_next_hunk(&GoToHunk, window, cx);
14658 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14659 });
14660 executor.run_until_parked();
14661 cx.assert_state_with_diff(
14662 r#"
14663 use some::modified;
14664
14665
14666 fn main() {
14667 - println!("hello");
14668 + ˇ println!("hello there");
14669
14670 println!("around the");
14671 println!("world");
14672 }
14673 "#
14674 .unindent(),
14675 );
14676
14677 cx.update_editor(|editor, window, cx| {
14678 for _ in 0..2 {
14679 editor.go_to_next_hunk(&GoToHunk, window, cx);
14680 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14681 }
14682 });
14683 executor.run_until_parked();
14684 cx.assert_state_with_diff(
14685 r#"
14686 - use some::mod;
14687 + ˇuse some::modified;
14688
14689
14690 fn main() {
14691 - println!("hello");
14692 + println!("hello there");
14693
14694 + println!("around the");
14695 println!("world");
14696 }
14697 "#
14698 .unindent(),
14699 );
14700
14701 cx.update_editor(|editor, window, cx| {
14702 editor.go_to_next_hunk(&GoToHunk, window, cx);
14703 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14704 });
14705 executor.run_until_parked();
14706 cx.assert_state_with_diff(
14707 r#"
14708 - use some::mod;
14709 + use some::modified;
14710
14711 - const A: u32 = 42;
14712 ˇ
14713 fn main() {
14714 - println!("hello");
14715 + println!("hello there");
14716
14717 + println!("around the");
14718 println!("world");
14719 }
14720 "#
14721 .unindent(),
14722 );
14723
14724 cx.update_editor(|editor, window, cx| {
14725 editor.cancel(&Cancel, window, cx);
14726 });
14727
14728 cx.assert_state_with_diff(
14729 r#"
14730 use some::modified;
14731
14732 ˇ
14733 fn main() {
14734 println!("hello there");
14735
14736 println!("around the");
14737 println!("world");
14738 }
14739 "#
14740 .unindent(),
14741 );
14742}
14743
14744#[gpui::test]
14745async fn test_diff_base_change_with_expanded_diff_hunks(
14746 executor: BackgroundExecutor,
14747 cx: &mut TestAppContext,
14748) {
14749 init_test(cx, |_| {});
14750
14751 let mut cx = EditorTestContext::new(cx).await;
14752
14753 let diff_base = r#"
14754 use some::mod1;
14755 use some::mod2;
14756
14757 const A: u32 = 42;
14758 const B: u32 = 42;
14759 const C: u32 = 42;
14760
14761 fn main() {
14762 println!("hello");
14763
14764 println!("world");
14765 }
14766 "#
14767 .unindent();
14768
14769 cx.set_state(
14770 &r#"
14771 use some::mod2;
14772
14773 const A: u32 = 42;
14774 const C: u32 = 42;
14775
14776 fn main(ˇ) {
14777 //println!("hello");
14778
14779 println!("world");
14780 //
14781 //
14782 }
14783 "#
14784 .unindent(),
14785 );
14786
14787 cx.set_head_text(&diff_base);
14788 executor.run_until_parked();
14789
14790 cx.update_editor(|editor, window, cx| {
14791 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14792 });
14793 executor.run_until_parked();
14794 cx.assert_state_with_diff(
14795 r#"
14796 - use some::mod1;
14797 use some::mod2;
14798
14799 const A: u32 = 42;
14800 - const B: u32 = 42;
14801 const C: u32 = 42;
14802
14803 fn main(ˇ) {
14804 - println!("hello");
14805 + //println!("hello");
14806
14807 println!("world");
14808 + //
14809 + //
14810 }
14811 "#
14812 .unindent(),
14813 );
14814
14815 cx.set_head_text("new diff base!");
14816 executor.run_until_parked();
14817 cx.assert_state_with_diff(
14818 r#"
14819 - new diff base!
14820 + use some::mod2;
14821 +
14822 + const A: u32 = 42;
14823 + const C: u32 = 42;
14824 +
14825 + fn main(ˇ) {
14826 + //println!("hello");
14827 +
14828 + println!("world");
14829 + //
14830 + //
14831 + }
14832 "#
14833 .unindent(),
14834 );
14835}
14836
14837#[gpui::test]
14838async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
14839 init_test(cx, |_| {});
14840
14841 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
14842 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
14843 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
14844 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
14845 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
14846 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
14847
14848 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
14849 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
14850 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
14851
14852 let multi_buffer = cx.new(|cx| {
14853 let mut multibuffer = MultiBuffer::new(ReadWrite);
14854 multibuffer.push_excerpts(
14855 buffer_1.clone(),
14856 [
14857 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14858 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14859 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
14860 ],
14861 cx,
14862 );
14863 multibuffer.push_excerpts(
14864 buffer_2.clone(),
14865 [
14866 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14867 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14868 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
14869 ],
14870 cx,
14871 );
14872 multibuffer.push_excerpts(
14873 buffer_3.clone(),
14874 [
14875 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14876 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14877 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
14878 ],
14879 cx,
14880 );
14881 multibuffer
14882 });
14883
14884 let editor =
14885 cx.add_window(|window, cx| Editor::new(EditorMode::Full, multi_buffer, None, window, cx));
14886 editor
14887 .update(cx, |editor, _window, cx| {
14888 for (buffer, diff_base) in [
14889 (buffer_1.clone(), file_1_old),
14890 (buffer_2.clone(), file_2_old),
14891 (buffer_3.clone(), file_3_old),
14892 ] {
14893 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
14894 editor
14895 .buffer
14896 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
14897 }
14898 })
14899 .unwrap();
14900
14901 let mut cx = EditorTestContext::for_editor(editor, cx).await;
14902 cx.run_until_parked();
14903
14904 cx.assert_editor_state(
14905 &"
14906 ˇaaa
14907 ccc
14908 ddd
14909
14910 ggg
14911 hhh
14912
14913
14914 lll
14915 mmm
14916 NNN
14917
14918 qqq
14919 rrr
14920
14921 uuu
14922 111
14923 222
14924 333
14925
14926 666
14927 777
14928
14929 000
14930 !!!"
14931 .unindent(),
14932 );
14933
14934 cx.update_editor(|editor, window, cx| {
14935 editor.select_all(&SelectAll, window, cx);
14936 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14937 });
14938 cx.executor().run_until_parked();
14939
14940 cx.assert_state_with_diff(
14941 "
14942 «aaa
14943 - bbb
14944 ccc
14945 ddd
14946
14947 ggg
14948 hhh
14949
14950
14951 lll
14952 mmm
14953 - nnn
14954 + NNN
14955
14956 qqq
14957 rrr
14958
14959 uuu
14960 111
14961 222
14962 333
14963
14964 + 666
14965 777
14966
14967 000
14968 !!!ˇ»"
14969 .unindent(),
14970 );
14971}
14972
14973#[gpui::test]
14974async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
14975 init_test(cx, |_| {});
14976
14977 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
14978 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
14979
14980 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
14981 let multi_buffer = cx.new(|cx| {
14982 let mut multibuffer = MultiBuffer::new(ReadWrite);
14983 multibuffer.push_excerpts(
14984 buffer.clone(),
14985 [
14986 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
14987 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
14988 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
14989 ],
14990 cx,
14991 );
14992 multibuffer
14993 });
14994
14995 let editor =
14996 cx.add_window(|window, cx| Editor::new(EditorMode::Full, multi_buffer, None, window, cx));
14997 editor
14998 .update(cx, |editor, _window, cx| {
14999 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
15000 editor
15001 .buffer
15002 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
15003 })
15004 .unwrap();
15005
15006 let mut cx = EditorTestContext::for_editor(editor, cx).await;
15007 cx.run_until_parked();
15008
15009 cx.update_editor(|editor, window, cx| {
15010 editor.expand_all_diff_hunks(&Default::default(), window, cx)
15011 });
15012 cx.executor().run_until_parked();
15013
15014 // When the start of a hunk coincides with the start of its excerpt,
15015 // the hunk is expanded. When the start of a a hunk is earlier than
15016 // the start of its excerpt, the hunk is not expanded.
15017 cx.assert_state_with_diff(
15018 "
15019 ˇaaa
15020 - bbb
15021 + BBB
15022
15023 - ddd
15024 - eee
15025 + DDD
15026 + EEE
15027 fff
15028
15029 iii
15030 "
15031 .unindent(),
15032 );
15033}
15034
15035#[gpui::test]
15036async fn test_edits_around_expanded_insertion_hunks(
15037 executor: BackgroundExecutor,
15038 cx: &mut TestAppContext,
15039) {
15040 init_test(cx, |_| {});
15041
15042 let mut cx = EditorTestContext::new(cx).await;
15043
15044 let diff_base = r#"
15045 use some::mod1;
15046 use some::mod2;
15047
15048 const A: u32 = 42;
15049
15050 fn main() {
15051 println!("hello");
15052
15053 println!("world");
15054 }
15055 "#
15056 .unindent();
15057 executor.run_until_parked();
15058 cx.set_state(
15059 &r#"
15060 use some::mod1;
15061 use some::mod2;
15062
15063 const A: u32 = 42;
15064 const B: u32 = 42;
15065 const C: u32 = 42;
15066 ˇ
15067
15068 fn main() {
15069 println!("hello");
15070
15071 println!("world");
15072 }
15073 "#
15074 .unindent(),
15075 );
15076
15077 cx.set_head_text(&diff_base);
15078 executor.run_until_parked();
15079
15080 cx.update_editor(|editor, window, cx| {
15081 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15082 });
15083 executor.run_until_parked();
15084
15085 cx.assert_state_with_diff(
15086 r#"
15087 use some::mod1;
15088 use some::mod2;
15089
15090 const A: u32 = 42;
15091 + const B: u32 = 42;
15092 + const C: u32 = 42;
15093 + ˇ
15094
15095 fn main() {
15096 println!("hello");
15097
15098 println!("world");
15099 }
15100 "#
15101 .unindent(),
15102 );
15103
15104 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
15105 executor.run_until_parked();
15106
15107 cx.assert_state_with_diff(
15108 r#"
15109 use some::mod1;
15110 use some::mod2;
15111
15112 const A: u32 = 42;
15113 + const B: u32 = 42;
15114 + const C: u32 = 42;
15115 + const D: u32 = 42;
15116 + ˇ
15117
15118 fn main() {
15119 println!("hello");
15120
15121 println!("world");
15122 }
15123 "#
15124 .unindent(),
15125 );
15126
15127 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
15128 executor.run_until_parked();
15129
15130 cx.assert_state_with_diff(
15131 r#"
15132 use some::mod1;
15133 use some::mod2;
15134
15135 const A: u32 = 42;
15136 + const B: u32 = 42;
15137 + const C: u32 = 42;
15138 + const D: u32 = 42;
15139 + const E: u32 = 42;
15140 + ˇ
15141
15142 fn main() {
15143 println!("hello");
15144
15145 println!("world");
15146 }
15147 "#
15148 .unindent(),
15149 );
15150
15151 cx.update_editor(|editor, window, cx| {
15152 editor.delete_line(&DeleteLine, window, cx);
15153 });
15154 executor.run_until_parked();
15155
15156 cx.assert_state_with_diff(
15157 r#"
15158 use some::mod1;
15159 use some::mod2;
15160
15161 const A: u32 = 42;
15162 + const B: u32 = 42;
15163 + const C: u32 = 42;
15164 + const D: u32 = 42;
15165 + const E: u32 = 42;
15166 ˇ
15167 fn main() {
15168 println!("hello");
15169
15170 println!("world");
15171 }
15172 "#
15173 .unindent(),
15174 );
15175
15176 cx.update_editor(|editor, window, cx| {
15177 editor.move_up(&MoveUp, window, cx);
15178 editor.delete_line(&DeleteLine, window, cx);
15179 editor.move_up(&MoveUp, window, cx);
15180 editor.delete_line(&DeleteLine, window, cx);
15181 editor.move_up(&MoveUp, window, cx);
15182 editor.delete_line(&DeleteLine, window, cx);
15183 });
15184 executor.run_until_parked();
15185 cx.assert_state_with_diff(
15186 r#"
15187 use some::mod1;
15188 use some::mod2;
15189
15190 const A: u32 = 42;
15191 + const B: u32 = 42;
15192 ˇ
15193 fn main() {
15194 println!("hello");
15195
15196 println!("world");
15197 }
15198 "#
15199 .unindent(),
15200 );
15201
15202 cx.update_editor(|editor, window, cx| {
15203 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
15204 editor.delete_line(&DeleteLine, window, cx);
15205 });
15206 executor.run_until_parked();
15207 cx.assert_state_with_diff(
15208 r#"
15209 ˇ
15210 fn main() {
15211 println!("hello");
15212
15213 println!("world");
15214 }
15215 "#
15216 .unindent(),
15217 );
15218}
15219
15220#[gpui::test]
15221async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
15222 init_test(cx, |_| {});
15223
15224 let mut cx = EditorTestContext::new(cx).await;
15225 cx.set_head_text(indoc! { "
15226 one
15227 two
15228 three
15229 four
15230 five
15231 "
15232 });
15233 cx.set_state(indoc! { "
15234 one
15235 ˇthree
15236 five
15237 "});
15238 cx.run_until_parked();
15239 cx.update_editor(|editor, window, cx| {
15240 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15241 });
15242 cx.assert_state_with_diff(
15243 indoc! { "
15244 one
15245 - two
15246 ˇthree
15247 - four
15248 five
15249 "}
15250 .to_string(),
15251 );
15252 cx.update_editor(|editor, window, cx| {
15253 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15254 });
15255
15256 cx.assert_state_with_diff(
15257 indoc! { "
15258 one
15259 ˇthree
15260 five
15261 "}
15262 .to_string(),
15263 );
15264
15265 cx.set_state(indoc! { "
15266 one
15267 ˇTWO
15268 three
15269 four
15270 five
15271 "});
15272 cx.run_until_parked();
15273 cx.update_editor(|editor, window, cx| {
15274 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15275 });
15276
15277 cx.assert_state_with_diff(
15278 indoc! { "
15279 one
15280 - two
15281 + ˇTWO
15282 three
15283 four
15284 five
15285 "}
15286 .to_string(),
15287 );
15288 cx.update_editor(|editor, window, cx| {
15289 editor.move_up(&Default::default(), window, cx);
15290 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15291 });
15292 cx.assert_state_with_diff(
15293 indoc! { "
15294 one
15295 ˇTWO
15296 three
15297 four
15298 five
15299 "}
15300 .to_string(),
15301 );
15302}
15303
15304#[gpui::test]
15305async fn test_edits_around_expanded_deletion_hunks(
15306 executor: BackgroundExecutor,
15307 cx: &mut TestAppContext,
15308) {
15309 init_test(cx, |_| {});
15310
15311 let mut cx = EditorTestContext::new(cx).await;
15312
15313 let diff_base = r#"
15314 use some::mod1;
15315 use some::mod2;
15316
15317 const A: u32 = 42;
15318 const B: u32 = 42;
15319 const C: u32 = 42;
15320
15321
15322 fn main() {
15323 println!("hello");
15324
15325 println!("world");
15326 }
15327 "#
15328 .unindent();
15329 executor.run_until_parked();
15330 cx.set_state(
15331 &r#"
15332 use some::mod1;
15333 use some::mod2;
15334
15335 ˇconst B: u32 = 42;
15336 const C: u32 = 42;
15337
15338
15339 fn main() {
15340 println!("hello");
15341
15342 println!("world");
15343 }
15344 "#
15345 .unindent(),
15346 );
15347
15348 cx.set_head_text(&diff_base);
15349 executor.run_until_parked();
15350
15351 cx.update_editor(|editor, window, cx| {
15352 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15353 });
15354 executor.run_until_parked();
15355
15356 cx.assert_state_with_diff(
15357 r#"
15358 use some::mod1;
15359 use some::mod2;
15360
15361 - const A: u32 = 42;
15362 ˇconst B: u32 = 42;
15363 const C: u32 = 42;
15364
15365
15366 fn main() {
15367 println!("hello");
15368
15369 println!("world");
15370 }
15371 "#
15372 .unindent(),
15373 );
15374
15375 cx.update_editor(|editor, window, cx| {
15376 editor.delete_line(&DeleteLine, window, cx);
15377 });
15378 executor.run_until_parked();
15379 cx.assert_state_with_diff(
15380 r#"
15381 use some::mod1;
15382 use some::mod2;
15383
15384 - const A: u32 = 42;
15385 - const B: u32 = 42;
15386 ˇconst C: u32 = 42;
15387
15388
15389 fn main() {
15390 println!("hello");
15391
15392 println!("world");
15393 }
15394 "#
15395 .unindent(),
15396 );
15397
15398 cx.update_editor(|editor, window, cx| {
15399 editor.delete_line(&DeleteLine, window, cx);
15400 });
15401 executor.run_until_parked();
15402 cx.assert_state_with_diff(
15403 r#"
15404 use some::mod1;
15405 use some::mod2;
15406
15407 - const A: u32 = 42;
15408 - const B: u32 = 42;
15409 - const C: u32 = 42;
15410 ˇ
15411
15412 fn main() {
15413 println!("hello");
15414
15415 println!("world");
15416 }
15417 "#
15418 .unindent(),
15419 );
15420
15421 cx.update_editor(|editor, window, cx| {
15422 editor.handle_input("replacement", window, cx);
15423 });
15424 executor.run_until_parked();
15425 cx.assert_state_with_diff(
15426 r#"
15427 use some::mod1;
15428 use some::mod2;
15429
15430 - const A: u32 = 42;
15431 - const B: u32 = 42;
15432 - const C: u32 = 42;
15433 -
15434 + replacementˇ
15435
15436 fn main() {
15437 println!("hello");
15438
15439 println!("world");
15440 }
15441 "#
15442 .unindent(),
15443 );
15444}
15445
15446#[gpui::test]
15447async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15448 init_test(cx, |_| {});
15449
15450 let mut cx = EditorTestContext::new(cx).await;
15451
15452 let base_text = r#"
15453 one
15454 two
15455 three
15456 four
15457 five
15458 "#
15459 .unindent();
15460 executor.run_until_parked();
15461 cx.set_state(
15462 &r#"
15463 one
15464 two
15465 fˇour
15466 five
15467 "#
15468 .unindent(),
15469 );
15470
15471 cx.set_head_text(&base_text);
15472 executor.run_until_parked();
15473
15474 cx.update_editor(|editor, window, cx| {
15475 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15476 });
15477 executor.run_until_parked();
15478
15479 cx.assert_state_with_diff(
15480 r#"
15481 one
15482 two
15483 - three
15484 fˇour
15485 five
15486 "#
15487 .unindent(),
15488 );
15489
15490 cx.update_editor(|editor, window, cx| {
15491 editor.backspace(&Backspace, window, cx);
15492 editor.backspace(&Backspace, window, cx);
15493 });
15494 executor.run_until_parked();
15495 cx.assert_state_with_diff(
15496 r#"
15497 one
15498 two
15499 - threeˇ
15500 - four
15501 + our
15502 five
15503 "#
15504 .unindent(),
15505 );
15506}
15507
15508#[gpui::test]
15509async fn test_edit_after_expanded_modification_hunk(
15510 executor: BackgroundExecutor,
15511 cx: &mut TestAppContext,
15512) {
15513 init_test(cx, |_| {});
15514
15515 let mut cx = EditorTestContext::new(cx).await;
15516
15517 let diff_base = r#"
15518 use some::mod1;
15519 use some::mod2;
15520
15521 const A: u32 = 42;
15522 const B: u32 = 42;
15523 const C: u32 = 42;
15524 const D: u32 = 42;
15525
15526
15527 fn main() {
15528 println!("hello");
15529
15530 println!("world");
15531 }"#
15532 .unindent();
15533
15534 cx.set_state(
15535 &r#"
15536 use some::mod1;
15537 use some::mod2;
15538
15539 const A: u32 = 42;
15540 const B: u32 = 42;
15541 const C: u32 = 43ˇ
15542 const D: u32 = 42;
15543
15544
15545 fn main() {
15546 println!("hello");
15547
15548 println!("world");
15549 }"#
15550 .unindent(),
15551 );
15552
15553 cx.set_head_text(&diff_base);
15554 executor.run_until_parked();
15555 cx.update_editor(|editor, window, cx| {
15556 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15557 });
15558 executor.run_until_parked();
15559
15560 cx.assert_state_with_diff(
15561 r#"
15562 use some::mod1;
15563 use some::mod2;
15564
15565 const A: u32 = 42;
15566 const B: u32 = 42;
15567 - const C: u32 = 42;
15568 + const C: u32 = 43ˇ
15569 const D: u32 = 42;
15570
15571
15572 fn main() {
15573 println!("hello");
15574
15575 println!("world");
15576 }"#
15577 .unindent(),
15578 );
15579
15580 cx.update_editor(|editor, window, cx| {
15581 editor.handle_input("\nnew_line\n", window, cx);
15582 });
15583 executor.run_until_parked();
15584
15585 cx.assert_state_with_diff(
15586 r#"
15587 use some::mod1;
15588 use some::mod2;
15589
15590 const A: u32 = 42;
15591 const B: u32 = 42;
15592 - const C: u32 = 42;
15593 + const C: u32 = 43
15594 + new_line
15595 + ˇ
15596 const D: u32 = 42;
15597
15598
15599 fn main() {
15600 println!("hello");
15601
15602 println!("world");
15603 }"#
15604 .unindent(),
15605 );
15606}
15607
15608#[gpui::test]
15609async fn test_stage_and_unstage_added_file_hunk(
15610 executor: BackgroundExecutor,
15611 cx: &mut TestAppContext,
15612) {
15613 init_test(cx, |_| {});
15614
15615 let mut cx = EditorTestContext::new(cx).await;
15616 cx.update_editor(|editor, _, cx| {
15617 editor.set_expand_all_diff_hunks(cx);
15618 });
15619
15620 let working_copy = r#"
15621 ˇfn main() {
15622 println!("hello, world!");
15623 }
15624 "#
15625 .unindent();
15626
15627 cx.set_state(&working_copy);
15628 executor.run_until_parked();
15629
15630 cx.assert_state_with_diff(
15631 r#"
15632 + ˇfn main() {
15633 + println!("hello, world!");
15634 + }
15635 "#
15636 .unindent(),
15637 );
15638 cx.assert_index_text(None);
15639
15640 cx.update_editor(|editor, window, cx| {
15641 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
15642 });
15643 executor.run_until_parked();
15644 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
15645 cx.assert_state_with_diff(
15646 r#"
15647 + ˇfn main() {
15648 + println!("hello, world!");
15649 + }
15650 "#
15651 .unindent(),
15652 );
15653
15654 cx.update_editor(|editor, window, cx| {
15655 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
15656 });
15657 executor.run_until_parked();
15658 cx.assert_index_text(None);
15659}
15660
15661async fn setup_indent_guides_editor(
15662 text: &str,
15663 cx: &mut TestAppContext,
15664) -> (BufferId, EditorTestContext) {
15665 init_test(cx, |_| {});
15666
15667 let mut cx = EditorTestContext::new(cx).await;
15668
15669 let buffer_id = cx.update_editor(|editor, window, cx| {
15670 editor.set_text(text, window, cx);
15671 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
15672
15673 buffer_ids[0]
15674 });
15675
15676 (buffer_id, cx)
15677}
15678
15679fn assert_indent_guides(
15680 range: Range<u32>,
15681 expected: Vec<IndentGuide>,
15682 active_indices: Option<Vec<usize>>,
15683 cx: &mut EditorTestContext,
15684) {
15685 let indent_guides = cx.update_editor(|editor, window, cx| {
15686 let snapshot = editor.snapshot(window, cx).display_snapshot;
15687 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
15688 editor,
15689 MultiBufferRow(range.start)..MultiBufferRow(range.end),
15690 true,
15691 &snapshot,
15692 cx,
15693 );
15694
15695 indent_guides.sort_by(|a, b| {
15696 a.depth.cmp(&b.depth).then(
15697 a.start_row
15698 .cmp(&b.start_row)
15699 .then(a.end_row.cmp(&b.end_row)),
15700 )
15701 });
15702 indent_guides
15703 });
15704
15705 if let Some(expected) = active_indices {
15706 let active_indices = cx.update_editor(|editor, window, cx| {
15707 let snapshot = editor.snapshot(window, cx).display_snapshot;
15708 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
15709 });
15710
15711 assert_eq!(
15712 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
15713 expected,
15714 "Active indent guide indices do not match"
15715 );
15716 }
15717
15718 assert_eq!(indent_guides, expected, "Indent guides do not match");
15719}
15720
15721fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
15722 IndentGuide {
15723 buffer_id,
15724 start_row: MultiBufferRow(start_row),
15725 end_row: MultiBufferRow(end_row),
15726 depth,
15727 tab_size: 4,
15728 settings: IndentGuideSettings {
15729 enabled: true,
15730 line_width: 1,
15731 active_line_width: 1,
15732 ..Default::default()
15733 },
15734 }
15735}
15736
15737#[gpui::test]
15738async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
15739 let (buffer_id, mut cx) = setup_indent_guides_editor(
15740 &"
15741 fn main() {
15742 let a = 1;
15743 }"
15744 .unindent(),
15745 cx,
15746 )
15747 .await;
15748
15749 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
15750}
15751
15752#[gpui::test]
15753async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
15754 let (buffer_id, mut cx) = setup_indent_guides_editor(
15755 &"
15756 fn main() {
15757 let a = 1;
15758 let b = 2;
15759 }"
15760 .unindent(),
15761 cx,
15762 )
15763 .await;
15764
15765 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
15766}
15767
15768#[gpui::test]
15769async fn test_indent_guide_nested(cx: &mut TestAppContext) {
15770 let (buffer_id, mut cx) = setup_indent_guides_editor(
15771 &"
15772 fn main() {
15773 let a = 1;
15774 if a == 3 {
15775 let b = 2;
15776 } else {
15777 let c = 3;
15778 }
15779 }"
15780 .unindent(),
15781 cx,
15782 )
15783 .await;
15784
15785 assert_indent_guides(
15786 0..8,
15787 vec![
15788 indent_guide(buffer_id, 1, 6, 0),
15789 indent_guide(buffer_id, 3, 3, 1),
15790 indent_guide(buffer_id, 5, 5, 1),
15791 ],
15792 None,
15793 &mut cx,
15794 );
15795}
15796
15797#[gpui::test]
15798async fn test_indent_guide_tab(cx: &mut TestAppContext) {
15799 let (buffer_id, mut cx) = setup_indent_guides_editor(
15800 &"
15801 fn main() {
15802 let a = 1;
15803 let b = 2;
15804 let c = 3;
15805 }"
15806 .unindent(),
15807 cx,
15808 )
15809 .await;
15810
15811 assert_indent_guides(
15812 0..5,
15813 vec![
15814 indent_guide(buffer_id, 1, 3, 0),
15815 indent_guide(buffer_id, 2, 2, 1),
15816 ],
15817 None,
15818 &mut cx,
15819 );
15820}
15821
15822#[gpui::test]
15823async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
15824 let (buffer_id, mut cx) = setup_indent_guides_editor(
15825 &"
15826 fn main() {
15827 let a = 1;
15828
15829 let c = 3;
15830 }"
15831 .unindent(),
15832 cx,
15833 )
15834 .await;
15835
15836 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
15837}
15838
15839#[gpui::test]
15840async fn test_indent_guide_complex(cx: &mut TestAppContext) {
15841 let (buffer_id, mut cx) = setup_indent_guides_editor(
15842 &"
15843 fn main() {
15844 let a = 1;
15845
15846 let c = 3;
15847
15848 if a == 3 {
15849 let b = 2;
15850 } else {
15851 let c = 3;
15852 }
15853 }"
15854 .unindent(),
15855 cx,
15856 )
15857 .await;
15858
15859 assert_indent_guides(
15860 0..11,
15861 vec![
15862 indent_guide(buffer_id, 1, 9, 0),
15863 indent_guide(buffer_id, 6, 6, 1),
15864 indent_guide(buffer_id, 8, 8, 1),
15865 ],
15866 None,
15867 &mut cx,
15868 );
15869}
15870
15871#[gpui::test]
15872async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
15873 let (buffer_id, mut cx) = setup_indent_guides_editor(
15874 &"
15875 fn main() {
15876 let a = 1;
15877
15878 let c = 3;
15879
15880 if a == 3 {
15881 let b = 2;
15882 } else {
15883 let c = 3;
15884 }
15885 }"
15886 .unindent(),
15887 cx,
15888 )
15889 .await;
15890
15891 assert_indent_guides(
15892 1..11,
15893 vec![
15894 indent_guide(buffer_id, 1, 9, 0),
15895 indent_guide(buffer_id, 6, 6, 1),
15896 indent_guide(buffer_id, 8, 8, 1),
15897 ],
15898 None,
15899 &mut cx,
15900 );
15901}
15902
15903#[gpui::test]
15904async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
15905 let (buffer_id, mut cx) = setup_indent_guides_editor(
15906 &"
15907 fn main() {
15908 let a = 1;
15909
15910 let c = 3;
15911
15912 if a == 3 {
15913 let b = 2;
15914 } else {
15915 let c = 3;
15916 }
15917 }"
15918 .unindent(),
15919 cx,
15920 )
15921 .await;
15922
15923 assert_indent_guides(
15924 1..10,
15925 vec![
15926 indent_guide(buffer_id, 1, 9, 0),
15927 indent_guide(buffer_id, 6, 6, 1),
15928 indent_guide(buffer_id, 8, 8, 1),
15929 ],
15930 None,
15931 &mut cx,
15932 );
15933}
15934
15935#[gpui::test]
15936async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
15937 let (buffer_id, mut cx) = setup_indent_guides_editor(
15938 &"
15939 block1
15940 block2
15941 block3
15942 block4
15943 block2
15944 block1
15945 block1"
15946 .unindent(),
15947 cx,
15948 )
15949 .await;
15950
15951 assert_indent_guides(
15952 1..10,
15953 vec![
15954 indent_guide(buffer_id, 1, 4, 0),
15955 indent_guide(buffer_id, 2, 3, 1),
15956 indent_guide(buffer_id, 3, 3, 2),
15957 ],
15958 None,
15959 &mut cx,
15960 );
15961}
15962
15963#[gpui::test]
15964async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
15965 let (buffer_id, mut cx) = setup_indent_guides_editor(
15966 &"
15967 block1
15968 block2
15969 block3
15970
15971 block1
15972 block1"
15973 .unindent(),
15974 cx,
15975 )
15976 .await;
15977
15978 assert_indent_guides(
15979 0..6,
15980 vec![
15981 indent_guide(buffer_id, 1, 2, 0),
15982 indent_guide(buffer_id, 2, 2, 1),
15983 ],
15984 None,
15985 &mut cx,
15986 );
15987}
15988
15989#[gpui::test]
15990async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
15991 let (buffer_id, mut cx) = setup_indent_guides_editor(
15992 &"
15993 block1
15994
15995
15996
15997 block2
15998 "
15999 .unindent(),
16000 cx,
16001 )
16002 .await;
16003
16004 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
16005}
16006
16007#[gpui::test]
16008async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
16009 let (buffer_id, mut cx) = setup_indent_guides_editor(
16010 &"
16011 def a:
16012 \tb = 3
16013 \tif True:
16014 \t\tc = 4
16015 \t\td = 5
16016 \tprint(b)
16017 "
16018 .unindent(),
16019 cx,
16020 )
16021 .await;
16022
16023 assert_indent_guides(
16024 0..6,
16025 vec![
16026 indent_guide(buffer_id, 1, 6, 0),
16027 indent_guide(buffer_id, 3, 4, 1),
16028 ],
16029 None,
16030 &mut cx,
16031 );
16032}
16033
16034#[gpui::test]
16035async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
16036 let (buffer_id, mut cx) = setup_indent_guides_editor(
16037 &"
16038 fn main() {
16039 let a = 1;
16040 }"
16041 .unindent(),
16042 cx,
16043 )
16044 .await;
16045
16046 cx.update_editor(|editor, window, cx| {
16047 editor.change_selections(None, window, cx, |s| {
16048 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16049 });
16050 });
16051
16052 assert_indent_guides(
16053 0..3,
16054 vec![indent_guide(buffer_id, 1, 1, 0)],
16055 Some(vec![0]),
16056 &mut cx,
16057 );
16058}
16059
16060#[gpui::test]
16061async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
16062 let (buffer_id, mut cx) = setup_indent_guides_editor(
16063 &"
16064 fn main() {
16065 if 1 == 2 {
16066 let a = 1;
16067 }
16068 }"
16069 .unindent(),
16070 cx,
16071 )
16072 .await;
16073
16074 cx.update_editor(|editor, window, cx| {
16075 editor.change_selections(None, window, cx, |s| {
16076 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16077 });
16078 });
16079
16080 assert_indent_guides(
16081 0..4,
16082 vec![
16083 indent_guide(buffer_id, 1, 3, 0),
16084 indent_guide(buffer_id, 2, 2, 1),
16085 ],
16086 Some(vec![1]),
16087 &mut cx,
16088 );
16089
16090 cx.update_editor(|editor, window, cx| {
16091 editor.change_selections(None, window, cx, |s| {
16092 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
16093 });
16094 });
16095
16096 assert_indent_guides(
16097 0..4,
16098 vec![
16099 indent_guide(buffer_id, 1, 3, 0),
16100 indent_guide(buffer_id, 2, 2, 1),
16101 ],
16102 Some(vec![1]),
16103 &mut cx,
16104 );
16105
16106 cx.update_editor(|editor, window, cx| {
16107 editor.change_selections(None, window, cx, |s| {
16108 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
16109 });
16110 });
16111
16112 assert_indent_guides(
16113 0..4,
16114 vec![
16115 indent_guide(buffer_id, 1, 3, 0),
16116 indent_guide(buffer_id, 2, 2, 1),
16117 ],
16118 Some(vec![0]),
16119 &mut cx,
16120 );
16121}
16122
16123#[gpui::test]
16124async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
16125 let (buffer_id, mut cx) = setup_indent_guides_editor(
16126 &"
16127 fn main() {
16128 let a = 1;
16129
16130 let b = 2;
16131 }"
16132 .unindent(),
16133 cx,
16134 )
16135 .await;
16136
16137 cx.update_editor(|editor, window, cx| {
16138 editor.change_selections(None, window, cx, |s| {
16139 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
16140 });
16141 });
16142
16143 assert_indent_guides(
16144 0..5,
16145 vec![indent_guide(buffer_id, 1, 3, 0)],
16146 Some(vec![0]),
16147 &mut cx,
16148 );
16149}
16150
16151#[gpui::test]
16152async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
16153 let (buffer_id, mut cx) = setup_indent_guides_editor(
16154 &"
16155 def m:
16156 a = 1
16157 pass"
16158 .unindent(),
16159 cx,
16160 )
16161 .await;
16162
16163 cx.update_editor(|editor, window, cx| {
16164 editor.change_selections(None, window, cx, |s| {
16165 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16166 });
16167 });
16168
16169 assert_indent_guides(
16170 0..3,
16171 vec![indent_guide(buffer_id, 1, 2, 0)],
16172 Some(vec![0]),
16173 &mut cx,
16174 );
16175}
16176
16177#[gpui::test]
16178async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
16179 init_test(cx, |_| {});
16180 let mut cx = EditorTestContext::new(cx).await;
16181 let text = indoc! {
16182 "
16183 impl A {
16184 fn b() {
16185 0;
16186 3;
16187 5;
16188 6;
16189 7;
16190 }
16191 }
16192 "
16193 };
16194 let base_text = indoc! {
16195 "
16196 impl A {
16197 fn b() {
16198 0;
16199 1;
16200 2;
16201 3;
16202 4;
16203 }
16204 fn c() {
16205 5;
16206 6;
16207 7;
16208 }
16209 }
16210 "
16211 };
16212
16213 cx.update_editor(|editor, window, cx| {
16214 editor.set_text(text, window, cx);
16215
16216 editor.buffer().update(cx, |multibuffer, cx| {
16217 let buffer = multibuffer.as_singleton().unwrap();
16218 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
16219
16220 multibuffer.set_all_diff_hunks_expanded(cx);
16221 multibuffer.add_diff(diff, cx);
16222
16223 buffer.read(cx).remote_id()
16224 })
16225 });
16226 cx.run_until_parked();
16227
16228 cx.assert_state_with_diff(
16229 indoc! { "
16230 impl A {
16231 fn b() {
16232 0;
16233 - 1;
16234 - 2;
16235 3;
16236 - 4;
16237 - }
16238 - fn c() {
16239 5;
16240 6;
16241 7;
16242 }
16243 }
16244 ˇ"
16245 }
16246 .to_string(),
16247 );
16248
16249 let mut actual_guides = cx.update_editor(|editor, window, cx| {
16250 editor
16251 .snapshot(window, cx)
16252 .buffer_snapshot
16253 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
16254 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
16255 .collect::<Vec<_>>()
16256 });
16257 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
16258 assert_eq!(
16259 actual_guides,
16260 vec![
16261 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
16262 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
16263 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
16264 ]
16265 );
16266}
16267
16268#[gpui::test]
16269async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16270 init_test(cx, |_| {});
16271 let mut cx = EditorTestContext::new(cx).await;
16272
16273 let diff_base = r#"
16274 a
16275 b
16276 c
16277 "#
16278 .unindent();
16279
16280 cx.set_state(
16281 &r#"
16282 ˇA
16283 b
16284 C
16285 "#
16286 .unindent(),
16287 );
16288 cx.set_head_text(&diff_base);
16289 cx.update_editor(|editor, window, cx| {
16290 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16291 });
16292 executor.run_until_parked();
16293
16294 let both_hunks_expanded = r#"
16295 - a
16296 + ˇA
16297 b
16298 - c
16299 + C
16300 "#
16301 .unindent();
16302
16303 cx.assert_state_with_diff(both_hunks_expanded.clone());
16304
16305 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16306 let snapshot = editor.snapshot(window, cx);
16307 let hunks = editor
16308 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16309 .collect::<Vec<_>>();
16310 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16311 let buffer_id = hunks[0].buffer_id;
16312 hunks
16313 .into_iter()
16314 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16315 .collect::<Vec<_>>()
16316 });
16317 assert_eq!(hunk_ranges.len(), 2);
16318
16319 cx.update_editor(|editor, _, cx| {
16320 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16321 });
16322 executor.run_until_parked();
16323
16324 let second_hunk_expanded = r#"
16325 ˇA
16326 b
16327 - c
16328 + C
16329 "#
16330 .unindent();
16331
16332 cx.assert_state_with_diff(second_hunk_expanded);
16333
16334 cx.update_editor(|editor, _, cx| {
16335 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16336 });
16337 executor.run_until_parked();
16338
16339 cx.assert_state_with_diff(both_hunks_expanded.clone());
16340
16341 cx.update_editor(|editor, _, cx| {
16342 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
16343 });
16344 executor.run_until_parked();
16345
16346 let first_hunk_expanded = r#"
16347 - a
16348 + ˇA
16349 b
16350 C
16351 "#
16352 .unindent();
16353
16354 cx.assert_state_with_diff(first_hunk_expanded);
16355
16356 cx.update_editor(|editor, _, cx| {
16357 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
16358 });
16359 executor.run_until_parked();
16360
16361 cx.assert_state_with_diff(both_hunks_expanded);
16362
16363 cx.set_state(
16364 &r#"
16365 ˇA
16366 b
16367 "#
16368 .unindent(),
16369 );
16370 cx.run_until_parked();
16371
16372 // TODO this cursor position seems bad
16373 cx.assert_state_with_diff(
16374 r#"
16375 - ˇa
16376 + A
16377 b
16378 "#
16379 .unindent(),
16380 );
16381
16382 cx.update_editor(|editor, window, cx| {
16383 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16384 });
16385
16386 cx.assert_state_with_diff(
16387 r#"
16388 - ˇa
16389 + A
16390 b
16391 - c
16392 "#
16393 .unindent(),
16394 );
16395
16396 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16397 let snapshot = editor.snapshot(window, cx);
16398 let hunks = editor
16399 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16400 .collect::<Vec<_>>();
16401 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16402 let buffer_id = hunks[0].buffer_id;
16403 hunks
16404 .into_iter()
16405 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16406 .collect::<Vec<_>>()
16407 });
16408 assert_eq!(hunk_ranges.len(), 2);
16409
16410 cx.update_editor(|editor, _, cx| {
16411 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
16412 });
16413 executor.run_until_parked();
16414
16415 cx.assert_state_with_diff(
16416 r#"
16417 - ˇa
16418 + A
16419 b
16420 "#
16421 .unindent(),
16422 );
16423}
16424
16425#[gpui::test]
16426async fn test_toggle_deletion_hunk_at_start_of_file(
16427 executor: BackgroundExecutor,
16428 cx: &mut TestAppContext,
16429) {
16430 init_test(cx, |_| {});
16431 let mut cx = EditorTestContext::new(cx).await;
16432
16433 let diff_base = r#"
16434 a
16435 b
16436 c
16437 "#
16438 .unindent();
16439
16440 cx.set_state(
16441 &r#"
16442 ˇb
16443 c
16444 "#
16445 .unindent(),
16446 );
16447 cx.set_head_text(&diff_base);
16448 cx.update_editor(|editor, window, cx| {
16449 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16450 });
16451 executor.run_until_parked();
16452
16453 let hunk_expanded = r#"
16454 - a
16455 ˇb
16456 c
16457 "#
16458 .unindent();
16459
16460 cx.assert_state_with_diff(hunk_expanded.clone());
16461
16462 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16463 let snapshot = editor.snapshot(window, cx);
16464 let hunks = editor
16465 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16466 .collect::<Vec<_>>();
16467 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16468 let buffer_id = hunks[0].buffer_id;
16469 hunks
16470 .into_iter()
16471 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16472 .collect::<Vec<_>>()
16473 });
16474 assert_eq!(hunk_ranges.len(), 1);
16475
16476 cx.update_editor(|editor, _, cx| {
16477 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16478 });
16479 executor.run_until_parked();
16480
16481 let hunk_collapsed = r#"
16482 ˇb
16483 c
16484 "#
16485 .unindent();
16486
16487 cx.assert_state_with_diff(hunk_collapsed);
16488
16489 cx.update_editor(|editor, _, cx| {
16490 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16491 });
16492 executor.run_until_parked();
16493
16494 cx.assert_state_with_diff(hunk_expanded.clone());
16495}
16496
16497#[gpui::test]
16498async fn test_display_diff_hunks(cx: &mut TestAppContext) {
16499 init_test(cx, |_| {});
16500
16501 let fs = FakeFs::new(cx.executor());
16502 fs.insert_tree(
16503 path!("/test"),
16504 json!({
16505 ".git": {},
16506 "file-1": "ONE\n",
16507 "file-2": "TWO\n",
16508 "file-3": "THREE\n",
16509 }),
16510 )
16511 .await;
16512
16513 fs.set_head_for_repo(
16514 path!("/test/.git").as_ref(),
16515 &[
16516 ("file-1".into(), "one\n".into()),
16517 ("file-2".into(), "two\n".into()),
16518 ("file-3".into(), "three\n".into()),
16519 ],
16520 );
16521
16522 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
16523 let mut buffers = vec![];
16524 for i in 1..=3 {
16525 let buffer = project
16526 .update(cx, |project, cx| {
16527 let path = format!(path!("/test/file-{}"), i);
16528 project.open_local_buffer(path, cx)
16529 })
16530 .await
16531 .unwrap();
16532 buffers.push(buffer);
16533 }
16534
16535 let multibuffer = cx.new(|cx| {
16536 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
16537 multibuffer.set_all_diff_hunks_expanded(cx);
16538 for buffer in &buffers {
16539 let snapshot = buffer.read(cx).snapshot();
16540 multibuffer.set_excerpts_for_path(
16541 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
16542 buffer.clone(),
16543 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
16544 DEFAULT_MULTIBUFFER_CONTEXT,
16545 cx,
16546 );
16547 }
16548 multibuffer
16549 });
16550
16551 let editor = cx.add_window(|window, cx| {
16552 Editor::new(EditorMode::Full, multibuffer, Some(project), window, cx)
16553 });
16554 cx.run_until_parked();
16555
16556 let snapshot = editor
16557 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16558 .unwrap();
16559 let hunks = snapshot
16560 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
16561 .map(|hunk| match hunk {
16562 DisplayDiffHunk::Unfolded {
16563 display_row_range, ..
16564 } => display_row_range,
16565 DisplayDiffHunk::Folded { .. } => unreachable!(),
16566 })
16567 .collect::<Vec<_>>();
16568 assert_eq!(
16569 hunks,
16570 [
16571 DisplayRow(2)..DisplayRow(4),
16572 DisplayRow(7)..DisplayRow(9),
16573 DisplayRow(12)..DisplayRow(14),
16574 ]
16575 );
16576}
16577
16578#[gpui::test]
16579async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
16580 init_test(cx, |_| {});
16581
16582 let mut cx = EditorTestContext::new(cx).await;
16583 cx.set_head_text(indoc! { "
16584 one
16585 two
16586 three
16587 four
16588 five
16589 "
16590 });
16591 cx.set_index_text(indoc! { "
16592 one
16593 two
16594 three
16595 four
16596 five
16597 "
16598 });
16599 cx.set_state(indoc! {"
16600 one
16601 TWO
16602 ˇTHREE
16603 FOUR
16604 five
16605 "});
16606 cx.run_until_parked();
16607 cx.update_editor(|editor, window, cx| {
16608 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16609 });
16610 cx.run_until_parked();
16611 cx.assert_index_text(Some(indoc! {"
16612 one
16613 TWO
16614 THREE
16615 FOUR
16616 five
16617 "}));
16618 cx.set_state(indoc! { "
16619 one
16620 TWO
16621 ˇTHREE-HUNDRED
16622 FOUR
16623 five
16624 "});
16625 cx.run_until_parked();
16626 cx.update_editor(|editor, window, cx| {
16627 let snapshot = editor.snapshot(window, cx);
16628 let hunks = editor
16629 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16630 .collect::<Vec<_>>();
16631 assert_eq!(hunks.len(), 1);
16632 assert_eq!(
16633 hunks[0].status(),
16634 DiffHunkStatus {
16635 kind: DiffHunkStatusKind::Modified,
16636 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
16637 }
16638 );
16639
16640 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16641 });
16642 cx.run_until_parked();
16643 cx.assert_index_text(Some(indoc! {"
16644 one
16645 TWO
16646 THREE-HUNDRED
16647 FOUR
16648 five
16649 "}));
16650}
16651
16652#[gpui::test]
16653fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
16654 init_test(cx, |_| {});
16655
16656 let editor = cx.add_window(|window, cx| {
16657 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
16658 build_editor(buffer, window, cx)
16659 });
16660
16661 let render_args = Arc::new(Mutex::new(None));
16662 let snapshot = editor
16663 .update(cx, |editor, window, cx| {
16664 let snapshot = editor.buffer().read(cx).snapshot(cx);
16665 let range =
16666 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
16667
16668 struct RenderArgs {
16669 row: MultiBufferRow,
16670 folded: bool,
16671 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
16672 }
16673
16674 let crease = Crease::inline(
16675 range,
16676 FoldPlaceholder::test(),
16677 {
16678 let toggle_callback = render_args.clone();
16679 move |row, folded, callback, _window, _cx| {
16680 *toggle_callback.lock() = Some(RenderArgs {
16681 row,
16682 folded,
16683 callback,
16684 });
16685 div()
16686 }
16687 },
16688 |_row, _folded, _window, _cx| div(),
16689 );
16690
16691 editor.insert_creases(Some(crease), cx);
16692 let snapshot = editor.snapshot(window, cx);
16693 let _div = snapshot.render_crease_toggle(
16694 MultiBufferRow(1),
16695 false,
16696 cx.entity().clone(),
16697 window,
16698 cx,
16699 );
16700 snapshot
16701 })
16702 .unwrap();
16703
16704 let render_args = render_args.lock().take().unwrap();
16705 assert_eq!(render_args.row, MultiBufferRow(1));
16706 assert!(!render_args.folded);
16707 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
16708
16709 cx.update_window(*editor, |_, window, cx| {
16710 (render_args.callback)(true, window, cx)
16711 })
16712 .unwrap();
16713 let snapshot = editor
16714 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16715 .unwrap();
16716 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
16717
16718 cx.update_window(*editor, |_, window, cx| {
16719 (render_args.callback)(false, window, cx)
16720 })
16721 .unwrap();
16722 let snapshot = editor
16723 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16724 .unwrap();
16725 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
16726}
16727
16728#[gpui::test]
16729async fn test_input_text(cx: &mut TestAppContext) {
16730 init_test(cx, |_| {});
16731 let mut cx = EditorTestContext::new(cx).await;
16732
16733 cx.set_state(
16734 &r#"ˇone
16735 two
16736
16737 three
16738 fourˇ
16739 five
16740
16741 siˇx"#
16742 .unindent(),
16743 );
16744
16745 cx.dispatch_action(HandleInput(String::new()));
16746 cx.assert_editor_state(
16747 &r#"ˇone
16748 two
16749
16750 three
16751 fourˇ
16752 five
16753
16754 siˇx"#
16755 .unindent(),
16756 );
16757
16758 cx.dispatch_action(HandleInput("AAAA".to_string()));
16759 cx.assert_editor_state(
16760 &r#"AAAAˇone
16761 two
16762
16763 three
16764 fourAAAAˇ
16765 five
16766
16767 siAAAAˇx"#
16768 .unindent(),
16769 );
16770}
16771
16772#[gpui::test]
16773async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
16774 init_test(cx, |_| {});
16775
16776 let mut cx = EditorTestContext::new(cx).await;
16777 cx.set_state(
16778 r#"let foo = 1;
16779let foo = 2;
16780let foo = 3;
16781let fooˇ = 4;
16782let foo = 5;
16783let foo = 6;
16784let foo = 7;
16785let foo = 8;
16786let foo = 9;
16787let foo = 10;
16788let foo = 11;
16789let foo = 12;
16790let foo = 13;
16791let foo = 14;
16792let foo = 15;"#,
16793 );
16794
16795 cx.update_editor(|e, window, cx| {
16796 assert_eq!(
16797 e.next_scroll_position,
16798 NextScrollCursorCenterTopBottom::Center,
16799 "Default next scroll direction is center",
16800 );
16801
16802 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16803 assert_eq!(
16804 e.next_scroll_position,
16805 NextScrollCursorCenterTopBottom::Top,
16806 "After center, next scroll direction should be top",
16807 );
16808
16809 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16810 assert_eq!(
16811 e.next_scroll_position,
16812 NextScrollCursorCenterTopBottom::Bottom,
16813 "After top, next scroll direction should be bottom",
16814 );
16815
16816 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16817 assert_eq!(
16818 e.next_scroll_position,
16819 NextScrollCursorCenterTopBottom::Center,
16820 "After bottom, scrolling should start over",
16821 );
16822
16823 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16824 assert_eq!(
16825 e.next_scroll_position,
16826 NextScrollCursorCenterTopBottom::Top,
16827 "Scrolling continues if retriggered fast enough"
16828 );
16829 });
16830
16831 cx.executor()
16832 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
16833 cx.executor().run_until_parked();
16834 cx.update_editor(|e, _, _| {
16835 assert_eq!(
16836 e.next_scroll_position,
16837 NextScrollCursorCenterTopBottom::Center,
16838 "If scrolling is not triggered fast enough, it should reset"
16839 );
16840 });
16841}
16842
16843#[gpui::test]
16844async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
16845 init_test(cx, |_| {});
16846 let mut cx = EditorLspTestContext::new_rust(
16847 lsp::ServerCapabilities {
16848 definition_provider: Some(lsp::OneOf::Left(true)),
16849 references_provider: Some(lsp::OneOf::Left(true)),
16850 ..lsp::ServerCapabilities::default()
16851 },
16852 cx,
16853 )
16854 .await;
16855
16856 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
16857 let go_to_definition = cx
16858 .lsp
16859 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
16860 move |params, _| async move {
16861 if empty_go_to_definition {
16862 Ok(None)
16863 } else {
16864 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
16865 uri: params.text_document_position_params.text_document.uri,
16866 range: lsp::Range::new(
16867 lsp::Position::new(4, 3),
16868 lsp::Position::new(4, 6),
16869 ),
16870 })))
16871 }
16872 },
16873 );
16874 let references = cx
16875 .lsp
16876 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
16877 Ok(Some(vec![lsp::Location {
16878 uri: params.text_document_position.text_document.uri,
16879 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
16880 }]))
16881 });
16882 (go_to_definition, references)
16883 };
16884
16885 cx.set_state(
16886 &r#"fn one() {
16887 let mut a = ˇtwo();
16888 }
16889
16890 fn two() {}"#
16891 .unindent(),
16892 );
16893 set_up_lsp_handlers(false, &mut cx);
16894 let navigated = cx
16895 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
16896 .await
16897 .expect("Failed to navigate to definition");
16898 assert_eq!(
16899 navigated,
16900 Navigated::Yes,
16901 "Should have navigated to definition from the GetDefinition response"
16902 );
16903 cx.assert_editor_state(
16904 &r#"fn one() {
16905 let mut a = two();
16906 }
16907
16908 fn «twoˇ»() {}"#
16909 .unindent(),
16910 );
16911
16912 let editors = cx.update_workspace(|workspace, _, cx| {
16913 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
16914 });
16915 cx.update_editor(|_, _, test_editor_cx| {
16916 assert_eq!(
16917 editors.len(),
16918 1,
16919 "Initially, only one, test, editor should be open in the workspace"
16920 );
16921 assert_eq!(
16922 test_editor_cx.entity(),
16923 editors.last().expect("Asserted len is 1").clone()
16924 );
16925 });
16926
16927 set_up_lsp_handlers(true, &mut cx);
16928 let navigated = cx
16929 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
16930 .await
16931 .expect("Failed to navigate to lookup references");
16932 assert_eq!(
16933 navigated,
16934 Navigated::Yes,
16935 "Should have navigated to references as a fallback after empty GoToDefinition response"
16936 );
16937 // We should not change the selections in the existing file,
16938 // if opening another milti buffer with the references
16939 cx.assert_editor_state(
16940 &r#"fn one() {
16941 let mut a = two();
16942 }
16943
16944 fn «twoˇ»() {}"#
16945 .unindent(),
16946 );
16947 let editors = cx.update_workspace(|workspace, _, cx| {
16948 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
16949 });
16950 cx.update_editor(|_, _, test_editor_cx| {
16951 assert_eq!(
16952 editors.len(),
16953 2,
16954 "After falling back to references search, we open a new editor with the results"
16955 );
16956 let references_fallback_text = editors
16957 .into_iter()
16958 .find(|new_editor| *new_editor != test_editor_cx.entity())
16959 .expect("Should have one non-test editor now")
16960 .read(test_editor_cx)
16961 .text(test_editor_cx);
16962 assert_eq!(
16963 references_fallback_text, "fn one() {\n let mut a = two();\n}",
16964 "Should use the range from the references response and not the GoToDefinition one"
16965 );
16966 });
16967}
16968
16969#[gpui::test]
16970async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
16971 init_test(cx, |_| {});
16972 cx.update(|cx| {
16973 let mut editor_settings = EditorSettings::get_global(cx).clone();
16974 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
16975 EditorSettings::override_global(editor_settings, cx);
16976 });
16977 let mut cx = EditorLspTestContext::new_rust(
16978 lsp::ServerCapabilities {
16979 definition_provider: Some(lsp::OneOf::Left(true)),
16980 references_provider: Some(lsp::OneOf::Left(true)),
16981 ..lsp::ServerCapabilities::default()
16982 },
16983 cx,
16984 )
16985 .await;
16986 let original_state = r#"fn one() {
16987 let mut a = ˇtwo();
16988 }
16989
16990 fn two() {}"#
16991 .unindent();
16992 cx.set_state(&original_state);
16993
16994 let mut go_to_definition = cx
16995 .lsp
16996 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
16997 move |_, _| async move { Ok(None) },
16998 );
16999 let _references = cx
17000 .lsp
17001 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
17002 panic!("Should not call for references with no go to definition fallback")
17003 });
17004
17005 let navigated = cx
17006 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17007 .await
17008 .expect("Failed to navigate to lookup references");
17009 go_to_definition
17010 .next()
17011 .await
17012 .expect("Should have called the go_to_definition handler");
17013
17014 assert_eq!(
17015 navigated,
17016 Navigated::No,
17017 "Should have navigated to references as a fallback after empty GoToDefinition response"
17018 );
17019 cx.assert_editor_state(&original_state);
17020 let editors = cx.update_workspace(|workspace, _, cx| {
17021 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17022 });
17023 cx.update_editor(|_, _, _| {
17024 assert_eq!(
17025 editors.len(),
17026 1,
17027 "After unsuccessful fallback, no other editor should have been opened"
17028 );
17029 });
17030}
17031
17032#[gpui::test]
17033async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
17034 init_test(cx, |_| {});
17035
17036 let language = Arc::new(Language::new(
17037 LanguageConfig::default(),
17038 Some(tree_sitter_rust::LANGUAGE.into()),
17039 ));
17040
17041 let text = r#"
17042 #[cfg(test)]
17043 mod tests() {
17044 #[test]
17045 fn runnable_1() {
17046 let a = 1;
17047 }
17048
17049 #[test]
17050 fn runnable_2() {
17051 let a = 1;
17052 let b = 2;
17053 }
17054 }
17055 "#
17056 .unindent();
17057
17058 let fs = FakeFs::new(cx.executor());
17059 fs.insert_file("/file.rs", Default::default()).await;
17060
17061 let project = Project::test(fs, ["/a".as_ref()], cx).await;
17062 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17063 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17064 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
17065 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
17066
17067 let editor = cx.new_window_entity(|window, cx| {
17068 Editor::new(
17069 EditorMode::Full,
17070 multi_buffer,
17071 Some(project.clone()),
17072 window,
17073 cx,
17074 )
17075 });
17076
17077 editor.update_in(cx, |editor, window, cx| {
17078 let snapshot = editor.buffer().read(cx).snapshot(cx);
17079 editor.tasks.insert(
17080 (buffer.read(cx).remote_id(), 3),
17081 RunnableTasks {
17082 templates: vec![],
17083 offset: snapshot.anchor_before(43),
17084 column: 0,
17085 extra_variables: HashMap::default(),
17086 context_range: BufferOffset(43)..BufferOffset(85),
17087 },
17088 );
17089 editor.tasks.insert(
17090 (buffer.read(cx).remote_id(), 8),
17091 RunnableTasks {
17092 templates: vec![],
17093 offset: snapshot.anchor_before(86),
17094 column: 0,
17095 extra_variables: HashMap::default(),
17096 context_range: BufferOffset(86)..BufferOffset(191),
17097 },
17098 );
17099
17100 // Test finding task when cursor is inside function body
17101 editor.change_selections(None, window, cx, |s| {
17102 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
17103 });
17104 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
17105 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
17106
17107 // Test finding task when cursor is on function name
17108 editor.change_selections(None, window, cx, |s| {
17109 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
17110 });
17111 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
17112 assert_eq!(row, 8, "Should find task when cursor is on function name");
17113 });
17114}
17115
17116#[gpui::test]
17117async fn test_folding_buffers(cx: &mut TestAppContext) {
17118 init_test(cx, |_| {});
17119
17120 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
17121 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
17122 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
17123
17124 let fs = FakeFs::new(cx.executor());
17125 fs.insert_tree(
17126 path!("/a"),
17127 json!({
17128 "first.rs": sample_text_1,
17129 "second.rs": sample_text_2,
17130 "third.rs": sample_text_3,
17131 }),
17132 )
17133 .await;
17134 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17135 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17136 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17137 let worktree = project.update(cx, |project, cx| {
17138 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17139 assert_eq!(worktrees.len(), 1);
17140 worktrees.pop().unwrap()
17141 });
17142 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17143
17144 let buffer_1 = project
17145 .update(cx, |project, cx| {
17146 project.open_buffer((worktree_id, "first.rs"), cx)
17147 })
17148 .await
17149 .unwrap();
17150 let buffer_2 = project
17151 .update(cx, |project, cx| {
17152 project.open_buffer((worktree_id, "second.rs"), cx)
17153 })
17154 .await
17155 .unwrap();
17156 let buffer_3 = project
17157 .update(cx, |project, cx| {
17158 project.open_buffer((worktree_id, "third.rs"), cx)
17159 })
17160 .await
17161 .unwrap();
17162
17163 let multi_buffer = cx.new(|cx| {
17164 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17165 multi_buffer.push_excerpts(
17166 buffer_1.clone(),
17167 [
17168 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17169 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17170 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17171 ],
17172 cx,
17173 );
17174 multi_buffer.push_excerpts(
17175 buffer_2.clone(),
17176 [
17177 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17178 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17179 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17180 ],
17181 cx,
17182 );
17183 multi_buffer.push_excerpts(
17184 buffer_3.clone(),
17185 [
17186 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17187 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17188 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17189 ],
17190 cx,
17191 );
17192 multi_buffer
17193 });
17194 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17195 Editor::new(
17196 EditorMode::Full,
17197 multi_buffer.clone(),
17198 Some(project.clone()),
17199 window,
17200 cx,
17201 )
17202 });
17203
17204 assert_eq!(
17205 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17206 "\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",
17207 );
17208
17209 multi_buffer_editor.update(cx, |editor, cx| {
17210 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
17211 });
17212 assert_eq!(
17213 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17214 "\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",
17215 "After folding the first buffer, its text should not be displayed"
17216 );
17217
17218 multi_buffer_editor.update(cx, |editor, cx| {
17219 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
17220 });
17221 assert_eq!(
17222 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17223 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
17224 "After folding the second buffer, its text should not be displayed"
17225 );
17226
17227 multi_buffer_editor.update(cx, |editor, cx| {
17228 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
17229 });
17230 assert_eq!(
17231 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17232 "\n\n\n\n\n",
17233 "After folding the third buffer, its text should not be displayed"
17234 );
17235
17236 // Emulate selection inside the fold logic, that should work
17237 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17238 editor
17239 .snapshot(window, cx)
17240 .next_line_boundary(Point::new(0, 4));
17241 });
17242
17243 multi_buffer_editor.update(cx, |editor, cx| {
17244 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
17245 });
17246 assert_eq!(
17247 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17248 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
17249 "After unfolding the second buffer, its text should be displayed"
17250 );
17251
17252 // Typing inside of buffer 1 causes that buffer to be unfolded.
17253 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17254 assert_eq!(
17255 multi_buffer
17256 .read(cx)
17257 .snapshot(cx)
17258 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
17259 .collect::<String>(),
17260 "bbbb"
17261 );
17262 editor.change_selections(None, window, cx, |selections| {
17263 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
17264 });
17265 editor.handle_input("B", window, cx);
17266 });
17267
17268 assert_eq!(
17269 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17270 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
17271 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
17272 );
17273
17274 multi_buffer_editor.update(cx, |editor, cx| {
17275 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
17276 });
17277 assert_eq!(
17278 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17279 "\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",
17280 "After unfolding the all buffers, all original text should be displayed"
17281 );
17282}
17283
17284#[gpui::test]
17285async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
17286 init_test(cx, |_| {});
17287
17288 let sample_text_1 = "1111\n2222\n3333".to_string();
17289 let sample_text_2 = "4444\n5555\n6666".to_string();
17290 let sample_text_3 = "7777\n8888\n9999".to_string();
17291
17292 let fs = FakeFs::new(cx.executor());
17293 fs.insert_tree(
17294 path!("/a"),
17295 json!({
17296 "first.rs": sample_text_1,
17297 "second.rs": sample_text_2,
17298 "third.rs": sample_text_3,
17299 }),
17300 )
17301 .await;
17302 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17303 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17304 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17305 let worktree = project.update(cx, |project, cx| {
17306 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17307 assert_eq!(worktrees.len(), 1);
17308 worktrees.pop().unwrap()
17309 });
17310 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17311
17312 let buffer_1 = project
17313 .update(cx, |project, cx| {
17314 project.open_buffer((worktree_id, "first.rs"), cx)
17315 })
17316 .await
17317 .unwrap();
17318 let buffer_2 = project
17319 .update(cx, |project, cx| {
17320 project.open_buffer((worktree_id, "second.rs"), cx)
17321 })
17322 .await
17323 .unwrap();
17324 let buffer_3 = project
17325 .update(cx, |project, cx| {
17326 project.open_buffer((worktree_id, "third.rs"), cx)
17327 })
17328 .await
17329 .unwrap();
17330
17331 let multi_buffer = cx.new(|cx| {
17332 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17333 multi_buffer.push_excerpts(
17334 buffer_1.clone(),
17335 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
17336 cx,
17337 );
17338 multi_buffer.push_excerpts(
17339 buffer_2.clone(),
17340 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
17341 cx,
17342 );
17343 multi_buffer.push_excerpts(
17344 buffer_3.clone(),
17345 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
17346 cx,
17347 );
17348 multi_buffer
17349 });
17350
17351 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17352 Editor::new(
17353 EditorMode::Full,
17354 multi_buffer,
17355 Some(project.clone()),
17356 window,
17357 cx,
17358 )
17359 });
17360
17361 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
17362 assert_eq!(
17363 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17364 full_text,
17365 );
17366
17367 multi_buffer_editor.update(cx, |editor, cx| {
17368 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
17369 });
17370 assert_eq!(
17371 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17372 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
17373 "After folding the first buffer, its text should not be displayed"
17374 );
17375
17376 multi_buffer_editor.update(cx, |editor, cx| {
17377 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
17378 });
17379
17380 assert_eq!(
17381 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17382 "\n\n\n\n\n\n7777\n8888\n9999",
17383 "After folding the second buffer, its text should not be displayed"
17384 );
17385
17386 multi_buffer_editor.update(cx, |editor, cx| {
17387 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
17388 });
17389 assert_eq!(
17390 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17391 "\n\n\n\n\n",
17392 "After folding the third buffer, its text should not be displayed"
17393 );
17394
17395 multi_buffer_editor.update(cx, |editor, cx| {
17396 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
17397 });
17398 assert_eq!(
17399 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17400 "\n\n\n\n4444\n5555\n6666\n\n",
17401 "After unfolding the second buffer, its text should be displayed"
17402 );
17403
17404 multi_buffer_editor.update(cx, |editor, cx| {
17405 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
17406 });
17407 assert_eq!(
17408 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17409 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
17410 "After unfolding the first buffer, its text should be displayed"
17411 );
17412
17413 multi_buffer_editor.update(cx, |editor, cx| {
17414 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
17415 });
17416 assert_eq!(
17417 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17418 full_text,
17419 "After unfolding all buffers, all original text should be displayed"
17420 );
17421}
17422
17423#[gpui::test]
17424async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
17425 init_test(cx, |_| {});
17426
17427 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
17428
17429 let fs = FakeFs::new(cx.executor());
17430 fs.insert_tree(
17431 path!("/a"),
17432 json!({
17433 "main.rs": sample_text,
17434 }),
17435 )
17436 .await;
17437 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17438 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17439 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17440 let worktree = project.update(cx, |project, cx| {
17441 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17442 assert_eq!(worktrees.len(), 1);
17443 worktrees.pop().unwrap()
17444 });
17445 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17446
17447 let buffer_1 = project
17448 .update(cx, |project, cx| {
17449 project.open_buffer((worktree_id, "main.rs"), cx)
17450 })
17451 .await
17452 .unwrap();
17453
17454 let multi_buffer = cx.new(|cx| {
17455 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17456 multi_buffer.push_excerpts(
17457 buffer_1.clone(),
17458 [ExcerptRange::new(
17459 Point::new(0, 0)
17460 ..Point::new(
17461 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
17462 0,
17463 ),
17464 )],
17465 cx,
17466 );
17467 multi_buffer
17468 });
17469 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17470 Editor::new(
17471 EditorMode::Full,
17472 multi_buffer,
17473 Some(project.clone()),
17474 window,
17475 cx,
17476 )
17477 });
17478
17479 let selection_range = Point::new(1, 0)..Point::new(2, 0);
17480 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17481 enum TestHighlight {}
17482 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
17483 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
17484 editor.highlight_text::<TestHighlight>(
17485 vec![highlight_range.clone()],
17486 HighlightStyle::color(Hsla::green()),
17487 cx,
17488 );
17489 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
17490 });
17491
17492 let full_text = format!("\n\n{sample_text}");
17493 assert_eq!(
17494 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17495 full_text,
17496 );
17497}
17498
17499#[gpui::test]
17500async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
17501 init_test(cx, |_| {});
17502 cx.update(|cx| {
17503 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
17504 "keymaps/default-linux.json",
17505 cx,
17506 )
17507 .unwrap();
17508 cx.bind_keys(default_key_bindings);
17509 });
17510
17511 let (editor, cx) = cx.add_window_view(|window, cx| {
17512 let multi_buffer = MultiBuffer::build_multi(
17513 [
17514 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
17515 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
17516 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
17517 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
17518 ],
17519 cx,
17520 );
17521 let mut editor = Editor::new(EditorMode::Full, multi_buffer.clone(), None, window, cx);
17522
17523 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
17524 // fold all but the second buffer, so that we test navigating between two
17525 // adjacent folded buffers, as well as folded buffers at the start and
17526 // end the multibuffer
17527 editor.fold_buffer(buffer_ids[0], cx);
17528 editor.fold_buffer(buffer_ids[2], cx);
17529 editor.fold_buffer(buffer_ids[3], cx);
17530
17531 editor
17532 });
17533 cx.simulate_resize(size(px(1000.), px(1000.)));
17534
17535 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
17536 cx.assert_excerpts_with_selections(indoc! {"
17537 [EXCERPT]
17538 ˇ[FOLDED]
17539 [EXCERPT]
17540 a1
17541 b1
17542 [EXCERPT]
17543 [FOLDED]
17544 [EXCERPT]
17545 [FOLDED]
17546 "
17547 });
17548 cx.simulate_keystroke("down");
17549 cx.assert_excerpts_with_selections(indoc! {"
17550 [EXCERPT]
17551 [FOLDED]
17552 [EXCERPT]
17553 ˇa1
17554 b1
17555 [EXCERPT]
17556 [FOLDED]
17557 [EXCERPT]
17558 [FOLDED]
17559 "
17560 });
17561 cx.simulate_keystroke("down");
17562 cx.assert_excerpts_with_selections(indoc! {"
17563 [EXCERPT]
17564 [FOLDED]
17565 [EXCERPT]
17566 a1
17567 ˇb1
17568 [EXCERPT]
17569 [FOLDED]
17570 [EXCERPT]
17571 [FOLDED]
17572 "
17573 });
17574 cx.simulate_keystroke("down");
17575 cx.assert_excerpts_with_selections(indoc! {"
17576 [EXCERPT]
17577 [FOLDED]
17578 [EXCERPT]
17579 a1
17580 b1
17581 ˇ[EXCERPT]
17582 [FOLDED]
17583 [EXCERPT]
17584 [FOLDED]
17585 "
17586 });
17587 cx.simulate_keystroke("down");
17588 cx.assert_excerpts_with_selections(indoc! {"
17589 [EXCERPT]
17590 [FOLDED]
17591 [EXCERPT]
17592 a1
17593 b1
17594 [EXCERPT]
17595 ˇ[FOLDED]
17596 [EXCERPT]
17597 [FOLDED]
17598 "
17599 });
17600 for _ in 0..5 {
17601 cx.simulate_keystroke("down");
17602 cx.assert_excerpts_with_selections(indoc! {"
17603 [EXCERPT]
17604 [FOLDED]
17605 [EXCERPT]
17606 a1
17607 b1
17608 [EXCERPT]
17609 [FOLDED]
17610 [EXCERPT]
17611 ˇ[FOLDED]
17612 "
17613 });
17614 }
17615
17616 cx.simulate_keystroke("up");
17617 cx.assert_excerpts_with_selections(indoc! {"
17618 [EXCERPT]
17619 [FOLDED]
17620 [EXCERPT]
17621 a1
17622 b1
17623 [EXCERPT]
17624 ˇ[FOLDED]
17625 [EXCERPT]
17626 [FOLDED]
17627 "
17628 });
17629 cx.simulate_keystroke("up");
17630 cx.assert_excerpts_with_selections(indoc! {"
17631 [EXCERPT]
17632 [FOLDED]
17633 [EXCERPT]
17634 a1
17635 b1
17636 ˇ[EXCERPT]
17637 [FOLDED]
17638 [EXCERPT]
17639 [FOLDED]
17640 "
17641 });
17642 cx.simulate_keystroke("up");
17643 cx.assert_excerpts_with_selections(indoc! {"
17644 [EXCERPT]
17645 [FOLDED]
17646 [EXCERPT]
17647 a1
17648 ˇb1
17649 [EXCERPT]
17650 [FOLDED]
17651 [EXCERPT]
17652 [FOLDED]
17653 "
17654 });
17655 cx.simulate_keystroke("up");
17656 cx.assert_excerpts_with_selections(indoc! {"
17657 [EXCERPT]
17658 [FOLDED]
17659 [EXCERPT]
17660 ˇa1
17661 b1
17662 [EXCERPT]
17663 [FOLDED]
17664 [EXCERPT]
17665 [FOLDED]
17666 "
17667 });
17668 for _ in 0..5 {
17669 cx.simulate_keystroke("up");
17670 cx.assert_excerpts_with_selections(indoc! {"
17671 [EXCERPT]
17672 ˇ[FOLDED]
17673 [EXCERPT]
17674 a1
17675 b1
17676 [EXCERPT]
17677 [FOLDED]
17678 [EXCERPT]
17679 [FOLDED]
17680 "
17681 });
17682 }
17683}
17684
17685#[gpui::test]
17686async fn test_inline_completion_text(cx: &mut TestAppContext) {
17687 init_test(cx, |_| {});
17688
17689 // Simple insertion
17690 assert_highlighted_edits(
17691 "Hello, world!",
17692 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
17693 true,
17694 cx,
17695 |highlighted_edits, cx| {
17696 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
17697 assert_eq!(highlighted_edits.highlights.len(), 1);
17698 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
17699 assert_eq!(
17700 highlighted_edits.highlights[0].1.background_color,
17701 Some(cx.theme().status().created_background)
17702 );
17703 },
17704 )
17705 .await;
17706
17707 // Replacement
17708 assert_highlighted_edits(
17709 "This is a test.",
17710 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
17711 false,
17712 cx,
17713 |highlighted_edits, cx| {
17714 assert_eq!(highlighted_edits.text, "That is a test.");
17715 assert_eq!(highlighted_edits.highlights.len(), 1);
17716 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
17717 assert_eq!(
17718 highlighted_edits.highlights[0].1.background_color,
17719 Some(cx.theme().status().created_background)
17720 );
17721 },
17722 )
17723 .await;
17724
17725 // Multiple edits
17726 assert_highlighted_edits(
17727 "Hello, world!",
17728 vec![
17729 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
17730 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
17731 ],
17732 false,
17733 cx,
17734 |highlighted_edits, cx| {
17735 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
17736 assert_eq!(highlighted_edits.highlights.len(), 2);
17737 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
17738 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
17739 assert_eq!(
17740 highlighted_edits.highlights[0].1.background_color,
17741 Some(cx.theme().status().created_background)
17742 );
17743 assert_eq!(
17744 highlighted_edits.highlights[1].1.background_color,
17745 Some(cx.theme().status().created_background)
17746 );
17747 },
17748 )
17749 .await;
17750
17751 // Multiple lines with edits
17752 assert_highlighted_edits(
17753 "First line\nSecond line\nThird line\nFourth line",
17754 vec![
17755 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
17756 (
17757 Point::new(2, 0)..Point::new(2, 10),
17758 "New third line".to_string(),
17759 ),
17760 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
17761 ],
17762 false,
17763 cx,
17764 |highlighted_edits, cx| {
17765 assert_eq!(
17766 highlighted_edits.text,
17767 "Second modified\nNew third line\nFourth updated line"
17768 );
17769 assert_eq!(highlighted_edits.highlights.len(), 3);
17770 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
17771 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
17772 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
17773 for highlight in &highlighted_edits.highlights {
17774 assert_eq!(
17775 highlight.1.background_color,
17776 Some(cx.theme().status().created_background)
17777 );
17778 }
17779 },
17780 )
17781 .await;
17782}
17783
17784#[gpui::test]
17785async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
17786 init_test(cx, |_| {});
17787
17788 // Deletion
17789 assert_highlighted_edits(
17790 "Hello, world!",
17791 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
17792 true,
17793 cx,
17794 |highlighted_edits, cx| {
17795 assert_eq!(highlighted_edits.text, "Hello, world!");
17796 assert_eq!(highlighted_edits.highlights.len(), 1);
17797 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
17798 assert_eq!(
17799 highlighted_edits.highlights[0].1.background_color,
17800 Some(cx.theme().status().deleted_background)
17801 );
17802 },
17803 )
17804 .await;
17805
17806 // Insertion
17807 assert_highlighted_edits(
17808 "Hello, world!",
17809 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
17810 true,
17811 cx,
17812 |highlighted_edits, cx| {
17813 assert_eq!(highlighted_edits.highlights.len(), 1);
17814 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
17815 assert_eq!(
17816 highlighted_edits.highlights[0].1.background_color,
17817 Some(cx.theme().status().created_background)
17818 );
17819 },
17820 )
17821 .await;
17822}
17823
17824async fn assert_highlighted_edits(
17825 text: &str,
17826 edits: Vec<(Range<Point>, String)>,
17827 include_deletions: bool,
17828 cx: &mut TestAppContext,
17829 assertion_fn: impl Fn(HighlightedText, &App),
17830) {
17831 let window = cx.add_window(|window, cx| {
17832 let buffer = MultiBuffer::build_simple(text, cx);
17833 Editor::new(EditorMode::Full, buffer, None, window, cx)
17834 });
17835 let cx = &mut VisualTestContext::from_window(*window, cx);
17836
17837 let (buffer, snapshot) = window
17838 .update(cx, |editor, _window, cx| {
17839 (
17840 editor.buffer().clone(),
17841 editor.buffer().read(cx).snapshot(cx),
17842 )
17843 })
17844 .unwrap();
17845
17846 let edits = edits
17847 .into_iter()
17848 .map(|(range, edit)| {
17849 (
17850 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
17851 edit,
17852 )
17853 })
17854 .collect::<Vec<_>>();
17855
17856 let text_anchor_edits = edits
17857 .clone()
17858 .into_iter()
17859 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
17860 .collect::<Vec<_>>();
17861
17862 let edit_preview = window
17863 .update(cx, |_, _window, cx| {
17864 buffer
17865 .read(cx)
17866 .as_singleton()
17867 .unwrap()
17868 .read(cx)
17869 .preview_edits(text_anchor_edits.into(), cx)
17870 })
17871 .unwrap()
17872 .await;
17873
17874 cx.update(|_window, cx| {
17875 let highlighted_edits = inline_completion_edit_text(
17876 &snapshot.as_singleton().unwrap().2,
17877 &edits,
17878 &edit_preview,
17879 include_deletions,
17880 cx,
17881 );
17882 assertion_fn(highlighted_edits, cx)
17883 });
17884}
17885
17886#[track_caller]
17887fn assert_breakpoint(
17888 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
17889 path: &Arc<Path>,
17890 expected: Vec<(u32, Breakpoint)>,
17891) {
17892 if expected.len() == 0usize {
17893 assert!(!breakpoints.contains_key(path), "{}", path.display());
17894 } else {
17895 let mut breakpoint = breakpoints
17896 .get(path)
17897 .unwrap()
17898 .into_iter()
17899 .map(|breakpoint| {
17900 (
17901 breakpoint.row,
17902 Breakpoint {
17903 message: breakpoint.message.clone(),
17904 state: breakpoint.state,
17905 condition: breakpoint.condition.clone(),
17906 hit_condition: breakpoint.hit_condition.clone(),
17907 },
17908 )
17909 })
17910 .collect::<Vec<_>>();
17911
17912 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
17913
17914 assert_eq!(expected, breakpoint);
17915 }
17916}
17917
17918fn add_log_breakpoint_at_cursor(
17919 editor: &mut Editor,
17920 log_message: &str,
17921 window: &mut Window,
17922 cx: &mut Context<Editor>,
17923) {
17924 let (anchor, bp) = editor
17925 .breakpoint_at_cursor_head(window, cx)
17926 .unwrap_or_else(|| {
17927 let cursor_position: Point = editor.selections.newest(cx).head();
17928
17929 let breakpoint_position = editor
17930 .snapshot(window, cx)
17931 .display_snapshot
17932 .buffer_snapshot
17933 .anchor_before(Point::new(cursor_position.row, 0));
17934
17935 (breakpoint_position, Breakpoint::new_log(&log_message))
17936 });
17937
17938 editor.edit_breakpoint_at_anchor(
17939 anchor,
17940 bp,
17941 BreakpointEditAction::EditLogMessage(log_message.into()),
17942 cx,
17943 );
17944}
17945
17946#[gpui::test]
17947async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
17948 init_test(cx, |_| {});
17949
17950 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
17951 let fs = FakeFs::new(cx.executor());
17952 fs.insert_tree(
17953 path!("/a"),
17954 json!({
17955 "main.rs": sample_text,
17956 }),
17957 )
17958 .await;
17959 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17960 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17961 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17962
17963 let fs = FakeFs::new(cx.executor());
17964 fs.insert_tree(
17965 path!("/a"),
17966 json!({
17967 "main.rs": sample_text,
17968 }),
17969 )
17970 .await;
17971 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17972 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17973 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17974 let worktree_id = workspace
17975 .update(cx, |workspace, _window, cx| {
17976 workspace.project().update(cx, |project, cx| {
17977 project.worktrees(cx).next().unwrap().read(cx).id()
17978 })
17979 })
17980 .unwrap();
17981
17982 let buffer = project
17983 .update(cx, |project, cx| {
17984 project.open_buffer((worktree_id, "main.rs"), cx)
17985 })
17986 .await
17987 .unwrap();
17988
17989 let (editor, cx) = cx.add_window_view(|window, cx| {
17990 Editor::new(
17991 EditorMode::Full,
17992 MultiBuffer::build_from_buffer(buffer, cx),
17993 Some(project.clone()),
17994 window,
17995 cx,
17996 )
17997 });
17998
17999 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18000 let abs_path = project.read_with(cx, |project, cx| {
18001 project
18002 .absolute_path(&project_path, cx)
18003 .map(|path_buf| Arc::from(path_buf.to_owned()))
18004 .unwrap()
18005 });
18006
18007 // assert we can add breakpoint on the first line
18008 editor.update_in(cx, |editor, window, cx| {
18009 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18010 editor.move_to_end(&MoveToEnd, window, cx);
18011 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18012 });
18013
18014 let breakpoints = editor.update(cx, |editor, cx| {
18015 editor
18016 .breakpoint_store()
18017 .as_ref()
18018 .unwrap()
18019 .read(cx)
18020 .all_breakpoints(cx)
18021 .clone()
18022 });
18023
18024 assert_eq!(1, breakpoints.len());
18025 assert_breakpoint(
18026 &breakpoints,
18027 &abs_path,
18028 vec![
18029 (0, Breakpoint::new_standard()),
18030 (3, Breakpoint::new_standard()),
18031 ],
18032 );
18033
18034 editor.update_in(cx, |editor, window, cx| {
18035 editor.move_to_beginning(&MoveToBeginning, window, cx);
18036 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18037 });
18038
18039 let breakpoints = editor.update(cx, |editor, cx| {
18040 editor
18041 .breakpoint_store()
18042 .as_ref()
18043 .unwrap()
18044 .read(cx)
18045 .all_breakpoints(cx)
18046 .clone()
18047 });
18048
18049 assert_eq!(1, breakpoints.len());
18050 assert_breakpoint(
18051 &breakpoints,
18052 &abs_path,
18053 vec![(3, Breakpoint::new_standard())],
18054 );
18055
18056 editor.update_in(cx, |editor, window, cx| {
18057 editor.move_to_end(&MoveToEnd, window, cx);
18058 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18059 });
18060
18061 let breakpoints = editor.update(cx, |editor, cx| {
18062 editor
18063 .breakpoint_store()
18064 .as_ref()
18065 .unwrap()
18066 .read(cx)
18067 .all_breakpoints(cx)
18068 .clone()
18069 });
18070
18071 assert_eq!(0, breakpoints.len());
18072 assert_breakpoint(&breakpoints, &abs_path, vec![]);
18073}
18074
18075#[gpui::test]
18076async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
18077 init_test(cx, |_| {});
18078
18079 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18080
18081 let fs = FakeFs::new(cx.executor());
18082 fs.insert_tree(
18083 path!("/a"),
18084 json!({
18085 "main.rs": sample_text,
18086 }),
18087 )
18088 .await;
18089 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18090 let (workspace, cx) =
18091 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
18092
18093 let worktree_id = workspace.update(cx, |workspace, cx| {
18094 workspace.project().update(cx, |project, cx| {
18095 project.worktrees(cx).next().unwrap().read(cx).id()
18096 })
18097 });
18098
18099 let buffer = project
18100 .update(cx, |project, cx| {
18101 project.open_buffer((worktree_id, "main.rs"), cx)
18102 })
18103 .await
18104 .unwrap();
18105
18106 let (editor, cx) = cx.add_window_view(|window, cx| {
18107 Editor::new(
18108 EditorMode::Full,
18109 MultiBuffer::build_from_buffer(buffer, cx),
18110 Some(project.clone()),
18111 window,
18112 cx,
18113 )
18114 });
18115
18116 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18117 let abs_path = project.read_with(cx, |project, cx| {
18118 project
18119 .absolute_path(&project_path, cx)
18120 .map(|path_buf| Arc::from(path_buf.to_owned()))
18121 .unwrap()
18122 });
18123
18124 editor.update_in(cx, |editor, window, cx| {
18125 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
18126 });
18127
18128 let breakpoints = editor.update(cx, |editor, cx| {
18129 editor
18130 .breakpoint_store()
18131 .as_ref()
18132 .unwrap()
18133 .read(cx)
18134 .all_breakpoints(cx)
18135 .clone()
18136 });
18137
18138 assert_breakpoint(
18139 &breakpoints,
18140 &abs_path,
18141 vec![(0, Breakpoint::new_log("hello world"))],
18142 );
18143
18144 // Removing a log message from a log breakpoint should remove it
18145 editor.update_in(cx, |editor, window, cx| {
18146 add_log_breakpoint_at_cursor(editor, "", window, cx);
18147 });
18148
18149 let breakpoints = editor.update(cx, |editor, cx| {
18150 editor
18151 .breakpoint_store()
18152 .as_ref()
18153 .unwrap()
18154 .read(cx)
18155 .all_breakpoints(cx)
18156 .clone()
18157 });
18158
18159 assert_breakpoint(&breakpoints, &abs_path, vec![]);
18160
18161 editor.update_in(cx, |editor, window, cx| {
18162 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18163 editor.move_to_end(&MoveToEnd, window, cx);
18164 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18165 // Not adding a log message to a standard breakpoint shouldn't remove it
18166 add_log_breakpoint_at_cursor(editor, "", window, cx);
18167 });
18168
18169 let breakpoints = editor.update(cx, |editor, cx| {
18170 editor
18171 .breakpoint_store()
18172 .as_ref()
18173 .unwrap()
18174 .read(cx)
18175 .all_breakpoints(cx)
18176 .clone()
18177 });
18178
18179 assert_breakpoint(
18180 &breakpoints,
18181 &abs_path,
18182 vec![
18183 (0, Breakpoint::new_standard()),
18184 (3, Breakpoint::new_standard()),
18185 ],
18186 );
18187
18188 editor.update_in(cx, |editor, window, cx| {
18189 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
18190 });
18191
18192 let breakpoints = editor.update(cx, |editor, cx| {
18193 editor
18194 .breakpoint_store()
18195 .as_ref()
18196 .unwrap()
18197 .read(cx)
18198 .all_breakpoints(cx)
18199 .clone()
18200 });
18201
18202 assert_breakpoint(
18203 &breakpoints,
18204 &abs_path,
18205 vec![
18206 (0, Breakpoint::new_standard()),
18207 (3, Breakpoint::new_log("hello world")),
18208 ],
18209 );
18210
18211 editor.update_in(cx, |editor, window, cx| {
18212 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
18213 });
18214
18215 let breakpoints = editor.update(cx, |editor, cx| {
18216 editor
18217 .breakpoint_store()
18218 .as_ref()
18219 .unwrap()
18220 .read(cx)
18221 .all_breakpoints(cx)
18222 .clone()
18223 });
18224
18225 assert_breakpoint(
18226 &breakpoints,
18227 &abs_path,
18228 vec![
18229 (0, Breakpoint::new_standard()),
18230 (3, Breakpoint::new_log("hello Earth!!")),
18231 ],
18232 );
18233}
18234
18235/// This also tests that Editor::breakpoint_at_cursor_head is working properly
18236/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
18237/// or when breakpoints were placed out of order. This tests for a regression too
18238#[gpui::test]
18239async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
18240 init_test(cx, |_| {});
18241
18242 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18243 let fs = FakeFs::new(cx.executor());
18244 fs.insert_tree(
18245 path!("/a"),
18246 json!({
18247 "main.rs": sample_text,
18248 }),
18249 )
18250 .await;
18251 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18252 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18253 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18254
18255 let fs = FakeFs::new(cx.executor());
18256 fs.insert_tree(
18257 path!("/a"),
18258 json!({
18259 "main.rs": sample_text,
18260 }),
18261 )
18262 .await;
18263 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18264 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18265 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18266 let worktree_id = workspace
18267 .update(cx, |workspace, _window, cx| {
18268 workspace.project().update(cx, |project, cx| {
18269 project.worktrees(cx).next().unwrap().read(cx).id()
18270 })
18271 })
18272 .unwrap();
18273
18274 let buffer = project
18275 .update(cx, |project, cx| {
18276 project.open_buffer((worktree_id, "main.rs"), cx)
18277 })
18278 .await
18279 .unwrap();
18280
18281 let (editor, cx) = cx.add_window_view(|window, cx| {
18282 Editor::new(
18283 EditorMode::Full,
18284 MultiBuffer::build_from_buffer(buffer, cx),
18285 Some(project.clone()),
18286 window,
18287 cx,
18288 )
18289 });
18290
18291 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18292 let abs_path = project.read_with(cx, |project, cx| {
18293 project
18294 .absolute_path(&project_path, cx)
18295 .map(|path_buf| Arc::from(path_buf.to_owned()))
18296 .unwrap()
18297 });
18298
18299 // assert we can add breakpoint on the first line
18300 editor.update_in(cx, |editor, window, cx| {
18301 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18302 editor.move_to_end(&MoveToEnd, window, cx);
18303 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18304 editor.move_up(&MoveUp, window, cx);
18305 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18306 });
18307
18308 let breakpoints = editor.update(cx, |editor, cx| {
18309 editor
18310 .breakpoint_store()
18311 .as_ref()
18312 .unwrap()
18313 .read(cx)
18314 .all_breakpoints(cx)
18315 .clone()
18316 });
18317
18318 assert_eq!(1, breakpoints.len());
18319 assert_breakpoint(
18320 &breakpoints,
18321 &abs_path,
18322 vec![
18323 (0, Breakpoint::new_standard()),
18324 (2, Breakpoint::new_standard()),
18325 (3, Breakpoint::new_standard()),
18326 ],
18327 );
18328
18329 editor.update_in(cx, |editor, window, cx| {
18330 editor.move_to_beginning(&MoveToBeginning, window, cx);
18331 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18332 editor.move_to_end(&MoveToEnd, window, cx);
18333 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18334 // Disabling a breakpoint that doesn't exist should do nothing
18335 editor.move_up(&MoveUp, window, cx);
18336 editor.move_up(&MoveUp, window, cx);
18337 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18338 });
18339
18340 let breakpoints = editor.update(cx, |editor, cx| {
18341 editor
18342 .breakpoint_store()
18343 .as_ref()
18344 .unwrap()
18345 .read(cx)
18346 .all_breakpoints(cx)
18347 .clone()
18348 });
18349
18350 let disable_breakpoint = {
18351 let mut bp = Breakpoint::new_standard();
18352 bp.state = BreakpointState::Disabled;
18353 bp
18354 };
18355
18356 assert_eq!(1, breakpoints.len());
18357 assert_breakpoint(
18358 &breakpoints,
18359 &abs_path,
18360 vec![
18361 (0, disable_breakpoint.clone()),
18362 (2, Breakpoint::new_standard()),
18363 (3, disable_breakpoint.clone()),
18364 ],
18365 );
18366
18367 editor.update_in(cx, |editor, window, cx| {
18368 editor.move_to_beginning(&MoveToBeginning, window, cx);
18369 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
18370 editor.move_to_end(&MoveToEnd, window, cx);
18371 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
18372 editor.move_up(&MoveUp, window, cx);
18373 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18374 });
18375
18376 let breakpoints = editor.update(cx, |editor, cx| {
18377 editor
18378 .breakpoint_store()
18379 .as_ref()
18380 .unwrap()
18381 .read(cx)
18382 .all_breakpoints(cx)
18383 .clone()
18384 });
18385
18386 assert_eq!(1, breakpoints.len());
18387 assert_breakpoint(
18388 &breakpoints,
18389 &abs_path,
18390 vec![
18391 (0, Breakpoint::new_standard()),
18392 (2, disable_breakpoint),
18393 (3, Breakpoint::new_standard()),
18394 ],
18395 );
18396}
18397
18398#[gpui::test]
18399async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
18400 init_test(cx, |_| {});
18401 let capabilities = lsp::ServerCapabilities {
18402 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
18403 prepare_provider: Some(true),
18404 work_done_progress_options: Default::default(),
18405 })),
18406 ..Default::default()
18407 };
18408 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
18409
18410 cx.set_state(indoc! {"
18411 struct Fˇoo {}
18412 "});
18413
18414 cx.update_editor(|editor, _, cx| {
18415 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
18416 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
18417 editor.highlight_background::<DocumentHighlightRead>(
18418 &[highlight_range],
18419 |c| c.editor_document_highlight_read_background,
18420 cx,
18421 );
18422 });
18423
18424 let mut prepare_rename_handler = cx
18425 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
18426 move |_, _, _| async move {
18427 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
18428 start: lsp::Position {
18429 line: 0,
18430 character: 7,
18431 },
18432 end: lsp::Position {
18433 line: 0,
18434 character: 10,
18435 },
18436 })))
18437 },
18438 );
18439 let prepare_rename_task = cx
18440 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
18441 .expect("Prepare rename was not started");
18442 prepare_rename_handler.next().await.unwrap();
18443 prepare_rename_task.await.expect("Prepare rename failed");
18444
18445 let mut rename_handler =
18446 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
18447 let edit = lsp::TextEdit {
18448 range: lsp::Range {
18449 start: lsp::Position {
18450 line: 0,
18451 character: 7,
18452 },
18453 end: lsp::Position {
18454 line: 0,
18455 character: 10,
18456 },
18457 },
18458 new_text: "FooRenamed".to_string(),
18459 };
18460 Ok(Some(lsp::WorkspaceEdit::new(
18461 // Specify the same edit twice
18462 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
18463 )))
18464 });
18465 let rename_task = cx
18466 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
18467 .expect("Confirm rename was not started");
18468 rename_handler.next().await.unwrap();
18469 rename_task.await.expect("Confirm rename failed");
18470 cx.run_until_parked();
18471
18472 // Despite two edits, only one is actually applied as those are identical
18473 cx.assert_editor_state(indoc! {"
18474 struct FooRenamedˇ {}
18475 "});
18476}
18477
18478#[gpui::test]
18479async fn test_rename_without_prepare(cx: &mut TestAppContext) {
18480 init_test(cx, |_| {});
18481 // These capabilities indicate that the server does not support prepare rename.
18482 let capabilities = lsp::ServerCapabilities {
18483 rename_provider: Some(lsp::OneOf::Left(true)),
18484 ..Default::default()
18485 };
18486 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
18487
18488 cx.set_state(indoc! {"
18489 struct Fˇoo {}
18490 "});
18491
18492 cx.update_editor(|editor, _window, cx| {
18493 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
18494 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
18495 editor.highlight_background::<DocumentHighlightRead>(
18496 &[highlight_range],
18497 |c| c.editor_document_highlight_read_background,
18498 cx,
18499 );
18500 });
18501
18502 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
18503 .expect("Prepare rename was not started")
18504 .await
18505 .expect("Prepare rename failed");
18506
18507 let mut rename_handler =
18508 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
18509 let edit = lsp::TextEdit {
18510 range: lsp::Range {
18511 start: lsp::Position {
18512 line: 0,
18513 character: 7,
18514 },
18515 end: lsp::Position {
18516 line: 0,
18517 character: 10,
18518 },
18519 },
18520 new_text: "FooRenamed".to_string(),
18521 };
18522 Ok(Some(lsp::WorkspaceEdit::new(
18523 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
18524 )))
18525 });
18526 let rename_task = cx
18527 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
18528 .expect("Confirm rename was not started");
18529 rename_handler.next().await.unwrap();
18530 rename_task.await.expect("Confirm rename failed");
18531 cx.run_until_parked();
18532
18533 // Correct range is renamed, as `surrounding_word` is used to find it.
18534 cx.assert_editor_state(indoc! {"
18535 struct FooRenamedˇ {}
18536 "});
18537}
18538
18539#[gpui::test]
18540async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
18541 init_test(cx, |_| {});
18542 let mut cx = EditorTestContext::new(cx).await;
18543
18544 let language = Arc::new(
18545 Language::new(
18546 LanguageConfig::default(),
18547 Some(tree_sitter_html::LANGUAGE.into()),
18548 )
18549 .with_brackets_query(
18550 r#"
18551 ("<" @open "/>" @close)
18552 ("</" @open ">" @close)
18553 ("<" @open ">" @close)
18554 ("\"" @open "\"" @close)
18555 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
18556 "#,
18557 )
18558 .unwrap(),
18559 );
18560 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
18561
18562 cx.set_state(indoc! {"
18563 <span>ˇ</span>
18564 "});
18565 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18566 cx.assert_editor_state(indoc! {"
18567 <span>
18568 ˇ
18569 </span>
18570 "});
18571
18572 cx.set_state(indoc! {"
18573 <span><span></span>ˇ</span>
18574 "});
18575 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18576 cx.assert_editor_state(indoc! {"
18577 <span><span></span>
18578 ˇ</span>
18579 "});
18580
18581 cx.set_state(indoc! {"
18582 <span>ˇ
18583 </span>
18584 "});
18585 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18586 cx.assert_editor_state(indoc! {"
18587 <span>
18588 ˇ
18589 </span>
18590 "});
18591}
18592
18593#[gpui::test(iterations = 10)]
18594async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
18595 init_test(cx, |_| {});
18596
18597 let fs = FakeFs::new(cx.executor());
18598 fs.insert_tree(
18599 path!("/dir"),
18600 json!({
18601 "a.ts": "a",
18602 }),
18603 )
18604 .await;
18605
18606 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
18607 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18608 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18609
18610 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18611 language_registry.add(Arc::new(Language::new(
18612 LanguageConfig {
18613 name: "TypeScript".into(),
18614 matcher: LanguageMatcher {
18615 path_suffixes: vec!["ts".to_string()],
18616 ..Default::default()
18617 },
18618 ..Default::default()
18619 },
18620 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18621 )));
18622 let mut fake_language_servers = language_registry.register_fake_lsp(
18623 "TypeScript",
18624 FakeLspAdapter {
18625 capabilities: lsp::ServerCapabilities {
18626 code_lens_provider: Some(lsp::CodeLensOptions {
18627 resolve_provider: Some(true),
18628 }),
18629 execute_command_provider: Some(lsp::ExecuteCommandOptions {
18630 commands: vec!["_the/command".to_string()],
18631 ..lsp::ExecuteCommandOptions::default()
18632 }),
18633 ..lsp::ServerCapabilities::default()
18634 },
18635 ..FakeLspAdapter::default()
18636 },
18637 );
18638
18639 let (buffer, _handle) = project
18640 .update(cx, |p, cx| {
18641 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
18642 })
18643 .await
18644 .unwrap();
18645 cx.executor().run_until_parked();
18646
18647 let fake_server = fake_language_servers.next().await.unwrap();
18648
18649 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
18650 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
18651 drop(buffer_snapshot);
18652 let actions = cx
18653 .update_window(*workspace, |_, window, cx| {
18654 project.code_actions(&buffer, anchor..anchor, window, cx)
18655 })
18656 .unwrap();
18657
18658 fake_server
18659 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
18660 Ok(Some(vec![
18661 lsp::CodeLens {
18662 range: lsp::Range::default(),
18663 command: Some(lsp::Command {
18664 title: "Code lens command".to_owned(),
18665 command: "_the/command".to_owned(),
18666 arguments: None,
18667 }),
18668 data: None,
18669 },
18670 lsp::CodeLens {
18671 range: lsp::Range::default(),
18672 command: Some(lsp::Command {
18673 title: "Command not in capabilities".to_owned(),
18674 command: "not in capabilities".to_owned(),
18675 arguments: None,
18676 }),
18677 data: None,
18678 },
18679 lsp::CodeLens {
18680 range: lsp::Range {
18681 start: lsp::Position {
18682 line: 1,
18683 character: 1,
18684 },
18685 end: lsp::Position {
18686 line: 1,
18687 character: 1,
18688 },
18689 },
18690 command: Some(lsp::Command {
18691 title: "Command not in range".to_owned(),
18692 command: "_the/command".to_owned(),
18693 arguments: None,
18694 }),
18695 data: None,
18696 },
18697 ]))
18698 })
18699 .next()
18700 .await;
18701
18702 let actions = actions.await.unwrap();
18703 assert_eq!(
18704 actions.len(),
18705 1,
18706 "Should have only one valid action for the 0..0 range"
18707 );
18708 let action = actions[0].clone();
18709 let apply = project.update(cx, |project, cx| {
18710 project.apply_code_action(buffer.clone(), action, true, cx)
18711 });
18712
18713 // Resolving the code action does not populate its edits. In absence of
18714 // edits, we must execute the given command.
18715 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
18716 |mut lens, _| async move {
18717 let lens_command = lens.command.as_mut().expect("should have a command");
18718 assert_eq!(lens_command.title, "Code lens command");
18719 lens_command.arguments = Some(vec![json!("the-argument")]);
18720 Ok(lens)
18721 },
18722 );
18723
18724 // While executing the command, the language server sends the editor
18725 // a `workspaceEdit` request.
18726 fake_server
18727 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
18728 let fake = fake_server.clone();
18729 move |params, _| {
18730 assert_eq!(params.command, "_the/command");
18731 let fake = fake.clone();
18732 async move {
18733 fake.server
18734 .request::<lsp::request::ApplyWorkspaceEdit>(
18735 lsp::ApplyWorkspaceEditParams {
18736 label: None,
18737 edit: lsp::WorkspaceEdit {
18738 changes: Some(
18739 [(
18740 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
18741 vec![lsp::TextEdit {
18742 range: lsp::Range::new(
18743 lsp::Position::new(0, 0),
18744 lsp::Position::new(0, 0),
18745 ),
18746 new_text: "X".into(),
18747 }],
18748 )]
18749 .into_iter()
18750 .collect(),
18751 ),
18752 ..Default::default()
18753 },
18754 },
18755 )
18756 .await
18757 .unwrap();
18758 Ok(Some(json!(null)))
18759 }
18760 }
18761 })
18762 .next()
18763 .await;
18764
18765 // Applying the code lens command returns a project transaction containing the edits
18766 // sent by the language server in its `workspaceEdit` request.
18767 let transaction = apply.await.unwrap();
18768 assert!(transaction.0.contains_key(&buffer));
18769 buffer.update(cx, |buffer, cx| {
18770 assert_eq!(buffer.text(), "Xa");
18771 buffer.undo(cx);
18772 assert_eq!(buffer.text(), "a");
18773 });
18774}
18775
18776#[gpui::test]
18777async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
18778 init_test(cx, |_| {});
18779
18780 let fs = FakeFs::new(cx.executor());
18781 let main_text = r#"fn main() {
18782println!("1");
18783println!("2");
18784println!("3");
18785println!("4");
18786println!("5");
18787}"#;
18788 let lib_text = "mod foo {}";
18789 fs.insert_tree(
18790 path!("/a"),
18791 json!({
18792 "lib.rs": lib_text,
18793 "main.rs": main_text,
18794 }),
18795 )
18796 .await;
18797
18798 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18799 let (workspace, cx) =
18800 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
18801 let worktree_id = workspace.update(cx, |workspace, cx| {
18802 workspace.project().update(cx, |project, cx| {
18803 project.worktrees(cx).next().unwrap().read(cx).id()
18804 })
18805 });
18806
18807 let expected_ranges = vec![
18808 Point::new(0, 0)..Point::new(0, 0),
18809 Point::new(1, 0)..Point::new(1, 1),
18810 Point::new(2, 0)..Point::new(2, 2),
18811 Point::new(3, 0)..Point::new(3, 3),
18812 ];
18813
18814 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
18815 let editor_1 = workspace
18816 .update_in(cx, |workspace, window, cx| {
18817 workspace.open_path(
18818 (worktree_id, "main.rs"),
18819 Some(pane_1.downgrade()),
18820 true,
18821 window,
18822 cx,
18823 )
18824 })
18825 .unwrap()
18826 .await
18827 .downcast::<Editor>()
18828 .unwrap();
18829 pane_1.update(cx, |pane, cx| {
18830 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18831 open_editor.update(cx, |editor, cx| {
18832 assert_eq!(
18833 editor.display_text(cx),
18834 main_text,
18835 "Original main.rs text on initial open",
18836 );
18837 assert_eq!(
18838 editor
18839 .selections
18840 .all::<Point>(cx)
18841 .into_iter()
18842 .map(|s| s.range())
18843 .collect::<Vec<_>>(),
18844 vec![Point::zero()..Point::zero()],
18845 "Default selections on initial open",
18846 );
18847 })
18848 });
18849 editor_1.update_in(cx, |editor, window, cx| {
18850 editor.change_selections(None, window, cx, |s| {
18851 s.select_ranges(expected_ranges.clone());
18852 });
18853 });
18854
18855 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
18856 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
18857 });
18858 let editor_2 = workspace
18859 .update_in(cx, |workspace, window, cx| {
18860 workspace.open_path(
18861 (worktree_id, "main.rs"),
18862 Some(pane_2.downgrade()),
18863 true,
18864 window,
18865 cx,
18866 )
18867 })
18868 .unwrap()
18869 .await
18870 .downcast::<Editor>()
18871 .unwrap();
18872 pane_2.update(cx, |pane, cx| {
18873 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18874 open_editor.update(cx, |editor, cx| {
18875 assert_eq!(
18876 editor.display_text(cx),
18877 main_text,
18878 "Original main.rs text on initial open in another panel",
18879 );
18880 assert_eq!(
18881 editor
18882 .selections
18883 .all::<Point>(cx)
18884 .into_iter()
18885 .map(|s| s.range())
18886 .collect::<Vec<_>>(),
18887 vec![Point::zero()..Point::zero()],
18888 "Default selections on initial open in another panel",
18889 );
18890 })
18891 });
18892
18893 editor_2.update_in(cx, |editor, window, cx| {
18894 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
18895 });
18896
18897 let _other_editor_1 = workspace
18898 .update_in(cx, |workspace, window, cx| {
18899 workspace.open_path(
18900 (worktree_id, "lib.rs"),
18901 Some(pane_1.downgrade()),
18902 true,
18903 window,
18904 cx,
18905 )
18906 })
18907 .unwrap()
18908 .await
18909 .downcast::<Editor>()
18910 .unwrap();
18911 pane_1
18912 .update_in(cx, |pane, window, cx| {
18913 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
18914 .unwrap()
18915 })
18916 .await
18917 .unwrap();
18918 drop(editor_1);
18919 pane_1.update(cx, |pane, cx| {
18920 pane.active_item()
18921 .unwrap()
18922 .downcast::<Editor>()
18923 .unwrap()
18924 .update(cx, |editor, cx| {
18925 assert_eq!(
18926 editor.display_text(cx),
18927 lib_text,
18928 "Other file should be open and active",
18929 );
18930 });
18931 assert_eq!(pane.items().count(), 1, "No other editors should be open");
18932 });
18933
18934 let _other_editor_2 = workspace
18935 .update_in(cx, |workspace, window, cx| {
18936 workspace.open_path(
18937 (worktree_id, "lib.rs"),
18938 Some(pane_2.downgrade()),
18939 true,
18940 window,
18941 cx,
18942 )
18943 })
18944 .unwrap()
18945 .await
18946 .downcast::<Editor>()
18947 .unwrap();
18948 pane_2
18949 .update_in(cx, |pane, window, cx| {
18950 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
18951 .unwrap()
18952 })
18953 .await
18954 .unwrap();
18955 drop(editor_2);
18956 pane_2.update(cx, |pane, cx| {
18957 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18958 open_editor.update(cx, |editor, cx| {
18959 assert_eq!(
18960 editor.display_text(cx),
18961 lib_text,
18962 "Other file should be open and active in another panel too",
18963 );
18964 });
18965 assert_eq!(
18966 pane.items().count(),
18967 1,
18968 "No other editors should be open in another pane",
18969 );
18970 });
18971
18972 let _editor_1_reopened = workspace
18973 .update_in(cx, |workspace, window, cx| {
18974 workspace.open_path(
18975 (worktree_id, "main.rs"),
18976 Some(pane_1.downgrade()),
18977 true,
18978 window,
18979 cx,
18980 )
18981 })
18982 .unwrap()
18983 .await
18984 .downcast::<Editor>()
18985 .unwrap();
18986 let _editor_2_reopened = workspace
18987 .update_in(cx, |workspace, window, cx| {
18988 workspace.open_path(
18989 (worktree_id, "main.rs"),
18990 Some(pane_2.downgrade()),
18991 true,
18992 window,
18993 cx,
18994 )
18995 })
18996 .unwrap()
18997 .await
18998 .downcast::<Editor>()
18999 .unwrap();
19000 pane_1.update(cx, |pane, cx| {
19001 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19002 open_editor.update(cx, |editor, cx| {
19003 assert_eq!(
19004 editor.display_text(cx),
19005 main_text,
19006 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
19007 );
19008 assert_eq!(
19009 editor
19010 .selections
19011 .all::<Point>(cx)
19012 .into_iter()
19013 .map(|s| s.range())
19014 .collect::<Vec<_>>(),
19015 expected_ranges,
19016 "Previous editor in the 1st panel had selections and should get them restored on reopen",
19017 );
19018 })
19019 });
19020 pane_2.update(cx, |pane, cx| {
19021 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19022 open_editor.update(cx, |editor, cx| {
19023 assert_eq!(
19024 editor.display_text(cx),
19025 r#"fn main() {
19026⋯rintln!("1");
19027⋯intln!("2");
19028⋯ntln!("3");
19029println!("4");
19030println!("5");
19031}"#,
19032 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
19033 );
19034 assert_eq!(
19035 editor
19036 .selections
19037 .all::<Point>(cx)
19038 .into_iter()
19039 .map(|s| s.range())
19040 .collect::<Vec<_>>(),
19041 vec![Point::zero()..Point::zero()],
19042 "Previous editor in the 2nd pane had no selections changed hence should restore none",
19043 );
19044 })
19045 });
19046}
19047
19048#[gpui::test]
19049async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
19050 init_test(cx, |_| {});
19051
19052 let fs = FakeFs::new(cx.executor());
19053 let main_text = r#"fn main() {
19054println!("1");
19055println!("2");
19056println!("3");
19057println!("4");
19058println!("5");
19059}"#;
19060 let lib_text = "mod foo {}";
19061 fs.insert_tree(
19062 path!("/a"),
19063 json!({
19064 "lib.rs": lib_text,
19065 "main.rs": main_text,
19066 }),
19067 )
19068 .await;
19069
19070 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19071 let (workspace, cx) =
19072 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19073 let worktree_id = workspace.update(cx, |workspace, cx| {
19074 workspace.project().update(cx, |project, cx| {
19075 project.worktrees(cx).next().unwrap().read(cx).id()
19076 })
19077 });
19078
19079 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
19080 let editor = workspace
19081 .update_in(cx, |workspace, window, cx| {
19082 workspace.open_path(
19083 (worktree_id, "main.rs"),
19084 Some(pane.downgrade()),
19085 true,
19086 window,
19087 cx,
19088 )
19089 })
19090 .unwrap()
19091 .await
19092 .downcast::<Editor>()
19093 .unwrap();
19094 pane.update(cx, |pane, cx| {
19095 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19096 open_editor.update(cx, |editor, cx| {
19097 assert_eq!(
19098 editor.display_text(cx),
19099 main_text,
19100 "Original main.rs text on initial open",
19101 );
19102 })
19103 });
19104 editor.update_in(cx, |editor, window, cx| {
19105 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
19106 });
19107
19108 cx.update_global(|store: &mut SettingsStore, cx| {
19109 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
19110 s.restore_on_file_reopen = Some(false);
19111 });
19112 });
19113 editor.update_in(cx, |editor, window, cx| {
19114 editor.fold_ranges(
19115 vec![
19116 Point::new(1, 0)..Point::new(1, 1),
19117 Point::new(2, 0)..Point::new(2, 2),
19118 Point::new(3, 0)..Point::new(3, 3),
19119 ],
19120 false,
19121 window,
19122 cx,
19123 );
19124 });
19125 pane.update_in(cx, |pane, window, cx| {
19126 pane.close_all_items(&CloseAllItems::default(), window, cx)
19127 .unwrap()
19128 })
19129 .await
19130 .unwrap();
19131 pane.update(cx, |pane, _| {
19132 assert!(pane.active_item().is_none());
19133 });
19134 cx.update_global(|store: &mut SettingsStore, cx| {
19135 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
19136 s.restore_on_file_reopen = Some(true);
19137 });
19138 });
19139
19140 let _editor_reopened = workspace
19141 .update_in(cx, |workspace, window, cx| {
19142 workspace.open_path(
19143 (worktree_id, "main.rs"),
19144 Some(pane.downgrade()),
19145 true,
19146 window,
19147 cx,
19148 )
19149 })
19150 .unwrap()
19151 .await
19152 .downcast::<Editor>()
19153 .unwrap();
19154 pane.update(cx, |pane, cx| {
19155 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19156 open_editor.update(cx, |editor, cx| {
19157 assert_eq!(
19158 editor.display_text(cx),
19159 main_text,
19160 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
19161 );
19162 })
19163 });
19164}
19165
19166fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
19167 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
19168 point..point
19169}
19170
19171fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
19172 let (text, ranges) = marked_text_ranges(marked_text, true);
19173 assert_eq!(editor.text(cx), text);
19174 assert_eq!(
19175 editor.selections.ranges(cx),
19176 ranges,
19177 "Assert selections are {}",
19178 marked_text
19179 );
19180}
19181
19182pub fn handle_signature_help_request(
19183 cx: &mut EditorLspTestContext,
19184 mocked_response: lsp::SignatureHelp,
19185) -> impl Future<Output = ()> + use<> {
19186 let mut request =
19187 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
19188 let mocked_response = mocked_response.clone();
19189 async move { Ok(Some(mocked_response)) }
19190 });
19191
19192 async move {
19193 request.next().await;
19194 }
19195}
19196
19197/// Handle completion request passing a marked string specifying where the completion
19198/// should be triggered from using '|' character, what range should be replaced, and what completions
19199/// should be returned using '<' and '>' to delimit the range.
19200///
19201/// Also see `handle_completion_request_with_insert_and_replace`.
19202#[track_caller]
19203pub fn handle_completion_request(
19204 cx: &mut EditorLspTestContext,
19205 marked_string: &str,
19206 completions: Vec<&'static str>,
19207 counter: Arc<AtomicUsize>,
19208) -> impl Future<Output = ()> {
19209 let complete_from_marker: TextRangeMarker = '|'.into();
19210 let replace_range_marker: TextRangeMarker = ('<', '>').into();
19211 let (_, mut marked_ranges) = marked_text_ranges_by(
19212 marked_string,
19213 vec![complete_from_marker.clone(), replace_range_marker.clone()],
19214 );
19215
19216 let complete_from_position =
19217 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
19218 let replace_range =
19219 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
19220
19221 let mut request =
19222 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
19223 let completions = completions.clone();
19224 counter.fetch_add(1, atomic::Ordering::Release);
19225 async move {
19226 assert_eq!(params.text_document_position.text_document.uri, url.clone());
19227 assert_eq!(
19228 params.text_document_position.position,
19229 complete_from_position
19230 );
19231 Ok(Some(lsp::CompletionResponse::Array(
19232 completions
19233 .iter()
19234 .map(|completion_text| lsp::CompletionItem {
19235 label: completion_text.to_string(),
19236 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
19237 range: replace_range,
19238 new_text: completion_text.to_string(),
19239 })),
19240 ..Default::default()
19241 })
19242 .collect(),
19243 )))
19244 }
19245 });
19246
19247 async move {
19248 request.next().await;
19249 }
19250}
19251
19252/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
19253/// given instead, which also contains an `insert` range.
19254///
19255/// This function uses the cursor position to mimic what Rust-Analyzer provides as the `insert` range,
19256/// that is, `replace_range.start..cursor_pos`.
19257pub fn handle_completion_request_with_insert_and_replace(
19258 cx: &mut EditorLspTestContext,
19259 marked_string: &str,
19260 completions: Vec<&'static str>,
19261 counter: Arc<AtomicUsize>,
19262) -> impl Future<Output = ()> {
19263 let complete_from_marker: TextRangeMarker = '|'.into();
19264 let replace_range_marker: TextRangeMarker = ('<', '>').into();
19265 let (_, mut marked_ranges) = marked_text_ranges_by(
19266 marked_string,
19267 vec![complete_from_marker.clone(), replace_range_marker.clone()],
19268 );
19269
19270 let complete_from_position =
19271 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
19272 let replace_range =
19273 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
19274
19275 let mut request =
19276 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
19277 let completions = completions.clone();
19278 counter.fetch_add(1, atomic::Ordering::Release);
19279 async move {
19280 assert_eq!(params.text_document_position.text_document.uri, url.clone());
19281 assert_eq!(
19282 params.text_document_position.position, complete_from_position,
19283 "marker `|` position doesn't match",
19284 );
19285 Ok(Some(lsp::CompletionResponse::Array(
19286 completions
19287 .iter()
19288 .map(|completion_text| lsp::CompletionItem {
19289 label: completion_text.to_string(),
19290 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
19291 lsp::InsertReplaceEdit {
19292 insert: lsp::Range {
19293 start: replace_range.start,
19294 end: complete_from_position,
19295 },
19296 replace: replace_range,
19297 new_text: completion_text.to_string(),
19298 },
19299 )),
19300 ..Default::default()
19301 })
19302 .collect(),
19303 )))
19304 }
19305 });
19306
19307 async move {
19308 request.next().await;
19309 }
19310}
19311
19312fn handle_resolve_completion_request(
19313 cx: &mut EditorLspTestContext,
19314 edits: Option<Vec<(&'static str, &'static str)>>,
19315) -> impl Future<Output = ()> {
19316 let edits = edits.map(|edits| {
19317 edits
19318 .iter()
19319 .map(|(marked_string, new_text)| {
19320 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
19321 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
19322 lsp::TextEdit::new(replace_range, new_text.to_string())
19323 })
19324 .collect::<Vec<_>>()
19325 });
19326
19327 let mut request =
19328 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
19329 let edits = edits.clone();
19330 async move {
19331 Ok(lsp::CompletionItem {
19332 additional_text_edits: edits,
19333 ..Default::default()
19334 })
19335 }
19336 });
19337
19338 async move {
19339 request.next().await;
19340 }
19341}
19342
19343pub(crate) fn update_test_language_settings(
19344 cx: &mut TestAppContext,
19345 f: impl Fn(&mut AllLanguageSettingsContent),
19346) {
19347 cx.update(|cx| {
19348 SettingsStore::update_global(cx, |store, cx| {
19349 store.update_user_settings::<AllLanguageSettings>(cx, f);
19350 });
19351 });
19352}
19353
19354pub(crate) fn update_test_project_settings(
19355 cx: &mut TestAppContext,
19356 f: impl Fn(&mut ProjectSettings),
19357) {
19358 cx.update(|cx| {
19359 SettingsStore::update_global(cx, |store, cx| {
19360 store.update_user_settings::<ProjectSettings>(cx, f);
19361 });
19362 });
19363}
19364
19365pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
19366 cx.update(|cx| {
19367 assets::Assets.load_test_fonts(cx);
19368 let store = SettingsStore::test(cx);
19369 cx.set_global(store);
19370 theme::init(theme::LoadThemes::JustBase, cx);
19371 release_channel::init(SemanticVersion::default(), cx);
19372 client::init_settings(cx);
19373 language::init(cx);
19374 Project::init_settings(cx);
19375 workspace::init_settings(cx);
19376 crate::init(cx);
19377 });
19378
19379 update_test_language_settings(cx, f);
19380}
19381
19382#[track_caller]
19383fn assert_hunk_revert(
19384 not_reverted_text_with_selections: &str,
19385 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
19386 expected_reverted_text_with_selections: &str,
19387 base_text: &str,
19388 cx: &mut EditorLspTestContext,
19389) {
19390 cx.set_state(not_reverted_text_with_selections);
19391 cx.set_head_text(base_text);
19392 cx.executor().run_until_parked();
19393
19394 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
19395 let snapshot = editor.snapshot(window, cx);
19396 let reverted_hunk_statuses = snapshot
19397 .buffer_snapshot
19398 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
19399 .map(|hunk| hunk.status().kind)
19400 .collect::<Vec<_>>();
19401
19402 editor.git_restore(&Default::default(), window, cx);
19403 reverted_hunk_statuses
19404 });
19405 cx.executor().run_until_parked();
19406 cx.assert_editor_state(expected_reverted_text_with_selections);
19407 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
19408}