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_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
5878 init_test(cx, |_| {});
5879
5880 let mut cx = EditorLspTestContext::new_rust(
5881 lsp::ServerCapabilities {
5882 document_formatting_provider: Some(lsp::OneOf::Left(true)),
5883 ..Default::default()
5884 },
5885 cx,
5886 )
5887 .await;
5888
5889 cx.set_state(indoc! {"
5890 line 1
5891 line 2
5892 linˇe 3
5893 line 4
5894 line 5
5895 "});
5896
5897 // Make an edit
5898 cx.update_editor(|editor, window, cx| {
5899 editor.handle_input("X", window, cx);
5900 });
5901
5902 // Move cursor to a different position
5903 cx.update_editor(|editor, window, cx| {
5904 editor.change_selections(None, window, cx, |s| {
5905 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
5906 });
5907 });
5908
5909 cx.assert_editor_state(indoc! {"
5910 line 1
5911 line 2
5912 linXe 3
5913 line 4
5914 liˇne 5
5915 "});
5916
5917 cx.lsp
5918 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
5919 Ok(Some(vec![lsp::TextEdit::new(
5920 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
5921 "PREFIX ".to_string(),
5922 )]))
5923 });
5924
5925 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
5926 .unwrap()
5927 .await
5928 .unwrap();
5929
5930 cx.assert_editor_state(indoc! {"
5931 PREFIX line 1
5932 line 2
5933 linXe 3
5934 line 4
5935 liˇne 5
5936 "});
5937
5938 // Undo formatting
5939 cx.update_editor(|editor, window, cx| {
5940 editor.undo(&Default::default(), window, cx);
5941 });
5942
5943 // Verify cursor moved back to position after edit
5944 cx.assert_editor_state(indoc! {"
5945 line 1
5946 line 2
5947 linXˇe 3
5948 line 4
5949 line 5
5950 "});
5951}
5952
5953#[gpui::test]
5954async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
5955 init_test(cx, |_| {});
5956
5957 let mut cx = EditorTestContext::new(cx).await;
5958 cx.set_state(
5959 r#"let foo = 2;
5960lˇet foo = 2;
5961let fooˇ = 2;
5962let foo = 2;
5963let foo = ˇ2;"#,
5964 );
5965
5966 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5967 .unwrap();
5968 cx.assert_editor_state(
5969 r#"let foo = 2;
5970«letˇ» foo = 2;
5971let «fooˇ» = 2;
5972let foo = 2;
5973let foo = «2ˇ»;"#,
5974 );
5975
5976 // noop for multiple selections with different contents
5977 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5978 .unwrap();
5979 cx.assert_editor_state(
5980 r#"let foo = 2;
5981«letˇ» foo = 2;
5982let «fooˇ» = 2;
5983let foo = 2;
5984let foo = «2ˇ»;"#,
5985 );
5986}
5987
5988#[gpui::test]
5989async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
5990 init_test(cx, |_| {});
5991
5992 let mut cx =
5993 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
5994
5995 cx.assert_editor_state(indoc! {"
5996 ˇbbb
5997 ccc
5998
5999 bbb
6000 ccc
6001 "});
6002 cx.dispatch_action(SelectPrevious::default());
6003 cx.assert_editor_state(indoc! {"
6004 «bbbˇ»
6005 ccc
6006
6007 bbb
6008 ccc
6009 "});
6010 cx.dispatch_action(SelectPrevious::default());
6011 cx.assert_editor_state(indoc! {"
6012 «bbbˇ»
6013 ccc
6014
6015 «bbbˇ»
6016 ccc
6017 "});
6018}
6019
6020#[gpui::test]
6021async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
6022 init_test(cx, |_| {});
6023
6024 let mut cx = EditorTestContext::new(cx).await;
6025 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6026
6027 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6028 .unwrap();
6029 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6030
6031 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6032 .unwrap();
6033 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6034
6035 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6036 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6037
6038 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6039 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6040
6041 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6042 .unwrap();
6043 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
6044
6045 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6046 .unwrap();
6047 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndef«abcˇ»\n«abcˇ»");
6048
6049 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6050 .unwrap();
6051 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
6052}
6053
6054#[gpui::test]
6055async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
6056 init_test(cx, |_| {});
6057
6058 let mut cx = EditorTestContext::new(cx).await;
6059 cx.set_state("aˇ");
6060
6061 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6062 .unwrap();
6063 cx.assert_editor_state("«aˇ»");
6064 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6065 .unwrap();
6066 cx.assert_editor_state("«aˇ»");
6067}
6068
6069#[gpui::test]
6070async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
6071 init_test(cx, |_| {});
6072
6073 let mut cx = EditorTestContext::new(cx).await;
6074 cx.set_state(
6075 r#"let foo = 2;
6076lˇet foo = 2;
6077let fooˇ = 2;
6078let foo = 2;
6079let foo = ˇ2;"#,
6080 );
6081
6082 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6083 .unwrap();
6084 cx.assert_editor_state(
6085 r#"let foo = 2;
6086«letˇ» foo = 2;
6087let «fooˇ» = 2;
6088let foo = 2;
6089let foo = «2ˇ»;"#,
6090 );
6091
6092 // noop for multiple selections with different contents
6093 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6094 .unwrap();
6095 cx.assert_editor_state(
6096 r#"let foo = 2;
6097«letˇ» foo = 2;
6098let «fooˇ» = 2;
6099let foo = 2;
6100let foo = «2ˇ»;"#,
6101 );
6102}
6103
6104#[gpui::test]
6105async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
6106 init_test(cx, |_| {});
6107
6108 let mut cx = EditorTestContext::new(cx).await;
6109 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6110
6111 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6112 .unwrap();
6113 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
6114
6115 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6116 .unwrap();
6117 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
6118
6119 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6120 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
6121
6122 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6123 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
6124
6125 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6126 .unwrap();
6127 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
6128
6129 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6130 .unwrap();
6131 cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
6132}
6133
6134#[gpui::test]
6135async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
6136 init_test(cx, |_| {});
6137
6138 let language = Arc::new(Language::new(
6139 LanguageConfig::default(),
6140 Some(tree_sitter_rust::LANGUAGE.into()),
6141 ));
6142
6143 let text = r#"
6144 use mod1::mod2::{mod3, mod4};
6145
6146 fn fn_1(param1: bool, param2: &str) {
6147 let var1 = "text";
6148 }
6149 "#
6150 .unindent();
6151
6152 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6153 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6154 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6155
6156 editor
6157 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6158 .await;
6159
6160 editor.update_in(cx, |editor, window, cx| {
6161 editor.change_selections(None, window, cx, |s| {
6162 s.select_display_ranges([
6163 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
6164 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
6165 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
6166 ]);
6167 });
6168 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6169 });
6170 editor.update(cx, |editor, cx| {
6171 assert_text_with_selections(
6172 editor,
6173 indoc! {r#"
6174 use mod1::mod2::{mod3, «mod4ˇ»};
6175
6176 fn fn_1«ˇ(param1: bool, param2: &str)» {
6177 let var1 = "«ˇtext»";
6178 }
6179 "#},
6180 cx,
6181 );
6182 });
6183
6184 editor.update_in(cx, |editor, window, cx| {
6185 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6186 });
6187 editor.update(cx, |editor, cx| {
6188 assert_text_with_selections(
6189 editor,
6190 indoc! {r#"
6191 use mod1::mod2::«{mod3, mod4}ˇ»;
6192
6193 «ˇfn fn_1(param1: bool, param2: &str) {
6194 let var1 = "text";
6195 }»
6196 "#},
6197 cx,
6198 );
6199 });
6200
6201 editor.update_in(cx, |editor, window, cx| {
6202 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6203 });
6204 assert_eq!(
6205 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6206 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6207 );
6208
6209 // Trying to expand the selected syntax node one more time has no effect.
6210 editor.update_in(cx, |editor, window, cx| {
6211 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6212 });
6213 assert_eq!(
6214 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6215 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6216 );
6217
6218 editor.update_in(cx, |editor, window, cx| {
6219 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6220 });
6221 editor.update(cx, |editor, cx| {
6222 assert_text_with_selections(
6223 editor,
6224 indoc! {r#"
6225 use mod1::mod2::«{mod3, mod4}ˇ»;
6226
6227 «ˇfn fn_1(param1: bool, param2: &str) {
6228 let var1 = "text";
6229 }»
6230 "#},
6231 cx,
6232 );
6233 });
6234
6235 editor.update_in(cx, |editor, window, cx| {
6236 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6237 });
6238 editor.update(cx, |editor, cx| {
6239 assert_text_with_selections(
6240 editor,
6241 indoc! {r#"
6242 use mod1::mod2::{mod3, «mod4ˇ»};
6243
6244 fn fn_1«ˇ(param1: bool, param2: &str)» {
6245 let var1 = "«ˇtext»";
6246 }
6247 "#},
6248 cx,
6249 );
6250 });
6251
6252 editor.update_in(cx, |editor, window, cx| {
6253 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6254 });
6255 editor.update(cx, |editor, cx| {
6256 assert_text_with_selections(
6257 editor,
6258 indoc! {r#"
6259 use mod1::mod2::{mod3, mo«ˇ»d4};
6260
6261 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6262 let var1 = "te«ˇ»xt";
6263 }
6264 "#},
6265 cx,
6266 );
6267 });
6268
6269 // Trying to shrink the selected syntax node one more time has no effect.
6270 editor.update_in(cx, |editor, window, cx| {
6271 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6272 });
6273 editor.update_in(cx, |editor, _, cx| {
6274 assert_text_with_selections(
6275 editor,
6276 indoc! {r#"
6277 use mod1::mod2::{mod3, mo«ˇ»d4};
6278
6279 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6280 let var1 = "te«ˇ»xt";
6281 }
6282 "#},
6283 cx,
6284 );
6285 });
6286
6287 // Ensure that we keep expanding the selection if the larger selection starts or ends within
6288 // a fold.
6289 editor.update_in(cx, |editor, window, cx| {
6290 editor.fold_creases(
6291 vec![
6292 Crease::simple(
6293 Point::new(0, 21)..Point::new(0, 24),
6294 FoldPlaceholder::test(),
6295 ),
6296 Crease::simple(
6297 Point::new(3, 20)..Point::new(3, 22),
6298 FoldPlaceholder::test(),
6299 ),
6300 ],
6301 true,
6302 window,
6303 cx,
6304 );
6305 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6306 });
6307 editor.update(cx, |editor, cx| {
6308 assert_text_with_selections(
6309 editor,
6310 indoc! {r#"
6311 use mod1::mod2::«{mod3, mod4}ˇ»;
6312
6313 fn fn_1«ˇ(param1: bool, param2: &str)» {
6314 «ˇlet var1 = "text";»
6315 }
6316 "#},
6317 cx,
6318 );
6319 });
6320}
6321
6322#[gpui::test]
6323async fn test_fold_function_bodies(cx: &mut TestAppContext) {
6324 init_test(cx, |_| {});
6325
6326 let base_text = r#"
6327 impl A {
6328 // this is an uncommitted comment
6329
6330 fn b() {
6331 c();
6332 }
6333
6334 // this is another uncommitted comment
6335
6336 fn d() {
6337 // e
6338 // f
6339 }
6340 }
6341
6342 fn g() {
6343 // h
6344 }
6345 "#
6346 .unindent();
6347
6348 let text = r#"
6349 ˇimpl A {
6350
6351 fn b() {
6352 c();
6353 }
6354
6355 fn d() {
6356 // e
6357 // f
6358 }
6359 }
6360
6361 fn g() {
6362 // h
6363 }
6364 "#
6365 .unindent();
6366
6367 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6368 cx.set_state(&text);
6369 cx.set_head_text(&base_text);
6370 cx.update_editor(|editor, window, cx| {
6371 editor.expand_all_diff_hunks(&Default::default(), window, cx);
6372 });
6373
6374 cx.assert_state_with_diff(
6375 "
6376 ˇimpl A {
6377 - // this is an uncommitted comment
6378
6379 fn b() {
6380 c();
6381 }
6382
6383 - // this is another uncommitted comment
6384 -
6385 fn d() {
6386 // e
6387 // f
6388 }
6389 }
6390
6391 fn g() {
6392 // h
6393 }
6394 "
6395 .unindent(),
6396 );
6397
6398 let expected_display_text = "
6399 impl A {
6400 // this is an uncommitted comment
6401
6402 fn b() {
6403 ⋯
6404 }
6405
6406 // this is another uncommitted comment
6407
6408 fn d() {
6409 ⋯
6410 }
6411 }
6412
6413 fn g() {
6414 ⋯
6415 }
6416 "
6417 .unindent();
6418
6419 cx.update_editor(|editor, window, cx| {
6420 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
6421 assert_eq!(editor.display_text(cx), expected_display_text);
6422 });
6423}
6424
6425#[gpui::test]
6426async fn test_autoindent(cx: &mut TestAppContext) {
6427 init_test(cx, |_| {});
6428
6429 let language = Arc::new(
6430 Language::new(
6431 LanguageConfig {
6432 brackets: BracketPairConfig {
6433 pairs: vec![
6434 BracketPair {
6435 start: "{".to_string(),
6436 end: "}".to_string(),
6437 close: false,
6438 surround: false,
6439 newline: true,
6440 },
6441 BracketPair {
6442 start: "(".to_string(),
6443 end: ")".to_string(),
6444 close: false,
6445 surround: false,
6446 newline: true,
6447 },
6448 ],
6449 ..Default::default()
6450 },
6451 ..Default::default()
6452 },
6453 Some(tree_sitter_rust::LANGUAGE.into()),
6454 )
6455 .with_indents_query(
6456 r#"
6457 (_ "(" ")" @end) @indent
6458 (_ "{" "}" @end) @indent
6459 "#,
6460 )
6461 .unwrap(),
6462 );
6463
6464 let text = "fn a() {}";
6465
6466 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6467 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6468 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6469 editor
6470 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6471 .await;
6472
6473 editor.update_in(cx, |editor, window, cx| {
6474 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
6475 editor.newline(&Newline, window, cx);
6476 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
6477 assert_eq!(
6478 editor.selections.ranges(cx),
6479 &[
6480 Point::new(1, 4)..Point::new(1, 4),
6481 Point::new(3, 4)..Point::new(3, 4),
6482 Point::new(5, 0)..Point::new(5, 0)
6483 ]
6484 );
6485 });
6486}
6487
6488#[gpui::test]
6489async fn test_autoindent_selections(cx: &mut TestAppContext) {
6490 init_test(cx, |_| {});
6491
6492 {
6493 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6494 cx.set_state(indoc! {"
6495 impl A {
6496
6497 fn b() {}
6498
6499 «fn c() {
6500
6501 }ˇ»
6502 }
6503 "});
6504
6505 cx.update_editor(|editor, window, cx| {
6506 editor.autoindent(&Default::default(), window, cx);
6507 });
6508
6509 cx.assert_editor_state(indoc! {"
6510 impl A {
6511
6512 fn b() {}
6513
6514 «fn c() {
6515
6516 }ˇ»
6517 }
6518 "});
6519 }
6520
6521 {
6522 let mut cx = EditorTestContext::new_multibuffer(
6523 cx,
6524 [indoc! { "
6525 impl A {
6526 «
6527 // a
6528 fn b(){}
6529 »
6530 «
6531 }
6532 fn c(){}
6533 »
6534 "}],
6535 );
6536
6537 let buffer = cx.update_editor(|editor, _, cx| {
6538 let buffer = editor.buffer().update(cx, |buffer, _| {
6539 buffer.all_buffers().iter().next().unwrap().clone()
6540 });
6541 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6542 buffer
6543 });
6544
6545 cx.run_until_parked();
6546 cx.update_editor(|editor, window, cx| {
6547 editor.select_all(&Default::default(), window, cx);
6548 editor.autoindent(&Default::default(), window, cx)
6549 });
6550 cx.run_until_parked();
6551
6552 cx.update(|_, cx| {
6553 assert_eq!(
6554 buffer.read(cx).text(),
6555 indoc! { "
6556 impl A {
6557
6558 // a
6559 fn b(){}
6560
6561
6562 }
6563 fn c(){}
6564
6565 " }
6566 )
6567 });
6568 }
6569}
6570
6571#[gpui::test]
6572async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
6573 init_test(cx, |_| {});
6574
6575 let mut cx = EditorTestContext::new(cx).await;
6576
6577 let language = Arc::new(Language::new(
6578 LanguageConfig {
6579 brackets: BracketPairConfig {
6580 pairs: vec![
6581 BracketPair {
6582 start: "{".to_string(),
6583 end: "}".to_string(),
6584 close: true,
6585 surround: true,
6586 newline: true,
6587 },
6588 BracketPair {
6589 start: "(".to_string(),
6590 end: ")".to_string(),
6591 close: true,
6592 surround: true,
6593 newline: true,
6594 },
6595 BracketPair {
6596 start: "/*".to_string(),
6597 end: " */".to_string(),
6598 close: true,
6599 surround: true,
6600 newline: true,
6601 },
6602 BracketPair {
6603 start: "[".to_string(),
6604 end: "]".to_string(),
6605 close: false,
6606 surround: false,
6607 newline: true,
6608 },
6609 BracketPair {
6610 start: "\"".to_string(),
6611 end: "\"".to_string(),
6612 close: true,
6613 surround: true,
6614 newline: false,
6615 },
6616 BracketPair {
6617 start: "<".to_string(),
6618 end: ">".to_string(),
6619 close: false,
6620 surround: true,
6621 newline: true,
6622 },
6623 ],
6624 ..Default::default()
6625 },
6626 autoclose_before: "})]".to_string(),
6627 ..Default::default()
6628 },
6629 Some(tree_sitter_rust::LANGUAGE.into()),
6630 ));
6631
6632 cx.language_registry().add(language.clone());
6633 cx.update_buffer(|buffer, cx| {
6634 buffer.set_language(Some(language), cx);
6635 });
6636
6637 cx.set_state(
6638 &r#"
6639 🏀ˇ
6640 εˇ
6641 ❤️ˇ
6642 "#
6643 .unindent(),
6644 );
6645
6646 // autoclose multiple nested brackets at multiple cursors
6647 cx.update_editor(|editor, window, cx| {
6648 editor.handle_input("{", window, cx);
6649 editor.handle_input("{", window, cx);
6650 editor.handle_input("{", window, cx);
6651 });
6652 cx.assert_editor_state(
6653 &"
6654 🏀{{{ˇ}}}
6655 ε{{{ˇ}}}
6656 ❤️{{{ˇ}}}
6657 "
6658 .unindent(),
6659 );
6660
6661 // insert a different closing bracket
6662 cx.update_editor(|editor, window, cx| {
6663 editor.handle_input(")", window, cx);
6664 });
6665 cx.assert_editor_state(
6666 &"
6667 🏀{{{)ˇ}}}
6668 ε{{{)ˇ}}}
6669 ❤️{{{)ˇ}}}
6670 "
6671 .unindent(),
6672 );
6673
6674 // skip over the auto-closed brackets when typing a closing bracket
6675 cx.update_editor(|editor, window, cx| {
6676 editor.move_right(&MoveRight, window, cx);
6677 editor.handle_input("}", window, cx);
6678 editor.handle_input("}", window, cx);
6679 editor.handle_input("}", window, cx);
6680 });
6681 cx.assert_editor_state(
6682 &"
6683 🏀{{{)}}}}ˇ
6684 ε{{{)}}}}ˇ
6685 ❤️{{{)}}}}ˇ
6686 "
6687 .unindent(),
6688 );
6689
6690 // autoclose multi-character pairs
6691 cx.set_state(
6692 &"
6693 ˇ
6694 ˇ
6695 "
6696 .unindent(),
6697 );
6698 cx.update_editor(|editor, window, cx| {
6699 editor.handle_input("/", window, cx);
6700 editor.handle_input("*", window, cx);
6701 });
6702 cx.assert_editor_state(
6703 &"
6704 /*ˇ */
6705 /*ˇ */
6706 "
6707 .unindent(),
6708 );
6709
6710 // one cursor autocloses a multi-character pair, one cursor
6711 // does not autoclose.
6712 cx.set_state(
6713 &"
6714 /ˇ
6715 ˇ
6716 "
6717 .unindent(),
6718 );
6719 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
6720 cx.assert_editor_state(
6721 &"
6722 /*ˇ */
6723 *ˇ
6724 "
6725 .unindent(),
6726 );
6727
6728 // Don't autoclose if the next character isn't whitespace and isn't
6729 // listed in the language's "autoclose_before" section.
6730 cx.set_state("ˇa b");
6731 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6732 cx.assert_editor_state("{ˇa b");
6733
6734 // Don't autoclose if `close` is false for the bracket pair
6735 cx.set_state("ˇ");
6736 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
6737 cx.assert_editor_state("[ˇ");
6738
6739 // Surround with brackets if text is selected
6740 cx.set_state("«aˇ» b");
6741 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6742 cx.assert_editor_state("{«aˇ»} b");
6743
6744 // Autoclose when not immediately after a word character
6745 cx.set_state("a ˇ");
6746 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6747 cx.assert_editor_state("a \"ˇ\"");
6748
6749 // Autoclose pair where the start and end characters are the same
6750 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6751 cx.assert_editor_state("a \"\"ˇ");
6752
6753 // Don't autoclose when immediately after a word character
6754 cx.set_state("aˇ");
6755 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6756 cx.assert_editor_state("a\"ˇ");
6757
6758 // Do autoclose when after a non-word character
6759 cx.set_state("{ˇ");
6760 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6761 cx.assert_editor_state("{\"ˇ\"");
6762
6763 // Non identical pairs autoclose regardless of preceding character
6764 cx.set_state("aˇ");
6765 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6766 cx.assert_editor_state("a{ˇ}");
6767
6768 // Don't autoclose pair if autoclose is disabled
6769 cx.set_state("ˇ");
6770 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6771 cx.assert_editor_state("<ˇ");
6772
6773 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
6774 cx.set_state("«aˇ» b");
6775 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6776 cx.assert_editor_state("<«aˇ»> b");
6777}
6778
6779#[gpui::test]
6780async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
6781 init_test(cx, |settings| {
6782 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
6783 });
6784
6785 let mut cx = EditorTestContext::new(cx).await;
6786
6787 let language = Arc::new(Language::new(
6788 LanguageConfig {
6789 brackets: BracketPairConfig {
6790 pairs: vec![
6791 BracketPair {
6792 start: "{".to_string(),
6793 end: "}".to_string(),
6794 close: true,
6795 surround: true,
6796 newline: true,
6797 },
6798 BracketPair {
6799 start: "(".to_string(),
6800 end: ")".to_string(),
6801 close: true,
6802 surround: true,
6803 newline: true,
6804 },
6805 BracketPair {
6806 start: "[".to_string(),
6807 end: "]".to_string(),
6808 close: false,
6809 surround: false,
6810 newline: true,
6811 },
6812 ],
6813 ..Default::default()
6814 },
6815 autoclose_before: "})]".to_string(),
6816 ..Default::default()
6817 },
6818 Some(tree_sitter_rust::LANGUAGE.into()),
6819 ));
6820
6821 cx.language_registry().add(language.clone());
6822 cx.update_buffer(|buffer, cx| {
6823 buffer.set_language(Some(language), cx);
6824 });
6825
6826 cx.set_state(
6827 &"
6828 ˇ
6829 ˇ
6830 ˇ
6831 "
6832 .unindent(),
6833 );
6834
6835 // ensure only matching closing brackets are skipped over
6836 cx.update_editor(|editor, window, cx| {
6837 editor.handle_input("}", window, cx);
6838 editor.move_left(&MoveLeft, window, cx);
6839 editor.handle_input(")", window, cx);
6840 editor.move_left(&MoveLeft, window, cx);
6841 });
6842 cx.assert_editor_state(
6843 &"
6844 ˇ)}
6845 ˇ)}
6846 ˇ)}
6847 "
6848 .unindent(),
6849 );
6850
6851 // skip-over closing brackets at multiple cursors
6852 cx.update_editor(|editor, window, cx| {
6853 editor.handle_input(")", window, cx);
6854 editor.handle_input("}", window, cx);
6855 });
6856 cx.assert_editor_state(
6857 &"
6858 )}ˇ
6859 )}ˇ
6860 )}ˇ
6861 "
6862 .unindent(),
6863 );
6864
6865 // ignore non-close brackets
6866 cx.update_editor(|editor, window, cx| {
6867 editor.handle_input("]", window, cx);
6868 editor.move_left(&MoveLeft, window, cx);
6869 editor.handle_input("]", window, cx);
6870 });
6871 cx.assert_editor_state(
6872 &"
6873 )}]ˇ]
6874 )}]ˇ]
6875 )}]ˇ]
6876 "
6877 .unindent(),
6878 );
6879}
6880
6881#[gpui::test]
6882async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
6883 init_test(cx, |_| {});
6884
6885 let mut cx = EditorTestContext::new(cx).await;
6886
6887 let html_language = Arc::new(
6888 Language::new(
6889 LanguageConfig {
6890 name: "HTML".into(),
6891 brackets: BracketPairConfig {
6892 pairs: vec![
6893 BracketPair {
6894 start: "<".into(),
6895 end: ">".into(),
6896 close: true,
6897 ..Default::default()
6898 },
6899 BracketPair {
6900 start: "{".into(),
6901 end: "}".into(),
6902 close: true,
6903 ..Default::default()
6904 },
6905 BracketPair {
6906 start: "(".into(),
6907 end: ")".into(),
6908 close: true,
6909 ..Default::default()
6910 },
6911 ],
6912 ..Default::default()
6913 },
6914 autoclose_before: "})]>".into(),
6915 ..Default::default()
6916 },
6917 Some(tree_sitter_html::LANGUAGE.into()),
6918 )
6919 .with_injection_query(
6920 r#"
6921 (script_element
6922 (raw_text) @injection.content
6923 (#set! injection.language "javascript"))
6924 "#,
6925 )
6926 .unwrap(),
6927 );
6928
6929 let javascript_language = Arc::new(Language::new(
6930 LanguageConfig {
6931 name: "JavaScript".into(),
6932 brackets: BracketPairConfig {
6933 pairs: vec![
6934 BracketPair {
6935 start: "/*".into(),
6936 end: " */".into(),
6937 close: true,
6938 ..Default::default()
6939 },
6940 BracketPair {
6941 start: "{".into(),
6942 end: "}".into(),
6943 close: true,
6944 ..Default::default()
6945 },
6946 BracketPair {
6947 start: "(".into(),
6948 end: ")".into(),
6949 close: true,
6950 ..Default::default()
6951 },
6952 ],
6953 ..Default::default()
6954 },
6955 autoclose_before: "})]>".into(),
6956 ..Default::default()
6957 },
6958 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
6959 ));
6960
6961 cx.language_registry().add(html_language.clone());
6962 cx.language_registry().add(javascript_language.clone());
6963
6964 cx.update_buffer(|buffer, cx| {
6965 buffer.set_language(Some(html_language), cx);
6966 });
6967
6968 cx.set_state(
6969 &r#"
6970 <body>ˇ
6971 <script>
6972 var x = 1;ˇ
6973 </script>
6974 </body>ˇ
6975 "#
6976 .unindent(),
6977 );
6978
6979 // Precondition: different languages are active at different locations.
6980 cx.update_editor(|editor, window, cx| {
6981 let snapshot = editor.snapshot(window, cx);
6982 let cursors = editor.selections.ranges::<usize>(cx);
6983 let languages = cursors
6984 .iter()
6985 .map(|c| snapshot.language_at(c.start).unwrap().name())
6986 .collect::<Vec<_>>();
6987 assert_eq!(
6988 languages,
6989 &["HTML".into(), "JavaScript".into(), "HTML".into()]
6990 );
6991 });
6992
6993 // Angle brackets autoclose in HTML, but not JavaScript.
6994 cx.update_editor(|editor, window, cx| {
6995 editor.handle_input("<", window, cx);
6996 editor.handle_input("a", window, cx);
6997 });
6998 cx.assert_editor_state(
6999 &r#"
7000 <body><aˇ>
7001 <script>
7002 var x = 1;<aˇ
7003 </script>
7004 </body><aˇ>
7005 "#
7006 .unindent(),
7007 );
7008
7009 // Curly braces and parens autoclose in both HTML and JavaScript.
7010 cx.update_editor(|editor, window, cx| {
7011 editor.handle_input(" b=", window, cx);
7012 editor.handle_input("{", window, cx);
7013 editor.handle_input("c", window, cx);
7014 editor.handle_input("(", window, cx);
7015 });
7016 cx.assert_editor_state(
7017 &r#"
7018 <body><a b={c(ˇ)}>
7019 <script>
7020 var x = 1;<a b={c(ˇ)}
7021 </script>
7022 </body><a b={c(ˇ)}>
7023 "#
7024 .unindent(),
7025 );
7026
7027 // Brackets that were already autoclosed are skipped.
7028 cx.update_editor(|editor, window, cx| {
7029 editor.handle_input(")", window, cx);
7030 editor.handle_input("d", window, cx);
7031 editor.handle_input("}", window, cx);
7032 });
7033 cx.assert_editor_state(
7034 &r#"
7035 <body><a b={c()d}ˇ>
7036 <script>
7037 var x = 1;<a b={c()d}ˇ
7038 </script>
7039 </body><a b={c()d}ˇ>
7040 "#
7041 .unindent(),
7042 );
7043 cx.update_editor(|editor, window, cx| {
7044 editor.handle_input(">", window, cx);
7045 });
7046 cx.assert_editor_state(
7047 &r#"
7048 <body><a b={c()d}>ˇ
7049 <script>
7050 var x = 1;<a b={c()d}>ˇ
7051 </script>
7052 </body><a b={c()d}>ˇ
7053 "#
7054 .unindent(),
7055 );
7056
7057 // Reset
7058 cx.set_state(
7059 &r#"
7060 <body>ˇ
7061 <script>
7062 var x = 1;ˇ
7063 </script>
7064 </body>ˇ
7065 "#
7066 .unindent(),
7067 );
7068
7069 cx.update_editor(|editor, window, cx| {
7070 editor.handle_input("<", window, cx);
7071 });
7072 cx.assert_editor_state(
7073 &r#"
7074 <body><ˇ>
7075 <script>
7076 var x = 1;<ˇ
7077 </script>
7078 </body><ˇ>
7079 "#
7080 .unindent(),
7081 );
7082
7083 // When backspacing, the closing angle brackets are removed.
7084 cx.update_editor(|editor, window, cx| {
7085 editor.backspace(&Backspace, window, cx);
7086 });
7087 cx.assert_editor_state(
7088 &r#"
7089 <body>ˇ
7090 <script>
7091 var x = 1;ˇ
7092 </script>
7093 </body>ˇ
7094 "#
7095 .unindent(),
7096 );
7097
7098 // Block comments autoclose in JavaScript, but not HTML.
7099 cx.update_editor(|editor, window, cx| {
7100 editor.handle_input("/", window, cx);
7101 editor.handle_input("*", window, cx);
7102 });
7103 cx.assert_editor_state(
7104 &r#"
7105 <body>/*ˇ
7106 <script>
7107 var x = 1;/*ˇ */
7108 </script>
7109 </body>/*ˇ
7110 "#
7111 .unindent(),
7112 );
7113}
7114
7115#[gpui::test]
7116async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
7117 init_test(cx, |_| {});
7118
7119 let mut cx = EditorTestContext::new(cx).await;
7120
7121 let rust_language = Arc::new(
7122 Language::new(
7123 LanguageConfig {
7124 name: "Rust".into(),
7125 brackets: serde_json::from_value(json!([
7126 { "start": "{", "end": "}", "close": true, "newline": true },
7127 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
7128 ]))
7129 .unwrap(),
7130 autoclose_before: "})]>".into(),
7131 ..Default::default()
7132 },
7133 Some(tree_sitter_rust::LANGUAGE.into()),
7134 )
7135 .with_override_query("(string_literal) @string")
7136 .unwrap(),
7137 );
7138
7139 cx.language_registry().add(rust_language.clone());
7140 cx.update_buffer(|buffer, cx| {
7141 buffer.set_language(Some(rust_language), cx);
7142 });
7143
7144 cx.set_state(
7145 &r#"
7146 let x = ˇ
7147 "#
7148 .unindent(),
7149 );
7150
7151 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
7152 cx.update_editor(|editor, window, cx| {
7153 editor.handle_input("\"", window, cx);
7154 });
7155 cx.assert_editor_state(
7156 &r#"
7157 let x = "ˇ"
7158 "#
7159 .unindent(),
7160 );
7161
7162 // Inserting another quotation mark. The cursor moves across the existing
7163 // automatically-inserted quotation mark.
7164 cx.update_editor(|editor, window, cx| {
7165 editor.handle_input("\"", window, cx);
7166 });
7167 cx.assert_editor_state(
7168 &r#"
7169 let x = ""ˇ
7170 "#
7171 .unindent(),
7172 );
7173
7174 // Reset
7175 cx.set_state(
7176 &r#"
7177 let x = ˇ
7178 "#
7179 .unindent(),
7180 );
7181
7182 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
7183 cx.update_editor(|editor, window, cx| {
7184 editor.handle_input("\"", window, cx);
7185 editor.handle_input(" ", window, cx);
7186 editor.move_left(&Default::default(), window, cx);
7187 editor.handle_input("\\", window, cx);
7188 editor.handle_input("\"", window, cx);
7189 });
7190 cx.assert_editor_state(
7191 &r#"
7192 let x = "\"ˇ "
7193 "#
7194 .unindent(),
7195 );
7196
7197 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
7198 // mark. Nothing is inserted.
7199 cx.update_editor(|editor, window, cx| {
7200 editor.move_right(&Default::default(), window, cx);
7201 editor.handle_input("\"", window, cx);
7202 });
7203 cx.assert_editor_state(
7204 &r#"
7205 let x = "\" "ˇ
7206 "#
7207 .unindent(),
7208 );
7209}
7210
7211#[gpui::test]
7212async fn test_surround_with_pair(cx: &mut TestAppContext) {
7213 init_test(cx, |_| {});
7214
7215 let language = Arc::new(Language::new(
7216 LanguageConfig {
7217 brackets: BracketPairConfig {
7218 pairs: vec![
7219 BracketPair {
7220 start: "{".to_string(),
7221 end: "}".to_string(),
7222 close: true,
7223 surround: true,
7224 newline: true,
7225 },
7226 BracketPair {
7227 start: "/* ".to_string(),
7228 end: "*/".to_string(),
7229 close: true,
7230 surround: true,
7231 ..Default::default()
7232 },
7233 ],
7234 ..Default::default()
7235 },
7236 ..Default::default()
7237 },
7238 Some(tree_sitter_rust::LANGUAGE.into()),
7239 ));
7240
7241 let text = r#"
7242 a
7243 b
7244 c
7245 "#
7246 .unindent();
7247
7248 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7249 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7250 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7251 editor
7252 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7253 .await;
7254
7255 editor.update_in(cx, |editor, window, cx| {
7256 editor.change_selections(None, window, cx, |s| {
7257 s.select_display_ranges([
7258 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7259 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7260 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
7261 ])
7262 });
7263
7264 editor.handle_input("{", window, cx);
7265 editor.handle_input("{", window, cx);
7266 editor.handle_input("{", window, cx);
7267 assert_eq!(
7268 editor.text(cx),
7269 "
7270 {{{a}}}
7271 {{{b}}}
7272 {{{c}}}
7273 "
7274 .unindent()
7275 );
7276 assert_eq!(
7277 editor.selections.display_ranges(cx),
7278 [
7279 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
7280 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
7281 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
7282 ]
7283 );
7284
7285 editor.undo(&Undo, window, cx);
7286 editor.undo(&Undo, window, cx);
7287 editor.undo(&Undo, window, cx);
7288 assert_eq!(
7289 editor.text(cx),
7290 "
7291 a
7292 b
7293 c
7294 "
7295 .unindent()
7296 );
7297 assert_eq!(
7298 editor.selections.display_ranges(cx),
7299 [
7300 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7301 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7302 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7303 ]
7304 );
7305
7306 // Ensure inserting the first character of a multi-byte bracket pair
7307 // doesn't surround the selections with the bracket.
7308 editor.handle_input("/", window, cx);
7309 assert_eq!(
7310 editor.text(cx),
7311 "
7312 /
7313 /
7314 /
7315 "
7316 .unindent()
7317 );
7318 assert_eq!(
7319 editor.selections.display_ranges(cx),
7320 [
7321 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7322 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7323 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7324 ]
7325 );
7326
7327 editor.undo(&Undo, window, cx);
7328 assert_eq!(
7329 editor.text(cx),
7330 "
7331 a
7332 b
7333 c
7334 "
7335 .unindent()
7336 );
7337 assert_eq!(
7338 editor.selections.display_ranges(cx),
7339 [
7340 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7341 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7342 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7343 ]
7344 );
7345
7346 // Ensure inserting the last character of a multi-byte bracket pair
7347 // doesn't surround the selections with the bracket.
7348 editor.handle_input("*", window, cx);
7349 assert_eq!(
7350 editor.text(cx),
7351 "
7352 *
7353 *
7354 *
7355 "
7356 .unindent()
7357 );
7358 assert_eq!(
7359 editor.selections.display_ranges(cx),
7360 [
7361 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7362 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7363 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7364 ]
7365 );
7366 });
7367}
7368
7369#[gpui::test]
7370async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
7371 init_test(cx, |_| {});
7372
7373 let language = Arc::new(Language::new(
7374 LanguageConfig {
7375 brackets: BracketPairConfig {
7376 pairs: vec![BracketPair {
7377 start: "{".to_string(),
7378 end: "}".to_string(),
7379 close: true,
7380 surround: true,
7381 newline: true,
7382 }],
7383 ..Default::default()
7384 },
7385 autoclose_before: "}".to_string(),
7386 ..Default::default()
7387 },
7388 Some(tree_sitter_rust::LANGUAGE.into()),
7389 ));
7390
7391 let text = r#"
7392 a
7393 b
7394 c
7395 "#
7396 .unindent();
7397
7398 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7399 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7400 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7401 editor
7402 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7403 .await;
7404
7405 editor.update_in(cx, |editor, window, cx| {
7406 editor.change_selections(None, window, cx, |s| {
7407 s.select_ranges([
7408 Point::new(0, 1)..Point::new(0, 1),
7409 Point::new(1, 1)..Point::new(1, 1),
7410 Point::new(2, 1)..Point::new(2, 1),
7411 ])
7412 });
7413
7414 editor.handle_input("{", window, cx);
7415 editor.handle_input("{", window, cx);
7416 editor.handle_input("_", window, cx);
7417 assert_eq!(
7418 editor.text(cx),
7419 "
7420 a{{_}}
7421 b{{_}}
7422 c{{_}}
7423 "
7424 .unindent()
7425 );
7426 assert_eq!(
7427 editor.selections.ranges::<Point>(cx),
7428 [
7429 Point::new(0, 4)..Point::new(0, 4),
7430 Point::new(1, 4)..Point::new(1, 4),
7431 Point::new(2, 4)..Point::new(2, 4)
7432 ]
7433 );
7434
7435 editor.backspace(&Default::default(), window, cx);
7436 editor.backspace(&Default::default(), window, cx);
7437 assert_eq!(
7438 editor.text(cx),
7439 "
7440 a{}
7441 b{}
7442 c{}
7443 "
7444 .unindent()
7445 );
7446 assert_eq!(
7447 editor.selections.ranges::<Point>(cx),
7448 [
7449 Point::new(0, 2)..Point::new(0, 2),
7450 Point::new(1, 2)..Point::new(1, 2),
7451 Point::new(2, 2)..Point::new(2, 2)
7452 ]
7453 );
7454
7455 editor.delete_to_previous_word_start(&Default::default(), window, cx);
7456 assert_eq!(
7457 editor.text(cx),
7458 "
7459 a
7460 b
7461 c
7462 "
7463 .unindent()
7464 );
7465 assert_eq!(
7466 editor.selections.ranges::<Point>(cx),
7467 [
7468 Point::new(0, 1)..Point::new(0, 1),
7469 Point::new(1, 1)..Point::new(1, 1),
7470 Point::new(2, 1)..Point::new(2, 1)
7471 ]
7472 );
7473 });
7474}
7475
7476#[gpui::test]
7477async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
7478 init_test(cx, |settings| {
7479 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7480 });
7481
7482 let mut cx = EditorTestContext::new(cx).await;
7483
7484 let language = Arc::new(Language::new(
7485 LanguageConfig {
7486 brackets: BracketPairConfig {
7487 pairs: vec![
7488 BracketPair {
7489 start: "{".to_string(),
7490 end: "}".to_string(),
7491 close: true,
7492 surround: true,
7493 newline: true,
7494 },
7495 BracketPair {
7496 start: "(".to_string(),
7497 end: ")".to_string(),
7498 close: true,
7499 surround: true,
7500 newline: true,
7501 },
7502 BracketPair {
7503 start: "[".to_string(),
7504 end: "]".to_string(),
7505 close: false,
7506 surround: true,
7507 newline: true,
7508 },
7509 ],
7510 ..Default::default()
7511 },
7512 autoclose_before: "})]".to_string(),
7513 ..Default::default()
7514 },
7515 Some(tree_sitter_rust::LANGUAGE.into()),
7516 ));
7517
7518 cx.language_registry().add(language.clone());
7519 cx.update_buffer(|buffer, cx| {
7520 buffer.set_language(Some(language), cx);
7521 });
7522
7523 cx.set_state(
7524 &"
7525 {(ˇ)}
7526 [[ˇ]]
7527 {(ˇ)}
7528 "
7529 .unindent(),
7530 );
7531
7532 cx.update_editor(|editor, window, cx| {
7533 editor.backspace(&Default::default(), window, cx);
7534 editor.backspace(&Default::default(), window, cx);
7535 });
7536
7537 cx.assert_editor_state(
7538 &"
7539 ˇ
7540 ˇ]]
7541 ˇ
7542 "
7543 .unindent(),
7544 );
7545
7546 cx.update_editor(|editor, window, cx| {
7547 editor.handle_input("{", window, cx);
7548 editor.handle_input("{", window, cx);
7549 editor.move_right(&MoveRight, window, cx);
7550 editor.move_right(&MoveRight, window, cx);
7551 editor.move_left(&MoveLeft, window, cx);
7552 editor.move_left(&MoveLeft, window, cx);
7553 editor.backspace(&Default::default(), window, cx);
7554 });
7555
7556 cx.assert_editor_state(
7557 &"
7558 {ˇ}
7559 {ˇ}]]
7560 {ˇ}
7561 "
7562 .unindent(),
7563 );
7564
7565 cx.update_editor(|editor, window, cx| {
7566 editor.backspace(&Default::default(), window, cx);
7567 });
7568
7569 cx.assert_editor_state(
7570 &"
7571 ˇ
7572 ˇ]]
7573 ˇ
7574 "
7575 .unindent(),
7576 );
7577}
7578
7579#[gpui::test]
7580async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
7581 init_test(cx, |_| {});
7582
7583 let language = Arc::new(Language::new(
7584 LanguageConfig::default(),
7585 Some(tree_sitter_rust::LANGUAGE.into()),
7586 ));
7587
7588 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
7589 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7590 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7591 editor
7592 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7593 .await;
7594
7595 editor.update_in(cx, |editor, window, cx| {
7596 editor.set_auto_replace_emoji_shortcode(true);
7597
7598 editor.handle_input("Hello ", window, cx);
7599 editor.handle_input(":wave", window, cx);
7600 assert_eq!(editor.text(cx), "Hello :wave".unindent());
7601
7602 editor.handle_input(":", window, cx);
7603 assert_eq!(editor.text(cx), "Hello 👋".unindent());
7604
7605 editor.handle_input(" :smile", window, cx);
7606 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
7607
7608 editor.handle_input(":", window, cx);
7609 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
7610
7611 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
7612 editor.handle_input(":wave", window, cx);
7613 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
7614
7615 editor.handle_input(":", window, cx);
7616 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
7617
7618 editor.handle_input(":1", window, cx);
7619 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
7620
7621 editor.handle_input(":", window, cx);
7622 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
7623
7624 // Ensure shortcode does not get replaced when it is part of a word
7625 editor.handle_input(" Test:wave", window, cx);
7626 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
7627
7628 editor.handle_input(":", window, cx);
7629 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
7630
7631 editor.set_auto_replace_emoji_shortcode(false);
7632
7633 // Ensure shortcode does not get replaced when auto replace is off
7634 editor.handle_input(" :wave", window, cx);
7635 assert_eq!(
7636 editor.text(cx),
7637 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
7638 );
7639
7640 editor.handle_input(":", window, cx);
7641 assert_eq!(
7642 editor.text(cx),
7643 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
7644 );
7645 });
7646}
7647
7648#[gpui::test]
7649async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
7650 init_test(cx, |_| {});
7651
7652 let (text, insertion_ranges) = marked_text_ranges(
7653 indoc! {"
7654 ˇ
7655 "},
7656 false,
7657 );
7658
7659 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7660 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7661
7662 _ = editor.update_in(cx, |editor, window, cx| {
7663 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
7664
7665 editor
7666 .insert_snippet(&insertion_ranges, snippet, window, cx)
7667 .unwrap();
7668
7669 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7670 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7671 assert_eq!(editor.text(cx), expected_text);
7672 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7673 }
7674
7675 assert(
7676 editor,
7677 cx,
7678 indoc! {"
7679 type «» =•
7680 "},
7681 );
7682
7683 assert!(editor.context_menu_visible(), "There should be a matches");
7684 });
7685}
7686
7687#[gpui::test]
7688async fn test_snippets(cx: &mut TestAppContext) {
7689 init_test(cx, |_| {});
7690
7691 let (text, insertion_ranges) = marked_text_ranges(
7692 indoc! {"
7693 a.ˇ b
7694 a.ˇ b
7695 a.ˇ b
7696 "},
7697 false,
7698 );
7699
7700 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7701 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7702
7703 editor.update_in(cx, |editor, window, cx| {
7704 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
7705
7706 editor
7707 .insert_snippet(&insertion_ranges, snippet, window, cx)
7708 .unwrap();
7709
7710 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7711 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7712 assert_eq!(editor.text(cx), expected_text);
7713 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7714 }
7715
7716 assert(
7717 editor,
7718 cx,
7719 indoc! {"
7720 a.f(«one», two, «three») b
7721 a.f(«one», two, «three») b
7722 a.f(«one», two, «three») b
7723 "},
7724 );
7725
7726 // Can't move earlier than the first tab stop
7727 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
7728 assert(
7729 editor,
7730 cx,
7731 indoc! {"
7732 a.f(«one», two, «three») b
7733 a.f(«one», two, «three») b
7734 a.f(«one», two, «three») b
7735 "},
7736 );
7737
7738 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7739 assert(
7740 editor,
7741 cx,
7742 indoc! {"
7743 a.f(one, «two», three) b
7744 a.f(one, «two», three) b
7745 a.f(one, «two», three) b
7746 "},
7747 );
7748
7749 editor.move_to_prev_snippet_tabstop(window, cx);
7750 assert(
7751 editor,
7752 cx,
7753 indoc! {"
7754 a.f(«one», two, «three») b
7755 a.f(«one», two, «three») b
7756 a.f(«one», two, «three») b
7757 "},
7758 );
7759
7760 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7761 assert(
7762 editor,
7763 cx,
7764 indoc! {"
7765 a.f(one, «two», three) b
7766 a.f(one, «two», three) b
7767 a.f(one, «two», three) b
7768 "},
7769 );
7770 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7771 assert(
7772 editor,
7773 cx,
7774 indoc! {"
7775 a.f(one, two, three)ˇ b
7776 a.f(one, two, three)ˇ b
7777 a.f(one, two, three)ˇ b
7778 "},
7779 );
7780
7781 // As soon as the last tab stop is reached, snippet state is gone
7782 editor.move_to_prev_snippet_tabstop(window, cx);
7783 assert(
7784 editor,
7785 cx,
7786 indoc! {"
7787 a.f(one, two, three)ˇ b
7788 a.f(one, two, three)ˇ b
7789 a.f(one, two, three)ˇ b
7790 "},
7791 );
7792 });
7793}
7794
7795#[gpui::test]
7796async fn test_document_format_during_save(cx: &mut TestAppContext) {
7797 init_test(cx, |_| {});
7798
7799 let fs = FakeFs::new(cx.executor());
7800 fs.insert_file(path!("/file.rs"), Default::default()).await;
7801
7802 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
7803
7804 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7805 language_registry.add(rust_lang());
7806 let mut fake_servers = language_registry.register_fake_lsp(
7807 "Rust",
7808 FakeLspAdapter {
7809 capabilities: lsp::ServerCapabilities {
7810 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7811 ..Default::default()
7812 },
7813 ..Default::default()
7814 },
7815 );
7816
7817 let buffer = project
7818 .update(cx, |project, cx| {
7819 project.open_local_buffer(path!("/file.rs"), cx)
7820 })
7821 .await
7822 .unwrap();
7823
7824 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7825 let (editor, cx) = cx.add_window_view(|window, cx| {
7826 build_editor_with_project(project.clone(), buffer, window, cx)
7827 });
7828 editor.update_in(cx, |editor, window, cx| {
7829 editor.set_text("one\ntwo\nthree\n", window, cx)
7830 });
7831 assert!(cx.read(|cx| editor.is_dirty(cx)));
7832
7833 cx.executor().start_waiting();
7834 let fake_server = fake_servers.next().await.unwrap();
7835
7836 {
7837 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
7838 move |params, _| async move {
7839 assert_eq!(
7840 params.text_document.uri,
7841 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7842 );
7843 assert_eq!(params.options.tab_size, 4);
7844 Ok(Some(vec![lsp::TextEdit::new(
7845 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7846 ", ".to_string(),
7847 )]))
7848 },
7849 );
7850 let save = editor
7851 .update_in(cx, |editor, window, cx| {
7852 editor.save(true, project.clone(), window, cx)
7853 })
7854 .unwrap();
7855 cx.executor().start_waiting();
7856 save.await;
7857
7858 assert_eq!(
7859 editor.update(cx, |editor, cx| editor.text(cx)),
7860 "one, two\nthree\n"
7861 );
7862 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7863 }
7864
7865 {
7866 editor.update_in(cx, |editor, window, cx| {
7867 editor.set_text("one\ntwo\nthree\n", window, cx)
7868 });
7869 assert!(cx.read(|cx| editor.is_dirty(cx)));
7870
7871 // Ensure we can still save even if formatting hangs.
7872 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
7873 move |params, _| async move {
7874 assert_eq!(
7875 params.text_document.uri,
7876 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7877 );
7878 futures::future::pending::<()>().await;
7879 unreachable!()
7880 },
7881 );
7882 let save = editor
7883 .update_in(cx, |editor, window, cx| {
7884 editor.save(true, project.clone(), window, cx)
7885 })
7886 .unwrap();
7887 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7888 cx.executor().start_waiting();
7889 save.await;
7890 assert_eq!(
7891 editor.update(cx, |editor, cx| editor.text(cx)),
7892 "one\ntwo\nthree\n"
7893 );
7894 }
7895
7896 // For non-dirty buffer, no formatting request should be sent
7897 {
7898 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7899
7900 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
7901 panic!("Should not be invoked on non-dirty buffer");
7902 });
7903 let save = editor
7904 .update_in(cx, |editor, window, cx| {
7905 editor.save(true, project.clone(), window, cx)
7906 })
7907 .unwrap();
7908 cx.executor().start_waiting();
7909 save.await;
7910 }
7911
7912 // Set rust language override and assert overridden tabsize is sent to language server
7913 update_test_language_settings(cx, |settings| {
7914 settings.languages.insert(
7915 "Rust".into(),
7916 LanguageSettingsContent {
7917 tab_size: NonZeroU32::new(8),
7918 ..Default::default()
7919 },
7920 );
7921 });
7922
7923 {
7924 editor.update_in(cx, |editor, window, cx| {
7925 editor.set_text("somehting_new\n", window, cx)
7926 });
7927 assert!(cx.read(|cx| editor.is_dirty(cx)));
7928 let _formatting_request_signal = fake_server
7929 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
7930 assert_eq!(
7931 params.text_document.uri,
7932 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7933 );
7934 assert_eq!(params.options.tab_size, 8);
7935 Ok(Some(vec![]))
7936 });
7937 let save = editor
7938 .update_in(cx, |editor, window, cx| {
7939 editor.save(true, project.clone(), window, cx)
7940 })
7941 .unwrap();
7942 cx.executor().start_waiting();
7943 save.await;
7944 }
7945}
7946
7947#[gpui::test]
7948async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
7949 init_test(cx, |_| {});
7950
7951 let cols = 4;
7952 let rows = 10;
7953 let sample_text_1 = sample_text(rows, cols, 'a');
7954 assert_eq!(
7955 sample_text_1,
7956 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
7957 );
7958 let sample_text_2 = sample_text(rows, cols, 'l');
7959 assert_eq!(
7960 sample_text_2,
7961 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
7962 );
7963 let sample_text_3 = sample_text(rows, cols, 'v');
7964 assert_eq!(
7965 sample_text_3,
7966 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
7967 );
7968
7969 let fs = FakeFs::new(cx.executor());
7970 fs.insert_tree(
7971 path!("/a"),
7972 json!({
7973 "main.rs": sample_text_1,
7974 "other.rs": sample_text_2,
7975 "lib.rs": sample_text_3,
7976 }),
7977 )
7978 .await;
7979
7980 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
7981 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7982 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
7983
7984 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7985 language_registry.add(rust_lang());
7986 let mut fake_servers = language_registry.register_fake_lsp(
7987 "Rust",
7988 FakeLspAdapter {
7989 capabilities: lsp::ServerCapabilities {
7990 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7991 ..Default::default()
7992 },
7993 ..Default::default()
7994 },
7995 );
7996
7997 let worktree = project.update(cx, |project, cx| {
7998 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
7999 assert_eq!(worktrees.len(), 1);
8000 worktrees.pop().unwrap()
8001 });
8002 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
8003
8004 let buffer_1 = project
8005 .update(cx, |project, cx| {
8006 project.open_buffer((worktree_id, "main.rs"), cx)
8007 })
8008 .await
8009 .unwrap();
8010 let buffer_2 = project
8011 .update(cx, |project, cx| {
8012 project.open_buffer((worktree_id, "other.rs"), cx)
8013 })
8014 .await
8015 .unwrap();
8016 let buffer_3 = project
8017 .update(cx, |project, cx| {
8018 project.open_buffer((worktree_id, "lib.rs"), cx)
8019 })
8020 .await
8021 .unwrap();
8022
8023 let multi_buffer = cx.new(|cx| {
8024 let mut multi_buffer = MultiBuffer::new(ReadWrite);
8025 multi_buffer.push_excerpts(
8026 buffer_1.clone(),
8027 [
8028 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8029 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8030 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8031 ],
8032 cx,
8033 );
8034 multi_buffer.push_excerpts(
8035 buffer_2.clone(),
8036 [
8037 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8038 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8039 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8040 ],
8041 cx,
8042 );
8043 multi_buffer.push_excerpts(
8044 buffer_3.clone(),
8045 [
8046 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8047 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8048 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8049 ],
8050 cx,
8051 );
8052 multi_buffer
8053 });
8054 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
8055 Editor::new(
8056 EditorMode::full(),
8057 multi_buffer,
8058 Some(project.clone()),
8059 window,
8060 cx,
8061 )
8062 });
8063
8064 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8065 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8066 s.select_ranges(Some(1..2))
8067 });
8068 editor.insert("|one|two|three|", window, cx);
8069 });
8070 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8071 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8072 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8073 s.select_ranges(Some(60..70))
8074 });
8075 editor.insert("|four|five|six|", window, cx);
8076 });
8077 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8078
8079 // First two buffers should be edited, but not the third one.
8080 assert_eq!(
8081 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8082 "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}",
8083 );
8084 buffer_1.update(cx, |buffer, _| {
8085 assert!(buffer.is_dirty());
8086 assert_eq!(
8087 buffer.text(),
8088 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
8089 )
8090 });
8091 buffer_2.update(cx, |buffer, _| {
8092 assert!(buffer.is_dirty());
8093 assert_eq!(
8094 buffer.text(),
8095 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
8096 )
8097 });
8098 buffer_3.update(cx, |buffer, _| {
8099 assert!(!buffer.is_dirty());
8100 assert_eq!(buffer.text(), sample_text_3,)
8101 });
8102 cx.executor().run_until_parked();
8103
8104 cx.executor().start_waiting();
8105 let save = multi_buffer_editor
8106 .update_in(cx, |editor, window, cx| {
8107 editor.save(true, project.clone(), window, cx)
8108 })
8109 .unwrap();
8110
8111 let fake_server = fake_servers.next().await.unwrap();
8112 fake_server
8113 .server
8114 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
8115 Ok(Some(vec![lsp::TextEdit::new(
8116 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8117 format!("[{} formatted]", params.text_document.uri),
8118 )]))
8119 })
8120 .detach();
8121 save.await;
8122
8123 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
8124 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
8125 assert_eq!(
8126 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8127 uri!(
8128 "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}"
8129 ),
8130 );
8131 buffer_1.update(cx, |buffer, _| {
8132 assert!(!buffer.is_dirty());
8133 assert_eq!(
8134 buffer.text(),
8135 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
8136 )
8137 });
8138 buffer_2.update(cx, |buffer, _| {
8139 assert!(!buffer.is_dirty());
8140 assert_eq!(
8141 buffer.text(),
8142 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
8143 )
8144 });
8145 buffer_3.update(cx, |buffer, _| {
8146 assert!(!buffer.is_dirty());
8147 assert_eq!(buffer.text(), sample_text_3,)
8148 });
8149}
8150
8151#[gpui::test]
8152async fn test_range_format_during_save(cx: &mut TestAppContext) {
8153 init_test(cx, |_| {});
8154
8155 let fs = FakeFs::new(cx.executor());
8156 fs.insert_file(path!("/file.rs"), Default::default()).await;
8157
8158 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8159
8160 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8161 language_registry.add(rust_lang());
8162 let mut fake_servers = language_registry.register_fake_lsp(
8163 "Rust",
8164 FakeLspAdapter {
8165 capabilities: lsp::ServerCapabilities {
8166 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
8167 ..Default::default()
8168 },
8169 ..Default::default()
8170 },
8171 );
8172
8173 let buffer = project
8174 .update(cx, |project, cx| {
8175 project.open_local_buffer(path!("/file.rs"), cx)
8176 })
8177 .await
8178 .unwrap();
8179
8180 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8181 let (editor, cx) = cx.add_window_view(|window, cx| {
8182 build_editor_with_project(project.clone(), buffer, window, cx)
8183 });
8184 editor.update_in(cx, |editor, window, cx| {
8185 editor.set_text("one\ntwo\nthree\n", window, cx)
8186 });
8187 assert!(cx.read(|cx| editor.is_dirty(cx)));
8188
8189 cx.executor().start_waiting();
8190 let fake_server = fake_servers.next().await.unwrap();
8191
8192 let save = editor
8193 .update_in(cx, |editor, window, cx| {
8194 editor.save(true, project.clone(), window, cx)
8195 })
8196 .unwrap();
8197 fake_server
8198 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8199 assert_eq!(
8200 params.text_document.uri,
8201 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8202 );
8203 assert_eq!(params.options.tab_size, 4);
8204 Ok(Some(vec![lsp::TextEdit::new(
8205 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8206 ", ".to_string(),
8207 )]))
8208 })
8209 .next()
8210 .await;
8211 cx.executor().start_waiting();
8212 save.await;
8213 assert_eq!(
8214 editor.update(cx, |editor, cx| editor.text(cx)),
8215 "one, two\nthree\n"
8216 );
8217 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8218
8219 editor.update_in(cx, |editor, window, cx| {
8220 editor.set_text("one\ntwo\nthree\n", window, cx)
8221 });
8222 assert!(cx.read(|cx| editor.is_dirty(cx)));
8223
8224 // Ensure we can still save even if formatting hangs.
8225 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
8226 move |params, _| async move {
8227 assert_eq!(
8228 params.text_document.uri,
8229 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8230 );
8231 futures::future::pending::<()>().await;
8232 unreachable!()
8233 },
8234 );
8235 let save = editor
8236 .update_in(cx, |editor, window, cx| {
8237 editor.save(true, project.clone(), window, cx)
8238 })
8239 .unwrap();
8240 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8241 cx.executor().start_waiting();
8242 save.await;
8243 assert_eq!(
8244 editor.update(cx, |editor, cx| editor.text(cx)),
8245 "one\ntwo\nthree\n"
8246 );
8247 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8248
8249 // For non-dirty buffer, no formatting request should be sent
8250 let save = editor
8251 .update_in(cx, |editor, window, cx| {
8252 editor.save(true, project.clone(), window, cx)
8253 })
8254 .unwrap();
8255 let _pending_format_request = fake_server
8256 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
8257 panic!("Should not be invoked on non-dirty buffer");
8258 })
8259 .next();
8260 cx.executor().start_waiting();
8261 save.await;
8262
8263 // Set Rust language override and assert overridden tabsize is sent to language server
8264 update_test_language_settings(cx, |settings| {
8265 settings.languages.insert(
8266 "Rust".into(),
8267 LanguageSettingsContent {
8268 tab_size: NonZeroU32::new(8),
8269 ..Default::default()
8270 },
8271 );
8272 });
8273
8274 editor.update_in(cx, |editor, window, cx| {
8275 editor.set_text("somehting_new\n", window, cx)
8276 });
8277 assert!(cx.read(|cx| editor.is_dirty(cx)));
8278 let save = editor
8279 .update_in(cx, |editor, window, cx| {
8280 editor.save(true, project.clone(), window, cx)
8281 })
8282 .unwrap();
8283 fake_server
8284 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8285 assert_eq!(
8286 params.text_document.uri,
8287 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8288 );
8289 assert_eq!(params.options.tab_size, 8);
8290 Ok(Some(vec![]))
8291 })
8292 .next()
8293 .await;
8294 cx.executor().start_waiting();
8295 save.await;
8296}
8297
8298#[gpui::test]
8299async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
8300 init_test(cx, |settings| {
8301 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8302 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8303 ))
8304 });
8305
8306 let fs = FakeFs::new(cx.executor());
8307 fs.insert_file(path!("/file.rs"), Default::default()).await;
8308
8309 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8310
8311 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8312 language_registry.add(Arc::new(Language::new(
8313 LanguageConfig {
8314 name: "Rust".into(),
8315 matcher: LanguageMatcher {
8316 path_suffixes: vec!["rs".to_string()],
8317 ..Default::default()
8318 },
8319 ..LanguageConfig::default()
8320 },
8321 Some(tree_sitter_rust::LANGUAGE.into()),
8322 )));
8323 update_test_language_settings(cx, |settings| {
8324 // Enable Prettier formatting for the same buffer, and ensure
8325 // LSP is called instead of Prettier.
8326 settings.defaults.prettier = Some(PrettierSettings {
8327 allowed: true,
8328 ..PrettierSettings::default()
8329 });
8330 });
8331 let mut fake_servers = language_registry.register_fake_lsp(
8332 "Rust",
8333 FakeLspAdapter {
8334 capabilities: lsp::ServerCapabilities {
8335 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8336 ..Default::default()
8337 },
8338 ..Default::default()
8339 },
8340 );
8341
8342 let buffer = project
8343 .update(cx, |project, cx| {
8344 project.open_local_buffer(path!("/file.rs"), cx)
8345 })
8346 .await
8347 .unwrap();
8348
8349 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8350 let (editor, cx) = cx.add_window_view(|window, cx| {
8351 build_editor_with_project(project.clone(), buffer, window, cx)
8352 });
8353 editor.update_in(cx, |editor, window, cx| {
8354 editor.set_text("one\ntwo\nthree\n", window, cx)
8355 });
8356
8357 cx.executor().start_waiting();
8358 let fake_server = fake_servers.next().await.unwrap();
8359
8360 let format = editor
8361 .update_in(cx, |editor, window, cx| {
8362 editor.perform_format(
8363 project.clone(),
8364 FormatTrigger::Manual,
8365 FormatTarget::Buffers,
8366 window,
8367 cx,
8368 )
8369 })
8370 .unwrap();
8371 fake_server
8372 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8373 assert_eq!(
8374 params.text_document.uri,
8375 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8376 );
8377 assert_eq!(params.options.tab_size, 4);
8378 Ok(Some(vec![lsp::TextEdit::new(
8379 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8380 ", ".to_string(),
8381 )]))
8382 })
8383 .next()
8384 .await;
8385 cx.executor().start_waiting();
8386 format.await;
8387 assert_eq!(
8388 editor.update(cx, |editor, cx| editor.text(cx)),
8389 "one, two\nthree\n"
8390 );
8391
8392 editor.update_in(cx, |editor, window, cx| {
8393 editor.set_text("one\ntwo\nthree\n", window, cx)
8394 });
8395 // Ensure we don't lock if formatting hangs.
8396 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8397 move |params, _| async move {
8398 assert_eq!(
8399 params.text_document.uri,
8400 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8401 );
8402 futures::future::pending::<()>().await;
8403 unreachable!()
8404 },
8405 );
8406 let format = editor
8407 .update_in(cx, |editor, window, cx| {
8408 editor.perform_format(
8409 project,
8410 FormatTrigger::Manual,
8411 FormatTarget::Buffers,
8412 window,
8413 cx,
8414 )
8415 })
8416 .unwrap();
8417 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8418 cx.executor().start_waiting();
8419 format.await;
8420 assert_eq!(
8421 editor.update(cx, |editor, cx| editor.text(cx)),
8422 "one\ntwo\nthree\n"
8423 );
8424}
8425
8426#[gpui::test]
8427async fn test_multiple_formatters(cx: &mut TestAppContext) {
8428 init_test(cx, |settings| {
8429 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
8430 settings.defaults.formatter =
8431 Some(language_settings::SelectedFormatter::List(FormatterList(
8432 vec![
8433 Formatter::LanguageServer { name: None },
8434 Formatter::CodeActions(
8435 [
8436 ("code-action-1".into(), true),
8437 ("code-action-2".into(), true),
8438 ]
8439 .into_iter()
8440 .collect(),
8441 ),
8442 ]
8443 .into(),
8444 )))
8445 });
8446
8447 let fs = FakeFs::new(cx.executor());
8448 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
8449 .await;
8450
8451 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8452 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8453 language_registry.add(rust_lang());
8454
8455 let mut fake_servers = language_registry.register_fake_lsp(
8456 "Rust",
8457 FakeLspAdapter {
8458 capabilities: lsp::ServerCapabilities {
8459 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8460 execute_command_provider: Some(lsp::ExecuteCommandOptions {
8461 commands: vec!["the-command-for-code-action-1".into()],
8462 ..Default::default()
8463 }),
8464 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
8465 ..Default::default()
8466 },
8467 ..Default::default()
8468 },
8469 );
8470
8471 let buffer = project
8472 .update(cx, |project, cx| {
8473 project.open_local_buffer(path!("/file.rs"), cx)
8474 })
8475 .await
8476 .unwrap();
8477
8478 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8479 let (editor, cx) = cx.add_window_view(|window, cx| {
8480 build_editor_with_project(project.clone(), buffer, window, cx)
8481 });
8482
8483 cx.executor().start_waiting();
8484
8485 let fake_server = fake_servers.next().await.unwrap();
8486 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8487 move |_params, _| async move {
8488 Ok(Some(vec![lsp::TextEdit::new(
8489 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8490 "applied-formatting\n".to_string(),
8491 )]))
8492 },
8493 );
8494 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
8495 move |params, _| async move {
8496 assert_eq!(
8497 params.context.only,
8498 Some(vec!["code-action-1".into(), "code-action-2".into()])
8499 );
8500 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
8501 Ok(Some(vec![
8502 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
8503 kind: Some("code-action-1".into()),
8504 edit: Some(lsp::WorkspaceEdit::new(
8505 [(
8506 uri.clone(),
8507 vec![lsp::TextEdit::new(
8508 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8509 "applied-code-action-1-edit\n".to_string(),
8510 )],
8511 )]
8512 .into_iter()
8513 .collect(),
8514 )),
8515 command: Some(lsp::Command {
8516 command: "the-command-for-code-action-1".into(),
8517 ..Default::default()
8518 }),
8519 ..Default::default()
8520 }),
8521 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
8522 kind: Some("code-action-2".into()),
8523 edit: Some(lsp::WorkspaceEdit::new(
8524 [(
8525 uri.clone(),
8526 vec![lsp::TextEdit::new(
8527 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8528 "applied-code-action-2-edit\n".to_string(),
8529 )],
8530 )]
8531 .into_iter()
8532 .collect(),
8533 )),
8534 ..Default::default()
8535 }),
8536 ]))
8537 },
8538 );
8539
8540 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
8541 move |params, _| async move { Ok(params) }
8542 });
8543
8544 let command_lock = Arc::new(futures::lock::Mutex::new(()));
8545 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
8546 let fake = fake_server.clone();
8547 let lock = command_lock.clone();
8548 move |params, _| {
8549 assert_eq!(params.command, "the-command-for-code-action-1");
8550 let fake = fake.clone();
8551 let lock = lock.clone();
8552 async move {
8553 lock.lock().await;
8554 fake.server
8555 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
8556 label: None,
8557 edit: lsp::WorkspaceEdit {
8558 changes: Some(
8559 [(
8560 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
8561 vec![lsp::TextEdit {
8562 range: lsp::Range::new(
8563 lsp::Position::new(0, 0),
8564 lsp::Position::new(0, 0),
8565 ),
8566 new_text: "applied-code-action-1-command\n".into(),
8567 }],
8568 )]
8569 .into_iter()
8570 .collect(),
8571 ),
8572 ..Default::default()
8573 },
8574 })
8575 .await
8576 .unwrap();
8577 Ok(Some(json!(null)))
8578 }
8579 }
8580 });
8581
8582 cx.executor().start_waiting();
8583 editor
8584 .update_in(cx, |editor, window, cx| {
8585 editor.perform_format(
8586 project.clone(),
8587 FormatTrigger::Manual,
8588 FormatTarget::Buffers,
8589 window,
8590 cx,
8591 )
8592 })
8593 .unwrap()
8594 .await;
8595 editor.update(cx, |editor, cx| {
8596 assert_eq!(
8597 editor.text(cx),
8598 r#"
8599 applied-code-action-2-edit
8600 applied-code-action-1-command
8601 applied-code-action-1-edit
8602 applied-formatting
8603 one
8604 two
8605 three
8606 "#
8607 .unindent()
8608 );
8609 });
8610
8611 editor.update_in(cx, |editor, window, cx| {
8612 editor.undo(&Default::default(), window, cx);
8613 assert_eq!(editor.text(cx), "one \ntwo \nthree");
8614 });
8615
8616 // Perform a manual edit while waiting for an LSP command
8617 // that's being run as part of a formatting code action.
8618 let lock_guard = command_lock.lock().await;
8619 let format = editor
8620 .update_in(cx, |editor, window, cx| {
8621 editor.perform_format(
8622 project.clone(),
8623 FormatTrigger::Manual,
8624 FormatTarget::Buffers,
8625 window,
8626 cx,
8627 )
8628 })
8629 .unwrap();
8630 cx.run_until_parked();
8631 editor.update(cx, |editor, cx| {
8632 assert_eq!(
8633 editor.text(cx),
8634 r#"
8635 applied-code-action-1-edit
8636 applied-formatting
8637 one
8638 two
8639 three
8640 "#
8641 .unindent()
8642 );
8643
8644 editor.buffer.update(cx, |buffer, cx| {
8645 let ix = buffer.len(cx);
8646 buffer.edit([(ix..ix, "edited\n")], None, cx);
8647 });
8648 });
8649
8650 // Allow the LSP command to proceed. Because the buffer was edited,
8651 // the second code action will not be run.
8652 drop(lock_guard);
8653 format.await;
8654 editor.update_in(cx, |editor, window, cx| {
8655 assert_eq!(
8656 editor.text(cx),
8657 r#"
8658 applied-code-action-1-command
8659 applied-code-action-1-edit
8660 applied-formatting
8661 one
8662 two
8663 three
8664 edited
8665 "#
8666 .unindent()
8667 );
8668
8669 // The manual edit is undone first, because it is the last thing the user did
8670 // (even though the command completed afterwards).
8671 editor.undo(&Default::default(), window, cx);
8672 assert_eq!(
8673 editor.text(cx),
8674 r#"
8675 applied-code-action-1-command
8676 applied-code-action-1-edit
8677 applied-formatting
8678 one
8679 two
8680 three
8681 "#
8682 .unindent()
8683 );
8684
8685 // All the formatting (including the command, which completed after the manual edit)
8686 // is undone together.
8687 editor.undo(&Default::default(), window, cx);
8688 assert_eq!(editor.text(cx), "one \ntwo \nthree");
8689 });
8690}
8691
8692#[gpui::test]
8693async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
8694 init_test(cx, |settings| {
8695 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8696 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8697 ))
8698 });
8699
8700 let fs = FakeFs::new(cx.executor());
8701 fs.insert_file(path!("/file.ts"), Default::default()).await;
8702
8703 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8704
8705 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8706 language_registry.add(Arc::new(Language::new(
8707 LanguageConfig {
8708 name: "TypeScript".into(),
8709 matcher: LanguageMatcher {
8710 path_suffixes: vec!["ts".to_string()],
8711 ..Default::default()
8712 },
8713 ..LanguageConfig::default()
8714 },
8715 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
8716 )));
8717 update_test_language_settings(cx, |settings| {
8718 settings.defaults.prettier = Some(PrettierSettings {
8719 allowed: true,
8720 ..PrettierSettings::default()
8721 });
8722 });
8723 let mut fake_servers = language_registry.register_fake_lsp(
8724 "TypeScript",
8725 FakeLspAdapter {
8726 capabilities: lsp::ServerCapabilities {
8727 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
8728 ..Default::default()
8729 },
8730 ..Default::default()
8731 },
8732 );
8733
8734 let buffer = project
8735 .update(cx, |project, cx| {
8736 project.open_local_buffer(path!("/file.ts"), cx)
8737 })
8738 .await
8739 .unwrap();
8740
8741 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8742 let (editor, cx) = cx.add_window_view(|window, cx| {
8743 build_editor_with_project(project.clone(), buffer, window, cx)
8744 });
8745 editor.update_in(cx, |editor, window, cx| {
8746 editor.set_text(
8747 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
8748 window,
8749 cx,
8750 )
8751 });
8752
8753 cx.executor().start_waiting();
8754 let fake_server = fake_servers.next().await.unwrap();
8755
8756 let format = editor
8757 .update_in(cx, |editor, window, cx| {
8758 editor.perform_code_action_kind(
8759 project.clone(),
8760 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
8761 window,
8762 cx,
8763 )
8764 })
8765 .unwrap();
8766 fake_server
8767 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
8768 assert_eq!(
8769 params.text_document.uri,
8770 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
8771 );
8772 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
8773 lsp::CodeAction {
8774 title: "Organize Imports".to_string(),
8775 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
8776 edit: Some(lsp::WorkspaceEdit {
8777 changes: Some(
8778 [(
8779 params.text_document.uri.clone(),
8780 vec![lsp::TextEdit::new(
8781 lsp::Range::new(
8782 lsp::Position::new(1, 0),
8783 lsp::Position::new(2, 0),
8784 ),
8785 "".to_string(),
8786 )],
8787 )]
8788 .into_iter()
8789 .collect(),
8790 ),
8791 ..Default::default()
8792 }),
8793 ..Default::default()
8794 },
8795 )]))
8796 })
8797 .next()
8798 .await;
8799 cx.executor().start_waiting();
8800 format.await;
8801 assert_eq!(
8802 editor.update(cx, |editor, cx| editor.text(cx)),
8803 "import { a } from 'module';\n\nconst x = a;\n"
8804 );
8805
8806 editor.update_in(cx, |editor, window, cx| {
8807 editor.set_text(
8808 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
8809 window,
8810 cx,
8811 )
8812 });
8813 // Ensure we don't lock if code action hangs.
8814 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
8815 move |params, _| async move {
8816 assert_eq!(
8817 params.text_document.uri,
8818 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
8819 );
8820 futures::future::pending::<()>().await;
8821 unreachable!()
8822 },
8823 );
8824 let format = editor
8825 .update_in(cx, |editor, window, cx| {
8826 editor.perform_code_action_kind(
8827 project,
8828 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
8829 window,
8830 cx,
8831 )
8832 })
8833 .unwrap();
8834 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
8835 cx.executor().start_waiting();
8836 format.await;
8837 assert_eq!(
8838 editor.update(cx, |editor, cx| editor.text(cx)),
8839 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
8840 );
8841}
8842
8843#[gpui::test]
8844async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
8845 init_test(cx, |_| {});
8846
8847 let mut cx = EditorLspTestContext::new_rust(
8848 lsp::ServerCapabilities {
8849 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8850 ..Default::default()
8851 },
8852 cx,
8853 )
8854 .await;
8855
8856 cx.set_state(indoc! {"
8857 one.twoˇ
8858 "});
8859
8860 // The format request takes a long time. When it completes, it inserts
8861 // a newline and an indent before the `.`
8862 cx.lsp
8863 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
8864 let executor = cx.background_executor().clone();
8865 async move {
8866 executor.timer(Duration::from_millis(100)).await;
8867 Ok(Some(vec![lsp::TextEdit {
8868 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
8869 new_text: "\n ".into(),
8870 }]))
8871 }
8872 });
8873
8874 // Submit a format request.
8875 let format_1 = cx
8876 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8877 .unwrap();
8878 cx.executor().run_until_parked();
8879
8880 // Submit a second format request.
8881 let format_2 = cx
8882 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8883 .unwrap();
8884 cx.executor().run_until_parked();
8885
8886 // Wait for both format requests to complete
8887 cx.executor().advance_clock(Duration::from_millis(200));
8888 cx.executor().start_waiting();
8889 format_1.await.unwrap();
8890 cx.executor().start_waiting();
8891 format_2.await.unwrap();
8892
8893 // The formatting edits only happens once.
8894 cx.assert_editor_state(indoc! {"
8895 one
8896 .twoˇ
8897 "});
8898}
8899
8900#[gpui::test]
8901async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
8902 init_test(cx, |settings| {
8903 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
8904 });
8905
8906 let mut cx = EditorLspTestContext::new_rust(
8907 lsp::ServerCapabilities {
8908 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8909 ..Default::default()
8910 },
8911 cx,
8912 )
8913 .await;
8914
8915 // Set up a buffer white some trailing whitespace and no trailing newline.
8916 cx.set_state(
8917 &[
8918 "one ", //
8919 "twoˇ", //
8920 "three ", //
8921 "four", //
8922 ]
8923 .join("\n"),
8924 );
8925
8926 // Submit a format request.
8927 let format = cx
8928 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8929 .unwrap();
8930
8931 // Record which buffer changes have been sent to the language server
8932 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
8933 cx.lsp
8934 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
8935 let buffer_changes = buffer_changes.clone();
8936 move |params, _| {
8937 buffer_changes.lock().extend(
8938 params
8939 .content_changes
8940 .into_iter()
8941 .map(|e| (e.range.unwrap(), e.text)),
8942 );
8943 }
8944 });
8945
8946 // Handle formatting requests to the language server.
8947 cx.lsp
8948 .set_request_handler::<lsp::request::Formatting, _, _>({
8949 let buffer_changes = buffer_changes.clone();
8950 move |_, _| {
8951 // When formatting is requested, trailing whitespace has already been stripped,
8952 // and the trailing newline has already been added.
8953 assert_eq!(
8954 &buffer_changes.lock()[1..],
8955 &[
8956 (
8957 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
8958 "".into()
8959 ),
8960 (
8961 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
8962 "".into()
8963 ),
8964 (
8965 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
8966 "\n".into()
8967 ),
8968 ]
8969 );
8970
8971 // Insert blank lines between each line of the buffer.
8972 async move {
8973 Ok(Some(vec![
8974 lsp::TextEdit {
8975 range: lsp::Range::new(
8976 lsp::Position::new(1, 0),
8977 lsp::Position::new(1, 0),
8978 ),
8979 new_text: "\n".into(),
8980 },
8981 lsp::TextEdit {
8982 range: lsp::Range::new(
8983 lsp::Position::new(2, 0),
8984 lsp::Position::new(2, 0),
8985 ),
8986 new_text: "\n".into(),
8987 },
8988 ]))
8989 }
8990 }
8991 });
8992
8993 // After formatting the buffer, the trailing whitespace is stripped,
8994 // a newline is appended, and the edits provided by the language server
8995 // have been applied.
8996 format.await.unwrap();
8997 cx.assert_editor_state(
8998 &[
8999 "one", //
9000 "", //
9001 "twoˇ", //
9002 "", //
9003 "three", //
9004 "four", //
9005 "", //
9006 ]
9007 .join("\n"),
9008 );
9009
9010 // Undoing the formatting undoes the trailing whitespace removal, the
9011 // trailing newline, and the LSP edits.
9012 cx.update_buffer(|buffer, cx| buffer.undo(cx));
9013 cx.assert_editor_state(
9014 &[
9015 "one ", //
9016 "twoˇ", //
9017 "three ", //
9018 "four", //
9019 ]
9020 .join("\n"),
9021 );
9022}
9023
9024#[gpui::test]
9025async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
9026 cx: &mut TestAppContext,
9027) {
9028 init_test(cx, |_| {});
9029
9030 cx.update(|cx| {
9031 cx.update_global::<SettingsStore, _>(|settings, cx| {
9032 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9033 settings.auto_signature_help = Some(true);
9034 });
9035 });
9036 });
9037
9038 let mut cx = EditorLspTestContext::new_rust(
9039 lsp::ServerCapabilities {
9040 signature_help_provider: Some(lsp::SignatureHelpOptions {
9041 ..Default::default()
9042 }),
9043 ..Default::default()
9044 },
9045 cx,
9046 )
9047 .await;
9048
9049 let language = Language::new(
9050 LanguageConfig {
9051 name: "Rust".into(),
9052 brackets: BracketPairConfig {
9053 pairs: vec![
9054 BracketPair {
9055 start: "{".to_string(),
9056 end: "}".to_string(),
9057 close: true,
9058 surround: true,
9059 newline: true,
9060 },
9061 BracketPair {
9062 start: "(".to_string(),
9063 end: ")".to_string(),
9064 close: true,
9065 surround: true,
9066 newline: true,
9067 },
9068 BracketPair {
9069 start: "/*".to_string(),
9070 end: " */".to_string(),
9071 close: true,
9072 surround: true,
9073 newline: true,
9074 },
9075 BracketPair {
9076 start: "[".to_string(),
9077 end: "]".to_string(),
9078 close: false,
9079 surround: false,
9080 newline: true,
9081 },
9082 BracketPair {
9083 start: "\"".to_string(),
9084 end: "\"".to_string(),
9085 close: true,
9086 surround: true,
9087 newline: false,
9088 },
9089 BracketPair {
9090 start: "<".to_string(),
9091 end: ">".to_string(),
9092 close: false,
9093 surround: true,
9094 newline: true,
9095 },
9096 ],
9097 ..Default::default()
9098 },
9099 autoclose_before: "})]".to_string(),
9100 ..Default::default()
9101 },
9102 Some(tree_sitter_rust::LANGUAGE.into()),
9103 );
9104 let language = Arc::new(language);
9105
9106 cx.language_registry().add(language.clone());
9107 cx.update_buffer(|buffer, cx| {
9108 buffer.set_language(Some(language), cx);
9109 });
9110
9111 cx.set_state(
9112 &r#"
9113 fn main() {
9114 sampleˇ
9115 }
9116 "#
9117 .unindent(),
9118 );
9119
9120 cx.update_editor(|editor, window, cx| {
9121 editor.handle_input("(", window, cx);
9122 });
9123 cx.assert_editor_state(
9124 &"
9125 fn main() {
9126 sample(ˇ)
9127 }
9128 "
9129 .unindent(),
9130 );
9131
9132 let mocked_response = lsp::SignatureHelp {
9133 signatures: vec![lsp::SignatureInformation {
9134 label: "fn sample(param1: u8, param2: u8)".to_string(),
9135 documentation: None,
9136 parameters: Some(vec![
9137 lsp::ParameterInformation {
9138 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9139 documentation: None,
9140 },
9141 lsp::ParameterInformation {
9142 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9143 documentation: None,
9144 },
9145 ]),
9146 active_parameter: None,
9147 }],
9148 active_signature: Some(0),
9149 active_parameter: Some(0),
9150 };
9151 handle_signature_help_request(&mut cx, mocked_response).await;
9152
9153 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9154 .await;
9155
9156 cx.editor(|editor, _, _| {
9157 let signature_help_state = editor.signature_help_state.popover().cloned();
9158 assert_eq!(
9159 signature_help_state.unwrap().label,
9160 "param1: u8, param2: u8"
9161 );
9162 });
9163}
9164
9165#[gpui::test]
9166async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
9167 init_test(cx, |_| {});
9168
9169 cx.update(|cx| {
9170 cx.update_global::<SettingsStore, _>(|settings, cx| {
9171 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9172 settings.auto_signature_help = Some(false);
9173 settings.show_signature_help_after_edits = Some(false);
9174 });
9175 });
9176 });
9177
9178 let mut cx = EditorLspTestContext::new_rust(
9179 lsp::ServerCapabilities {
9180 signature_help_provider: Some(lsp::SignatureHelpOptions {
9181 ..Default::default()
9182 }),
9183 ..Default::default()
9184 },
9185 cx,
9186 )
9187 .await;
9188
9189 let language = Language::new(
9190 LanguageConfig {
9191 name: "Rust".into(),
9192 brackets: BracketPairConfig {
9193 pairs: vec![
9194 BracketPair {
9195 start: "{".to_string(),
9196 end: "}".to_string(),
9197 close: true,
9198 surround: true,
9199 newline: true,
9200 },
9201 BracketPair {
9202 start: "(".to_string(),
9203 end: ")".to_string(),
9204 close: true,
9205 surround: true,
9206 newline: true,
9207 },
9208 BracketPair {
9209 start: "/*".to_string(),
9210 end: " */".to_string(),
9211 close: true,
9212 surround: true,
9213 newline: true,
9214 },
9215 BracketPair {
9216 start: "[".to_string(),
9217 end: "]".to_string(),
9218 close: false,
9219 surround: false,
9220 newline: true,
9221 },
9222 BracketPair {
9223 start: "\"".to_string(),
9224 end: "\"".to_string(),
9225 close: true,
9226 surround: true,
9227 newline: false,
9228 },
9229 BracketPair {
9230 start: "<".to_string(),
9231 end: ">".to_string(),
9232 close: false,
9233 surround: true,
9234 newline: true,
9235 },
9236 ],
9237 ..Default::default()
9238 },
9239 autoclose_before: "})]".to_string(),
9240 ..Default::default()
9241 },
9242 Some(tree_sitter_rust::LANGUAGE.into()),
9243 );
9244 let language = Arc::new(language);
9245
9246 cx.language_registry().add(language.clone());
9247 cx.update_buffer(|buffer, cx| {
9248 buffer.set_language(Some(language), cx);
9249 });
9250
9251 // Ensure that signature_help is not called when no signature help is enabled.
9252 cx.set_state(
9253 &r#"
9254 fn main() {
9255 sampleˇ
9256 }
9257 "#
9258 .unindent(),
9259 );
9260 cx.update_editor(|editor, window, cx| {
9261 editor.handle_input("(", window, cx);
9262 });
9263 cx.assert_editor_state(
9264 &"
9265 fn main() {
9266 sample(ˇ)
9267 }
9268 "
9269 .unindent(),
9270 );
9271 cx.editor(|editor, _, _| {
9272 assert!(editor.signature_help_state.task().is_none());
9273 });
9274
9275 let mocked_response = lsp::SignatureHelp {
9276 signatures: vec![lsp::SignatureInformation {
9277 label: "fn sample(param1: u8, param2: u8)".to_string(),
9278 documentation: None,
9279 parameters: Some(vec![
9280 lsp::ParameterInformation {
9281 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9282 documentation: None,
9283 },
9284 lsp::ParameterInformation {
9285 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9286 documentation: None,
9287 },
9288 ]),
9289 active_parameter: None,
9290 }],
9291 active_signature: Some(0),
9292 active_parameter: Some(0),
9293 };
9294
9295 // Ensure that signature_help is called when enabled afte edits
9296 cx.update(|_, cx| {
9297 cx.update_global::<SettingsStore, _>(|settings, cx| {
9298 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9299 settings.auto_signature_help = Some(false);
9300 settings.show_signature_help_after_edits = Some(true);
9301 });
9302 });
9303 });
9304 cx.set_state(
9305 &r#"
9306 fn main() {
9307 sampleˇ
9308 }
9309 "#
9310 .unindent(),
9311 );
9312 cx.update_editor(|editor, window, cx| {
9313 editor.handle_input("(", window, cx);
9314 });
9315 cx.assert_editor_state(
9316 &"
9317 fn main() {
9318 sample(ˇ)
9319 }
9320 "
9321 .unindent(),
9322 );
9323 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9324 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9325 .await;
9326 cx.update_editor(|editor, _, _| {
9327 let signature_help_state = editor.signature_help_state.popover().cloned();
9328 assert!(signature_help_state.is_some());
9329 assert_eq!(
9330 signature_help_state.unwrap().label,
9331 "param1: u8, param2: u8"
9332 );
9333 editor.signature_help_state = SignatureHelpState::default();
9334 });
9335
9336 // Ensure that signature_help is called when auto signature help override is enabled
9337 cx.update(|_, cx| {
9338 cx.update_global::<SettingsStore, _>(|settings, cx| {
9339 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9340 settings.auto_signature_help = Some(true);
9341 settings.show_signature_help_after_edits = Some(false);
9342 });
9343 });
9344 });
9345 cx.set_state(
9346 &r#"
9347 fn main() {
9348 sampleˇ
9349 }
9350 "#
9351 .unindent(),
9352 );
9353 cx.update_editor(|editor, window, cx| {
9354 editor.handle_input("(", window, cx);
9355 });
9356 cx.assert_editor_state(
9357 &"
9358 fn main() {
9359 sample(ˇ)
9360 }
9361 "
9362 .unindent(),
9363 );
9364 handle_signature_help_request(&mut cx, mocked_response).await;
9365 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9366 .await;
9367 cx.editor(|editor, _, _| {
9368 let signature_help_state = editor.signature_help_state.popover().cloned();
9369 assert!(signature_help_state.is_some());
9370 assert_eq!(
9371 signature_help_state.unwrap().label,
9372 "param1: u8, param2: u8"
9373 );
9374 });
9375}
9376
9377#[gpui::test]
9378async fn test_signature_help(cx: &mut TestAppContext) {
9379 init_test(cx, |_| {});
9380 cx.update(|cx| {
9381 cx.update_global::<SettingsStore, _>(|settings, cx| {
9382 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9383 settings.auto_signature_help = Some(true);
9384 });
9385 });
9386 });
9387
9388 let mut cx = EditorLspTestContext::new_rust(
9389 lsp::ServerCapabilities {
9390 signature_help_provider: Some(lsp::SignatureHelpOptions {
9391 ..Default::default()
9392 }),
9393 ..Default::default()
9394 },
9395 cx,
9396 )
9397 .await;
9398
9399 // A test that directly calls `show_signature_help`
9400 cx.update_editor(|editor, window, cx| {
9401 editor.show_signature_help(&ShowSignatureHelp, window, cx);
9402 });
9403
9404 let mocked_response = lsp::SignatureHelp {
9405 signatures: vec![lsp::SignatureInformation {
9406 label: "fn sample(param1: u8, param2: u8)".to_string(),
9407 documentation: None,
9408 parameters: Some(vec![
9409 lsp::ParameterInformation {
9410 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9411 documentation: None,
9412 },
9413 lsp::ParameterInformation {
9414 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9415 documentation: None,
9416 },
9417 ]),
9418 active_parameter: None,
9419 }],
9420 active_signature: Some(0),
9421 active_parameter: Some(0),
9422 };
9423 handle_signature_help_request(&mut cx, mocked_response).await;
9424
9425 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9426 .await;
9427
9428 cx.editor(|editor, _, _| {
9429 let signature_help_state = editor.signature_help_state.popover().cloned();
9430 assert!(signature_help_state.is_some());
9431 assert_eq!(
9432 signature_help_state.unwrap().label,
9433 "param1: u8, param2: u8"
9434 );
9435 });
9436
9437 // When exiting outside from inside the brackets, `signature_help` is closed.
9438 cx.set_state(indoc! {"
9439 fn main() {
9440 sample(ˇ);
9441 }
9442
9443 fn sample(param1: u8, param2: u8) {}
9444 "});
9445
9446 cx.update_editor(|editor, window, cx| {
9447 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
9448 });
9449
9450 let mocked_response = lsp::SignatureHelp {
9451 signatures: Vec::new(),
9452 active_signature: None,
9453 active_parameter: None,
9454 };
9455 handle_signature_help_request(&mut cx, mocked_response).await;
9456
9457 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
9458 .await;
9459
9460 cx.editor(|editor, _, _| {
9461 assert!(!editor.signature_help_state.is_shown());
9462 });
9463
9464 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
9465 cx.set_state(indoc! {"
9466 fn main() {
9467 sample(ˇ);
9468 }
9469
9470 fn sample(param1: u8, param2: u8) {}
9471 "});
9472
9473 let mocked_response = lsp::SignatureHelp {
9474 signatures: vec![lsp::SignatureInformation {
9475 label: "fn sample(param1: u8, param2: u8)".to_string(),
9476 documentation: None,
9477 parameters: Some(vec![
9478 lsp::ParameterInformation {
9479 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9480 documentation: None,
9481 },
9482 lsp::ParameterInformation {
9483 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9484 documentation: None,
9485 },
9486 ]),
9487 active_parameter: None,
9488 }],
9489 active_signature: Some(0),
9490 active_parameter: Some(0),
9491 };
9492 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9493 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9494 .await;
9495 cx.editor(|editor, _, _| {
9496 assert!(editor.signature_help_state.is_shown());
9497 });
9498
9499 // Restore the popover with more parameter input
9500 cx.set_state(indoc! {"
9501 fn main() {
9502 sample(param1, param2ˇ);
9503 }
9504
9505 fn sample(param1: u8, param2: u8) {}
9506 "});
9507
9508 let mocked_response = lsp::SignatureHelp {
9509 signatures: vec![lsp::SignatureInformation {
9510 label: "fn sample(param1: u8, param2: u8)".to_string(),
9511 documentation: None,
9512 parameters: Some(vec![
9513 lsp::ParameterInformation {
9514 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9515 documentation: None,
9516 },
9517 lsp::ParameterInformation {
9518 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9519 documentation: None,
9520 },
9521 ]),
9522 active_parameter: None,
9523 }],
9524 active_signature: Some(0),
9525 active_parameter: Some(1),
9526 };
9527 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9528 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9529 .await;
9530
9531 // When selecting a range, the popover is gone.
9532 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
9533 cx.update_editor(|editor, window, cx| {
9534 editor.change_selections(None, window, cx, |s| {
9535 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
9536 })
9537 });
9538 cx.assert_editor_state(indoc! {"
9539 fn main() {
9540 sample(param1, «ˇparam2»);
9541 }
9542
9543 fn sample(param1: u8, param2: u8) {}
9544 "});
9545 cx.editor(|editor, _, _| {
9546 assert!(!editor.signature_help_state.is_shown());
9547 });
9548
9549 // When unselecting again, the popover is back if within the brackets.
9550 cx.update_editor(|editor, window, cx| {
9551 editor.change_selections(None, window, cx, |s| {
9552 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9553 })
9554 });
9555 cx.assert_editor_state(indoc! {"
9556 fn main() {
9557 sample(param1, ˇparam2);
9558 }
9559
9560 fn sample(param1: u8, param2: u8) {}
9561 "});
9562 handle_signature_help_request(&mut cx, mocked_response).await;
9563 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9564 .await;
9565 cx.editor(|editor, _, _| {
9566 assert!(editor.signature_help_state.is_shown());
9567 });
9568
9569 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
9570 cx.update_editor(|editor, window, cx| {
9571 editor.change_selections(None, window, cx, |s| {
9572 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
9573 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9574 })
9575 });
9576 cx.assert_editor_state(indoc! {"
9577 fn main() {
9578 sample(param1, ˇparam2);
9579 }
9580
9581 fn sample(param1: u8, param2: u8) {}
9582 "});
9583
9584 let mocked_response = lsp::SignatureHelp {
9585 signatures: vec![lsp::SignatureInformation {
9586 label: "fn sample(param1: u8, param2: u8)".to_string(),
9587 documentation: None,
9588 parameters: Some(vec![
9589 lsp::ParameterInformation {
9590 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9591 documentation: None,
9592 },
9593 lsp::ParameterInformation {
9594 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9595 documentation: None,
9596 },
9597 ]),
9598 active_parameter: None,
9599 }],
9600 active_signature: Some(0),
9601 active_parameter: Some(1),
9602 };
9603 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9604 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9605 .await;
9606 cx.update_editor(|editor, _, cx| {
9607 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
9608 });
9609 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
9610 .await;
9611 cx.update_editor(|editor, window, cx| {
9612 editor.change_selections(None, window, cx, |s| {
9613 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
9614 })
9615 });
9616 cx.assert_editor_state(indoc! {"
9617 fn main() {
9618 sample(param1, «ˇparam2»);
9619 }
9620
9621 fn sample(param1: u8, param2: u8) {}
9622 "});
9623 cx.update_editor(|editor, window, cx| {
9624 editor.change_selections(None, window, cx, |s| {
9625 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9626 })
9627 });
9628 cx.assert_editor_state(indoc! {"
9629 fn main() {
9630 sample(param1, ˇparam2);
9631 }
9632
9633 fn sample(param1: u8, param2: u8) {}
9634 "});
9635 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
9636 .await;
9637}
9638
9639#[gpui::test]
9640async fn test_completion_mode(cx: &mut TestAppContext) {
9641 init_test(cx, |_| {});
9642 let mut cx = EditorLspTestContext::new_rust(
9643 lsp::ServerCapabilities {
9644 completion_provider: Some(lsp::CompletionOptions {
9645 resolve_provider: Some(true),
9646 ..Default::default()
9647 }),
9648 ..Default::default()
9649 },
9650 cx,
9651 )
9652 .await;
9653
9654 struct Run {
9655 run_description: &'static str,
9656 initial_state: String,
9657 buffer_marked_text: String,
9658 completion_text: &'static str,
9659 expected_with_insert_mode: String,
9660 expected_with_replace_mode: String,
9661 expected_with_replace_subsequence_mode: String,
9662 expected_with_replace_suffix_mode: String,
9663 }
9664
9665 let runs = [
9666 Run {
9667 run_description: "Start of word matches completion text",
9668 initial_state: "before ediˇ after".into(),
9669 buffer_marked_text: "before <edi|> after".into(),
9670 completion_text: "editor",
9671 expected_with_insert_mode: "before editorˇ after".into(),
9672 expected_with_replace_mode: "before editorˇ after".into(),
9673 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
9674 expected_with_replace_suffix_mode: "before editorˇ after".into(),
9675 },
9676 Run {
9677 run_description: "Accept same text at the middle of the word",
9678 initial_state: "before ediˇtor after".into(),
9679 buffer_marked_text: "before <edi|tor> after".into(),
9680 completion_text: "editor",
9681 expected_with_insert_mode: "before editorˇtor after".into(),
9682 expected_with_replace_mode: "before ediˇtor after".into(),
9683 expected_with_replace_subsequence_mode: "before ediˇtor after".into(),
9684 expected_with_replace_suffix_mode: "before ediˇtor after".into(),
9685 },
9686 Run {
9687 run_description: "End of word matches completion text -- cursor at end",
9688 initial_state: "before torˇ after".into(),
9689 buffer_marked_text: "before <tor|> after".into(),
9690 completion_text: "editor",
9691 expected_with_insert_mode: "before editorˇ after".into(),
9692 expected_with_replace_mode: "before editorˇ after".into(),
9693 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
9694 expected_with_replace_suffix_mode: "before editorˇ after".into(),
9695 },
9696 Run {
9697 run_description: "End of word matches completion text -- cursor at start",
9698 initial_state: "before ˇtor after".into(),
9699 buffer_marked_text: "before <|tor> after".into(),
9700 completion_text: "editor",
9701 expected_with_insert_mode: "before editorˇtor after".into(),
9702 expected_with_replace_mode: "before editorˇ after".into(),
9703 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
9704 expected_with_replace_suffix_mode: "before editorˇ after".into(),
9705 },
9706 Run {
9707 run_description: "Prepend text containing whitespace",
9708 initial_state: "pˇfield: bool".into(),
9709 buffer_marked_text: "<p|field>: bool".into(),
9710 completion_text: "pub ",
9711 expected_with_insert_mode: "pub ˇfield: bool".into(),
9712 expected_with_replace_mode: "pub ˇ: bool".into(),
9713 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
9714 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
9715 },
9716 Run {
9717 run_description: "Add element to start of list",
9718 initial_state: "[element_ˇelement_2]".into(),
9719 buffer_marked_text: "[<element_|element_2>]".into(),
9720 completion_text: "element_1",
9721 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
9722 expected_with_replace_mode: "[element_1ˇ]".into(),
9723 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
9724 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
9725 },
9726 Run {
9727 run_description: "Add element to start of list -- first and second elements are equal",
9728 initial_state: "[elˇelement]".into(),
9729 buffer_marked_text: "[<el|element>]".into(),
9730 completion_text: "element",
9731 expected_with_insert_mode: "[elementˇelement]".into(),
9732 expected_with_replace_mode: "[elˇement]".into(),
9733 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
9734 expected_with_replace_suffix_mode: "[elˇement]".into(),
9735 },
9736 Run {
9737 run_description: "Ends with matching suffix",
9738 initial_state: "SubˇError".into(),
9739 buffer_marked_text: "<Sub|Error>".into(),
9740 completion_text: "SubscriptionError",
9741 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
9742 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
9743 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
9744 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
9745 },
9746 Run {
9747 run_description: "Suffix is a subsequence -- contiguous",
9748 initial_state: "SubˇErr".into(),
9749 buffer_marked_text: "<Sub|Err>".into(),
9750 completion_text: "SubscriptionError",
9751 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
9752 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
9753 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
9754 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
9755 },
9756 Run {
9757 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
9758 initial_state: "Suˇscrirr".into(),
9759 buffer_marked_text: "<Su|scrirr>".into(),
9760 completion_text: "SubscriptionError",
9761 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
9762 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
9763 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
9764 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
9765 },
9766 Run {
9767 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
9768 initial_state: "foo(indˇix)".into(),
9769 buffer_marked_text: "foo(<ind|ix>)".into(),
9770 completion_text: "node_index",
9771 expected_with_insert_mode: "foo(node_indexˇix)".into(),
9772 expected_with_replace_mode: "foo(node_indexˇ)".into(),
9773 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
9774 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
9775 },
9776 ];
9777
9778 for run in runs {
9779 let run_variations = [
9780 (LspInsertMode::Insert, run.expected_with_insert_mode),
9781 (LspInsertMode::Replace, run.expected_with_replace_mode),
9782 (
9783 LspInsertMode::ReplaceSubsequence,
9784 run.expected_with_replace_subsequence_mode,
9785 ),
9786 (
9787 LspInsertMode::ReplaceSuffix,
9788 run.expected_with_replace_suffix_mode,
9789 ),
9790 ];
9791
9792 for (lsp_insert_mode, expected_text) in run_variations {
9793 eprintln!(
9794 "run = {:?}, mode = {lsp_insert_mode:.?}",
9795 run.run_description,
9796 );
9797
9798 update_test_language_settings(&mut cx, |settings| {
9799 settings.defaults.completions = Some(CompletionSettings {
9800 lsp_insert_mode,
9801 words: WordsCompletionMode::Disabled,
9802 lsp: true,
9803 lsp_fetch_timeout_ms: 0,
9804 });
9805 });
9806
9807 cx.set_state(&run.initial_state);
9808 cx.update_editor(|editor, window, cx| {
9809 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
9810 });
9811
9812 let counter = Arc::new(AtomicUsize::new(0));
9813 handle_completion_request_with_insert_and_replace(
9814 &mut cx,
9815 &run.buffer_marked_text,
9816 vec![run.completion_text],
9817 counter.clone(),
9818 )
9819 .await;
9820 cx.condition(|editor, _| editor.context_menu_visible())
9821 .await;
9822 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
9823
9824 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9825 editor
9826 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9827 .unwrap()
9828 });
9829 cx.assert_editor_state(&expected_text);
9830 handle_resolve_completion_request(&mut cx, None).await;
9831 apply_additional_edits.await.unwrap();
9832 }
9833 }
9834}
9835
9836#[gpui::test]
9837async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
9838 init_test(cx, |_| {});
9839 let mut cx = EditorLspTestContext::new_rust(
9840 lsp::ServerCapabilities {
9841 completion_provider: Some(lsp::CompletionOptions {
9842 resolve_provider: Some(true),
9843 ..Default::default()
9844 }),
9845 ..Default::default()
9846 },
9847 cx,
9848 )
9849 .await;
9850
9851 let initial_state = "SubˇError";
9852 let buffer_marked_text = "<Sub|Error>";
9853 let completion_text = "SubscriptionError";
9854 let expected_with_insert_mode = "SubscriptionErrorˇError";
9855 let expected_with_replace_mode = "SubscriptionErrorˇ";
9856
9857 update_test_language_settings(&mut cx, |settings| {
9858 settings.defaults.completions = Some(CompletionSettings {
9859 words: WordsCompletionMode::Disabled,
9860 // set the opposite here to ensure that the action is overriding the default behavior
9861 lsp_insert_mode: LspInsertMode::Insert,
9862 lsp: true,
9863 lsp_fetch_timeout_ms: 0,
9864 });
9865 });
9866
9867 cx.set_state(initial_state);
9868 cx.update_editor(|editor, window, cx| {
9869 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
9870 });
9871
9872 let counter = Arc::new(AtomicUsize::new(0));
9873 handle_completion_request_with_insert_and_replace(
9874 &mut cx,
9875 &buffer_marked_text,
9876 vec![completion_text],
9877 counter.clone(),
9878 )
9879 .await;
9880 cx.condition(|editor, _| editor.context_menu_visible())
9881 .await;
9882 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
9883
9884 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9885 editor
9886 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
9887 .unwrap()
9888 });
9889 cx.assert_editor_state(&expected_with_replace_mode);
9890 handle_resolve_completion_request(&mut cx, None).await;
9891 apply_additional_edits.await.unwrap();
9892
9893 update_test_language_settings(&mut cx, |settings| {
9894 settings.defaults.completions = Some(CompletionSettings {
9895 words: WordsCompletionMode::Disabled,
9896 // set the opposite here to ensure that the action is overriding the default behavior
9897 lsp_insert_mode: LspInsertMode::Replace,
9898 lsp: true,
9899 lsp_fetch_timeout_ms: 0,
9900 });
9901 });
9902
9903 cx.set_state(initial_state);
9904 cx.update_editor(|editor, window, cx| {
9905 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
9906 });
9907 handle_completion_request_with_insert_and_replace(
9908 &mut cx,
9909 &buffer_marked_text,
9910 vec![completion_text],
9911 counter.clone(),
9912 )
9913 .await;
9914 cx.condition(|editor, _| editor.context_menu_visible())
9915 .await;
9916 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
9917
9918 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9919 editor
9920 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
9921 .unwrap()
9922 });
9923 cx.assert_editor_state(&expected_with_insert_mode);
9924 handle_resolve_completion_request(&mut cx, None).await;
9925 apply_additional_edits.await.unwrap();
9926}
9927
9928#[gpui::test]
9929async fn test_completion(cx: &mut TestAppContext) {
9930 init_test(cx, |_| {});
9931
9932 let mut cx = EditorLspTestContext::new_rust(
9933 lsp::ServerCapabilities {
9934 completion_provider: Some(lsp::CompletionOptions {
9935 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9936 resolve_provider: Some(true),
9937 ..Default::default()
9938 }),
9939 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9940 ..Default::default()
9941 },
9942 cx,
9943 )
9944 .await;
9945 let counter = Arc::new(AtomicUsize::new(0));
9946
9947 cx.set_state(indoc! {"
9948 oneˇ
9949 two
9950 three
9951 "});
9952 cx.simulate_keystroke(".");
9953 handle_completion_request(
9954 &mut cx,
9955 indoc! {"
9956 one.|<>
9957 two
9958 three
9959 "},
9960 vec!["first_completion", "second_completion"],
9961 counter.clone(),
9962 )
9963 .await;
9964 cx.condition(|editor, _| editor.context_menu_visible())
9965 .await;
9966 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
9967
9968 let _handler = handle_signature_help_request(
9969 &mut cx,
9970 lsp::SignatureHelp {
9971 signatures: vec![lsp::SignatureInformation {
9972 label: "test signature".to_string(),
9973 documentation: None,
9974 parameters: Some(vec![lsp::ParameterInformation {
9975 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
9976 documentation: None,
9977 }]),
9978 active_parameter: None,
9979 }],
9980 active_signature: None,
9981 active_parameter: None,
9982 },
9983 );
9984 cx.update_editor(|editor, window, cx| {
9985 assert!(
9986 !editor.signature_help_state.is_shown(),
9987 "No signature help was called for"
9988 );
9989 editor.show_signature_help(&ShowSignatureHelp, window, cx);
9990 });
9991 cx.run_until_parked();
9992 cx.update_editor(|editor, _, _| {
9993 assert!(
9994 !editor.signature_help_state.is_shown(),
9995 "No signature help should be shown when completions menu is open"
9996 );
9997 });
9998
9999 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10000 editor.context_menu_next(&Default::default(), window, cx);
10001 editor
10002 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10003 .unwrap()
10004 });
10005 cx.assert_editor_state(indoc! {"
10006 one.second_completionˇ
10007 two
10008 three
10009 "});
10010
10011 handle_resolve_completion_request(
10012 &mut cx,
10013 Some(vec![
10014 (
10015 //This overlaps with the primary completion edit which is
10016 //misbehavior from the LSP spec, test that we filter it out
10017 indoc! {"
10018 one.second_ˇcompletion
10019 two
10020 threeˇ
10021 "},
10022 "overlapping additional edit",
10023 ),
10024 (
10025 indoc! {"
10026 one.second_completion
10027 two
10028 threeˇ
10029 "},
10030 "\nadditional edit",
10031 ),
10032 ]),
10033 )
10034 .await;
10035 apply_additional_edits.await.unwrap();
10036 cx.assert_editor_state(indoc! {"
10037 one.second_completionˇ
10038 two
10039 three
10040 additional edit
10041 "});
10042
10043 cx.set_state(indoc! {"
10044 one.second_completion
10045 twoˇ
10046 threeˇ
10047 additional edit
10048 "});
10049 cx.simulate_keystroke(" ");
10050 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
10051 cx.simulate_keystroke("s");
10052 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
10053
10054 cx.assert_editor_state(indoc! {"
10055 one.second_completion
10056 two sˇ
10057 three sˇ
10058 additional edit
10059 "});
10060 handle_completion_request(
10061 &mut cx,
10062 indoc! {"
10063 one.second_completion
10064 two s
10065 three <s|>
10066 additional edit
10067 "},
10068 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
10069 counter.clone(),
10070 )
10071 .await;
10072 cx.condition(|editor, _| editor.context_menu_visible())
10073 .await;
10074 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
10075
10076 cx.simulate_keystroke("i");
10077
10078 handle_completion_request(
10079 &mut cx,
10080 indoc! {"
10081 one.second_completion
10082 two si
10083 three <si|>
10084 additional edit
10085 "},
10086 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
10087 counter.clone(),
10088 )
10089 .await;
10090 cx.condition(|editor, _| editor.context_menu_visible())
10091 .await;
10092 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
10093
10094 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10095 editor
10096 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10097 .unwrap()
10098 });
10099 cx.assert_editor_state(indoc! {"
10100 one.second_completion
10101 two sixth_completionˇ
10102 three sixth_completionˇ
10103 additional edit
10104 "});
10105
10106 apply_additional_edits.await.unwrap();
10107
10108 update_test_language_settings(&mut cx, |settings| {
10109 settings.defaults.show_completions_on_input = Some(false);
10110 });
10111 cx.set_state("editorˇ");
10112 cx.simulate_keystroke(".");
10113 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
10114 cx.simulate_keystrokes("c l o");
10115 cx.assert_editor_state("editor.cloˇ");
10116 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
10117 cx.update_editor(|editor, window, cx| {
10118 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10119 });
10120 handle_completion_request(
10121 &mut cx,
10122 "editor.<clo|>",
10123 vec!["close", "clobber"],
10124 counter.clone(),
10125 )
10126 .await;
10127 cx.condition(|editor, _| editor.context_menu_visible())
10128 .await;
10129 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
10130
10131 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10132 editor
10133 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10134 .unwrap()
10135 });
10136 cx.assert_editor_state("editor.closeˇ");
10137 handle_resolve_completion_request(&mut cx, None).await;
10138 apply_additional_edits.await.unwrap();
10139}
10140
10141#[gpui::test]
10142async fn test_word_completion(cx: &mut TestAppContext) {
10143 let lsp_fetch_timeout_ms = 10;
10144 init_test(cx, |language_settings| {
10145 language_settings.defaults.completions = Some(CompletionSettings {
10146 words: WordsCompletionMode::Fallback,
10147 lsp: true,
10148 lsp_fetch_timeout_ms: 10,
10149 lsp_insert_mode: LspInsertMode::Insert,
10150 });
10151 });
10152
10153 let mut cx = EditorLspTestContext::new_rust(
10154 lsp::ServerCapabilities {
10155 completion_provider: Some(lsp::CompletionOptions {
10156 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10157 ..lsp::CompletionOptions::default()
10158 }),
10159 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10160 ..lsp::ServerCapabilities::default()
10161 },
10162 cx,
10163 )
10164 .await;
10165
10166 let throttle_completions = Arc::new(AtomicBool::new(false));
10167
10168 let lsp_throttle_completions = throttle_completions.clone();
10169 let _completion_requests_handler =
10170 cx.lsp
10171 .server
10172 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
10173 let lsp_throttle_completions = lsp_throttle_completions.clone();
10174 let cx = cx.clone();
10175 async move {
10176 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
10177 cx.background_executor()
10178 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
10179 .await;
10180 }
10181 Ok(Some(lsp::CompletionResponse::Array(vec![
10182 lsp::CompletionItem {
10183 label: "first".into(),
10184 ..lsp::CompletionItem::default()
10185 },
10186 lsp::CompletionItem {
10187 label: "last".into(),
10188 ..lsp::CompletionItem::default()
10189 },
10190 ])))
10191 }
10192 });
10193
10194 cx.set_state(indoc! {"
10195 oneˇ
10196 two
10197 three
10198 "});
10199 cx.simulate_keystroke(".");
10200 cx.executor().run_until_parked();
10201 cx.condition(|editor, _| editor.context_menu_visible())
10202 .await;
10203 cx.update_editor(|editor, window, cx| {
10204 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10205 {
10206 assert_eq!(
10207 completion_menu_entries(&menu),
10208 &["first", "last"],
10209 "When LSP server is fast to reply, no fallback word completions are used"
10210 );
10211 } else {
10212 panic!("expected completion menu to be open");
10213 }
10214 editor.cancel(&Cancel, window, cx);
10215 });
10216 cx.executor().run_until_parked();
10217 cx.condition(|editor, _| !editor.context_menu_visible())
10218 .await;
10219
10220 throttle_completions.store(true, atomic::Ordering::Release);
10221 cx.simulate_keystroke(".");
10222 cx.executor()
10223 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
10224 cx.executor().run_until_parked();
10225 cx.condition(|editor, _| editor.context_menu_visible())
10226 .await;
10227 cx.update_editor(|editor, _, _| {
10228 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10229 {
10230 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
10231 "When LSP server is slow, document words can be shown instead, if configured accordingly");
10232 } else {
10233 panic!("expected completion menu to be open");
10234 }
10235 });
10236}
10237
10238#[gpui::test]
10239async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
10240 init_test(cx, |language_settings| {
10241 language_settings.defaults.completions = Some(CompletionSettings {
10242 words: WordsCompletionMode::Enabled,
10243 lsp: true,
10244 lsp_fetch_timeout_ms: 0,
10245 lsp_insert_mode: LspInsertMode::Insert,
10246 });
10247 });
10248
10249 let mut cx = EditorLspTestContext::new_rust(
10250 lsp::ServerCapabilities {
10251 completion_provider: Some(lsp::CompletionOptions {
10252 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10253 ..lsp::CompletionOptions::default()
10254 }),
10255 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10256 ..lsp::ServerCapabilities::default()
10257 },
10258 cx,
10259 )
10260 .await;
10261
10262 let _completion_requests_handler =
10263 cx.lsp
10264 .server
10265 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
10266 Ok(Some(lsp::CompletionResponse::Array(vec![
10267 lsp::CompletionItem {
10268 label: "first".into(),
10269 ..lsp::CompletionItem::default()
10270 },
10271 lsp::CompletionItem {
10272 label: "last".into(),
10273 ..lsp::CompletionItem::default()
10274 },
10275 ])))
10276 });
10277
10278 cx.set_state(indoc! {"ˇ
10279 first
10280 last
10281 second
10282 "});
10283 cx.simulate_keystroke(".");
10284 cx.executor().run_until_parked();
10285 cx.condition(|editor, _| editor.context_menu_visible())
10286 .await;
10287 cx.update_editor(|editor, _, _| {
10288 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10289 {
10290 assert_eq!(
10291 completion_menu_entries(&menu),
10292 &["first", "last", "second"],
10293 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
10294 );
10295 } else {
10296 panic!("expected completion menu to be open");
10297 }
10298 });
10299}
10300
10301#[gpui::test]
10302async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
10303 init_test(cx, |language_settings| {
10304 language_settings.defaults.completions = Some(CompletionSettings {
10305 words: WordsCompletionMode::Disabled,
10306 lsp: true,
10307 lsp_fetch_timeout_ms: 0,
10308 lsp_insert_mode: LspInsertMode::Insert,
10309 });
10310 });
10311
10312 let mut cx = EditorLspTestContext::new_rust(
10313 lsp::ServerCapabilities {
10314 completion_provider: Some(lsp::CompletionOptions {
10315 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10316 ..lsp::CompletionOptions::default()
10317 }),
10318 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10319 ..lsp::ServerCapabilities::default()
10320 },
10321 cx,
10322 )
10323 .await;
10324
10325 let _completion_requests_handler =
10326 cx.lsp
10327 .server
10328 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
10329 panic!("LSP completions should not be queried when dealing with word completions")
10330 });
10331
10332 cx.set_state(indoc! {"ˇ
10333 first
10334 last
10335 second
10336 "});
10337 cx.update_editor(|editor, window, cx| {
10338 editor.show_word_completions(&ShowWordCompletions, window, cx);
10339 });
10340 cx.executor().run_until_parked();
10341 cx.condition(|editor, _| editor.context_menu_visible())
10342 .await;
10343 cx.update_editor(|editor, _, _| {
10344 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10345 {
10346 assert_eq!(
10347 completion_menu_entries(&menu),
10348 &["first", "last", "second"],
10349 "`ShowWordCompletions` action should show word completions"
10350 );
10351 } else {
10352 panic!("expected completion menu to be open");
10353 }
10354 });
10355
10356 cx.simulate_keystroke("l");
10357 cx.executor().run_until_parked();
10358 cx.condition(|editor, _| editor.context_menu_visible())
10359 .await;
10360 cx.update_editor(|editor, _, _| {
10361 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10362 {
10363 assert_eq!(
10364 completion_menu_entries(&menu),
10365 &["last"],
10366 "After showing word completions, further editing should filter them and not query the LSP"
10367 );
10368 } else {
10369 panic!("expected completion menu to be open");
10370 }
10371 });
10372}
10373
10374#[gpui::test]
10375async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
10376 init_test(cx, |language_settings| {
10377 language_settings.defaults.completions = Some(CompletionSettings {
10378 words: WordsCompletionMode::Fallback,
10379 lsp: false,
10380 lsp_fetch_timeout_ms: 0,
10381 lsp_insert_mode: LspInsertMode::Insert,
10382 });
10383 });
10384
10385 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
10386
10387 cx.set_state(indoc! {"ˇ
10388 0_usize
10389 let
10390 33
10391 4.5f32
10392 "});
10393 cx.update_editor(|editor, window, cx| {
10394 editor.show_completions(&ShowCompletions::default(), window, cx);
10395 });
10396 cx.executor().run_until_parked();
10397 cx.condition(|editor, _| editor.context_menu_visible())
10398 .await;
10399 cx.update_editor(|editor, window, cx| {
10400 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10401 {
10402 assert_eq!(
10403 completion_menu_entries(&menu),
10404 &["let"],
10405 "With no digits in the completion query, no digits should be in the word completions"
10406 );
10407 } else {
10408 panic!("expected completion menu to be open");
10409 }
10410 editor.cancel(&Cancel, window, cx);
10411 });
10412
10413 cx.set_state(indoc! {"3ˇ
10414 0_usize
10415 let
10416 3
10417 33.35f32
10418 "});
10419 cx.update_editor(|editor, window, cx| {
10420 editor.show_completions(&ShowCompletions::default(), window, cx);
10421 });
10422 cx.executor().run_until_parked();
10423 cx.condition(|editor, _| editor.context_menu_visible())
10424 .await;
10425 cx.update_editor(|editor, _, _| {
10426 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10427 {
10428 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
10429 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
10430 } else {
10431 panic!("expected completion menu to be open");
10432 }
10433 });
10434}
10435
10436fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
10437 let position = || lsp::Position {
10438 line: params.text_document_position.position.line,
10439 character: params.text_document_position.position.character,
10440 };
10441 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
10442 range: lsp::Range {
10443 start: position(),
10444 end: position(),
10445 },
10446 new_text: text.to_string(),
10447 }))
10448}
10449
10450#[gpui::test]
10451async fn test_multiline_completion(cx: &mut TestAppContext) {
10452 init_test(cx, |_| {});
10453
10454 let fs = FakeFs::new(cx.executor());
10455 fs.insert_tree(
10456 path!("/a"),
10457 json!({
10458 "main.ts": "a",
10459 }),
10460 )
10461 .await;
10462
10463 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
10464 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10465 let typescript_language = Arc::new(Language::new(
10466 LanguageConfig {
10467 name: "TypeScript".into(),
10468 matcher: LanguageMatcher {
10469 path_suffixes: vec!["ts".to_string()],
10470 ..LanguageMatcher::default()
10471 },
10472 line_comments: vec!["// ".into()],
10473 ..LanguageConfig::default()
10474 },
10475 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
10476 ));
10477 language_registry.add(typescript_language.clone());
10478 let mut fake_servers = language_registry.register_fake_lsp(
10479 "TypeScript",
10480 FakeLspAdapter {
10481 capabilities: lsp::ServerCapabilities {
10482 completion_provider: Some(lsp::CompletionOptions {
10483 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10484 ..lsp::CompletionOptions::default()
10485 }),
10486 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10487 ..lsp::ServerCapabilities::default()
10488 },
10489 // Emulate vtsls label generation
10490 label_for_completion: Some(Box::new(|item, _| {
10491 let text = if let Some(description) = item
10492 .label_details
10493 .as_ref()
10494 .and_then(|label_details| label_details.description.as_ref())
10495 {
10496 format!("{} {}", item.label, description)
10497 } else if let Some(detail) = &item.detail {
10498 format!("{} {}", item.label, detail)
10499 } else {
10500 item.label.clone()
10501 };
10502 let len = text.len();
10503 Some(language::CodeLabel {
10504 text,
10505 runs: Vec::new(),
10506 filter_range: 0..len,
10507 })
10508 })),
10509 ..FakeLspAdapter::default()
10510 },
10511 );
10512 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10513 let cx = &mut VisualTestContext::from_window(*workspace, cx);
10514 let worktree_id = workspace
10515 .update(cx, |workspace, _window, cx| {
10516 workspace.project().update(cx, |project, cx| {
10517 project.worktrees(cx).next().unwrap().read(cx).id()
10518 })
10519 })
10520 .unwrap();
10521 let _buffer = project
10522 .update(cx, |project, cx| {
10523 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
10524 })
10525 .await
10526 .unwrap();
10527 let editor = workspace
10528 .update(cx, |workspace, window, cx| {
10529 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
10530 })
10531 .unwrap()
10532 .await
10533 .unwrap()
10534 .downcast::<Editor>()
10535 .unwrap();
10536 let fake_server = fake_servers.next().await.unwrap();
10537
10538 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
10539 let multiline_label_2 = "a\nb\nc\n";
10540 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
10541 let multiline_description = "d\ne\nf\n";
10542 let multiline_detail_2 = "g\nh\ni\n";
10543
10544 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
10545 move |params, _| async move {
10546 Ok(Some(lsp::CompletionResponse::Array(vec![
10547 lsp::CompletionItem {
10548 label: multiline_label.to_string(),
10549 text_edit: gen_text_edit(¶ms, "new_text_1"),
10550 ..lsp::CompletionItem::default()
10551 },
10552 lsp::CompletionItem {
10553 label: "single line label 1".to_string(),
10554 detail: Some(multiline_detail.to_string()),
10555 text_edit: gen_text_edit(¶ms, "new_text_2"),
10556 ..lsp::CompletionItem::default()
10557 },
10558 lsp::CompletionItem {
10559 label: "single line label 2".to_string(),
10560 label_details: Some(lsp::CompletionItemLabelDetails {
10561 description: Some(multiline_description.to_string()),
10562 detail: None,
10563 }),
10564 text_edit: gen_text_edit(¶ms, "new_text_2"),
10565 ..lsp::CompletionItem::default()
10566 },
10567 lsp::CompletionItem {
10568 label: multiline_label_2.to_string(),
10569 detail: Some(multiline_detail_2.to_string()),
10570 text_edit: gen_text_edit(¶ms, "new_text_3"),
10571 ..lsp::CompletionItem::default()
10572 },
10573 lsp::CompletionItem {
10574 label: "Label with many spaces and \t but without newlines".to_string(),
10575 detail: Some(
10576 "Details with many spaces and \t but without newlines".to_string(),
10577 ),
10578 text_edit: gen_text_edit(¶ms, "new_text_4"),
10579 ..lsp::CompletionItem::default()
10580 },
10581 ])))
10582 },
10583 );
10584
10585 editor.update_in(cx, |editor, window, cx| {
10586 cx.focus_self(window);
10587 editor.move_to_end(&MoveToEnd, window, cx);
10588 editor.handle_input(".", window, cx);
10589 });
10590 cx.run_until_parked();
10591 completion_handle.next().await.unwrap();
10592
10593 editor.update(cx, |editor, _| {
10594 assert!(editor.context_menu_visible());
10595 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10596 {
10597 let completion_labels = menu
10598 .completions
10599 .borrow()
10600 .iter()
10601 .map(|c| c.label.text.clone())
10602 .collect::<Vec<_>>();
10603 assert_eq!(
10604 completion_labels,
10605 &[
10606 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
10607 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
10608 "single line label 2 d e f ",
10609 "a b c g h i ",
10610 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
10611 ],
10612 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
10613 );
10614
10615 for completion in menu
10616 .completions
10617 .borrow()
10618 .iter() {
10619 assert_eq!(
10620 completion.label.filter_range,
10621 0..completion.label.text.len(),
10622 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
10623 );
10624 }
10625 } else {
10626 panic!("expected completion menu to be open");
10627 }
10628 });
10629}
10630
10631#[gpui::test]
10632async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
10633 init_test(cx, |_| {});
10634 let mut cx = EditorLspTestContext::new_rust(
10635 lsp::ServerCapabilities {
10636 completion_provider: Some(lsp::CompletionOptions {
10637 trigger_characters: Some(vec![".".to_string()]),
10638 ..Default::default()
10639 }),
10640 ..Default::default()
10641 },
10642 cx,
10643 )
10644 .await;
10645 cx.lsp
10646 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
10647 Ok(Some(lsp::CompletionResponse::Array(vec![
10648 lsp::CompletionItem {
10649 label: "first".into(),
10650 ..Default::default()
10651 },
10652 lsp::CompletionItem {
10653 label: "last".into(),
10654 ..Default::default()
10655 },
10656 ])))
10657 });
10658 cx.set_state("variableˇ");
10659 cx.simulate_keystroke(".");
10660 cx.executor().run_until_parked();
10661
10662 cx.update_editor(|editor, _, _| {
10663 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10664 {
10665 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
10666 } else {
10667 panic!("expected completion menu to be open");
10668 }
10669 });
10670
10671 cx.update_editor(|editor, window, cx| {
10672 editor.move_page_down(&MovePageDown::default(), window, cx);
10673 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10674 {
10675 assert!(
10676 menu.selected_item == 1,
10677 "expected PageDown to select the last item from the context menu"
10678 );
10679 } else {
10680 panic!("expected completion menu to stay open after PageDown");
10681 }
10682 });
10683
10684 cx.update_editor(|editor, window, cx| {
10685 editor.move_page_up(&MovePageUp::default(), window, cx);
10686 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10687 {
10688 assert!(
10689 menu.selected_item == 0,
10690 "expected PageUp to select the first item from the context menu"
10691 );
10692 } else {
10693 panic!("expected completion menu to stay open after PageUp");
10694 }
10695 });
10696}
10697
10698#[gpui::test]
10699async fn test_completion_sort(cx: &mut TestAppContext) {
10700 init_test(cx, |_| {});
10701 let mut cx = EditorLspTestContext::new_rust(
10702 lsp::ServerCapabilities {
10703 completion_provider: Some(lsp::CompletionOptions {
10704 trigger_characters: Some(vec![".".to_string()]),
10705 ..Default::default()
10706 }),
10707 ..Default::default()
10708 },
10709 cx,
10710 )
10711 .await;
10712 cx.lsp
10713 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
10714 Ok(Some(lsp::CompletionResponse::Array(vec![
10715 lsp::CompletionItem {
10716 label: "Range".into(),
10717 sort_text: Some("a".into()),
10718 ..Default::default()
10719 },
10720 lsp::CompletionItem {
10721 label: "r".into(),
10722 sort_text: Some("b".into()),
10723 ..Default::default()
10724 },
10725 lsp::CompletionItem {
10726 label: "ret".into(),
10727 sort_text: Some("c".into()),
10728 ..Default::default()
10729 },
10730 lsp::CompletionItem {
10731 label: "return".into(),
10732 sort_text: Some("d".into()),
10733 ..Default::default()
10734 },
10735 lsp::CompletionItem {
10736 label: "slice".into(),
10737 sort_text: Some("d".into()),
10738 ..Default::default()
10739 },
10740 ])))
10741 });
10742 cx.set_state("rˇ");
10743 cx.executor().run_until_parked();
10744 cx.update_editor(|editor, window, cx| {
10745 editor.show_completions(
10746 &ShowCompletions {
10747 trigger: Some("r".into()),
10748 },
10749 window,
10750 cx,
10751 );
10752 });
10753 cx.executor().run_until_parked();
10754
10755 cx.update_editor(|editor, _, _| {
10756 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10757 {
10758 assert_eq!(
10759 completion_menu_entries(&menu),
10760 &["r", "ret", "Range", "return"]
10761 );
10762 } else {
10763 panic!("expected completion menu to be open");
10764 }
10765 });
10766}
10767
10768#[gpui::test]
10769async fn test_as_is_completions(cx: &mut TestAppContext) {
10770 init_test(cx, |_| {});
10771 let mut cx = EditorLspTestContext::new_rust(
10772 lsp::ServerCapabilities {
10773 completion_provider: Some(lsp::CompletionOptions {
10774 ..Default::default()
10775 }),
10776 ..Default::default()
10777 },
10778 cx,
10779 )
10780 .await;
10781 cx.lsp
10782 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
10783 Ok(Some(lsp::CompletionResponse::Array(vec![
10784 lsp::CompletionItem {
10785 label: "unsafe".into(),
10786 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
10787 range: lsp::Range {
10788 start: lsp::Position {
10789 line: 1,
10790 character: 2,
10791 },
10792 end: lsp::Position {
10793 line: 1,
10794 character: 3,
10795 },
10796 },
10797 new_text: "unsafe".to_string(),
10798 })),
10799 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
10800 ..Default::default()
10801 },
10802 ])))
10803 });
10804 cx.set_state("fn a() {}\n nˇ");
10805 cx.executor().run_until_parked();
10806 cx.update_editor(|editor, window, cx| {
10807 editor.show_completions(
10808 &ShowCompletions {
10809 trigger: Some("\n".into()),
10810 },
10811 window,
10812 cx,
10813 );
10814 });
10815 cx.executor().run_until_parked();
10816
10817 cx.update_editor(|editor, window, cx| {
10818 editor.confirm_completion(&Default::default(), window, cx)
10819 });
10820 cx.executor().run_until_parked();
10821 cx.assert_editor_state("fn a() {}\n unsafeˇ");
10822}
10823
10824#[gpui::test]
10825async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
10826 init_test(cx, |_| {});
10827
10828 let mut cx = EditorLspTestContext::new_rust(
10829 lsp::ServerCapabilities {
10830 completion_provider: Some(lsp::CompletionOptions {
10831 trigger_characters: Some(vec![".".to_string()]),
10832 resolve_provider: Some(true),
10833 ..Default::default()
10834 }),
10835 ..Default::default()
10836 },
10837 cx,
10838 )
10839 .await;
10840
10841 cx.set_state("fn main() { let a = 2ˇ; }");
10842 cx.simulate_keystroke(".");
10843 let completion_item = lsp::CompletionItem {
10844 label: "Some".into(),
10845 kind: Some(lsp::CompletionItemKind::SNIPPET),
10846 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
10847 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
10848 kind: lsp::MarkupKind::Markdown,
10849 value: "```rust\nSome(2)\n```".to_string(),
10850 })),
10851 deprecated: Some(false),
10852 sort_text: Some("Some".to_string()),
10853 filter_text: Some("Some".to_string()),
10854 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
10855 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
10856 range: lsp::Range {
10857 start: lsp::Position {
10858 line: 0,
10859 character: 22,
10860 },
10861 end: lsp::Position {
10862 line: 0,
10863 character: 22,
10864 },
10865 },
10866 new_text: "Some(2)".to_string(),
10867 })),
10868 additional_text_edits: Some(vec![lsp::TextEdit {
10869 range: lsp::Range {
10870 start: lsp::Position {
10871 line: 0,
10872 character: 20,
10873 },
10874 end: lsp::Position {
10875 line: 0,
10876 character: 22,
10877 },
10878 },
10879 new_text: "".to_string(),
10880 }]),
10881 ..Default::default()
10882 };
10883
10884 let closure_completion_item = completion_item.clone();
10885 let counter = Arc::new(AtomicUsize::new(0));
10886 let counter_clone = counter.clone();
10887 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
10888 let task_completion_item = closure_completion_item.clone();
10889 counter_clone.fetch_add(1, atomic::Ordering::Release);
10890 async move {
10891 Ok(Some(lsp::CompletionResponse::Array(vec![
10892 task_completion_item,
10893 ])))
10894 }
10895 });
10896
10897 cx.condition(|editor, _| editor.context_menu_visible())
10898 .await;
10899 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
10900 assert!(request.next().await.is_some());
10901 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10902
10903 cx.simulate_keystrokes("S o m");
10904 cx.condition(|editor, _| editor.context_menu_visible())
10905 .await;
10906 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
10907 assert!(request.next().await.is_some());
10908 assert!(request.next().await.is_some());
10909 assert!(request.next().await.is_some());
10910 request.close();
10911 assert!(request.next().await.is_none());
10912 assert_eq!(
10913 counter.load(atomic::Ordering::Acquire),
10914 4,
10915 "With the completions menu open, only one LSP request should happen per input"
10916 );
10917}
10918
10919#[gpui::test]
10920async fn test_toggle_comment(cx: &mut TestAppContext) {
10921 init_test(cx, |_| {});
10922 let mut cx = EditorTestContext::new(cx).await;
10923 let language = Arc::new(Language::new(
10924 LanguageConfig {
10925 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
10926 ..Default::default()
10927 },
10928 Some(tree_sitter_rust::LANGUAGE.into()),
10929 ));
10930 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
10931
10932 // If multiple selections intersect a line, the line is only toggled once.
10933 cx.set_state(indoc! {"
10934 fn a() {
10935 «//b();
10936 ˇ»// «c();
10937 //ˇ» d();
10938 }
10939 "});
10940
10941 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10942
10943 cx.assert_editor_state(indoc! {"
10944 fn a() {
10945 «b();
10946 c();
10947 ˇ» d();
10948 }
10949 "});
10950
10951 // The comment prefix is inserted at the same column for every line in a
10952 // selection.
10953 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10954
10955 cx.assert_editor_state(indoc! {"
10956 fn a() {
10957 // «b();
10958 // c();
10959 ˇ»// d();
10960 }
10961 "});
10962
10963 // If a selection ends at the beginning of a line, that line is not toggled.
10964 cx.set_selections_state(indoc! {"
10965 fn a() {
10966 // b();
10967 «// c();
10968 ˇ» // d();
10969 }
10970 "});
10971
10972 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10973
10974 cx.assert_editor_state(indoc! {"
10975 fn a() {
10976 // b();
10977 «c();
10978 ˇ» // d();
10979 }
10980 "});
10981
10982 // If a selection span a single line and is empty, the line is toggled.
10983 cx.set_state(indoc! {"
10984 fn a() {
10985 a();
10986 b();
10987 ˇ
10988 }
10989 "});
10990
10991 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10992
10993 cx.assert_editor_state(indoc! {"
10994 fn a() {
10995 a();
10996 b();
10997 //•ˇ
10998 }
10999 "});
11000
11001 // If a selection span multiple lines, empty lines are not toggled.
11002 cx.set_state(indoc! {"
11003 fn a() {
11004 «a();
11005
11006 c();ˇ»
11007 }
11008 "});
11009
11010 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11011
11012 cx.assert_editor_state(indoc! {"
11013 fn a() {
11014 // «a();
11015
11016 // c();ˇ»
11017 }
11018 "});
11019
11020 // If a selection includes multiple comment prefixes, all lines are uncommented.
11021 cx.set_state(indoc! {"
11022 fn a() {
11023 «// a();
11024 /// b();
11025 //! c();ˇ»
11026 }
11027 "});
11028
11029 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11030
11031 cx.assert_editor_state(indoc! {"
11032 fn a() {
11033 «a();
11034 b();
11035 c();ˇ»
11036 }
11037 "});
11038}
11039
11040#[gpui::test]
11041async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
11042 init_test(cx, |_| {});
11043 let mut cx = EditorTestContext::new(cx).await;
11044 let language = Arc::new(Language::new(
11045 LanguageConfig {
11046 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
11047 ..Default::default()
11048 },
11049 Some(tree_sitter_rust::LANGUAGE.into()),
11050 ));
11051 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
11052
11053 let toggle_comments = &ToggleComments {
11054 advance_downwards: false,
11055 ignore_indent: true,
11056 };
11057
11058 // If multiple selections intersect a line, the line is only toggled once.
11059 cx.set_state(indoc! {"
11060 fn a() {
11061 // «b();
11062 // c();
11063 // ˇ» d();
11064 }
11065 "});
11066
11067 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11068
11069 cx.assert_editor_state(indoc! {"
11070 fn a() {
11071 «b();
11072 c();
11073 ˇ» d();
11074 }
11075 "});
11076
11077 // The comment prefix is inserted at the beginning of each line
11078 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11079
11080 cx.assert_editor_state(indoc! {"
11081 fn a() {
11082 // «b();
11083 // c();
11084 // ˇ» d();
11085 }
11086 "});
11087
11088 // If a selection ends at the beginning of a line, that line is not toggled.
11089 cx.set_selections_state(indoc! {"
11090 fn a() {
11091 // b();
11092 // «c();
11093 ˇ»// d();
11094 }
11095 "});
11096
11097 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11098
11099 cx.assert_editor_state(indoc! {"
11100 fn a() {
11101 // b();
11102 «c();
11103 ˇ»// d();
11104 }
11105 "});
11106
11107 // If a selection span a single line and is empty, the line is toggled.
11108 cx.set_state(indoc! {"
11109 fn a() {
11110 a();
11111 b();
11112 ˇ
11113 }
11114 "});
11115
11116 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11117
11118 cx.assert_editor_state(indoc! {"
11119 fn a() {
11120 a();
11121 b();
11122 //ˇ
11123 }
11124 "});
11125
11126 // If a selection span multiple lines, empty lines are not toggled.
11127 cx.set_state(indoc! {"
11128 fn a() {
11129 «a();
11130
11131 c();ˇ»
11132 }
11133 "});
11134
11135 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11136
11137 cx.assert_editor_state(indoc! {"
11138 fn a() {
11139 // «a();
11140
11141 // c();ˇ»
11142 }
11143 "});
11144
11145 // If a selection includes multiple comment prefixes, all lines are uncommented.
11146 cx.set_state(indoc! {"
11147 fn a() {
11148 // «a();
11149 /// b();
11150 //! c();ˇ»
11151 }
11152 "});
11153
11154 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11155
11156 cx.assert_editor_state(indoc! {"
11157 fn a() {
11158 «a();
11159 b();
11160 c();ˇ»
11161 }
11162 "});
11163}
11164
11165#[gpui::test]
11166async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
11167 init_test(cx, |_| {});
11168
11169 let language = Arc::new(Language::new(
11170 LanguageConfig {
11171 line_comments: vec!["// ".into()],
11172 ..Default::default()
11173 },
11174 Some(tree_sitter_rust::LANGUAGE.into()),
11175 ));
11176
11177 let mut cx = EditorTestContext::new(cx).await;
11178
11179 cx.language_registry().add(language.clone());
11180 cx.update_buffer(|buffer, cx| {
11181 buffer.set_language(Some(language), cx);
11182 });
11183
11184 let toggle_comments = &ToggleComments {
11185 advance_downwards: true,
11186 ignore_indent: false,
11187 };
11188
11189 // Single cursor on one line -> advance
11190 // Cursor moves horizontally 3 characters as well on non-blank line
11191 cx.set_state(indoc!(
11192 "fn a() {
11193 ˇdog();
11194 cat();
11195 }"
11196 ));
11197 cx.update_editor(|editor, window, cx| {
11198 editor.toggle_comments(toggle_comments, window, cx);
11199 });
11200 cx.assert_editor_state(indoc!(
11201 "fn a() {
11202 // dog();
11203 catˇ();
11204 }"
11205 ));
11206
11207 // Single selection on one line -> don't advance
11208 cx.set_state(indoc!(
11209 "fn a() {
11210 «dog()ˇ»;
11211 cat();
11212 }"
11213 ));
11214 cx.update_editor(|editor, window, cx| {
11215 editor.toggle_comments(toggle_comments, window, cx);
11216 });
11217 cx.assert_editor_state(indoc!(
11218 "fn a() {
11219 // «dog()ˇ»;
11220 cat();
11221 }"
11222 ));
11223
11224 // Multiple cursors on one line -> advance
11225 cx.set_state(indoc!(
11226 "fn a() {
11227 ˇdˇog();
11228 cat();
11229 }"
11230 ));
11231 cx.update_editor(|editor, window, cx| {
11232 editor.toggle_comments(toggle_comments, window, cx);
11233 });
11234 cx.assert_editor_state(indoc!(
11235 "fn a() {
11236 // dog();
11237 catˇ(ˇ);
11238 }"
11239 ));
11240
11241 // Multiple cursors on one line, with selection -> don't advance
11242 cx.set_state(indoc!(
11243 "fn a() {
11244 ˇdˇog«()ˇ»;
11245 cat();
11246 }"
11247 ));
11248 cx.update_editor(|editor, window, cx| {
11249 editor.toggle_comments(toggle_comments, window, cx);
11250 });
11251 cx.assert_editor_state(indoc!(
11252 "fn a() {
11253 // ˇdˇog«()ˇ»;
11254 cat();
11255 }"
11256 ));
11257
11258 // Single cursor on one line -> advance
11259 // Cursor moves to column 0 on blank line
11260 cx.set_state(indoc!(
11261 "fn a() {
11262 ˇdog();
11263
11264 cat();
11265 }"
11266 ));
11267 cx.update_editor(|editor, window, cx| {
11268 editor.toggle_comments(toggle_comments, window, cx);
11269 });
11270 cx.assert_editor_state(indoc!(
11271 "fn a() {
11272 // dog();
11273 ˇ
11274 cat();
11275 }"
11276 ));
11277
11278 // Single cursor on one line -> advance
11279 // Cursor starts and ends at column 0
11280 cx.set_state(indoc!(
11281 "fn a() {
11282 ˇ dog();
11283 cat();
11284 }"
11285 ));
11286 cx.update_editor(|editor, window, cx| {
11287 editor.toggle_comments(toggle_comments, window, cx);
11288 });
11289 cx.assert_editor_state(indoc!(
11290 "fn a() {
11291 // dog();
11292 ˇ cat();
11293 }"
11294 ));
11295}
11296
11297#[gpui::test]
11298async fn test_toggle_block_comment(cx: &mut TestAppContext) {
11299 init_test(cx, |_| {});
11300
11301 let mut cx = EditorTestContext::new(cx).await;
11302
11303 let html_language = Arc::new(
11304 Language::new(
11305 LanguageConfig {
11306 name: "HTML".into(),
11307 block_comment: Some(("<!-- ".into(), " -->".into())),
11308 ..Default::default()
11309 },
11310 Some(tree_sitter_html::LANGUAGE.into()),
11311 )
11312 .with_injection_query(
11313 r#"
11314 (script_element
11315 (raw_text) @injection.content
11316 (#set! injection.language "javascript"))
11317 "#,
11318 )
11319 .unwrap(),
11320 );
11321
11322 let javascript_language = Arc::new(Language::new(
11323 LanguageConfig {
11324 name: "JavaScript".into(),
11325 line_comments: vec!["// ".into()],
11326 ..Default::default()
11327 },
11328 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
11329 ));
11330
11331 cx.language_registry().add(html_language.clone());
11332 cx.language_registry().add(javascript_language.clone());
11333 cx.update_buffer(|buffer, cx| {
11334 buffer.set_language(Some(html_language), cx);
11335 });
11336
11337 // Toggle comments for empty selections
11338 cx.set_state(
11339 &r#"
11340 <p>A</p>ˇ
11341 <p>B</p>ˇ
11342 <p>C</p>ˇ
11343 "#
11344 .unindent(),
11345 );
11346 cx.update_editor(|editor, window, cx| {
11347 editor.toggle_comments(&ToggleComments::default(), window, cx)
11348 });
11349 cx.assert_editor_state(
11350 &r#"
11351 <!-- <p>A</p>ˇ -->
11352 <!-- <p>B</p>ˇ -->
11353 <!-- <p>C</p>ˇ -->
11354 "#
11355 .unindent(),
11356 );
11357 cx.update_editor(|editor, window, cx| {
11358 editor.toggle_comments(&ToggleComments::default(), window, cx)
11359 });
11360 cx.assert_editor_state(
11361 &r#"
11362 <p>A</p>ˇ
11363 <p>B</p>ˇ
11364 <p>C</p>ˇ
11365 "#
11366 .unindent(),
11367 );
11368
11369 // Toggle comments for mixture of empty and non-empty selections, where
11370 // multiple selections occupy a given line.
11371 cx.set_state(
11372 &r#"
11373 <p>A«</p>
11374 <p>ˇ»B</p>ˇ
11375 <p>C«</p>
11376 <p>ˇ»D</p>ˇ
11377 "#
11378 .unindent(),
11379 );
11380
11381 cx.update_editor(|editor, window, cx| {
11382 editor.toggle_comments(&ToggleComments::default(), window, cx)
11383 });
11384 cx.assert_editor_state(
11385 &r#"
11386 <!-- <p>A«</p>
11387 <p>ˇ»B</p>ˇ -->
11388 <!-- <p>C«</p>
11389 <p>ˇ»D</p>ˇ -->
11390 "#
11391 .unindent(),
11392 );
11393 cx.update_editor(|editor, window, cx| {
11394 editor.toggle_comments(&ToggleComments::default(), window, cx)
11395 });
11396 cx.assert_editor_state(
11397 &r#"
11398 <p>A«</p>
11399 <p>ˇ»B</p>ˇ
11400 <p>C«</p>
11401 <p>ˇ»D</p>ˇ
11402 "#
11403 .unindent(),
11404 );
11405
11406 // Toggle comments when different languages are active for different
11407 // selections.
11408 cx.set_state(
11409 &r#"
11410 ˇ<script>
11411 ˇvar x = new Y();
11412 ˇ</script>
11413 "#
11414 .unindent(),
11415 );
11416 cx.executor().run_until_parked();
11417 cx.update_editor(|editor, window, cx| {
11418 editor.toggle_comments(&ToggleComments::default(), window, cx)
11419 });
11420 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
11421 // Uncommenting and commenting from this position brings in even more wrong artifacts.
11422 cx.assert_editor_state(
11423 &r#"
11424 <!-- ˇ<script> -->
11425 // ˇvar x = new Y();
11426 <!-- ˇ</script> -->
11427 "#
11428 .unindent(),
11429 );
11430}
11431
11432#[gpui::test]
11433fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
11434 init_test(cx, |_| {});
11435
11436 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
11437 let multibuffer = cx.new(|cx| {
11438 let mut multibuffer = MultiBuffer::new(ReadWrite);
11439 multibuffer.push_excerpts(
11440 buffer.clone(),
11441 [
11442 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
11443 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
11444 ],
11445 cx,
11446 );
11447 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
11448 multibuffer
11449 });
11450
11451 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
11452 editor.update_in(cx, |editor, window, cx| {
11453 assert_eq!(editor.text(cx), "aaaa\nbbbb");
11454 editor.change_selections(None, window, cx, |s| {
11455 s.select_ranges([
11456 Point::new(0, 0)..Point::new(0, 0),
11457 Point::new(1, 0)..Point::new(1, 0),
11458 ])
11459 });
11460
11461 editor.handle_input("X", window, cx);
11462 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
11463 assert_eq!(
11464 editor.selections.ranges(cx),
11465 [
11466 Point::new(0, 1)..Point::new(0, 1),
11467 Point::new(1, 1)..Point::new(1, 1),
11468 ]
11469 );
11470
11471 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
11472 editor.change_selections(None, window, cx, |s| {
11473 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
11474 });
11475 editor.backspace(&Default::default(), window, cx);
11476 assert_eq!(editor.text(cx), "Xa\nbbb");
11477 assert_eq!(
11478 editor.selections.ranges(cx),
11479 [Point::new(1, 0)..Point::new(1, 0)]
11480 );
11481
11482 editor.change_selections(None, window, cx, |s| {
11483 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
11484 });
11485 editor.backspace(&Default::default(), window, cx);
11486 assert_eq!(editor.text(cx), "X\nbb");
11487 assert_eq!(
11488 editor.selections.ranges(cx),
11489 [Point::new(0, 1)..Point::new(0, 1)]
11490 );
11491 });
11492}
11493
11494#[gpui::test]
11495fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
11496 init_test(cx, |_| {});
11497
11498 let markers = vec![('[', ']').into(), ('(', ')').into()];
11499 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
11500 indoc! {"
11501 [aaaa
11502 (bbbb]
11503 cccc)",
11504 },
11505 markers.clone(),
11506 );
11507 let excerpt_ranges = markers.into_iter().map(|marker| {
11508 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
11509 ExcerptRange::new(context.clone())
11510 });
11511 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
11512 let multibuffer = cx.new(|cx| {
11513 let mut multibuffer = MultiBuffer::new(ReadWrite);
11514 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
11515 multibuffer
11516 });
11517
11518 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
11519 editor.update_in(cx, |editor, window, cx| {
11520 let (expected_text, selection_ranges) = marked_text_ranges(
11521 indoc! {"
11522 aaaa
11523 bˇbbb
11524 bˇbbˇb
11525 cccc"
11526 },
11527 true,
11528 );
11529 assert_eq!(editor.text(cx), expected_text);
11530 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
11531
11532 editor.handle_input("X", window, cx);
11533
11534 let (expected_text, expected_selections) = marked_text_ranges(
11535 indoc! {"
11536 aaaa
11537 bXˇbbXb
11538 bXˇbbXˇb
11539 cccc"
11540 },
11541 false,
11542 );
11543 assert_eq!(editor.text(cx), expected_text);
11544 assert_eq!(editor.selections.ranges(cx), expected_selections);
11545
11546 editor.newline(&Newline, window, cx);
11547 let (expected_text, expected_selections) = marked_text_ranges(
11548 indoc! {"
11549 aaaa
11550 bX
11551 ˇbbX
11552 b
11553 bX
11554 ˇbbX
11555 ˇb
11556 cccc"
11557 },
11558 false,
11559 );
11560 assert_eq!(editor.text(cx), expected_text);
11561 assert_eq!(editor.selections.ranges(cx), expected_selections);
11562 });
11563}
11564
11565#[gpui::test]
11566fn test_refresh_selections(cx: &mut TestAppContext) {
11567 init_test(cx, |_| {});
11568
11569 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
11570 let mut excerpt1_id = None;
11571 let multibuffer = cx.new(|cx| {
11572 let mut multibuffer = MultiBuffer::new(ReadWrite);
11573 excerpt1_id = multibuffer
11574 .push_excerpts(
11575 buffer.clone(),
11576 [
11577 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
11578 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
11579 ],
11580 cx,
11581 )
11582 .into_iter()
11583 .next();
11584 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
11585 multibuffer
11586 });
11587
11588 let editor = cx.add_window(|window, cx| {
11589 let mut editor = build_editor(multibuffer.clone(), window, cx);
11590 let snapshot = editor.snapshot(window, cx);
11591 editor.change_selections(None, window, cx, |s| {
11592 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
11593 });
11594 editor.begin_selection(
11595 Point::new(2, 1).to_display_point(&snapshot),
11596 true,
11597 1,
11598 window,
11599 cx,
11600 );
11601 assert_eq!(
11602 editor.selections.ranges(cx),
11603 [
11604 Point::new(1, 3)..Point::new(1, 3),
11605 Point::new(2, 1)..Point::new(2, 1),
11606 ]
11607 );
11608 editor
11609 });
11610
11611 // Refreshing selections is a no-op when excerpts haven't changed.
11612 _ = editor.update(cx, |editor, window, cx| {
11613 editor.change_selections(None, window, cx, |s| s.refresh());
11614 assert_eq!(
11615 editor.selections.ranges(cx),
11616 [
11617 Point::new(1, 3)..Point::new(1, 3),
11618 Point::new(2, 1)..Point::new(2, 1),
11619 ]
11620 );
11621 });
11622
11623 multibuffer.update(cx, |multibuffer, cx| {
11624 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
11625 });
11626 _ = editor.update(cx, |editor, window, cx| {
11627 // Removing an excerpt causes the first selection to become degenerate.
11628 assert_eq!(
11629 editor.selections.ranges(cx),
11630 [
11631 Point::new(0, 0)..Point::new(0, 0),
11632 Point::new(0, 1)..Point::new(0, 1)
11633 ]
11634 );
11635
11636 // Refreshing selections will relocate the first selection to the original buffer
11637 // location.
11638 editor.change_selections(None, window, cx, |s| s.refresh());
11639 assert_eq!(
11640 editor.selections.ranges(cx),
11641 [
11642 Point::new(0, 1)..Point::new(0, 1),
11643 Point::new(0, 3)..Point::new(0, 3)
11644 ]
11645 );
11646 assert!(editor.selections.pending_anchor().is_some());
11647 });
11648}
11649
11650#[gpui::test]
11651fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
11652 init_test(cx, |_| {});
11653
11654 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
11655 let mut excerpt1_id = None;
11656 let multibuffer = cx.new(|cx| {
11657 let mut multibuffer = MultiBuffer::new(ReadWrite);
11658 excerpt1_id = multibuffer
11659 .push_excerpts(
11660 buffer.clone(),
11661 [
11662 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
11663 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
11664 ],
11665 cx,
11666 )
11667 .into_iter()
11668 .next();
11669 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
11670 multibuffer
11671 });
11672
11673 let editor = cx.add_window(|window, cx| {
11674 let mut editor = build_editor(multibuffer.clone(), window, cx);
11675 let snapshot = editor.snapshot(window, cx);
11676 editor.begin_selection(
11677 Point::new(1, 3).to_display_point(&snapshot),
11678 false,
11679 1,
11680 window,
11681 cx,
11682 );
11683 assert_eq!(
11684 editor.selections.ranges(cx),
11685 [Point::new(1, 3)..Point::new(1, 3)]
11686 );
11687 editor
11688 });
11689
11690 multibuffer.update(cx, |multibuffer, cx| {
11691 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
11692 });
11693 _ = editor.update(cx, |editor, window, cx| {
11694 assert_eq!(
11695 editor.selections.ranges(cx),
11696 [Point::new(0, 0)..Point::new(0, 0)]
11697 );
11698
11699 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
11700 editor.change_selections(None, window, cx, |s| s.refresh());
11701 assert_eq!(
11702 editor.selections.ranges(cx),
11703 [Point::new(0, 3)..Point::new(0, 3)]
11704 );
11705 assert!(editor.selections.pending_anchor().is_some());
11706 });
11707}
11708
11709#[gpui::test]
11710async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
11711 init_test(cx, |_| {});
11712
11713 let language = Arc::new(
11714 Language::new(
11715 LanguageConfig {
11716 brackets: BracketPairConfig {
11717 pairs: vec![
11718 BracketPair {
11719 start: "{".to_string(),
11720 end: "}".to_string(),
11721 close: true,
11722 surround: true,
11723 newline: true,
11724 },
11725 BracketPair {
11726 start: "/* ".to_string(),
11727 end: " */".to_string(),
11728 close: true,
11729 surround: true,
11730 newline: true,
11731 },
11732 ],
11733 ..Default::default()
11734 },
11735 ..Default::default()
11736 },
11737 Some(tree_sitter_rust::LANGUAGE.into()),
11738 )
11739 .with_indents_query("")
11740 .unwrap(),
11741 );
11742
11743 let text = concat!(
11744 "{ }\n", //
11745 " x\n", //
11746 " /* */\n", //
11747 "x\n", //
11748 "{{} }\n", //
11749 );
11750
11751 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
11752 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11753 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11754 editor
11755 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11756 .await;
11757
11758 editor.update_in(cx, |editor, window, cx| {
11759 editor.change_selections(None, window, cx, |s| {
11760 s.select_display_ranges([
11761 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
11762 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
11763 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
11764 ])
11765 });
11766 editor.newline(&Newline, window, cx);
11767
11768 assert_eq!(
11769 editor.buffer().read(cx).read(cx).text(),
11770 concat!(
11771 "{ \n", // Suppress rustfmt
11772 "\n", //
11773 "}\n", //
11774 " x\n", //
11775 " /* \n", //
11776 " \n", //
11777 " */\n", //
11778 "x\n", //
11779 "{{} \n", //
11780 "}\n", //
11781 )
11782 );
11783 });
11784}
11785
11786#[gpui::test]
11787fn test_highlighted_ranges(cx: &mut TestAppContext) {
11788 init_test(cx, |_| {});
11789
11790 let editor = cx.add_window(|window, cx| {
11791 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
11792 build_editor(buffer.clone(), window, cx)
11793 });
11794
11795 _ = editor.update(cx, |editor, window, cx| {
11796 struct Type1;
11797 struct Type2;
11798
11799 let buffer = editor.buffer.read(cx).snapshot(cx);
11800
11801 let anchor_range =
11802 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
11803
11804 editor.highlight_background::<Type1>(
11805 &[
11806 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
11807 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
11808 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
11809 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
11810 ],
11811 |_| Hsla::red(),
11812 cx,
11813 );
11814 editor.highlight_background::<Type2>(
11815 &[
11816 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
11817 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
11818 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
11819 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
11820 ],
11821 |_| Hsla::green(),
11822 cx,
11823 );
11824
11825 let snapshot = editor.snapshot(window, cx);
11826 let mut highlighted_ranges = editor.background_highlights_in_range(
11827 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
11828 &snapshot,
11829 cx.theme().colors(),
11830 );
11831 // Enforce a consistent ordering based on color without relying on the ordering of the
11832 // highlight's `TypeId` which is non-executor.
11833 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
11834 assert_eq!(
11835 highlighted_ranges,
11836 &[
11837 (
11838 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
11839 Hsla::red(),
11840 ),
11841 (
11842 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
11843 Hsla::red(),
11844 ),
11845 (
11846 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
11847 Hsla::green(),
11848 ),
11849 (
11850 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
11851 Hsla::green(),
11852 ),
11853 ]
11854 );
11855 assert_eq!(
11856 editor.background_highlights_in_range(
11857 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
11858 &snapshot,
11859 cx.theme().colors(),
11860 ),
11861 &[(
11862 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
11863 Hsla::red(),
11864 )]
11865 );
11866 });
11867}
11868
11869#[gpui::test]
11870async fn test_following(cx: &mut TestAppContext) {
11871 init_test(cx, |_| {});
11872
11873 let fs = FakeFs::new(cx.executor());
11874 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
11875
11876 let buffer = project.update(cx, |project, cx| {
11877 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
11878 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
11879 });
11880 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
11881 let follower = cx.update(|cx| {
11882 cx.open_window(
11883 WindowOptions {
11884 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
11885 gpui::Point::new(px(0.), px(0.)),
11886 gpui::Point::new(px(10.), px(80.)),
11887 ))),
11888 ..Default::default()
11889 },
11890 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
11891 )
11892 .unwrap()
11893 });
11894
11895 let is_still_following = Rc::new(RefCell::new(true));
11896 let follower_edit_event_count = Rc::new(RefCell::new(0));
11897 let pending_update = Rc::new(RefCell::new(None));
11898 let leader_entity = leader.root(cx).unwrap();
11899 let follower_entity = follower.root(cx).unwrap();
11900 _ = follower.update(cx, {
11901 let update = pending_update.clone();
11902 let is_still_following = is_still_following.clone();
11903 let follower_edit_event_count = follower_edit_event_count.clone();
11904 |_, window, cx| {
11905 cx.subscribe_in(
11906 &leader_entity,
11907 window,
11908 move |_, leader, event, window, cx| {
11909 leader.read(cx).add_event_to_update_proto(
11910 event,
11911 &mut update.borrow_mut(),
11912 window,
11913 cx,
11914 );
11915 },
11916 )
11917 .detach();
11918
11919 cx.subscribe_in(
11920 &follower_entity,
11921 window,
11922 move |_, _, event: &EditorEvent, _window, _cx| {
11923 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
11924 *is_still_following.borrow_mut() = false;
11925 }
11926
11927 if let EditorEvent::BufferEdited = event {
11928 *follower_edit_event_count.borrow_mut() += 1;
11929 }
11930 },
11931 )
11932 .detach();
11933 }
11934 });
11935
11936 // Update the selections only
11937 _ = leader.update(cx, |leader, window, cx| {
11938 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
11939 });
11940 follower
11941 .update(cx, |follower, window, cx| {
11942 follower.apply_update_proto(
11943 &project,
11944 pending_update.borrow_mut().take().unwrap(),
11945 window,
11946 cx,
11947 )
11948 })
11949 .unwrap()
11950 .await
11951 .unwrap();
11952 _ = follower.update(cx, |follower, _, cx| {
11953 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
11954 });
11955 assert!(*is_still_following.borrow());
11956 assert_eq!(*follower_edit_event_count.borrow(), 0);
11957
11958 // Update the scroll position only
11959 _ = leader.update(cx, |leader, window, cx| {
11960 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
11961 });
11962 follower
11963 .update(cx, |follower, window, cx| {
11964 follower.apply_update_proto(
11965 &project,
11966 pending_update.borrow_mut().take().unwrap(),
11967 window,
11968 cx,
11969 )
11970 })
11971 .unwrap()
11972 .await
11973 .unwrap();
11974 assert_eq!(
11975 follower
11976 .update(cx, |follower, _, cx| follower.scroll_position(cx))
11977 .unwrap(),
11978 gpui::Point::new(1.5, 3.5)
11979 );
11980 assert!(*is_still_following.borrow());
11981 assert_eq!(*follower_edit_event_count.borrow(), 0);
11982
11983 // Update the selections and scroll position. The follower's scroll position is updated
11984 // via autoscroll, not via the leader's exact scroll position.
11985 _ = leader.update(cx, |leader, window, cx| {
11986 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
11987 leader.request_autoscroll(Autoscroll::newest(), cx);
11988 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
11989 });
11990 follower
11991 .update(cx, |follower, window, cx| {
11992 follower.apply_update_proto(
11993 &project,
11994 pending_update.borrow_mut().take().unwrap(),
11995 window,
11996 cx,
11997 )
11998 })
11999 .unwrap()
12000 .await
12001 .unwrap();
12002 _ = follower.update(cx, |follower, _, cx| {
12003 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
12004 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
12005 });
12006 assert!(*is_still_following.borrow());
12007
12008 // Creating a pending selection that precedes another selection
12009 _ = leader.update(cx, |leader, window, cx| {
12010 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
12011 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
12012 });
12013 follower
12014 .update(cx, |follower, window, cx| {
12015 follower.apply_update_proto(
12016 &project,
12017 pending_update.borrow_mut().take().unwrap(),
12018 window,
12019 cx,
12020 )
12021 })
12022 .unwrap()
12023 .await
12024 .unwrap();
12025 _ = follower.update(cx, |follower, _, cx| {
12026 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
12027 });
12028 assert!(*is_still_following.borrow());
12029
12030 // Extend the pending selection so that it surrounds another selection
12031 _ = leader.update(cx, |leader, window, cx| {
12032 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
12033 });
12034 follower
12035 .update(cx, |follower, window, cx| {
12036 follower.apply_update_proto(
12037 &project,
12038 pending_update.borrow_mut().take().unwrap(),
12039 window,
12040 cx,
12041 )
12042 })
12043 .unwrap()
12044 .await
12045 .unwrap();
12046 _ = follower.update(cx, |follower, _, cx| {
12047 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
12048 });
12049
12050 // Scrolling locally breaks the follow
12051 _ = follower.update(cx, |follower, window, cx| {
12052 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
12053 follower.set_scroll_anchor(
12054 ScrollAnchor {
12055 anchor: top_anchor,
12056 offset: gpui::Point::new(0.0, 0.5),
12057 },
12058 window,
12059 cx,
12060 );
12061 });
12062 assert!(!(*is_still_following.borrow()));
12063}
12064
12065#[gpui::test]
12066async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
12067 init_test(cx, |_| {});
12068
12069 let fs = FakeFs::new(cx.executor());
12070 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
12071 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12072 let pane = workspace
12073 .update(cx, |workspace, _, _| workspace.active_pane().clone())
12074 .unwrap();
12075
12076 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12077
12078 let leader = pane.update_in(cx, |_, window, cx| {
12079 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
12080 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
12081 });
12082
12083 // Start following the editor when it has no excerpts.
12084 let mut state_message =
12085 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
12086 let workspace_entity = workspace.root(cx).unwrap();
12087 let follower_1 = cx
12088 .update_window(*workspace.deref(), |_, window, cx| {
12089 Editor::from_state_proto(
12090 workspace_entity,
12091 ViewId {
12092 creator: Default::default(),
12093 id: 0,
12094 },
12095 &mut state_message,
12096 window,
12097 cx,
12098 )
12099 })
12100 .unwrap()
12101 .unwrap()
12102 .await
12103 .unwrap();
12104
12105 let update_message = Rc::new(RefCell::new(None));
12106 follower_1.update_in(cx, {
12107 let update = update_message.clone();
12108 |_, window, cx| {
12109 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
12110 leader.read(cx).add_event_to_update_proto(
12111 event,
12112 &mut update.borrow_mut(),
12113 window,
12114 cx,
12115 );
12116 })
12117 .detach();
12118 }
12119 });
12120
12121 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
12122 (
12123 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
12124 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
12125 )
12126 });
12127
12128 // Insert some excerpts.
12129 leader.update(cx, |leader, cx| {
12130 leader.buffer.update(cx, |multibuffer, cx| {
12131 let excerpt_ids = multibuffer.push_excerpts(
12132 buffer_1.clone(),
12133 [
12134 ExcerptRange::new(1..6),
12135 ExcerptRange::new(12..15),
12136 ExcerptRange::new(0..3),
12137 ],
12138 cx,
12139 );
12140 multibuffer.insert_excerpts_after(
12141 excerpt_ids[0],
12142 buffer_2.clone(),
12143 [ExcerptRange::new(8..12), ExcerptRange::new(0..6)],
12144 cx,
12145 );
12146 });
12147 });
12148
12149 // Apply the update of adding the excerpts.
12150 follower_1
12151 .update_in(cx, |follower, window, cx| {
12152 follower.apply_update_proto(
12153 &project,
12154 update_message.borrow().clone().unwrap(),
12155 window,
12156 cx,
12157 )
12158 })
12159 .await
12160 .unwrap();
12161 assert_eq!(
12162 follower_1.update(cx, |editor, cx| editor.text(cx)),
12163 leader.update(cx, |editor, cx| editor.text(cx))
12164 );
12165 update_message.borrow_mut().take();
12166
12167 // Start following separately after it already has excerpts.
12168 let mut state_message =
12169 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
12170 let workspace_entity = workspace.root(cx).unwrap();
12171 let follower_2 = cx
12172 .update_window(*workspace.deref(), |_, window, cx| {
12173 Editor::from_state_proto(
12174 workspace_entity,
12175 ViewId {
12176 creator: Default::default(),
12177 id: 0,
12178 },
12179 &mut state_message,
12180 window,
12181 cx,
12182 )
12183 })
12184 .unwrap()
12185 .unwrap()
12186 .await
12187 .unwrap();
12188 assert_eq!(
12189 follower_2.update(cx, |editor, cx| editor.text(cx)),
12190 leader.update(cx, |editor, cx| editor.text(cx))
12191 );
12192
12193 // Remove some excerpts.
12194 leader.update(cx, |leader, cx| {
12195 leader.buffer.update(cx, |multibuffer, cx| {
12196 let excerpt_ids = multibuffer.excerpt_ids();
12197 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
12198 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
12199 });
12200 });
12201
12202 // Apply the update of removing the excerpts.
12203 follower_1
12204 .update_in(cx, |follower, window, cx| {
12205 follower.apply_update_proto(
12206 &project,
12207 update_message.borrow().clone().unwrap(),
12208 window,
12209 cx,
12210 )
12211 })
12212 .await
12213 .unwrap();
12214 follower_2
12215 .update_in(cx, |follower, window, cx| {
12216 follower.apply_update_proto(
12217 &project,
12218 update_message.borrow().clone().unwrap(),
12219 window,
12220 cx,
12221 )
12222 })
12223 .await
12224 .unwrap();
12225 update_message.borrow_mut().take();
12226 assert_eq!(
12227 follower_1.update(cx, |editor, cx| editor.text(cx)),
12228 leader.update(cx, |editor, cx| editor.text(cx))
12229 );
12230}
12231
12232#[gpui::test]
12233async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
12234 init_test(cx, |_| {});
12235
12236 let mut cx = EditorTestContext::new(cx).await;
12237 let lsp_store =
12238 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
12239
12240 cx.set_state(indoc! {"
12241 ˇfn func(abc def: i32) -> u32 {
12242 }
12243 "});
12244
12245 cx.update(|_, cx| {
12246 lsp_store.update(cx, |lsp_store, cx| {
12247 lsp_store
12248 .update_diagnostics(
12249 LanguageServerId(0),
12250 lsp::PublishDiagnosticsParams {
12251 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
12252 version: None,
12253 diagnostics: vec![
12254 lsp::Diagnostic {
12255 range: lsp::Range::new(
12256 lsp::Position::new(0, 11),
12257 lsp::Position::new(0, 12),
12258 ),
12259 severity: Some(lsp::DiagnosticSeverity::ERROR),
12260 ..Default::default()
12261 },
12262 lsp::Diagnostic {
12263 range: lsp::Range::new(
12264 lsp::Position::new(0, 12),
12265 lsp::Position::new(0, 15),
12266 ),
12267 severity: Some(lsp::DiagnosticSeverity::ERROR),
12268 ..Default::default()
12269 },
12270 lsp::Diagnostic {
12271 range: lsp::Range::new(
12272 lsp::Position::new(0, 25),
12273 lsp::Position::new(0, 28),
12274 ),
12275 severity: Some(lsp::DiagnosticSeverity::ERROR),
12276 ..Default::default()
12277 },
12278 ],
12279 },
12280 &[],
12281 cx,
12282 )
12283 .unwrap()
12284 });
12285 });
12286
12287 executor.run_until_parked();
12288
12289 cx.update_editor(|editor, window, cx| {
12290 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12291 });
12292
12293 cx.assert_editor_state(indoc! {"
12294 fn func(abc def: i32) -> ˇu32 {
12295 }
12296 "});
12297
12298 cx.update_editor(|editor, window, cx| {
12299 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12300 });
12301
12302 cx.assert_editor_state(indoc! {"
12303 fn func(abc ˇdef: i32) -> u32 {
12304 }
12305 "});
12306
12307 cx.update_editor(|editor, window, cx| {
12308 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12309 });
12310
12311 cx.assert_editor_state(indoc! {"
12312 fn func(abcˇ def: i32) -> u32 {
12313 }
12314 "});
12315
12316 cx.update_editor(|editor, window, cx| {
12317 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12318 });
12319
12320 cx.assert_editor_state(indoc! {"
12321 fn func(abc def: i32) -> ˇu32 {
12322 }
12323 "});
12324}
12325
12326#[gpui::test]
12327async fn cycle_through_same_place_diagnostics(
12328 executor: BackgroundExecutor,
12329 cx: &mut TestAppContext,
12330) {
12331 init_test(cx, |_| {});
12332
12333 let mut cx = EditorTestContext::new(cx).await;
12334 let lsp_store =
12335 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
12336
12337 cx.set_state(indoc! {"
12338 ˇfn func(abc def: i32) -> u32 {
12339 }
12340 "});
12341
12342 cx.update(|_, cx| {
12343 lsp_store.update(cx, |lsp_store, cx| {
12344 lsp_store
12345 .update_diagnostics(
12346 LanguageServerId(0),
12347 lsp::PublishDiagnosticsParams {
12348 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
12349 version: None,
12350 diagnostics: vec![
12351 lsp::Diagnostic {
12352 range: lsp::Range::new(
12353 lsp::Position::new(0, 11),
12354 lsp::Position::new(0, 12),
12355 ),
12356 severity: Some(lsp::DiagnosticSeverity::ERROR),
12357 ..Default::default()
12358 },
12359 lsp::Diagnostic {
12360 range: lsp::Range::new(
12361 lsp::Position::new(0, 12),
12362 lsp::Position::new(0, 15),
12363 ),
12364 severity: Some(lsp::DiagnosticSeverity::ERROR),
12365 ..Default::default()
12366 },
12367 lsp::Diagnostic {
12368 range: lsp::Range::new(
12369 lsp::Position::new(0, 12),
12370 lsp::Position::new(0, 15),
12371 ),
12372 severity: Some(lsp::DiagnosticSeverity::ERROR),
12373 ..Default::default()
12374 },
12375 lsp::Diagnostic {
12376 range: lsp::Range::new(
12377 lsp::Position::new(0, 25),
12378 lsp::Position::new(0, 28),
12379 ),
12380 severity: Some(lsp::DiagnosticSeverity::ERROR),
12381 ..Default::default()
12382 },
12383 ],
12384 },
12385 &[],
12386 cx,
12387 )
12388 .unwrap()
12389 });
12390 });
12391 executor.run_until_parked();
12392
12393 //// Backward
12394
12395 // Fourth diagnostic
12396 cx.update_editor(|editor, window, cx| {
12397 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12398 });
12399 cx.assert_editor_state(indoc! {"
12400 fn func(abc def: i32) -> ˇu32 {
12401 }
12402 "});
12403
12404 // Third diagnostic
12405 cx.update_editor(|editor, window, cx| {
12406 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12407 });
12408 cx.assert_editor_state(indoc! {"
12409 fn func(abc ˇdef: i32) -> u32 {
12410 }
12411 "});
12412
12413 // Second diagnostic, same place
12414 cx.update_editor(|editor, window, cx| {
12415 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12416 });
12417 cx.assert_editor_state(indoc! {"
12418 fn func(abc ˇdef: i32) -> u32 {
12419 }
12420 "});
12421
12422 // First diagnostic
12423 cx.update_editor(|editor, window, cx| {
12424 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12425 });
12426 cx.assert_editor_state(indoc! {"
12427 fn func(abcˇ def: i32) -> u32 {
12428 }
12429 "});
12430
12431 // Wrapped over, fourth diagnostic
12432 cx.update_editor(|editor, window, cx| {
12433 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12434 });
12435 cx.assert_editor_state(indoc! {"
12436 fn func(abc def: i32) -> ˇu32 {
12437 }
12438 "});
12439
12440 cx.update_editor(|editor, window, cx| {
12441 editor.move_to_beginning(&MoveToBeginning, window, cx);
12442 });
12443 cx.assert_editor_state(indoc! {"
12444 ˇfn func(abc def: i32) -> u32 {
12445 }
12446 "});
12447
12448 //// Forward
12449
12450 // First diagnostic
12451 cx.update_editor(|editor, window, cx| {
12452 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12453 });
12454 cx.assert_editor_state(indoc! {"
12455 fn func(abcˇ def: i32) -> u32 {
12456 }
12457 "});
12458
12459 // Second diagnostic
12460 cx.update_editor(|editor, window, cx| {
12461 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12462 });
12463 cx.assert_editor_state(indoc! {"
12464 fn func(abc ˇdef: i32) -> u32 {
12465 }
12466 "});
12467
12468 // Third diagnostic, same place
12469 cx.update_editor(|editor, window, cx| {
12470 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12471 });
12472 cx.assert_editor_state(indoc! {"
12473 fn func(abc ˇdef: i32) -> u32 {
12474 }
12475 "});
12476
12477 // Fourth diagnostic
12478 cx.update_editor(|editor, window, cx| {
12479 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12480 });
12481 cx.assert_editor_state(indoc! {"
12482 fn func(abc def: i32) -> ˇu32 {
12483 }
12484 "});
12485
12486 // Wrapped around, first diagnostic
12487 cx.update_editor(|editor, window, cx| {
12488 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12489 });
12490 cx.assert_editor_state(indoc! {"
12491 fn func(abcˇ def: i32) -> u32 {
12492 }
12493 "});
12494}
12495
12496#[gpui::test]
12497async fn active_diagnostics_dismiss_after_invalidation(
12498 executor: BackgroundExecutor,
12499 cx: &mut TestAppContext,
12500) {
12501 init_test(cx, |_| {});
12502
12503 let mut cx = EditorTestContext::new(cx).await;
12504 let lsp_store =
12505 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
12506
12507 cx.set_state(indoc! {"
12508 ˇfn func(abc def: i32) -> u32 {
12509 }
12510 "});
12511
12512 let message = "Something's wrong!";
12513 cx.update(|_, cx| {
12514 lsp_store.update(cx, |lsp_store, cx| {
12515 lsp_store
12516 .update_diagnostics(
12517 LanguageServerId(0),
12518 lsp::PublishDiagnosticsParams {
12519 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
12520 version: None,
12521 diagnostics: vec![lsp::Diagnostic {
12522 range: lsp::Range::new(
12523 lsp::Position::new(0, 11),
12524 lsp::Position::new(0, 12),
12525 ),
12526 severity: Some(lsp::DiagnosticSeverity::ERROR),
12527 message: message.to_string(),
12528 ..Default::default()
12529 }],
12530 },
12531 &[],
12532 cx,
12533 )
12534 .unwrap()
12535 });
12536 });
12537 executor.run_until_parked();
12538
12539 cx.update_editor(|editor, window, cx| {
12540 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12541 assert_eq!(
12542 editor
12543 .active_diagnostics
12544 .as_ref()
12545 .map(|diagnostics_group| diagnostics_group.primary_message.as_str()),
12546 Some(message),
12547 "Should have a diagnostics group activated"
12548 );
12549 });
12550 cx.assert_editor_state(indoc! {"
12551 fn func(abcˇ def: i32) -> u32 {
12552 }
12553 "});
12554
12555 cx.update(|_, cx| {
12556 lsp_store.update(cx, |lsp_store, cx| {
12557 lsp_store
12558 .update_diagnostics(
12559 LanguageServerId(0),
12560 lsp::PublishDiagnosticsParams {
12561 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
12562 version: None,
12563 diagnostics: Vec::new(),
12564 },
12565 &[],
12566 cx,
12567 )
12568 .unwrap()
12569 });
12570 });
12571 executor.run_until_parked();
12572 cx.update_editor(|editor, _, _| {
12573 assert_eq!(
12574 editor.active_diagnostics, None,
12575 "After no diagnostics set to the editor, no diagnostics should be active"
12576 );
12577 });
12578 cx.assert_editor_state(indoc! {"
12579 fn func(abcˇ def: i32) -> u32 {
12580 }
12581 "});
12582
12583 cx.update_editor(|editor, window, cx| {
12584 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12585 assert_eq!(
12586 editor.active_diagnostics, None,
12587 "Should be no diagnostics to go to and activate"
12588 );
12589 });
12590 cx.assert_editor_state(indoc! {"
12591 fn func(abcˇ def: i32) -> u32 {
12592 }
12593 "});
12594}
12595
12596#[gpui::test]
12597async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
12598 init_test(cx, |_| {});
12599
12600 let mut cx = EditorTestContext::new(cx).await;
12601
12602 cx.set_state(indoc! {"
12603 fn func(abˇc def: i32) -> u32 {
12604 }
12605 "});
12606 let lsp_store =
12607 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
12608
12609 cx.update(|_, cx| {
12610 lsp_store.update(cx, |lsp_store, cx| {
12611 lsp_store.update_diagnostics(
12612 LanguageServerId(0),
12613 lsp::PublishDiagnosticsParams {
12614 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
12615 version: None,
12616 diagnostics: vec![lsp::Diagnostic {
12617 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 12)),
12618 severity: Some(lsp::DiagnosticSeverity::ERROR),
12619 message: "we've had problems with <https://link.one>, and <https://link.two> is broken".to_string(),
12620 ..Default::default()
12621 }],
12622 },
12623 &[],
12624 cx,
12625 )
12626 })
12627 }).unwrap();
12628 cx.run_until_parked();
12629 cx.update_editor(|editor, window, cx| {
12630 hover_popover::hover(editor, &Default::default(), window, cx)
12631 });
12632 cx.run_until_parked();
12633 cx.update_editor(|editor, _, _| assert!(editor.hover_state.diagnostic_popover.is_some()))
12634}
12635
12636#[gpui::test]
12637async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
12638 init_test(cx, |_| {});
12639
12640 let mut cx = EditorTestContext::new(cx).await;
12641
12642 let diff_base = r#"
12643 use some::mod;
12644
12645 const A: u32 = 42;
12646
12647 fn main() {
12648 println!("hello");
12649
12650 println!("world");
12651 }
12652 "#
12653 .unindent();
12654
12655 // Edits are modified, removed, modified, added
12656 cx.set_state(
12657 &r#"
12658 use some::modified;
12659
12660 ˇ
12661 fn main() {
12662 println!("hello there");
12663
12664 println!("around the");
12665 println!("world");
12666 }
12667 "#
12668 .unindent(),
12669 );
12670
12671 cx.set_head_text(&diff_base);
12672 executor.run_until_parked();
12673
12674 cx.update_editor(|editor, window, cx| {
12675 //Wrap around the bottom of the buffer
12676 for _ in 0..3 {
12677 editor.go_to_next_hunk(&GoToHunk, window, cx);
12678 }
12679 });
12680
12681 cx.assert_editor_state(
12682 &r#"
12683 ˇuse some::modified;
12684
12685
12686 fn main() {
12687 println!("hello there");
12688
12689 println!("around the");
12690 println!("world");
12691 }
12692 "#
12693 .unindent(),
12694 );
12695
12696 cx.update_editor(|editor, window, cx| {
12697 //Wrap around the top of the buffer
12698 for _ in 0..2 {
12699 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12700 }
12701 });
12702
12703 cx.assert_editor_state(
12704 &r#"
12705 use some::modified;
12706
12707
12708 fn main() {
12709 ˇ println!("hello there");
12710
12711 println!("around the");
12712 println!("world");
12713 }
12714 "#
12715 .unindent(),
12716 );
12717
12718 cx.update_editor(|editor, window, cx| {
12719 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12720 });
12721
12722 cx.assert_editor_state(
12723 &r#"
12724 use some::modified;
12725
12726 ˇ
12727 fn main() {
12728 println!("hello there");
12729
12730 println!("around the");
12731 println!("world");
12732 }
12733 "#
12734 .unindent(),
12735 );
12736
12737 cx.update_editor(|editor, window, cx| {
12738 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12739 });
12740
12741 cx.assert_editor_state(
12742 &r#"
12743 ˇuse some::modified;
12744
12745
12746 fn main() {
12747 println!("hello there");
12748
12749 println!("around the");
12750 println!("world");
12751 }
12752 "#
12753 .unindent(),
12754 );
12755
12756 cx.update_editor(|editor, window, cx| {
12757 for _ in 0..2 {
12758 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12759 }
12760 });
12761
12762 cx.assert_editor_state(
12763 &r#"
12764 use some::modified;
12765
12766
12767 fn main() {
12768 ˇ println!("hello there");
12769
12770 println!("around the");
12771 println!("world");
12772 }
12773 "#
12774 .unindent(),
12775 );
12776
12777 cx.update_editor(|editor, window, cx| {
12778 editor.fold(&Fold, window, cx);
12779 });
12780
12781 cx.update_editor(|editor, window, cx| {
12782 editor.go_to_next_hunk(&GoToHunk, window, cx);
12783 });
12784
12785 cx.assert_editor_state(
12786 &r#"
12787 ˇuse some::modified;
12788
12789
12790 fn main() {
12791 println!("hello there");
12792
12793 println!("around the");
12794 println!("world");
12795 }
12796 "#
12797 .unindent(),
12798 );
12799}
12800
12801#[test]
12802fn test_split_words() {
12803 fn split(text: &str) -> Vec<&str> {
12804 split_words(text).collect()
12805 }
12806
12807 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
12808 assert_eq!(split("hello_world"), &["hello_", "world"]);
12809 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
12810 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
12811 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
12812 assert_eq!(split("helloworld"), &["helloworld"]);
12813
12814 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
12815}
12816
12817#[gpui::test]
12818async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
12819 init_test(cx, |_| {});
12820
12821 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
12822 let mut assert = |before, after| {
12823 let _state_context = cx.set_state(before);
12824 cx.run_until_parked();
12825 cx.update_editor(|editor, window, cx| {
12826 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
12827 });
12828 cx.run_until_parked();
12829 cx.assert_editor_state(after);
12830 };
12831
12832 // Outside bracket jumps to outside of matching bracket
12833 assert("console.logˇ(var);", "console.log(var)ˇ;");
12834 assert("console.log(var)ˇ;", "console.logˇ(var);");
12835
12836 // Inside bracket jumps to inside of matching bracket
12837 assert("console.log(ˇvar);", "console.log(varˇ);");
12838 assert("console.log(varˇ);", "console.log(ˇvar);");
12839
12840 // When outside a bracket and inside, favor jumping to the inside bracket
12841 assert(
12842 "console.log('foo', [1, 2, 3]ˇ);",
12843 "console.log(ˇ'foo', [1, 2, 3]);",
12844 );
12845 assert(
12846 "console.log(ˇ'foo', [1, 2, 3]);",
12847 "console.log('foo', [1, 2, 3]ˇ);",
12848 );
12849
12850 // Bias forward if two options are equally likely
12851 assert(
12852 "let result = curried_fun()ˇ();",
12853 "let result = curried_fun()()ˇ;",
12854 );
12855
12856 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
12857 assert(
12858 indoc! {"
12859 function test() {
12860 console.log('test')ˇ
12861 }"},
12862 indoc! {"
12863 function test() {
12864 console.logˇ('test')
12865 }"},
12866 );
12867}
12868
12869#[gpui::test]
12870async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
12871 init_test(cx, |_| {});
12872
12873 let fs = FakeFs::new(cx.executor());
12874 fs.insert_tree(
12875 path!("/a"),
12876 json!({
12877 "main.rs": "fn main() { let a = 5; }",
12878 "other.rs": "// Test file",
12879 }),
12880 )
12881 .await;
12882 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12883
12884 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12885 language_registry.add(Arc::new(Language::new(
12886 LanguageConfig {
12887 name: "Rust".into(),
12888 matcher: LanguageMatcher {
12889 path_suffixes: vec!["rs".to_string()],
12890 ..Default::default()
12891 },
12892 brackets: BracketPairConfig {
12893 pairs: vec![BracketPair {
12894 start: "{".to_string(),
12895 end: "}".to_string(),
12896 close: true,
12897 surround: true,
12898 newline: true,
12899 }],
12900 disabled_scopes_by_bracket_ix: Vec::new(),
12901 },
12902 ..Default::default()
12903 },
12904 Some(tree_sitter_rust::LANGUAGE.into()),
12905 )));
12906 let mut fake_servers = language_registry.register_fake_lsp(
12907 "Rust",
12908 FakeLspAdapter {
12909 capabilities: lsp::ServerCapabilities {
12910 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
12911 first_trigger_character: "{".to_string(),
12912 more_trigger_character: None,
12913 }),
12914 ..Default::default()
12915 },
12916 ..Default::default()
12917 },
12918 );
12919
12920 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12921
12922 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12923
12924 let worktree_id = workspace
12925 .update(cx, |workspace, _, cx| {
12926 workspace.project().update(cx, |project, cx| {
12927 project.worktrees(cx).next().unwrap().read(cx).id()
12928 })
12929 })
12930 .unwrap();
12931
12932 let buffer = project
12933 .update(cx, |project, cx| {
12934 project.open_local_buffer(path!("/a/main.rs"), cx)
12935 })
12936 .await
12937 .unwrap();
12938 let editor_handle = workspace
12939 .update(cx, |workspace, window, cx| {
12940 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
12941 })
12942 .unwrap()
12943 .await
12944 .unwrap()
12945 .downcast::<Editor>()
12946 .unwrap();
12947
12948 cx.executor().start_waiting();
12949 let fake_server = fake_servers.next().await.unwrap();
12950
12951 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
12952 |params, _| async move {
12953 assert_eq!(
12954 params.text_document_position.text_document.uri,
12955 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
12956 );
12957 assert_eq!(
12958 params.text_document_position.position,
12959 lsp::Position::new(0, 21),
12960 );
12961
12962 Ok(Some(vec![lsp::TextEdit {
12963 new_text: "]".to_string(),
12964 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12965 }]))
12966 },
12967 );
12968
12969 editor_handle.update_in(cx, |editor, window, cx| {
12970 window.focus(&editor.focus_handle(cx));
12971 editor.change_selections(None, window, cx, |s| {
12972 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
12973 });
12974 editor.handle_input("{", window, cx);
12975 });
12976
12977 cx.executor().run_until_parked();
12978
12979 buffer.update(cx, |buffer, _| {
12980 assert_eq!(
12981 buffer.text(),
12982 "fn main() { let a = {5}; }",
12983 "No extra braces from on type formatting should appear in the buffer"
12984 )
12985 });
12986}
12987
12988#[gpui::test]
12989async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
12990 init_test(cx, |_| {});
12991
12992 let fs = FakeFs::new(cx.executor());
12993 fs.insert_tree(
12994 path!("/a"),
12995 json!({
12996 "main.rs": "fn main() { let a = 5; }",
12997 "other.rs": "// Test file",
12998 }),
12999 )
13000 .await;
13001
13002 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13003
13004 let server_restarts = Arc::new(AtomicUsize::new(0));
13005 let closure_restarts = Arc::clone(&server_restarts);
13006 let language_server_name = "test language server";
13007 let language_name: LanguageName = "Rust".into();
13008
13009 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13010 language_registry.add(Arc::new(Language::new(
13011 LanguageConfig {
13012 name: language_name.clone(),
13013 matcher: LanguageMatcher {
13014 path_suffixes: vec!["rs".to_string()],
13015 ..Default::default()
13016 },
13017 ..Default::default()
13018 },
13019 Some(tree_sitter_rust::LANGUAGE.into()),
13020 )));
13021 let mut fake_servers = language_registry.register_fake_lsp(
13022 "Rust",
13023 FakeLspAdapter {
13024 name: language_server_name,
13025 initialization_options: Some(json!({
13026 "testOptionValue": true
13027 })),
13028 initializer: Some(Box::new(move |fake_server| {
13029 let task_restarts = Arc::clone(&closure_restarts);
13030 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
13031 task_restarts.fetch_add(1, atomic::Ordering::Release);
13032 futures::future::ready(Ok(()))
13033 });
13034 })),
13035 ..Default::default()
13036 },
13037 );
13038
13039 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13040 let _buffer = project
13041 .update(cx, |project, cx| {
13042 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
13043 })
13044 .await
13045 .unwrap();
13046 let _fake_server = fake_servers.next().await.unwrap();
13047 update_test_language_settings(cx, |language_settings| {
13048 language_settings.languages.insert(
13049 language_name.clone(),
13050 LanguageSettingsContent {
13051 tab_size: NonZeroU32::new(8),
13052 ..Default::default()
13053 },
13054 );
13055 });
13056 cx.executor().run_until_parked();
13057 assert_eq!(
13058 server_restarts.load(atomic::Ordering::Acquire),
13059 0,
13060 "Should not restart LSP server on an unrelated change"
13061 );
13062
13063 update_test_project_settings(cx, |project_settings| {
13064 project_settings.lsp.insert(
13065 "Some other server name".into(),
13066 LspSettings {
13067 binary: None,
13068 settings: None,
13069 initialization_options: Some(json!({
13070 "some other init value": false
13071 })),
13072 enable_lsp_tasks: false,
13073 },
13074 );
13075 });
13076 cx.executor().run_until_parked();
13077 assert_eq!(
13078 server_restarts.load(atomic::Ordering::Acquire),
13079 0,
13080 "Should not restart LSP server on an unrelated LSP settings change"
13081 );
13082
13083 update_test_project_settings(cx, |project_settings| {
13084 project_settings.lsp.insert(
13085 language_server_name.into(),
13086 LspSettings {
13087 binary: None,
13088 settings: None,
13089 initialization_options: Some(json!({
13090 "anotherInitValue": false
13091 })),
13092 enable_lsp_tasks: false,
13093 },
13094 );
13095 });
13096 cx.executor().run_until_parked();
13097 assert_eq!(
13098 server_restarts.load(atomic::Ordering::Acquire),
13099 1,
13100 "Should restart LSP server on a related LSP settings change"
13101 );
13102
13103 update_test_project_settings(cx, |project_settings| {
13104 project_settings.lsp.insert(
13105 language_server_name.into(),
13106 LspSettings {
13107 binary: None,
13108 settings: None,
13109 initialization_options: Some(json!({
13110 "anotherInitValue": false
13111 })),
13112 enable_lsp_tasks: false,
13113 },
13114 );
13115 });
13116 cx.executor().run_until_parked();
13117 assert_eq!(
13118 server_restarts.load(atomic::Ordering::Acquire),
13119 1,
13120 "Should not restart LSP server on a related LSP settings change that is the same"
13121 );
13122
13123 update_test_project_settings(cx, |project_settings| {
13124 project_settings.lsp.insert(
13125 language_server_name.into(),
13126 LspSettings {
13127 binary: None,
13128 settings: None,
13129 initialization_options: None,
13130 enable_lsp_tasks: false,
13131 },
13132 );
13133 });
13134 cx.executor().run_until_parked();
13135 assert_eq!(
13136 server_restarts.load(atomic::Ordering::Acquire),
13137 2,
13138 "Should restart LSP server on another related LSP settings change"
13139 );
13140}
13141
13142#[gpui::test]
13143async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
13144 init_test(cx, |_| {});
13145
13146 let mut cx = EditorLspTestContext::new_rust(
13147 lsp::ServerCapabilities {
13148 completion_provider: Some(lsp::CompletionOptions {
13149 trigger_characters: Some(vec![".".to_string()]),
13150 resolve_provider: Some(true),
13151 ..Default::default()
13152 }),
13153 ..Default::default()
13154 },
13155 cx,
13156 )
13157 .await;
13158
13159 cx.set_state("fn main() { let a = 2ˇ; }");
13160 cx.simulate_keystroke(".");
13161 let completion_item = lsp::CompletionItem {
13162 label: "some".into(),
13163 kind: Some(lsp::CompletionItemKind::SNIPPET),
13164 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13165 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13166 kind: lsp::MarkupKind::Markdown,
13167 value: "```rust\nSome(2)\n```".to_string(),
13168 })),
13169 deprecated: Some(false),
13170 sort_text: Some("fffffff2".to_string()),
13171 filter_text: Some("some".to_string()),
13172 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13173 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13174 range: lsp::Range {
13175 start: lsp::Position {
13176 line: 0,
13177 character: 22,
13178 },
13179 end: lsp::Position {
13180 line: 0,
13181 character: 22,
13182 },
13183 },
13184 new_text: "Some(2)".to_string(),
13185 })),
13186 additional_text_edits: Some(vec![lsp::TextEdit {
13187 range: lsp::Range {
13188 start: lsp::Position {
13189 line: 0,
13190 character: 20,
13191 },
13192 end: lsp::Position {
13193 line: 0,
13194 character: 22,
13195 },
13196 },
13197 new_text: "".to_string(),
13198 }]),
13199 ..Default::default()
13200 };
13201
13202 let closure_completion_item = completion_item.clone();
13203 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13204 let task_completion_item = closure_completion_item.clone();
13205 async move {
13206 Ok(Some(lsp::CompletionResponse::Array(vec![
13207 task_completion_item,
13208 ])))
13209 }
13210 });
13211
13212 request.next().await;
13213
13214 cx.condition(|editor, _| editor.context_menu_visible())
13215 .await;
13216 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13217 editor
13218 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13219 .unwrap()
13220 });
13221 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
13222
13223 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
13224 let task_completion_item = completion_item.clone();
13225 async move { Ok(task_completion_item) }
13226 })
13227 .next()
13228 .await
13229 .unwrap();
13230 apply_additional_edits.await.unwrap();
13231 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
13232}
13233
13234#[gpui::test]
13235async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
13236 init_test(cx, |_| {});
13237
13238 let mut cx = EditorLspTestContext::new_rust(
13239 lsp::ServerCapabilities {
13240 completion_provider: Some(lsp::CompletionOptions {
13241 trigger_characters: Some(vec![".".to_string()]),
13242 resolve_provider: Some(true),
13243 ..Default::default()
13244 }),
13245 ..Default::default()
13246 },
13247 cx,
13248 )
13249 .await;
13250
13251 cx.set_state("fn main() { let a = 2ˇ; }");
13252 cx.simulate_keystroke(".");
13253
13254 let item1 = lsp::CompletionItem {
13255 label: "method id()".to_string(),
13256 filter_text: Some("id".to_string()),
13257 detail: None,
13258 documentation: None,
13259 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13260 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13261 new_text: ".id".to_string(),
13262 })),
13263 ..lsp::CompletionItem::default()
13264 };
13265
13266 let item2 = lsp::CompletionItem {
13267 label: "other".to_string(),
13268 filter_text: Some("other".to_string()),
13269 detail: None,
13270 documentation: None,
13271 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13272 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13273 new_text: ".other".to_string(),
13274 })),
13275 ..lsp::CompletionItem::default()
13276 };
13277
13278 let item1 = item1.clone();
13279 cx.set_request_handler::<lsp::request::Completion, _, _>({
13280 let item1 = item1.clone();
13281 move |_, _, _| {
13282 let item1 = item1.clone();
13283 let item2 = item2.clone();
13284 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
13285 }
13286 })
13287 .next()
13288 .await;
13289
13290 cx.condition(|editor, _| editor.context_menu_visible())
13291 .await;
13292 cx.update_editor(|editor, _, _| {
13293 let context_menu = editor.context_menu.borrow_mut();
13294 let context_menu = context_menu
13295 .as_ref()
13296 .expect("Should have the context menu deployed");
13297 match context_menu {
13298 CodeContextMenu::Completions(completions_menu) => {
13299 let completions = completions_menu.completions.borrow_mut();
13300 assert_eq!(
13301 completions
13302 .iter()
13303 .map(|completion| &completion.label.text)
13304 .collect::<Vec<_>>(),
13305 vec!["method id()", "other"]
13306 )
13307 }
13308 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13309 }
13310 });
13311
13312 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
13313 let item1 = item1.clone();
13314 move |_, item_to_resolve, _| {
13315 let item1 = item1.clone();
13316 async move {
13317 if item1 == item_to_resolve {
13318 Ok(lsp::CompletionItem {
13319 label: "method id()".to_string(),
13320 filter_text: Some("id".to_string()),
13321 detail: Some("Now resolved!".to_string()),
13322 documentation: Some(lsp::Documentation::String("Docs".to_string())),
13323 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13324 range: lsp::Range::new(
13325 lsp::Position::new(0, 22),
13326 lsp::Position::new(0, 22),
13327 ),
13328 new_text: ".id".to_string(),
13329 })),
13330 ..lsp::CompletionItem::default()
13331 })
13332 } else {
13333 Ok(item_to_resolve)
13334 }
13335 }
13336 }
13337 })
13338 .next()
13339 .await
13340 .unwrap();
13341 cx.run_until_parked();
13342
13343 cx.update_editor(|editor, window, cx| {
13344 editor.context_menu_next(&Default::default(), window, cx);
13345 });
13346
13347 cx.update_editor(|editor, _, _| {
13348 let context_menu = editor.context_menu.borrow_mut();
13349 let context_menu = context_menu
13350 .as_ref()
13351 .expect("Should have the context menu deployed");
13352 match context_menu {
13353 CodeContextMenu::Completions(completions_menu) => {
13354 let completions = completions_menu.completions.borrow_mut();
13355 assert_eq!(
13356 completions
13357 .iter()
13358 .map(|completion| &completion.label.text)
13359 .collect::<Vec<_>>(),
13360 vec!["method id() Now resolved!", "other"],
13361 "Should update first completion label, but not second as the filter text did not match."
13362 );
13363 }
13364 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13365 }
13366 });
13367}
13368
13369#[gpui::test]
13370async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
13371 init_test(cx, |_| {});
13372
13373 let mut cx = EditorLspTestContext::new_rust(
13374 lsp::ServerCapabilities {
13375 completion_provider: Some(lsp::CompletionOptions {
13376 trigger_characters: Some(vec![".".to_string()]),
13377 resolve_provider: Some(true),
13378 ..Default::default()
13379 }),
13380 ..Default::default()
13381 },
13382 cx,
13383 )
13384 .await;
13385
13386 cx.set_state("fn main() { let a = 2ˇ; }");
13387 cx.simulate_keystroke(".");
13388
13389 let unresolved_item_1 = lsp::CompletionItem {
13390 label: "id".to_string(),
13391 filter_text: Some("id".to_string()),
13392 detail: None,
13393 documentation: None,
13394 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13395 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13396 new_text: ".id".to_string(),
13397 })),
13398 ..lsp::CompletionItem::default()
13399 };
13400 let resolved_item_1 = lsp::CompletionItem {
13401 additional_text_edits: Some(vec![lsp::TextEdit {
13402 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
13403 new_text: "!!".to_string(),
13404 }]),
13405 ..unresolved_item_1.clone()
13406 };
13407 let unresolved_item_2 = lsp::CompletionItem {
13408 label: "other".to_string(),
13409 filter_text: Some("other".to_string()),
13410 detail: None,
13411 documentation: None,
13412 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13413 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13414 new_text: ".other".to_string(),
13415 })),
13416 ..lsp::CompletionItem::default()
13417 };
13418 let resolved_item_2 = lsp::CompletionItem {
13419 additional_text_edits: Some(vec![lsp::TextEdit {
13420 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
13421 new_text: "??".to_string(),
13422 }]),
13423 ..unresolved_item_2.clone()
13424 };
13425
13426 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
13427 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
13428 cx.lsp
13429 .server
13430 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
13431 let unresolved_item_1 = unresolved_item_1.clone();
13432 let resolved_item_1 = resolved_item_1.clone();
13433 let unresolved_item_2 = unresolved_item_2.clone();
13434 let resolved_item_2 = resolved_item_2.clone();
13435 let resolve_requests_1 = resolve_requests_1.clone();
13436 let resolve_requests_2 = resolve_requests_2.clone();
13437 move |unresolved_request, _| {
13438 let unresolved_item_1 = unresolved_item_1.clone();
13439 let resolved_item_1 = resolved_item_1.clone();
13440 let unresolved_item_2 = unresolved_item_2.clone();
13441 let resolved_item_2 = resolved_item_2.clone();
13442 let resolve_requests_1 = resolve_requests_1.clone();
13443 let resolve_requests_2 = resolve_requests_2.clone();
13444 async move {
13445 if unresolved_request == unresolved_item_1 {
13446 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
13447 Ok(resolved_item_1.clone())
13448 } else if unresolved_request == unresolved_item_2 {
13449 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
13450 Ok(resolved_item_2.clone())
13451 } else {
13452 panic!("Unexpected completion item {unresolved_request:?}")
13453 }
13454 }
13455 }
13456 })
13457 .detach();
13458
13459 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13460 let unresolved_item_1 = unresolved_item_1.clone();
13461 let unresolved_item_2 = unresolved_item_2.clone();
13462 async move {
13463 Ok(Some(lsp::CompletionResponse::Array(vec![
13464 unresolved_item_1,
13465 unresolved_item_2,
13466 ])))
13467 }
13468 })
13469 .next()
13470 .await;
13471
13472 cx.condition(|editor, _| editor.context_menu_visible())
13473 .await;
13474 cx.update_editor(|editor, _, _| {
13475 let context_menu = editor.context_menu.borrow_mut();
13476 let context_menu = context_menu
13477 .as_ref()
13478 .expect("Should have the context menu deployed");
13479 match context_menu {
13480 CodeContextMenu::Completions(completions_menu) => {
13481 let completions = completions_menu.completions.borrow_mut();
13482 assert_eq!(
13483 completions
13484 .iter()
13485 .map(|completion| &completion.label.text)
13486 .collect::<Vec<_>>(),
13487 vec!["id", "other"]
13488 )
13489 }
13490 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13491 }
13492 });
13493 cx.run_until_parked();
13494
13495 cx.update_editor(|editor, window, cx| {
13496 editor.context_menu_next(&ContextMenuNext, window, cx);
13497 });
13498 cx.run_until_parked();
13499 cx.update_editor(|editor, window, cx| {
13500 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
13501 });
13502 cx.run_until_parked();
13503 cx.update_editor(|editor, window, cx| {
13504 editor.context_menu_next(&ContextMenuNext, window, cx);
13505 });
13506 cx.run_until_parked();
13507 cx.update_editor(|editor, window, cx| {
13508 editor
13509 .compose_completion(&ComposeCompletion::default(), window, cx)
13510 .expect("No task returned")
13511 })
13512 .await
13513 .expect("Completion failed");
13514 cx.run_until_parked();
13515
13516 cx.update_editor(|editor, _, cx| {
13517 assert_eq!(
13518 resolve_requests_1.load(atomic::Ordering::Acquire),
13519 1,
13520 "Should always resolve once despite multiple selections"
13521 );
13522 assert_eq!(
13523 resolve_requests_2.load(atomic::Ordering::Acquire),
13524 1,
13525 "Should always resolve once after multiple selections and applying the completion"
13526 );
13527 assert_eq!(
13528 editor.text(cx),
13529 "fn main() { let a = ??.other; }",
13530 "Should use resolved data when applying the completion"
13531 );
13532 });
13533}
13534
13535#[gpui::test]
13536async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
13537 init_test(cx, |_| {});
13538
13539 let item_0 = lsp::CompletionItem {
13540 label: "abs".into(),
13541 insert_text: Some("abs".into()),
13542 data: Some(json!({ "very": "special"})),
13543 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
13544 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
13545 lsp::InsertReplaceEdit {
13546 new_text: "abs".to_string(),
13547 insert: lsp::Range::default(),
13548 replace: lsp::Range::default(),
13549 },
13550 )),
13551 ..lsp::CompletionItem::default()
13552 };
13553 let items = iter::once(item_0.clone())
13554 .chain((11..51).map(|i| lsp::CompletionItem {
13555 label: format!("item_{}", i),
13556 insert_text: Some(format!("item_{}", i)),
13557 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
13558 ..lsp::CompletionItem::default()
13559 }))
13560 .collect::<Vec<_>>();
13561
13562 let default_commit_characters = vec!["?".to_string()];
13563 let default_data = json!({ "default": "data"});
13564 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
13565 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
13566 let default_edit_range = lsp::Range {
13567 start: lsp::Position {
13568 line: 0,
13569 character: 5,
13570 },
13571 end: lsp::Position {
13572 line: 0,
13573 character: 5,
13574 },
13575 };
13576
13577 let mut cx = EditorLspTestContext::new_rust(
13578 lsp::ServerCapabilities {
13579 completion_provider: Some(lsp::CompletionOptions {
13580 trigger_characters: Some(vec![".".to_string()]),
13581 resolve_provider: Some(true),
13582 ..Default::default()
13583 }),
13584 ..Default::default()
13585 },
13586 cx,
13587 )
13588 .await;
13589
13590 cx.set_state("fn main() { let a = 2ˇ; }");
13591 cx.simulate_keystroke(".");
13592
13593 let completion_data = default_data.clone();
13594 let completion_characters = default_commit_characters.clone();
13595 let completion_items = items.clone();
13596 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13597 let default_data = completion_data.clone();
13598 let default_commit_characters = completion_characters.clone();
13599 let items = completion_items.clone();
13600 async move {
13601 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13602 items,
13603 item_defaults: Some(lsp::CompletionListItemDefaults {
13604 data: Some(default_data.clone()),
13605 commit_characters: Some(default_commit_characters.clone()),
13606 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
13607 default_edit_range,
13608 )),
13609 insert_text_format: Some(default_insert_text_format),
13610 insert_text_mode: Some(default_insert_text_mode),
13611 }),
13612 ..lsp::CompletionList::default()
13613 })))
13614 }
13615 })
13616 .next()
13617 .await;
13618
13619 let resolved_items = Arc::new(Mutex::new(Vec::new()));
13620 cx.lsp
13621 .server
13622 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
13623 let closure_resolved_items = resolved_items.clone();
13624 move |item_to_resolve, _| {
13625 let closure_resolved_items = closure_resolved_items.clone();
13626 async move {
13627 closure_resolved_items.lock().push(item_to_resolve.clone());
13628 Ok(item_to_resolve)
13629 }
13630 }
13631 })
13632 .detach();
13633
13634 cx.condition(|editor, _| editor.context_menu_visible())
13635 .await;
13636 cx.run_until_parked();
13637 cx.update_editor(|editor, _, _| {
13638 let menu = editor.context_menu.borrow_mut();
13639 match menu.as_ref().expect("should have the completions menu") {
13640 CodeContextMenu::Completions(completions_menu) => {
13641 assert_eq!(
13642 completions_menu
13643 .entries
13644 .borrow()
13645 .iter()
13646 .map(|mat| mat.string.clone())
13647 .collect::<Vec<String>>(),
13648 items
13649 .iter()
13650 .map(|completion| completion.label.clone())
13651 .collect::<Vec<String>>()
13652 );
13653 }
13654 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
13655 }
13656 });
13657 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
13658 // with 4 from the end.
13659 assert_eq!(
13660 *resolved_items.lock(),
13661 [&items[0..16], &items[items.len() - 4..items.len()]]
13662 .concat()
13663 .iter()
13664 .cloned()
13665 .map(|mut item| {
13666 if item.data.is_none() {
13667 item.data = Some(default_data.clone());
13668 }
13669 item
13670 })
13671 .collect::<Vec<lsp::CompletionItem>>(),
13672 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
13673 );
13674 resolved_items.lock().clear();
13675
13676 cx.update_editor(|editor, window, cx| {
13677 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
13678 });
13679 cx.run_until_parked();
13680 // Completions that have already been resolved are skipped.
13681 assert_eq!(
13682 *resolved_items.lock(),
13683 items[items.len() - 16..items.len() - 4]
13684 .iter()
13685 .cloned()
13686 .map(|mut item| {
13687 if item.data.is_none() {
13688 item.data = Some(default_data.clone());
13689 }
13690 item
13691 })
13692 .collect::<Vec<lsp::CompletionItem>>()
13693 );
13694 resolved_items.lock().clear();
13695}
13696
13697#[gpui::test]
13698async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
13699 init_test(cx, |_| {});
13700
13701 let mut cx = EditorLspTestContext::new(
13702 Language::new(
13703 LanguageConfig {
13704 matcher: LanguageMatcher {
13705 path_suffixes: vec!["jsx".into()],
13706 ..Default::default()
13707 },
13708 overrides: [(
13709 "element".into(),
13710 LanguageConfigOverride {
13711 completion_query_characters: Override::Set(['-'].into_iter().collect()),
13712 ..Default::default()
13713 },
13714 )]
13715 .into_iter()
13716 .collect(),
13717 ..Default::default()
13718 },
13719 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
13720 )
13721 .with_override_query("(jsx_self_closing_element) @element")
13722 .unwrap(),
13723 lsp::ServerCapabilities {
13724 completion_provider: Some(lsp::CompletionOptions {
13725 trigger_characters: Some(vec![":".to_string()]),
13726 ..Default::default()
13727 }),
13728 ..Default::default()
13729 },
13730 cx,
13731 )
13732 .await;
13733
13734 cx.lsp
13735 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13736 Ok(Some(lsp::CompletionResponse::Array(vec![
13737 lsp::CompletionItem {
13738 label: "bg-blue".into(),
13739 ..Default::default()
13740 },
13741 lsp::CompletionItem {
13742 label: "bg-red".into(),
13743 ..Default::default()
13744 },
13745 lsp::CompletionItem {
13746 label: "bg-yellow".into(),
13747 ..Default::default()
13748 },
13749 ])))
13750 });
13751
13752 cx.set_state(r#"<p class="bgˇ" />"#);
13753
13754 // Trigger completion when typing a dash, because the dash is an extra
13755 // word character in the 'element' scope, which contains the cursor.
13756 cx.simulate_keystroke("-");
13757 cx.executor().run_until_parked();
13758 cx.update_editor(|editor, _, _| {
13759 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13760 {
13761 assert_eq!(
13762 completion_menu_entries(&menu),
13763 &["bg-red", "bg-blue", "bg-yellow"]
13764 );
13765 } else {
13766 panic!("expected completion menu to be open");
13767 }
13768 });
13769
13770 cx.simulate_keystroke("l");
13771 cx.executor().run_until_parked();
13772 cx.update_editor(|editor, _, _| {
13773 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13774 {
13775 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
13776 } else {
13777 panic!("expected completion menu to be open");
13778 }
13779 });
13780
13781 // When filtering completions, consider the character after the '-' to
13782 // be the start of a subword.
13783 cx.set_state(r#"<p class="yelˇ" />"#);
13784 cx.simulate_keystroke("l");
13785 cx.executor().run_until_parked();
13786 cx.update_editor(|editor, _, _| {
13787 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13788 {
13789 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
13790 } else {
13791 panic!("expected completion menu to be open");
13792 }
13793 });
13794}
13795
13796fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
13797 let entries = menu.entries.borrow();
13798 entries.iter().map(|mat| mat.string.clone()).collect()
13799}
13800
13801#[gpui::test]
13802async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
13803 init_test(cx, |settings| {
13804 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
13805 FormatterList(vec![Formatter::Prettier].into()),
13806 ))
13807 });
13808
13809 let fs = FakeFs::new(cx.executor());
13810 fs.insert_file(path!("/file.ts"), Default::default()).await;
13811
13812 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
13813 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13814
13815 language_registry.add(Arc::new(Language::new(
13816 LanguageConfig {
13817 name: "TypeScript".into(),
13818 matcher: LanguageMatcher {
13819 path_suffixes: vec!["ts".to_string()],
13820 ..Default::default()
13821 },
13822 ..Default::default()
13823 },
13824 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
13825 )));
13826 update_test_language_settings(cx, |settings| {
13827 settings.defaults.prettier = Some(PrettierSettings {
13828 allowed: true,
13829 ..PrettierSettings::default()
13830 });
13831 });
13832
13833 let test_plugin = "test_plugin";
13834 let _ = language_registry.register_fake_lsp(
13835 "TypeScript",
13836 FakeLspAdapter {
13837 prettier_plugins: vec![test_plugin],
13838 ..Default::default()
13839 },
13840 );
13841
13842 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
13843 let buffer = project
13844 .update(cx, |project, cx| {
13845 project.open_local_buffer(path!("/file.ts"), cx)
13846 })
13847 .await
13848 .unwrap();
13849
13850 let buffer_text = "one\ntwo\nthree\n";
13851 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13852 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
13853 editor.update_in(cx, |editor, window, cx| {
13854 editor.set_text(buffer_text, window, cx)
13855 });
13856
13857 editor
13858 .update_in(cx, |editor, window, cx| {
13859 editor.perform_format(
13860 project.clone(),
13861 FormatTrigger::Manual,
13862 FormatTarget::Buffers,
13863 window,
13864 cx,
13865 )
13866 })
13867 .unwrap()
13868 .await;
13869 assert_eq!(
13870 editor.update(cx, |editor, cx| editor.text(cx)),
13871 buffer_text.to_string() + prettier_format_suffix,
13872 "Test prettier formatting was not applied to the original buffer text",
13873 );
13874
13875 update_test_language_settings(cx, |settings| {
13876 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
13877 });
13878 let format = editor.update_in(cx, |editor, window, cx| {
13879 editor.perform_format(
13880 project.clone(),
13881 FormatTrigger::Manual,
13882 FormatTarget::Buffers,
13883 window,
13884 cx,
13885 )
13886 });
13887 format.await.unwrap();
13888 assert_eq!(
13889 editor.update(cx, |editor, cx| editor.text(cx)),
13890 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
13891 "Autoformatting (via test prettier) was not applied to the original buffer text",
13892 );
13893}
13894
13895#[gpui::test]
13896async fn test_addition_reverts(cx: &mut TestAppContext) {
13897 init_test(cx, |_| {});
13898 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13899 let base_text = indoc! {r#"
13900 struct Row;
13901 struct Row1;
13902 struct Row2;
13903
13904 struct Row4;
13905 struct Row5;
13906 struct Row6;
13907
13908 struct Row8;
13909 struct Row9;
13910 struct Row10;"#};
13911
13912 // When addition hunks are not adjacent to carets, no hunk revert is performed
13913 assert_hunk_revert(
13914 indoc! {r#"struct Row;
13915 struct Row1;
13916 struct Row1.1;
13917 struct Row1.2;
13918 struct Row2;ˇ
13919
13920 struct Row4;
13921 struct Row5;
13922 struct Row6;
13923
13924 struct Row8;
13925 ˇstruct Row9;
13926 struct Row9.1;
13927 struct Row9.2;
13928 struct Row9.3;
13929 struct Row10;"#},
13930 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
13931 indoc! {r#"struct Row;
13932 struct Row1;
13933 struct Row1.1;
13934 struct Row1.2;
13935 struct Row2;ˇ
13936
13937 struct Row4;
13938 struct Row5;
13939 struct Row6;
13940
13941 struct Row8;
13942 ˇstruct Row9;
13943 struct Row9.1;
13944 struct Row9.2;
13945 struct Row9.3;
13946 struct Row10;"#},
13947 base_text,
13948 &mut cx,
13949 );
13950 // Same for selections
13951 assert_hunk_revert(
13952 indoc! {r#"struct Row;
13953 struct Row1;
13954 struct Row2;
13955 struct Row2.1;
13956 struct Row2.2;
13957 «ˇ
13958 struct Row4;
13959 struct» Row5;
13960 «struct Row6;
13961 ˇ»
13962 struct Row9.1;
13963 struct Row9.2;
13964 struct Row9.3;
13965 struct Row8;
13966 struct Row9;
13967 struct Row10;"#},
13968 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
13969 indoc! {r#"struct Row;
13970 struct Row1;
13971 struct Row2;
13972 struct Row2.1;
13973 struct Row2.2;
13974 «ˇ
13975 struct Row4;
13976 struct» Row5;
13977 «struct Row6;
13978 ˇ»
13979 struct Row9.1;
13980 struct Row9.2;
13981 struct Row9.3;
13982 struct Row8;
13983 struct Row9;
13984 struct Row10;"#},
13985 base_text,
13986 &mut cx,
13987 );
13988
13989 // When carets and selections intersect the addition hunks, those are reverted.
13990 // Adjacent carets got merged.
13991 assert_hunk_revert(
13992 indoc! {r#"struct Row;
13993 ˇ// something on the top
13994 struct Row1;
13995 struct Row2;
13996 struct Roˇw3.1;
13997 struct Row2.2;
13998 struct Row2.3;ˇ
13999
14000 struct Row4;
14001 struct ˇRow5.1;
14002 struct Row5.2;
14003 struct «Rowˇ»5.3;
14004 struct Row5;
14005 struct Row6;
14006 ˇ
14007 struct Row9.1;
14008 struct «Rowˇ»9.2;
14009 struct «ˇRow»9.3;
14010 struct Row8;
14011 struct Row9;
14012 «ˇ// something on bottom»
14013 struct Row10;"#},
14014 vec![
14015 DiffHunkStatusKind::Added,
14016 DiffHunkStatusKind::Added,
14017 DiffHunkStatusKind::Added,
14018 DiffHunkStatusKind::Added,
14019 DiffHunkStatusKind::Added,
14020 ],
14021 indoc! {r#"struct Row;
14022 ˇstruct Row1;
14023 struct Row2;
14024 ˇ
14025 struct Row4;
14026 ˇstruct Row5;
14027 struct Row6;
14028 ˇ
14029 ˇstruct Row8;
14030 struct Row9;
14031 ˇstruct Row10;"#},
14032 base_text,
14033 &mut cx,
14034 );
14035}
14036
14037#[gpui::test]
14038async fn test_modification_reverts(cx: &mut TestAppContext) {
14039 init_test(cx, |_| {});
14040 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14041 let base_text = indoc! {r#"
14042 struct Row;
14043 struct Row1;
14044 struct Row2;
14045
14046 struct Row4;
14047 struct Row5;
14048 struct Row6;
14049
14050 struct Row8;
14051 struct Row9;
14052 struct Row10;"#};
14053
14054 // Modification hunks behave the same as the addition ones.
14055 assert_hunk_revert(
14056 indoc! {r#"struct Row;
14057 struct Row1;
14058 struct Row33;
14059 ˇ
14060 struct Row4;
14061 struct Row5;
14062 struct Row6;
14063 ˇ
14064 struct Row99;
14065 struct Row9;
14066 struct Row10;"#},
14067 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
14068 indoc! {r#"struct Row;
14069 struct Row1;
14070 struct Row33;
14071 ˇ
14072 struct Row4;
14073 struct Row5;
14074 struct Row6;
14075 ˇ
14076 struct Row99;
14077 struct Row9;
14078 struct Row10;"#},
14079 base_text,
14080 &mut cx,
14081 );
14082 assert_hunk_revert(
14083 indoc! {r#"struct Row;
14084 struct Row1;
14085 struct Row33;
14086 «ˇ
14087 struct Row4;
14088 struct» Row5;
14089 «struct Row6;
14090 ˇ»
14091 struct Row99;
14092 struct Row9;
14093 struct Row10;"#},
14094 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
14095 indoc! {r#"struct Row;
14096 struct Row1;
14097 struct Row33;
14098 «ˇ
14099 struct Row4;
14100 struct» Row5;
14101 «struct Row6;
14102 ˇ»
14103 struct Row99;
14104 struct Row9;
14105 struct Row10;"#},
14106 base_text,
14107 &mut cx,
14108 );
14109
14110 assert_hunk_revert(
14111 indoc! {r#"ˇstruct Row1.1;
14112 struct Row1;
14113 «ˇstr»uct Row22;
14114
14115 struct ˇRow44;
14116 struct Row5;
14117 struct «Rˇ»ow66;ˇ
14118
14119 «struˇ»ct Row88;
14120 struct Row9;
14121 struct Row1011;ˇ"#},
14122 vec![
14123 DiffHunkStatusKind::Modified,
14124 DiffHunkStatusKind::Modified,
14125 DiffHunkStatusKind::Modified,
14126 DiffHunkStatusKind::Modified,
14127 DiffHunkStatusKind::Modified,
14128 DiffHunkStatusKind::Modified,
14129 ],
14130 indoc! {r#"struct Row;
14131 ˇstruct Row1;
14132 struct Row2;
14133 ˇ
14134 struct Row4;
14135 ˇstruct Row5;
14136 struct Row6;
14137 ˇ
14138 struct Row8;
14139 ˇstruct Row9;
14140 struct Row10;ˇ"#},
14141 base_text,
14142 &mut cx,
14143 );
14144}
14145
14146#[gpui::test]
14147async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
14148 init_test(cx, |_| {});
14149 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14150 let base_text = indoc! {r#"
14151 one
14152
14153 two
14154 three
14155 "#};
14156
14157 cx.set_head_text(base_text);
14158 cx.set_state("\nˇ\n");
14159 cx.executor().run_until_parked();
14160 cx.update_editor(|editor, _window, cx| {
14161 editor.expand_selected_diff_hunks(cx);
14162 });
14163 cx.executor().run_until_parked();
14164 cx.update_editor(|editor, window, cx| {
14165 editor.backspace(&Default::default(), window, cx);
14166 });
14167 cx.run_until_parked();
14168 cx.assert_state_with_diff(
14169 indoc! {r#"
14170
14171 - two
14172 - threeˇ
14173 +
14174 "#}
14175 .to_string(),
14176 );
14177}
14178
14179#[gpui::test]
14180async fn test_deletion_reverts(cx: &mut TestAppContext) {
14181 init_test(cx, |_| {});
14182 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14183 let base_text = indoc! {r#"struct Row;
14184struct Row1;
14185struct Row2;
14186
14187struct Row4;
14188struct Row5;
14189struct Row6;
14190
14191struct Row8;
14192struct Row9;
14193struct Row10;"#};
14194
14195 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
14196 assert_hunk_revert(
14197 indoc! {r#"struct Row;
14198 struct Row2;
14199
14200 ˇstruct Row4;
14201 struct Row5;
14202 struct Row6;
14203 ˇ
14204 struct Row8;
14205 struct Row10;"#},
14206 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14207 indoc! {r#"struct Row;
14208 struct Row2;
14209
14210 ˇstruct Row4;
14211 struct Row5;
14212 struct Row6;
14213 ˇ
14214 struct Row8;
14215 struct Row10;"#},
14216 base_text,
14217 &mut cx,
14218 );
14219 assert_hunk_revert(
14220 indoc! {r#"struct Row;
14221 struct Row2;
14222
14223 «ˇstruct Row4;
14224 struct» Row5;
14225 «struct Row6;
14226 ˇ»
14227 struct Row8;
14228 struct Row10;"#},
14229 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14230 indoc! {r#"struct Row;
14231 struct Row2;
14232
14233 «ˇstruct Row4;
14234 struct» Row5;
14235 «struct Row6;
14236 ˇ»
14237 struct Row8;
14238 struct Row10;"#},
14239 base_text,
14240 &mut cx,
14241 );
14242
14243 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
14244 assert_hunk_revert(
14245 indoc! {r#"struct Row;
14246 ˇstruct Row2;
14247
14248 struct Row4;
14249 struct Row5;
14250 struct Row6;
14251
14252 struct Row8;ˇ
14253 struct Row10;"#},
14254 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14255 indoc! {r#"struct Row;
14256 struct Row1;
14257 ˇstruct Row2;
14258
14259 struct Row4;
14260 struct Row5;
14261 struct Row6;
14262
14263 struct Row8;ˇ
14264 struct Row9;
14265 struct Row10;"#},
14266 base_text,
14267 &mut cx,
14268 );
14269 assert_hunk_revert(
14270 indoc! {r#"struct Row;
14271 struct Row2«ˇ;
14272 struct Row4;
14273 struct» Row5;
14274 «struct Row6;
14275
14276 struct Row8;ˇ»
14277 struct Row10;"#},
14278 vec![
14279 DiffHunkStatusKind::Deleted,
14280 DiffHunkStatusKind::Deleted,
14281 DiffHunkStatusKind::Deleted,
14282 ],
14283 indoc! {r#"struct Row;
14284 struct Row1;
14285 struct Row2«ˇ;
14286
14287 struct Row4;
14288 struct» Row5;
14289 «struct Row6;
14290
14291 struct Row8;ˇ»
14292 struct Row9;
14293 struct Row10;"#},
14294 base_text,
14295 &mut cx,
14296 );
14297}
14298
14299#[gpui::test]
14300async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
14301 init_test(cx, |_| {});
14302
14303 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
14304 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
14305 let base_text_3 =
14306 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
14307
14308 let text_1 = edit_first_char_of_every_line(base_text_1);
14309 let text_2 = edit_first_char_of_every_line(base_text_2);
14310 let text_3 = edit_first_char_of_every_line(base_text_3);
14311
14312 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
14313 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
14314 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
14315
14316 let multibuffer = cx.new(|cx| {
14317 let mut multibuffer = MultiBuffer::new(ReadWrite);
14318 multibuffer.push_excerpts(
14319 buffer_1.clone(),
14320 [
14321 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14322 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14323 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14324 ],
14325 cx,
14326 );
14327 multibuffer.push_excerpts(
14328 buffer_2.clone(),
14329 [
14330 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14331 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14332 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14333 ],
14334 cx,
14335 );
14336 multibuffer.push_excerpts(
14337 buffer_3.clone(),
14338 [
14339 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14340 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14341 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14342 ],
14343 cx,
14344 );
14345 multibuffer
14346 });
14347
14348 let fs = FakeFs::new(cx.executor());
14349 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
14350 let (editor, cx) = cx
14351 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
14352 editor.update_in(cx, |editor, _window, cx| {
14353 for (buffer, diff_base) in [
14354 (buffer_1.clone(), base_text_1),
14355 (buffer_2.clone(), base_text_2),
14356 (buffer_3.clone(), base_text_3),
14357 ] {
14358 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
14359 editor
14360 .buffer
14361 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
14362 }
14363 });
14364 cx.executor().run_until_parked();
14365
14366 editor.update_in(cx, |editor, window, cx| {
14367 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}");
14368 editor.select_all(&SelectAll, window, cx);
14369 editor.git_restore(&Default::default(), window, cx);
14370 });
14371 cx.executor().run_until_parked();
14372
14373 // When all ranges are selected, all buffer hunks are reverted.
14374 editor.update(cx, |editor, cx| {
14375 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");
14376 });
14377 buffer_1.update(cx, |buffer, _| {
14378 assert_eq!(buffer.text(), base_text_1);
14379 });
14380 buffer_2.update(cx, |buffer, _| {
14381 assert_eq!(buffer.text(), base_text_2);
14382 });
14383 buffer_3.update(cx, |buffer, _| {
14384 assert_eq!(buffer.text(), base_text_3);
14385 });
14386
14387 editor.update_in(cx, |editor, window, cx| {
14388 editor.undo(&Default::default(), window, cx);
14389 });
14390
14391 editor.update_in(cx, |editor, window, cx| {
14392 editor.change_selections(None, window, cx, |s| {
14393 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
14394 });
14395 editor.git_restore(&Default::default(), window, cx);
14396 });
14397
14398 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
14399 // but not affect buffer_2 and its related excerpts.
14400 editor.update(cx, |editor, cx| {
14401 assert_eq!(
14402 editor.text(cx),
14403 "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}"
14404 );
14405 });
14406 buffer_1.update(cx, |buffer, _| {
14407 assert_eq!(buffer.text(), base_text_1);
14408 });
14409 buffer_2.update(cx, |buffer, _| {
14410 assert_eq!(
14411 buffer.text(),
14412 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
14413 );
14414 });
14415 buffer_3.update(cx, |buffer, _| {
14416 assert_eq!(
14417 buffer.text(),
14418 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
14419 );
14420 });
14421
14422 fn edit_first_char_of_every_line(text: &str) -> String {
14423 text.split('\n')
14424 .map(|line| format!("X{}", &line[1..]))
14425 .collect::<Vec<_>>()
14426 .join("\n")
14427 }
14428}
14429
14430#[gpui::test]
14431async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
14432 init_test(cx, |_| {});
14433
14434 let cols = 4;
14435 let rows = 10;
14436 let sample_text_1 = sample_text(rows, cols, 'a');
14437 assert_eq!(
14438 sample_text_1,
14439 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
14440 );
14441 let sample_text_2 = sample_text(rows, cols, 'l');
14442 assert_eq!(
14443 sample_text_2,
14444 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
14445 );
14446 let sample_text_3 = sample_text(rows, cols, 'v');
14447 assert_eq!(
14448 sample_text_3,
14449 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
14450 );
14451
14452 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
14453 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
14454 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
14455
14456 let multi_buffer = cx.new(|cx| {
14457 let mut multibuffer = MultiBuffer::new(ReadWrite);
14458 multibuffer.push_excerpts(
14459 buffer_1.clone(),
14460 [
14461 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14462 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14463 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14464 ],
14465 cx,
14466 );
14467 multibuffer.push_excerpts(
14468 buffer_2.clone(),
14469 [
14470 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14471 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14472 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14473 ],
14474 cx,
14475 );
14476 multibuffer.push_excerpts(
14477 buffer_3.clone(),
14478 [
14479 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14480 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14481 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14482 ],
14483 cx,
14484 );
14485 multibuffer
14486 });
14487
14488 let fs = FakeFs::new(cx.executor());
14489 fs.insert_tree(
14490 "/a",
14491 json!({
14492 "main.rs": sample_text_1,
14493 "other.rs": sample_text_2,
14494 "lib.rs": sample_text_3,
14495 }),
14496 )
14497 .await;
14498 let project = Project::test(fs, ["/a".as_ref()], cx).await;
14499 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14500 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14501 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
14502 Editor::new(
14503 EditorMode::full(),
14504 multi_buffer,
14505 Some(project.clone()),
14506 window,
14507 cx,
14508 )
14509 });
14510 let multibuffer_item_id = workspace
14511 .update(cx, |workspace, window, cx| {
14512 assert!(
14513 workspace.active_item(cx).is_none(),
14514 "active item should be None before the first item is added"
14515 );
14516 workspace.add_item_to_active_pane(
14517 Box::new(multi_buffer_editor.clone()),
14518 None,
14519 true,
14520 window,
14521 cx,
14522 );
14523 let active_item = workspace
14524 .active_item(cx)
14525 .expect("should have an active item after adding the multi buffer");
14526 assert!(
14527 !active_item.is_singleton(cx),
14528 "A multi buffer was expected to active after adding"
14529 );
14530 active_item.item_id()
14531 })
14532 .unwrap();
14533 cx.executor().run_until_parked();
14534
14535 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14536 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14537 s.select_ranges(Some(1..2))
14538 });
14539 editor.open_excerpts(&OpenExcerpts, window, cx);
14540 });
14541 cx.executor().run_until_parked();
14542 let first_item_id = workspace
14543 .update(cx, |workspace, window, cx| {
14544 let active_item = workspace
14545 .active_item(cx)
14546 .expect("should have an active item after navigating into the 1st buffer");
14547 let first_item_id = active_item.item_id();
14548 assert_ne!(
14549 first_item_id, multibuffer_item_id,
14550 "Should navigate into the 1st buffer and activate it"
14551 );
14552 assert!(
14553 active_item.is_singleton(cx),
14554 "New active item should be a singleton buffer"
14555 );
14556 assert_eq!(
14557 active_item
14558 .act_as::<Editor>(cx)
14559 .expect("should have navigated into an editor for the 1st buffer")
14560 .read(cx)
14561 .text(cx),
14562 sample_text_1
14563 );
14564
14565 workspace
14566 .go_back(workspace.active_pane().downgrade(), window, cx)
14567 .detach_and_log_err(cx);
14568
14569 first_item_id
14570 })
14571 .unwrap();
14572 cx.executor().run_until_parked();
14573 workspace
14574 .update(cx, |workspace, _, cx| {
14575 let active_item = workspace
14576 .active_item(cx)
14577 .expect("should have an active item after navigating back");
14578 assert_eq!(
14579 active_item.item_id(),
14580 multibuffer_item_id,
14581 "Should navigate back to the multi buffer"
14582 );
14583 assert!(!active_item.is_singleton(cx));
14584 })
14585 .unwrap();
14586
14587 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14588 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14589 s.select_ranges(Some(39..40))
14590 });
14591 editor.open_excerpts(&OpenExcerpts, window, cx);
14592 });
14593 cx.executor().run_until_parked();
14594 let second_item_id = workspace
14595 .update(cx, |workspace, window, cx| {
14596 let active_item = workspace
14597 .active_item(cx)
14598 .expect("should have an active item after navigating into the 2nd buffer");
14599 let second_item_id = active_item.item_id();
14600 assert_ne!(
14601 second_item_id, multibuffer_item_id,
14602 "Should navigate away from the multibuffer"
14603 );
14604 assert_ne!(
14605 second_item_id, first_item_id,
14606 "Should navigate into the 2nd buffer and activate it"
14607 );
14608 assert!(
14609 active_item.is_singleton(cx),
14610 "New active item should be a singleton buffer"
14611 );
14612 assert_eq!(
14613 active_item
14614 .act_as::<Editor>(cx)
14615 .expect("should have navigated into an editor")
14616 .read(cx)
14617 .text(cx),
14618 sample_text_2
14619 );
14620
14621 workspace
14622 .go_back(workspace.active_pane().downgrade(), window, cx)
14623 .detach_and_log_err(cx);
14624
14625 second_item_id
14626 })
14627 .unwrap();
14628 cx.executor().run_until_parked();
14629 workspace
14630 .update(cx, |workspace, _, cx| {
14631 let active_item = workspace
14632 .active_item(cx)
14633 .expect("should have an active item after navigating back from the 2nd buffer");
14634 assert_eq!(
14635 active_item.item_id(),
14636 multibuffer_item_id,
14637 "Should navigate back from the 2nd buffer to the multi buffer"
14638 );
14639 assert!(!active_item.is_singleton(cx));
14640 })
14641 .unwrap();
14642
14643 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14644 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14645 s.select_ranges(Some(70..70))
14646 });
14647 editor.open_excerpts(&OpenExcerpts, window, cx);
14648 });
14649 cx.executor().run_until_parked();
14650 workspace
14651 .update(cx, |workspace, window, cx| {
14652 let active_item = workspace
14653 .active_item(cx)
14654 .expect("should have an active item after navigating into the 3rd buffer");
14655 let third_item_id = active_item.item_id();
14656 assert_ne!(
14657 third_item_id, multibuffer_item_id,
14658 "Should navigate into the 3rd buffer and activate it"
14659 );
14660 assert_ne!(third_item_id, first_item_id);
14661 assert_ne!(third_item_id, second_item_id);
14662 assert!(
14663 active_item.is_singleton(cx),
14664 "New active item should be a singleton buffer"
14665 );
14666 assert_eq!(
14667 active_item
14668 .act_as::<Editor>(cx)
14669 .expect("should have navigated into an editor")
14670 .read(cx)
14671 .text(cx),
14672 sample_text_3
14673 );
14674
14675 workspace
14676 .go_back(workspace.active_pane().downgrade(), window, cx)
14677 .detach_and_log_err(cx);
14678 })
14679 .unwrap();
14680 cx.executor().run_until_parked();
14681 workspace
14682 .update(cx, |workspace, _, cx| {
14683 let active_item = workspace
14684 .active_item(cx)
14685 .expect("should have an active item after navigating back from the 3rd buffer");
14686 assert_eq!(
14687 active_item.item_id(),
14688 multibuffer_item_id,
14689 "Should navigate back from the 3rd buffer to the multi buffer"
14690 );
14691 assert!(!active_item.is_singleton(cx));
14692 })
14693 .unwrap();
14694}
14695
14696#[gpui::test]
14697async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14698 init_test(cx, |_| {});
14699
14700 let mut cx = EditorTestContext::new(cx).await;
14701
14702 let diff_base = r#"
14703 use some::mod;
14704
14705 const A: u32 = 42;
14706
14707 fn main() {
14708 println!("hello");
14709
14710 println!("world");
14711 }
14712 "#
14713 .unindent();
14714
14715 cx.set_state(
14716 &r#"
14717 use some::modified;
14718
14719 ˇ
14720 fn main() {
14721 println!("hello there");
14722
14723 println!("around the");
14724 println!("world");
14725 }
14726 "#
14727 .unindent(),
14728 );
14729
14730 cx.set_head_text(&diff_base);
14731 executor.run_until_parked();
14732
14733 cx.update_editor(|editor, window, cx| {
14734 editor.go_to_next_hunk(&GoToHunk, window, cx);
14735 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14736 });
14737 executor.run_until_parked();
14738 cx.assert_state_with_diff(
14739 r#"
14740 use some::modified;
14741
14742
14743 fn main() {
14744 - println!("hello");
14745 + ˇ println!("hello there");
14746
14747 println!("around the");
14748 println!("world");
14749 }
14750 "#
14751 .unindent(),
14752 );
14753
14754 cx.update_editor(|editor, window, cx| {
14755 for _ in 0..2 {
14756 editor.go_to_next_hunk(&GoToHunk, window, cx);
14757 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14758 }
14759 });
14760 executor.run_until_parked();
14761 cx.assert_state_with_diff(
14762 r#"
14763 - use some::mod;
14764 + ˇuse some::modified;
14765
14766
14767 fn main() {
14768 - println!("hello");
14769 + println!("hello there");
14770
14771 + println!("around the");
14772 println!("world");
14773 }
14774 "#
14775 .unindent(),
14776 );
14777
14778 cx.update_editor(|editor, window, cx| {
14779 editor.go_to_next_hunk(&GoToHunk, window, cx);
14780 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14781 });
14782 executor.run_until_parked();
14783 cx.assert_state_with_diff(
14784 r#"
14785 - use some::mod;
14786 + use some::modified;
14787
14788 - const A: u32 = 42;
14789 ˇ
14790 fn main() {
14791 - println!("hello");
14792 + println!("hello there");
14793
14794 + println!("around the");
14795 println!("world");
14796 }
14797 "#
14798 .unindent(),
14799 );
14800
14801 cx.update_editor(|editor, window, cx| {
14802 editor.cancel(&Cancel, window, cx);
14803 });
14804
14805 cx.assert_state_with_diff(
14806 r#"
14807 use some::modified;
14808
14809 ˇ
14810 fn main() {
14811 println!("hello there");
14812
14813 println!("around the");
14814 println!("world");
14815 }
14816 "#
14817 .unindent(),
14818 );
14819}
14820
14821#[gpui::test]
14822async fn test_diff_base_change_with_expanded_diff_hunks(
14823 executor: BackgroundExecutor,
14824 cx: &mut TestAppContext,
14825) {
14826 init_test(cx, |_| {});
14827
14828 let mut cx = EditorTestContext::new(cx).await;
14829
14830 let diff_base = r#"
14831 use some::mod1;
14832 use some::mod2;
14833
14834 const A: u32 = 42;
14835 const B: u32 = 42;
14836 const C: u32 = 42;
14837
14838 fn main() {
14839 println!("hello");
14840
14841 println!("world");
14842 }
14843 "#
14844 .unindent();
14845
14846 cx.set_state(
14847 &r#"
14848 use some::mod2;
14849
14850 const A: u32 = 42;
14851 const C: u32 = 42;
14852
14853 fn main(ˇ) {
14854 //println!("hello");
14855
14856 println!("world");
14857 //
14858 //
14859 }
14860 "#
14861 .unindent(),
14862 );
14863
14864 cx.set_head_text(&diff_base);
14865 executor.run_until_parked();
14866
14867 cx.update_editor(|editor, window, cx| {
14868 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14869 });
14870 executor.run_until_parked();
14871 cx.assert_state_with_diff(
14872 r#"
14873 - use some::mod1;
14874 use some::mod2;
14875
14876 const A: u32 = 42;
14877 - const B: u32 = 42;
14878 const C: u32 = 42;
14879
14880 fn main(ˇ) {
14881 - println!("hello");
14882 + //println!("hello");
14883
14884 println!("world");
14885 + //
14886 + //
14887 }
14888 "#
14889 .unindent(),
14890 );
14891
14892 cx.set_head_text("new diff base!");
14893 executor.run_until_parked();
14894 cx.assert_state_with_diff(
14895 r#"
14896 - new diff base!
14897 + use some::mod2;
14898 +
14899 + const A: u32 = 42;
14900 + const C: u32 = 42;
14901 +
14902 + fn main(ˇ) {
14903 + //println!("hello");
14904 +
14905 + println!("world");
14906 + //
14907 + //
14908 + }
14909 "#
14910 .unindent(),
14911 );
14912}
14913
14914#[gpui::test]
14915async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
14916 init_test(cx, |_| {});
14917
14918 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
14919 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
14920 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
14921 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
14922 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
14923 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
14924
14925 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
14926 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
14927 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
14928
14929 let multi_buffer = cx.new(|cx| {
14930 let mut multibuffer = MultiBuffer::new(ReadWrite);
14931 multibuffer.push_excerpts(
14932 buffer_1.clone(),
14933 [
14934 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14935 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14936 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
14937 ],
14938 cx,
14939 );
14940 multibuffer.push_excerpts(
14941 buffer_2.clone(),
14942 [
14943 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14944 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14945 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
14946 ],
14947 cx,
14948 );
14949 multibuffer.push_excerpts(
14950 buffer_3.clone(),
14951 [
14952 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14953 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14954 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
14955 ],
14956 cx,
14957 );
14958 multibuffer
14959 });
14960
14961 let editor =
14962 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
14963 editor
14964 .update(cx, |editor, _window, cx| {
14965 for (buffer, diff_base) in [
14966 (buffer_1.clone(), file_1_old),
14967 (buffer_2.clone(), file_2_old),
14968 (buffer_3.clone(), file_3_old),
14969 ] {
14970 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
14971 editor
14972 .buffer
14973 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
14974 }
14975 })
14976 .unwrap();
14977
14978 let mut cx = EditorTestContext::for_editor(editor, cx).await;
14979 cx.run_until_parked();
14980
14981 cx.assert_editor_state(
14982 &"
14983 ˇaaa
14984 ccc
14985 ddd
14986
14987 ggg
14988 hhh
14989
14990
14991 lll
14992 mmm
14993 NNN
14994
14995 qqq
14996 rrr
14997
14998 uuu
14999 111
15000 222
15001 333
15002
15003 666
15004 777
15005
15006 000
15007 !!!"
15008 .unindent(),
15009 );
15010
15011 cx.update_editor(|editor, window, cx| {
15012 editor.select_all(&SelectAll, window, cx);
15013 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15014 });
15015 cx.executor().run_until_parked();
15016
15017 cx.assert_state_with_diff(
15018 "
15019 «aaa
15020 - bbb
15021 ccc
15022 ddd
15023
15024 ggg
15025 hhh
15026
15027
15028 lll
15029 mmm
15030 - nnn
15031 + NNN
15032
15033 qqq
15034 rrr
15035
15036 uuu
15037 111
15038 222
15039 333
15040
15041 + 666
15042 777
15043
15044 000
15045 !!!ˇ»"
15046 .unindent(),
15047 );
15048}
15049
15050#[gpui::test]
15051async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
15052 init_test(cx, |_| {});
15053
15054 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
15055 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
15056
15057 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
15058 let multi_buffer = cx.new(|cx| {
15059 let mut multibuffer = MultiBuffer::new(ReadWrite);
15060 multibuffer.push_excerpts(
15061 buffer.clone(),
15062 [
15063 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
15064 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
15065 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
15066 ],
15067 cx,
15068 );
15069 multibuffer
15070 });
15071
15072 let editor =
15073 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
15074 editor
15075 .update(cx, |editor, _window, cx| {
15076 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
15077 editor
15078 .buffer
15079 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
15080 })
15081 .unwrap();
15082
15083 let mut cx = EditorTestContext::for_editor(editor, cx).await;
15084 cx.run_until_parked();
15085
15086 cx.update_editor(|editor, window, cx| {
15087 editor.expand_all_diff_hunks(&Default::default(), window, cx)
15088 });
15089 cx.executor().run_until_parked();
15090
15091 // When the start of a hunk coincides with the start of its excerpt,
15092 // the hunk is expanded. When the start of a a hunk is earlier than
15093 // the start of its excerpt, the hunk is not expanded.
15094 cx.assert_state_with_diff(
15095 "
15096 ˇaaa
15097 - bbb
15098 + BBB
15099
15100 - ddd
15101 - eee
15102 + DDD
15103 + EEE
15104 fff
15105
15106 iii
15107 "
15108 .unindent(),
15109 );
15110}
15111
15112#[gpui::test]
15113async fn test_edits_around_expanded_insertion_hunks(
15114 executor: BackgroundExecutor,
15115 cx: &mut TestAppContext,
15116) {
15117 init_test(cx, |_| {});
15118
15119 let mut cx = EditorTestContext::new(cx).await;
15120
15121 let diff_base = r#"
15122 use some::mod1;
15123 use some::mod2;
15124
15125 const A: u32 = 42;
15126
15127 fn main() {
15128 println!("hello");
15129
15130 println!("world");
15131 }
15132 "#
15133 .unindent();
15134 executor.run_until_parked();
15135 cx.set_state(
15136 &r#"
15137 use some::mod1;
15138 use some::mod2;
15139
15140 const A: u32 = 42;
15141 const B: u32 = 42;
15142 const C: u32 = 42;
15143 ˇ
15144
15145 fn main() {
15146 println!("hello");
15147
15148 println!("world");
15149 }
15150 "#
15151 .unindent(),
15152 );
15153
15154 cx.set_head_text(&diff_base);
15155 executor.run_until_parked();
15156
15157 cx.update_editor(|editor, window, cx| {
15158 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15159 });
15160 executor.run_until_parked();
15161
15162 cx.assert_state_with_diff(
15163 r#"
15164 use some::mod1;
15165 use some::mod2;
15166
15167 const A: u32 = 42;
15168 + const B: u32 = 42;
15169 + const C: u32 = 42;
15170 + ˇ
15171
15172 fn main() {
15173 println!("hello");
15174
15175 println!("world");
15176 }
15177 "#
15178 .unindent(),
15179 );
15180
15181 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
15182 executor.run_until_parked();
15183
15184 cx.assert_state_with_diff(
15185 r#"
15186 use some::mod1;
15187 use some::mod2;
15188
15189 const A: u32 = 42;
15190 + const B: u32 = 42;
15191 + const C: u32 = 42;
15192 + const D: u32 = 42;
15193 + ˇ
15194
15195 fn main() {
15196 println!("hello");
15197
15198 println!("world");
15199 }
15200 "#
15201 .unindent(),
15202 );
15203
15204 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
15205 executor.run_until_parked();
15206
15207 cx.assert_state_with_diff(
15208 r#"
15209 use some::mod1;
15210 use some::mod2;
15211
15212 const A: u32 = 42;
15213 + const B: u32 = 42;
15214 + const C: u32 = 42;
15215 + const D: u32 = 42;
15216 + const E: u32 = 42;
15217 + ˇ
15218
15219 fn main() {
15220 println!("hello");
15221
15222 println!("world");
15223 }
15224 "#
15225 .unindent(),
15226 );
15227
15228 cx.update_editor(|editor, window, cx| {
15229 editor.delete_line(&DeleteLine, window, cx);
15230 });
15231 executor.run_until_parked();
15232
15233 cx.assert_state_with_diff(
15234 r#"
15235 use some::mod1;
15236 use some::mod2;
15237
15238 const A: u32 = 42;
15239 + const B: u32 = 42;
15240 + const C: u32 = 42;
15241 + const D: u32 = 42;
15242 + const E: u32 = 42;
15243 ˇ
15244 fn main() {
15245 println!("hello");
15246
15247 println!("world");
15248 }
15249 "#
15250 .unindent(),
15251 );
15252
15253 cx.update_editor(|editor, window, cx| {
15254 editor.move_up(&MoveUp, window, cx);
15255 editor.delete_line(&DeleteLine, window, cx);
15256 editor.move_up(&MoveUp, window, cx);
15257 editor.delete_line(&DeleteLine, window, cx);
15258 editor.move_up(&MoveUp, window, cx);
15259 editor.delete_line(&DeleteLine, window, cx);
15260 });
15261 executor.run_until_parked();
15262 cx.assert_state_with_diff(
15263 r#"
15264 use some::mod1;
15265 use some::mod2;
15266
15267 const A: u32 = 42;
15268 + const B: u32 = 42;
15269 ˇ
15270 fn main() {
15271 println!("hello");
15272
15273 println!("world");
15274 }
15275 "#
15276 .unindent(),
15277 );
15278
15279 cx.update_editor(|editor, window, cx| {
15280 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
15281 editor.delete_line(&DeleteLine, window, cx);
15282 });
15283 executor.run_until_parked();
15284 cx.assert_state_with_diff(
15285 r#"
15286 ˇ
15287 fn main() {
15288 println!("hello");
15289
15290 println!("world");
15291 }
15292 "#
15293 .unindent(),
15294 );
15295}
15296
15297#[gpui::test]
15298async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
15299 init_test(cx, |_| {});
15300
15301 let mut cx = EditorTestContext::new(cx).await;
15302 cx.set_head_text(indoc! { "
15303 one
15304 two
15305 three
15306 four
15307 five
15308 "
15309 });
15310 cx.set_state(indoc! { "
15311 one
15312 ˇthree
15313 five
15314 "});
15315 cx.run_until_parked();
15316 cx.update_editor(|editor, window, cx| {
15317 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15318 });
15319 cx.assert_state_with_diff(
15320 indoc! { "
15321 one
15322 - two
15323 ˇthree
15324 - four
15325 five
15326 "}
15327 .to_string(),
15328 );
15329 cx.update_editor(|editor, window, cx| {
15330 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15331 });
15332
15333 cx.assert_state_with_diff(
15334 indoc! { "
15335 one
15336 ˇthree
15337 five
15338 "}
15339 .to_string(),
15340 );
15341
15342 cx.set_state(indoc! { "
15343 one
15344 ˇTWO
15345 three
15346 four
15347 five
15348 "});
15349 cx.run_until_parked();
15350 cx.update_editor(|editor, window, cx| {
15351 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15352 });
15353
15354 cx.assert_state_with_diff(
15355 indoc! { "
15356 one
15357 - two
15358 + ˇTWO
15359 three
15360 four
15361 five
15362 "}
15363 .to_string(),
15364 );
15365 cx.update_editor(|editor, window, cx| {
15366 editor.move_up(&Default::default(), window, cx);
15367 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15368 });
15369 cx.assert_state_with_diff(
15370 indoc! { "
15371 one
15372 ˇTWO
15373 three
15374 four
15375 five
15376 "}
15377 .to_string(),
15378 );
15379}
15380
15381#[gpui::test]
15382async fn test_edits_around_expanded_deletion_hunks(
15383 executor: BackgroundExecutor,
15384 cx: &mut TestAppContext,
15385) {
15386 init_test(cx, |_| {});
15387
15388 let mut cx = EditorTestContext::new(cx).await;
15389
15390 let diff_base = r#"
15391 use some::mod1;
15392 use some::mod2;
15393
15394 const A: u32 = 42;
15395 const B: u32 = 42;
15396 const C: u32 = 42;
15397
15398
15399 fn main() {
15400 println!("hello");
15401
15402 println!("world");
15403 }
15404 "#
15405 .unindent();
15406 executor.run_until_parked();
15407 cx.set_state(
15408 &r#"
15409 use some::mod1;
15410 use some::mod2;
15411
15412 ˇconst B: u32 = 42;
15413 const C: u32 = 42;
15414
15415
15416 fn main() {
15417 println!("hello");
15418
15419 println!("world");
15420 }
15421 "#
15422 .unindent(),
15423 );
15424
15425 cx.set_head_text(&diff_base);
15426 executor.run_until_parked();
15427
15428 cx.update_editor(|editor, window, cx| {
15429 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15430 });
15431 executor.run_until_parked();
15432
15433 cx.assert_state_with_diff(
15434 r#"
15435 use some::mod1;
15436 use some::mod2;
15437
15438 - const A: u32 = 42;
15439 ˇconst B: u32 = 42;
15440 const C: u32 = 42;
15441
15442
15443 fn main() {
15444 println!("hello");
15445
15446 println!("world");
15447 }
15448 "#
15449 .unindent(),
15450 );
15451
15452 cx.update_editor(|editor, window, cx| {
15453 editor.delete_line(&DeleteLine, window, cx);
15454 });
15455 executor.run_until_parked();
15456 cx.assert_state_with_diff(
15457 r#"
15458 use some::mod1;
15459 use some::mod2;
15460
15461 - const A: u32 = 42;
15462 - const B: u32 = 42;
15463 ˇconst C: u32 = 42;
15464
15465
15466 fn main() {
15467 println!("hello");
15468
15469 println!("world");
15470 }
15471 "#
15472 .unindent(),
15473 );
15474
15475 cx.update_editor(|editor, window, cx| {
15476 editor.delete_line(&DeleteLine, window, cx);
15477 });
15478 executor.run_until_parked();
15479 cx.assert_state_with_diff(
15480 r#"
15481 use some::mod1;
15482 use some::mod2;
15483
15484 - const A: u32 = 42;
15485 - const B: u32 = 42;
15486 - const C: u32 = 42;
15487 ˇ
15488
15489 fn main() {
15490 println!("hello");
15491
15492 println!("world");
15493 }
15494 "#
15495 .unindent(),
15496 );
15497
15498 cx.update_editor(|editor, window, cx| {
15499 editor.handle_input("replacement", window, cx);
15500 });
15501 executor.run_until_parked();
15502 cx.assert_state_with_diff(
15503 r#"
15504 use some::mod1;
15505 use some::mod2;
15506
15507 - const A: u32 = 42;
15508 - const B: u32 = 42;
15509 - const C: u32 = 42;
15510 -
15511 + replacementˇ
15512
15513 fn main() {
15514 println!("hello");
15515
15516 println!("world");
15517 }
15518 "#
15519 .unindent(),
15520 );
15521}
15522
15523#[gpui::test]
15524async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15525 init_test(cx, |_| {});
15526
15527 let mut cx = EditorTestContext::new(cx).await;
15528
15529 let base_text = r#"
15530 one
15531 two
15532 three
15533 four
15534 five
15535 "#
15536 .unindent();
15537 executor.run_until_parked();
15538 cx.set_state(
15539 &r#"
15540 one
15541 two
15542 fˇour
15543 five
15544 "#
15545 .unindent(),
15546 );
15547
15548 cx.set_head_text(&base_text);
15549 executor.run_until_parked();
15550
15551 cx.update_editor(|editor, window, cx| {
15552 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15553 });
15554 executor.run_until_parked();
15555
15556 cx.assert_state_with_diff(
15557 r#"
15558 one
15559 two
15560 - three
15561 fˇour
15562 five
15563 "#
15564 .unindent(),
15565 );
15566
15567 cx.update_editor(|editor, window, cx| {
15568 editor.backspace(&Backspace, window, cx);
15569 editor.backspace(&Backspace, window, cx);
15570 });
15571 executor.run_until_parked();
15572 cx.assert_state_with_diff(
15573 r#"
15574 one
15575 two
15576 - threeˇ
15577 - four
15578 + our
15579 five
15580 "#
15581 .unindent(),
15582 );
15583}
15584
15585#[gpui::test]
15586async fn test_edit_after_expanded_modification_hunk(
15587 executor: BackgroundExecutor,
15588 cx: &mut TestAppContext,
15589) {
15590 init_test(cx, |_| {});
15591
15592 let mut cx = EditorTestContext::new(cx).await;
15593
15594 let diff_base = r#"
15595 use some::mod1;
15596 use some::mod2;
15597
15598 const A: u32 = 42;
15599 const B: u32 = 42;
15600 const C: u32 = 42;
15601 const D: u32 = 42;
15602
15603
15604 fn main() {
15605 println!("hello");
15606
15607 println!("world");
15608 }"#
15609 .unindent();
15610
15611 cx.set_state(
15612 &r#"
15613 use some::mod1;
15614 use some::mod2;
15615
15616 const A: u32 = 42;
15617 const B: u32 = 42;
15618 const C: u32 = 43ˇ
15619 const D: u32 = 42;
15620
15621
15622 fn main() {
15623 println!("hello");
15624
15625 println!("world");
15626 }"#
15627 .unindent(),
15628 );
15629
15630 cx.set_head_text(&diff_base);
15631 executor.run_until_parked();
15632 cx.update_editor(|editor, window, cx| {
15633 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15634 });
15635 executor.run_until_parked();
15636
15637 cx.assert_state_with_diff(
15638 r#"
15639 use some::mod1;
15640 use some::mod2;
15641
15642 const A: u32 = 42;
15643 const B: u32 = 42;
15644 - const C: u32 = 42;
15645 + const C: u32 = 43ˇ
15646 const D: u32 = 42;
15647
15648
15649 fn main() {
15650 println!("hello");
15651
15652 println!("world");
15653 }"#
15654 .unindent(),
15655 );
15656
15657 cx.update_editor(|editor, window, cx| {
15658 editor.handle_input("\nnew_line\n", window, cx);
15659 });
15660 executor.run_until_parked();
15661
15662 cx.assert_state_with_diff(
15663 r#"
15664 use some::mod1;
15665 use some::mod2;
15666
15667 const A: u32 = 42;
15668 const B: u32 = 42;
15669 - const C: u32 = 42;
15670 + const C: u32 = 43
15671 + new_line
15672 + ˇ
15673 const D: u32 = 42;
15674
15675
15676 fn main() {
15677 println!("hello");
15678
15679 println!("world");
15680 }"#
15681 .unindent(),
15682 );
15683}
15684
15685#[gpui::test]
15686async fn test_stage_and_unstage_added_file_hunk(
15687 executor: BackgroundExecutor,
15688 cx: &mut TestAppContext,
15689) {
15690 init_test(cx, |_| {});
15691
15692 let mut cx = EditorTestContext::new(cx).await;
15693 cx.update_editor(|editor, _, cx| {
15694 editor.set_expand_all_diff_hunks(cx);
15695 });
15696
15697 let working_copy = r#"
15698 ˇfn main() {
15699 println!("hello, world!");
15700 }
15701 "#
15702 .unindent();
15703
15704 cx.set_state(&working_copy);
15705 executor.run_until_parked();
15706
15707 cx.assert_state_with_diff(
15708 r#"
15709 + ˇfn main() {
15710 + println!("hello, world!");
15711 + }
15712 "#
15713 .unindent(),
15714 );
15715 cx.assert_index_text(None);
15716
15717 cx.update_editor(|editor, window, cx| {
15718 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
15719 });
15720 executor.run_until_parked();
15721 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
15722 cx.assert_state_with_diff(
15723 r#"
15724 + ˇfn main() {
15725 + println!("hello, world!");
15726 + }
15727 "#
15728 .unindent(),
15729 );
15730
15731 cx.update_editor(|editor, window, cx| {
15732 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
15733 });
15734 executor.run_until_parked();
15735 cx.assert_index_text(None);
15736}
15737
15738async fn setup_indent_guides_editor(
15739 text: &str,
15740 cx: &mut TestAppContext,
15741) -> (BufferId, EditorTestContext) {
15742 init_test(cx, |_| {});
15743
15744 let mut cx = EditorTestContext::new(cx).await;
15745
15746 let buffer_id = cx.update_editor(|editor, window, cx| {
15747 editor.set_text(text, window, cx);
15748 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
15749
15750 buffer_ids[0]
15751 });
15752
15753 (buffer_id, cx)
15754}
15755
15756fn assert_indent_guides(
15757 range: Range<u32>,
15758 expected: Vec<IndentGuide>,
15759 active_indices: Option<Vec<usize>>,
15760 cx: &mut EditorTestContext,
15761) {
15762 let indent_guides = cx.update_editor(|editor, window, cx| {
15763 let snapshot = editor.snapshot(window, cx).display_snapshot;
15764 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
15765 editor,
15766 MultiBufferRow(range.start)..MultiBufferRow(range.end),
15767 true,
15768 &snapshot,
15769 cx,
15770 );
15771
15772 indent_guides.sort_by(|a, b| {
15773 a.depth.cmp(&b.depth).then(
15774 a.start_row
15775 .cmp(&b.start_row)
15776 .then(a.end_row.cmp(&b.end_row)),
15777 )
15778 });
15779 indent_guides
15780 });
15781
15782 if let Some(expected) = active_indices {
15783 let active_indices = cx.update_editor(|editor, window, cx| {
15784 let snapshot = editor.snapshot(window, cx).display_snapshot;
15785 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
15786 });
15787
15788 assert_eq!(
15789 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
15790 expected,
15791 "Active indent guide indices do not match"
15792 );
15793 }
15794
15795 assert_eq!(indent_guides, expected, "Indent guides do not match");
15796}
15797
15798fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
15799 IndentGuide {
15800 buffer_id,
15801 start_row: MultiBufferRow(start_row),
15802 end_row: MultiBufferRow(end_row),
15803 depth,
15804 tab_size: 4,
15805 settings: IndentGuideSettings {
15806 enabled: true,
15807 line_width: 1,
15808 active_line_width: 1,
15809 ..Default::default()
15810 },
15811 }
15812}
15813
15814#[gpui::test]
15815async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
15816 let (buffer_id, mut cx) = setup_indent_guides_editor(
15817 &"
15818 fn main() {
15819 let a = 1;
15820 }"
15821 .unindent(),
15822 cx,
15823 )
15824 .await;
15825
15826 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
15827}
15828
15829#[gpui::test]
15830async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
15831 let (buffer_id, mut cx) = setup_indent_guides_editor(
15832 &"
15833 fn main() {
15834 let a = 1;
15835 let b = 2;
15836 }"
15837 .unindent(),
15838 cx,
15839 )
15840 .await;
15841
15842 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
15843}
15844
15845#[gpui::test]
15846async fn test_indent_guide_nested(cx: &mut TestAppContext) {
15847 let (buffer_id, mut cx) = setup_indent_guides_editor(
15848 &"
15849 fn main() {
15850 let a = 1;
15851 if a == 3 {
15852 let b = 2;
15853 } else {
15854 let c = 3;
15855 }
15856 }"
15857 .unindent(),
15858 cx,
15859 )
15860 .await;
15861
15862 assert_indent_guides(
15863 0..8,
15864 vec![
15865 indent_guide(buffer_id, 1, 6, 0),
15866 indent_guide(buffer_id, 3, 3, 1),
15867 indent_guide(buffer_id, 5, 5, 1),
15868 ],
15869 None,
15870 &mut cx,
15871 );
15872}
15873
15874#[gpui::test]
15875async fn test_indent_guide_tab(cx: &mut TestAppContext) {
15876 let (buffer_id, mut cx) = setup_indent_guides_editor(
15877 &"
15878 fn main() {
15879 let a = 1;
15880 let b = 2;
15881 let c = 3;
15882 }"
15883 .unindent(),
15884 cx,
15885 )
15886 .await;
15887
15888 assert_indent_guides(
15889 0..5,
15890 vec![
15891 indent_guide(buffer_id, 1, 3, 0),
15892 indent_guide(buffer_id, 2, 2, 1),
15893 ],
15894 None,
15895 &mut cx,
15896 );
15897}
15898
15899#[gpui::test]
15900async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
15901 let (buffer_id, mut cx) = setup_indent_guides_editor(
15902 &"
15903 fn main() {
15904 let a = 1;
15905
15906 let c = 3;
15907 }"
15908 .unindent(),
15909 cx,
15910 )
15911 .await;
15912
15913 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
15914}
15915
15916#[gpui::test]
15917async fn test_indent_guide_complex(cx: &mut TestAppContext) {
15918 let (buffer_id, mut cx) = setup_indent_guides_editor(
15919 &"
15920 fn main() {
15921 let a = 1;
15922
15923 let c = 3;
15924
15925 if a == 3 {
15926 let b = 2;
15927 } else {
15928 let c = 3;
15929 }
15930 }"
15931 .unindent(),
15932 cx,
15933 )
15934 .await;
15935
15936 assert_indent_guides(
15937 0..11,
15938 vec![
15939 indent_guide(buffer_id, 1, 9, 0),
15940 indent_guide(buffer_id, 6, 6, 1),
15941 indent_guide(buffer_id, 8, 8, 1),
15942 ],
15943 None,
15944 &mut cx,
15945 );
15946}
15947
15948#[gpui::test]
15949async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
15950 let (buffer_id, mut cx) = setup_indent_guides_editor(
15951 &"
15952 fn main() {
15953 let a = 1;
15954
15955 let c = 3;
15956
15957 if a == 3 {
15958 let b = 2;
15959 } else {
15960 let c = 3;
15961 }
15962 }"
15963 .unindent(),
15964 cx,
15965 )
15966 .await;
15967
15968 assert_indent_guides(
15969 1..11,
15970 vec![
15971 indent_guide(buffer_id, 1, 9, 0),
15972 indent_guide(buffer_id, 6, 6, 1),
15973 indent_guide(buffer_id, 8, 8, 1),
15974 ],
15975 None,
15976 &mut cx,
15977 );
15978}
15979
15980#[gpui::test]
15981async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
15982 let (buffer_id, mut cx) = setup_indent_guides_editor(
15983 &"
15984 fn main() {
15985 let a = 1;
15986
15987 let c = 3;
15988
15989 if a == 3 {
15990 let b = 2;
15991 } else {
15992 let c = 3;
15993 }
15994 }"
15995 .unindent(),
15996 cx,
15997 )
15998 .await;
15999
16000 assert_indent_guides(
16001 1..10,
16002 vec![
16003 indent_guide(buffer_id, 1, 9, 0),
16004 indent_guide(buffer_id, 6, 6, 1),
16005 indent_guide(buffer_id, 8, 8, 1),
16006 ],
16007 None,
16008 &mut cx,
16009 );
16010}
16011
16012#[gpui::test]
16013async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
16014 let (buffer_id, mut cx) = setup_indent_guides_editor(
16015 &"
16016 block1
16017 block2
16018 block3
16019 block4
16020 block2
16021 block1
16022 block1"
16023 .unindent(),
16024 cx,
16025 )
16026 .await;
16027
16028 assert_indent_guides(
16029 1..10,
16030 vec![
16031 indent_guide(buffer_id, 1, 4, 0),
16032 indent_guide(buffer_id, 2, 3, 1),
16033 indent_guide(buffer_id, 3, 3, 2),
16034 ],
16035 None,
16036 &mut cx,
16037 );
16038}
16039
16040#[gpui::test]
16041async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
16042 let (buffer_id, mut cx) = setup_indent_guides_editor(
16043 &"
16044 block1
16045 block2
16046 block3
16047
16048 block1
16049 block1"
16050 .unindent(),
16051 cx,
16052 )
16053 .await;
16054
16055 assert_indent_guides(
16056 0..6,
16057 vec![
16058 indent_guide(buffer_id, 1, 2, 0),
16059 indent_guide(buffer_id, 2, 2, 1),
16060 ],
16061 None,
16062 &mut cx,
16063 );
16064}
16065
16066#[gpui::test]
16067async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
16068 let (buffer_id, mut cx) = setup_indent_guides_editor(
16069 &"
16070 block1
16071
16072
16073
16074 block2
16075 "
16076 .unindent(),
16077 cx,
16078 )
16079 .await;
16080
16081 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
16082}
16083
16084#[gpui::test]
16085async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
16086 let (buffer_id, mut cx) = setup_indent_guides_editor(
16087 &"
16088 def a:
16089 \tb = 3
16090 \tif True:
16091 \t\tc = 4
16092 \t\td = 5
16093 \tprint(b)
16094 "
16095 .unindent(),
16096 cx,
16097 )
16098 .await;
16099
16100 assert_indent_guides(
16101 0..6,
16102 vec![
16103 indent_guide(buffer_id, 1, 6, 0),
16104 indent_guide(buffer_id, 3, 4, 1),
16105 ],
16106 None,
16107 &mut cx,
16108 );
16109}
16110
16111#[gpui::test]
16112async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
16113 let (buffer_id, mut cx) = setup_indent_guides_editor(
16114 &"
16115 fn main() {
16116 let a = 1;
16117 }"
16118 .unindent(),
16119 cx,
16120 )
16121 .await;
16122
16123 cx.update_editor(|editor, window, cx| {
16124 editor.change_selections(None, window, cx, |s| {
16125 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16126 });
16127 });
16128
16129 assert_indent_guides(
16130 0..3,
16131 vec![indent_guide(buffer_id, 1, 1, 0)],
16132 Some(vec![0]),
16133 &mut cx,
16134 );
16135}
16136
16137#[gpui::test]
16138async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
16139 let (buffer_id, mut cx) = setup_indent_guides_editor(
16140 &"
16141 fn main() {
16142 if 1 == 2 {
16143 let a = 1;
16144 }
16145 }"
16146 .unindent(),
16147 cx,
16148 )
16149 .await;
16150
16151 cx.update_editor(|editor, window, cx| {
16152 editor.change_selections(None, window, cx, |s| {
16153 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16154 });
16155 });
16156
16157 assert_indent_guides(
16158 0..4,
16159 vec![
16160 indent_guide(buffer_id, 1, 3, 0),
16161 indent_guide(buffer_id, 2, 2, 1),
16162 ],
16163 Some(vec![1]),
16164 &mut cx,
16165 );
16166
16167 cx.update_editor(|editor, window, cx| {
16168 editor.change_selections(None, window, cx, |s| {
16169 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
16170 });
16171 });
16172
16173 assert_indent_guides(
16174 0..4,
16175 vec![
16176 indent_guide(buffer_id, 1, 3, 0),
16177 indent_guide(buffer_id, 2, 2, 1),
16178 ],
16179 Some(vec![1]),
16180 &mut cx,
16181 );
16182
16183 cx.update_editor(|editor, window, cx| {
16184 editor.change_selections(None, window, cx, |s| {
16185 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
16186 });
16187 });
16188
16189 assert_indent_guides(
16190 0..4,
16191 vec![
16192 indent_guide(buffer_id, 1, 3, 0),
16193 indent_guide(buffer_id, 2, 2, 1),
16194 ],
16195 Some(vec![0]),
16196 &mut cx,
16197 );
16198}
16199
16200#[gpui::test]
16201async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
16202 let (buffer_id, mut cx) = setup_indent_guides_editor(
16203 &"
16204 fn main() {
16205 let a = 1;
16206
16207 let b = 2;
16208 }"
16209 .unindent(),
16210 cx,
16211 )
16212 .await;
16213
16214 cx.update_editor(|editor, window, cx| {
16215 editor.change_selections(None, window, cx, |s| {
16216 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
16217 });
16218 });
16219
16220 assert_indent_guides(
16221 0..5,
16222 vec![indent_guide(buffer_id, 1, 3, 0)],
16223 Some(vec![0]),
16224 &mut cx,
16225 );
16226}
16227
16228#[gpui::test]
16229async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
16230 let (buffer_id, mut cx) = setup_indent_guides_editor(
16231 &"
16232 def m:
16233 a = 1
16234 pass"
16235 .unindent(),
16236 cx,
16237 )
16238 .await;
16239
16240 cx.update_editor(|editor, window, cx| {
16241 editor.change_selections(None, window, cx, |s| {
16242 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16243 });
16244 });
16245
16246 assert_indent_guides(
16247 0..3,
16248 vec![indent_guide(buffer_id, 1, 2, 0)],
16249 Some(vec![0]),
16250 &mut cx,
16251 );
16252}
16253
16254#[gpui::test]
16255async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
16256 init_test(cx, |_| {});
16257 let mut cx = EditorTestContext::new(cx).await;
16258 let text = indoc! {
16259 "
16260 impl A {
16261 fn b() {
16262 0;
16263 3;
16264 5;
16265 6;
16266 7;
16267 }
16268 }
16269 "
16270 };
16271 let base_text = indoc! {
16272 "
16273 impl A {
16274 fn b() {
16275 0;
16276 1;
16277 2;
16278 3;
16279 4;
16280 }
16281 fn c() {
16282 5;
16283 6;
16284 7;
16285 }
16286 }
16287 "
16288 };
16289
16290 cx.update_editor(|editor, window, cx| {
16291 editor.set_text(text, window, cx);
16292
16293 editor.buffer().update(cx, |multibuffer, cx| {
16294 let buffer = multibuffer.as_singleton().unwrap();
16295 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
16296
16297 multibuffer.set_all_diff_hunks_expanded(cx);
16298 multibuffer.add_diff(diff, cx);
16299
16300 buffer.read(cx).remote_id()
16301 })
16302 });
16303 cx.run_until_parked();
16304
16305 cx.assert_state_with_diff(
16306 indoc! { "
16307 impl A {
16308 fn b() {
16309 0;
16310 - 1;
16311 - 2;
16312 3;
16313 - 4;
16314 - }
16315 - fn c() {
16316 5;
16317 6;
16318 7;
16319 }
16320 }
16321 ˇ"
16322 }
16323 .to_string(),
16324 );
16325
16326 let mut actual_guides = cx.update_editor(|editor, window, cx| {
16327 editor
16328 .snapshot(window, cx)
16329 .buffer_snapshot
16330 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
16331 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
16332 .collect::<Vec<_>>()
16333 });
16334 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
16335 assert_eq!(
16336 actual_guides,
16337 vec![
16338 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
16339 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
16340 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
16341 ]
16342 );
16343}
16344
16345#[gpui::test]
16346async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16347 init_test(cx, |_| {});
16348 let mut cx = EditorTestContext::new(cx).await;
16349
16350 let diff_base = r#"
16351 a
16352 b
16353 c
16354 "#
16355 .unindent();
16356
16357 cx.set_state(
16358 &r#"
16359 ˇA
16360 b
16361 C
16362 "#
16363 .unindent(),
16364 );
16365 cx.set_head_text(&diff_base);
16366 cx.update_editor(|editor, window, cx| {
16367 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16368 });
16369 executor.run_until_parked();
16370
16371 let both_hunks_expanded = r#"
16372 - a
16373 + ˇA
16374 b
16375 - c
16376 + C
16377 "#
16378 .unindent();
16379
16380 cx.assert_state_with_diff(both_hunks_expanded.clone());
16381
16382 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16383 let snapshot = editor.snapshot(window, cx);
16384 let hunks = editor
16385 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16386 .collect::<Vec<_>>();
16387 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16388 let buffer_id = hunks[0].buffer_id;
16389 hunks
16390 .into_iter()
16391 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16392 .collect::<Vec<_>>()
16393 });
16394 assert_eq!(hunk_ranges.len(), 2);
16395
16396 cx.update_editor(|editor, _, cx| {
16397 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16398 });
16399 executor.run_until_parked();
16400
16401 let second_hunk_expanded = r#"
16402 ˇA
16403 b
16404 - c
16405 + C
16406 "#
16407 .unindent();
16408
16409 cx.assert_state_with_diff(second_hunk_expanded);
16410
16411 cx.update_editor(|editor, _, cx| {
16412 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16413 });
16414 executor.run_until_parked();
16415
16416 cx.assert_state_with_diff(both_hunks_expanded.clone());
16417
16418 cx.update_editor(|editor, _, cx| {
16419 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
16420 });
16421 executor.run_until_parked();
16422
16423 let first_hunk_expanded = r#"
16424 - a
16425 + ˇA
16426 b
16427 C
16428 "#
16429 .unindent();
16430
16431 cx.assert_state_with_diff(first_hunk_expanded);
16432
16433 cx.update_editor(|editor, _, cx| {
16434 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
16435 });
16436 executor.run_until_parked();
16437
16438 cx.assert_state_with_diff(both_hunks_expanded);
16439
16440 cx.set_state(
16441 &r#"
16442 ˇA
16443 b
16444 "#
16445 .unindent(),
16446 );
16447 cx.run_until_parked();
16448
16449 // TODO this cursor position seems bad
16450 cx.assert_state_with_diff(
16451 r#"
16452 - ˇa
16453 + A
16454 b
16455 "#
16456 .unindent(),
16457 );
16458
16459 cx.update_editor(|editor, window, cx| {
16460 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16461 });
16462
16463 cx.assert_state_with_diff(
16464 r#"
16465 - ˇa
16466 + A
16467 b
16468 - c
16469 "#
16470 .unindent(),
16471 );
16472
16473 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16474 let snapshot = editor.snapshot(window, cx);
16475 let hunks = editor
16476 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16477 .collect::<Vec<_>>();
16478 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16479 let buffer_id = hunks[0].buffer_id;
16480 hunks
16481 .into_iter()
16482 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16483 .collect::<Vec<_>>()
16484 });
16485 assert_eq!(hunk_ranges.len(), 2);
16486
16487 cx.update_editor(|editor, _, cx| {
16488 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
16489 });
16490 executor.run_until_parked();
16491
16492 cx.assert_state_with_diff(
16493 r#"
16494 - ˇa
16495 + A
16496 b
16497 "#
16498 .unindent(),
16499 );
16500}
16501
16502#[gpui::test]
16503async fn test_toggle_deletion_hunk_at_start_of_file(
16504 executor: BackgroundExecutor,
16505 cx: &mut TestAppContext,
16506) {
16507 init_test(cx, |_| {});
16508 let mut cx = EditorTestContext::new(cx).await;
16509
16510 let diff_base = r#"
16511 a
16512 b
16513 c
16514 "#
16515 .unindent();
16516
16517 cx.set_state(
16518 &r#"
16519 ˇb
16520 c
16521 "#
16522 .unindent(),
16523 );
16524 cx.set_head_text(&diff_base);
16525 cx.update_editor(|editor, window, cx| {
16526 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16527 });
16528 executor.run_until_parked();
16529
16530 let hunk_expanded = r#"
16531 - a
16532 ˇb
16533 c
16534 "#
16535 .unindent();
16536
16537 cx.assert_state_with_diff(hunk_expanded.clone());
16538
16539 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16540 let snapshot = editor.snapshot(window, cx);
16541 let hunks = editor
16542 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16543 .collect::<Vec<_>>();
16544 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16545 let buffer_id = hunks[0].buffer_id;
16546 hunks
16547 .into_iter()
16548 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16549 .collect::<Vec<_>>()
16550 });
16551 assert_eq!(hunk_ranges.len(), 1);
16552
16553 cx.update_editor(|editor, _, cx| {
16554 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16555 });
16556 executor.run_until_parked();
16557
16558 let hunk_collapsed = r#"
16559 ˇb
16560 c
16561 "#
16562 .unindent();
16563
16564 cx.assert_state_with_diff(hunk_collapsed);
16565
16566 cx.update_editor(|editor, _, cx| {
16567 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16568 });
16569 executor.run_until_parked();
16570
16571 cx.assert_state_with_diff(hunk_expanded.clone());
16572}
16573
16574#[gpui::test]
16575async fn test_display_diff_hunks(cx: &mut TestAppContext) {
16576 init_test(cx, |_| {});
16577
16578 let fs = FakeFs::new(cx.executor());
16579 fs.insert_tree(
16580 path!("/test"),
16581 json!({
16582 ".git": {},
16583 "file-1": "ONE\n",
16584 "file-2": "TWO\n",
16585 "file-3": "THREE\n",
16586 }),
16587 )
16588 .await;
16589
16590 fs.set_head_for_repo(
16591 path!("/test/.git").as_ref(),
16592 &[
16593 ("file-1".into(), "one\n".into()),
16594 ("file-2".into(), "two\n".into()),
16595 ("file-3".into(), "three\n".into()),
16596 ],
16597 );
16598
16599 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
16600 let mut buffers = vec![];
16601 for i in 1..=3 {
16602 let buffer = project
16603 .update(cx, |project, cx| {
16604 let path = format!(path!("/test/file-{}"), i);
16605 project.open_local_buffer(path, cx)
16606 })
16607 .await
16608 .unwrap();
16609 buffers.push(buffer);
16610 }
16611
16612 let multibuffer = cx.new(|cx| {
16613 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
16614 multibuffer.set_all_diff_hunks_expanded(cx);
16615 for buffer in &buffers {
16616 let snapshot = buffer.read(cx).snapshot();
16617 multibuffer.set_excerpts_for_path(
16618 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
16619 buffer.clone(),
16620 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
16621 DEFAULT_MULTIBUFFER_CONTEXT,
16622 cx,
16623 );
16624 }
16625 multibuffer
16626 });
16627
16628 let editor = cx.add_window(|window, cx| {
16629 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
16630 });
16631 cx.run_until_parked();
16632
16633 let snapshot = editor
16634 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16635 .unwrap();
16636 let hunks = snapshot
16637 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
16638 .map(|hunk| match hunk {
16639 DisplayDiffHunk::Unfolded {
16640 display_row_range, ..
16641 } => display_row_range,
16642 DisplayDiffHunk::Folded { .. } => unreachable!(),
16643 })
16644 .collect::<Vec<_>>();
16645 assert_eq!(
16646 hunks,
16647 [
16648 DisplayRow(2)..DisplayRow(4),
16649 DisplayRow(7)..DisplayRow(9),
16650 DisplayRow(12)..DisplayRow(14),
16651 ]
16652 );
16653}
16654
16655#[gpui::test]
16656async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
16657 init_test(cx, |_| {});
16658
16659 let mut cx = EditorTestContext::new(cx).await;
16660 cx.set_head_text(indoc! { "
16661 one
16662 two
16663 three
16664 four
16665 five
16666 "
16667 });
16668 cx.set_index_text(indoc! { "
16669 one
16670 two
16671 three
16672 four
16673 five
16674 "
16675 });
16676 cx.set_state(indoc! {"
16677 one
16678 TWO
16679 ˇTHREE
16680 FOUR
16681 five
16682 "});
16683 cx.run_until_parked();
16684 cx.update_editor(|editor, window, cx| {
16685 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16686 });
16687 cx.run_until_parked();
16688 cx.assert_index_text(Some(indoc! {"
16689 one
16690 TWO
16691 THREE
16692 FOUR
16693 five
16694 "}));
16695 cx.set_state(indoc! { "
16696 one
16697 TWO
16698 ˇTHREE-HUNDRED
16699 FOUR
16700 five
16701 "});
16702 cx.run_until_parked();
16703 cx.update_editor(|editor, window, cx| {
16704 let snapshot = editor.snapshot(window, cx);
16705 let hunks = editor
16706 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16707 .collect::<Vec<_>>();
16708 assert_eq!(hunks.len(), 1);
16709 assert_eq!(
16710 hunks[0].status(),
16711 DiffHunkStatus {
16712 kind: DiffHunkStatusKind::Modified,
16713 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
16714 }
16715 );
16716
16717 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16718 });
16719 cx.run_until_parked();
16720 cx.assert_index_text(Some(indoc! {"
16721 one
16722 TWO
16723 THREE-HUNDRED
16724 FOUR
16725 five
16726 "}));
16727}
16728
16729#[gpui::test]
16730fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
16731 init_test(cx, |_| {});
16732
16733 let editor = cx.add_window(|window, cx| {
16734 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
16735 build_editor(buffer, window, cx)
16736 });
16737
16738 let render_args = Arc::new(Mutex::new(None));
16739 let snapshot = editor
16740 .update(cx, |editor, window, cx| {
16741 let snapshot = editor.buffer().read(cx).snapshot(cx);
16742 let range =
16743 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
16744
16745 struct RenderArgs {
16746 row: MultiBufferRow,
16747 folded: bool,
16748 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
16749 }
16750
16751 let crease = Crease::inline(
16752 range,
16753 FoldPlaceholder::test(),
16754 {
16755 let toggle_callback = render_args.clone();
16756 move |row, folded, callback, _window, _cx| {
16757 *toggle_callback.lock() = Some(RenderArgs {
16758 row,
16759 folded,
16760 callback,
16761 });
16762 div()
16763 }
16764 },
16765 |_row, _folded, _window, _cx| div(),
16766 );
16767
16768 editor.insert_creases(Some(crease), cx);
16769 let snapshot = editor.snapshot(window, cx);
16770 let _div = snapshot.render_crease_toggle(
16771 MultiBufferRow(1),
16772 false,
16773 cx.entity().clone(),
16774 window,
16775 cx,
16776 );
16777 snapshot
16778 })
16779 .unwrap();
16780
16781 let render_args = render_args.lock().take().unwrap();
16782 assert_eq!(render_args.row, MultiBufferRow(1));
16783 assert!(!render_args.folded);
16784 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
16785
16786 cx.update_window(*editor, |_, window, cx| {
16787 (render_args.callback)(true, window, cx)
16788 })
16789 .unwrap();
16790 let snapshot = editor
16791 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16792 .unwrap();
16793 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
16794
16795 cx.update_window(*editor, |_, window, cx| {
16796 (render_args.callback)(false, window, cx)
16797 })
16798 .unwrap();
16799 let snapshot = editor
16800 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16801 .unwrap();
16802 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
16803}
16804
16805#[gpui::test]
16806async fn test_input_text(cx: &mut TestAppContext) {
16807 init_test(cx, |_| {});
16808 let mut cx = EditorTestContext::new(cx).await;
16809
16810 cx.set_state(
16811 &r#"ˇone
16812 two
16813
16814 three
16815 fourˇ
16816 five
16817
16818 siˇx"#
16819 .unindent(),
16820 );
16821
16822 cx.dispatch_action(HandleInput(String::new()));
16823 cx.assert_editor_state(
16824 &r#"ˇone
16825 two
16826
16827 three
16828 fourˇ
16829 five
16830
16831 siˇx"#
16832 .unindent(),
16833 );
16834
16835 cx.dispatch_action(HandleInput("AAAA".to_string()));
16836 cx.assert_editor_state(
16837 &r#"AAAAˇone
16838 two
16839
16840 three
16841 fourAAAAˇ
16842 five
16843
16844 siAAAAˇx"#
16845 .unindent(),
16846 );
16847}
16848
16849#[gpui::test]
16850async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
16851 init_test(cx, |_| {});
16852
16853 let mut cx = EditorTestContext::new(cx).await;
16854 cx.set_state(
16855 r#"let foo = 1;
16856let foo = 2;
16857let foo = 3;
16858let fooˇ = 4;
16859let foo = 5;
16860let foo = 6;
16861let foo = 7;
16862let foo = 8;
16863let foo = 9;
16864let foo = 10;
16865let foo = 11;
16866let foo = 12;
16867let foo = 13;
16868let foo = 14;
16869let foo = 15;"#,
16870 );
16871
16872 cx.update_editor(|e, window, cx| {
16873 assert_eq!(
16874 e.next_scroll_position,
16875 NextScrollCursorCenterTopBottom::Center,
16876 "Default next scroll direction is center",
16877 );
16878
16879 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16880 assert_eq!(
16881 e.next_scroll_position,
16882 NextScrollCursorCenterTopBottom::Top,
16883 "After center, next scroll direction should be top",
16884 );
16885
16886 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16887 assert_eq!(
16888 e.next_scroll_position,
16889 NextScrollCursorCenterTopBottom::Bottom,
16890 "After top, next scroll direction should be bottom",
16891 );
16892
16893 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16894 assert_eq!(
16895 e.next_scroll_position,
16896 NextScrollCursorCenterTopBottom::Center,
16897 "After bottom, scrolling should start over",
16898 );
16899
16900 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16901 assert_eq!(
16902 e.next_scroll_position,
16903 NextScrollCursorCenterTopBottom::Top,
16904 "Scrolling continues if retriggered fast enough"
16905 );
16906 });
16907
16908 cx.executor()
16909 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
16910 cx.executor().run_until_parked();
16911 cx.update_editor(|e, _, _| {
16912 assert_eq!(
16913 e.next_scroll_position,
16914 NextScrollCursorCenterTopBottom::Center,
16915 "If scrolling is not triggered fast enough, it should reset"
16916 );
16917 });
16918}
16919
16920#[gpui::test]
16921async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
16922 init_test(cx, |_| {});
16923 let mut cx = EditorLspTestContext::new_rust(
16924 lsp::ServerCapabilities {
16925 definition_provider: Some(lsp::OneOf::Left(true)),
16926 references_provider: Some(lsp::OneOf::Left(true)),
16927 ..lsp::ServerCapabilities::default()
16928 },
16929 cx,
16930 )
16931 .await;
16932
16933 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
16934 let go_to_definition = cx
16935 .lsp
16936 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
16937 move |params, _| async move {
16938 if empty_go_to_definition {
16939 Ok(None)
16940 } else {
16941 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
16942 uri: params.text_document_position_params.text_document.uri,
16943 range: lsp::Range::new(
16944 lsp::Position::new(4, 3),
16945 lsp::Position::new(4, 6),
16946 ),
16947 })))
16948 }
16949 },
16950 );
16951 let references = cx
16952 .lsp
16953 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
16954 Ok(Some(vec![lsp::Location {
16955 uri: params.text_document_position.text_document.uri,
16956 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
16957 }]))
16958 });
16959 (go_to_definition, references)
16960 };
16961
16962 cx.set_state(
16963 &r#"fn one() {
16964 let mut a = ˇtwo();
16965 }
16966
16967 fn two() {}"#
16968 .unindent(),
16969 );
16970 set_up_lsp_handlers(false, &mut cx);
16971 let navigated = cx
16972 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
16973 .await
16974 .expect("Failed to navigate to definition");
16975 assert_eq!(
16976 navigated,
16977 Navigated::Yes,
16978 "Should have navigated to definition from the GetDefinition response"
16979 );
16980 cx.assert_editor_state(
16981 &r#"fn one() {
16982 let mut a = two();
16983 }
16984
16985 fn «twoˇ»() {}"#
16986 .unindent(),
16987 );
16988
16989 let editors = cx.update_workspace(|workspace, _, cx| {
16990 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
16991 });
16992 cx.update_editor(|_, _, test_editor_cx| {
16993 assert_eq!(
16994 editors.len(),
16995 1,
16996 "Initially, only one, test, editor should be open in the workspace"
16997 );
16998 assert_eq!(
16999 test_editor_cx.entity(),
17000 editors.last().expect("Asserted len is 1").clone()
17001 );
17002 });
17003
17004 set_up_lsp_handlers(true, &mut cx);
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 assert_eq!(
17010 navigated,
17011 Navigated::Yes,
17012 "Should have navigated to references as a fallback after empty GoToDefinition response"
17013 );
17014 // We should not change the selections in the existing file,
17015 // if opening another milti buffer with the references
17016 cx.assert_editor_state(
17017 &r#"fn one() {
17018 let mut a = two();
17019 }
17020
17021 fn «twoˇ»() {}"#
17022 .unindent(),
17023 );
17024 let editors = cx.update_workspace(|workspace, _, cx| {
17025 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17026 });
17027 cx.update_editor(|_, _, test_editor_cx| {
17028 assert_eq!(
17029 editors.len(),
17030 2,
17031 "After falling back to references search, we open a new editor with the results"
17032 );
17033 let references_fallback_text = editors
17034 .into_iter()
17035 .find(|new_editor| *new_editor != test_editor_cx.entity())
17036 .expect("Should have one non-test editor now")
17037 .read(test_editor_cx)
17038 .text(test_editor_cx);
17039 assert_eq!(
17040 references_fallback_text, "fn one() {\n let mut a = two();\n}",
17041 "Should use the range from the references response and not the GoToDefinition one"
17042 );
17043 });
17044}
17045
17046#[gpui::test]
17047async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
17048 init_test(cx, |_| {});
17049 cx.update(|cx| {
17050 let mut editor_settings = EditorSettings::get_global(cx).clone();
17051 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
17052 EditorSettings::override_global(editor_settings, cx);
17053 });
17054 let mut cx = EditorLspTestContext::new_rust(
17055 lsp::ServerCapabilities {
17056 definition_provider: Some(lsp::OneOf::Left(true)),
17057 references_provider: Some(lsp::OneOf::Left(true)),
17058 ..lsp::ServerCapabilities::default()
17059 },
17060 cx,
17061 )
17062 .await;
17063 let original_state = r#"fn one() {
17064 let mut a = ˇtwo();
17065 }
17066
17067 fn two() {}"#
17068 .unindent();
17069 cx.set_state(&original_state);
17070
17071 let mut go_to_definition = cx
17072 .lsp
17073 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
17074 move |_, _| async move { Ok(None) },
17075 );
17076 let _references = cx
17077 .lsp
17078 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
17079 panic!("Should not call for references with no go to definition fallback")
17080 });
17081
17082 let navigated = cx
17083 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17084 .await
17085 .expect("Failed to navigate to lookup references");
17086 go_to_definition
17087 .next()
17088 .await
17089 .expect("Should have called the go_to_definition handler");
17090
17091 assert_eq!(
17092 navigated,
17093 Navigated::No,
17094 "Should have navigated to references as a fallback after empty GoToDefinition response"
17095 );
17096 cx.assert_editor_state(&original_state);
17097 let editors = cx.update_workspace(|workspace, _, cx| {
17098 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17099 });
17100 cx.update_editor(|_, _, _| {
17101 assert_eq!(
17102 editors.len(),
17103 1,
17104 "After unsuccessful fallback, no other editor should have been opened"
17105 );
17106 });
17107}
17108
17109#[gpui::test]
17110async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
17111 init_test(cx, |_| {});
17112
17113 let language = Arc::new(Language::new(
17114 LanguageConfig::default(),
17115 Some(tree_sitter_rust::LANGUAGE.into()),
17116 ));
17117
17118 let text = r#"
17119 #[cfg(test)]
17120 mod tests() {
17121 #[test]
17122 fn runnable_1() {
17123 let a = 1;
17124 }
17125
17126 #[test]
17127 fn runnable_2() {
17128 let a = 1;
17129 let b = 2;
17130 }
17131 }
17132 "#
17133 .unindent();
17134
17135 let fs = FakeFs::new(cx.executor());
17136 fs.insert_file("/file.rs", Default::default()).await;
17137
17138 let project = Project::test(fs, ["/a".as_ref()], cx).await;
17139 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17140 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17141 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
17142 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
17143
17144 let editor = cx.new_window_entity(|window, cx| {
17145 Editor::new(
17146 EditorMode::full(),
17147 multi_buffer,
17148 Some(project.clone()),
17149 window,
17150 cx,
17151 )
17152 });
17153
17154 editor.update_in(cx, |editor, window, cx| {
17155 let snapshot = editor.buffer().read(cx).snapshot(cx);
17156 editor.tasks.insert(
17157 (buffer.read(cx).remote_id(), 3),
17158 RunnableTasks {
17159 templates: vec![],
17160 offset: snapshot.anchor_before(43),
17161 column: 0,
17162 extra_variables: HashMap::default(),
17163 context_range: BufferOffset(43)..BufferOffset(85),
17164 },
17165 );
17166 editor.tasks.insert(
17167 (buffer.read(cx).remote_id(), 8),
17168 RunnableTasks {
17169 templates: vec![],
17170 offset: snapshot.anchor_before(86),
17171 column: 0,
17172 extra_variables: HashMap::default(),
17173 context_range: BufferOffset(86)..BufferOffset(191),
17174 },
17175 );
17176
17177 // Test finding task when cursor is inside function body
17178 editor.change_selections(None, window, cx, |s| {
17179 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
17180 });
17181 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
17182 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
17183
17184 // Test finding task when cursor is on function name
17185 editor.change_selections(None, window, cx, |s| {
17186 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
17187 });
17188 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
17189 assert_eq!(row, 8, "Should find task when cursor is on function name");
17190 });
17191}
17192
17193#[gpui::test]
17194async fn test_folding_buffers(cx: &mut TestAppContext) {
17195 init_test(cx, |_| {});
17196
17197 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
17198 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
17199 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
17200
17201 let fs = FakeFs::new(cx.executor());
17202 fs.insert_tree(
17203 path!("/a"),
17204 json!({
17205 "first.rs": sample_text_1,
17206 "second.rs": sample_text_2,
17207 "third.rs": sample_text_3,
17208 }),
17209 )
17210 .await;
17211 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17212 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17213 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17214 let worktree = project.update(cx, |project, cx| {
17215 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17216 assert_eq!(worktrees.len(), 1);
17217 worktrees.pop().unwrap()
17218 });
17219 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17220
17221 let buffer_1 = project
17222 .update(cx, |project, cx| {
17223 project.open_buffer((worktree_id, "first.rs"), cx)
17224 })
17225 .await
17226 .unwrap();
17227 let buffer_2 = project
17228 .update(cx, |project, cx| {
17229 project.open_buffer((worktree_id, "second.rs"), cx)
17230 })
17231 .await
17232 .unwrap();
17233 let buffer_3 = project
17234 .update(cx, |project, cx| {
17235 project.open_buffer((worktree_id, "third.rs"), cx)
17236 })
17237 .await
17238 .unwrap();
17239
17240 let multi_buffer = cx.new(|cx| {
17241 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17242 multi_buffer.push_excerpts(
17243 buffer_1.clone(),
17244 [
17245 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17246 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17247 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17248 ],
17249 cx,
17250 );
17251 multi_buffer.push_excerpts(
17252 buffer_2.clone(),
17253 [
17254 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17255 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17256 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17257 ],
17258 cx,
17259 );
17260 multi_buffer.push_excerpts(
17261 buffer_3.clone(),
17262 [
17263 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17264 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17265 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17266 ],
17267 cx,
17268 );
17269 multi_buffer
17270 });
17271 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17272 Editor::new(
17273 EditorMode::full(),
17274 multi_buffer.clone(),
17275 Some(project.clone()),
17276 window,
17277 cx,
17278 )
17279 });
17280
17281 assert_eq!(
17282 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17283 "\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",
17284 );
17285
17286 multi_buffer_editor.update(cx, |editor, cx| {
17287 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
17288 });
17289 assert_eq!(
17290 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17291 "\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",
17292 "After folding the first buffer, its text should not be displayed"
17293 );
17294
17295 multi_buffer_editor.update(cx, |editor, cx| {
17296 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
17297 });
17298 assert_eq!(
17299 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17300 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
17301 "After folding the second buffer, its text should not be displayed"
17302 );
17303
17304 multi_buffer_editor.update(cx, |editor, cx| {
17305 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
17306 });
17307 assert_eq!(
17308 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17309 "\n\n\n\n\n",
17310 "After folding the third buffer, its text should not be displayed"
17311 );
17312
17313 // Emulate selection inside the fold logic, that should work
17314 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17315 editor
17316 .snapshot(window, cx)
17317 .next_line_boundary(Point::new(0, 4));
17318 });
17319
17320 multi_buffer_editor.update(cx, |editor, cx| {
17321 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
17322 });
17323 assert_eq!(
17324 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17325 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
17326 "After unfolding the second buffer, its text should be displayed"
17327 );
17328
17329 // Typing inside of buffer 1 causes that buffer to be unfolded.
17330 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17331 assert_eq!(
17332 multi_buffer
17333 .read(cx)
17334 .snapshot(cx)
17335 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
17336 .collect::<String>(),
17337 "bbbb"
17338 );
17339 editor.change_selections(None, window, cx, |selections| {
17340 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
17341 });
17342 editor.handle_input("B", window, cx);
17343 });
17344
17345 assert_eq!(
17346 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17347 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
17348 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
17349 );
17350
17351 multi_buffer_editor.update(cx, |editor, cx| {
17352 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
17353 });
17354 assert_eq!(
17355 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17356 "\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",
17357 "After unfolding the all buffers, all original text should be displayed"
17358 );
17359}
17360
17361#[gpui::test]
17362async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
17363 init_test(cx, |_| {});
17364
17365 let sample_text_1 = "1111\n2222\n3333".to_string();
17366 let sample_text_2 = "4444\n5555\n6666".to_string();
17367 let sample_text_3 = "7777\n8888\n9999".to_string();
17368
17369 let fs = FakeFs::new(cx.executor());
17370 fs.insert_tree(
17371 path!("/a"),
17372 json!({
17373 "first.rs": sample_text_1,
17374 "second.rs": sample_text_2,
17375 "third.rs": sample_text_3,
17376 }),
17377 )
17378 .await;
17379 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17380 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17381 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17382 let worktree = project.update(cx, |project, cx| {
17383 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17384 assert_eq!(worktrees.len(), 1);
17385 worktrees.pop().unwrap()
17386 });
17387 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17388
17389 let buffer_1 = project
17390 .update(cx, |project, cx| {
17391 project.open_buffer((worktree_id, "first.rs"), cx)
17392 })
17393 .await
17394 .unwrap();
17395 let buffer_2 = project
17396 .update(cx, |project, cx| {
17397 project.open_buffer((worktree_id, "second.rs"), cx)
17398 })
17399 .await
17400 .unwrap();
17401 let buffer_3 = project
17402 .update(cx, |project, cx| {
17403 project.open_buffer((worktree_id, "third.rs"), cx)
17404 })
17405 .await
17406 .unwrap();
17407
17408 let multi_buffer = cx.new(|cx| {
17409 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17410 multi_buffer.push_excerpts(
17411 buffer_1.clone(),
17412 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
17413 cx,
17414 );
17415 multi_buffer.push_excerpts(
17416 buffer_2.clone(),
17417 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
17418 cx,
17419 );
17420 multi_buffer.push_excerpts(
17421 buffer_3.clone(),
17422 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
17423 cx,
17424 );
17425 multi_buffer
17426 });
17427
17428 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17429 Editor::new(
17430 EditorMode::full(),
17431 multi_buffer,
17432 Some(project.clone()),
17433 window,
17434 cx,
17435 )
17436 });
17437
17438 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
17439 assert_eq!(
17440 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17441 full_text,
17442 );
17443
17444 multi_buffer_editor.update(cx, |editor, cx| {
17445 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
17446 });
17447 assert_eq!(
17448 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17449 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
17450 "After folding the first buffer, its text should not be displayed"
17451 );
17452
17453 multi_buffer_editor.update(cx, |editor, cx| {
17454 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
17455 });
17456
17457 assert_eq!(
17458 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17459 "\n\n\n\n\n\n7777\n8888\n9999",
17460 "After folding the second buffer, its text should not be displayed"
17461 );
17462
17463 multi_buffer_editor.update(cx, |editor, cx| {
17464 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
17465 });
17466 assert_eq!(
17467 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17468 "\n\n\n\n\n",
17469 "After folding the third buffer, its text should not be displayed"
17470 );
17471
17472 multi_buffer_editor.update(cx, |editor, cx| {
17473 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
17474 });
17475 assert_eq!(
17476 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17477 "\n\n\n\n4444\n5555\n6666\n\n",
17478 "After unfolding the second buffer, its text should be displayed"
17479 );
17480
17481 multi_buffer_editor.update(cx, |editor, cx| {
17482 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
17483 });
17484 assert_eq!(
17485 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17486 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
17487 "After unfolding the first buffer, its text should be displayed"
17488 );
17489
17490 multi_buffer_editor.update(cx, |editor, cx| {
17491 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
17492 });
17493 assert_eq!(
17494 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17495 full_text,
17496 "After unfolding all buffers, all original text should be displayed"
17497 );
17498}
17499
17500#[gpui::test]
17501async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
17502 init_test(cx, |_| {});
17503
17504 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
17505
17506 let fs = FakeFs::new(cx.executor());
17507 fs.insert_tree(
17508 path!("/a"),
17509 json!({
17510 "main.rs": sample_text,
17511 }),
17512 )
17513 .await;
17514 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17515 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17516 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17517 let worktree = project.update(cx, |project, cx| {
17518 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17519 assert_eq!(worktrees.len(), 1);
17520 worktrees.pop().unwrap()
17521 });
17522 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17523
17524 let buffer_1 = project
17525 .update(cx, |project, cx| {
17526 project.open_buffer((worktree_id, "main.rs"), cx)
17527 })
17528 .await
17529 .unwrap();
17530
17531 let multi_buffer = cx.new(|cx| {
17532 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17533 multi_buffer.push_excerpts(
17534 buffer_1.clone(),
17535 [ExcerptRange::new(
17536 Point::new(0, 0)
17537 ..Point::new(
17538 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
17539 0,
17540 ),
17541 )],
17542 cx,
17543 );
17544 multi_buffer
17545 });
17546 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17547 Editor::new(
17548 EditorMode::full(),
17549 multi_buffer,
17550 Some(project.clone()),
17551 window,
17552 cx,
17553 )
17554 });
17555
17556 let selection_range = Point::new(1, 0)..Point::new(2, 0);
17557 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17558 enum TestHighlight {}
17559 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
17560 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
17561 editor.highlight_text::<TestHighlight>(
17562 vec![highlight_range.clone()],
17563 HighlightStyle::color(Hsla::green()),
17564 cx,
17565 );
17566 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
17567 });
17568
17569 let full_text = format!("\n\n{sample_text}");
17570 assert_eq!(
17571 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17572 full_text,
17573 );
17574}
17575
17576#[gpui::test]
17577async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
17578 init_test(cx, |_| {});
17579 cx.update(|cx| {
17580 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
17581 "keymaps/default-linux.json",
17582 cx,
17583 )
17584 .unwrap();
17585 cx.bind_keys(default_key_bindings);
17586 });
17587
17588 let (editor, cx) = cx.add_window_view(|window, cx| {
17589 let multi_buffer = MultiBuffer::build_multi(
17590 [
17591 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
17592 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
17593 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
17594 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
17595 ],
17596 cx,
17597 );
17598 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
17599
17600 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
17601 // fold all but the second buffer, so that we test navigating between two
17602 // adjacent folded buffers, as well as folded buffers at the start and
17603 // end the multibuffer
17604 editor.fold_buffer(buffer_ids[0], cx);
17605 editor.fold_buffer(buffer_ids[2], cx);
17606 editor.fold_buffer(buffer_ids[3], cx);
17607
17608 editor
17609 });
17610 cx.simulate_resize(size(px(1000.), px(1000.)));
17611
17612 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
17613 cx.assert_excerpts_with_selections(indoc! {"
17614 [EXCERPT]
17615 ˇ[FOLDED]
17616 [EXCERPT]
17617 a1
17618 b1
17619 [EXCERPT]
17620 [FOLDED]
17621 [EXCERPT]
17622 [FOLDED]
17623 "
17624 });
17625 cx.simulate_keystroke("down");
17626 cx.assert_excerpts_with_selections(indoc! {"
17627 [EXCERPT]
17628 [FOLDED]
17629 [EXCERPT]
17630 ˇa1
17631 b1
17632 [EXCERPT]
17633 [FOLDED]
17634 [EXCERPT]
17635 [FOLDED]
17636 "
17637 });
17638 cx.simulate_keystroke("down");
17639 cx.assert_excerpts_with_selections(indoc! {"
17640 [EXCERPT]
17641 [FOLDED]
17642 [EXCERPT]
17643 a1
17644 ˇb1
17645 [EXCERPT]
17646 [FOLDED]
17647 [EXCERPT]
17648 [FOLDED]
17649 "
17650 });
17651 cx.simulate_keystroke("down");
17652 cx.assert_excerpts_with_selections(indoc! {"
17653 [EXCERPT]
17654 [FOLDED]
17655 [EXCERPT]
17656 a1
17657 b1
17658 ˇ[EXCERPT]
17659 [FOLDED]
17660 [EXCERPT]
17661 [FOLDED]
17662 "
17663 });
17664 cx.simulate_keystroke("down");
17665 cx.assert_excerpts_with_selections(indoc! {"
17666 [EXCERPT]
17667 [FOLDED]
17668 [EXCERPT]
17669 a1
17670 b1
17671 [EXCERPT]
17672 ˇ[FOLDED]
17673 [EXCERPT]
17674 [FOLDED]
17675 "
17676 });
17677 for _ in 0..5 {
17678 cx.simulate_keystroke("down");
17679 cx.assert_excerpts_with_selections(indoc! {"
17680 [EXCERPT]
17681 [FOLDED]
17682 [EXCERPT]
17683 a1
17684 b1
17685 [EXCERPT]
17686 [FOLDED]
17687 [EXCERPT]
17688 ˇ[FOLDED]
17689 "
17690 });
17691 }
17692
17693 cx.simulate_keystroke("up");
17694 cx.assert_excerpts_with_selections(indoc! {"
17695 [EXCERPT]
17696 [FOLDED]
17697 [EXCERPT]
17698 a1
17699 b1
17700 [EXCERPT]
17701 ˇ[FOLDED]
17702 [EXCERPT]
17703 [FOLDED]
17704 "
17705 });
17706 cx.simulate_keystroke("up");
17707 cx.assert_excerpts_with_selections(indoc! {"
17708 [EXCERPT]
17709 [FOLDED]
17710 [EXCERPT]
17711 a1
17712 b1
17713 ˇ[EXCERPT]
17714 [FOLDED]
17715 [EXCERPT]
17716 [FOLDED]
17717 "
17718 });
17719 cx.simulate_keystroke("up");
17720 cx.assert_excerpts_with_selections(indoc! {"
17721 [EXCERPT]
17722 [FOLDED]
17723 [EXCERPT]
17724 a1
17725 ˇb1
17726 [EXCERPT]
17727 [FOLDED]
17728 [EXCERPT]
17729 [FOLDED]
17730 "
17731 });
17732 cx.simulate_keystroke("up");
17733 cx.assert_excerpts_with_selections(indoc! {"
17734 [EXCERPT]
17735 [FOLDED]
17736 [EXCERPT]
17737 ˇa1
17738 b1
17739 [EXCERPT]
17740 [FOLDED]
17741 [EXCERPT]
17742 [FOLDED]
17743 "
17744 });
17745 for _ in 0..5 {
17746 cx.simulate_keystroke("up");
17747 cx.assert_excerpts_with_selections(indoc! {"
17748 [EXCERPT]
17749 ˇ[FOLDED]
17750 [EXCERPT]
17751 a1
17752 b1
17753 [EXCERPT]
17754 [FOLDED]
17755 [EXCERPT]
17756 [FOLDED]
17757 "
17758 });
17759 }
17760}
17761
17762#[gpui::test]
17763async fn test_inline_completion_text(cx: &mut TestAppContext) {
17764 init_test(cx, |_| {});
17765
17766 // Simple insertion
17767 assert_highlighted_edits(
17768 "Hello, world!",
17769 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
17770 true,
17771 cx,
17772 |highlighted_edits, cx| {
17773 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
17774 assert_eq!(highlighted_edits.highlights.len(), 1);
17775 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
17776 assert_eq!(
17777 highlighted_edits.highlights[0].1.background_color,
17778 Some(cx.theme().status().created_background)
17779 );
17780 },
17781 )
17782 .await;
17783
17784 // Replacement
17785 assert_highlighted_edits(
17786 "This is a test.",
17787 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
17788 false,
17789 cx,
17790 |highlighted_edits, cx| {
17791 assert_eq!(highlighted_edits.text, "That is a test.");
17792 assert_eq!(highlighted_edits.highlights.len(), 1);
17793 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
17794 assert_eq!(
17795 highlighted_edits.highlights[0].1.background_color,
17796 Some(cx.theme().status().created_background)
17797 );
17798 },
17799 )
17800 .await;
17801
17802 // Multiple edits
17803 assert_highlighted_edits(
17804 "Hello, world!",
17805 vec![
17806 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
17807 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
17808 ],
17809 false,
17810 cx,
17811 |highlighted_edits, cx| {
17812 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
17813 assert_eq!(highlighted_edits.highlights.len(), 2);
17814 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
17815 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
17816 assert_eq!(
17817 highlighted_edits.highlights[0].1.background_color,
17818 Some(cx.theme().status().created_background)
17819 );
17820 assert_eq!(
17821 highlighted_edits.highlights[1].1.background_color,
17822 Some(cx.theme().status().created_background)
17823 );
17824 },
17825 )
17826 .await;
17827
17828 // Multiple lines with edits
17829 assert_highlighted_edits(
17830 "First line\nSecond line\nThird line\nFourth line",
17831 vec![
17832 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
17833 (
17834 Point::new(2, 0)..Point::new(2, 10),
17835 "New third line".to_string(),
17836 ),
17837 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
17838 ],
17839 false,
17840 cx,
17841 |highlighted_edits, cx| {
17842 assert_eq!(
17843 highlighted_edits.text,
17844 "Second modified\nNew third line\nFourth updated line"
17845 );
17846 assert_eq!(highlighted_edits.highlights.len(), 3);
17847 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
17848 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
17849 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
17850 for highlight in &highlighted_edits.highlights {
17851 assert_eq!(
17852 highlight.1.background_color,
17853 Some(cx.theme().status().created_background)
17854 );
17855 }
17856 },
17857 )
17858 .await;
17859}
17860
17861#[gpui::test]
17862async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
17863 init_test(cx, |_| {});
17864
17865 // Deletion
17866 assert_highlighted_edits(
17867 "Hello, world!",
17868 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
17869 true,
17870 cx,
17871 |highlighted_edits, cx| {
17872 assert_eq!(highlighted_edits.text, "Hello, world!");
17873 assert_eq!(highlighted_edits.highlights.len(), 1);
17874 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
17875 assert_eq!(
17876 highlighted_edits.highlights[0].1.background_color,
17877 Some(cx.theme().status().deleted_background)
17878 );
17879 },
17880 )
17881 .await;
17882
17883 // Insertion
17884 assert_highlighted_edits(
17885 "Hello, world!",
17886 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
17887 true,
17888 cx,
17889 |highlighted_edits, cx| {
17890 assert_eq!(highlighted_edits.highlights.len(), 1);
17891 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
17892 assert_eq!(
17893 highlighted_edits.highlights[0].1.background_color,
17894 Some(cx.theme().status().created_background)
17895 );
17896 },
17897 )
17898 .await;
17899}
17900
17901async fn assert_highlighted_edits(
17902 text: &str,
17903 edits: Vec<(Range<Point>, String)>,
17904 include_deletions: bool,
17905 cx: &mut TestAppContext,
17906 assertion_fn: impl Fn(HighlightedText, &App),
17907) {
17908 let window = cx.add_window(|window, cx| {
17909 let buffer = MultiBuffer::build_simple(text, cx);
17910 Editor::new(EditorMode::full(), buffer, None, window, cx)
17911 });
17912 let cx = &mut VisualTestContext::from_window(*window, cx);
17913
17914 let (buffer, snapshot) = window
17915 .update(cx, |editor, _window, cx| {
17916 (
17917 editor.buffer().clone(),
17918 editor.buffer().read(cx).snapshot(cx),
17919 )
17920 })
17921 .unwrap();
17922
17923 let edits = edits
17924 .into_iter()
17925 .map(|(range, edit)| {
17926 (
17927 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
17928 edit,
17929 )
17930 })
17931 .collect::<Vec<_>>();
17932
17933 let text_anchor_edits = edits
17934 .clone()
17935 .into_iter()
17936 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
17937 .collect::<Vec<_>>();
17938
17939 let edit_preview = window
17940 .update(cx, |_, _window, cx| {
17941 buffer
17942 .read(cx)
17943 .as_singleton()
17944 .unwrap()
17945 .read(cx)
17946 .preview_edits(text_anchor_edits.into(), cx)
17947 })
17948 .unwrap()
17949 .await;
17950
17951 cx.update(|_window, cx| {
17952 let highlighted_edits = inline_completion_edit_text(
17953 &snapshot.as_singleton().unwrap().2,
17954 &edits,
17955 &edit_preview,
17956 include_deletions,
17957 cx,
17958 );
17959 assertion_fn(highlighted_edits, cx)
17960 });
17961}
17962
17963#[track_caller]
17964fn assert_breakpoint(
17965 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
17966 path: &Arc<Path>,
17967 expected: Vec<(u32, Breakpoint)>,
17968) {
17969 if expected.len() == 0usize {
17970 assert!(!breakpoints.contains_key(path), "{}", path.display());
17971 } else {
17972 let mut breakpoint = breakpoints
17973 .get(path)
17974 .unwrap()
17975 .into_iter()
17976 .map(|breakpoint| {
17977 (
17978 breakpoint.row,
17979 Breakpoint {
17980 message: breakpoint.message.clone(),
17981 state: breakpoint.state,
17982 condition: breakpoint.condition.clone(),
17983 hit_condition: breakpoint.hit_condition.clone(),
17984 },
17985 )
17986 })
17987 .collect::<Vec<_>>();
17988
17989 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
17990
17991 assert_eq!(expected, breakpoint);
17992 }
17993}
17994
17995fn add_log_breakpoint_at_cursor(
17996 editor: &mut Editor,
17997 log_message: &str,
17998 window: &mut Window,
17999 cx: &mut Context<Editor>,
18000) {
18001 let (anchor, bp) = editor
18002 .breakpoints_at_cursors(window, cx)
18003 .first()
18004 .and_then(|(anchor, bp)| {
18005 if let Some(bp) = bp {
18006 Some((*anchor, bp.clone()))
18007 } else {
18008 None
18009 }
18010 })
18011 .unwrap_or_else(|| {
18012 let cursor_position: Point = editor.selections.newest(cx).head();
18013
18014 let breakpoint_position = editor
18015 .snapshot(window, cx)
18016 .display_snapshot
18017 .buffer_snapshot
18018 .anchor_before(Point::new(cursor_position.row, 0));
18019
18020 (breakpoint_position, Breakpoint::new_log(&log_message))
18021 });
18022
18023 editor.edit_breakpoint_at_anchor(
18024 anchor,
18025 bp,
18026 BreakpointEditAction::EditLogMessage(log_message.into()),
18027 cx,
18028 );
18029}
18030
18031#[gpui::test]
18032async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
18033 init_test(cx, |_| {});
18034
18035 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18036 let fs = FakeFs::new(cx.executor());
18037 fs.insert_tree(
18038 path!("/a"),
18039 json!({
18040 "main.rs": sample_text,
18041 }),
18042 )
18043 .await;
18044 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18045 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18046 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18047
18048 let fs = FakeFs::new(cx.executor());
18049 fs.insert_tree(
18050 path!("/a"),
18051 json!({
18052 "main.rs": sample_text,
18053 }),
18054 )
18055 .await;
18056 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18057 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18058 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18059 let worktree_id = workspace
18060 .update(cx, |workspace, _window, cx| {
18061 workspace.project().update(cx, |project, cx| {
18062 project.worktrees(cx).next().unwrap().read(cx).id()
18063 })
18064 })
18065 .unwrap();
18066
18067 let buffer = project
18068 .update(cx, |project, cx| {
18069 project.open_buffer((worktree_id, "main.rs"), cx)
18070 })
18071 .await
18072 .unwrap();
18073
18074 let (editor, cx) = cx.add_window_view(|window, cx| {
18075 Editor::new(
18076 EditorMode::full(),
18077 MultiBuffer::build_from_buffer(buffer, cx),
18078 Some(project.clone()),
18079 window,
18080 cx,
18081 )
18082 });
18083
18084 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18085 let abs_path = project.read_with(cx, |project, cx| {
18086 project
18087 .absolute_path(&project_path, cx)
18088 .map(|path_buf| Arc::from(path_buf.to_owned()))
18089 .unwrap()
18090 });
18091
18092 // assert we can add breakpoint on the first line
18093 editor.update_in(cx, |editor, window, cx| {
18094 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18095 editor.move_to_end(&MoveToEnd, window, cx);
18096 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18097 });
18098
18099 let breakpoints = editor.update(cx, |editor, cx| {
18100 editor
18101 .breakpoint_store()
18102 .as_ref()
18103 .unwrap()
18104 .read(cx)
18105 .all_breakpoints(cx)
18106 .clone()
18107 });
18108
18109 assert_eq!(1, breakpoints.len());
18110 assert_breakpoint(
18111 &breakpoints,
18112 &abs_path,
18113 vec![
18114 (0, Breakpoint::new_standard()),
18115 (3, Breakpoint::new_standard()),
18116 ],
18117 );
18118
18119 editor.update_in(cx, |editor, window, cx| {
18120 editor.move_to_beginning(&MoveToBeginning, window, cx);
18121 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18122 });
18123
18124 let breakpoints = editor.update(cx, |editor, cx| {
18125 editor
18126 .breakpoint_store()
18127 .as_ref()
18128 .unwrap()
18129 .read(cx)
18130 .all_breakpoints(cx)
18131 .clone()
18132 });
18133
18134 assert_eq!(1, breakpoints.len());
18135 assert_breakpoint(
18136 &breakpoints,
18137 &abs_path,
18138 vec![(3, Breakpoint::new_standard())],
18139 );
18140
18141 editor.update_in(cx, |editor, window, cx| {
18142 editor.move_to_end(&MoveToEnd, window, cx);
18143 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18144 });
18145
18146 let breakpoints = editor.update(cx, |editor, cx| {
18147 editor
18148 .breakpoint_store()
18149 .as_ref()
18150 .unwrap()
18151 .read(cx)
18152 .all_breakpoints(cx)
18153 .clone()
18154 });
18155
18156 assert_eq!(0, breakpoints.len());
18157 assert_breakpoint(&breakpoints, &abs_path, vec![]);
18158}
18159
18160#[gpui::test]
18161async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
18162 init_test(cx, |_| {});
18163
18164 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18165
18166 let fs = FakeFs::new(cx.executor());
18167 fs.insert_tree(
18168 path!("/a"),
18169 json!({
18170 "main.rs": sample_text,
18171 }),
18172 )
18173 .await;
18174 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18175 let (workspace, cx) =
18176 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
18177
18178 let worktree_id = workspace.update(cx, |workspace, cx| {
18179 workspace.project().update(cx, |project, cx| {
18180 project.worktrees(cx).next().unwrap().read(cx).id()
18181 })
18182 });
18183
18184 let buffer = project
18185 .update(cx, |project, cx| {
18186 project.open_buffer((worktree_id, "main.rs"), cx)
18187 })
18188 .await
18189 .unwrap();
18190
18191 let (editor, cx) = cx.add_window_view(|window, cx| {
18192 Editor::new(
18193 EditorMode::full(),
18194 MultiBuffer::build_from_buffer(buffer, cx),
18195 Some(project.clone()),
18196 window,
18197 cx,
18198 )
18199 });
18200
18201 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18202 let abs_path = project.read_with(cx, |project, cx| {
18203 project
18204 .absolute_path(&project_path, cx)
18205 .map(|path_buf| Arc::from(path_buf.to_owned()))
18206 .unwrap()
18207 });
18208
18209 editor.update_in(cx, |editor, window, cx| {
18210 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
18211 });
18212
18213 let breakpoints = editor.update(cx, |editor, cx| {
18214 editor
18215 .breakpoint_store()
18216 .as_ref()
18217 .unwrap()
18218 .read(cx)
18219 .all_breakpoints(cx)
18220 .clone()
18221 });
18222
18223 assert_breakpoint(
18224 &breakpoints,
18225 &abs_path,
18226 vec![(0, Breakpoint::new_log("hello world"))],
18227 );
18228
18229 // Removing a log message from a log breakpoint should remove it
18230 editor.update_in(cx, |editor, window, cx| {
18231 add_log_breakpoint_at_cursor(editor, "", window, cx);
18232 });
18233
18234 let breakpoints = editor.update(cx, |editor, cx| {
18235 editor
18236 .breakpoint_store()
18237 .as_ref()
18238 .unwrap()
18239 .read(cx)
18240 .all_breakpoints(cx)
18241 .clone()
18242 });
18243
18244 assert_breakpoint(&breakpoints, &abs_path, vec![]);
18245
18246 editor.update_in(cx, |editor, window, cx| {
18247 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18248 editor.move_to_end(&MoveToEnd, window, cx);
18249 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18250 // Not adding a log message to a standard breakpoint shouldn't remove it
18251 add_log_breakpoint_at_cursor(editor, "", window, cx);
18252 });
18253
18254 let breakpoints = editor.update(cx, |editor, cx| {
18255 editor
18256 .breakpoint_store()
18257 .as_ref()
18258 .unwrap()
18259 .read(cx)
18260 .all_breakpoints(cx)
18261 .clone()
18262 });
18263
18264 assert_breakpoint(
18265 &breakpoints,
18266 &abs_path,
18267 vec![
18268 (0, Breakpoint::new_standard()),
18269 (3, Breakpoint::new_standard()),
18270 ],
18271 );
18272
18273 editor.update_in(cx, |editor, window, cx| {
18274 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
18275 });
18276
18277 let breakpoints = editor.update(cx, |editor, cx| {
18278 editor
18279 .breakpoint_store()
18280 .as_ref()
18281 .unwrap()
18282 .read(cx)
18283 .all_breakpoints(cx)
18284 .clone()
18285 });
18286
18287 assert_breakpoint(
18288 &breakpoints,
18289 &abs_path,
18290 vec![
18291 (0, Breakpoint::new_standard()),
18292 (3, Breakpoint::new_log("hello world")),
18293 ],
18294 );
18295
18296 editor.update_in(cx, |editor, window, cx| {
18297 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
18298 });
18299
18300 let breakpoints = editor.update(cx, |editor, cx| {
18301 editor
18302 .breakpoint_store()
18303 .as_ref()
18304 .unwrap()
18305 .read(cx)
18306 .all_breakpoints(cx)
18307 .clone()
18308 });
18309
18310 assert_breakpoint(
18311 &breakpoints,
18312 &abs_path,
18313 vec![
18314 (0, Breakpoint::new_standard()),
18315 (3, Breakpoint::new_log("hello Earth!!")),
18316 ],
18317 );
18318}
18319
18320/// This also tests that Editor::breakpoint_at_cursor_head is working properly
18321/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
18322/// or when breakpoints were placed out of order. This tests for a regression too
18323#[gpui::test]
18324async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
18325 init_test(cx, |_| {});
18326
18327 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18328 let fs = FakeFs::new(cx.executor());
18329 fs.insert_tree(
18330 path!("/a"),
18331 json!({
18332 "main.rs": sample_text,
18333 }),
18334 )
18335 .await;
18336 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18337 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18338 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18339
18340 let fs = FakeFs::new(cx.executor());
18341 fs.insert_tree(
18342 path!("/a"),
18343 json!({
18344 "main.rs": sample_text,
18345 }),
18346 )
18347 .await;
18348 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18349 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18350 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18351 let worktree_id = workspace
18352 .update(cx, |workspace, _window, cx| {
18353 workspace.project().update(cx, |project, cx| {
18354 project.worktrees(cx).next().unwrap().read(cx).id()
18355 })
18356 })
18357 .unwrap();
18358
18359 let buffer = project
18360 .update(cx, |project, cx| {
18361 project.open_buffer((worktree_id, "main.rs"), cx)
18362 })
18363 .await
18364 .unwrap();
18365
18366 let (editor, cx) = cx.add_window_view(|window, cx| {
18367 Editor::new(
18368 EditorMode::full(),
18369 MultiBuffer::build_from_buffer(buffer, cx),
18370 Some(project.clone()),
18371 window,
18372 cx,
18373 )
18374 });
18375
18376 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18377 let abs_path = project.read_with(cx, |project, cx| {
18378 project
18379 .absolute_path(&project_path, cx)
18380 .map(|path_buf| Arc::from(path_buf.to_owned()))
18381 .unwrap()
18382 });
18383
18384 // assert we can add breakpoint on the first line
18385 editor.update_in(cx, |editor, window, cx| {
18386 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18387 editor.move_to_end(&MoveToEnd, window, cx);
18388 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18389 editor.move_up(&MoveUp, window, cx);
18390 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18391 });
18392
18393 let breakpoints = editor.update(cx, |editor, cx| {
18394 editor
18395 .breakpoint_store()
18396 .as_ref()
18397 .unwrap()
18398 .read(cx)
18399 .all_breakpoints(cx)
18400 .clone()
18401 });
18402
18403 assert_eq!(1, breakpoints.len());
18404 assert_breakpoint(
18405 &breakpoints,
18406 &abs_path,
18407 vec![
18408 (0, Breakpoint::new_standard()),
18409 (2, Breakpoint::new_standard()),
18410 (3, Breakpoint::new_standard()),
18411 ],
18412 );
18413
18414 editor.update_in(cx, |editor, window, cx| {
18415 editor.move_to_beginning(&MoveToBeginning, window, cx);
18416 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18417 editor.move_to_end(&MoveToEnd, window, cx);
18418 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18419 // Disabling a breakpoint that doesn't exist should do nothing
18420 editor.move_up(&MoveUp, window, cx);
18421 editor.move_up(&MoveUp, window, cx);
18422 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18423 });
18424
18425 let breakpoints = editor.update(cx, |editor, cx| {
18426 editor
18427 .breakpoint_store()
18428 .as_ref()
18429 .unwrap()
18430 .read(cx)
18431 .all_breakpoints(cx)
18432 .clone()
18433 });
18434
18435 let disable_breakpoint = {
18436 let mut bp = Breakpoint::new_standard();
18437 bp.state = BreakpointState::Disabled;
18438 bp
18439 };
18440
18441 assert_eq!(1, breakpoints.len());
18442 assert_breakpoint(
18443 &breakpoints,
18444 &abs_path,
18445 vec![
18446 (0, disable_breakpoint.clone()),
18447 (2, Breakpoint::new_standard()),
18448 (3, disable_breakpoint.clone()),
18449 ],
18450 );
18451
18452 editor.update_in(cx, |editor, window, cx| {
18453 editor.move_to_beginning(&MoveToBeginning, window, cx);
18454 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
18455 editor.move_to_end(&MoveToEnd, window, cx);
18456 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
18457 editor.move_up(&MoveUp, window, cx);
18458 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18459 });
18460
18461 let breakpoints = editor.update(cx, |editor, cx| {
18462 editor
18463 .breakpoint_store()
18464 .as_ref()
18465 .unwrap()
18466 .read(cx)
18467 .all_breakpoints(cx)
18468 .clone()
18469 });
18470
18471 assert_eq!(1, breakpoints.len());
18472 assert_breakpoint(
18473 &breakpoints,
18474 &abs_path,
18475 vec![
18476 (0, Breakpoint::new_standard()),
18477 (2, disable_breakpoint),
18478 (3, Breakpoint::new_standard()),
18479 ],
18480 );
18481}
18482
18483#[gpui::test]
18484async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
18485 init_test(cx, |_| {});
18486 let capabilities = lsp::ServerCapabilities {
18487 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
18488 prepare_provider: Some(true),
18489 work_done_progress_options: Default::default(),
18490 })),
18491 ..Default::default()
18492 };
18493 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
18494
18495 cx.set_state(indoc! {"
18496 struct Fˇoo {}
18497 "});
18498
18499 cx.update_editor(|editor, _, cx| {
18500 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
18501 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
18502 editor.highlight_background::<DocumentHighlightRead>(
18503 &[highlight_range],
18504 |c| c.editor_document_highlight_read_background,
18505 cx,
18506 );
18507 });
18508
18509 let mut prepare_rename_handler = cx
18510 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
18511 move |_, _, _| async move {
18512 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
18513 start: lsp::Position {
18514 line: 0,
18515 character: 7,
18516 },
18517 end: lsp::Position {
18518 line: 0,
18519 character: 10,
18520 },
18521 })))
18522 },
18523 );
18524 let prepare_rename_task = cx
18525 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
18526 .expect("Prepare rename was not started");
18527 prepare_rename_handler.next().await.unwrap();
18528 prepare_rename_task.await.expect("Prepare rename failed");
18529
18530 let mut rename_handler =
18531 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
18532 let edit = lsp::TextEdit {
18533 range: lsp::Range {
18534 start: lsp::Position {
18535 line: 0,
18536 character: 7,
18537 },
18538 end: lsp::Position {
18539 line: 0,
18540 character: 10,
18541 },
18542 },
18543 new_text: "FooRenamed".to_string(),
18544 };
18545 Ok(Some(lsp::WorkspaceEdit::new(
18546 // Specify the same edit twice
18547 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
18548 )))
18549 });
18550 let rename_task = cx
18551 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
18552 .expect("Confirm rename was not started");
18553 rename_handler.next().await.unwrap();
18554 rename_task.await.expect("Confirm rename failed");
18555 cx.run_until_parked();
18556
18557 // Despite two edits, only one is actually applied as those are identical
18558 cx.assert_editor_state(indoc! {"
18559 struct FooRenamedˇ {}
18560 "});
18561}
18562
18563#[gpui::test]
18564async fn test_rename_without_prepare(cx: &mut TestAppContext) {
18565 init_test(cx, |_| {});
18566 // These capabilities indicate that the server does not support prepare rename.
18567 let capabilities = lsp::ServerCapabilities {
18568 rename_provider: Some(lsp::OneOf::Left(true)),
18569 ..Default::default()
18570 };
18571 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
18572
18573 cx.set_state(indoc! {"
18574 struct Fˇoo {}
18575 "});
18576
18577 cx.update_editor(|editor, _window, cx| {
18578 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
18579 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
18580 editor.highlight_background::<DocumentHighlightRead>(
18581 &[highlight_range],
18582 |c| c.editor_document_highlight_read_background,
18583 cx,
18584 );
18585 });
18586
18587 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
18588 .expect("Prepare rename was not started")
18589 .await
18590 .expect("Prepare rename failed");
18591
18592 let mut rename_handler =
18593 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
18594 let edit = lsp::TextEdit {
18595 range: lsp::Range {
18596 start: lsp::Position {
18597 line: 0,
18598 character: 7,
18599 },
18600 end: lsp::Position {
18601 line: 0,
18602 character: 10,
18603 },
18604 },
18605 new_text: "FooRenamed".to_string(),
18606 };
18607 Ok(Some(lsp::WorkspaceEdit::new(
18608 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
18609 )))
18610 });
18611 let rename_task = cx
18612 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
18613 .expect("Confirm rename was not started");
18614 rename_handler.next().await.unwrap();
18615 rename_task.await.expect("Confirm rename failed");
18616 cx.run_until_parked();
18617
18618 // Correct range is renamed, as `surrounding_word` is used to find it.
18619 cx.assert_editor_state(indoc! {"
18620 struct FooRenamedˇ {}
18621 "});
18622}
18623
18624#[gpui::test]
18625async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
18626 init_test(cx, |_| {});
18627 let mut cx = EditorTestContext::new(cx).await;
18628
18629 let language = Arc::new(
18630 Language::new(
18631 LanguageConfig::default(),
18632 Some(tree_sitter_html::LANGUAGE.into()),
18633 )
18634 .with_brackets_query(
18635 r#"
18636 ("<" @open "/>" @close)
18637 ("</" @open ">" @close)
18638 ("<" @open ">" @close)
18639 ("\"" @open "\"" @close)
18640 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
18641 "#,
18642 )
18643 .unwrap(),
18644 );
18645 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
18646
18647 cx.set_state(indoc! {"
18648 <span>ˇ</span>
18649 "});
18650 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18651 cx.assert_editor_state(indoc! {"
18652 <span>
18653 ˇ
18654 </span>
18655 "});
18656
18657 cx.set_state(indoc! {"
18658 <span><span></span>ˇ</span>
18659 "});
18660 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18661 cx.assert_editor_state(indoc! {"
18662 <span><span></span>
18663 ˇ</span>
18664 "});
18665
18666 cx.set_state(indoc! {"
18667 <span>ˇ
18668 </span>
18669 "});
18670 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18671 cx.assert_editor_state(indoc! {"
18672 <span>
18673 ˇ
18674 </span>
18675 "});
18676}
18677
18678#[gpui::test(iterations = 10)]
18679async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
18680 init_test(cx, |_| {});
18681
18682 let fs = FakeFs::new(cx.executor());
18683 fs.insert_tree(
18684 path!("/dir"),
18685 json!({
18686 "a.ts": "a",
18687 }),
18688 )
18689 .await;
18690
18691 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
18692 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18693 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18694
18695 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18696 language_registry.add(Arc::new(Language::new(
18697 LanguageConfig {
18698 name: "TypeScript".into(),
18699 matcher: LanguageMatcher {
18700 path_suffixes: vec!["ts".to_string()],
18701 ..Default::default()
18702 },
18703 ..Default::default()
18704 },
18705 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18706 )));
18707 let mut fake_language_servers = language_registry.register_fake_lsp(
18708 "TypeScript",
18709 FakeLspAdapter {
18710 capabilities: lsp::ServerCapabilities {
18711 code_lens_provider: Some(lsp::CodeLensOptions {
18712 resolve_provider: Some(true),
18713 }),
18714 execute_command_provider: Some(lsp::ExecuteCommandOptions {
18715 commands: vec!["_the/command".to_string()],
18716 ..lsp::ExecuteCommandOptions::default()
18717 }),
18718 ..lsp::ServerCapabilities::default()
18719 },
18720 ..FakeLspAdapter::default()
18721 },
18722 );
18723
18724 let (buffer, _handle) = project
18725 .update(cx, |p, cx| {
18726 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
18727 })
18728 .await
18729 .unwrap();
18730 cx.executor().run_until_parked();
18731
18732 let fake_server = fake_language_servers.next().await.unwrap();
18733
18734 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
18735 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
18736 drop(buffer_snapshot);
18737 let actions = cx
18738 .update_window(*workspace, |_, window, cx| {
18739 project.code_actions(&buffer, anchor..anchor, window, cx)
18740 })
18741 .unwrap();
18742
18743 fake_server
18744 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
18745 Ok(Some(vec![
18746 lsp::CodeLens {
18747 range: lsp::Range::default(),
18748 command: Some(lsp::Command {
18749 title: "Code lens command".to_owned(),
18750 command: "_the/command".to_owned(),
18751 arguments: None,
18752 }),
18753 data: None,
18754 },
18755 lsp::CodeLens {
18756 range: lsp::Range::default(),
18757 command: Some(lsp::Command {
18758 title: "Command not in capabilities".to_owned(),
18759 command: "not in capabilities".to_owned(),
18760 arguments: None,
18761 }),
18762 data: None,
18763 },
18764 lsp::CodeLens {
18765 range: lsp::Range {
18766 start: lsp::Position {
18767 line: 1,
18768 character: 1,
18769 },
18770 end: lsp::Position {
18771 line: 1,
18772 character: 1,
18773 },
18774 },
18775 command: Some(lsp::Command {
18776 title: "Command not in range".to_owned(),
18777 command: "_the/command".to_owned(),
18778 arguments: None,
18779 }),
18780 data: None,
18781 },
18782 ]))
18783 })
18784 .next()
18785 .await;
18786
18787 let actions = actions.await.unwrap();
18788 assert_eq!(
18789 actions.len(),
18790 1,
18791 "Should have only one valid action for the 0..0 range"
18792 );
18793 let action = actions[0].clone();
18794 let apply = project.update(cx, |project, cx| {
18795 project.apply_code_action(buffer.clone(), action, true, cx)
18796 });
18797
18798 // Resolving the code action does not populate its edits. In absence of
18799 // edits, we must execute the given command.
18800 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
18801 |mut lens, _| async move {
18802 let lens_command = lens.command.as_mut().expect("should have a command");
18803 assert_eq!(lens_command.title, "Code lens command");
18804 lens_command.arguments = Some(vec![json!("the-argument")]);
18805 Ok(lens)
18806 },
18807 );
18808
18809 // While executing the command, the language server sends the editor
18810 // a `workspaceEdit` request.
18811 fake_server
18812 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
18813 let fake = fake_server.clone();
18814 move |params, _| {
18815 assert_eq!(params.command, "_the/command");
18816 let fake = fake.clone();
18817 async move {
18818 fake.server
18819 .request::<lsp::request::ApplyWorkspaceEdit>(
18820 lsp::ApplyWorkspaceEditParams {
18821 label: None,
18822 edit: lsp::WorkspaceEdit {
18823 changes: Some(
18824 [(
18825 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
18826 vec![lsp::TextEdit {
18827 range: lsp::Range::new(
18828 lsp::Position::new(0, 0),
18829 lsp::Position::new(0, 0),
18830 ),
18831 new_text: "X".into(),
18832 }],
18833 )]
18834 .into_iter()
18835 .collect(),
18836 ),
18837 ..Default::default()
18838 },
18839 },
18840 )
18841 .await
18842 .unwrap();
18843 Ok(Some(json!(null)))
18844 }
18845 }
18846 })
18847 .next()
18848 .await;
18849
18850 // Applying the code lens command returns a project transaction containing the edits
18851 // sent by the language server in its `workspaceEdit` request.
18852 let transaction = apply.await.unwrap();
18853 assert!(transaction.0.contains_key(&buffer));
18854 buffer.update(cx, |buffer, cx| {
18855 assert_eq!(buffer.text(), "Xa");
18856 buffer.undo(cx);
18857 assert_eq!(buffer.text(), "a");
18858 });
18859}
18860
18861#[gpui::test]
18862async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
18863 init_test(cx, |_| {});
18864
18865 let fs = FakeFs::new(cx.executor());
18866 let main_text = r#"fn main() {
18867println!("1");
18868println!("2");
18869println!("3");
18870println!("4");
18871println!("5");
18872}"#;
18873 let lib_text = "mod foo {}";
18874 fs.insert_tree(
18875 path!("/a"),
18876 json!({
18877 "lib.rs": lib_text,
18878 "main.rs": main_text,
18879 }),
18880 )
18881 .await;
18882
18883 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18884 let (workspace, cx) =
18885 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
18886 let worktree_id = workspace.update(cx, |workspace, cx| {
18887 workspace.project().update(cx, |project, cx| {
18888 project.worktrees(cx).next().unwrap().read(cx).id()
18889 })
18890 });
18891
18892 let expected_ranges = vec![
18893 Point::new(0, 0)..Point::new(0, 0),
18894 Point::new(1, 0)..Point::new(1, 1),
18895 Point::new(2, 0)..Point::new(2, 2),
18896 Point::new(3, 0)..Point::new(3, 3),
18897 ];
18898
18899 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
18900 let editor_1 = workspace
18901 .update_in(cx, |workspace, window, cx| {
18902 workspace.open_path(
18903 (worktree_id, "main.rs"),
18904 Some(pane_1.downgrade()),
18905 true,
18906 window,
18907 cx,
18908 )
18909 })
18910 .unwrap()
18911 .await
18912 .downcast::<Editor>()
18913 .unwrap();
18914 pane_1.update(cx, |pane, cx| {
18915 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18916 open_editor.update(cx, |editor, cx| {
18917 assert_eq!(
18918 editor.display_text(cx),
18919 main_text,
18920 "Original main.rs text on initial open",
18921 );
18922 assert_eq!(
18923 editor
18924 .selections
18925 .all::<Point>(cx)
18926 .into_iter()
18927 .map(|s| s.range())
18928 .collect::<Vec<_>>(),
18929 vec![Point::zero()..Point::zero()],
18930 "Default selections on initial open",
18931 );
18932 })
18933 });
18934 editor_1.update_in(cx, |editor, window, cx| {
18935 editor.change_selections(None, window, cx, |s| {
18936 s.select_ranges(expected_ranges.clone());
18937 });
18938 });
18939
18940 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
18941 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
18942 });
18943 let editor_2 = workspace
18944 .update_in(cx, |workspace, window, cx| {
18945 workspace.open_path(
18946 (worktree_id, "main.rs"),
18947 Some(pane_2.downgrade()),
18948 true,
18949 window,
18950 cx,
18951 )
18952 })
18953 .unwrap()
18954 .await
18955 .downcast::<Editor>()
18956 .unwrap();
18957 pane_2.update(cx, |pane, cx| {
18958 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18959 open_editor.update(cx, |editor, cx| {
18960 assert_eq!(
18961 editor.display_text(cx),
18962 main_text,
18963 "Original main.rs text on initial open in another panel",
18964 );
18965 assert_eq!(
18966 editor
18967 .selections
18968 .all::<Point>(cx)
18969 .into_iter()
18970 .map(|s| s.range())
18971 .collect::<Vec<_>>(),
18972 vec![Point::zero()..Point::zero()],
18973 "Default selections on initial open in another panel",
18974 );
18975 })
18976 });
18977
18978 editor_2.update_in(cx, |editor, window, cx| {
18979 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
18980 });
18981
18982 let _other_editor_1 = workspace
18983 .update_in(cx, |workspace, window, cx| {
18984 workspace.open_path(
18985 (worktree_id, "lib.rs"),
18986 Some(pane_1.downgrade()),
18987 true,
18988 window,
18989 cx,
18990 )
18991 })
18992 .unwrap()
18993 .await
18994 .downcast::<Editor>()
18995 .unwrap();
18996 pane_1
18997 .update_in(cx, |pane, window, cx| {
18998 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
18999 .unwrap()
19000 })
19001 .await
19002 .unwrap();
19003 drop(editor_1);
19004 pane_1.update(cx, |pane, cx| {
19005 pane.active_item()
19006 .unwrap()
19007 .downcast::<Editor>()
19008 .unwrap()
19009 .update(cx, |editor, cx| {
19010 assert_eq!(
19011 editor.display_text(cx),
19012 lib_text,
19013 "Other file should be open and active",
19014 );
19015 });
19016 assert_eq!(pane.items().count(), 1, "No other editors should be open");
19017 });
19018
19019 let _other_editor_2 = workspace
19020 .update_in(cx, |workspace, window, cx| {
19021 workspace.open_path(
19022 (worktree_id, "lib.rs"),
19023 Some(pane_2.downgrade()),
19024 true,
19025 window,
19026 cx,
19027 )
19028 })
19029 .unwrap()
19030 .await
19031 .downcast::<Editor>()
19032 .unwrap();
19033 pane_2
19034 .update_in(cx, |pane, window, cx| {
19035 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
19036 .unwrap()
19037 })
19038 .await
19039 .unwrap();
19040 drop(editor_2);
19041 pane_2.update(cx, |pane, cx| {
19042 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19043 open_editor.update(cx, |editor, cx| {
19044 assert_eq!(
19045 editor.display_text(cx),
19046 lib_text,
19047 "Other file should be open and active in another panel too",
19048 );
19049 });
19050 assert_eq!(
19051 pane.items().count(),
19052 1,
19053 "No other editors should be open in another pane",
19054 );
19055 });
19056
19057 let _editor_1_reopened = workspace
19058 .update_in(cx, |workspace, window, cx| {
19059 workspace.open_path(
19060 (worktree_id, "main.rs"),
19061 Some(pane_1.downgrade()),
19062 true,
19063 window,
19064 cx,
19065 )
19066 })
19067 .unwrap()
19068 .await
19069 .downcast::<Editor>()
19070 .unwrap();
19071 let _editor_2_reopened = workspace
19072 .update_in(cx, |workspace, window, cx| {
19073 workspace.open_path(
19074 (worktree_id, "main.rs"),
19075 Some(pane_2.downgrade()),
19076 true,
19077 window,
19078 cx,
19079 )
19080 })
19081 .unwrap()
19082 .await
19083 .downcast::<Editor>()
19084 .unwrap();
19085 pane_1.update(cx, |pane, cx| {
19086 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19087 open_editor.update(cx, |editor, cx| {
19088 assert_eq!(
19089 editor.display_text(cx),
19090 main_text,
19091 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
19092 );
19093 assert_eq!(
19094 editor
19095 .selections
19096 .all::<Point>(cx)
19097 .into_iter()
19098 .map(|s| s.range())
19099 .collect::<Vec<_>>(),
19100 expected_ranges,
19101 "Previous editor in the 1st panel had selections and should get them restored on reopen",
19102 );
19103 })
19104 });
19105 pane_2.update(cx, |pane, cx| {
19106 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19107 open_editor.update(cx, |editor, cx| {
19108 assert_eq!(
19109 editor.display_text(cx),
19110 r#"fn main() {
19111⋯rintln!("1");
19112⋯intln!("2");
19113⋯ntln!("3");
19114println!("4");
19115println!("5");
19116}"#,
19117 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
19118 );
19119 assert_eq!(
19120 editor
19121 .selections
19122 .all::<Point>(cx)
19123 .into_iter()
19124 .map(|s| s.range())
19125 .collect::<Vec<_>>(),
19126 vec![Point::zero()..Point::zero()],
19127 "Previous editor in the 2nd pane had no selections changed hence should restore none",
19128 );
19129 })
19130 });
19131}
19132
19133#[gpui::test]
19134async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
19135 init_test(cx, |_| {});
19136
19137 let fs = FakeFs::new(cx.executor());
19138 let main_text = r#"fn main() {
19139println!("1");
19140println!("2");
19141println!("3");
19142println!("4");
19143println!("5");
19144}"#;
19145 let lib_text = "mod foo {}";
19146 fs.insert_tree(
19147 path!("/a"),
19148 json!({
19149 "lib.rs": lib_text,
19150 "main.rs": main_text,
19151 }),
19152 )
19153 .await;
19154
19155 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19156 let (workspace, cx) =
19157 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19158 let worktree_id = workspace.update(cx, |workspace, cx| {
19159 workspace.project().update(cx, |project, cx| {
19160 project.worktrees(cx).next().unwrap().read(cx).id()
19161 })
19162 });
19163
19164 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
19165 let editor = workspace
19166 .update_in(cx, |workspace, window, cx| {
19167 workspace.open_path(
19168 (worktree_id, "main.rs"),
19169 Some(pane.downgrade()),
19170 true,
19171 window,
19172 cx,
19173 )
19174 })
19175 .unwrap()
19176 .await
19177 .downcast::<Editor>()
19178 .unwrap();
19179 pane.update(cx, |pane, cx| {
19180 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19181 open_editor.update(cx, |editor, cx| {
19182 assert_eq!(
19183 editor.display_text(cx),
19184 main_text,
19185 "Original main.rs text on initial open",
19186 );
19187 })
19188 });
19189 editor.update_in(cx, |editor, window, cx| {
19190 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
19191 });
19192
19193 cx.update_global(|store: &mut SettingsStore, cx| {
19194 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
19195 s.restore_on_file_reopen = Some(false);
19196 });
19197 });
19198 editor.update_in(cx, |editor, window, cx| {
19199 editor.fold_ranges(
19200 vec![
19201 Point::new(1, 0)..Point::new(1, 1),
19202 Point::new(2, 0)..Point::new(2, 2),
19203 Point::new(3, 0)..Point::new(3, 3),
19204 ],
19205 false,
19206 window,
19207 cx,
19208 );
19209 });
19210 pane.update_in(cx, |pane, window, cx| {
19211 pane.close_all_items(&CloseAllItems::default(), window, cx)
19212 .unwrap()
19213 })
19214 .await
19215 .unwrap();
19216 pane.update(cx, |pane, _| {
19217 assert!(pane.active_item().is_none());
19218 });
19219 cx.update_global(|store: &mut SettingsStore, cx| {
19220 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
19221 s.restore_on_file_reopen = Some(true);
19222 });
19223 });
19224
19225 let _editor_reopened = workspace
19226 .update_in(cx, |workspace, window, cx| {
19227 workspace.open_path(
19228 (worktree_id, "main.rs"),
19229 Some(pane.downgrade()),
19230 true,
19231 window,
19232 cx,
19233 )
19234 })
19235 .unwrap()
19236 .await
19237 .downcast::<Editor>()
19238 .unwrap();
19239 pane.update(cx, |pane, cx| {
19240 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19241 open_editor.update(cx, |editor, cx| {
19242 assert_eq!(
19243 editor.display_text(cx),
19244 main_text,
19245 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
19246 );
19247 })
19248 });
19249}
19250
19251fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
19252 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
19253 point..point
19254}
19255
19256fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
19257 let (text, ranges) = marked_text_ranges(marked_text, true);
19258 assert_eq!(editor.text(cx), text);
19259 assert_eq!(
19260 editor.selections.ranges(cx),
19261 ranges,
19262 "Assert selections are {}",
19263 marked_text
19264 );
19265}
19266
19267pub fn handle_signature_help_request(
19268 cx: &mut EditorLspTestContext,
19269 mocked_response: lsp::SignatureHelp,
19270) -> impl Future<Output = ()> + use<> {
19271 let mut request =
19272 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
19273 let mocked_response = mocked_response.clone();
19274 async move { Ok(Some(mocked_response)) }
19275 });
19276
19277 async move {
19278 request.next().await;
19279 }
19280}
19281
19282/// Handle completion request passing a marked string specifying where the completion
19283/// should be triggered from using '|' character, what range should be replaced, and what completions
19284/// should be returned using '<' and '>' to delimit the range.
19285///
19286/// Also see `handle_completion_request_with_insert_and_replace`.
19287#[track_caller]
19288pub fn handle_completion_request(
19289 cx: &mut EditorLspTestContext,
19290 marked_string: &str,
19291 completions: Vec<&'static str>,
19292 counter: Arc<AtomicUsize>,
19293) -> impl Future<Output = ()> {
19294 let complete_from_marker: TextRangeMarker = '|'.into();
19295 let replace_range_marker: TextRangeMarker = ('<', '>').into();
19296 let (_, mut marked_ranges) = marked_text_ranges_by(
19297 marked_string,
19298 vec![complete_from_marker.clone(), replace_range_marker.clone()],
19299 );
19300
19301 let complete_from_position =
19302 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
19303 let replace_range =
19304 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
19305
19306 let mut request =
19307 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
19308 let completions = completions.clone();
19309 counter.fetch_add(1, atomic::Ordering::Release);
19310 async move {
19311 assert_eq!(params.text_document_position.text_document.uri, url.clone());
19312 assert_eq!(
19313 params.text_document_position.position,
19314 complete_from_position
19315 );
19316 Ok(Some(lsp::CompletionResponse::Array(
19317 completions
19318 .iter()
19319 .map(|completion_text| lsp::CompletionItem {
19320 label: completion_text.to_string(),
19321 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
19322 range: replace_range,
19323 new_text: completion_text.to_string(),
19324 })),
19325 ..Default::default()
19326 })
19327 .collect(),
19328 )))
19329 }
19330 });
19331
19332 async move {
19333 request.next().await;
19334 }
19335}
19336
19337/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
19338/// given instead, which also contains an `insert` range.
19339///
19340/// This function uses the cursor position to mimic what Rust-Analyzer provides as the `insert` range,
19341/// that is, `replace_range.start..cursor_pos`.
19342pub fn handle_completion_request_with_insert_and_replace(
19343 cx: &mut EditorLspTestContext,
19344 marked_string: &str,
19345 completions: Vec<&'static str>,
19346 counter: Arc<AtomicUsize>,
19347) -> impl Future<Output = ()> {
19348 let complete_from_marker: TextRangeMarker = '|'.into();
19349 let replace_range_marker: TextRangeMarker = ('<', '>').into();
19350 let (_, mut marked_ranges) = marked_text_ranges_by(
19351 marked_string,
19352 vec![complete_from_marker.clone(), replace_range_marker.clone()],
19353 );
19354
19355 let complete_from_position =
19356 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
19357 let replace_range =
19358 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
19359
19360 let mut request =
19361 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
19362 let completions = completions.clone();
19363 counter.fetch_add(1, atomic::Ordering::Release);
19364 async move {
19365 assert_eq!(params.text_document_position.text_document.uri, url.clone());
19366 assert_eq!(
19367 params.text_document_position.position, complete_from_position,
19368 "marker `|` position doesn't match",
19369 );
19370 Ok(Some(lsp::CompletionResponse::Array(
19371 completions
19372 .iter()
19373 .map(|completion_text| lsp::CompletionItem {
19374 label: completion_text.to_string(),
19375 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
19376 lsp::InsertReplaceEdit {
19377 insert: lsp::Range {
19378 start: replace_range.start,
19379 end: complete_from_position,
19380 },
19381 replace: replace_range,
19382 new_text: completion_text.to_string(),
19383 },
19384 )),
19385 ..Default::default()
19386 })
19387 .collect(),
19388 )))
19389 }
19390 });
19391
19392 async move {
19393 request.next().await;
19394 }
19395}
19396
19397fn handle_resolve_completion_request(
19398 cx: &mut EditorLspTestContext,
19399 edits: Option<Vec<(&'static str, &'static str)>>,
19400) -> impl Future<Output = ()> {
19401 let edits = edits.map(|edits| {
19402 edits
19403 .iter()
19404 .map(|(marked_string, new_text)| {
19405 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
19406 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
19407 lsp::TextEdit::new(replace_range, new_text.to_string())
19408 })
19409 .collect::<Vec<_>>()
19410 });
19411
19412 let mut request =
19413 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
19414 let edits = edits.clone();
19415 async move {
19416 Ok(lsp::CompletionItem {
19417 additional_text_edits: edits,
19418 ..Default::default()
19419 })
19420 }
19421 });
19422
19423 async move {
19424 request.next().await;
19425 }
19426}
19427
19428pub(crate) fn update_test_language_settings(
19429 cx: &mut TestAppContext,
19430 f: impl Fn(&mut AllLanguageSettingsContent),
19431) {
19432 cx.update(|cx| {
19433 SettingsStore::update_global(cx, |store, cx| {
19434 store.update_user_settings::<AllLanguageSettings>(cx, f);
19435 });
19436 });
19437}
19438
19439pub(crate) fn update_test_project_settings(
19440 cx: &mut TestAppContext,
19441 f: impl Fn(&mut ProjectSettings),
19442) {
19443 cx.update(|cx| {
19444 SettingsStore::update_global(cx, |store, cx| {
19445 store.update_user_settings::<ProjectSettings>(cx, f);
19446 });
19447 });
19448}
19449
19450pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
19451 cx.update(|cx| {
19452 assets::Assets.load_test_fonts(cx);
19453 let store = SettingsStore::test(cx);
19454 cx.set_global(store);
19455 theme::init(theme::LoadThemes::JustBase, cx);
19456 release_channel::init(SemanticVersion::default(), cx);
19457 client::init_settings(cx);
19458 language::init(cx);
19459 Project::init_settings(cx);
19460 workspace::init_settings(cx);
19461 crate::init(cx);
19462 });
19463
19464 update_test_language_settings(cx, f);
19465}
19466
19467#[track_caller]
19468fn assert_hunk_revert(
19469 not_reverted_text_with_selections: &str,
19470 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
19471 expected_reverted_text_with_selections: &str,
19472 base_text: &str,
19473 cx: &mut EditorLspTestContext,
19474) {
19475 cx.set_state(not_reverted_text_with_selections);
19476 cx.set_head_text(base_text);
19477 cx.executor().run_until_parked();
19478
19479 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
19480 let snapshot = editor.snapshot(window, cx);
19481 let reverted_hunk_statuses = snapshot
19482 .buffer_snapshot
19483 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
19484 .map(|hunk| hunk.status().kind)
19485 .collect::<Vec<_>>();
19486
19487 editor.git_restore(&Default::default(), window, cx);
19488 reverted_hunk_statuses
19489 });
19490 cx.executor().run_until_parked();
19491 cx.assert_editor_state(expected_reverted_text_with_selections);
19492 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
19493}